Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(dashboard): wait for save when switching preview tab #7299

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import { Separator } from '@/components/primitives/separator';
import { getComponentByType } from '@/components/workflow-editor/steps/component-utils';
import { EmailPreviewHeader } from '@/components/workflow-editor/steps/email/email-preview';
import { EmailTabsSection } from '@/components/workflow-editor/steps/email/email-tabs-section';
import { type UiSchema } from '@novu/shared';
import { UiSchemaGroupEnum, type UiSchema } from '@novu/shared';

type EmailEditorProps = { uiSchema: UiSchema };
export const EmailEditor = (props: EmailEditorProps) => {
const { uiSchema } = props;
const { body, subject } = uiSchema?.properties ?? {};

if (uiSchema.group !== UiSchemaGroupEnum.EMAIL) {
SokratisVidros marked this conversation as resolved.
Show resolved Hide resolved
return null;
}

const { body, subject } = uiSchema.properties ?? {};

return (
<div className="flex h-full flex-col">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,76 +1,41 @@
import { Cross2Icon } from '@radix-ui/react-icons';
import { useFormContext } from 'react-hook-form';
import { RiEdit2Line, RiPencilRuler2Line } from 'react-icons/ri';
import { useNavigate } from 'react-router-dom';

import { Notification5Fill } from '@/components/icons';
import { Button } from '@/components/primitives/button';
import { Separator } from '@/components/primitives/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/primitives/tabs';
import { StepEditorProps } from '@/components/workflow-editor/steps/configure-step-template-form';
import { WorkflowOriginEnum } from '@novu/shared';
import { EmailEditor } from '@/components/workflow-editor/steps/email/email-editor';
import { EmailEditorPreview } from '@/components/workflow-editor/steps/email/email-editor-preview';
import { CustomStepControls } from '../controls/custom-step-controls';
import { EmailTabsSection } from '@/components/workflow-editor/steps/email/email-tabs-section';
import { WorkflowOriginEnum } from '@novu/shared';
import { StepEditorProps } from '@/components/workflow-editor/steps/configure-step-template-form';
import { TemplateTabs } from '@/components/workflow-editor/steps/template-tabs';
import { useState } from 'react';

const tabsContentClassName = 'h-full w-full overflow-y-auto data-[state=inactive]:hidden';

export const EmailTabs = (props: StepEditorProps) => {
const { workflow, step } = props;
const { dataSchema, uiSchema } = step.controls;
const form = useFormContext();
const navigate = useNavigate();
const [tabsValue, setTabsValue] = useState('editor');

return (
<Tabs defaultValue="editor" value={tabsValue} onValueChange={setTabsValue} className="flex h-full flex-1 flex-col">
<header className="flex flex-row items-center gap-3 px-3 py-1.5">
<div className="mr-auto flex items-center gap-2.5 text-sm font-medium">
<RiEdit2Line className="size-4" />
<span>Configure Template</span>
</div>
<TabsList className="w-min">
<TabsTrigger value="editor" className="gap-1.5">
<RiPencilRuler2Line className="size-5 p-0.5" />
<span>Editor</span>
</TabsTrigger>
<TabsTrigger value="preview" className="gap-1.5">
<Notification5Fill className="size-5 p-0.5" />
<span>Preview</span>
</TabsTrigger>
</TabsList>
const isNovuCloud = workflow.origin === WorkflowOriginEnum.NOVU_CLOUD && uiSchema;
const isExternal = workflow.origin === WorkflowOriginEnum.EXTERNAL;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our goal should be to align Dashboard and Code First workflow management. That is, uiSchema should be available in both. Let's remove it from this check.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently its not available in both and the uiSchema type is optional on the response, there needs to be a check somewhere and this is the only place where the check makes sense.

If we want to achieve what you are describing there needs to be additional changes on API which I don't want to include as part of this UI bugfix.


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

Comment on lines +17 to +29
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added more explicit conditions so its unified across steps

<Button
variant="ghost"
size="xs"
className="size-6"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
navigate('../', { relative: 'path' });
}}
>
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</Button>
</header>
<Separator />
<TabsContent value="editor" forceMount className={tabsContentClassName}>
{workflow.origin === WorkflowOriginEnum.NOVU_CLOUD && uiSchema && <EmailEditor uiSchema={uiSchema} />}
{workflow.origin === WorkflowOriginEnum.EXTERNAL && (
<EmailTabsSection>
<CustomStepControls dataSchema={dataSchema} origin={workflow.origin} />
</EmailTabsSection>
)}
</TabsContent>
<TabsContent value="preview" forceMount className={tabsContentClassName}>
{tabsValue === 'preview' && (
<EmailEditorPreview workflow={workflow} step={step} formValues={form.getValues()} />
)}
</TabsContent>
<Separator />
</Tabs>
const previewContent = <EmailEditorPreview workflow={workflow} step={step} formValues={form.getValues()} />;

return (
<TemplateTabs
editorContent={editorContent}
previewContent={previewContent}
tabsValue={tabsValue}
onTabChange={setTabsValue}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ const redirectKey = 'redirect';
const primaryActionKey = 'primaryAction';
const secondaryActionKey = 'secondaryAction';

export const InAppEditor = ({ uiSchema }: { uiSchema?: UiSchema }) => {
if (!uiSchema || uiSchema?.group !== UiSchemaGroupEnum.IN_APP) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if its better to check for uiSchema in child or parent - moved to parent for consistency with email-editor

export const InAppEditor = ({ uiSchema }: { uiSchema: UiSchema }) => {
if (uiSchema.group !== UiSchemaGroupEnum.IN_APP) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

return null;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,69 +1,41 @@
import { Cross2Icon } from '@radix-ui/react-icons';
import { useFormContext } from 'react-hook-form';
import { RiEdit2Line, RiPencilRuler2Line } from 'react-icons/ri';
import { useNavigate } from 'react-router-dom';

import { Notification5Fill } from '@/components/icons';
import { Button } from '@/components/primitives/button';
import { Separator } from '@/components/primitives/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/primitives/tabs';
import { InAppEditor } from '@/components/workflow-editor/steps/in-app/in-app-editor';
import { InAppEditorPreview } from '@/components/workflow-editor/steps/in-app/in-app-editor-preview';
import { CustomStepControls } from '../controls/custom-step-controls';
import { StepEditorProps } from '@/components/workflow-editor/steps/configure-step-template-form';
import { InAppTabsSection } from '@/components/workflow-editor/steps/in-app/in-app-tabs-section';

const tabsContentClassName = 'h-full w-full overflow-y-auto';
import { TemplateTabs } from '@/components/workflow-editor/steps/template-tabs';
import { WorkflowOriginEnum } from '@/utils/enums';
import { useState } from 'react';

export const InAppTabs = (props: StepEditorProps) => {
const { workflow, step } = props;
const { dataSchema, uiSchema } = step.controls;
const form = useFormContext();
const navigate = useNavigate();
const [tabsValue, setTabsValue] = useState('editor');

return (
<Tabs defaultValue="editor" className="flex h-full flex-1 flex-col">
<header className="flex flex-row items-center gap-3 px-3 py-1.5">
<div className="mr-auto flex items-center gap-2.5 text-sm font-medium">
<RiEdit2Line className="size-4" />
<span>Configure Template</span>
</div>
<TabsList className="w-min">
<TabsTrigger value="editor" className="gap-1.5">
<RiPencilRuler2Line className="size-5 p-0.5" />
<span>Editor</span>
</TabsTrigger>
<TabsTrigger value="preview" className="gap-1.5">
<Notification5Fill className="size-5 p-0.5" />
<span>Preview</span>
</TabsTrigger>
</TabsList>
const isNovuCloud = workflow.origin === WorkflowOriginEnum.NOVU_CLOUD && uiSchema;
const isExternal = workflow.origin === WorkflowOriginEnum.EXTERNAL;

<Button
variant="ghost"
size="xs"
className="size-6"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
navigate('../', { relative: 'path' });
}}
>
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</Button>
</header>
<Separator />
<TabsContent value="editor" className={tabsContentClassName}>
<InAppEditor uiSchema={uiSchema} />
const editorContent = (
<>
{isNovuCloud && <InAppEditor uiSchema={uiSchema} />}
{isExternal && (
<InAppTabsSection>
<CustomStepControls dataSchema={dataSchema} origin={workflow.origin} />
</InAppTabsSection>
</TabsContent>
<TabsContent value="preview" className={tabsContentClassName}>
<InAppEditorPreview workflow={workflow} step={step} formValues={form.getValues()} />
</TabsContent>
<Separator />
</Tabs>
)}
</>
);

const previewContent = <InAppEditorPreview workflow={workflow} step={step} formValues={form.getValues()} />;

return (
<TemplateTabs
editorContent={editorContent}
previewContent={previewContent}
tabsValue={tabsValue}
onTabChange={setTabsValue}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,62 +1,26 @@
/**
* This component is used as a placeholder for the other step configuration until the actual configuration is implemented.
*/
import { Cross2Icon } from '@radix-ui/react-icons';
import { RiEdit2Line, RiPencilRuler2Line } from 'react-icons/ri';
import { useNavigate } from 'react-router-dom';

import { Notification5Fill } from '@/components/icons';
import { Button } from '@/components/primitives/button';
import { Separator } from '@/components/primitives/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/primitives/tabs';
import { CustomStepControls } from '@/components/workflow-editor/steps/controls/custom-step-controls';
import type { StepEditorProps } from '@/components/workflow-editor/steps/configure-step-template-form';

const tabsContentClassName = 'h-full w-full px-3 py-3.5 overflow-y-auto';
import { useState } from 'react';
import { CustomStepControls } from './controls/custom-step-controls';
import { TemplateTabs } from './template-tabs';
import type { StepEditorProps } from './configure-step-template-form';

export const OtherStepTabs = ({ workflow, step }: StepEditorProps) => {
const { dataSchema } = step.controls;
const navigate = useNavigate();
const [tabsValue, setTabsValue] = useState('editor');

return (
<Tabs defaultValue="editor" className="flex h-full flex-1 flex-col">
<header className="flex flex-row items-center gap-3 px-3 py-1.5">
<div className="mr-auto flex items-center gap-2.5 text-sm font-medium">
<RiEdit2Line className="size-4" />
<span>Configure Template</span>
</div>
<TabsList className="w-min">
<TabsTrigger value="editor" className="gap-1.5">
<RiPencilRuler2Line className="size-5 p-0.5" />
<span>Editor</span>
</TabsTrigger>
<TabsTrigger value="preview" className="gap-1.5" disabled>
<Notification5Fill className="size-5 p-0.5" />
<span>Preview</span>
</TabsTrigger>
</TabsList>
const editorContent = (
<div className="px-3 py-5">
<CustomStepControls dataSchema={dataSchema} origin={workflow.origin} />
</div>
);

const previewContent = null;

<Button
variant="ghost"
size="xs"
className="size-6"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
navigate('../', { relative: 'path' });
}}
>
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</Button>
</header>
<Separator />
<TabsContent value="editor" className={tabsContentClassName}>
<div className="px-3 py-5">
<CustomStepControls dataSchema={dataSchema} origin={workflow.origin} />
</div>
</TabsContent>
<Separator />
</Tabs>
return (
<TemplateTabs
editorContent={editorContent}
previewContent={previewContent}
tabsValue={tabsValue}
onTabChange={setTabsValue}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Cross2Icon } from '@radix-ui/react-icons';
import { RiEdit2Line, RiPencilRuler2Line } from 'react-icons/ri';
import { useNavigate } from 'react-router-dom';

import { Notification5Fill } from '@/components/icons';
import { Button } from '@/components/primitives/button';
import { Separator } from '@/components/primitives/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/primitives/tabs';

interface TemplateTabsProps {
editorContent: React.ReactNode;
previewContent?: React.ReactNode;
tabsValue: string;
onTabChange: (tab: string) => void;
}

export const TemplateTabs = ({ editorContent, previewContent, tabsValue, onTabChange }: TemplateTabsProps) => {
const navigate = useNavigate();

return (
<Tabs defaultValue="editor" value={tabsValue} onValueChange={onTabChange} className="flex h-full flex-1 flex-col">
<header className="flex flex-row items-center gap-3 px-3 py-1.5">
<div className="mr-auto flex items-center gap-2.5 text-sm font-medium">
<RiEdit2Line className="size-4" />
<span>Configure Template</span>
</div>
<TabsList className="w-min">
<TabsTrigger value="editor" className="gap-1.5">
<RiPencilRuler2Line className="size-5 p-0.5" />
<span>Editor</span>
</TabsTrigger>
<TabsTrigger value="preview" className="gap-1.5" disabled={!previewContent}>
<Notification5Fill className="size-5 p-0.5" />
<span>Preview</span>
</TabsTrigger>
</TabsList>

<Button
variant="ghost"
size="xs"
className="size-6"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
navigate('../', { relative: 'path' });
}}
>
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</Button>
</header>
<Separator />
<TabsContent value="editor" className="h-full w-full overflow-y-auto">
{editorContent}
</TabsContent>
<TabsContent value="preview" className="h-full w-full overflow-y-auto">
{previewContent}
</TabsContent>
<Separator />
</Tabs>
);
};
Loading