Skip to content

Commit

Permalink
feat(dashboard): sms step
Browse files Browse the repository at this point in the history
  • Loading branch information
ChmaraX committed Dec 17, 2024
1 parent 75af9d3 commit 9d42e7f
Show file tree
Hide file tree
Showing 12 changed files with 581 additions and 43 deletions.
215 changes: 215 additions & 0 deletions apps/dashboard/public/images/phones/iphone-sms.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 15 additions & 9 deletions apps/dashboard/src/components/workflow-editor/add-step-menu.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => {};

Expand Down Expand Up @@ -132,7 +132,13 @@ export const AddStepMenu = ({
Push
</MenuItem>
<MenuItem stepType={StepTypeEnum.CHAT}>Chat</MenuItem>
<MenuItem stepType={StepTypeEnum.SMS}>SMS</MenuItem>
<MenuItem
stepType={StepTypeEnum.SMS}
disabled={!arePushChatSMSEnabled}
onClick={() => handleMenuItemClick(StepTypeEnum.SMS)}
>
SMS
</MenuItem>
</MenuItemsGroup>
</MenuGroup>
<MenuGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -55,7 +55,9 @@ export const getComponentByType = ({ component }: { component?: UiComponentEnum
case UiComponentEnum.PUSH_SUBJECT: {
return <BaseSubject />;
}

case UiComponentEnum.SMS_BODY: {
return <BaseBody />;
}
default: {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<StepTypeEnum, () => React.JSX.Element | null> = {
[StepTypeEnum.DELAY]: DelayControlValues,
Expand All @@ -66,7 +66,7 @@ const STEP_TYPE_TO_INLINE_CONTROL_VALUES: Record<StepTypeEnum, () => React.JSX.E
const STEP_TYPE_TO_PREVIEW: Record<StepTypeEnum, ((props: HTMLAttributes<HTMLDivElement>) => ReactNode) | null> = {
[StepTypeEnum.IN_APP]: ConfigureInAppStepPreview,
[StepTypeEnum.EMAIL]: ConfigureEmailStepPreview,
[StepTypeEnum.SMS]: null,
[StepTypeEnum.SMS]: ConfigureSmsStepPreview,
[StepTypeEnum.CHAT]: null,
[StepTypeEnum.PUSH]: null,
[StepTypeEnum.CUSTOM]: null,
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<StepTypeEnum, (args: StepEditorProps) => 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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <SmsPreview isPreviewPending={isPreviewPending} previewData={previewData} />;
};
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
};

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<string | undefined>(getInitialAccordionValue(editorValue));
const [payloadError, setPayloadError] = useState('');
const [height, setHeight] = useState(0);
const contentRef = useRef<HTMLDivElement>(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 (
<InAppTabsSection>
<div className="relative flex flex-col gap-3">
<div className="flex items-center gap-2.5 text-sm font-medium">
<Notification5Fill className="size-3" />
SMS template editor
</div>
<div className="flex flex-col items-center justify-center gap-2">
<SmsPreview isPreviewPending={isPreviewPending} previewData={previewData} />
</div>
<Accordion type="single" collapsible value={accordionValue} onValueChange={setAccordionValue}>
<AccordionItem value="payload">
<AccordionTrigger>
<div className="flex items-center gap-1">
<Code2 className="size-5" />
Configure preview
</div>
</AccordionTrigger>
<AccordionContent
ref={contentRef}
className="flex flex-col gap-2"
style={{ '--radix-collapsible-content-height': `${height}px` } as CSSProperties}
>
<Editor
value={editorValue}
onChange={setEditorValue}
lang="json"
extensions={extensions}
className="border-neutral-alpha-200 bg-background text-foreground-600 mx-0 mt-0 rounded-lg border border-dashed p-3"
/>
{payloadError && <p className="text-destructive text-xs">{payloadError}</p>}
<Button
size="xs"
type="button"
variant="outline"
className="self-end"
onClick={() => {
try {
previewStep();
setPayloadError('');
} catch (e) {
setPayloadError(String(e));
}
}}
>
Apply
</Button>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</InAppTabsSection>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex h-full flex-col">
<SmsTabsSection className="py-5">
<div className="flex flex-col gap-1 rounded-lg border border-neutral-100 p-1">
{getComponentByType({ component: body.component })}
</div>
</SmsTabsSection>
</div>
);
};
Loading

0 comments on commit 9d42e7f

Please sign in to comment.