diff --git a/apps/dashboard/public/images/phones/iphone-push.svg b/apps/dashboard/public/images/phones/iphone-push.svg new file mode 100644 index 00000000000..491443825fe --- /dev/null +++ b/apps/dashboard/public/images/phones/iphone-push.svg @@ -0,0 +1 @@ + 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 be8ee2174aa..1ac0f911535 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 @@ -50,6 +50,7 @@ import { } from '@/utils/constants'; import { buildRoute, ROUTES } from '@/utils/routes'; import { buildDefaultValues, buildDefaultValuesOfDataSchema, buildDynamicZodSchema } from '@/utils/schema'; +import { ConfigurePushStepPreview } from './push/configure-push-step-preview'; const STEP_TYPE_TO_INLINE_CONTROL_VALUES: Record React.JSX.Element | null> = { [StepTypeEnum.DELAY]: DelayControlValues, @@ -68,7 +69,7 @@ const STEP_TYPE_TO_PREVIEW: Record { + 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/push/push-editor-preview.tsx b/apps/dashboard/src/components/workflow-editor/steps/push/push-editor-preview.tsx new file mode 100644 index 00000000000..c9031dfc436 --- /dev/null +++ b/apps/dashboard/src/components/workflow-editor/steps/push/push-editor-preview.tsx @@ -0,0 +1,116 @@ +import { CSSProperties, useEffect, useRef, useState } from 'react'; +import { type StepDataDto, type WorkflowResponseDto } from '@novu/shared'; +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 { loadLanguage } from '@uiw/codemirror-extensions-langs'; +import { useEditorPreview } from '../use-editor-preview'; +import { PushPreview } from './push-preview'; +import { RiCellphoneFill } from 'react-icons/ri'; +import { PushTabsSection } from './push-tabs-section'; + +const getInitialAccordionValue = (value: string) => { + try { + return Object.keys(JSON.parse(value)).length > 0 ? 'payload' : undefined; + } catch (e) { + return undefined; + } +}; + +type PushEditorPreviewProps = { + workflow: WorkflowResponseDto; + step: StepDataDto; + formValues: Record; +}; + +const extensions = [loadLanguage('json')?.extension ?? []]; + +export const PushEditorPreview = ({ workflow, step, formValues }: PushEditorPreviewProps) => { + 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 ( + + + + + Push template editor + + + + + + + This preview shows how your message will appear on mobile. Actual rendering may vary by device. + + + + + + + + + Configure preview + + + + + {payloadError && {payloadError}} + { + try { + previewStep(); + setPayloadError(''); + } catch (e) { + setPayloadError(String(e)); + } + }} + > + Apply + + + + + + + ); +}; diff --git a/apps/dashboard/src/components/workflow-editor/steps/push/push-preview.tsx b/apps/dashboard/src/components/workflow-editor/steps/push/push-preview.tsx new file mode 100644 index 00000000000..b2a09e7cb28 --- /dev/null +++ b/apps/dashboard/src/components/workflow-editor/steps/push/push-preview.tsx @@ -0,0 +1,129 @@ +import { HTMLAttributes } from 'react'; +import { HTMLMotionProps, motion } from 'motion/react'; +import { ChannelTypeEnum, GeneratePreviewResponseDto } from '@novu/shared'; +import { Skeleton } from '@/components/primitives/skeleton'; +import { cn } from '@/utils/ui'; + +export function PushPreview({ + isPreviewPending, + previewData, +}: { + isPreviewPending: boolean; + previewData?: GeneratePreviewResponseDto; +}) { + if (isPreviewPending) { + return ( + + + + + + + + + ); + } + + if (previewData?.result.type !== ChannelTypeEnum.PUSH) { + return ( + + + + + + + + ); + } + + return ( + + + + + + + + + + + ); +} + +type PushSubjectPreviewProps = HTMLAttributes & { + subject?: string; + isPending: boolean; +}; +export const PushSubjectPreview = ({ subject, isPending, className, ...rest }: PushSubjectPreviewProps) => { + if (isPending) { + return ; + } + + return ( + + + {subject} + + now + + ); +}; + +type PushBodyPreviewProps = HTMLAttributes & { + body?: string; + isPending: boolean; +}; +export const PushBodyPreview = ({ body, isPending, className, ...rest }: PushBodyPreviewProps) => { + if (isPending) { + return ( + + + + + ); + } + + return ( + + {body} + + ); +}; + +export const PushContentContainerPreview = ({ children, className, ...rest }: HTMLMotionProps<'div'>) => { + return ( + + {children} + + ); +}; + +export const PushBackgroundWithPhone = ({ children, className, ...rest }: HTMLAttributes) => { + return ( + + {children} + + ); +}; + +export const PushNotificationContainer = ({ children, className, ...rest }: HTMLAttributes) => { + return ( + + {children} + + ); +}; diff --git a/apps/dashboard/src/components/workflow-editor/steps/push/push-tabs.tsx b/apps/dashboard/src/components/workflow-editor/steps/push/push-tabs.tsx index 44bde28b18b..ad9fb2a1e39 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/push/push-tabs.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/push/push-tabs.tsx @@ -4,12 +4,14 @@ import { StepEditorProps } from '@/components/workflow-editor/steps/configure-st import { PushEditor } from '@/components/workflow-editor/steps/push/push-editor'; import { CustomStepControls } from '../controls/custom-step-controls'; import { TemplateTabs } from '../template-tabs'; +import { PushEditorPreview } from './push-editor-preview'; +import { useFormContext } from 'react-hook-form'; export const PushTabs = (props: StepEditorProps) => { const { workflow, step } = props; const { dataSchema, uiSchema } = step.controls; const [tabsValue, setTabsValue] = useState('editor'); - + const form = useFormContext(); const isNovuCloud = workflow.origin === WorkflowOriginEnum.NOVU_CLOUD && uiSchema; const isExternal = workflow.origin === WorkflowOriginEnum.EXTERNAL; @@ -20,7 +22,7 @@ export const PushTabs = (props: StepEditorProps) => { > ); - const previewContent = <>TODO>; + const previewContent = ; return (
{payloadError}