UNPKG

reblock

Version:

Build interactive Slack surfaces with React

443 lines (442 loc) 10.8 kB
import { assertNoChildren, dateToSlackTimestamp, getTextChild, getTextProperty, jsxToImageObject, plainDateToString, } from '../helpers' import { Temporal } from 'temporal-polyfill' function jsxChildrenToOptions(children, elementName, plainTextOnly = false) { const options = [] const selectedOptions = [] for (const child of children) { if (child.type !== 'instance') { throw new Error(`Only ${elementName} elements allowed here`) } if (child.element !== elementName) { throw new Error(`Only ${elementName} elements allowed here`) } if (plainTextOnly && child.props.mrkdwn) { throw new Error('Only plain text allowed here') } const option = child.props.mrkdwn && !plainTextOnly ? { text: { type: 'mrkdwn', text: getTextChild(child), }, } : { text: { type: 'plain_text', text: getTextChild(child), }, } options.push(option) if (child.props.selected) { selectedOptions.push(option) } } return { options, initial_options: selectedOptions, initial_option: selectedOptions[0], } } export function jsxToBlockElement(jsx) { if (jsx.type === 'text') { return { type: 'plain_text', text: jsx.text, } } if (jsx.element === 'mrkdwn') { return { type: 'mrkdwn', text: getTextChild(jsx), } } // Common props const confirm = jsx.props.confirm const focus_on_load = !!jsx.props.focus const placeholder = jsx.props.placeholder ? { type: 'plain_text', text: getTextProperty(jsx.props.placeholder, true), } : undefined const action_id = `reblock_${jsx.id}` if (jsx.element === 'button') { if (jsx.props.workflow) { return { type: 'workflow_button', workflow: jsx.props.workflow, text: { type: 'plain_text', text: getTextChild(jsx), }, style: jsx.props.primary ? 'primary' : jsx.props.danger ? 'danger' : undefined, accessibility_label: getTextProperty(jsx.props.alt), confirm, } } return { type: 'button', text: { type: 'plain_text', text: getTextChild(jsx), }, url: getTextProperty(jsx.props.url), style: jsx.props.primary ? 'primary' : jsx.props.danger ? 'danger' : undefined, accessibility_label: getTextProperty(jsx.props.alt), confirm, action_id, } } if (jsx.element === 'text') { return { type: 'plain_text_input', initial_value: getTextProperty(jsx.props.initial), multiline: !!jsx.props.multiline, min_length: jsx.props.minLength ? Number(jsx.props.minLength) : undefined, max_length: jsx.props.maxLength ? Number(jsx.props.maxLength) : undefined, placeholder, focus_on_load, action_id, } } if (jsx.element === 'textarea') { return { type: 'rich_text_input', // TODO: initial_value placeholder, focus_on_load, action_id, } } if (jsx.element === 'datepicker') { assertNoChildren(jsx) return { type: 'datepicker', initial_date: plainDateToString(jsx.props.initial), confirm, placeholder, focus_on_load, action_id, } } if (jsx.element === 'datetimepicker') { assertNoChildren(jsx) return { type: 'datetimepicker', initial_date_time: dateToSlackTimestamp(jsx.props.initial), confirm, focus_on_load, action_id, } } if (jsx.element === 'timepicker') { assertNoChildren(jsx) const timezoneRaw = jsx.props.timezone let timezone = getTextProperty(timezoneRaw) if (timezoneRaw instanceof Temporal.TimeZone) { timezone = timezoneRaw.id } const initialRaw = jsx.props.initial let initial_time = getTextProperty(initialRaw) if (initialRaw instanceof Temporal.PlainTime) { initial_time = initialRaw.toString() } return { type: 'timepicker', initial_time, timezone, confirm, placeholder, focus_on_load, action_id, } } if (jsx.element === 'email') { assertNoChildren(jsx) return { type: 'email_text_input', initial_value: getTextProperty(jsx.props.initial), placeholder, action_id, } } if (jsx.element === 'url') { assertNoChildren(jsx) return { type: 'url_text_input', initial_value: getTextProperty(jsx.props.initial), placeholder, focus_on_load, action_id, } } if (jsx.element === 'number') { assertNoChildren(jsx) const numberString = (input) => { if (typeof input === 'number') { return String(input) } if (typeof input === 'string') { return input } return undefined } return { type: 'number_input', is_decimal_allowed: !!jsx.props.decimal, initial_value: numberString(jsx.props.initial), min_value: numberString(jsx.props.min), max_value: numberString(jsx.props.max), placeholder, focus_on_load, action_id, } } if (jsx.element === 'file') { assertNoChildren(jsx) return { type: 'file_input', filetypes: jsx.props.filetypes, max_files: Number(jsx.props.maxFiles ?? 10), action_id, } } if (jsx.element === 'checkboxes') { return { type: 'checkboxes', ...jsxChildrenToOptions(jsx.children, 'checkbox'), confirm, focus_on_load, action_id, } } if (jsx.element === 'radio') { return { type: 'radio_buttons', ...jsxChildrenToOptions(jsx.children, 'option'), confirm, focus_on_load, action_id, } } if (jsx.element === 'select') { return { type: jsx.props.multi ? 'multi_static_select' : 'static_select', ...jsxChildrenToOptions(jsx.children, 'option', true), max_selected_items: jsx.props.max, placeholder, confirm, focus_on_load, action_id, } } if (jsx.element === 'selectuser') { assertNoChildren(jsx) if (jsx.props.multi) { return { type: 'multi_users_select', initial_users: jsx.props.initial, max_selected_items: jsx.props.max, placeholder, confirm, focus_on_load, action_id, } } return { type: 'users_select', initial_user: getTextProperty(jsx.props.initial), placeholder, confirm, focus_on_load, action_id, } } if (jsx.element === 'selectconversation') { assertNoChildren(jsx) if (jsx.props.multi) { return { type: 'multi_conversations_select', initial_conversations: jsx.props.initial, max_selected_items: jsx.props.max, default_to_current_conversation: !!jsx.props.defaultToCurrent, filter: jsx.props.filter, placeholder, confirm, focus_on_load, action_id, } } return { type: 'conversations_select', initial_conversation: getTextProperty(jsx.props.initial), default_to_current_conversation: !!jsx.props.defaultToCurrent, filter: jsx.props.filter, placeholder, confirm, focus_on_load, action_id, } } if (jsx.element === 'selectchannel') { assertNoChildren(jsx) if (jsx.props.multi) { return { type: 'multi_channels_select', initial_channels: jsx.props.initial, max_selected_items: jsx.props.max, placeholder, confirm, focus_on_load, action_id, } } return { type: 'channels_select', initial_channel: getTextProperty(jsx.props.initial), placeholder, confirm, focus_on_load, action_id, } } if (jsx.element === 'overflow') { return { type: 'overflow', options: jsxChildrenToOptions(jsx.children, 'option', true).options, confirm, action_id, } } if (jsx.element === 'img') { if (jsx.props.title) { throw new Error( 'Title not allowed on image element, only image blocks allow titles' ) } return jsxToImageObject(jsx) } throw new Error(`Unsupported block element: ${jsx.element}`) } export function blockElementIsSectionAccessory(element) { return [ 'image', 'button', 'checkboxes', 'datepicker', 'multi_users_select', 'multi_static_select', 'multi_conversations_select', 'multi_channels_select', 'multi_external_select', 'overflow', 'radio_buttons', 'users_select', 'static_select', 'conversations_select', 'channels_select', 'external_select', 'timepicker', 'workflow_button', ].includes(element.type) } /** The JSX tag names which correspond to block elements an input block allows */ export const inputBlockElementTagNames = [ 'text', 'textarea', 'datepicker', 'datetimepicker', 'timepicker', 'email', 'url', 'number', 'file', 'checkboxes', 'radio', 'select', 'selectuser', 'selectconversation', 'selectchannel', ] export function blockElementIsInputBlockElement(element) { return [ 'checkboxes', 'datepicker', 'multi_users_select', 'multi_static_select', 'multi_conversations_select', 'multi_channels_select', 'multi_external_select', 'radio_buttons', 'users_select', 'static_select', 'conversations_select', 'channels_select', 'external_select', 'timepicker', 'datetimepicker', 'email_text_input', 'file_input', 'number_input', 'plain_text_input', 'rich_text_input', 'url_text_input', ].includes(element.type) } /** The JSX tag names which correspond to block elements an actions block allows */ export const actionsBlockElementTagNames = [ 'button', 'checkboxes', 'datepicker', 'datetimepicker', 'timepicker', 'select', 'selectuser', 'selectconversation', 'selectchannel', 'overflow', 'radio', 'textarea', ] export function blockElementIsActionsBlockElement(element) { return [ 'button', 'checkboxes', 'datepicker', 'multi_users_select', 'multi_static_select', 'multi_conversations_select', 'multi_channels_select', 'multi_external_select', 'overflow', 'radio_buttons', 'users_select', 'static_select', 'conversations_select', 'channels_select', 'external_select', 'timepicker', 'workflow_button', 'datetimepicker', 'rich_text_input', ].includes(element.type) } export function blockElementIsContextBlockElement(element) { return ['image', 'mrkdwn', 'plain_text'].includes(element.type) }