Skip to content

Commit

Permalink
refactor(dashboard): unify preview accordion component and preserve c…
Browse files Browse the repository at this point in the history
…hanges on tab switch (#7353)
  • Loading branch information
scopsy authored Dec 23, 2024
1 parent fd9293f commit 65ea928
Show file tree
Hide file tree
Showing 14 changed files with 272 additions and 460 deletions.
Original file line number Diff line number Diff line change
@@ -1,61 +1,26 @@
import { CSSProperties, useEffect, useRef, useState } from 'react';
import { RiChat1Fill } from 'react-icons/ri';
import { type StepDataDto, type WorkflowResponseDto } from '@novu/shared';
import { GeneratePreviewResponseDto } 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 '@/components/workflow-editor/steps/use-editor-preview';
import { ChatPreview } from '@/components/workflow-editor/steps/chat/chat-preview';
import { TabsSection } from '@/components/workflow-editor/steps/tabs-section';
import { InlineToast } from '@/components/primitives/inline-toast';

const getInitialAccordionValue = (value: string) => {
try {
return Object.keys(JSON.parse(value)).length > 0 ? 'payload' : undefined;
} catch (e) {
return undefined;
}
};
import { ConfigurePreviewAccordion } from '../shared/configure-preview-accordion';

type ChatEditorPreviewProps = {
workflow: WorkflowResponseDto;
step: StepDataDto;
formValues: Record<string, unknown>;
editorValue: string;
setEditorValue: (value: string) => void;
previewStep: () => void;
previewData?: GeneratePreviewResponseDto;
isPreviewPending: boolean;
};

const extensions = [loadLanguage('json')?.extension ?? []];

export const ChatEditorPreview = ({ workflow, step, formValues }: ChatEditorPreviewProps) => {
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]);

export const ChatEditorPreview = ({
editorValue,
setEditorValue,
previewStep,
previewData,
isPreviewPending = false,
}: ChatEditorPreviewProps) => {
return (
<TabsSection>
<div className="relative flex flex-col gap-3">
Expand All @@ -70,46 +35,7 @@ export const ChatEditorPreview = ({ workflow, step, formValues }: ChatEditorPrev
className="w-full px-3"
/>
</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>
<ConfigurePreviewAccordion editorValue={editorValue} setEditorValue={setEditorValue} onUpdate={previewStep} />
</div>
</TabsSection>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CustomStepControls } from '@/components/workflow-editor/steps/controls/
import { TemplateTabs } from '@/components/workflow-editor/steps/template-tabs';
import { ChatEditor } from '@/components/workflow-editor/steps/chat/chat-editor';
import { ChatEditorPreview } from '@/components/workflow-editor/steps/chat/chat-editor-preview';
import { useEditorPreview } from '../use-editor-preview';

export const ChatTabs = (props: StepEditorProps) => {
const { workflow, step } = props;
Expand All @@ -16,21 +17,36 @@ export const ChatTabs = (props: StepEditorProps) => {
const isNovuCloud = !!(workflow.origin === WorkflowOriginEnum.NOVU_CLOUD && uiSchema);
const isExternal = workflow.origin === WorkflowOriginEnum.EXTERNAL;

const { editorValue, setEditorValue, previewStep, previewData, isPreviewPending } = useEditorPreview({
workflowSlug: workflow.workflowId,
stepSlug: step.stepId,
controlValues: form.getValues(),
});

const editorContent = (
<>
{isNovuCloud && <ChatEditor uiSchema={uiSchema} />}
{isExternal && <CustomStepControls dataSchema={dataSchema} origin={workflow.origin} />}
</>
);

const previewContent = <ChatEditorPreview workflow={workflow} step={step} formValues={form.getValues()} />;
const previewContent = (
<ChatEditorPreview
editorValue={editorValue}
setEditorValue={setEditorValue}
previewStep={previewStep}
previewData={previewData}
isPreviewPending={isPreviewPending}
/>
);

return (
<TemplateTabs
editorContent={editorContent}
previewContent={previewContent}
tabsValue={tabsValue}
onTabChange={setTabsValue}
previewStep={previewStep}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { ChannelTypeEnum, type StepDataDto, type WorkflowResponseDto } from '@novu/shared';
import { CSSProperties, useEffect, useRef, useState } from 'react';
import { ChannelTypeEnum, GeneratePreviewResponseDto } from '@novu/shared';
import { useState } from 'react';
import { RiMacLine, RiSmartphoneFill } from 'react-icons/ri';

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 { Skeleton } from '@/components/primitives/skeleton';
import { Tabs, TabsList, TabsTrigger } from '@/components/primitives/tabs';
import {
EmailPreviewBody,
EmailPreviewBodyMobile,
Expand All @@ -16,54 +11,28 @@ import {
EmailPreviewSubjectMobile,
} from '@/components/workflow-editor/steps/email/email-preview';
import { EmailTabsSection } from '@/components/workflow-editor/steps/email/email-tabs-section';
import { Tabs, TabsList, TabsTrigger } from '@/components/primitives/tabs';
import { TabsContent } from '@radix-ui/react-tabs';
import { loadLanguage } from '@uiw/codemirror-extensions-langs';
import { RiMacLine, RiSmartphoneFill } from 'react-icons/ri';
import { useEditorPreview } from '../use-editor-preview';
import { Separator } from '@/components/primitives/separator';

const getInitialAccordionValue = (value: string) => {
try {
return Object.keys(JSON.parse(value)).length > 0 ? 'payload' : undefined;
} catch (e) {
return undefined;
}
};
import { Skeleton } from '@/components/primitives/skeleton';
import { ConfigurePreviewAccordion } from '../shared/configure-preview-accordion';

type EmailEditorPreviewProps = {
workflow: WorkflowResponseDto;
step: StepDataDto;
formValues: Record<string, unknown>;
editorValue: string;
setEditorValue: (value: string) => void;
previewStep: () => void;
previewData?: GeneratePreviewResponseDto;
isPreviewPending: boolean;
};

export const EmailEditorPreview = ({ workflow, step, formValues }: EmailEditorPreviewProps) => {
const workflowSlug = workflow.workflowId;
const stepSlug = step.stepId;
const { editorValue, setEditorValue, isPreviewPending, previewData, previewStep } = useEditorPreview({
workflowSlug,
stepSlug,
controlValues: formValues,
});
const [accordionValue, setAccordionValue] = useState<string | undefined>(getInitialAccordionValue(editorValue));
const [payloadError, setPayloadError] = useState('');
const [height, setHeight] = useState(0);
export const EmailEditorPreview = ({
editorValue,
setEditorValue,
previewStep,
previewData,
isPreviewPending = false,
}: EmailEditorPreviewProps) => {
const [activeTab, setActiveTab] = useState('desktop');
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 (
<Tabs value={activeTab} onValueChange={setActiveTab}>
Expand Down Expand Up @@ -115,46 +84,7 @@ export const EmailEditorPreview = ({ workflow, step, formValues }: EmailEditorPr
</>
)}
<EmailTabsSection>
<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={[loadLanguage('json')?.extension ?? []]}
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>
<ConfigurePreviewAccordion editorValue={editorValue} setEditorValue={setEditorValue} onUpdate={previewStep} />
</EmailTabsSection>
</div>
</Tabs>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { EmailEditorPreview } from '@/components/workflow-editor/steps/email/ema
import { CustomStepControls } from '../controls/custom-step-controls';
import { StepEditorProps } from '@/components/workflow-editor/steps/configure-step-template-form';
import { TemplateTabs } from '@/components/workflow-editor/steps/template-tabs';
import { useEditorPreview } from '../use-editor-preview';

export const EmailTabs = (props: StepEditorProps) => {
const { workflow, step } = props;
Expand All @@ -16,17 +17,32 @@ export const EmailTabs = (props: StepEditorProps) => {
const isNovuCloud = workflow.origin === WorkflowOriginEnum.NOVU_CLOUD && uiSchema;
const isExternal = workflow.origin === WorkflowOriginEnum.EXTERNAL;

const { editorValue, setEditorValue, previewStep, previewData, isPreviewPending } = useEditorPreview({
workflowSlug: workflow.workflowId,
stepSlug: step.stepId,
controlValues: form.getValues(),
});

const editorContent = (
<>
{isNovuCloud && <EmailEditor uiSchema={uiSchema} />}
{isExternal && <CustomStepControls dataSchema={dataSchema} origin={workflow.origin} />}
</>
);

const previewContent = <EmailEditorPreview workflow={workflow} step={step} formValues={form.getValues()} />;
const previewContent = (
<EmailEditorPreview
editorValue={editorValue}
setEditorValue={setEditorValue}
previewStep={previewStep}
previewData={previewData}
isPreviewPending={isPreviewPending}
/>
);

return (
<TemplateTabs
previewStep={previewStep}
editorContent={editorContent}
previewContent={previewContent}
tabsValue={tabsValue}
Expand Down
Loading

0 comments on commit 65ea928

Please sign in to comment.