From 9d42e7f97b23df10aa86bf32b69e08bb685dc8db Mon Sep 17 00:00:00 2001 From: Adam Chmara Date: Tue, 17 Dec 2024 15:45:07 +0100 Subject: [PATCH] feat(dashboard): sms step --- .../public/images/phones/iphone-sms.svg | 215 ++++++++++++++++++ .../workflow-editor/add-step-menu.tsx | 24 +- .../workflow-editor/steps/component-utils.tsx | 18 +- .../steps/configure-step-form.tsx | 30 +-- .../steps/configure-step-template-form.tsx | 23 +- .../steps/sms/configure-sms-step-preview.tsx | 35 +++ .../steps/sms/sms-editor-preview.tsx | 111 +++++++++ .../workflow-editor/steps/sms/sms-editor.tsx | 19 ++ .../workflow-editor/steps/sms/sms-phone.tsx | 61 +++++ .../workflow-editor/steps/sms/sms-preview.tsx | 44 ++++ .../steps/sms/sms-tabs-section.tsx | 8 + .../workflow-editor/steps/sms/sms-tabs.tsx | 36 +++ 12 files changed, 581 insertions(+), 43 deletions(-) create mode 100644 apps/dashboard/public/images/phones/iphone-sms.svg create mode 100644 apps/dashboard/src/components/workflow-editor/steps/sms/configure-sms-step-preview.tsx create mode 100644 apps/dashboard/src/components/workflow-editor/steps/sms/sms-editor-preview.tsx create mode 100644 apps/dashboard/src/components/workflow-editor/steps/sms/sms-editor.tsx create mode 100644 apps/dashboard/src/components/workflow-editor/steps/sms/sms-phone.tsx create mode 100644 apps/dashboard/src/components/workflow-editor/steps/sms/sms-preview.tsx create mode 100644 apps/dashboard/src/components/workflow-editor/steps/sms/sms-tabs-section.tsx create mode 100644 apps/dashboard/src/components/workflow-editor/steps/sms/sms-tabs.tsx diff --git a/apps/dashboard/public/images/phones/iphone-sms.svg b/apps/dashboard/public/images/phones/iphone-sms.svg new file mode 100644 index 00000000000..09521b553bf --- /dev/null +++ b/apps/dashboard/public/images/phones/iphone-sms.svg @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/dashboard/src/components/workflow-editor/add-step-menu.tsx b/apps/dashboard/src/components/workflow-editor/add-step-menu.tsx index 1474e8b9d00..46fffdf9f17 100644 --- a/apps/dashboard/src/components/workflow-editor/add-step-menu.tsx +++ b/apps/dashboard/src/components/workflow-editor/add-step-menu.tsx @@ -1,15 +1,15 @@ +import { useFeatureFlag } from '@/hooks/use-feature-flag'; +import { STEP_TYPE_TO_COLOR } from '@/utils/color'; +import { StepTypeEnum } from '@/utils/enums'; +import { cn } from '@/utils/ui'; +import { FeatureFlagsKeysEnum } from '@novu/shared'; +import { PopoverPortal } from '@radix-ui/react-popover'; import React, { ReactNode, useState } from 'react'; import { RiAddLine } from 'react-icons/ri'; -import { PopoverPortal } from '@radix-ui/react-popover'; -import { Node } from './base-node'; -import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover'; import { STEP_TYPE_TO_ICON } from '../icons/utils'; import { Badge } from '../primitives/badge'; -import { cn } from '@/utils/ui'; -import { StepTypeEnum } from '@/utils/enums'; -import { STEP_TYPE_TO_COLOR } from '@/utils/color'; -import { useFeatureFlag } from '@/hooks/use-feature-flag'; -import { FeatureFlagsKeysEnum } from '@novu/shared'; +import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover'; +import { Node } from './base-node'; const noop = () => {}; @@ -132,7 +132,13 @@ export const AddStepMenu = ({ Push Chat - SMS + handleMenuItemClick(StepTypeEnum.SMS)} + > + SMS + diff --git a/apps/dashboard/src/components/workflow-editor/steps/component-utils.tsx b/apps/dashboard/src/components/workflow-editor/steps/component-utils.tsx index 27e630e55f2..9b0a22ea798 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/component-utils.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/component-utils.tsx @@ -1,15 +1,15 @@ import { UiComponentEnum } from '@novu/shared'; -import { InAppAction } from '@/components/workflow-editor/steps/in-app/in-app-action'; -import { InAppSubject } from '@/components/workflow-editor/steps/in-app/in-app-subject'; -import { InAppBody } from '@/components/workflow-editor/steps/in-app/in-app-body'; -import { InAppAvatar } from '@/components/workflow-editor/steps/in-app/in-app-avatar'; -import { InAppRedirect } from '@/components/workflow-editor/steps/in-app/in-app-redirect'; import { DelayAmount } from '@/components/workflow-editor/steps/delay/delay-amount'; -import { Maily } from '@/components/workflow-editor/steps/email/maily'; -import { EmailSubject } from '@/components/workflow-editor/steps/email/email-subject'; import { DigestKey } from '@/components/workflow-editor/steps/digest/digest-key'; import { DigestWindow } from '@/components/workflow-editor/steps/digest/digest-window'; +import { EmailSubject } from '@/components/workflow-editor/steps/email/email-subject'; +import { Maily } from '@/components/workflow-editor/steps/email/maily'; +import { InAppAction } from '@/components/workflow-editor/steps/in-app/in-app-action'; +import { InAppAvatar } from '@/components/workflow-editor/steps/in-app/in-app-avatar'; +import { InAppBody } from '@/components/workflow-editor/steps/in-app/in-app-body'; +import { InAppRedirect } from '@/components/workflow-editor/steps/in-app/in-app-redirect'; +import { InAppSubject } from '@/components/workflow-editor/steps/in-app/in-app-subject'; import { BaseBody } from './base/base-body'; import { BaseSubject } from './base/base-subject'; @@ -55,7 +55,9 @@ export const getComponentByType = ({ component }: { component?: UiComponentEnum case UiComponentEnum.PUSH_SUBJECT: { return ; } - + case UiComponentEnum.SMS_BODY: { + return ; + } default: { return null; } diff --git a/apps/dashboard/src/components/workflow-editor/steps/configure-step-form.tsx b/apps/dashboard/src/components/workflow-editor/steps/configure-step-form.tsx index c66bfa63ab9..be8ee2174aa 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/configure-step-form.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/configure-step-form.tsx @@ -9,12 +9,12 @@ import { WorkflowOriginEnum, WorkflowResponseDto, } from '@novu/shared'; +import merge from 'lodash.merge'; import { AnimatePresence, motion } from 'motion/react'; -import { useEffect, useCallback, useMemo, useState, HTMLAttributes, ReactNode } from 'react'; +import { HTMLAttributes, ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; import { useForm } from 'react-hook-form'; import { RiArrowLeftSLine, RiArrowRightSLine, RiCloseFill, RiDeleteBin2Line, RiPencilRuler2Fill } from 'react-icons/ri'; import { Link, useNavigate } from 'react-router-dom'; -import merge from 'lodash.merge'; import { ConfirmationModal } from '@/components/confirmation-modal'; import { PageMeta } from '@/components/page-meta'; @@ -32,24 +32,24 @@ import { getFirstControlsErrorMessage, updateStepInWorkflow, } from '@/components/workflow-editor/step-utils'; +import { ConfigureStepTemplateIssueCta } from '@/components/workflow-editor/steps/configure-step-template-issue-cta'; +import { DelayControlValues } from '@/components/workflow-editor/steps/delay/delay-control-values'; +import { DigestControlValues } from '@/components/workflow-editor/steps/digest/digest-control-values'; +import { ConfigureEmailStepPreview } from '@/components/workflow-editor/steps/email/configure-email-step-preview'; +import { ConfigureInAppStepPreview } from '@/components/workflow-editor/steps/in-app/configure-in-app-step-preview'; +import { SaveFormContext } from '@/components/workflow-editor/steps/save-form-context'; import { SdkBanner } from '@/components/workflow-editor/steps/sdk-banner'; -import { buildRoute, ROUTES } from '@/utils/routes'; +import { ConfigureSmsStepPreview } from '@/components/workflow-editor/steps/sms/configure-sms-step-preview'; +import { useFeatureFlag } from '@/hooks/use-feature-flag'; +import { useFormAutosave } from '@/hooks/use-form-autosave'; import { AUTOCOMPLETE_PASSWORD_MANAGERS_OFF, INLINE_CONFIGURABLE_STEP_TYPES, - TEMPLATE_CONFIGURABLE_STEP_TYPES, STEP_TYPE_LABELS, + TEMPLATE_CONFIGURABLE_STEP_TYPES, } from '@/utils/constants'; -import { useFormAutosave } from '@/hooks/use-form-autosave'; -import { buildDefaultValuesOfDataSchema, buildDynamicZodSchema } from '@/utils/schema'; -import { buildDefaultValues } from '@/utils/schema'; -import { DelayControlValues } from '@/components/workflow-editor/steps/delay/delay-control-values'; -import { ConfigureStepTemplateIssueCta } from '@/components/workflow-editor/steps/configure-step-template-issue-cta'; -import { ConfigureInAppStepPreview } from '@/components/workflow-editor/steps/in-app/configure-in-app-step-preview'; -import { ConfigureEmailStepPreview } from '@/components/workflow-editor/steps/email/configure-email-step-preview'; -import { useFeatureFlag } from '@/hooks/use-feature-flag'; -import { DigestControlValues } from '@/components/workflow-editor/steps/digest/digest-control-values'; -import { SaveFormContext } from '@/components/workflow-editor/steps/save-form-context'; +import { buildRoute, ROUTES } from '@/utils/routes'; +import { buildDefaultValues, buildDefaultValuesOfDataSchema, buildDynamicZodSchema } from '@/utils/schema'; const STEP_TYPE_TO_INLINE_CONTROL_VALUES: Record React.JSX.Element | null> = { [StepTypeEnum.DELAY]: DelayControlValues, @@ -66,7 +66,7 @@ const STEP_TYPE_TO_INLINE_CONTROL_VALUES: Record React.JSX.E const STEP_TYPE_TO_PREVIEW: Record) => ReactNode) | null> = { [StepTypeEnum.IN_APP]: ConfigureInAppStepPreview, [StepTypeEnum.EMAIL]: ConfigureEmailStepPreview, - [StepTypeEnum.SMS]: null, + [StepTypeEnum.SMS]: ConfigureSmsStepPreview, [StepTypeEnum.CHAT]: null, [StepTypeEnum.PUSH]: null, [StepTypeEnum.CUSTOM]: null, diff --git a/apps/dashboard/src/components/workflow-editor/steps/configure-step-template-form.tsx b/apps/dashboard/src/components/workflow-editor/steps/configure-step-template-form.tsx index bcdc813ae36..21ca629e138 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/configure-step-template-form.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/configure-step-template-form.tsx @@ -1,7 +1,3 @@ -import { useCallback, useEffect, useMemo } from 'react'; -import merge from 'lodash.merge'; -import isEqual from 'lodash.isequal'; -import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { type StepDataDto, @@ -10,23 +6,28 @@ import { UpdateWorkflowDto, type WorkflowResponseDto, } from '@novu/shared'; +import isEqual from 'lodash.isequal'; +import merge from 'lodash.merge'; +import { useCallback, useEffect, useMemo } from 'react'; +import { useForm } from 'react-hook-form'; +import { Form } from '@/components/primitives/form/form'; import { flattenIssues, updateStepInWorkflow } from '@/components/workflow-editor/step-utils'; +import { EmailTabs } from '@/components/workflow-editor/steps/email/email-tabs'; import { InAppTabs } from '@/components/workflow-editor/steps/in-app/in-app-tabs'; -import { buildDefaultValues, buildDefaultValuesOfDataSchema, buildDynamicZodSchema } from '@/utils/schema'; -import { OtherStepTabs } from './other-steps-tabs'; -import { Form } from '@/components/primitives/form/form'; -import { useFormAutosave } from '@/hooks/use-form-autosave'; +import { PushTabs } from '@/components/workflow-editor/steps/push/push-tabs'; import { SaveFormContext } from '@/components/workflow-editor/steps/save-form-context'; -import { EmailTabs } from '@/components/workflow-editor/steps/email/email-tabs'; +import { SmsTabs } from '@/components/workflow-editor/steps/sms/sms-tabs'; +import { useFormAutosave } from '@/hooks/use-form-autosave'; +import { buildDefaultValues, buildDefaultValuesOfDataSchema, buildDynamicZodSchema } from '@/utils/schema'; import { CommonCustomControlValues } from './common/common-custom-control-values'; -import { PushTabs } from '@/components/workflow-editor/steps/push/push-tabs'; +import { OtherStepTabs } from './other-steps-tabs'; const STEP_TYPE_TO_TEMPLATE_FORM: Record React.JSX.Element | null> = { [StepTypeEnum.EMAIL]: EmailTabs, [StepTypeEnum.CHAT]: OtherStepTabs, [StepTypeEnum.IN_APP]: InAppTabs, - [StepTypeEnum.SMS]: OtherStepTabs, + [StepTypeEnum.SMS]: SmsTabs, [StepTypeEnum.PUSH]: PushTabs, [StepTypeEnum.DIGEST]: CommonCustomControlValues, [StepTypeEnum.DELAY]: CommonCustomControlValues, diff --git a/apps/dashboard/src/components/workflow-editor/steps/sms/configure-sms-step-preview.tsx b/apps/dashboard/src/components/workflow-editor/steps/sms/configure-sms-step-preview.tsx new file mode 100644 index 00000000000..69aa09bf82a --- /dev/null +++ b/apps/dashboard/src/components/workflow-editor/steps/sms/configure-sms-step-preview.tsx @@ -0,0 +1,35 @@ +import * as Sentry from '@sentry/react'; +import { useEffect } from 'react'; +import { useParams } from 'react-router-dom'; + +import { SmsPreview } from '@/components/workflow-editor/steps/sms/sms-preview'; +import { useWorkflow } from '@/components/workflow-editor/workflow-provider'; +import { usePreviewStep } from '@/hooks/use-preview-step'; + +export const ConfigureSmsStepPreview = () => { + const { + previewStep, + data: previewData, + isPending: isPreviewPending, + } = usePreviewStep({ + onError: (error) => Sentry.captureException(error), + }); + const { step, isPending } = useWorkflow(); + + const { workflowSlug, stepSlug } = useParams<{ + workflowSlug: string; + stepSlug: string; + }>(); + + useEffect(() => { + if (!workflowSlug || !stepSlug || !step || isPending) return; + + previewStep({ + workflowSlug, + stepSlug, + previewData: { controlValues: step.controls.values, previewPayload: {} }, + }); + }, [workflowSlug, stepSlug, previewStep, step, isPending]); + + return ; +}; diff --git a/apps/dashboard/src/components/workflow-editor/steps/sms/sms-editor-preview.tsx b/apps/dashboard/src/components/workflow-editor/steps/sms/sms-editor-preview.tsx new file mode 100644 index 00000000000..017e6853115 --- /dev/null +++ b/apps/dashboard/src/components/workflow-editor/steps/sms/sms-editor-preview.tsx @@ -0,0 +1,111 @@ +import { type StepDataDto, type WorkflowResponseDto } from '@novu/shared'; +import { CSSProperties, useEffect, useRef, useState } from 'react'; + +import { Notification5Fill } from '@/components/icons'; +import { Code2 } from '@/components/icons/code-2'; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/primitives/accordion'; +import { Button } from '@/components/primitives/button'; +import { Editor } from '@/components/primitives/editor'; +import { InAppTabsSection } from '@/components/workflow-editor/steps/in-app/in-app-tabs-section'; +import { SmsPreview } from '@/components/workflow-editor/steps/sms/sms-preview'; +import { loadLanguage } from '@uiw/codemirror-extensions-langs'; +import { useEditorPreview } from '../use-editor-preview'; + +const getInitialAccordionValue = (value: string) => { + try { + return Object.keys(JSON.parse(value)).length > 0 ? 'payload' : undefined; + } catch (e) { + return undefined; + } +}; + +type SmsEditorPreviewProps = { + workflow: WorkflowResponseDto; + step: StepDataDto; + formValues: Record; +}; + +const extensions = [loadLanguage('json')?.extension ?? []]; + +export const SmsEditorPreview = ({ workflow, step, formValues }: SmsEditorPreviewProps) => { + const workflowSlug = workflow.workflowId; + const stepSlug = step.stepId; + const { editorValue, setEditorValue, previewStep, previewData, isPreviewPending } = useEditorPreview({ + workflowSlug, + stepSlug, + controlValues: formValues, + }); + const [accordionValue, setAccordionValue] = useState(getInitialAccordionValue(editorValue)); + const [payloadError, setPayloadError] = useState(''); + const [height, setHeight] = useState(0); + const contentRef = useRef(null); + + useEffect(() => { + setAccordionValue(getInitialAccordionValue(editorValue)); + }, [editorValue]); + + useEffect(() => { + const timeout = setTimeout(() => { + if (contentRef.current) { + const rect = contentRef.current.getBoundingClientRect(); + setHeight(rect.height); + } + }, 0); + + return () => clearTimeout(timeout); + }, [editorValue]); + + return ( + +
+
+ + SMS template editor +
+
+ +
+ + + +
+ + Configure preview +
+
+ + + {payloadError &&

{payloadError}

} + +
+
+
+
+
+ ); +}; diff --git a/apps/dashboard/src/components/workflow-editor/steps/sms/sms-editor.tsx b/apps/dashboard/src/components/workflow-editor/steps/sms/sms-editor.tsx new file mode 100644 index 00000000000..246a88a4b2f --- /dev/null +++ b/apps/dashboard/src/components/workflow-editor/steps/sms/sms-editor.tsx @@ -0,0 +1,19 @@ +import { getComponentByType } from '@/components/workflow-editor/steps/component-utils'; +import { SmsTabsSection } from '@/components/workflow-editor/steps/sms/sms-tabs-section'; +import { type UiSchema } from '@novu/shared'; + +type SmsEditorProps = { uiSchema: UiSchema }; +export const SmsEditor = (props: SmsEditorProps) => { + const { uiSchema } = props; + const { body } = uiSchema.properties ?? {}; + + return ( +
+ +
+ {getComponentByType({ component: body.component })} +
+
+
+ ); +}; diff --git a/apps/dashboard/src/components/workflow-editor/steps/sms/sms-phone.tsx b/apps/dashboard/src/components/workflow-editor/steps/sms/sms-phone.tsx new file mode 100644 index 00000000000..962f83c477e --- /dev/null +++ b/apps/dashboard/src/components/workflow-editor/steps/sms/sms-phone.tsx @@ -0,0 +1,61 @@ +import { motion } from 'motion/react'; + +const SmsChatBubble = ({ children }: { children: React.ReactNode }) => ( + +
{children}
+
+); + +const ErrorChatBubble = ({ children }: { children: React.ReactNode }) => ( + +
{children}
+
+); + +const TypingIndicator = () => ( + +
+
+
+
+
+
+); + +export const SmsPhone = ({ + smsBody, + isLoading = false, + error = false, +}: { + smsBody: string; + isLoading?: boolean; + error?: boolean; +}) => ( +
+
+ {isLoading ? ( + + ) : error ? ( + {smsBody} + ) : ( + {smsBody} + )} +
+ SMS Phone +
+); diff --git a/apps/dashboard/src/components/workflow-editor/steps/sms/sms-preview.tsx b/apps/dashboard/src/components/workflow-editor/steps/sms/sms-preview.tsx new file mode 100644 index 00000000000..6a8d1d41a94 --- /dev/null +++ b/apps/dashboard/src/components/workflow-editor/steps/sms/sms-preview.tsx @@ -0,0 +1,44 @@ +import { SmsPhone } from '@/components/workflow-editor/steps/sms/sms-phone'; +import { ChannelTypeEnum, type GeneratePreviewResponseDto } from '@novu/shared'; +import { ReactNode } from 'react'; + +const SmsPreviewContainer = ({ children }: { children: ReactNode }) => { + return
{children}
; +}; + +export const SmsPreview = ({ + isPreviewPending, + previewData, +}: { + isPreviewPending: boolean; + previewData?: GeneratePreviewResponseDto; +}) => { + const previewResult = previewData?.result; + + if (isPreviewPending || previewData === undefined) { + return ( + + + + ); + } + + const isValidSmsPreview = + previewResult && previewResult.type === ChannelTypeEnum.SMS && previewResult.preview.body.length > 0; + + if (!isValidSmsPreview) { + return ( + + + + ); + } + + const smsBody = previewResult.preview.body; + + return ( + + + + ); +}; diff --git a/apps/dashboard/src/components/workflow-editor/steps/sms/sms-tabs-section.tsx b/apps/dashboard/src/components/workflow-editor/steps/sms/sms-tabs-section.tsx new file mode 100644 index 00000000000..c5968c0b72c --- /dev/null +++ b/apps/dashboard/src/components/workflow-editor/steps/sms/sms-tabs-section.tsx @@ -0,0 +1,8 @@ +import { cn } from '@/utils/ui'; +import { HTMLAttributes } from 'react'; + +type SmsTabsSectionProps = HTMLAttributes; +export const SmsTabsSection = (props: SmsTabsSectionProps) => { + const { className, ...rest } = props; + return
; +}; diff --git a/apps/dashboard/src/components/workflow-editor/steps/sms/sms-tabs.tsx b/apps/dashboard/src/components/workflow-editor/steps/sms/sms-tabs.tsx new file mode 100644 index 00000000000..5bda1b6e89f --- /dev/null +++ b/apps/dashboard/src/components/workflow-editor/steps/sms/sms-tabs.tsx @@ -0,0 +1,36 @@ +import { StepEditorProps } from '@/components/workflow-editor/steps/configure-step-template-form'; +import { CustomStepControls } from '@/components/workflow-editor/steps/controls/custom-step-controls'; +import { SmsEditor } from '@/components/workflow-editor/steps/sms/sms-editor'; +import { SmsEditorPreview } from '@/components/workflow-editor/steps/sms/sms-editor-preview'; +import { TemplateTabs } from '@/components/workflow-editor/steps/template-tabs'; +import { WorkflowOriginEnum } from '@novu/shared'; +import { useState } from 'react'; +import { useFormContext } from 'react-hook-form'; + +export const SmsTabs = (props: StepEditorProps) => { + const { workflow, step } = props; + const { dataSchema, uiSchema } = step.controls; + const form = useFormContext(); + const [tabsValue, setTabsValue] = useState('editor'); + + const isNovuCloud = workflow.origin === WorkflowOriginEnum.NOVU_CLOUD && uiSchema; + const isExternal = workflow.origin === WorkflowOriginEnum.EXTERNAL; + + const editorContent = ( + <> + {isNovuCloud && } + {isExternal && } + + ); + + const previewContent = ; + + return ( + + ); +};