= ({
8 | blockId,
9 | children,
10 | }) => ({
11 | type: 'actions',
12 | block_id: blockId,
13 | elements: [].concat(children),
14 | });
15 |
--------------------------------------------------------------------------------
/src/components/AnyText.ts:
--------------------------------------------------------------------------------
1 | import { PlainText } from './PlainText';
2 | import { MarkdownText } from './MarkdownText';
3 |
4 | export type AnyText = typeof PlainText | typeof MarkdownText;
5 |
--------------------------------------------------------------------------------
/src/components/Block.ts:
--------------------------------------------------------------------------------
1 | import { FC } from '..';
2 | import { KnownBlock } from '@slack/types';
3 | import { HeaderBlockSpec } from './HeaderBlock';
4 |
5 | export interface BlockProps {
6 | blockId?: string;
7 | }
8 |
9 | // slack types don't yet include HeaderBlock
10 | export type AllowedBlocks = KnownBlock | HeaderBlockSpec;
11 |
12 | export type Block = FC
;
13 |
--------------------------------------------------------------------------------
/src/components/BlockElement.ts:
--------------------------------------------------------------------------------
1 | import { FC } from '..';
2 | import {
3 | ImageElement as ImageElementSpec,
4 | Button as ButtonSpec,
5 | Button,
6 | Overflow,
7 | Datepicker,
8 | Select,
9 | MultiSelect,
10 | Action,
11 | } from '@slack/types';
12 |
13 | export type ActionType = 'button';
14 |
15 | export type ActionSpec = ButtonSpec;
16 |
17 | export type ElementType = 'image' | 'user' | ActionType;
18 |
19 | export type ElementSpec =
20 | | ImageElementSpec
21 | // | ActionSpec
22 | | Button
23 | | Overflow
24 | | Datepicker
25 | | Select
26 | | MultiSelect
27 | | Action;
28 |
29 | export type BlockElement
= FC
;
30 |
--------------------------------------------------------------------------------
/src/components/BlockQuote.ts:
--------------------------------------------------------------------------------
1 | import { ContainerProps } from './ContainerProps';
2 | import { MarkdownTextProps, MarkdownText } from './MarkdownText';
3 | import { joinTextChildren } from './Text';
4 |
5 | export type BlockQuoteProps = MarkdownTextProps & ContainerProps;
6 |
7 | const applyMarkdownQuotation = (message: string): string => {
8 | return '>' + message.replace(/\n/g, '\n>');
9 | };
10 |
11 | export const BlockQuote: typeof MarkdownText = (props: BlockQuoteProps) => {
12 | return {
13 | type: 'mrkdwn',
14 | text: applyMarkdownQuotation(joinTextChildren(props.children)),
15 | verbatim: props.verbatim,
16 | };
17 | };
18 |
19 | export default BlockQuote;
20 |
--------------------------------------------------------------------------------
/src/components/ButtonElement.ts:
--------------------------------------------------------------------------------
1 | import { FC } from '..';
2 | import { Button as ButtonSpec } from '@slack/types';
3 | import { ContainerProps } from './ContainerProps';
4 | import { joinTextChildren } from './Text';
5 |
6 | export interface ButtonElementProps extends ContainerProps {
7 | actionId: string;
8 | url?: string;
9 | value?: string;
10 | style?: 'primary' | 'danger';
11 | }
12 |
13 | export const ButtonElement: FC = ({
14 | children,
15 | actionId: action_id,
16 | style,
17 | url,
18 | value,
19 | }) => ({
20 | type: 'button',
21 | text: {
22 | // plain_text allows only plain_text
23 | type: 'plain_text',
24 | emoji: true,
25 | text: joinTextChildren(children),
26 | },
27 | action_id,
28 | url,
29 | value,
30 | style,
31 | });
32 |
--------------------------------------------------------------------------------
/src/components/ChannelsSelect.ts:
--------------------------------------------------------------------------------
1 | import { joinTextChildren } from './Text';
2 | import { ChannelsSelect as ChannelSelectSpec } from '@slack/types';
3 | import { ContainerProps } from './ContainerProps';
4 | import { FC } from '..';
5 |
6 | export interface ChannelSelectProps extends ContainerProps {
7 | initialChannel?: string;
8 | actionId?: string;
9 | }
10 |
11 | export const ChannelSelect: FC = ({
12 | children,
13 | initialChannel,
14 | actionId,
15 | }) => {
16 | const select: ChannelSelectSpec = {
17 | type: 'channels_select',
18 | initial_channel: initialChannel,
19 | action_id: actionId,
20 | };
21 |
22 | if (children) {
23 | select.placeholder = {
24 | type: 'plain_text',
25 | text: joinTextChildren(children),
26 | };
27 | }
28 |
29 | return select;
30 | };
31 |
--------------------------------------------------------------------------------
/src/components/Checkbox.ts:
--------------------------------------------------------------------------------
1 | import { Checkboxes } from '@slack/types';
2 | import { FC } from '..';
3 | import { buildInputOptions, InputOption } from './shared/inputOption';
4 |
5 | export interface CheckboxElementProps {
6 | initialOptions?: InputOption[];
7 | actionId: string;
8 | confirm?: boolean;
9 | options: InputOption[];
10 | }
11 |
12 | export const CheckboxElement: FC = ({
13 | initialOptions,
14 | actionId,
15 | options,
16 | }) => ({
17 | type: 'checkboxes',
18 | action_id: actionId,
19 | options: buildInputOptions(options),
20 | ...(initialOptions && {
21 | initial_options: buildInputOptions(initialOptions),
22 | }),
23 | });
24 |
--------------------------------------------------------------------------------
/src/components/Childless.ts:
--------------------------------------------------------------------------------
1 | export interface Childless {
2 | children?: never;
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/ContainerProps.ts:
--------------------------------------------------------------------------------
1 | export interface ContainerProps {
2 | children?: C | C[];
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/ContextBlock.ts:
--------------------------------------------------------------------------------
1 | import { Block, BlockProps } from './Block';
2 | import { ContextBlock as ContextBlockSpec } from '@slack/types';
3 | import { ContainerProps } from './ContainerProps';
4 | import { AnyText } from './AnyText';
5 | import { ImageElement } from './ImageElement';
6 |
7 | export type ContextBlockProps = BlockProps &
8 | ContainerProps | ReturnType>;
9 |
10 | export const ContextBlock: Block = ({
11 | children,
12 | blockId,
13 | }) => ({
14 | type: 'context',
15 | elements: [].concat(children),
16 | block_id: blockId,
17 | });
18 |
--------------------------------------------------------------------------------
/src/components/ConversationsSelect.ts:
--------------------------------------------------------------------------------
1 | import { joinTextChildren } from './Text';
2 | import { ConversationsSelect as ConversationsSelectSpec } from '@slack/types';
3 | import { ContainerProps } from './ContainerProps';
4 | import { FC } from '..';
5 |
6 | export interface ConversationSelectProps extends ContainerProps {
7 | initialConversation?: string;
8 | actionId: string;
9 | filter?: {
10 | include?: ('im' | 'mpim' | 'private' | 'public')[];
11 | excludeExternalSharedChannels?: boolean;
12 | excludeBotUsers?: boolean;
13 | };
14 | exclude_external_shared_channels?: boolean;
15 | exclude_bot_users?: boolean;
16 | responseUrlEnabled?: boolean;
17 | }
18 |
19 | export const ConversationsSelect: FC<
20 | ConversationSelectProps,
21 | ConversationsSelectSpec
22 | > = ({
23 | children,
24 | initialConversation,
25 | actionId,
26 | filter,
27 | responseUrlEnabled = false,
28 | }) => {
29 | const select: ConversationsSelectSpec = {
30 | type: 'conversations_select',
31 | initial_conversation: initialConversation,
32 | action_id: actionId,
33 | response_url_enabled: responseUrlEnabled,
34 | ...(filter && {
35 | filter: {
36 | include: filter.include,
37 | exclude_bot_users: filter.excludeBotUsers,
38 | exclude_external_shared_channels: filter.excludeExternalSharedChannels,
39 | },
40 | }),
41 | };
42 |
43 | if (children) {
44 | select.placeholder = {
45 | type: 'plain_text',
46 | text: joinTextChildren(children),
47 | };
48 | }
49 |
50 | return select;
51 | };
52 |
--------------------------------------------------------------------------------
/src/components/DividerBlock.ts:
--------------------------------------------------------------------------------
1 | import { Block, BlockProps } from './Block';
2 | import { DividerBlock as DividerBlockSpec } from '@slack/types';
3 | import { Childless } from './Childless';
4 |
5 | export type DividerBlockProps = BlockProps & Childless;
6 |
7 | export const DividerBlock: Block = ({
8 | blockId,
9 | }) => ({
10 | type: 'divider',
11 | block_id: blockId,
12 | });
13 |
--------------------------------------------------------------------------------
/src/components/ExternalSelect.ts:
--------------------------------------------------------------------------------
1 | import { MultiExternalSelect } from '@slack/types';
2 | import { FC, SelectElementProps } from '..';
3 | import { buildInputOptions, InputOption } from './shared/inputOption';
4 |
5 | export interface MultiExternalSelectElementProps extends SelectElementProps {
6 | initialOptions?: InputOption[];
7 | }
8 |
9 | export const MultiExternalSelectElement: FC<
10 | MultiExternalSelectElementProps,
11 | MultiExternalSelect
12 | > = ({ placeholder, actionId, initialOptions }) => ({
13 | type: 'multi_external_select',
14 | placeholder,
15 | action_id: actionId,
16 | min_query_length: 0,
17 | initial_options: buildInputOptions(initialOptions),
18 | });
19 |
--------------------------------------------------------------------------------
/src/components/HeaderBlock.ts:
--------------------------------------------------------------------------------
1 | import { Block, BlockProps } from './Block';
2 | import { ContainerProps } from './ContainerProps';
3 | import { PlainText } from './PlainText';
4 |
5 | export interface HeaderBlockProps
6 | extends BlockProps,
7 | ContainerProps {}
8 |
9 | export interface HeaderBlockSpec {
10 | type: 'header';
11 | text: ReturnType;
12 | }
13 |
14 | export const HeaderBlock: Block = ({
15 | children,
16 | blockId,
17 | }) => ({
18 | type: 'header',
19 | block_id: blockId,
20 | text: [].concat(children)[0],
21 | });
22 |
--------------------------------------------------------------------------------
/src/components/Home.ts:
--------------------------------------------------------------------------------
1 | // https://api.slack.com/surfaces/tabs/using
2 |
3 | import { FC, ModalProps, ModalSpec } from '..';
4 |
5 | export const Home: FC = ({
6 | children,
7 | callbackId,
8 | title,
9 | submitButtonText,
10 | closeButtonText,
11 | privateMetadata,
12 | clearOnClose,
13 | notifyOnClose,
14 | }) => {
15 | const modal: ModalSpec = {
16 | type: 'home',
17 | callback_id: callbackId,
18 | blocks: Array.isArray(children) ? children : [].concat(children),
19 | title: { type: 'plain_text', text: title },
20 | private_metadata: privateMetadata,
21 | clear_on_close: clearOnClose,
22 | notify_on_close: notifyOnClose,
23 | };
24 |
25 | if (submitButtonText) {
26 | modal.submit = { type: 'plain_text', text: submitButtonText };
27 | }
28 | if (closeButtonText) {
29 | modal.close = { type: 'plain_text', text: closeButtonText };
30 | }
31 |
32 | return modal;
33 | };
34 |
--------------------------------------------------------------------------------
/src/components/ImageBlock.ts:
--------------------------------------------------------------------------------
1 | import { Block, BlockProps } from './Block';
2 | import { ImageBlock as ImageBlockSpec } from '@slack/types';
3 | import { Childless } from './Childless';
4 |
5 | export interface ImageBlockProps extends BlockProps, Childless {
6 | altText: string;
7 | imageUrl: string;
8 | title?: string;
9 | }
10 |
11 | export const ImageBlock: Block = ({
12 | blockId,
13 | imageUrl,
14 | altText,
15 | title,
16 | }) => ({
17 | type: 'image',
18 | block_id: blockId,
19 | image_url: imageUrl,
20 | alt_text: altText,
21 | title: {
22 | type: 'plain_text',
23 | text: title,
24 | emoji: true,
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/src/components/ImageElement.ts:
--------------------------------------------------------------------------------
1 | import { FC } from '..';
2 | import { ImageElement as ImageElementSpec } from '@slack/types';
3 |
4 | export interface ImageElementProps {
5 | imageUrl: string;
6 | altText: string;
7 | }
8 |
9 | export const ImageElement: FC = ({
10 | imageUrl: image_url,
11 | altText: alt_text,
12 | }) => ({
13 | type: 'image',
14 | image_url,
15 | alt_text,
16 | });
17 |
--------------------------------------------------------------------------------
/src/components/InputBlock.ts:
--------------------------------------------------------------------------------
1 | import { Block, BlockProps } from './Block';
2 | import { InputBlock as InputBlockSpec } from '@slack/types';
3 | import { ContainerProps } from './ContainerProps';
4 |
5 | export interface InputBlockProps extends BlockProps, ContainerProps {
6 | label: string;
7 | hint?: string;
8 | optional?: boolean;
9 | }
10 |
11 | export const InputBlock: Block = ({
12 | label,
13 | hint,
14 | blockId,
15 | children,
16 | optional,
17 | }) => {
18 | const spec: InputBlockSpec = {
19 | type: 'input',
20 | label: { type: 'plain_text', text: label, emoji: true },
21 | block_id: blockId,
22 | element: children[0],
23 | optional,
24 | };
25 |
26 | if (hint) {
27 | spec.hint = { type: 'plain_text', text: hint, emoji: true };
28 | }
29 |
30 | return spec;
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/LineBreak.ts:
--------------------------------------------------------------------------------
1 | import { Span } from './Span';
2 | import { Childless } from './Childless';
3 |
4 | export const LineBreak: Span = () => {
5 | return '\n';
6 | };
7 |
--------------------------------------------------------------------------------
/src/components/Link.ts:
--------------------------------------------------------------------------------
1 | import { Span } from './Span';
2 | import { ContainerProps } from './ContainerProps';
3 | import { joinTextChildren } from './Text';
4 |
5 | // https://api.slack.com/reference/surfaces/formatting#escaping
6 | const escape = (s: string) =>
7 | s.replace('&', '&').replace('<', '<').replace('>', '>');
8 |
9 | export interface LinkProps extends ContainerProps {
10 | href: string;
11 | }
12 |
13 | export const Link: Span = (props: LinkProps) => {
14 | return `<${props.href}|${escape(joinTextChildren(props.children))}>`;
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/MarkdownText.ts:
--------------------------------------------------------------------------------
1 | import { TextProps, Text, joinTextChildren } from './Text';
2 | import { MrkdwnElement } from '@slack/types';
3 |
4 | export interface MarkdownTextProps extends TextProps {
5 | verbatim?: boolean;
6 | }
7 |
8 | export const MarkdownText: Text = ({
9 | children,
10 | verbatim = false,
11 | }) => ({
12 | type: 'mrkdwn',
13 | text: joinTextChildren(children),
14 | verbatim,
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/Mention.ts:
--------------------------------------------------------------------------------
1 | import { Span } from './Span';
2 |
3 | export interface MentionProps {
4 | userId: string;
5 | }
6 |
7 | export const Mention: Span = ({ userId }) => `<@${userId}>`;
8 |
--------------------------------------------------------------------------------
/src/components/Message.ts:
--------------------------------------------------------------------------------
1 | import { FC } from '..';
2 | import { ContainerProps } from './ContainerProps';
3 | import { Block } from './Block';
4 | import { AltText } from './MessageText';
5 | import { MessageTextSpec } from './Text';
6 | import { KnownBlock } from '@slack/types';
7 |
8 | export type MessageType = 'ephemeral' | 'in_channel';
9 |
10 | export interface MessageProps
11 | extends ContainerProps>> {
12 | altText: ReturnType;
13 | responseType?: MessageType;
14 | channel?: string;
15 | token?: string;
16 | asUser?: boolean;
17 | }
18 |
19 | export interface MessageSpec extends MessageTextSpec {
20 | response_type: MessageType;
21 | channel?: string;
22 | as_user?: boolean;
23 | token?: string;
24 | blocks?: ReturnType>[];
25 | }
26 |
27 | export const Message: FC = ({
28 | children,
29 | responseType = 'in_channel',
30 | channel,
31 | token,
32 | altText,
33 | asUser,
34 | }) => {
35 | const message = {
36 | response_type: responseType,
37 | blocks: Array.isArray(children) ? children : [].concat(children),
38 | as_user: asUser,
39 | channel,
40 | token,
41 | ...altText,
42 | };
43 |
44 | if (asUser) {
45 | message.as_user = asUser;
46 | }
47 | return message;
48 | };
49 |
--------------------------------------------------------------------------------
/src/components/MessageText.ts:
--------------------------------------------------------------------------------
1 | // https://api.slack.com/reference/messaging/payload
2 | import {
3 | TextProps,
4 | MessageText as Text,
5 | joinTextChildren,
6 | MessageTextSpec,
7 | } from './Text';
8 |
9 | export interface MessageTextProps extends TextProps {
10 | mrkdwn?: boolean;
11 | }
12 |
13 | export const AltText: Text = ({
14 | children,
15 | mrkdwn = true,
16 | }) => ({
17 | mrkdwn,
18 | text: joinTextChildren(children),
19 | });
20 |
--------------------------------------------------------------------------------
/src/components/Modal.ts:
--------------------------------------------------------------------------------
1 | // https://api.slack.com/reference/surfaces/views
2 |
3 | import { FC } from '..';
4 | import { ContainerProps } from './ContainerProps';
5 | import { Block } from './Block';
6 | import { KnownBlock, View } from '@slack/types';
7 |
8 | export interface ModalProps
9 | extends ContainerProps>> {
10 | title: string;
11 | callbackId?: string;
12 | submitButtonText?: string;
13 | closeButtonText?: string;
14 | privateMetadata?: string;
15 | clearOnClose?: boolean;
16 | notifyOnClose?: boolean;
17 | }
18 |
19 | // TODO: maybe just rename Modal -> View. slack docs make this a bit confusing
20 | // in the meantime exporting this spec for consistency
21 | export type ModalSpec = View;
22 |
23 | export const Modal: FC = ({
24 | children,
25 | callbackId,
26 | title,
27 | submitButtonText,
28 | closeButtonText,
29 | privateMetadata,
30 | clearOnClose,
31 | notifyOnClose,
32 | }) => {
33 | const modal: ModalSpec = {
34 | type: 'modal',
35 | callback_id: callbackId,
36 | blocks: Array.isArray(children) ? children : [].concat(children),
37 | title: { type: 'plain_text', text: title },
38 | private_metadata: privateMetadata,
39 | clear_on_close: clearOnClose,
40 | notify_on_close: notifyOnClose,
41 | };
42 |
43 | if (submitButtonText) {
44 | modal.submit = { type: 'plain_text', text: submitButtonText };
45 | }
46 | if (closeButtonText) {
47 | modal.close = { type: 'plain_text', text: closeButtonText };
48 | }
49 |
50 | return modal;
51 | };
52 |
--------------------------------------------------------------------------------
/src/components/MultiSelectElement.ts:
--------------------------------------------------------------------------------
1 | import { MultiStaticSelect } from '@slack/types';
2 | import { FC, SelectElementProps } from '..';
3 | import { buildInputOptions, InputOption } from './shared/inputOption';
4 |
5 | export interface MultiSelectElementProps extends SelectElementProps {
6 | initialOptions?: InputOption[];
7 | }
8 |
9 | export const MultiSelectElement: FC<
10 | MultiSelectElementProps,
11 | MultiStaticSelect
12 | > = ({ placeholder, actionId, options, optionGroups, initialOptions }) => ({
13 | type: 'multi_static_select',
14 | placeholder,
15 | action_id: actionId,
16 | ...(options && { options: buildInputOptions(options) }),
17 | ...(optionGroups && {
18 | option_groups: optionGroups.map(group => ({
19 | label: group.label,
20 | options: buildInputOptions(group.options),
21 | })),
22 | }),
23 | ...(initialOptions && {
24 | initial_options: buildInputOptions(initialOptions),
25 | }),
26 | });
27 |
--------------------------------------------------------------------------------
/src/components/OverflowMenuElement.ts:
--------------------------------------------------------------------------------
1 | import { StaticSelect, Overflow } from '@slack/types';
2 | import { FC } from '..';
3 | import { buildInputOptions, InputOption } from './shared/inputOption';
4 |
5 | export interface OverflowMenuElementProps {
6 | actionId: string;
7 | options: InputOption[];
8 | }
9 |
10 | export const OverflowMenuElement: FC = ({
11 | actionId,
12 | options,
13 | }) => ({
14 | type: 'overflow',
15 | action_id: actionId,
16 | ...(options && { options: buildInputOptions(options) }),
17 | });
18 |
--------------------------------------------------------------------------------
/src/components/PlainText.ts:
--------------------------------------------------------------------------------
1 | import { TextProps, Text, joinTextChildren } from './Text';
2 | import { PlainTextElement } from '@slack/types';
3 |
4 | export interface PlainTextProps extends TextProps {
5 | emoji?: boolean;
6 | }
7 |
8 | export const PlainText: Text = ({
9 | children,
10 | emoji = false,
11 | }) => ({
12 | type: 'plain_text',
13 | text: joinTextChildren(children),
14 | emoji,
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/PlainTextInputElement.ts:
--------------------------------------------------------------------------------
1 | import { FC } from '..';
2 | import { PlainTextInput as PlainTextInputSpec } from '@slack/types';
3 |
4 | export interface PlainTextInputProps {
5 | placeholderText?: string;
6 | initialValue?: string;
7 | multiline?: boolean;
8 | minLength?: number;
9 | maxLength?: number;
10 | actionId?: string;
11 | }
12 |
13 | export const PlainTextInputElement: FC<
14 | PlainTextInputProps,
15 | PlainTextInputSpec
16 | > = ({
17 | placeholderText,
18 | initialValue,
19 | multiline,
20 | minLength: min_length,
21 | maxLength: max_length,
22 | actionId: action_id,
23 | }) => {
24 | const plainTextInput: PlainTextInputSpec = {
25 | type: 'plain_text_input',
26 | multiline,
27 | min_length,
28 | max_length,
29 | action_id,
30 | };
31 |
32 | if (placeholderText) {
33 | plainTextInput.placeholder = { type: 'plain_text', text: placeholderText };
34 | }
35 |
36 | if (initialValue) {
37 | plainTextInput.initial_value = initialValue;
38 | }
39 |
40 | return plainTextInput;
41 | };
42 |
--------------------------------------------------------------------------------
/src/components/ProgressBar.ts:
--------------------------------------------------------------------------------
1 | import { Span } from './Span';
2 |
3 | const COMPLETED_CHAR = '▓';
4 | const INCOMPLETE_CHAR = '░';
5 |
6 | interface ProgressBarProps {
7 | value: number;
8 | total: number;
9 | color?: 'red' | 'black';
10 | columnWidth?: number;
11 | }
12 |
13 | export const ProgressBar: Span = ({
14 | value,
15 | total,
16 | color = 'black',
17 | columnWidth = 5,
18 | }) => {
19 | const completedCount = Math.ceil((value / total) * columnWidth);
20 | const incompleteCount = columnWidth - completedCount;
21 | const segments =
22 | COMPLETED_CHAR.repeat(completedCount) +
23 | INCOMPLETE_CHAR.repeat(incompleteCount);
24 |
25 | return color === 'red' ? `\`${segments}\`` : segments;
26 | };
27 |
--------------------------------------------------------------------------------
/src/components/RadioButtons.ts:
--------------------------------------------------------------------------------
1 | import { RadioButtons } from '@slack/types';
2 | import { FC } from '..';
3 | import { buildInputOptions, InputOption } from './shared/inputOption';
4 |
5 | export interface RadioButtonsElementProps {
6 | initialOption?: InputOption;
7 | actionId: string;
8 | options: InputOption[];
9 | }
10 |
11 | export const RadioButtonsElement: FC<
12 | RadioButtonsElementProps,
13 | RadioButtons
14 | > = ({ initialOption, actionId, options }) => ({
15 | type: 'radio_buttons',
16 | action_id: actionId,
17 | options: buildInputOptions(options),
18 | initial_option: buildInputOptions([initialOption])[0],
19 | });
20 |
--------------------------------------------------------------------------------
/src/components/SectionBlock.ts:
--------------------------------------------------------------------------------
1 | import { Block, BlockProps } from './Block';
2 | import { SectionBlock as SectionBlockSpec } from '@slack/types';
3 | import { ContainerProps } from './ContainerProps';
4 | import { AnyText } from './AnyText';
5 | import { ElementSpec } from './BlockElement';
6 |
7 | export interface SectionBlockProps extends BlockProps, ContainerProps {
8 | fields?: ReturnType[];
9 | accessory?: ElementSpec;
10 | }
11 |
12 | export const SectionBlock: Block = ({
13 | children,
14 | accessory,
15 | fields,
16 | blockId,
17 | }) => ({
18 | type: 'section',
19 | block_id: blockId,
20 | text: [].concat(children)[0],
21 | accessory,
22 | fields,
23 | });
24 |
--------------------------------------------------------------------------------
/src/components/SingleSelectElement.ts:
--------------------------------------------------------------------------------
1 | import { StaticSelect, PlainTextElement } from '@slack/types';
2 | import { FC } from '..';
3 | import { buildInputOptions, InputOption } from './shared/inputOption';
4 |
5 | export interface SelectElementProps {
6 | placeholder: PlainTextElement;
7 | actionId: string;
8 | options?: InputOption[];
9 | optionGroups?: { label: PlainTextElement; options: InputOption[] }[];
10 | }
11 |
12 | export interface SingleSelectElementProps extends SelectElementProps {
13 | initialOption?: InputOption;
14 | }
15 |
16 | export const SingleSelectElement: FC<
17 | SingleSelectElementProps,
18 | StaticSelect
19 | > = ({ placeholder, actionId, options, optionGroups, initialOption }) => ({
20 | type: 'static_select',
21 | placeholder,
22 | action_id: actionId,
23 | ...(options && { options: buildInputOptions(options) }),
24 | ...(optionGroups && {
25 | option_groups: optionGroups.map(group => ({
26 | label: group.label,
27 | options: buildInputOptions(group.options),
28 | })),
29 | }),
30 | initial_option: buildInputOptions([initialOption])[0],
31 | });
32 |
--------------------------------------------------------------------------------
/src/components/Span.ts:
--------------------------------------------------------------------------------
1 | import { FC } from '..';
2 |
3 | export type Span = FC
;
4 |
--------------------------------------------------------------------------------
/src/components/Text.ts:
--------------------------------------------------------------------------------
1 | import { FC } from '..';
2 | import { MrkdwnElement, PlainTextElement } from '@slack/types';
3 | import { ContainerProps } from './ContainerProps';
4 |
5 | // https://api.slack.com/reference/messaging/composition-objects#text
6 |
7 | // custom 'spec' since this isn't defined in '@slack/types'
8 | export interface MessageTextSpec {
9 | text: string;
10 | mrkdwn: boolean;
11 | }
12 |
13 | export type TextType = 'plain_text' | 'mrkdwn';
14 |
15 | export type TextProps = ContainerProps;
16 |
17 | export type TextElementSpec = MrkdwnElement | PlainTextElement;
18 |
19 | export type Text = FC
;
20 |
21 | export type MessageText
= FC<
22 | P,
23 | E
24 | >;
25 |
26 | export const joinTextChildren = (children: string | string[]): string => {
27 | // console.log('TEXT CHILDREN', children);
28 | return [].concat(children).join('');
29 | };
30 |
--------------------------------------------------------------------------------
/src/components/TimePicker.ts:
--------------------------------------------------------------------------------
1 | import { PlainTextElement } from '@slack/types';
2 | import { FC } from '..';
3 |
4 | export interface TimePickerSpec {
5 | initial_time?: string;
6 | placeholder: PlainTextElement;
7 | action_id: string;
8 | type: 'timepicker';
9 | }
10 |
11 | export interface TimePickerElementProps {
12 | initialTime?: string;
13 | placeholder: PlainTextElement;
14 | actionId: string;
15 | }
16 |
17 | export const TimePickerElement: FC = ({
18 | initialTime,
19 | actionId,
20 | placeholder,
21 | }) => ({
22 | type: 'timepicker',
23 | action_id: actionId,
24 | initial_time: initialTime,
25 | placeholder,
26 | });
27 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Message';
2 |
3 | export * from './Modal';
4 | export * from './Home';
5 | export * from './ActionsBlock';
6 | export * from './ContextBlock';
7 | export * from './DividerBlock';
8 | export * from './InputBlock';
9 | export * from './SectionBlock';
10 | export * from './ImageBlock';
11 | export * from './HeaderBlock';
12 |
13 | export * from './ButtonElement';
14 | export * from './ImageElement';
15 | export * from './PlainTextInputElement';
16 | export * from './ChannelsSelect';
17 | export * from './ConversationsSelect';
18 | export * from './SingleSelectElement';
19 | export * from './MultiSelectElement';
20 | export * from './ExternalSelect';
21 | export * from './Checkbox';
22 | export * from './RadioButtons';
23 | export * from './OverflowMenuElement';
24 | export { InputOption } from './shared/inputOption';
25 |
26 | export * from './PlainText';
27 | export * from './MarkdownText';
28 | export * from './MessageText';
29 |
30 | export * from './Link';
31 | export * from './Mention';
32 | export * from './ProgressBar';
33 | export * from './LineBreak';
34 | export * from './BlockQuote';
35 | export * from './TimePicker';
36 |
--------------------------------------------------------------------------------
/src/components/shared/inputOption.ts:
--------------------------------------------------------------------------------
1 | import { Option } from '@slack/types';
2 | import { AnyText } from '../AnyText';
3 |
4 | export interface InputOption {
5 | text: ReturnType;
6 | value?: string;
7 | description?: string;
8 | url?: string;
9 | }
10 |
11 | export const buildInputOptions = (inputOptions: InputOption[]): Option[] =>
12 | inputOptions
13 | ? inputOptions.map(inputOption => {
14 | if (!inputOption) {
15 | return;
16 | }
17 | const option: Option = {
18 | text: inputOption.text,
19 | };
20 | if (inputOption.url) {
21 | option.url = inputOption.url;
22 | }
23 | if (inputOption.value) {
24 | option.value = inputOption.value;
25 | }
26 | if (inputOption.description) {
27 | option.description = {
28 | text: inputOption.description,
29 | type: 'plain_text',
30 | emoji: true,
31 | };
32 | }
33 | return option;
34 | })
35 | : [];
36 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import flattenDeep from 'lodash.flattendeep';
2 |
3 | export type SlackSpec = {} | string;
4 |
5 | const pruneFields = (o: {}): Partial =>
6 | o
7 | ? Object.keys(o).reduce(
8 | (obj, k) => (o[k] !== undefined ? { ...obj, [k]: o[k] } : obj),
9 | {}
10 | )
11 | : undefined;
12 |
13 | type Props = { children?: unknown } & P;
14 |
15 | export type FC
= (props: Props
) => R;
16 |
17 | const resolveDeep = async (thing: any) => {
18 | if (!thing) {
19 | return thing;
20 | } else if (Array.isArray(thing)) {
21 | return await Promise.all(thing.map(item => resolveDeep(item)));
22 | } else if (thing.__proto__ === Promise.prototype) {
23 | return await Promise.resolve(thing);
24 | } else if (typeof thing === 'object') {
25 | if (Object.getPrototypeOf(thing).constructor.toString().match(/class/)) {
26 | return await Promise.resolve(thing);
27 | }
28 | const resolvedPairs = await Promise.all(
29 | Object.keys(thing).map(async key => [key, await resolveDeep(thing[key])])
30 | );
31 | return Object.fromEntries(resolvedPairs);
32 | } else if (typeof thing === 'function') {
33 | return await Promise.resolve(thing);
34 | } else {
35 | return thing;
36 | }
37 | };
38 |
39 | // for now, this is a hack to help with typescript checking.
40 | // if you wrap a top-level JSX element with `await render(<>)`,
41 | // it's a better experience than just warpping with `await (<>)`.
42 | // in the future, most of the promise awaiting should happen in
43 | // render. h should just return the tree of JSX elements, and
44 | // render should walk the tree and process/await/execute hooks,
45 | // etc.
46 | export const render = async (
47 | rootElement: Promise
48 | ): Promise => await rootElement;
49 |
50 | export namespace slack {
51 | export const h = async (
52 | node: FC<{}, any>,
53 | props: Props<{}>,
54 | ...children: any[]
55 | ): Promise => {
56 | if (typeof node !== 'function') {
57 | throw new Error('node not an FC');
58 | }
59 |
60 | const resolvedProps = await resolveDeep(props || {});
61 |
62 | const spec = await node({
63 | ...resolvedProps,
64 | children: flattenDeep(
65 | await Promise.all(
66 | children.map(async child => {
67 | if (Array.isArray(child)) {
68 | return await Promise.all(flattenDeep(child));
69 | } else if (
70 | typeof child === 'function' ||
71 | typeof child === 'object'
72 | ) {
73 | return await Promise.resolve(child);
74 | }
75 |
76 | return child;
77 | })
78 | )
79 | ).filter(child => !!child),
80 | });
81 |
82 | return typeof spec === 'string' || Array.isArray(spec)
83 | ? spec
84 | : pruneFields(spec);
85 | };
86 |
87 | export const Fragment = ({ children }): JSX.Element => {
88 | return children as JSX.Element;
89 | };
90 |
91 | export namespace JSX {
92 | export type Element = any;
93 | export interface ElementAttributesProperty {
94 | props: {};
95 | }
96 | export interface ElementChildrenAttribute {
97 | children: {};
98 | }
99 | }
100 | }
101 |
102 | export * from './components';
103 | export { renderMarkdown } from './renderMarkdown';
104 |
--------------------------------------------------------------------------------
/src/renderMarkdown.ts:
--------------------------------------------------------------------------------
1 | import slackify from 'slackify-markdown';
2 |
3 | export const renderMarkdown = (markdown: string): string => slackify(markdown);
4 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "commonjs",
5 | "sourceMap": true,
6 | "moduleResolution": "node",
7 | "emitDeclarationOnly": false,
8 | "declaration": true,
9 | // "outFile": "dist/index.d.ts",
10 | "outDir": "dist",
11 | "strict": false,
12 | "allowSyntheticDefaultImports": true,
13 | "esModuleInterop": true,
14 | "jsx": "react"
15 | },
16 | "exclude": ["src/__tests__"],
17 | "include": ["src", "@types"]
18 | }
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Target latest version of ECMAScript.
4 | "target": "esnext",
5 | "module": "commonjs",
6 | "sourceMap": true,
7 | // Search under node_modules for non-relative imports.
8 | "moduleResolution": "node",
9 | // Process & infer types from .js files.
10 | "allowJs": true,
11 | // Generates corresponding '.d.ts' file.
12 | "declaration": true,
13 | // Don't emit; allow Babel to transform files.
14 | "noEmit": true,
15 | // Enable strictest settings like strictNullChecks & noImplicitAny.
16 | "strict": false,
17 | // Disallow features that require cross-file information for emit.
18 | // "isolatedModules": true,
19 | // Import non-ES modules as default imports.
20 | "esModuleInterop": true,
21 | "jsx": "preserve",
22 | "jsxFactory": "slack.h"
23 | },
24 | "include": ["src", "@types"]
25 | }
26 |
--------------------------------------------------------------------------------