Repository: speee/jsx-slack
Pull request #72 (2019-10-15 01:58:08)

Output warning about deprecated dialog components
We are planning v1 major update with adding integration with something wonderful that may be announced on [Spec](https://slack.com/spec). And we would remove the outdated features at the same time.

By this PR, the deprecation warning will be outputted when trying to use the outdated dialog components by `@speee-js/jsx-slack/dialog` and `jsxslack` template literal tag.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e27d468..0c476d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 ## [Unreleased]
 
+### Deprecated
+
+- Output warning about deprecated dialog components ([#72](https://github.com/speee/jsx-slack/pull/72))
+
 ## v0.10.2 - 2019-10-11
 
 ### Fixed
diff --git a/jest.config.js b/jest.config.js
index 7840bc6..02a4e4c 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -10,6 +10,8 @@ module.exports = {
   ],
   coveragePathIgnorePatterns: ['/node_modules/', '.*\\.d\\.ts'],
   coverageThreshold: { global: { lines: 95 } },
+  resetMocks: true,
+  restoreMocks: true,
   testEnvironment: 'node',
   testRegex: '(/(test|__tests__)/(?![_.]).*|(\\.|/)(test|spec))\\.[jt]sx?$',
   transform: { ...tsjPreset.transform },
diff --git a/src/dialog/Dialog.tsx b/src/dialog/Dialog.tsx
index 67cd5cd..8068278 100644
--- a/src/dialog/Dialog.tsx
+++ b/src/dialog/Dialog.tsx
@@ -20,6 +20,7 @@ interface DialogElementProps {
   title?: string
 }
 
+/** @deprecated Classic dialog support was deprecated in favor of Slack Modals and will remove in v1. Please migrate into Modal provided by the main entry point. */
 export const validateElement = (props: DialogElementProps) => {
   if (props.label.length > 48)
     throw new DialogValidationError(
@@ -47,7 +48,7 @@ export const validateElement = (props: DialogElementProps) => {
   return { hint }
 }
 
-/** @deprecated A classic dialog support was deprecated in favor of Slack Modals. Please migrate into <Modal> container provided by main entrypoint. */
+/** @deprecated Classic dialog support was deprecated in favor of Slack Modals and will remove in v1. Please migrate into Modal provided by the main entry point. */
 export const Dialog: JSXSlack.FC<DialogProps> = props => {
   let { submitLabel } = props
   let stateJSON
diff --git a/src/dialog/Input.tsx b/src/dialog/Input.tsx
index a52d869..132a467 100644
--- a/src/dialog/Input.tsx
+++ b/src/dialog/Input.tsx
@@ -58,7 +58,7 @@ type InputProps =
 
 type InputElementInternal = HiddenInputProps | SubmitInputProps
 
-// <Input> has an interface to keep compatibility with HTML as possible.
+/** @deprecated Classic dialog support was deprecated in favor of Slack Modals and will remove in v1. Please migrate into Modal provided by the main entry point. */
 export const Input: JSXSlack.FC<InputProps> = props => {
   let subtype: TextProps['subtype'] | undefined
 
diff --git a/src/dialog/Select.tsx b/src/dialog/Select.tsx
index c082477..ac4a75d 100644
--- a/src/dialog/Select.tsx
+++ b/src/dialog/Select.tsx
@@ -133,6 +133,7 @@ const generateFragment = (
   return fragment
 }
 
+/** @deprecated Classic dialog support was deprecated in favor of Slack Modals and will remove in v1. Please migrate into Modal provided by the main entry point. */
 export const SelectFragment: JSXSlack.FC<SelectFragmentProps> = props => {
   const opts = filter(props.children)
 
@@ -190,6 +191,7 @@ export const SelectFragment: JSXSlack.FC<SelectFragmentProps> = props => {
   }
 }
 
+/** @deprecated Classic dialog support was deprecated in favor of Slack Modals and will remove in v1. Please migrate into Modal provided by the main entry point. */
 export const Select: JSXSlack.FC<SelectProps> = props => (
   <ObjectOutput<StaticSelectElement>
     {...baseProps(props)}
@@ -198,6 +200,7 @@ export const Select: JSXSlack.FC<SelectProps> = props => (
   />
 )
 
+/** @deprecated Classic dialog support was deprecated in favor of Slack Modals and will remove in v1. Please migrate into Modal provided by the main entry point. */
 export const ExternalSelect: JSXSlack.FC<ExternalSelectProps> = props => {
   const initial = (() => {
     if (props.initialOption) {
@@ -225,6 +228,7 @@ export const ExternalSelect: JSXSlack.FC<ExternalSelectProps> = props => {
   )
 }
 
+/** @deprecated Classic dialog support was deprecated in favor of Slack Modals and will remove in v1. Please migrate into Modal provided by the main entry point. */
 export const UsersSelect: JSXSlack.FC<UsersSelectProps> = props => (
   <ObjectOutput<UsersSelectElement>
     {...baseProps(props)}
@@ -233,6 +237,7 @@ export const UsersSelect: JSXSlack.FC<UsersSelectProps> = props => (
   />
 )
 
+/** @deprecated Classic dialog support was deprecated in favor of Slack Modals and will remove in v1. Please migrate into Modal provided by the main entry point. */
 export const ConversationsSelect: JSXSlack.FC<
   ConversationsSelectProps
 > = props => (
@@ -243,6 +248,7 @@ export const ConversationsSelect: JSXSlack.FC<
   />
 )
 
+/** @deprecated Classic dialog support was deprecated in favor of Slack Modals and will remove in v1. Please migrate into Modal provided by the main entry point. */
 export const ChannelsSelect: JSXSlack.FC<ChannelsSelectProps> = props => (
   <ObjectOutput<ChannelsSelectElement>
     {...baseProps(props)}
diff --git a/src/dialog/Textarea.tsx b/src/dialog/Textarea.tsx
index 5224509..0cf8b33 100644
--- a/src/dialog/Textarea.tsx
+++ b/src/dialog/Textarea.tsx
@@ -33,6 +33,7 @@ export type TextareaElement = Pick<
   | 'value'
 > & { type: 'textarea' }
 
+/** @deprecated A classic dialog support was deprecated in favor of Slack Modals and will remove in v1. Please migrate into Modal provided by main entrypoint. */
 export const Textarea: JSXSlack.FC<TextareaProps> = props => {
   const validated = validateElement(props)
   const maxLength = coerceToInteger(props.maxLength)
diff --git a/src/dialog/error.ts b/src/dialog/error.ts
index 95439a4..5b92bf6 100644
--- a/src/dialog/error.ts
+++ b/src/dialog/error.ts
@@ -1,3 +1,4 @@
+/** @deprecated Classic dialog support was deprecated in favor of Slack Modals and will remove in v1. Modal's constraint is not so much stricter as dialog so we don't provide an error object for the modal now. */
 export class DialogValidationError extends Error {
   public constructor(message?: string) {
     super(message)
diff --git a/src/dialog/index.ts b/src/dialog/index.ts
index b653b3a..091a8de 100644
--- a/src/dialog/index.ts
+++ b/src/dialog/index.ts
@@ -1,2 +1,6 @@
 export * from './components'
 export { DialogValidationError } from './error'
+
+console.warn(
+  '[DEPRECATION WARNING] Classic dialog support was deprecated in favor of Slack Modals and will remove in v1. Please migrate into Modal provided by the main entry point.'
+)
diff --git a/src/tag.ts b/src/tag.ts
index 591f714..4714878 100644
--- a/src/tag.ts
+++ b/src/tag.ts
@@ -61,43 +61,77 @@ const render = (parsed: unknown) => {
 }
 
 // Resolve built-in components
-const resolveComponent = (target: unknown, context: any = undefined) => {
-  if (!(typeof target === 'object' && target)) return target
-
-  const node = target as VirtualNode
-  let { type } = node
+// TODO: Please remove in v1. This logic should not be required when the deprecated dialog was removed.
+const resolveComponent = (t: unknown) => {
+  let usingDeprecatedDialog = false
+
+  const resolveComponentInner = (target: unknown, context: any) => {
+    if (!(typeof target === 'object' && target)) return target
+
+    const node = target as VirtualNode
+    let { type } = node
+
+    if (typeof type === 'string') {
+      if (
+        type.startsWith('Dialog.') &&
+        Object.prototype.hasOwnProperty.call(dialogComponents, type.slice(7))
+      ) {
+        // `Dialog.` prefix
+        type = dialogComponents[type.slice(7)]
+      } else if (
+        context &&
+        Object.prototype.hasOwnProperty.call(context, type)
+      ) {
+        // Resolve from current context
+        type = context[type]
+      } else if (
+        Object.prototype.hasOwnProperty.call(blockKitComponents, type)
+      ) {
+        // Block Kit (default)
+        type = blockKitComponents[type]
+      } else if (Object.prototype.hasOwnProperty.call(dialogComponents, type)) {
+        // Dialog (when not resolved in others)
+        type = dialogComponents[type]
+      }
+    }
 
-  if (typeof type === 'string') {
+    // Detect deprecated dialog
     if (
-      type.startsWith('Dialog.') &&
-      Object.prototype.hasOwnProperty.call(dialogComponents, type.slice(7))
+      !Object.values(blockKitComponents).includes(type) &&
+      Object.values(dialogComponents).includes(type)
     ) {
-      // `Dialog.` prefix
-      type = dialogComponents[type.slice(7)]
-    } else if (context && Object.prototype.hasOwnProperty.call(context, type)) {
-      // Resolve from current context
-      type = context[type]
-    } else if (Object.prototype.hasOwnProperty.call(blockKitComponents, type)) {
-      // Block Kit (default)
-      type = blockKitComponents[type]
-    } else if (Object.prototype.hasOwnProperty.call(dialogComponents, type)) {
-      // Dialog (when not resolved in others)
-      type = dialogComponents[type]
+      usingDeprecatedDialog = true
     }
-  }
 
-  let childrenContext: any = context
+    let childrenContext: any = context
+
+    if (!childrenContext) {
+      if (type === blockKitComponents.Blocks) {
+        childrenContext = blockKitComponents
+      }
+      if (type === dialogComponents.Dialog) {
+        childrenContext = dialogComponents
+      }
+    }
 
-  if (!childrenContext) {
-    if (type === blockKitComponents.Blocks) childrenContext = blockKitComponents
-    if (type === dialogComponents.Dialog) childrenContext = dialogComponents
+    return {
+      type,
+      props: node.props,
+      children: node.children.map(c =>
+        resolveComponentInner(c, childrenContext)
+      ),
+    }
   }
 
-  return {
-    type,
-    props: node.props,
-    children: node.children.map(c => resolveComponent(c, childrenContext)),
+  const result = resolveComponentInner(t, undefined)
+
+  if (usingDeprecatedDialog) {
+    console.warn(
+      '[DEPRECATION WARNING] Classic dialog support was deprecated in favor of Slack Modals and will remove in v1. Please migrate into Modal provided by the main entry point.'
+    )
   }
+
+  return result
 }
 
 const parse = (template: TemplateStringsArray, ...substitutions: any[]) => {


Test patch:
diff --git a/test/tag.tsx b/test/tag.tsx
index 7748cd9..c057b09 100644
--- a/test/tag.tsx
+++ b/test/tag.tsx
@@ -21,6 +21,12 @@ import JSXSlack, {
 } from '../src/index'
 
 describe('Tagged template', () => {
+  let warnSpy: jest.SpyInstance
+
+  beforeEach(() => {
+    warnSpy = jest.spyOn(console, 'warn')
+  })
+
   it('allows converting Block Kit JSX to JSON without transpiler', () => {
     const count = 2
     const template = jsxslack`
@@ -66,6 +72,7 @@ describe('Tagged template', () => {
         </Blocks>
       )
     )
+    expect(warnSpy).toBeCalledTimes(0)
   })
 
   it('allows converting dialog JSX to JSON without transpiler', () => {
@@ -98,6 +105,7 @@ describe('Tagged template', () => {
         </Dialog>
       )
     )
+    expect(warnSpy).toBeCalledTimes(1)
   })
 
   it('can use imported components through interpolation', () => {
@@ -114,6 +122,7 @@ describe('Tagged template', () => {
         <${Input} type="submit" value="Submit dialog" />
       <//>
     `).toMatchSnapshot()
+    expect(warnSpy).toBeCalledTimes(1)
   })
 
   it('can use fragmented options in <Select>', () => {
@@ -143,6 +152,7 @@ describe('Tagged template', () => {
         </Blocks>
       )
     )
+    expect(warnSpy).toBeCalledTimes(0)
   })
 
   it('can use interpolations through conditional rendering', () => {
@@ -162,6 +172,7 @@ describe('Tagged template', () => {
         </Blocks>
       )
     )
+    expect(warnSpy).toBeCalledTimes(0)
   })
 
   it('has same decode behavior compatible with JSX for HTML entities', () => {
@@ -280,6 +291,9 @@ describe('Tagged template', () => {
           </Dialog>
         )
       )
+
+      // Warn 3 times by each components
+      expect(warnSpy).toBeCalledTimes(3)
     })
   })
 })