Repository: speee/jsx-slack
Pull request #177 (2020-07-21 04:57:29)

Add support of type prop and workflow_step type for <Modal>
This change makes `<Modal>` allow defining `type` prop and specifying `workflow_step` type, for [building the custom workflow step in Slack Workflow Builder](https://api.slack.com/workflows/steps).

In jsx-slack, `workflow_step` can use as the subtype of the modal container.

```javascript
<Modal type="workflow_step">
  <Section>Hello!</Section>
</Modal>

/*
{
  "type": "workflow_step",
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "Hello!",
        "verbatim": true
      }
    }
  ]
}
*/
```

`type` is an optional prop and the default is `modal`. No change required for users using the traditional modal.

## ToDo

- [x] Documentation
- [x] Auto-completion in demo



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 20e0875..b8bbd17 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 ## [Unreleased]
 
+### Added
+
+- `type` prop and [`workflow_step` type](https://api.slack.com/reference/workflows/configuration-view) for `<Modal>` ([#176](https://github.com/speee/jsx-slack/issues/176), [#177](https://github.com/speee/jsx-slack/pull/177))
+
 ## v2.2.1 - 2020-07-17
 
 ### Fixed
diff --git a/demo/schema.js b/demo/schema.js
index 540d1fc..6cc8344 100644
--- a/demo/schema.js
+++ b/demo/schema.js
@@ -81,6 +81,7 @@ const schema = {
   Modal: {
     attrs: {
       title: null,
+      type: ['modal', 'workflow_step'],
       close: null,
       submit: null,
       privateMetadata: null,
diff --git a/docs/block-containers.md b/docs/block-containers.md
index a074d90..e971150 100644
--- a/docs/block-containers.md
+++ b/docs/block-containers.md
@@ -68,13 +68,20 @@ export const shareModal = (opts) => (
 
 ### Props
 
+- `type` (optional): Set a type of modal.
+  - [`modal`](https://api.slack.com/reference/surfaces/views) _(default)_: The regular modal surface.
+  - [`workflow_step`](https://api.slack.com/reference/workflows/configuration-view): The modal surface for [custom workflow step](https://api.slack.com/workflows/steps). In this type, some props for around of the content are ignored.
+
+* `privateMetadata` (optional): An optional string that can be found in payloads of some interactive events Slack app received (3000 characters maximum). [By setting function, you can use the custom transformer to serialize hidden values set up via `<Input type="hidden">`](block-elements.md#custom-transformer).
+* `callbackId` (optional): An identifier for this modal to recognize it in various events. (255 characters maximum)
+
+#### Props for `modal` type
+
 - `title` (**required**): An user-facing title of the modal. (24 characters maximum)
 - `close` (optional): A text for close button of the modal. (24 characters maximum)
 - `submit` (optional): A text for submit button of the modal. The value specified in this prop is preferred than [`<Input type="submit">`](block-elements.md#input-submit) (24 characters maximum)
-- `privateMetadata` (optional): An optional string that can be found in payloads of some interactive events Slack app received (3000 characters maximum). [By setting function, you can use the custom transformer to serialize hidden values set up via `<Input type="hidden">`](block-elements.md#custom-transformer).
 - `clearOnClose` (optional): If enabled by setting `true`, all stacked views will be cleared by close button.
 - `notifyOnClose` (optional): If enabled by setting `true`, `view_closed` event will be sent to request URL of Slack app when closed modal.
-- `callbackId` (optional): An identifier for this modal to recognize it in various events. (255 characters maximum)
 - `externalId` (optional): A unique ID for all views on a per-team basis.
 
 > :information_source: Slack requires the submit text when modal has component for inputs, so jsx-slack would set the text "Submit" as the default value of `submit` prop if you are setting no submit text in any way together with using input components.
diff --git a/src/block-kit/container/Modal.tsx b/src/block-kit/container/Modal.tsx
index c5dfa64..246432b 100644
--- a/src/block-kit/container/Modal.tsx
+++ b/src/block-kit/container/Modal.tsx
@@ -6,6 +6,7 @@ import {
   createComponent,
   createElementInternal,
 } from '../../jsx'
+import { DistributedProps } from '../../utils'
 import { plainText } from '../composition/utils'
 import { Select } from '../elements/Select'
 import { Textarea } from '../input/Textarea'
@@ -20,7 +21,7 @@ import {
   generateSectionValidator,
 } from './utils'
 
-interface ModalProps {
+interface ModalPropsBase {
   children: JSXSlack.ChildNodes
 
   /**
@@ -28,23 +29,6 @@ interface ModalProps {
    */
   callbackId?: string
 
-  /**
-   * Set whether all stacked views will clear by the close button on this modal.
-   */
-  clearOnClose?: boolean
-
-  /** A text for close button of the modal. (24 characters maximum) */
-  close?: string
-
-  /** A unique ID for all views on a per-team basis. */
-  externalId?: string
-
-  /**
-   * Set whether to send `view_closed` event to the request URL of Slack app
-   * when closed modal.
-   */
-  notifyOnClose?: boolean
-
   /**
    * An optional metadata string for handling stored data in callback events
    * from Slack API. (3000 characters maximum)
@@ -77,6 +61,25 @@ interface ModalProps {
    * string, or `undefined` if won't assign private metadata.
    */
   privateMetadata?: string | PrivateMetadataTransformer
+}
+
+interface BasicModalProps extends ModalPropsBase {
+  /**
+   * Set whether all stacked views will clear by the close button on this modal.
+   */
+  clearOnClose?: boolean
+
+  /** A text for close button of the modal. (24 characters maximum) */
+  close?: string
+
+  /** A unique ID for all views on a per-team basis. */
+  externalId?: string
+
+  /**
+   * Set whether to send `view_closed` event to the request URL of Slack app
+   * when closed modal.
+   */
+  notifyOnClose?: boolean
 
   /**
    * A text for submit button of the modal. (24 characters maximum)
@@ -89,8 +92,23 @@ interface ModalProps {
 
   /** An user-facing title of the modal. (24 characters maximum) */
   title: string
+
+  /**
+   * Set a type of modal.
+   *
+   * - `modal` (default): The regular modal surface.
+   * - `workflow_step`: The modal surface for {@link https://api.slack.com/workflows/steps|custom workflow step}.
+   * In this type, some props for around of the content are ignored.
+   */
+  type?: 'modal'
+}
+
+interface WorkflowStepModalProps extends ModalPropsBase {
+  type: 'workflow_step'
 }
 
+type ModalProps = DistributedProps<BasicModalProps | WorkflowStepModalProps>
+
 const ModalBlocks = generateBlocksContainer({
   name: 'Modal',
   availableBlockTypes: {
@@ -201,18 +219,25 @@ export const Modal = createComponent<ModalProps, View>('Modal', (props) => {
     return pmObject && JSON.stringify(pmObject)
   })()
 
-  return {
-    type: 'modal',
-    title: plainText(props.title),
-    callback_id: props.callbackId,
-    external_id: props.externalId,
+  // TODO: Remove any type annotation if @slack/types supported workflow_step type
+  const type: any = props.type || 'modal'
+
+  const basicModalPayloads: Partial<View> = {
+    title: plainText(props.title || ''),
     submit: props.submit ? plainText(props.submit) : submit,
     close: props.close ? plainText(props.close) : undefined,
-    private_metadata: privateMetadata,
     clear_on_close:
       props.clearOnClose !== undefined ? !!props.clearOnClose : undefined,
     notify_on_close:
       props.notifyOnClose !== undefined ? !!props.notifyOnClose : undefined,
+  }
+
+  return {
+    type,
+    callback_id: props.callbackId,
+    external_id: props.externalId,
+    private_metadata: privateMetadata,
+    ...(type === 'modal' ? basicModalPayloads : {}),
     blocks: cleanMeta(<ModalBlocks children={children} />) as any[],
   }
 })


Test patch:
diff --git a/test/block-kit/container-components.tsx b/test/block-kit/container-components.tsx
index 3a16dc8..ec9e1ea 100644
--- a/test/block-kit/container-components.tsx
+++ b/test/block-kit/container-components.tsx
@@ -204,6 +204,43 @@ describe('Container components', () => {
         </Modal>
       ).not.toStrictEqual(expect.objectContaining({ submit }))
     })
+
+    describe('with `workflow_step` type', () => {
+      it('ignores some props about for around of the modal content', () => {
+        expect(
+          <Modal type="workflow_step">
+            <Input type="submit" value="submit" />
+          </Modal>
+        ).toStrictEqual({
+          type: 'workflow_step',
+          blocks: [],
+        })
+
+        const workflowStepFull = (
+          // @ts-expect-error
+          <Modal
+            callbackId="callback_id"
+            clearOnClose
+            close="Close"
+            externalId="external_id"
+            notifyOnClose={false}
+            privateMetadata="private_metadata"
+            submit="Submit"
+            title="test"
+            type="workflow_step"
+          >
+            <Section>Hello!</Section>
+          </Modal>
+        )
+
+        expect(workflowStepFull['type']).toBe('workflow_step')
+        expect(workflowStepFull).not.toHaveProperty('title')
+        expect(workflowStepFull).not.toHaveProperty('submit')
+        expect(workflowStepFull).not.toHaveProperty('close')
+        expect(workflowStepFull).not.toHaveProperty('clear_on_close')
+        expect(workflowStepFull).not.toHaveProperty('notify_on_close')
+      })
+    })
   })
 
   describe('<Home>', () => {