From 244bf29c862705ac1c0645991343ca5fa56ee2dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Tue, 28 Mar 2023 15:32:58 +0200 Subject: [PATCH 001/152] feat: refactor layout of workflow --- apps/web/src/App.tsx | 27 ++- apps/web/src/design-system/button/Button.tsx | 3 +- .../segmented-control/SegmentedControl.tsx | 3 + .../src/pages/templates/TemplatesListPage.tsx | 2 +- .../templates/components/SnippetPage.tsx | 23 ++ .../templates/components/SubPageWrapper.tsx | 51 ++++ .../templates/components/TemplateEditor.tsx | 228 +++++++++++++----- .../components/TemplatePageHeader.tsx | 145 ----------- .../components/TemplatePushEditor.tsx | 7 + .../components/TemplateSMSEditor.tsx | 7 + .../templates/components/TemplateSettings.tsx | 101 +++----- .../components/TemplateTriggerModal.tsx | 50 ---- ...TestWorkflowModal.tsx => TestWorkflow.tsx} | 154 ++++++------ .../components/TestWorkflowModal.cy.tsx | 23 +- .../components/TriggerSegmentControl.tsx | 36 +++ .../templates/components/UserPreference.tsx | 12 + .../components/WorkflowSettingsTabs.tsx | 35 +++ .../chat-editor/TemplateChatEditor.tsx | 7 + .../email-editor/EmailMessagesCards.tsx | 16 +- .../in-app-editor/TemplateInAppEditor.tsx | 65 ++--- .../NotificationSettingsForm.tsx | 166 +++++++------ .../TemplatePreference.tsx | 75 ++---- .../templates/editor/TemplateEditorPage.tsx | 127 +--------- .../editor/TemplateEditorProvider.tsx | 2 +- .../src/pages/templates/hooks/useBasePath.ts | 9 + .../templates/workflow/DigestMetadata.tsx | 19 +- .../workflow/ShouldStopOnFailSwitch.tsx | 1 + .../workflow/SideBar/AddStepMenu.tsx | 26 +- .../templates/workflow/SideBar/Sidebar.tsx | 66 +++++ .../workflow/SideBar/StepSettings.tsx | 44 ---- .../templates/workflow/StepActiveSwitch.tsx | 1 + .../templates/workflow/WorkflowEditor.tsx | 207 ++++++++-------- .../workflow/workflow/FlowEditor.tsx | 39 ++- .../layout/MinimalTemplatesSideBar.tsx | 50 ---- .../workflow/node-types/ChannelNode.tsx | 27 ++- .../workflow/node-types/WorkflowNode.tsx | 16 +- .../pages/user-preference/UserPreference.tsx | 39 --- 37 files changed, 915 insertions(+), 994 deletions(-) create mode 100644 apps/web/src/pages/templates/components/SnippetPage.tsx create mode 100644 apps/web/src/pages/templates/components/SubPageWrapper.tsx delete mode 100644 apps/web/src/pages/templates/components/TemplatePageHeader.tsx delete mode 100644 apps/web/src/pages/templates/components/TemplateTriggerModal.tsx rename apps/web/src/pages/templates/components/{TestWorkflowModal.tsx => TestWorkflow.tsx} (50%) create mode 100644 apps/web/src/pages/templates/components/TriggerSegmentControl.tsx create mode 100644 apps/web/src/pages/templates/components/UserPreference.tsx create mode 100644 apps/web/src/pages/templates/components/WorkflowSettingsTabs.tsx create mode 100644 apps/web/src/pages/templates/hooks/useBasePath.ts create mode 100644 apps/web/src/pages/templates/workflow/SideBar/Sidebar.tsx delete mode 100644 apps/web/src/pages/templates/workflow/workflow/layout/MinimalTemplatesSideBar.tsx delete mode 100644 apps/web/src/pages/user-preference/UserPreference.tsx diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 9b271db600f..ae1f4af2920 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -40,6 +40,12 @@ import { RequiredAuth } from './components/layout/RequiredAuth'; import { GetStarted } from './pages/quick-start/steps/GetStarted'; import { DigestPreview } from './pages/quick-start/steps/DigestPreview'; import { TemplatesDigestPlaygroundPage } from './pages/templates/TemplatesDigestPlaygroundPage'; +import { Sidebar } from './pages/templates/workflow/SideBar/Sidebar'; +import { TemplateSettings } from './pages/templates/components/TemplateSettings'; +import { UserPreference } from './pages/templates/components/UserPreference'; +import { TestWorkflow } from './pages/templates/components/TestWorkflow'; +import { SnippetPage } from './pages/templates/components/SnippetPage'; +import { TemplateEditor } from './pages/templates/components/TemplateEditor'; if (LOGROCKET_ID && window !== undefined) { LogRocket.init(LOGROCKET_ID, { @@ -184,7 +190,14 @@ function App() { } - /> + > + } /> + } /> + } /> + } /> + } /> + } /> + } - /> + > + } /> + } /> + } /> + } /> + } /> + } + /> + } /> } /> } /> diff --git a/apps/web/src/design-system/button/Button.tsx b/apps/web/src/design-system/button/Button.tsx index 32643e354d8..a04858ab32b 100644 --- a/apps/web/src/design-system/button/Button.tsx +++ b/apps/web/src/design-system/button/Button.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Button as MantineButton } from '@mantine/core'; +import { Button as MantineButton, Sx } from '@mantine/core'; import useStyles from './Button.styles'; import { SpacingProps } from '../shared/spacing.props'; @@ -17,6 +17,7 @@ interface IButtonProps extends JSX.ElementChildrenAttribute, SpacingProps { onClick?: (e: any) => void; inherit?: boolean; pulse?: boolean; + sx?: Sx; } /** diff --git a/apps/web/src/design-system/segmented-control/SegmentedControl.tsx b/apps/web/src/design-system/segmented-control/SegmentedControl.tsx index 4aea33c359f..344ef388db7 100644 --- a/apps/web/src/design-system/segmented-control/SegmentedControl.tsx +++ b/apps/web/src/design-system/segmented-control/SegmentedControl.tsx @@ -4,6 +4,7 @@ import { SegmentedControlProps, SegmentedControlItem, LoadingOverlay, + Sx, } from '@mantine/core'; import useStyles from './SegmentedControl.styles'; import { colors } from '../config'; @@ -15,6 +16,8 @@ interface ISegmentedControlProps { value?: string; onChange?(value: string): void; loading?: boolean; + fullWidth?: boolean; + sx?: Sx | (Sx | undefined)[]; } /** diff --git a/apps/web/src/pages/templates/TemplatesListPage.tsx b/apps/web/src/pages/templates/TemplatesListPage.tsx index 12e9d6f7cef..4840933289b 100644 --- a/apps/web/src/pages/templates/TemplatesListPage.tsx +++ b/apps/web/src/pages/templates/TemplatesListPage.tsx @@ -129,7 +129,7 @@ function NotificationList() { icon={} data-test-id="create-template-btn" > - New + Create Workflow } /> diff --git a/apps/web/src/pages/templates/components/SnippetPage.tsx b/apps/web/src/pages/templates/components/SnippetPage.tsx new file mode 100644 index 00000000000..b8a4e98c09a --- /dev/null +++ b/apps/web/src/pages/templates/components/SnippetPage.tsx @@ -0,0 +1,23 @@ +import { Text } from '@mantine/core'; +import { colors } from '../../../design-system'; +import { TriggerSnippetTabs } from './TriggerSnippetTabs'; +import { useTemplateEditorForm } from './TemplateEditorFormProvider'; +import { SubPageWrapper } from './SubPageWrapper'; +import { TriggerSegmentControl } from './TriggerSegmentControl'; + +export function SnippetPage() { + const { trigger } = useTemplateEditorForm(); + + return ( + <> + + + Test trigger as if you sent it from your API or implement it by copy/pasting it into the codebase of your + application + + + {trigger && } + + + ); +} diff --git a/apps/web/src/pages/templates/components/SubPageWrapper.tsx b/apps/web/src/pages/templates/components/SubPageWrapper.tsx new file mode 100644 index 00000000000..5dedf64a0b8 --- /dev/null +++ b/apps/web/src/pages/templates/components/SubPageWrapper.tsx @@ -0,0 +1,51 @@ +import { Group, Stack, Title, UnstyledButton, useMantineColorScheme } from '@mantine/core'; +import { CSSProperties } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { colors } from '../../../design-system'; +import { Close } from '../../../design-system/icons/actions/Close'; +import { useBasePath } from '../hooks/useBasePath'; + +export const SubPageWrapper = ({ + children, + title, + style, + color = colors.B60, +}: { + children: any; + title: string | any; + style?: CSSProperties | undefined; + color?: string; +}) => { + const navigate = useNavigate(); + const path = useBasePath(); + const { colorScheme } = useMantineColorScheme(); + + return ( +
+ + + + {title} + + { + navigate(path); + }} + > + + + + + {children} +
+ ); +}; diff --git a/apps/web/src/pages/templates/components/TemplateEditor.tsx b/apps/web/src/pages/templates/components/TemplateEditor.tsx index 4974d0c1cc6..f345daff157 100644 --- a/apps/web/src/pages/templates/components/TemplateEditor.tsx +++ b/apps/web/src/pages/templates/components/TemplateEditor.tsx @@ -7,16 +7,82 @@ import { TemplateSMSEditor } from './TemplateSMSEditor'; import type { IForm } from './formTypes'; import { TemplatePushEditor } from './TemplatePushEditor'; import { TemplateChatEditor } from './chat-editor/TemplateChatEditor'; -import { useActiveIntegrations } from '../../../hooks'; -import { ActivePageEnum } from '../../../constants/editorEnums'; - -export const TemplateEditor = ({ - activePage, - activeStepIndex, -}: { - activePage: ActivePageEnum; - activeStepIndex: number; -}) => { +import { useActiveIntegrations, useEnvController } from '../../../hooks'; +import { useOutletContext, useParams } from 'react-router-dom'; +import { SubPageWrapper } from './SubPageWrapper'; +import { DigestMetadata } from '../workflow/DigestMetadata'; +import { DelayMetadata } from '../workflow/DelayMetadata'; +import { Button, colors } from '../../../design-system'; +import styled from '@emotion/styled'; +import { Bell, Chat, DigestGradient, Mail, Mobile, Sms, TimerGradient, Trash } from '../../../design-system/icons'; +import { getChannel, NodeTypeEnum } from '../shared/channels'; +import { Group } from '@mantine/core'; + +const getPageTitle = (channel: StepTypeEnum) => { + if (channel === StepTypeEnum.EMAIL) { + return ( + + Email + + ); + } + + if (channel === StepTypeEnum.IN_APP) { + return ( + + In-App + + ); + } + + if (channel === StepTypeEnum.CHAT) { + return ( + + Chat + + ); + } + + if (channel === StepTypeEnum.PUSH) { + return ( + + Push + + ); + } + + if (channel === StepTypeEnum.SMS) { + return ( + + SMS + + ); + } + + if (channel === StepTypeEnum.DELAY) { + return ( + + Delay + + ); + } + + if (channel === StepTypeEnum.DIGEST) { + return ( + + Digest + + ); + } + + return channel; +}; + +export const TemplateEditor = () => { + const { channel, stepUuid = '' } = useParams<{ + channel: StepTypeEnum | undefined; + stepUuid: string; + }>(); const { integrations } = useActiveIntegrations(); const { control, @@ -24,13 +90,56 @@ export const TemplateEditor = ({ watch, } = useFormContext(); const steps = watch('steps'); + const { readonly } = useEnvController(); + + const { onDelete }: any = useOutletContext(); + + if (channel === undefined) { + return null; + } + + if (channel === StepTypeEnum.IN_APP) { + return ( + + {steps.map((message, index) => { + return message.template.type === StepTypeEnum.IN_APP && message.uuid === stepUuid ? ( + + ) : null; + })} + + ); + } + + if (channel === StepTypeEnum.EMAIL) { + return ( + + {steps.map((message, index) => { + return message.template.type === StepTypeEnum.EMAIL && message.uuid === stepUuid ? ( + integration.channel === ChannelTypeEnum.EMAIL)} + /> + ) : null; + })} + + ); + } return ( -
- {activePage === ActivePageEnum.SMS && ( -
- {steps.map((message, index) => { - return message.template.type === StepTypeEnum.SMS && activeStepIndex === index ? ( + <> + + {channel === StepTypeEnum.SMS && + steps.map((message, index) => { + return message.template.type === StepTypeEnum.SMS && message.uuid === stepUuid ? ( ) : null; })} -
- )} - {activePage === ActivePageEnum.EMAIL && ( -
- {steps.map((message, index) => { - return message.template.type === StepTypeEnum.EMAIL && activeStepIndex === index ? ( - integration.channel === ChannelTypeEnum.EMAIL) - } - /> - ) : null; - })} -
- )} - {activePage === ActivePageEnum.IN_APP && ( - <> - {steps.map((message, index) => { - return message.template.type === StepTypeEnum.IN_APP && activeStepIndex === index ? ( - - ) : null; - })} - - )} - {activePage === ActivePageEnum.PUSH && ( -
- {steps.map((message, index) => { - return message.template.type === StepTypeEnum.PUSH && activeStepIndex === index ? ( + {channel === StepTypeEnum.PUSH && + steps.map((message, index) => { + return message.template.type === StepTypeEnum.PUSH && message.uuid === stepUuid ? ( ) : null; })} -
- )} - {activePage === ActivePageEnum.CHAT && ( -
- {steps.map((message, index) => { - return message.template.type === StepTypeEnum.CHAT && activeStepIndex === index ? ( + {channel === StepTypeEnum.CHAT && + steps.map((message, index) => { + return message.template.type === StepTypeEnum.CHAT && message.uuid === stepUuid ? ( ) : null; })} -
- )} -
+ {channel === StepTypeEnum.DIGEST && + steps.map((message, index) => { + return message.template.type === StepTypeEnum.DIGEST && message.uuid === stepUuid ? ( + + ) : null; + })} + {channel === StepTypeEnum.DELAY && + steps.map((message, index) => { + return message.template.type === StepTypeEnum.DELAY && message.uuid === stepUuid ? ( + + ) : null; + })} + { + onDelete(stepUuid); + }} + disabled={readonly} + > + + Delete {getChannel(channel?.toString() ?? '')?.type === NodeTypeEnum.CHANNEL ? 'Step' : 'Action'} + + + ); }; + +const DeleteStepButton = styled(Button)` + //display: flex; + //position: inherit; + //bottom: 15px; + //left: 20px; + //right: 20px; + background: rgba(229, 69, 69, 0.15); + color: ${colors.error}; + box-shadow: none; + :hover { + background: rgba(229, 69, 69, 0.15); + } +`; diff --git a/apps/web/src/pages/templates/components/TemplatePageHeader.tsx b/apps/web/src/pages/templates/components/TemplatePageHeader.tsx deleted file mode 100644 index 59552b65c89..00000000000 --- a/apps/web/src/pages/templates/components/TemplatePageHeader.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { Center, Container, Grid, Group } from '@mantine/core'; - -import { Button, colors, Switch, Title, Text } from '../../../design-system'; -import { ArrowLeft } from '../../../design-system/icons'; -import { EditorPages } from '../editor/TemplateEditorPage'; -import { useEnvController } from '../../../hooks'; -import { When } from '../../../components/utils/When'; -import { useTemplateEditorForm } from './TemplateEditorFormProvider'; -import { useStatusChangeControllerHook } from './useStatusChangeController'; -import { ActivePageEnum } from '../../../constants/editorEnums'; - -const Header = ({ - activePage, - editMode, - name = 'Workflow Editor', -}: { - editMode: boolean; - activePage: ActivePageEnum; - name?: string; -}) => { - if (activePage === ActivePageEnum.SETTINGS) { - return <>{editMode ? 'Edit Template' : 'Create new template'}; - } - if (activePage === ActivePageEnum.WORKFLOW) { - return <>{name}; - } - - if (activePage === ActivePageEnum.USER_PREFERENCE) { - return <>{'User Preference Editor'}; - } - - if (activePage === ActivePageEnum.SMS) { - return <>{'Edit SMS Template'}; - } - - if (activePage === ActivePageEnum.EMAIL) { - return <>{'Edit Email Template'}; - } - - if (activePage === ActivePageEnum.PUSH) { - return <>{'Edit Push Template'}; - } - - if (activePage === ActivePageEnum.CHAT) { - return <>{'Edit Chat Template'}; - } - - if (activePage === ActivePageEnum.IN_APP) { - return <>{'Edit Notification Template'}; - } - - return <>{editMode ? 'Edit Template' : 'Create new template'}; -}; - -interface Props { - templateId: string; - loading: boolean; - disableSubmit: boolean; - setActivePage: (activePage: ActivePageEnum) => void; - activePage: ActivePageEnum; - onTestWorkflowClicked: () => void; -} - -export const TemplatePageHeader = ({ - templateId, - loading, - disableSubmit, - activePage, - setActivePage, - onTestWorkflowClicked, -}: Props) => { - const { template, editMode } = useTemplateEditorForm(); - const { readonly } = useEnvController(); - - const { isTemplateActive, changeActiveStatus, isStatusChangeLoading } = useStatusChangeControllerHook( - templateId, - template - ); - - return ( - - -
- - <Header editMode={editMode} activePage={activePage} name={template?.name} /> - - -
{ - setActivePage( - activePage === ActivePageEnum.WORKFLOW ? ActivePageEnum.SETTINGS : ActivePageEnum.WORKFLOW - ); - }} - inline - style={{ cursor: 'pointer' }} - > - - - Go Back - -
-
-
-
- - - {editMode && ( - - changeActiveStatus(e.target.checked)} - checked={isTemplateActive || false} - /> - - )} - - - - - - - - - - -
-
-
- ); -}; diff --git a/apps/web/src/pages/templates/components/TemplatePushEditor.tsx b/apps/web/src/pages/templates/components/TemplatePushEditor.tsx index 83d298dc0cd..674a8c20736 100644 --- a/apps/web/src/pages/templates/components/TemplatePushEditor.tsx +++ b/apps/web/src/pages/templates/components/TemplatePushEditor.tsx @@ -6,6 +6,9 @@ import type { IForm } from './formTypes'; import { Textarea } from '../../../design-system'; import { useEnvController, useVariablesManager } from '../../../hooks'; import { VariableManager } from './VariableManager'; +import { Group } from '@mantine/core'; +import { StepActiveSwitch } from '../workflow/StepActiveSwitch'; +import { ShouldStopOnFailSwitch } from '../workflow/ShouldStopOnFailSwitch'; const templateFields = ['content', 'title']; @@ -28,6 +31,10 @@ export function TemplatePushEditor({ return ( <> {!isIntegrationActive ? : null} + + + + {!isIntegrationActive ? : null} + + + + { - const { colorScheme } = useMantineColorScheme(); +export const TemplateSettings = () => { + const { templateId = '' } = useParams<{ templateId: string }>(); const { readonly } = useEnvController(); - const { template, editMode, trigger } = useTemplateEditorForm(); + const { editMode, trigger } = useTemplateEditorForm(); const [toDelete, setToDelete] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [isError, setIsError] = useState(undefined); @@ -49,68 +48,40 @@ export const TemplateSettings = ({ activePage, setActivePage, templateId }) => { }; return ( -
- - - - + + + {editMode && ( + + + - - - -
- {activePage === ActivePageEnum.SETTINGS && ( - <> - - {editMode && ( - - - Delete Template - - )} - - - )} - - {template && trigger && activePage === ActivePageEnum.TRIGGER_SNIPPET && ( - - )} -
-
-
-
+ Delete Workflow + + + )} + + ); }; -const SideBarWrapper = styled.div<{ dark: boolean }>` - border-right: 1px solid ${({ dark }) => (dark ? colors.B20 : colors.BGLight)}; - height: 100%; -`; - const DeleteNotificationButton = styled(Button)` - position: absolute; - right: 20px; - bottom: 20px; background: rgba(229, 69, 69, 0.15); color: ${colors.error}; box-shadow: none; diff --git a/apps/web/src/pages/templates/components/TemplateTriggerModal.tsx b/apps/web/src/pages/templates/components/TemplateTriggerModal.tsx deleted file mode 100644 index 06197f70a44..00000000000 --- a/apps/web/src/pages/templates/components/TemplateTriggerModal.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { INotificationTrigger } from '@novu/shared'; -import { Modal, useMantineTheme } from '@mantine/core'; -import { Button, colors, shadows, Title } from '../../../design-system'; -import { TriggerSnippetTabs } from './TriggerSnippetTabs'; - -export function TemplateTriggerModal({ - isVisible, - onDismiss, - trigger, -}: { - isVisible: boolean; - onDismiss: () => void; - trigger: INotificationTrigger; -}) { - const theme = useMantineTheme(); - const dark = theme.colorScheme === 'dark'; - - return ( - Trigger implementation code} - data-test-id="success-trigger-modal" - shadow={dark ? shadows.dark : shadows.medium} - radius="md" - size="xl" - > - -
- -
-
- ); -} diff --git a/apps/web/src/pages/templates/components/TestWorkflowModal.tsx b/apps/web/src/pages/templates/components/TestWorkflow.tsx similarity index 50% rename from apps/web/src/pages/templates/components/TestWorkflowModal.tsx rename to apps/web/src/pages/templates/components/TestWorkflow.tsx index dde8e8f7fc6..5807f1ffce2 100644 --- a/apps/web/src/pages/templates/components/TestWorkflowModal.tsx +++ b/apps/web/src/pages/templates/components/TestWorkflow.tsx @@ -1,15 +1,20 @@ -import { useMemo, useEffect } from 'react'; -import { JsonInput } from '@mantine/core'; +import { useMemo, useEffect, useState } from 'react'; +import { Group, JsonInput, Text } from '@mantine/core'; import { useForm } from '@mantine/form'; import { useMutation } from '@tanstack/react-query'; import * as Sentry from '@sentry/react'; -import { INotificationTrigger, IUserEntity, INotificationTriggerVariable } from '@novu/shared'; -import { Button, Title, Modal } from '../../../design-system'; +import { IUserEntity, INotificationTriggerVariable } from '@novu/shared'; +import { Button, colors, SegmentedControl } from '../../../design-system'; import { inputStyles } from '../../../design-system/config/inputs.styles'; import { errorMessage, successMessage } from '../../../utils/notifications'; import { useAuthContext } from '../../../components/providers/AuthProvider'; import { getSubscriberValue, getPayloadValue } from './TriggerSnippetTabs'; import { testTrigger } from '../../../api/notification-templates'; +import { useTemplateEditorForm } from './TemplateEditorFormProvider'; +import { ExecutionDetailsModalWrapper } from './ExecutionDetailsModalWrapper'; +import { useDisclosure } from '@mantine/hooks'; +import { SubPageWrapper } from './SubPageWrapper'; +import { TriggerSegmentControl } from './TriggerSegmentControl'; const makeToValue = (subscriberVariables: INotificationTriggerVariable[], currentUser?: IUserEntity) => { const subsVars = getSubscriberValue( @@ -29,21 +34,12 @@ function subscriberExist(subscriberVariables: INotificationTriggerVariable[]) { return subscriberVariables?.some((variable) => variable.name === 'subscriberId'); } -export function TestWorkflowModal({ - isVisible, - onDismiss, - trigger, - setTransactionId, - openExecutionModal, -}: { - isVisible: boolean; - onDismiss: () => void; - openExecutionModal: () => void; - setTransactionId: (id: string) => void; - trigger: INotificationTrigger; -}) { +export function TestWorkflow() { + const [transactionId, setTransactionId] = useState(''); + const { trigger } = useTemplateEditorForm(); const { currentUser } = useAuthContext(); const { mutateAsync: triggerTestEvent } = useMutation(testTrigger); + const [executionModalOpened, { close: closeExecutionModal, open: openExecutionModal }] = useDisclosure(false); const subscriberVariables = useMemo(() => { if (trigger?.subscriberVariables && subscriberExist(trigger?.subscriberVariables)) { @@ -94,11 +90,8 @@ export function TestWorkflowModal({ overrides, }); - const { transactionId = '' } = response; - - setTransactionId(transactionId); + setTransactionId(response.transactionId || ''); successMessage('Template triggered successfully'); - onDismiss(); openExecutionModal(); } catch (e: any) { Sentry.captureException(e); @@ -107,56 +100,73 @@ export function TestWorkflowModal({ }; return ( - Test Trigger } - data-test-id="test-trigger-modal" - > -
{ - form.onSubmit(onTrigger)(e); - e.stopPropagation(); - }} - > - - - -
- -
- -
+ <> + + + Test trigger as if you sent it from your API or implement it by copy/pasting it into the codebase of your + application + + +
{ + form.onSubmit(onTrigger)(e); + e.stopPropagation(); + }} + > + + + + + + + +
+ + ); } diff --git a/apps/web/src/pages/templates/components/TestWorkflowModal.cy.tsx b/apps/web/src/pages/templates/components/TestWorkflowModal.cy.tsx index 2861cc4916b..8313b7bf457 100644 --- a/apps/web/src/pages/templates/components/TestWorkflowModal.cy.tsx +++ b/apps/web/src/pages/templates/components/TestWorkflowModal.cy.tsx @@ -2,7 +2,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { TriggerTypeEnum } from '@novu/shared'; import { TestWrapper } from '../../../testing'; -import { TestWorkflowModal } from './TestWorkflowModal'; +import { TestWorkflow } from './TestWorkflow'; const queryClient = new QueryClient(); @@ -11,13 +11,7 @@ describe('TestWorkflowModal Component', function () { cy.mount( - {}} - setTransactionId={() => {}} - openExecutionModal={() => {}} - trigger={{ variables: [], type: TriggerTypeEnum.EVENT, identifier: '1234', subscriberVariables: [] }} - /> + ); @@ -31,18 +25,7 @@ describe('TestWorkflowModal Component', function () { cy.mount( - {}} - openExecutionModal={() => {}} - setTransactionId={() => {}} - trigger={{ - variables: [{ name: 'firstVariable' }, { name: 'secondVariable' }], - type: TriggerTypeEnum.EVENT, - identifier: '1234', - subscriberVariables: [{ name: 'email' }], - }} - /> + ); diff --git a/apps/web/src/pages/templates/components/TriggerSegmentControl.tsx b/apps/web/src/pages/templates/components/TriggerSegmentControl.tsx new file mode 100644 index 00000000000..1c123820c97 --- /dev/null +++ b/apps/web/src/pages/templates/components/TriggerSegmentControl.tsx @@ -0,0 +1,36 @@ +import { useMemo } from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { SegmentedControl } from '../../../design-system'; +import { useBasePath } from '../hooks/useBasePath'; + +export const TriggerSegmentControl = () => { + const basePath = useBasePath(); + const navigate = useNavigate(); + const { pathname } = useLocation(); + const value = useMemo(() => { + return pathname.replace(basePath + '/', ''); + }, [pathname, basePath]); + + return ( + { + navigate(basePath + '/' + segmentValue); + }} + /> + ); +}; diff --git a/apps/web/src/pages/templates/components/UserPreference.tsx b/apps/web/src/pages/templates/components/UserPreference.tsx new file mode 100644 index 00000000000..0a619f1b73f --- /dev/null +++ b/apps/web/src/pages/templates/components/UserPreference.tsx @@ -0,0 +1,12 @@ +import { TemplatePreference } from './notification-setting-form/TemplatePreference'; +import { SubPageWrapper } from './SubPageWrapper'; +import { WorkflowSettingsTabs } from './WorkflowSettingsTabs'; + +export function UserPreference() { + return ( + + + + + ); +} diff --git a/apps/web/src/pages/templates/components/WorkflowSettingsTabs.tsx b/apps/web/src/pages/templates/components/WorkflowSettingsTabs.tsx new file mode 100644 index 00000000000..ca8df8f78ba --- /dev/null +++ b/apps/web/src/pages/templates/components/WorkflowSettingsTabs.tsx @@ -0,0 +1,35 @@ +import { Tabs } from '@mantine/core'; +import { useMemo } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import useStyles from '../../../design-system/tabs/Tabs.styles'; +import { useBasePath } from '../hooks/useBasePath'; + +export const WorkflowSettingsTabs = () => { + const { classes } = useStyles(false); + const basePath = useBasePath(); + const navigate = useNavigate(); + const { pathname } = useLocation(); + const value = useMemo(() => { + return pathname.replace(basePath + '/', ''); + }, [pathname, basePath]); + + return ( + { + navigate(basePath + '/' + tabValue); + }} + variant="default" + value={value} + classNames={classes} + > + + General + Channels + Providers + + + ); +}; diff --git a/apps/web/src/pages/templates/components/chat-editor/TemplateChatEditor.tsx b/apps/web/src/pages/templates/components/chat-editor/TemplateChatEditor.tsx index e88e66d021b..d27af82662d 100644 --- a/apps/web/src/pages/templates/components/chat-editor/TemplateChatEditor.tsx +++ b/apps/web/src/pages/templates/components/chat-editor/TemplateChatEditor.tsx @@ -6,6 +6,9 @@ import type { IForm } from '../formTypes'; import { LackIntegrationError } from '../LackIntegrationError'; import { Textarea } from '../../../../design-system'; import { VariableManager } from '../VariableManager'; +import { Group } from '@mantine/core'; +import { StepActiveSwitch } from '../../workflow/StepActiveSwitch'; +import { ShouldStopOnFailSwitch } from '../../workflow/ShouldStopOnFailSwitch'; const templateFields = ['content']; @@ -28,6 +31,10 @@ export function TemplateChatEditor({ return ( <> {!isIntegrationActive ? : null} + + + + (); useHotkeys([ [ @@ -65,6 +74,11 @@ export function EmailMessagesCards({ index, isIntegrationActive }: { index: numb position: 'relative', }} > + + + + + diff --git a/apps/web/src/pages/templates/components/in-app-editor/TemplateInAppEditor.tsx b/apps/web/src/pages/templates/components/in-app-editor/TemplateInAppEditor.tsx index a68cf73b4b2..6c888e8c3a2 100644 --- a/apps/web/src/pages/templates/components/in-app-editor/TemplateInAppEditor.tsx +++ b/apps/web/src/pages/templates/components/in-app-editor/TemplateInAppEditor.tsx @@ -1,4 +1,4 @@ -import { Stack } from '@mantine/core'; +import { Group, Stack } from '@mantine/core'; import { Control, Controller, useFormContext } from 'react-hook-form'; import { useState, useMemo } from 'react'; @@ -7,6 +7,8 @@ import { Input } from '../../../../design-system'; import { useEnvController, useVariablesManager } from '../../../../hooks'; import { InAppContentCard } from './InAppContentCard'; import { VariableManagerModal } from '../VariableManagerModal'; +import { StepActiveSwitch } from '../../workflow/StepActiveSwitch'; +import { ShouldStopOnFailSwitch } from '../../workflow/ShouldStopOnFailSwitch'; const getVariableContents = (template: ITemplates) => { const baseContent = ['content']; @@ -34,37 +36,36 @@ export function TemplateInAppEditor({ control, index }: { control: Control -
- - ( - - )} - /> - { - setModalOpen(true); - }} - /> - -
+ + + + + + ( + + )} + /> + { + setModalOpen(true); + }} + /> + + ); diff --git a/apps/web/src/pages/templates/components/notification-setting-form/NotificationSettingsForm.tsx b/apps/web/src/pages/templates/components/notification-setting-form/NotificationSettingsForm.tsx index e378e60a71a..92b8097315b 100644 --- a/apps/web/src/pages/templates/components/notification-setting-form/NotificationSettingsForm.tsx +++ b/apps/web/src/pages/templates/components/notification-setting-form/NotificationSettingsForm.tsx @@ -1,23 +1,20 @@ import { useEffect } from 'react'; -import { ActionIcon, Grid } from '@mantine/core'; +import { ActionIcon, Grid, Stack } from '@mantine/core'; import { useClipboard } from '@mantine/hooks'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Controller, useFormContext } from 'react-hook-form'; import { INotificationTrigger } from '@novu/shared'; import { api } from '../../../../api/api.client'; -import { Input, Select, Tooltip } from '../../../../design-system'; +import { Input, Select, Switch, Tooltip } from '../../../../design-system'; import { Check, Copy } from '../../../../design-system/icons'; import { useEnvController, useNotificationGroup } from '../../../../hooks'; import type { IForm } from '../formTypes'; +import { useTemplateEditorForm } from '../TemplateEditorFormProvider'; +import { useParams } from 'react-router-dom'; +import { useStatusChangeControllerHook } from '../useStatusChangeController'; -export const NotificationSettingsForm = ({ - editMode, - trigger, -}: { - editMode: boolean; - trigger?: INotificationTrigger; -}) => { +export const NotificationSettingsForm = ({ trigger }: { trigger?: INotificationTrigger }) => { const idClipboard = useClipboard({ timeout: 1000 }); const queryClient = useQueryClient(); const { readonly } = useEnvController(); @@ -28,6 +25,14 @@ export const NotificationSettingsForm = ({ getValues, } = useFormContext(); + const { template, editMode } = useTemplateEditorForm(); + const { templateId = '' } = useParams<{ templateId: string }>(); + + const { isTemplateActive, changeActiveStatus, isStatusChangeLoading } = useStatusChangeControllerHook( + templateId, + template + ); + const { groups, loading: loadingGroups } = useNotificationGroup(); const { isLoading: loadingCreateGroup, mutateAsync: createNotificationGroup } = useMutation< { name: string; _id: string }, @@ -72,73 +77,27 @@ export const NotificationSettingsForm = ({ return ( <> - - - ( - - )} - /> - ( - + + {editMode && ( + + changeActiveStatus(e.target.checked)} + checked={isTemplateActive || false} /> - )} - /> - - - {trigger && ( - ( - - idClipboard.copy(field.value)}> - {idClipboard.copied ? : } - - - } - /> - )} - /> + )} + + + )} + /> + {trigger && ( + ( + + idClipboard.copy(field.value)}> + {idClipboard.copied ? : } + + + } + /> + )} + /> + )} + ( + + )} + /> ); }; diff --git a/apps/web/src/pages/templates/components/notification-setting-form/TemplatePreference.tsx b/apps/web/src/pages/templates/components/notification-setting-form/TemplatePreference.tsx index 70b8892664e..8f4ae388e07 100644 --- a/apps/web/src/pages/templates/components/notification-setting-form/TemplatePreference.tsx +++ b/apps/web/src/pages/templates/components/notification-setting-form/TemplatePreference.tsx @@ -1,5 +1,5 @@ import { FunctionComponent } from 'react'; -import { Grid, Input, InputWrapperProps } from '@mantine/core'; +import { Group, Input, InputWrapperProps, Text } from '@mantine/core'; import styled from '@emotion/styled'; import { useFormContext, Controller } from 'react-hook-form'; @@ -12,14 +12,8 @@ import type { IForm } from '../formTypes'; export function TemplatePreference() { return ( <> - - - - - - - - + + ); } @@ -48,24 +42,21 @@ export function ChannelPreference() { description="Check the channels you would like to be ON by default" styles={inputStyles} > - - {Object.keys(preferences).map((key) => { - const label = channels.find((channel) => channel.tabKey === key)?.label; - const checked = preferences[key] || false; - - return ( - - handleCheckboxChange(e, key)} - /> - - ); - })} - + {Object.keys(preferences).map((key) => { + const label = channels.find((channel) => channel.tabKey === key)?.label; + const checked = preferences[key] || false; + + return ( + handleCheckboxChange(e, key)} + /> + ); + })} ); }} @@ -75,6 +66,7 @@ export function ChannelPreference() { export function CriticalPreference() { const { control } = useFormContext(); + const { readonly } = useEnvController(); return ( { return ( - } - styles={inputStyles} - children={null} - /> + + + Users will be able to manage subscriptions + + + ); }} /> ); } -function CriticalDescription({ field }) { - const { readonly } = useEnvController(); - - return ( - - {"When on, the template will not show in the user preferences, meaning they wouldn't be able to opt out."} - - - ); -} - export const InputWrapperProxy: FunctionComponent = ({ children, ...props }) => { return {children}; }; @@ -113,12 +94,6 @@ export const InputWrapperProxy: FunctionComponent = ({ childr const InputBackground = styled(InputWrapperProxy)` background: ${({ theme }) => (theme.colorScheme === 'dark' ? colors.B17 : colors.B98)}; border-radius: 7px; - padding: 20px; -`; - -const DescriptionWrapper = styled.div` - display: flex; - justify-content: space-between; `; const StyledCheckbox = styled(CheckboxProxy)<{ checked: boolean }>` diff --git a/apps/web/src/pages/templates/editor/TemplateEditorPage.tsx b/apps/web/src/pages/templates/editor/TemplateEditorPage.tsx index 1fdc617fd41..c95f1f7dd91 100644 --- a/apps/web/src/pages/templates/editor/TemplateEditorPage.tsx +++ b/apps/web/src/pages/templates/editor/TemplateEditorPage.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { useNavigate, useParams, useLocation } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; import { useDisclosure } from '@mantine/hooks'; import { ReactFlowProvider } from 'react-flow-renderer'; import { FieldErrors, useFormContext } from 'react-hook-form'; @@ -8,17 +8,8 @@ import PageContainer from '../../../components/layout/components/PageContainer'; import PageMeta from '../../../components/layout/components/PageMeta'; import type { IForm } from '../components/formTypes'; import WorkflowEditor from '../workflow/WorkflowEditor'; -import { TemplateEditor } from '../components/TemplateEditor'; -import { TemplateSettings } from '../components/TemplateSettings'; -import { TemplatePageHeader } from '../components/TemplatePageHeader'; -import { TemplateTriggerModal } from '../components/TemplateTriggerModal'; -import { usePrompt, useSearchParams, useEnvController, useActiveIntegrations } from '../../../hooks'; -import { UnsavedChangesModal } from '../components/UnsavedChangesModal'; -import { When } from '../../../components/utils/When'; -import { UserPreference } from '../../user-preference/UserPreference'; -import { TestWorkflowModal } from '../components/TestWorkflowModal'; +import { useSearchParams, useEnvController } from '../../../hooks'; import { SaveChangesModal } from '../components/SaveChangesModal'; -import { ExecutionDetailsModalWrapper } from '../components/ExecutionDetailsModalWrapper'; import { BlueprintModal } from '../components/BlueprintModal'; import { useTemplateEditorForm } from '../components/TemplateEditorFormProvider'; import { errorMessage } from '../../../utils/notifications'; @@ -37,54 +28,22 @@ export const EditorPages = [ ]; export default function TemplateEditorPage() { - const { templateId = '' } = useParams<{ templateId: string }>(); const navigate = useNavigate(); const location = useLocation(); - const { readonly, environment } = useEnvController(); - const [transactionId, setTransactionId] = useState(''); + const { environment } = useEnvController(); const [isTriggerModalVisible, setTriggerModalVisible] = useState(false); - const onTriggerModalDismiss = () => { - navigate('/templates'); - }; - const { isLoading: isIntegrationsLoading } = useActiveIntegrations(); - const { - template, - isLoading, - isCreating, - isUpdating, - editMode, - createdTemplateId, - trigger, - onSubmit, - addStep, - deleteStep, - } = useTemplateEditorForm(); - const { activePage, setActivePage, activeStepIndex } = useTemplateEditorContext(); + const { template, isCreating, isUpdating, editMode, onSubmit } = useTemplateEditorForm(); + const { setActivePage } = useTemplateEditorContext(); const methods = useFormContext(); - const { - formState: { isDirty }, - handleSubmit, - } = methods; + const { handleSubmit } = methods; const isCreateTemplatePage = location.pathname === ROUTES.TEMPLATES_CREATE; - const [showModal, confirmNavigation, cancelNavigation] = usePrompt(isDirty); const onInvalid = async (errors: FieldErrors) => { errorMessage(getExplicitErrors(errors)); }; - const [testWorkflowModalOpened, { close: closeTestWorkflowModal, open: openTestWorkflowModal }] = useDisclosure( - false, - { - onClose() { - if (!editMode) { - navigate(`/templates/edit/${createdTemplateId}`); - } - }, - } - ); const [saveChangesModalOpened, { close: closeSaveChangesModal, open: openSaveChangesModal }] = useDisclosure(false); - const [executionModalOpened, { close: closeExecutionModal, open: openExecutionModal }] = useDisclosure(false); const searchParams = useSearchParams(); @@ -100,7 +59,6 @@ export default function TemplateEditorPage() { const onConfirmSaveChanges = async (data: IForm) => { await onSubmit(data); closeSaveChangesModal(); - openTestWorkflowModal(); }; const onSubmitHandler = async (data: IForm) => { @@ -111,14 +69,6 @@ export default function TemplateEditorPage() { }); }; - const onTestWorkflowClicked = () => { - if (isDirty) { - openSaveChangesModal(); - } else { - openTestWorkflowModal(); - } - }; - useEffect(() => { if (environment && template) { if (environment._id !== template._environmentId) { @@ -148,58 +98,9 @@ export default function TemplateEditorPage() { onSubmit={handleSubmit(onSubmitHandler, onInvalid)} style={{ minHeight: '100%' }} > - - - - - {(activePage === ActivePageEnum.SETTINGS || activePage === ActivePageEnum.TRIGGER_SNIPPET) && ( - - )} - - {activePage === ActivePageEnum.WORKFLOW && ( - - - - )} - - - - - {!isLoading && !isIntegrationsLoading ? ( - - ) : null} - {trigger && ( - - )} - {trigger && !isDirty && ( - - )} + + + - - ); diff --git a/apps/web/src/pages/templates/editor/TemplateEditorProvider.tsx b/apps/web/src/pages/templates/editor/TemplateEditorProvider.tsx index 3a0564f96f1..39091b15737 100644 --- a/apps/web/src/pages/templates/editor/TemplateEditorProvider.tsx +++ b/apps/web/src/pages/templates/editor/TemplateEditorProvider.tsx @@ -26,7 +26,7 @@ const TemplateEditorContext = React.createContext({ export const useTemplateEditorContext = () => React.useContext(TemplateEditorContext); export const TemplateEditorProvider = ({ children }) => { - const [activePage, setActivePage] = useState(ActivePageEnum.SETTINGS); + const [activePage, setActivePage] = useState(ActivePageEnum.WORKFLOW); const [selectedNodeId, setSelectedNodeId] = useState(''); const { watch } = useFormContext(); const steps = watch('steps'); diff --git a/apps/web/src/pages/templates/hooks/useBasePath.ts b/apps/web/src/pages/templates/hooks/useBasePath.ts new file mode 100644 index 00000000000..0ff4e7c4c1d --- /dev/null +++ b/apps/web/src/pages/templates/hooks/useBasePath.ts @@ -0,0 +1,9 @@ +import { useParams } from 'react-router-dom'; +import { useTemplateEditorForm } from '../components/TemplateEditorFormProvider'; + +export const useBasePath = () => { + const { editMode } = useTemplateEditorForm(); + const { templateId = '' } = useParams<{ templateId: string }>(); + + return editMode ? `/templates/edit/${templateId}` : '/templates/create'; +}; diff --git a/apps/web/src/pages/templates/workflow/DigestMetadata.tsx b/apps/web/src/pages/templates/workflow/DigestMetadata.tsx index 2129486b9eb..dac233d7f2d 100644 --- a/apps/web/src/pages/templates/workflow/DigestMetadata.tsx +++ b/apps/web/src/pages/templates/workflow/DigestMetadata.tsx @@ -7,19 +7,26 @@ import { When } from '../../../components/utils/When'; import { Input, Select, Switch, Button } from '../../../design-system'; import { inputStyles } from '../../../design-system/config/inputs.styles'; import { useEnvController } from '../../../hooks'; +import { useTemplateEditorForm } from '../components/TemplateEditorFormProvider'; +import { useNavigate } from 'react-router-dom'; +import { useBasePath } from '../hooks/useBasePath'; const StyledSwitch = styled(Switch)` max-width: 100% !important; margin-top: 15px; `; -export const DigestMetadata = ({ control, index, loading, disableSubmit, onSideMenuClose }) => { +export const DigestMetadata = ({ control, index }) => { + const { isCreating, isUpdating, isLoading } = useTemplateEditorForm(); const { readonly } = useEnvController(); const { - formState: { errors, isSubmitted }, + formState: { errors, isSubmitted, isDirty }, watch, trigger, } = useFormContext(); + const isSubmitDisabled = readonly || isLoading || isCreating || !isDirty; + const navigate = useNavigate(); + const basePath = useBasePath(); const type = watch(`steps.${index}.metadata.type`); const showErrors = isSubmitted && errors?.steps; @@ -234,9 +241,11 @@ export const DigestMetadata = ({ control, index, loading, disableSubmit, onSideM mb={15} variant="outline" data-test-id="delete-step-button" - loading={loading} - disabled={disableSubmit} - onClick={onSideMenuClose} + loading={isCreating || isUpdating} + disabled={isSubmitDisabled} + onClick={() => { + navigate(basePath); + }} > Save diff --git a/apps/web/src/pages/templates/workflow/ShouldStopOnFailSwitch.tsx b/apps/web/src/pages/templates/workflow/ShouldStopOnFailSwitch.tsx index 59be611f25c..d96567fd33f 100644 --- a/apps/web/src/pages/templates/workflow/ShouldStopOnFailSwitch.tsx +++ b/apps/web/src/pages/templates/workflow/ShouldStopOnFailSwitch.tsx @@ -28,4 +28,5 @@ export const ShouldStopOnFailSwitch = ({ control, index }) => { const StyledSwitch = styled(Switch)` max-width: 100% !important; + width: auto; `; diff --git a/apps/web/src/pages/templates/workflow/SideBar/AddStepMenu.tsx b/apps/web/src/pages/templates/workflow/SideBar/AddStepMenu.tsx index 4fdd39b4a7b..d6715691629 100644 --- a/apps/web/src/pages/templates/workflow/SideBar/AddStepMenu.tsx +++ b/apps/web/src/pages/templates/workflow/SideBar/AddStepMenu.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { Stack } from '@mantine/core'; -import { colors, DragButton, Text, Title } from '../../../../design-system'; +import { Stack, Title } from '@mantine/core'; +import { colors, DragButton, Text } from '../../../../design-system'; import { channels, NodeTypeEnum } from '../../shared/channels'; import { useEnvController } from '../../../../hooks'; import { When } from '../../../../components/utils/When'; @@ -19,11 +19,9 @@ export function AddStepMenu({ return ( - Steps to add - - Drag and drop new steps to the canvas - You can not drag and drop new steps in Production - + + Channels + @@ -39,11 +37,9 @@ export function AddStepMenu({ marginTop: '15px', }} > - Actions - - Add actions to the flow - You can not drag and drop new actions in Production - + + Actions + @@ -73,10 +69,6 @@ const DraggableNode = ({ channel, setDragging, onDragStart }) => ( role="presentation" aria-grabbed="true" > - + ); diff --git a/apps/web/src/pages/templates/workflow/SideBar/Sidebar.tsx b/apps/web/src/pages/templates/workflow/SideBar/Sidebar.tsx new file mode 100644 index 00000000000..4ec3bf5fd95 --- /dev/null +++ b/apps/web/src/pages/templates/workflow/SideBar/Sidebar.tsx @@ -0,0 +1,66 @@ +import { Container, Group, Stack, useMantineColorScheme } from '@mantine/core'; +import { colors } from '@novu/notification-center'; +import { Background, BackgroundVariant } from 'react-flow-renderer'; +import { AddStepMenu } from './AddStepMenu'; +import styled from '@emotion/styled'; +import { Link, useNavigate, useOutletContext } from 'react-router-dom'; +import { Button } from '../../../../design-system'; +import { Settings } from '../../../../design-system/icons'; +import { useBasePath } from '../../hooks/useBasePath'; + +export const Sidebar = () => { + const { colorScheme } = useMantineColorScheme(); + const onDragStart = (event, nodeType) => { + event.dataTransfer.setData('application/reactflow', nodeType); + event.dataTransfer.effectAllowed = 'move'; + }; + const { setDragging }: any = useOutletContext(); + const navigate = useNavigate(); + const basePath = useBasePath(); + + return ( + <> + + + + + + + + + + + + +
+ + + + +
+ + ); +}; + +const SideBarWrapper = styled.div<{ dark: boolean }>` + background-color: ${({ dark }) => (dark ? colors.B17 : colors.white)} !important; + position: relative; + z-index: 1; + border-radius: 12px; +`; diff --git a/apps/web/src/pages/templates/workflow/SideBar/StepSettings.tsx b/apps/web/src/pages/templates/workflow/SideBar/StepSettings.tsx index 6f8a2034372..39a7ed21aa5 100644 --- a/apps/web/src/pages/templates/workflow/SideBar/StepSettings.tsx +++ b/apps/web/src/pages/templates/workflow/SideBar/StepSettings.tsx @@ -117,50 +117,6 @@ export function StepSettings({
- - - - Digest Properties - - - - - - Configure the digest parameters. Read more about the digest engine{' '} - - here - - . - - - - - - - - - - - - Delay Properties - - - - - - Configure the delay parameters. - - - - {activeStepIndex > 0 && } - -
{ const StyledSwitch = styled(Switch)` max-width: 100% !important; + width: auto; `; diff --git a/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx b/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx index 1e479a41904..6e2cf0ac0e3 100644 --- a/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx +++ b/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx @@ -1,79 +1,46 @@ -import { useEffect, useState } from 'react'; -import { useFormContext } from 'react-hook-form'; +import { useState } from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; import styled from '@emotion/styled'; -import { Grid, useMantineColorScheme } from '@mantine/core'; import { FilterPartTypeEnum, StepTypeEnum } from '@novu/shared'; import { showNotification } from '@mantine/notifications'; import FlowEditor from './workflow/FlowEditor'; -import { colors } from '../../../design-system'; import { channels, getChannel, NodeTypeEnum } from '../shared/channels'; import type { IForm } from '../components/formTypes'; -import { useEnvController } from '../../../hooks'; import { When } from '../../../components/utils/When'; -import { TemplatePageHeader } from '../components/TemplatePageHeader'; -import { EditorPages } from '../editor/TemplateEditorPage'; import { DeleteConfirmModal } from '../components/DeleteConfirmModal'; import { FilterModal } from '../filter/FilterModal'; -import { StepSettings } from './SideBar/StepSettings'; -import { AddStepMenu } from './SideBar/AddStepMenu'; -import { useTemplateFetcher } from '../../../api/hooks'; -import { ActivePageEnum } from '../../../constants/editorEnums'; import { useTemplateEditorContext } from '../editor/TemplateEditorProvider'; +import { useTemplateEditorForm } from '../components/TemplateEditorFormProvider'; +import { Outlet, useParams } from 'react-router-dom'; +import { Container, TextInput } from '@mantine/core'; +import { useEnvController } from '../../../hooks'; -const WorkflowEditor = ({ - setActivePage, - templateId, - activePage, - onTestWorkflowClicked, - isCreatingTemplate, - isUpdatingTemplate, - addStep, - deleteStep, -}: { - setActivePage: (string) => void; - templateId: string; - activePage: ActivePageEnum; - onTestWorkflowClicked: () => void; - isCreatingTemplate: boolean; - isUpdatingTemplate: boolean; - addStep: (channelType: StepTypeEnum, id: string, index?: number) => void; - deleteStep: (index: number) => void; -}) => { - const { setSelectedNodeId, activeStepIndex, selectedChannel } = useTemplateEditorContext(); +const WorkflowEditor = () => { + const { addStep, deleteStep } = useTemplateEditorForm(); + const { channel } = useParams<{ + channel: StepTypeEnum | undefined; + }>(); + const { activeStepIndex } = useTemplateEditorContext(); const hasActiveStepSelected = activeStepIndex >= 0; - const { colorScheme } = useMantineColorScheme(); const [dragging, setDragging] = useState(false); - const onDragStart = (event, nodeType) => { - event.dataTransfer.setData('application/reactflow', nodeType); - event.dataTransfer.effectAllowed = 'move'; - }; const { control, watch, - clearErrors, - formState: { errors, isDirty: isDirtyForm, isSubmitted }, + formState: { errors, isSubmitted }, } = useFormContext(); - const { isInitialLoading: loadingEditTemplate } = useTemplateFetcher({ templateId }); + const { readonly } = useEnvController(); + const showErrors = isSubmitted && errors?.steps; const [filterOpen, setFilterOpen] = useState(false); const steps = watch('steps'); - const { readonly } = useEnvController(); const [toDelete, setToDelete] = useState(''); - const setActivePageWrapper = (page: ActivePageEnum) => { - if (!isSubmitted && EditorPages.includes(page)) { - clearErrors('steps'); - } - setActivePage(page); - }; - const confirmDelete = () => { - const index = steps.findIndex((item) => item._id === toDelete); + const index = steps.findIndex((item) => item.uuid === toDelete); deleteStep(index); - setSelectedNodeId(''); setToDelete(''); }; @@ -81,11 +48,11 @@ const WorkflowEditor = ({ setToDelete(''); }; - const onDelete = (id) => { - const currentStep = steps.find((step) => step.id === id); + const onDelete = (uuid) => { + const currentStep = steps.find((step) => step.uuid === uuid); if (!currentStep) { - setToDelete(id); + setToDelete(uuid); return; } @@ -117,43 +84,76 @@ const WorkflowEditor = ({ return; } - setToDelete(id); + setToDelete(uuid); }; - useEffect(() => { - setSelectedNodeId(''); - }, []); + if (channel && [StepTypeEnum.EMAIL, StepTypeEnum.IN_APP].includes(channel)) { + return ( + <> + + 0} + confirm={confirmDelete} + cancel={cancelDelete} + /> + + ); + } return ( <> - - +
- - + + { + return ( + ({ + wrapper: { + background: 'transparent', + width: '100%', + }, + input: { + background: 'transparent', + border: 'none', + fontSize: '20px', + fontWeight: 'bolder', + padding: 0, + lineHeight: '28px', + minHeight: 'auto', + height: 'auto', + width: '100%', + }, + })} + {...field} + value={field.value || ''} + error={showErrors && fieldState.error?.message} + type="text" + data-test-id="title" + placeholder="Enter notification name" + disabled={readonly} + /> + ); + }} + /> + + - - - - {selectedChannel ? ( - - ) : ( - - )} - - - +
+
+ +
+ 0} confirm={confirmDelete} cancel={cancelDelete} @@ -201,12 +196,6 @@ const WorkflowEditor = ({ export default WorkflowEditor; -const SideBarWrapper = styled.div<{ dark: boolean }>` - background-color: ${({ dark }) => (dark ? colors.B17 : colors.white)}; - height: 100%; - position: relative; -`; - export const StyledNav = styled.div` padding: 15px 20px; height: 100%; diff --git a/apps/web/src/pages/templates/workflow/workflow/FlowEditor.tsx b/apps/web/src/pages/templates/workflow/workflow/FlowEditor.tsx index cd8515c491b..c06e02615fa 100644 --- a/apps/web/src/pages/templates/workflow/workflow/FlowEditor.tsx +++ b/apps/web/src/pages/templates/workflow/workflow/FlowEditor.tsx @@ -26,11 +26,10 @@ import { getChannel } from '../../shared/channels'; import type { IForm, IStepEntity } from '../../components/formTypes'; import AddNode from './node-types/AddNode'; import { useEnvController } from '../../../../hooks'; -import { MinimalTemplatesSideBar } from './layout/MinimalTemplatesSideBar'; import { getFormattedStepErrors } from '../../shared/errors'; import { AddNodeEdge, IAddNodeEdge } from './edge-types/AddNodeEdge'; -import { useTemplateEditorForm } from '../../components/TemplateEditorFormProvider'; -import { ActivePageEnum } from '../../../../constants/editorEnums'; +import { useBasePath } from '../../hooks/useBasePath'; +import { useNavigate } from 'react-router-dom'; const nodeTypes = { channelNode: ChannelNode, @@ -52,20 +51,14 @@ const initialNodes: Node[] = [ const initialEdges: Edge[] = []; export function FlowEditor({ - activePage, - setActivePage, steps, - setSelectedNodeId, addStep, dragging, errors, onDelete, }: { - activePage: ActivePageEnum; - setActivePage: (string) => void; onDelete: (id: string) => void; steps: IStepEntity[]; - setSelectedNodeId: (nodeId: string) => void; addStep: (channelType: StepTypeEnum, id: string, index?: number) => void; dragging: boolean; errors: any; @@ -77,14 +70,13 @@ export function FlowEditor({ const [reactFlowInstance, setReactFlowInstance] = useState(); const { setViewport } = useReactFlow(); const { readonly } = useEnvController(); - const { template, trigger } = useTemplateEditorForm(); const { trigger: triggerErrors } = useFormContext(); const [displayEdgeTimeout, setDisplayEdgeTimeout] = useState>(new Map()); useEffect(() => { const clientWidth = reactFlowWrapper.current?.clientWidth; const middle = clientWidth ? clientWidth / 2 - 100 : 0; - const zoomView = nodes.length > 4 ? 0.75 : 1; + const zoomView = 1; const xyPos = reactFlowInstance?.project({ x: middle, y: 0 }); setViewport({ x: xyPos?.x ?? middle, y: xyPos?.y ?? 0, zoom: zoomView }, { duration: 800 }); }, [reactFlowInstance]); @@ -107,11 +99,15 @@ export function FlowEditor({ [steps] ); + const basePath = useBasePath(); + const navigate = useNavigate(); + const onNodeClick = useCallback((event, node) => { event.preventDefault(); - setSelectedNodeId(node.id); + + navigate(basePath + `/${node.data.channelType}/${node.data.uuid}`); if (node.id === '1') { - setSelectedNodeId(''); + navigate(basePath + '/testworkflow'); } }, []); @@ -219,7 +215,7 @@ export function FlowEditor({ index: i, error: getFormattedStepErrors(i, errors), onDelete, - setActivePage, + uuid: step.uuid, }, }; } @@ -294,7 +290,14 @@ export function FlowEditor({ return ( <> -
+
setSelectedNodeId('')} /* * TODO: for now this disables the deletion of a step using delete/backspace keys * as it will require some sort of refactoring of how we save the workflow state @@ -326,11 +328,6 @@ export function FlowEditor({ deleteKeyCode={null} {...reactFlowDefaultProps} > - void; - showTriggerSection: boolean; -}) { - return ( - - - - ); -} - -const Wrapper = styled.div` - position: absolute; - width: 100px; - padding: 25px; - z-index: 5; - - ${NavSection} { - margin-bottom: 20px; - } - - ${WrapperButton} { - height: 50px; - width: 50px; - margin: 0; - } - - ${IconWrapper} { - margin-left: 0; - @media screen and (max-width: 1400px) { - } - } -`; diff --git a/apps/web/src/pages/templates/workflow/workflow/node-types/ChannelNode.tsx b/apps/web/src/pages/templates/workflow/workflow/node-types/ChannelNode.tsx index 6db5a83b879..528ae17d938 100644 --- a/apps/web/src/pages/templates/workflow/workflow/node-types/ChannelNode.tsx +++ b/apps/web/src/pages/templates/workflow/workflow/node-types/ChannelNode.tsx @@ -3,7 +3,8 @@ import { Handle, Position, getOutgoers, useReactFlow, useNodes } from 'react-flo import { ChannelTypeEnum, StepTypeEnum } from '@novu/shared'; import { WorkflowNode } from './WorkflowNode'; -import { useTemplateEditorContext } from '../../../editor/TemplateEditorProvider'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useBasePath } from '../../../hooks/useBasePath'; interface NodeData { Icon: React.FC; @@ -12,30 +13,26 @@ interface NodeData { tabKey: ChannelTypeEnum; index: number; testId: string; - onDelete: (id: string) => void; + onDelete: (uuid: string) => void; error: string; - setActivePage: (string) => void; active?: boolean; channelType: StepTypeEnum; + uuid: string; } export default memo( ({ data, selected, id, dragging }: { data: NodeData; selected: boolean; id: string; dragging: boolean }) => { - const { selectedNodeId } = useTemplateEditorContext(); const { getNode, getEdges, getNodes } = useReactFlow(); const nodes = useNodes(); const thisNode = getNode(id); const isParent = thisNode ? getOutgoers(thisNode, getNodes(), getEdges()).length : false; const noChildStyle = isParent ? {} : { border: 'none', background: 'transparent' }; const [count, setCount] = useState(0); + const navigate = useNavigate(); + const basePath = useBasePath(); + const { stepUuid = '' } = useParams<{ stepUuid: string }>(); useEffect(() => { - if (![StepTypeEnum.EMAIL, StepTypeEnum.IN_APP].includes(data.channelType)) { - setCount(0); - - return; - } - const items = nodes .filter((node) => node.type === 'channelNode') .filter((node) => { @@ -62,18 +59,22 @@ export default memo( return (
{ + data.onDelete(data.uuid); + }} tabKey={data.tabKey} channelType={data.channelType} Icon={data.Icon} label={data.label + (count > 0 ? ` (${count})` : '')} - active={id === selectedNodeId} + active={stepUuid === data.uuid} disabled={!data.active} id={id} index={data.index} dragging={dragging} + onClick={() => { + navigate(basePath + `/${data.channelType}/${data.uuid}`); + }} /> diff --git a/apps/web/src/pages/templates/workflow/workflow/node-types/WorkflowNode.tsx b/apps/web/src/pages/templates/workflow/workflow/node-types/WorkflowNode.tsx index ab808166f6a..0419577ee8b 100644 --- a/apps/web/src/pages/templates/workflow/workflow/node-types/WorkflowNode.tsx +++ b/apps/web/src/pages/templates/workflow/workflow/node-types/WorkflowNode.tsx @@ -3,7 +3,6 @@ import { Popover as MantinePopover, ActionIcon, createStyles, MantineTheme, Menu import styled from '@emotion/styled'; import { useFormContext } from 'react-hook-form'; import { ChannelTypeEnum, StepTypeEnum } from '@novu/shared'; - import { Text } from '../../../../../design-system/typography/text/Text'; import { Switch } from '../../../../../design-system/switch/Switch'; import { useStyles } from '../../../../../design-system/template-button/TemplateButton.styles'; @@ -48,10 +47,10 @@ interface ITemplateButtonProps { showDots?: boolean; id?: string; index?: number; - onDelete?: (id: string) => void; + onDelete?: () => void; dragging?: boolean; - setActivePage?: (string) => void; disabled?: boolean; + onClick?: () => void; } const useMenuStyles = createStyles((theme: MantineTheme) => { @@ -117,8 +116,8 @@ export function WorkflowNode({ id = undefined, onDelete = () => {}, dragging = false, - setActivePage = (page: string) => {}, disabled: initDisabled, + onClick = () => {}, }: ITemplateButtonProps) { const segment = useSegment(); const { readonly: readonlyEnv } = useEnvController(); @@ -176,10 +175,10 @@ export function WorkflowNode({ }, [watch]); useEffect(() => { - if (showDotMenu && (dragging || !active)) { + if (showDotMenu && dragging) { setShowDotMenu(false); } - }, [dragging, showDotMenu, active]); + }, [dragging, showDotMenu]); return ( <> @@ -222,6 +221,7 @@ export function WorkflowNode({ style={{ pointerEvents: 'all' }} component="span" onClick={(e) => { + e.stopPropagation(); e.preventDefault(); setShowDotMenu(!showDotMenu); }} @@ -252,7 +252,7 @@ export function WorkflowNode({ onClick={(e) => { e.stopPropagation(); setShowDotMenu(false); - setActivePage(tabKey === ChannelTypeEnum.IN_APP ? tabKey : capitalize(channelKey)); + onClick(); }} > Edit Template @@ -267,7 +267,7 @@ export function WorkflowNode({ data-test-id="delete-step-action" onClick={() => { setShowDotMenu(false); - onDelete(id || ''); + onDelete(); }} > Delete {isChannel ? 'Step' : 'Action'} diff --git a/apps/web/src/pages/user-preference/UserPreference.tsx b/apps/web/src/pages/user-preference/UserPreference.tsx deleted file mode 100644 index a0f2c4eb842..00000000000 --- a/apps/web/src/pages/user-preference/UserPreference.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Grid, useMantineColorScheme } from '@mantine/core'; -import styled from '@emotion/styled'; - -import { colors } from '../../design-system'; -import { useTemplateEditorForm } from '../templates/components/TemplateEditorFormProvider'; -import { TemplatesSideBar } from '../templates/components/TemplatesSideBar'; -import { TemplatePreference } from '../templates/components/notification-setting-form/TemplatePreference'; - -export function UserPreference({ activePage, setActivePage }) { - const { colorScheme } = useMantineColorScheme(); - const { template, trigger } = useTemplateEditorForm(); - - return ( -
- - - - - - - - -
- -
-
-
-
- ); -} - -const SideBarWrapper = styled.div<{ dark: boolean }>` - border-right: 1px solid ${({ dark }) => (dark ? colors.B20 : colors.BGLight)}; - height: 100%; -`; From db5d00456868a44007a9f7d85f8c787ca7a52616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Tue, 28 Mar 2023 16:14:33 +0200 Subject: [PATCH 002/152] feat: add filter modal to each channel view --- .../design-system/icons/actions/Filter.tsx | 12 ++ .../components/TemplatePushEditor.tsx | 9 +- .../components/TemplateSMSEditor.tsx | 6 +- .../chat-editor/TemplateChatEditor.tsx | 6 +- .../email-editor/EmailMessagesCards.tsx | 18 +- .../in-app-editor/TemplateInAppEditor.tsx | 12 +- .../workflow/SideBar/StepSettings.tsx | 198 ++++-------------- .../templates/workflow/WorkflowEditor.tsx | 20 -- 8 files changed, 69 insertions(+), 212 deletions(-) create mode 100644 apps/web/src/design-system/icons/actions/Filter.tsx diff --git a/apps/web/src/design-system/icons/actions/Filter.tsx b/apps/web/src/design-system/icons/actions/Filter.tsx new file mode 100644 index 00000000000..855246f7e56 --- /dev/null +++ b/apps/web/src/design-system/icons/actions/Filter.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +/* eslint-disable */ +export function Filter(props: React.ComponentPropsWithoutRef<'svg'>) { + return ( + + + + ); +} diff --git a/apps/web/src/pages/templates/components/TemplatePushEditor.tsx b/apps/web/src/pages/templates/components/TemplatePushEditor.tsx index 674a8c20736..2de91ceda70 100644 --- a/apps/web/src/pages/templates/components/TemplatePushEditor.tsx +++ b/apps/web/src/pages/templates/components/TemplatePushEditor.tsx @@ -6,9 +6,7 @@ import type { IForm } from './formTypes'; import { Textarea } from '../../../design-system'; import { useEnvController, useVariablesManager } from '../../../hooks'; import { VariableManager } from './VariableManager'; -import { Group } from '@mantine/core'; -import { StepActiveSwitch } from '../workflow/StepActiveSwitch'; -import { ShouldStopOnFailSwitch } from '../workflow/ShouldStopOnFailSwitch'; +import { StepSettings } from '../workflow/SideBar/StepSettings'; const templateFields = ['content', 'title']; @@ -31,10 +29,7 @@ export function TemplatePushEditor({ return ( <> {!isIntegrationActive ? : null} - - - - + {!isIntegrationActive ? : null} - - - - + {!isIntegrationActive ? : null} - - - - + (); useHotkeys([ [ @@ -74,11 +66,7 @@ export function EmailMessagesCards({ index, isIntegrationActive }: { index: numb position: 'relative', }} > - - - - - + diff --git a/apps/web/src/pages/templates/components/in-app-editor/TemplateInAppEditor.tsx b/apps/web/src/pages/templates/components/in-app-editor/TemplateInAppEditor.tsx index 6c888e8c3a2..32ccd755028 100644 --- a/apps/web/src/pages/templates/components/in-app-editor/TemplateInAppEditor.tsx +++ b/apps/web/src/pages/templates/components/in-app-editor/TemplateInAppEditor.tsx @@ -1,14 +1,13 @@ -import { Group, Stack } from '@mantine/core'; +import { Stack } from '@mantine/core'; import { Control, Controller, useFormContext } from 'react-hook-form'; -import { useState, useMemo } from 'react'; +import { useState } from 'react'; import type { IForm, ITemplates } from '../formTypes'; import { Input } from '../../../../design-system'; import { useEnvController, useVariablesManager } from '../../../../hooks'; import { InAppContentCard } from './InAppContentCard'; import { VariableManagerModal } from '../VariableManagerModal'; -import { StepActiveSwitch } from '../../workflow/StepActiveSwitch'; -import { ShouldStopOnFailSwitch } from '../../workflow/ShouldStopOnFailSwitch'; +import { StepSettings } from '../../workflow/SideBar/StepSettings'; const getVariableContents = (template: ITemplates) => { const baseContent = ['content']; @@ -36,10 +35,7 @@ export function TemplateInAppEditor({ control, index }: { control: Control - - - - + { - return typeof text !== 'string' ? '' : text.charAt(0).toUpperCase() + text.slice(1); -}; +import { useParams } from 'react-router-dom'; +import { StepTypeEnum } from '@novu/shared'; +import { When } from '../../../../components/utils/When'; +import { FilterModal } from '../../filter/FilterModal'; +import { useState } from 'react'; +import { Filter } from '../../../../design-system/icons/actions/Filter'; -export function StepSettings({ - setActivePage, - setFilterOpen, - isLoading, - isUpdateLoading, - loadingEditTemplate, - onDelete, -}: { - setActivePage: (string) => void; - setFilterOpen: (value: ((prevState: boolean) => boolean) | boolean) => void; - isLoading: boolean; - isUpdateLoading: boolean; - loadingEditTemplate: boolean; - onDelete: (id) => void; -}) { +export function StepSettings({ index }: { index: number }) { + const { readonly } = useEnvController(); const { control, - formState: { errors, isDirty }, + formState: { errors }, } = useFormContext(); - const { selectedNodeId, setSelectedNodeId, activeStep, activeStepIndex, selectedChannel } = - useTemplateEditorContext(); - const hasActiveStepSelected = activeStepIndex >= 0; - const { readonly } = useEnvController(); - const onSideMenuClose = () => setSelectedNodeId(''); - const isSubmitDisabled = readonly || loadingEditTemplate || isLoading || !isDirty; + const [filterOpen, setFilterOpen] = useState(false); + + const { channel } = useParams<{ + channel: StepTypeEnum; + }>(); return ( - - - - - - - {getChannel(selectedChannel ?? '')?.label} Properties - - - - - - -
- { - setActivePage( - selectedChannel === StepTypeEnum.IN_APP ? selectedChannel : capitalize(selectedChannel ?? '') - ); - }} - > - {readonly ? 'View' : 'Edit'} Template - -
- - - - - - - - - - -
- - - {activeStep && } - { - setFilterOpen(true); - }} - disabled={readonly} - data-test-id="add-filter-btn" - > - - Add filter - - + <> + + + + + + -
- + + + { + setFilterOpen(false); + }} + confirm={() => { + setFilterOpen(false); + }} + control={control} + stepIndex={index} + /> + ); } - -const ButtonWrapper = styled.div` - display: flex; - justify-content: space-between; -`; - -const EditTemplateButton = styled(Button)` - background-color: transparent; -`; - -const FilterButton = styled(Button)` - background: ${({ theme }) => (theme.colorScheme === 'dark' ? colors.B20 : colors.white)}; - box-shadow: 0px 5px 20px rgb(0 0 0 / 20%); - :hover { - background-color: ${({ theme }) => (theme.colorScheme === 'dark' ? colors.B20 : colors.white)}; - } -`; - -const MenuBar = styled.div` - display: flex; - justify-content: space-between; - flex-direction: column; - height: 100%; -`; - -const DeleteStepButton = styled(Button)` - //display: flex; - //position: inherit; - //bottom: 15px; - //left: 20px; - //right: 20px; - background: rgba(229, 69, 69, 0.15); - color: ${colors.error}; - box-shadow: none; - :hover { - background: rgba(229, 69, 69, 0.15); - } -`; diff --git a/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx b/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx index 6e2cf0ac0e3..34dba68e924 100644 --- a/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx +++ b/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx @@ -7,10 +7,7 @@ import { showNotification } from '@mantine/notifications'; import FlowEditor from './workflow/FlowEditor'; import { channels, getChannel, NodeTypeEnum } from '../shared/channels'; import type { IForm } from '../components/formTypes'; -import { When } from '../../../components/utils/When'; import { DeleteConfirmModal } from '../components/DeleteConfirmModal'; -import { FilterModal } from '../filter/FilterModal'; -import { useTemplateEditorContext } from '../editor/TemplateEditorProvider'; import { useTemplateEditorForm } from '../components/TemplateEditorFormProvider'; import { Outlet, useParams } from 'react-router-dom'; import { Container, TextInput } from '@mantine/core'; @@ -21,8 +18,6 @@ const WorkflowEditor = () => { const { channel } = useParams<{ channel: StepTypeEnum | undefined; }>(); - const { activeStepIndex } = useTemplateEditorContext(); - const hasActiveStepSelected = activeStepIndex >= 0; const [dragging, setDragging] = useState(false); const { @@ -32,8 +27,6 @@ const WorkflowEditor = () => { } = useFormContext(); const { readonly } = useEnvController(); const showErrors = isSubmitted && errors?.steps; - - const [filterOpen, setFilterOpen] = useState(false); const steps = watch('steps'); const [toDelete, setToDelete] = useState(''); @@ -154,19 +147,6 @@ const WorkflowEditor = () => { /> - - { - setFilterOpen(false); - }} - confirm={() => { - setFilterOpen(false); - }} - control={control} - stepIndex={activeStepIndex} - /> -
Date: Tue, 28 Mar 2023 17:33:05 +0200 Subject: [PATCH 003/152] refactor: remove not needed states --- apps/web/src/App.tsx | 31 +---- apps/web/src/constants/editorEnums.ts | 11 -- .../template-button/TemplateButton.tsx | 9 -- apps/web/src/hooks/useBlueprint.ts | 3 +- .../templates/components/BlueprintModal.tsx | 7 +- .../components/TemplateEditorFormProvider.tsx | 85 +++++++------ .../templates/components/TemplateSettings.tsx | 37 +++--- .../templates/components/TemplatesSideBar.tsx | 114 ------------------ .../NotificationSettingsForm.tsx | 50 +++----- .../components/useTemplateController.ts | 1 - .../editor/DigestWorkflowTourTooltip.tsx | 10 +- .../templates/editor/TemplateEditorPage.tsx | 49 +++----- .../editor/TemplateEditorProvider.tsx | 66 ---------- .../editor/useDigestWorkflowTour.tsx | 12 +- .../src/pages/templates/hooks/useBasePath.ts | 4 +- .../workflow/SideBar/AddStepMenu.tsx | 9 +- 16 files changed, 116 insertions(+), 382 deletions(-) delete mode 100644 apps/web/src/constants/editorEnums.ts delete mode 100644 apps/web/src/pages/templates/components/TemplatesSideBar.tsx delete mode 100644 apps/web/src/pages/templates/editor/TemplateEditorProvider.tsx diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index ae1f4af2920..53c09812861 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -34,8 +34,6 @@ import { NotificationCenter } from './pages/quick-start/steps/NotificationCenter import { FrameworkSetup } from './pages/quick-start/steps/FrameworkSetup'; import { Setup } from './pages/quick-start/steps/Setup'; import { Trigger } from './pages/quick-start/steps/Trigger'; -import { TemplateEditorProvider } from './pages/templates/editor/TemplateEditorProvider'; -import { TemplateEditorFormProvider } from './pages/templates/components/TemplateEditorFormProvider'; import { RequiredAuth } from './components/layout/RequiredAuth'; import { GetStarted } from './pages/quick-start/steps/GetStarted'; import { DigestPreview } from './pages/quick-start/steps/DigestPreview'; @@ -181,33 +179,8 @@ function App() { }> } /> } /> - - - - - - } - > - } /> - } /> - } /> - } /> - } /> - } /> - - - - - - - } - > + } /> + }> } /> } /> } /> diff --git a/apps/web/src/constants/editorEnums.ts b/apps/web/src/constants/editorEnums.ts deleted file mode 100644 index e6ceaeeed40..00000000000 --- a/apps/web/src/constants/editorEnums.ts +++ /dev/null @@ -1,11 +0,0 @@ -export enum ActivePageEnum { - SETTINGS = 'Settings', - WORKFLOW = 'Workflow', - USER_PREFERENCE = 'UserPreference', - SMS = 'Sms', - EMAIL = 'Email', - IN_APP = 'in_app', - PUSH = 'Push', - CHAT = 'Chat', - TRIGGER_SNIPPET = 'TriggerSnippet', -} diff --git a/apps/web/src/design-system/template-button/TemplateButton.tsx b/apps/web/src/design-system/template-button/TemplateButton.tsx index 44cf241011d..e3209a6bf54 100644 --- a/apps/web/src/design-system/template-button/TemplateButton.tsx +++ b/apps/web/src/design-system/template-button/TemplateButton.tsx @@ -9,7 +9,6 @@ import { useStyles } from './TemplateButton.styles'; import { colors } from '../config'; import { Button } from './Button'; import { IconWrapper } from './IconWrapper'; -import { ActivePageEnum } from '../../constants/editorEnums'; const usePopoverStyles = createStyles(() => ({ dropdown: { @@ -75,14 +74,6 @@ export function TemplateButton({ return; } - if (tabKey === ActivePageEnum.WORKFLOW) { - const valid = await trigger(['name', 'notificationGroupId'], { shouldFocus: true }); - - if (!valid) { - return; - } - } - changeTab(tabKey); }} data-test-id={testId} diff --git a/apps/web/src/hooks/useBlueprint.ts b/apps/web/src/hooks/useBlueprint.ts index 86d95c2a3f4..ae948341f93 100644 --- a/apps/web/src/hooks/useBlueprint.ts +++ b/apps/web/src/hooks/useBlueprint.ts @@ -3,7 +3,6 @@ import { useNavigate, useLocation } from 'react-router-dom'; import { useEffect } from 'react'; import { getToken } from './useAuthController'; import { useSegment } from '../components/providers/SegmentProvider'; -import { ActivePageEnum } from '../constants/editorEnums'; export const useBlueprint = () => { const searchParams = useSearchParams(); @@ -16,7 +15,7 @@ export const useBlueprint = () => { const token = getToken(); if (id && token !== null) { - navigate(`/templates/create?page=${ActivePageEnum.WORKFLOW}`, { + navigate(`/templates/create`, { replace: true, }); } diff --git a/apps/web/src/pages/templates/components/BlueprintModal.tsx b/apps/web/src/pages/templates/components/BlueprintModal.tsx index 4980939643b..c9b18d15e80 100644 --- a/apps/web/src/pages/templates/components/BlueprintModal.tsx +++ b/apps/web/src/pages/templates/components/BlueprintModal.tsx @@ -10,7 +10,6 @@ import { createTemplateFromBluePrintId, getBlueprintTemplateById } from '../../. import { errorMessage } from '../../../utils/notifications'; import { When } from '../../../components/utils/When'; import { useSegment } from '../../../components/providers/SegmentProvider'; -import { ActivePageEnum } from '../../../constants/editorEnums'; export function BlueprintModal() { const theme = useMantineTheme(); @@ -20,10 +19,10 @@ export function BlueprintModal() { segment.track('Blueprint canceled', { blueprintId: localStorage.getItem('blueprintId'), }); - localStorage.removeItem('blueprintId'); navigate('/templates', { replace: true, }); + localStorage.removeItem('blueprintId'); }; const { mutateAsync: updateOnBoardingStatus } = useMutation< @@ -54,13 +53,13 @@ export function BlueprintModal() { const { mutate, isLoading: isCreating } = useMutation(createTemplateFromBluePrintId, { onSuccess: (template) => { - localStorage.removeItem('blueprintId'); if (template) { disableOnboarding(); - navigate(`/templates/edit/${template?._id}?page=${ActivePageEnum.WORKFLOW}`, { + navigate(`/templates/edit/${template?._id}`, { replace: true, }); } + localStorage.removeItem('blueprintId'); }, onError: (err: any) => { if (err?.message) { diff --git a/apps/web/src/pages/templates/components/TemplateEditorFormProvider.tsx b/apps/web/src/pages/templates/components/TemplateEditorFormProvider.tsx index 53c6a72fd3e..ee1270dedef 100644 --- a/apps/web/src/pages/templates/components/TemplateEditorFormProvider.tsx +++ b/apps/web/src/pages/templates/components/TemplateEditorFormProvider.tsx @@ -1,7 +1,7 @@ import { createContext, useEffect, useMemo, useCallback, useContext, useState } from 'react'; import { FormProvider, useForm, useFieldArray } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { DigestTypeEnum, INotificationTemplate, INotificationTrigger } from '@novu/shared'; import * as Sentry from '@sentry/react'; import { StepTypeEnum, ActorTypeEnum, EmailBlockTypeEnum, IEmailBlock, TextAlignEnum } from '@novu/shared'; @@ -12,6 +12,7 @@ import { mapNotificationTemplateToForm, mapFormToCreateNotificationTemplate } fr import { errorMessage } from '../../../utils/notifications'; import { schema } from './notificationTemplateSchema'; import { v4 as uuid4 } from 'uuid'; +import { useNotificationGroup } from '../../../hooks'; const defaultEmailBlocks: IEmailBlock[] = [ { @@ -65,10 +66,9 @@ interface ITemplateEditorFormContext { isCreating: boolean; isUpdating: boolean; isDeleting: boolean; - editMode: boolean; trigger?: INotificationTrigger; createdTemplateId?: string; - onSubmit: (data: IForm, callbacks?: { onCreateSuccess: () => void }) => Promise; + onSubmit: (data: IForm) => Promise; addStep: (channelType: StepTypeEnum, id: string, stepIndex?: number) => void; deleteStep: (index: number) => void; } @@ -78,7 +78,6 @@ const TemplateEditorFormContext = createContext({ isCreating: false, isUpdating: false, isDeleting: false, - editMode: true, trigger: undefined, createdTemplateId: undefined, onSubmit: (() => {}) as any, @@ -87,7 +86,7 @@ const TemplateEditorFormContext = createContext({ }); const defaultValues: IForm = { - name: '', + name: 'Untitled', notificationGroupId: '', description: '', identifier: '', @@ -110,13 +109,11 @@ const TemplateEditorFormProvider = ({ children }) => { defaultValues, mode: 'onChange', }); - const [{ editMode, trigger, createdTemplateId }, setState] = useState<{ - editMode: boolean; + const navigate = useNavigate(); + const [{ trigger, createdTemplateId }, setState] = useState<{ trigger?: INotificationTrigger; createdTemplateId?: string; - }>({ - editMode: !!templateId, - }); + }>({}); const setTrigger = useCallback( (newTrigger: INotificationTrigger) => setState((old) => ({ ...old, trigger: newTrigger })), @@ -147,6 +144,7 @@ const TemplateEditorFormProvider = ({ children }) => { updateNotificationTemplate, createNotificationTemplate, } = useTemplateController(templateId); + const { groups, loading: loadingGroups } = useNotificationGroup(); useEffect(() => { if (isDirtyForm) { @@ -160,43 +158,50 @@ const TemplateEditorFormProvider = ({ children }) => { } }, [isDirtyForm, template]); + useEffect(() => { + if (!!templateId || groups.length === 0 || localStorage.getItem('blueprintId') !== null) { + return; + } + + const values = methods.getValues(); + + if (!values.notificationGroupId) { + values.notificationGroupId = groups[0]._id; + } + + const submit = async () => { + const payloadToCreate = mapFormToCreateNotificationTemplate(values); + const response = await createNotificationTemplate({ ...payloadToCreate, active: true, draft: false }); + setTrigger(response.triggers[0]); + setCreatedTemplateId(response._id || ''); + reset(payloadToCreate); + navigate(`/templates/edit/${response._id || ''}`); + }; + + submit(); + }, [templateId, groups, localStorage.getItem('blueprintId')]); + const onSubmit = useCallback( - async (form: IForm, { onCreateSuccess } = {}) => { + async (form: IForm) => { const payloadToCreate = mapFormToCreateNotificationTemplate(form); try { - if (editMode) { - const response = await updateNotificationTemplate({ - id: templateId, - data: { - ...payloadToCreate, - identifier: form.identifier, - }, - }); - setTrigger(response.triggers[0]); - reset(form); - } else { - const response = await createNotificationTemplate({ ...payloadToCreate, active: true, draft: false }); - setTrigger(response.triggers[0]); - setCreatedTemplateId(response._id || ''); - reset(payloadToCreate); - onCreateSuccess?.(); - } + const response = await updateNotificationTemplate({ + id: templateId, + data: { + ...payloadToCreate, + identifier: form.identifier, + }, + }); + setTrigger(response.triggers[0]); + reset(form); } catch (e: any) { Sentry.captureException(e); errorMessage(e.message || 'Unexpected error occurred'); } }, - [ - templateId, - editMode, - updateNotificationTemplate, - createNotificationTemplate, - reset, - setTrigger, - setCreatedTemplateId, - ] + [templateId, updateNotificationTemplate, reset, setTrigger] ); const addStep = useCallback( @@ -222,11 +227,10 @@ const TemplateEditorFormProvider = ({ children }) => { const value = useMemo( () => ({ template, - isLoading, + isLoading: isLoading || loadingGroups, isCreating, isUpdating, isDeleting, - editMode: editMode, trigger: trigger, createdTemplateId: createdTemplateId, onSubmit, @@ -239,12 +243,13 @@ const TemplateEditorFormProvider = ({ children }) => { isCreating, isUpdating, isDeleting, - editMode, + trigger, createdTemplateId, onSubmit, addStep, deleteStep, + loadingGroups, ] ); diff --git a/apps/web/src/pages/templates/components/TemplateSettings.tsx b/apps/web/src/pages/templates/components/TemplateSettings.tsx index 81119af438c..72c97fe2a86 100644 --- a/apps/web/src/pages/templates/components/TemplateSettings.tsx +++ b/apps/web/src/pages/templates/components/TemplateSettings.tsx @@ -17,7 +17,7 @@ import { WorkflowSettingsTabs } from './WorkflowSettingsTabs'; export const TemplateSettings = () => { const { templateId = '' } = useParams<{ templateId: string }>(); const { readonly } = useEnvController(); - const { editMode, trigger } = useTemplateEditorForm(); + const { trigger } = useTemplateEditorForm(); const [toDelete, setToDelete] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [isError, setIsError] = useState(undefined); @@ -51,24 +51,23 @@ export const TemplateSettings = () => { - {editMode && ( - - - - Delete Workflow - - - )} + + + + + Delete Workflow + + void; - showTriggerSection: boolean; - minimalView?: boolean; -}) { - const { errors } = useFormState<{ - name: string; - notificationGroupId: string; - }>(); - - const theme = useMantineTheme(); - const textColor = theme.colorScheme === 'dark' ? colors.B40 : colors.B70; - - return ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - {showTriggerSection && ( - - - - Implementation Code - - - - - - - - - - )} - - ); -} - -export const NavSection = styled.div``; - -const StyledTooltip = styled(Tooltip)` - width: 100%; - display: flex; -`; diff --git a/apps/web/src/pages/templates/components/notification-setting-form/NotificationSettingsForm.tsx b/apps/web/src/pages/templates/components/notification-setting-form/NotificationSettingsForm.tsx index 92b8097315b..d97a07ca3d4 100644 --- a/apps/web/src/pages/templates/components/notification-setting-form/NotificationSettingsForm.tsx +++ b/apps/web/src/pages/templates/components/notification-setting-form/NotificationSettingsForm.tsx @@ -1,4 +1,3 @@ -import { useEffect } from 'react'; import { ActionIcon, Grid, Stack } from '@mantine/core'; import { useClipboard } from '@mantine/hooks'; import { useMutation, useQueryClient } from '@tanstack/react-query'; @@ -25,7 +24,7 @@ export const NotificationSettingsForm = ({ trigger }: { trigger?: INotificationT getValues, } = useFormContext(); - const { template, editMode } = useTemplateEditorForm(); + const { template } = useTemplateEditorForm(); const { templateId = '' } = useParams<{ templateId: string }>(); const { isTemplateActive, changeActiveStatus, isStatusChangeLoading } = useStatusChangeControllerHook( @@ -46,19 +45,6 @@ export const NotificationSettingsForm = ({ trigger }: { trigger?: INotificationT }, }); - useEffect(() => { - const group = getValues('notificationGroupId'); - if (groups?.length && !editMode && !group) { - selectFirstGroupByDefault(); - } - }, [groups, editMode]); - - function selectFirstGroupByDefault() { - setTimeout(() => { - setValue('notificationGroupId', groups[0]._id); - }, 500); - } - function addGroupItem(newGroup: string): undefined { if (newGroup) { createNotificationGroup({ @@ -79,23 +65,21 @@ export const NotificationSettingsForm = ({ trigger }: { trigger?: INotificationT <> - {editMode && ( - - changeActiveStatus(e.target.checked)} - checked={isTemplateActive || false} - /> - - )} + + changeActiveStatus(e.target.checked)} + checked={isTemplateActive || false} + /> + (
+ Create Group {newGroup}
@@ -136,7 +119,6 @@ export const NotificationSettingsForm = ({ trigger }: { trigger?: INotificationT {...field} data-test-id="title" disabled={readonly} - required={!editMode} value={field.value || ''} error={errors.name?.message} label="Name" diff --git a/apps/web/src/pages/templates/components/useTemplateController.ts b/apps/web/src/pages/templates/components/useTemplateController.ts index d0d3ff44f05..0ba4c5f069e 100644 --- a/apps/web/src/pages/templates/components/useTemplateController.ts +++ b/apps/web/src/pages/templates/components/useTemplateController.ts @@ -17,7 +17,6 @@ export function useTemplateController(templateId?: string) { >(createTemplate, { onSuccess: async () => { await client.refetchQueries([QueryKeys.changesCount]); - successMessage('Template saved successfully'); }, }); diff --git a/apps/web/src/pages/templates/editor/DigestWorkflowTourTooltip.tsx b/apps/web/src/pages/templates/editor/DigestWorkflowTourTooltip.tsx index 12e2dcef77a..97c9f2e84d4 100644 --- a/apps/web/src/pages/templates/editor/DigestWorkflowTourTooltip.tsx +++ b/apps/web/src/pages/templates/editor/DigestWorkflowTourTooltip.tsx @@ -8,10 +8,10 @@ import { StepTypeEnum } from '@novu/shared'; import { useTour } from './TourProvider'; import { Button, colors, DotsNavigation } from '../../../design-system'; import { Clock, LetterOpened, BellWithNotification } from '../../../design-system/icons'; -import { useTemplateEditorContext } from './TemplateEditorProvider'; import { IForm } from '../components/formTypes'; import { useSegment } from '../../../components/providers/SegmentProvider'; import { DigestWorkflowTourAnalyticsEnum, HINT_INDEX_TO_CLICK_ANALYTICS, ordinalNumbers } from '../constants'; +import { useBasePath } from '../hooks/useBasePath'; const ICONS = [Clock, LetterOpened, BellWithNotification]; const TITLE = ['Set-up time interval', 'Set-up email content', 'Test your workflow']; @@ -72,26 +72,26 @@ export const DigestWorkflowTourTooltip = ({ size, }: TooltipRenderProps) => { const segment = useSegment(); - const { setSelectedNodeId } = useTemplateEditorContext(); const { watch } = useFormContext(); const steps = watch('steps'); const { stopTour, setStep } = useTour(); const location = useLocation(); const navigate = useNavigate(); const Icon = ICONS[index]; + const basePath = useBasePath(); const handleOnClick = (tourStepIndex: number, isFromNavigation = false) => { if (tourStepIndex === 0) { const digestStep = steps.find((el) => el.template?.type === StepTypeEnum.DIGEST); setStep(tourStepIndex); - setSelectedNodeId(digestStep?.id || ''); + navigate(basePath + '/' + StepTypeEnum.DIGEST + '/' + digestStep?.uuid); } else if (tourStepIndex === 1) { const emailStep = steps.find((el) => el.template?.type === StepTypeEnum.EMAIL); setStep(tourStepIndex); - setSelectedNodeId(emailStep?.id || ''); + navigate(basePath + '/' + StepTypeEnum.EMAIL + '/' + emailStep?.uuid); } else if (tourStepIndex === 2) { setStep(tourStepIndex); - setSelectedNodeId(''); + navigate(basePath); } const stepIndex = isFromNavigation ? tourStepIndex : index; diff --git a/apps/web/src/pages/templates/editor/TemplateEditorPage.tsx b/apps/web/src/pages/templates/editor/TemplateEditorPage.tsx index c95f1f7dd91..19e900eacc8 100644 --- a/apps/web/src/pages/templates/editor/TemplateEditorPage.tsx +++ b/apps/web/src/pages/templates/editor/TemplateEditorPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { useDisclosure } from '@mantine/hooks'; import { ReactFlowProvider } from 'react-flow-renderer'; @@ -8,32 +8,20 @@ import PageContainer from '../../../components/layout/components/PageContainer'; import PageMeta from '../../../components/layout/components/PageMeta'; import type { IForm } from '../components/formTypes'; import WorkflowEditor from '../workflow/WorkflowEditor'; -import { useSearchParams, useEnvController } from '../../../hooks'; +import { useEnvController } from '../../../hooks'; import { SaveChangesModal } from '../components/SaveChangesModal'; import { BlueprintModal } from '../components/BlueprintModal'; -import { useTemplateEditorForm } from '../components/TemplateEditorFormProvider'; +import { TemplateEditorFormProvider, useTemplateEditorForm } from '../components/TemplateEditorFormProvider'; import { errorMessage } from '../../../utils/notifications'; import { getExplicitErrors } from '../shared/errors'; import { ROUTES } from '../../../constants/routes.enum'; -import { ActivePageEnum } from '../../../constants/editorEnums'; -import { useTemplateEditorContext } from './TemplateEditorProvider'; import { TourProvider } from './TourProvider'; -export const EditorPages = [ - ActivePageEnum.CHAT, - ActivePageEnum.SMS, - ActivePageEnum.PUSH, - ActivePageEnum.EMAIL, - ActivePageEnum.IN_APP, -]; - -export default function TemplateEditorPage() { +function BaseTemplateEditorPage() { const navigate = useNavigate(); const location = useLocation(); const { environment } = useEnvController(); - const [isTriggerModalVisible, setTriggerModalVisible] = useState(false); - const { template, isCreating, isUpdating, editMode, onSubmit } = useTemplateEditorForm(); - const { setActivePage } = useTemplateEditorContext(); + const { template, isCreating, isUpdating, onSubmit } = useTemplateEditorForm(); const methods = useFormContext(); const { handleSubmit } = methods; @@ -45,28 +33,13 @@ export default function TemplateEditorPage() { const [saveChangesModalOpened, { close: closeSaveChangesModal, open: openSaveChangesModal }] = useDisclosure(false); - const searchParams = useSearchParams(); - - useEffect(() => { - const page = searchParams.page; - if (page !== ActivePageEnum.WORKFLOW) { - return; - } - - setActivePage(page); - }, [searchParams.page]); - const onConfirmSaveChanges = async (data: IForm) => { await onSubmit(data); closeSaveChangesModal(); }; const onSubmitHandler = async (data: IForm) => { - await onSubmit(data, { - onCreateSuccess: () => { - setTriggerModalVisible(true); - }, - }); + await onSubmit(data); }; useEffect(() => { @@ -91,7 +64,7 @@ export default function TemplateEditorPage() { <> - +
); } + +export default function TemplateEditorPage() { + return ( + + + + ); +} diff --git a/apps/web/src/pages/templates/editor/TemplateEditorProvider.tsx b/apps/web/src/pages/templates/editor/TemplateEditorProvider.tsx deleted file mode 100644 index 39091b15737..00000000000 --- a/apps/web/src/pages/templates/editor/TemplateEditorProvider.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { useMemo, useState, useCallback } from 'react'; -import { useFormContext } from 'react-hook-form'; -import { StepTypeEnum } from '@novu/shared'; - -import { ActivePageEnum } from '../../../constants/editorEnums'; -import { IForm, IStepEntity } from '../components/formTypes'; - -interface ITemplateEditorContext { - activePage: ActivePageEnum; - setActivePage: (page: ActivePageEnum) => void; - selectedNodeId: string; - setSelectedNodeId: (nodeId: string) => void; - activeStepIndex: number; - activeStep?: IStepEntity; - selectedChannel?: StepTypeEnum; -} - -const TemplateEditorContext = React.createContext({ - activePage: ActivePageEnum.SETTINGS, - setActivePage: () => {}, - selectedNodeId: '', - setSelectedNodeId: () => {}, - activeStepIndex: -1, -}); - -export const useTemplateEditorContext = () => React.useContext(TemplateEditorContext); - -export const TemplateEditorProvider = ({ children }) => { - const [activePage, setActivePage] = useState(ActivePageEnum.WORKFLOW); - const [selectedNodeId, setSelectedNodeId] = useState(''); - const { watch } = useFormContext(); - const steps = watch('steps'); - - const activeStepIndex = useMemo(() => { - if (selectedNodeId.length === 0) { - return -1; - } - - return steps.findIndex((item) => item._id === selectedNodeId || item.id === selectedNodeId); - }, [selectedNodeId, steps]); - - const activeStep: IStepEntity | undefined = steps[activeStepIndex]; - - const setActivePageCallback = useCallback((page: ActivePageEnum) => { - setActivePage(page); - }, []); - - const setSelectedNodeIdCallback = useCallback((id: string) => { - setSelectedNodeId(id); - }, []); - - const value = useMemo( - () => ({ - activePage, - setActivePage: setActivePageCallback, - selectedNodeId, - setSelectedNodeId: setSelectedNodeIdCallback, - activeStepIndex, - activeStep, - selectedChannel: activeStep?.template.type, - }), - [activePage, selectedNodeId, activeStepIndex, activeStep, setActivePageCallback, setSelectedNodeIdCallback] - ); - - return {children}; -}; diff --git a/apps/web/src/pages/templates/editor/useDigestWorkflowTour.tsx b/apps/web/src/pages/templates/editor/useDigestWorkflowTour.tsx index 901ab0a216f..dd18c9c9243 100644 --- a/apps/web/src/pages/templates/editor/useDigestWorkflowTour.tsx +++ b/apps/web/src/pages/templates/editor/useDigestWorkflowTour.tsx @@ -1,14 +1,13 @@ import { useEffect } from 'react'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { Step } from 'react-joyride'; import { useFormContext } from 'react-hook-form'; import { StepTypeEnum } from '@novu/shared'; -import { useTemplateEditorContext } from './TemplateEditorProvider'; -import { ActivePageEnum } from '../../../constants/editorEnums'; import { useEffectOnce } from '../../../hooks'; import { IForm } from '../components/formTypes'; import { DigestWorkflowTourTooltip } from './DigestWorkflowTourTooltip'; +import { useBasePath } from '../hooks/useBasePath'; const digestTourSteps: Step[] = [ { @@ -44,16 +43,17 @@ const digestTourSteps: Step[] = [ ]; export const useDigestWorkflowTour = ({ startTour }: { startTour: () => void }) => { - const { setSelectedNodeId, setActivePage } = useTemplateEditorContext(); const location = useLocation(); const queryParams = new URLSearchParams(location.search); const isDigestTour = queryParams.get('tour') == 'digest'; const { watch } = useFormContext(); const steps = watch('steps'); + const navigate = useNavigate(); + const basePath = useBasePath(); useEffect(() => { if (isDigestTour) { - setActivePage(ActivePageEnum.WORKFLOW); + navigate(basePath + '/'); } }, []); @@ -62,7 +62,7 @@ export const useDigestWorkflowTour = ({ startTour }: { startTour: () => void }) const digestStep = steps.find((step) => step.template?.type === StepTypeEnum.DIGEST); if (digestStep) { setTimeout(() => { - setSelectedNodeId(digestStep.id || ''); + navigate(basePath + '/' + StepTypeEnum.DIGEST + '/' + digestStep?.uuid); startTour(); }, 0); } diff --git a/apps/web/src/pages/templates/hooks/useBasePath.ts b/apps/web/src/pages/templates/hooks/useBasePath.ts index 0ff4e7c4c1d..6d6f78657dd 100644 --- a/apps/web/src/pages/templates/hooks/useBasePath.ts +++ b/apps/web/src/pages/templates/hooks/useBasePath.ts @@ -1,9 +1,7 @@ import { useParams } from 'react-router-dom'; -import { useTemplateEditorForm } from '../components/TemplateEditorFormProvider'; export const useBasePath = () => { - const { editMode } = useTemplateEditorForm(); const { templateId = '' } = useParams<{ templateId: string }>(); - return editMode ? `/templates/edit/${templateId}` : '/templates/create'; + return `/templates/edit/${templateId}`; }; diff --git a/apps/web/src/pages/templates/workflow/SideBar/AddStepMenu.tsx b/apps/web/src/pages/templates/workflow/SideBar/AddStepMenu.tsx index d6715691629..036e385fb65 100644 --- a/apps/web/src/pages/templates/workflow/SideBar/AddStepMenu.tsx +++ b/apps/web/src/pages/templates/workflow/SideBar/AddStepMenu.tsx @@ -4,7 +4,6 @@ import { colors, DragButton, Text } from '../../../../design-system'; import { channels, NodeTypeEnum } from '../../shared/channels'; import { useEnvController } from '../../../../hooks'; import { When } from '../../../../components/utils/When'; -import { NavSection } from '../../components/TemplatesSideBar'; import { StyledNav } from '../WorkflowEditor'; export function AddStepMenu({ @@ -18,11 +17,11 @@ export function AddStepMenu({ return ( - +
Channels - +
{channels @@ -32,7 +31,7 @@ export function AddStepMenu({ ))} - Actions - +
{channels From 05c500a81d6de167c75b603a20e68473df645eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Wed, 29 Mar 2023 06:59:08 +0200 Subject: [PATCH 004/152] feat: change dot menu to delete button only --- .../components/TemplateSMSEditor.tsx | 3 - .../chat-editor/TemplateChatEditor.tsx | 3 - .../workflow/workflow/FlowEditor.tsx | 8 +- .../workflow/node-types/ChannelNode.tsx | 8 +- .../workflow/node-types/TriggerNode.tsx | 2 +- .../workflow/node-types/WorkflowNode.tsx | 143 +++--------------- 6 files changed, 29 insertions(+), 138 deletions(-) diff --git a/apps/web/src/pages/templates/components/TemplateSMSEditor.tsx b/apps/web/src/pages/templates/components/TemplateSMSEditor.tsx index 6cc8490e66c..b26f9ddce49 100644 --- a/apps/web/src/pages/templates/components/TemplateSMSEditor.tsx +++ b/apps/web/src/pages/templates/components/TemplateSMSEditor.tsx @@ -6,9 +6,6 @@ import type { IForm } from './formTypes'; import { Textarea } from '../../../design-system'; import { useEnvController, useVariablesManager } from '../../../hooks'; import { VariableManager } from './VariableManager'; -import { Group } from '@mantine/core'; -import { StepActiveSwitch } from '../workflow/StepActiveSwitch'; -import { ShouldStopOnFailSwitch } from '../workflow/ShouldStopOnFailSwitch'; import { StepSettings } from '../workflow/SideBar/StepSettings'; const templateFields = ['content']; diff --git a/apps/web/src/pages/templates/components/chat-editor/TemplateChatEditor.tsx b/apps/web/src/pages/templates/components/chat-editor/TemplateChatEditor.tsx index 9b51fe2c29e..37015695d80 100644 --- a/apps/web/src/pages/templates/components/chat-editor/TemplateChatEditor.tsx +++ b/apps/web/src/pages/templates/components/chat-editor/TemplateChatEditor.tsx @@ -6,9 +6,6 @@ import type { IForm } from '../formTypes'; import { LackIntegrationError } from '../LackIntegrationError'; import { Textarea } from '../../../../design-system'; import { VariableManager } from '../VariableManager'; -import { Group } from '@mantine/core'; -import { StepActiveSwitch } from '../../workflow/StepActiveSwitch'; -import { ShouldStopOnFailSwitch } from '../../workflow/ShouldStopOnFailSwitch'; import { StepSettings } from '../../workflow/SideBar/StepSettings'; const templateFields = ['content']; diff --git a/apps/web/src/pages/templates/workflow/workflow/FlowEditor.tsx b/apps/web/src/pages/templates/workflow/workflow/FlowEditor.tsx index c06e02615fa..870e25b0dfe 100644 --- a/apps/web/src/pages/templates/workflow/workflow/FlowEditor.tsx +++ b/apps/web/src/pages/templates/workflow/workflow/FlowEditor.tsx @@ -77,7 +77,7 @@ export function FlowEditor({ const clientWidth = reactFlowWrapper.current?.clientWidth; const middle = clientWidth ? clientWidth / 2 - 100 : 0; const zoomView = 1; - const xyPos = reactFlowInstance?.project({ x: middle, y: 0 }); + const xyPos = reactFlowInstance?.project({ x: middle, y: 2 }); setViewport({ x: xyPos?.x ?? middle, y: xyPos?.y ?? 0, zoom: zoomView }, { duration: 800 }); }, [reactFlowInstance]); @@ -105,8 +105,10 @@ export function FlowEditor({ const onNodeClick = useCallback((event, node) => { event.preventDefault(); - navigate(basePath + `/${node.data.channelType}/${node.data.uuid}`); - if (node.id === '1') { + if (node.type === 'channelNode') { + navigate(basePath + `/${node.data.channelType}/${node.data.uuid}`); + } + if (node.type === 'triggerNode') { navigate(basePath + '/testworkflow'); } }, []); diff --git a/apps/web/src/pages/templates/workflow/workflow/node-types/ChannelNode.tsx b/apps/web/src/pages/templates/workflow/workflow/node-types/ChannelNode.tsx index 528ae17d938..7b27972b7d7 100644 --- a/apps/web/src/pages/templates/workflow/workflow/node-types/ChannelNode.tsx +++ b/apps/web/src/pages/templates/workflow/workflow/node-types/ChannelNode.tsx @@ -3,8 +3,7 @@ import { Handle, Position, getOutgoers, useReactFlow, useNodes } from 'react-flo import { ChannelTypeEnum, StepTypeEnum } from '@novu/shared'; import { WorkflowNode } from './WorkflowNode'; -import { useNavigate, useParams } from 'react-router-dom'; -import { useBasePath } from '../../../hooks/useBasePath'; +import { useParams } from 'react-router-dom'; interface NodeData { Icon: React.FC; @@ -28,8 +27,6 @@ export default memo( const isParent = thisNode ? getOutgoers(thisNode, getNodes(), getEdges()).length : false; const noChildStyle = isParent ? {} : { border: 'none', background: 'transparent' }; const [count, setCount] = useState(0); - const navigate = useNavigate(); - const basePath = useBasePath(); const { stepUuid = '' } = useParams<{ stepUuid: string }>(); useEffect(() => { @@ -72,9 +69,6 @@ export default memo( id={id} index={data.index} dragging={dragging} - onClick={() => { - navigate(basePath + `/${data.channelType}/${data.uuid}`); - }} /> diff --git a/apps/web/src/pages/templates/workflow/workflow/node-types/TriggerNode.tsx b/apps/web/src/pages/templates/workflow/workflow/node-types/TriggerNode.tsx index 2d823401ee8..c318c4e197c 100644 --- a/apps/web/src/pages/templates/workflow/workflow/node-types/TriggerNode.tsx +++ b/apps/web/src/pages/templates/workflow/workflow/node-types/TriggerNode.tsx @@ -13,7 +13,7 @@ export default memo(({ selected }: { selected: boolean }) => { return (
{ - return typeof text !== 'string' ? '' : text.charAt(0).toUpperCase() + text.slice(1); -}; - interface ITemplateButtonProps { Icon: React.FC; label: string; @@ -44,43 +39,14 @@ interface ITemplateButtonProps { switchButton?: (boolean) => void; changeTab?: (string) => void; errors?: boolean | string; - showDots?: boolean; + showDelete?: boolean; id?: string; index?: number; onDelete?: () => void; dragging?: boolean; disabled?: boolean; - onClick?: () => void; } -const useMenuStyles = createStyles((theme: MantineTheme) => { - const dark = theme.colorScheme === 'dark'; - - return { - arrow: { - width: '7px', - height: '7px', - backgroundColor: dark ? colors.B20 : colors.white, - borderColor: dark ? colors.B30 : colors.B85, - }, - dropdown: { - minWidth: 220, - backgroundColor: dark ? colors.B20 : colors.white, - color: dark ? theme.white : colors.B40, - borderColor: dark ? colors.B30 : colors.B85, - }, - item: { - borerRadius: '5px', - color: `${dark ? theme.white : colors.B40} !important`, - fontWeight: 400, - fontSize: '14px', - }, - itemHovered: { - backgroundColor: dark ? colors.B30 : colors.B98, - }, - }; -}); - const usePopoverStyles = createStyles(() => ({ dropdown: { padding: '12px 15px 14px', @@ -112,20 +78,17 @@ export function WorkflowNode({ index, testId, errors: initialErrors = false, - showDots = true, + showDelete = true, id = undefined, onDelete = () => {}, dragging = false, disabled: initDisabled, - onClick = () => {}, }: ITemplateButtonProps) { const segment = useSegment(); const { readonly: readonlyEnv } = useEnvController(); const { integrations } = useActiveIntegrations({ refetchOnMount: false, refetchOnWindowFocus: false }); const { cx, classes, theme } = useStyles(); - const { classes: menuClasses } = useMenuStyles(); const [popoverOpened, setPopoverOpened] = useState(false); - const [showDotMenu, setShowDotMenu] = useState(false); const [disabled, setDisabled] = useState(initDisabled); const [isIntegrationsModalVisible, setIntegrationsModalVisible] = useState(false); const disabledColor = disabled ? { color: theme.colorScheme === 'dark' ? colors.B40 : colors.B70 } : {}; @@ -133,8 +96,8 @@ export function WorkflowNode({ const { classes: popoverClasses } = usePopoverStyles(); const viewport = useViewport(); const channelKey = tabKey ?? ''; - const isChannel = getChannel(channelKey)?.type === NodeTypeEnum.CHANNEL; const { isLimitReached } = useIntegrationLimit(ChannelTypeEnum.EMAIL); + const [hover, setHover] = useState(false); const hasActiveIntegration = useMemo(() => { const isChannelStep = [StepTypeEnum.EMAIL, StepTypeEnum.PUSH, StepTypeEnum.SMS, StepTypeEnum.CHAT].includes( @@ -174,18 +137,18 @@ export function WorkflowNode({ return () => subscription.unsubscribe(); }, [watch]); - useEffect(() => { - if (showDotMenu && dragging) { - setShowDotMenu(false); - } - }, [dragging, showDotMenu]); - return ( <> setPopoverOpened(true)} - onMouseLeave={() => setPopoverOpened(false)} + onMouseEnter={() => { + setPopoverOpened(true); + setHover(true); + }} + onMouseLeave={() => { + setPopoverOpened(false); + setHover(false); + }} data-test-id={testId} className={cx(classes.button, { [classes.active]: active })} > @@ -203,77 +166,15 @@ export function WorkflowNode({ {action && !readonly && ( switchButton && switchButton(e.target.checked)} /> )} - - setShowDotMenu(false)} - clickOutsideEvents={MENU_CLICK_OUTSIDE_EVENTS} + + { + e.stopPropagation(); + onDelete(); + }} > - - { - e.stopPropagation(); - e.preventDefault(); - setShowDotMenu(!showDotMenu); - }} - > - - - - - - - } - data-test-id="edit-step-action" - onClick={(e) => { - e.stopPropagation(); - setShowDotMenu(false); - onClick(); - }} - > - Edit Template - - - } - data-test-id="delete-step-action" - onClick={() => { - setShowDotMenu(false); - onDelete(); - }} - > - Delete {isChannel ? 'Step' : 'Action'} - - - + + From 59e1e563eb78f4d5cb6bc7a9d48919a4a5bce9f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Wed, 29 Mar 2023 12:09:58 +0200 Subject: [PATCH 005/152] fix: Copy for delete modal and step fields --- .../icons/general/InfoCircle.tsx | 19 ++ apps/web/src/design-system/input/Input.tsx | 3 +- .../segmented-control/SegmentedControl.tsx | 1 + .../components/DeleteConfirmModal.tsx | 18 +- .../templates/components/SubPageWrapper.tsx | 1 + .../templates/components/TemplateEditor.tsx | 210 ++++++++++-------- .../templates/components/TemplateSettings.tsx | 16 ++ .../in-app-editor/TemplateInAppEditor.tsx | 1 - .../templates/workflow/DelayMetadata.tsx | 65 +++--- .../templates/workflow/DigestMetadata.tsx | 177 ++++++--------- .../templates/workflow/IntervalRadios.tsx | 47 ++++ .../templates/workflow/LabelWithTooltip.tsx | 20 ++ .../workflow/ShouldStopOnFailSwitch.tsx | 2 +- .../workflow/SideBar/StepSettings.tsx | 4 +- .../templates/workflow/StepActiveSwitch.tsx | 2 +- .../templates/workflow/WorkflowEditor.tsx | 29 +-- 16 files changed, 364 insertions(+), 251 deletions(-) create mode 100644 apps/web/src/design-system/icons/general/InfoCircle.tsx create mode 100644 apps/web/src/pages/templates/workflow/IntervalRadios.tsx create mode 100644 apps/web/src/pages/templates/workflow/LabelWithTooltip.tsx diff --git a/apps/web/src/design-system/icons/general/InfoCircle.tsx b/apps/web/src/design-system/icons/general/InfoCircle.tsx new file mode 100644 index 00000000000..53fadfb6730 --- /dev/null +++ b/apps/web/src/design-system/icons/general/InfoCircle.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +/* eslint-disable */ +export function InfoCircle(props: React.ComponentPropsWithoutRef<'svg'>) { + return ( + + + + + + + + + + + ); +} diff --git a/apps/web/src/design-system/input/Input.tsx b/apps/web/src/design-system/input/Input.tsx index 4fa12ecf6f1..67f2d1ba013 100644 --- a/apps/web/src/design-system/input/Input.tsx +++ b/apps/web/src/design-system/input/Input.tsx @@ -1,5 +1,5 @@ import React, { ChangeEvent, FocusEvent } from 'react'; -import { TextInputProps, TextInput as MantineTextInput } from '@mantine/core'; +import { TextInputProps, TextInput as MantineTextInput, Sx, Styles } from '@mantine/core'; import { inputStyles } from '../config/inputs.styles'; import { SpacingProps } from '../shared/spacing.props'; @@ -18,6 +18,7 @@ interface IInputProps extends SpacingProps { min?: string | number; max?: string | number; onBlur?: (event: FocusEvent) => void; + styles?: Styles>; } /** diff --git a/apps/web/src/design-system/segmented-control/SegmentedControl.tsx b/apps/web/src/design-system/segmented-control/SegmentedControl.tsx index 344ef388db7..cc016d09193 100644 --- a/apps/web/src/design-system/segmented-control/SegmentedControl.tsx +++ b/apps/web/src/design-system/segmented-control/SegmentedControl.tsx @@ -18,6 +18,7 @@ interface ISegmentedControlProps { loading?: boolean; fullWidth?: boolean; sx?: Sx | (Sx | undefined)[]; + disabled?: boolean; } /** diff --git a/apps/web/src/pages/templates/components/DeleteConfirmModal.tsx b/apps/web/src/pages/templates/components/DeleteConfirmModal.tsx index 745c67adb5e..f10e2e29d8c 100644 --- a/apps/web/src/pages/templates/components/DeleteConfirmModal.tsx +++ b/apps/web/src/pages/templates/components/DeleteConfirmModal.tsx @@ -4,18 +4,26 @@ import { Button, colors, shadows, Title, Text } from '../../../design-system'; export function DeleteConfirmModal({ target, + title, + description, isOpen, cancel, confirm, + confirmButtonText = 'Yes', + cancelButtonText = 'No', isLoading, error, }: { - target: string; + target?: string; isOpen: boolean; cancel: () => void; confirm: () => void; isLoading?: boolean; error?: string; + title?: string; + description?: string; + confirmButtonText?: string; + cancelButtonText?: string; }) { const theme = useMantineTheme(); @@ -36,7 +44,7 @@ export function DeleteConfirmModal({ paddingTop: '180px', }, }} - title={Delete {target}} + title={{title ? title : `Delete${target ? ' ' + target : ''}`}} sx={{ backdropFilter: 'blur(10px)' }} shadow={theme.colorScheme === 'dark' ? shadows.dark : shadows.medium} radius="md" @@ -56,13 +64,13 @@ export function DeleteConfirmModal({ {error} )} - Would you like to delete this {target}? + {description ? description : `Would you like to delete this${target ? ' ' + target : ''}?`}
diff --git a/apps/web/src/pages/templates/components/SubPageWrapper.tsx b/apps/web/src/pages/templates/components/SubPageWrapper.tsx index 5dedf64a0b8..481266839c9 100644 --- a/apps/web/src/pages/templates/components/SubPageWrapper.tsx +++ b/apps/web/src/pages/templates/components/SubPageWrapper.tsx @@ -28,6 +28,7 @@ export const SubPageWrapper = ({ borderRadius: '0 7px 7px 0 ', height: '100%', width: 460, + position: 'relative', ...style, }} > diff --git a/apps/web/src/pages/templates/components/TemplateEditor.tsx b/apps/web/src/pages/templates/components/TemplateEditor.tsx index f345daff157..6f780b2767f 100644 --- a/apps/web/src/pages/templates/components/TemplateEditor.tsx +++ b/apps/web/src/pages/templates/components/TemplateEditor.tsx @@ -8,15 +8,17 @@ import type { IForm } from './formTypes'; import { TemplatePushEditor } from './TemplatePushEditor'; import { TemplateChatEditor } from './chat-editor/TemplateChatEditor'; import { useActiveIntegrations, useEnvController } from '../../../hooks'; -import { useOutletContext, useParams } from 'react-router-dom'; +import { useNavigate, useOutletContext, useParams } from 'react-router-dom'; import { SubPageWrapper } from './SubPageWrapper'; import { DigestMetadata } from '../workflow/DigestMetadata'; import { DelayMetadata } from '../workflow/DelayMetadata'; import { Button, colors } from '../../../design-system'; import styled from '@emotion/styled'; import { Bell, Chat, DigestGradient, Mail, Mobile, Sms, TimerGradient, Trash } from '../../../design-system/icons'; -import { getChannel, NodeTypeEnum } from '../shared/channels'; import { Group } from '@mantine/core'; +import { When } from '../../../components/utils/When'; +import { useEffect, useMemo } from 'react'; +import { useBasePath } from '../hooks/useBasePath'; const getPageTitle = (channel: StepTypeEnum) => { if (channel === StepTypeEnum.EMAIL) { @@ -78,6 +80,70 @@ const getPageTitle = (channel: StepTypeEnum) => { return channel; }; +const DeleteRow = () => { + const { channel, stepUuid = '' } = useParams<{ + channel: StepTypeEnum; + stepUuid: string; + }>(); + const { readonly } = useEnvController(); + const { onDelete }: any = useOutletContext(); + + if (!channel) { + return null; + } + + return ( + + +
+ + + + Learn more in the docs + + + + + Learn more in the docs + + + { + onDelete(stepUuid); + }} + disabled={readonly} + > + + Delete Step + + + ); +}; + export const TemplateEditor = () => { const { channel, stepUuid = '' } = useParams<{ channel: StepTypeEnum | undefined; @@ -90,11 +156,23 @@ export const TemplateEditor = () => { watch, } = useFormContext(); const steps = watch('steps'); - const { readonly } = useEnvController(); - const { onDelete }: any = useOutletContext(); + const index = useMemo( + () => steps.findIndex((message) => message.template.type === channel && message.uuid === stepUuid), + [channel, stepUuid, steps] + ); + + const navigate = useNavigate(); + const basePath = useBasePath(); - if (channel === undefined) { + useEffect(() => { + if (index > -1) { + return; + } + navigate(basePath); + }, [index]); + + if (index === -1 || channel === undefined) { return null; } @@ -103,13 +181,10 @@ export const TemplateEditor = () => { - {steps.map((message, index) => { - return message.template.type === StepTypeEnum.IN_APP && message.uuid === stepUuid ? ( - - ) : null; - })} + + ); } @@ -119,92 +194,49 @@ export const TemplateEditor = () => { - {steps.map((message, index) => { - return message.template.type === StepTypeEnum.EMAIL && message.uuid === stepUuid ? ( - integration.channel === ChannelTypeEnum.EMAIL)} - /> - ) : null; - })} + integration.channel === ChannelTypeEnum.EMAIL)} + /> + + ); } return ( <> - - {channel === StepTypeEnum.SMS && - steps.map((message, index) => { - return message.template.type === StepTypeEnum.SMS && message.uuid === stepUuid ? ( - integration.channel === ChannelTypeEnum.SMS)} - /> - ) : null; - })} - {channel === StepTypeEnum.PUSH && - steps.map((message, index) => { - return message.template.type === StepTypeEnum.PUSH && message.uuid === stepUuid ? ( - integration.channel === ChannelTypeEnum.PUSH) - } - /> - ) : null; - })} - {channel === StepTypeEnum.CHAT && - steps.map((message, index) => { - return message.template.type === StepTypeEnum.CHAT && message.uuid === stepUuid ? ( - integration.channel === ChannelTypeEnum.CHAT) - } - /> - ) : null; - })} - {channel === StepTypeEnum.DIGEST && - steps.map((message, index) => { - return message.template.type === StepTypeEnum.DIGEST && message.uuid === stepUuid ? ( - - ) : null; - })} - {channel === StepTypeEnum.DELAY && - steps.map((message, index) => { - return message.template.type === StepTypeEnum.DELAY && message.uuid === stepUuid ? ( - - ) : null; - })} - { - onDelete(stepUuid); - }} - disabled={readonly} - > - + {channel === StepTypeEnum.SMS && ( + integration.channel === ChannelTypeEnum.SMS)} + /> + )} + {channel === StepTypeEnum.PUSH && ( + integration.channel === ChannelTypeEnum.PUSH)} + /> + )} + {channel === StepTypeEnum.CHAT && ( + integration.channel === ChannelTypeEnum.CHAT)} /> - Delete {getChannel(channel?.toString() ?? '')?.type === NodeTypeEnum.CHANNEL ? 'Step' : 'Action'} - + )} + {channel === StepTypeEnum.DIGEST && } + {channel === StepTypeEnum.DELAY && } + ); diff --git a/apps/web/src/pages/templates/components/TemplateSettings.tsx b/apps/web/src/pages/templates/components/TemplateSettings.tsx index 72c97fe2a86..b872c728a28 100644 --- a/apps/web/src/pages/templates/components/TemplateSettings.tsx +++ b/apps/web/src/pages/templates/components/TemplateSettings.tsx @@ -13,6 +13,8 @@ import { deleteTemplateById } from '../../../api/notification-templates'; import { ROUTES } from '../../../constants/routes.enum'; import { SubPageWrapper } from './SubPageWrapper'; import { WorkflowSettingsTabs } from './WorkflowSettingsTabs'; +import { useFormContext } from 'react-hook-form'; +import { IForm } from './formTypes'; export const TemplateSettings = () => { const { templateId = '' } = useParams<{ templateId: string }>(); @@ -22,6 +24,9 @@ export const TemplateSettings = () => { const [isDeleting, setIsDeleting] = useState(false); const [isError, setIsError] = useState(undefined); const navigate = useNavigate(); + const { watch } = useFormContext(); + + const name = watch('name'); const confirmDelete = async () => { setIsDeleting(true); @@ -76,6 +81,17 @@ export const TemplateSettings = () => { isLoading={isDeleting} error={isError} /> + ); }; diff --git a/apps/web/src/pages/templates/components/in-app-editor/TemplateInAppEditor.tsx b/apps/web/src/pages/templates/components/in-app-editor/TemplateInAppEditor.tsx index 32ccd755028..d7fe47cf023 100644 --- a/apps/web/src/pages/templates/components/in-app-editor/TemplateInAppEditor.tsx +++ b/apps/web/src/pages/templates/components/in-app-editor/TemplateInAppEditor.tsx @@ -47,7 +47,6 @@ export function TemplateInAppEditor({ control, index }: { control: Control { const { readonly } = useEnvController(); @@ -30,19 +32,22 @@ export const DelayMetadata = ({ control, index }) => { name={`steps.${index}.metadata.type`} render={({ field }) => { return ( - - ); - }} - /> + @@ -120,9 +115,13 @@ export const DelayMetadata = ({ control, index }) => { {...field} value={field.value || ''} disabled={readonly} - label="Path for scheduled date" + label={ + + } placeholder="For example: sendAt" - description="The path in payload for the scheduled delay date" error={showErrors && fieldState.error?.message} type="text" data-test-id="batch-key" diff --git a/apps/web/src/pages/templates/workflow/DigestMetadata.tsx b/apps/web/src/pages/templates/workflow/DigestMetadata.tsx index dac233d7f2d..f9dffe2e1de 100644 --- a/apps/web/src/pages/templates/workflow/DigestMetadata.tsx +++ b/apps/web/src/pages/templates/workflow/DigestMetadata.tsx @@ -1,15 +1,15 @@ -import { Grid, Input as MantineInput } from '@mantine/core'; +import { Grid, Group, Input as MantineInput, Stack } from '@mantine/core'; import { Controller, useFormContext } from 'react-hook-form'; import styled from '@emotion/styled'; -import { DigestTypeEnum, DigestUnitEnum } from '@novu/shared'; +import { DigestTypeEnum } from '@novu/shared'; import { When } from '../../../components/utils/When'; -import { Input, Select, Switch, Button } from '../../../design-system'; +import { colors, Input, SegmentedControl, Switch, Tooltip } from '../../../design-system'; import { inputStyles } from '../../../design-system/config/inputs.styles'; import { useEnvController } from '../../../hooks'; -import { useTemplateEditorForm } from '../components/TemplateEditorFormProvider'; -import { useNavigate } from 'react-router-dom'; -import { useBasePath } from '../hooks/useBasePath'; +import { IntervalRadios } from './IntervalRadios'; +import { InfoCircle } from '../../../design-system/icons/general/InfoCircle'; +import { LabelWithTooltip } from './LabelWithTooltip'; const StyledSwitch = styled(Switch)` max-width: 100% !important; @@ -17,26 +17,58 @@ const StyledSwitch = styled(Switch)` `; export const DigestMetadata = ({ control, index }) => { - const { isCreating, isUpdating, isLoading } = useTemplateEditorForm(); const { readonly } = useEnvController(); const { formState: { errors, isSubmitted, isDirty }, watch, trigger, } = useFormContext(); - const isSubmitDisabled = readonly || isLoading || isCreating || !isDirty; - const navigate = useNavigate(); - const basePath = useBasePath(); const type = watch(`steps.${index}.metadata.type`); const showErrors = isSubmitted && errors?.steps; return ( <> +
+ { + return ( + { + field.onChange(segmentValue); + await trigger(`steps.${index}.metadata`); + }} + data-test-id="digest-type" + /> + ); + }} + /> +
+ } styles={inputStyles} > { data-test-id="time-amount" placeholder="0" disabled={readonly} + styles={(theme) => ({ + ...inputStyles(theme), + input: { + textAlign: 'center', + ...inputStyles(theme).input, + }, + })} /> ); }} /> - { - return ( - { - field.onChange(value); - await trigger(`steps.${index}.metadata`); - }} - data-test-id="digest-type" - /> - ); - }} - /> -
- + } styles={inputStyles} > { placeholder="0" required disabled={readonly} + styles={(theme) => ({ + ...inputStyles(theme), + input: { + textAlign: 'center', + ...inputStyles(theme).input, + }, + })} /> ); }} /> - { - return ( - + } placeholder="For example: post_id" - description="Used to group messages using this payload key, by default only subscriberId is used" error={fieldState.error?.message} type="text" data-test-id="batch-key" @@ -235,20 +216,6 @@ export const DigestMetadata = ({ control, index }) => { }} />
- ); }; diff --git a/apps/web/src/pages/templates/workflow/IntervalRadios.tsx b/apps/web/src/pages/templates/workflow/IntervalRadios.tsx new file mode 100644 index 00000000000..164e967111f --- /dev/null +++ b/apps/web/src/pages/templates/workflow/IntervalRadios.tsx @@ -0,0 +1,47 @@ +import { Radio } from '@mantine/core'; +import { DigestUnitEnum } from '@novu/shared'; +import { Controller } from 'react-hook-form'; +import { useEnvController } from '../../../hooks'; + +const options = [ + { value: DigestUnitEnum.SECONDS, label: 'Sec' }, + { value: DigestUnitEnum.MINUTES, label: 'Min' }, + { value: DigestUnitEnum.HOURS, label: 'Hours' }, + { value: DigestUnitEnum.DAYS, label: 'Days' }, +]; + +export const IntervalRadios = ({ control, name, showErrors }) => { + const { readonly } = useEnvController(); + + return ( + { + return ( + + {options.map((option) => ( + ({ + label: { + paddingLeft: 8, + }, + })} + disabled={readonly} + value={option.value} + label={option.label} + /> + ))} + + ); + }} + /> + ); +}; diff --git a/apps/web/src/pages/templates/workflow/LabelWithTooltip.tsx b/apps/web/src/pages/templates/workflow/LabelWithTooltip.tsx new file mode 100644 index 00000000000..4bc3af1634a --- /dev/null +++ b/apps/web/src/pages/templates/workflow/LabelWithTooltip.tsx @@ -0,0 +1,20 @@ +import { Group, Stack } from '@mantine/core'; +import { colors, Tooltip } from '../../../design-system'; +import { InfoCircle } from '../../../design-system/icons/general/InfoCircle'; + +export const LabelWithTooltip = ({ label, tooltip }) => { + return ( + + {label} + + + + + + + ); +}; diff --git a/apps/web/src/pages/templates/workflow/ShouldStopOnFailSwitch.tsx b/apps/web/src/pages/templates/workflow/ShouldStopOnFailSwitch.tsx index d96567fd33f..1ebea842f76 100644 --- a/apps/web/src/pages/templates/workflow/ShouldStopOnFailSwitch.tsx +++ b/apps/web/src/pages/templates/workflow/ShouldStopOnFailSwitch.tsx @@ -17,7 +17,7 @@ export const ShouldStopOnFailSwitch = ({ control, index }) => { {...field} disabled={readonly} checked={value} - label="Stop workflow if this step fails?" + label="Stop if step fails" data-test-id="step-should-stop-on-fail-switch" /> ); diff --git a/apps/web/src/pages/templates/workflow/SideBar/StepSettings.tsx b/apps/web/src/pages/templates/workflow/SideBar/StepSettings.tsx index 4c5d68f1332..c5e7c7d0871 100644 --- a/apps/web/src/pages/templates/workflow/SideBar/StepSettings.tsx +++ b/apps/web/src/pages/templates/workflow/SideBar/StepSettings.tsx @@ -28,8 +28,8 @@ export function StepSettings({ index }: { index: number }) { return ( <> - - + + diff --git a/apps/web/src/pages/templates/workflow/StepActiveSwitch.tsx b/apps/web/src/pages/templates/workflow/StepActiveSwitch.tsx index e0cf7b5e646..8af6ad34113 100644 --- a/apps/web/src/pages/templates/workflow/StepActiveSwitch.tsx +++ b/apps/web/src/pages/templates/workflow/StepActiveSwitch.tsx @@ -17,7 +17,7 @@ export const StepActiveSwitch = ({ control, index }) => { {...field} disabled={readonly} checked={value} - label={`Step is ${value ? 'active' : 'not active'}`} + label={value ? 'Active' : 'Inactive'} data-test-id="step-active-switch" /> ); diff --git a/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx b/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx index 34dba68e924..385923119f3 100644 --- a/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx +++ b/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx @@ -10,7 +10,7 @@ import type { IForm } from '../components/formTypes'; import { DeleteConfirmModal } from '../components/DeleteConfirmModal'; import { useTemplateEditorForm } from '../components/TemplateEditorFormProvider'; import { Outlet, useParams } from 'react-router-dom'; -import { Container, TextInput } from '@mantine/core'; +import { Container, TextInput, useMantineColorScheme } from '@mantine/core'; import { useEnvController } from '../../../hooks'; const WorkflowEditor = () => { @@ -31,6 +31,8 @@ const WorkflowEditor = () => { const [toDelete, setToDelete] = useState(''); + const { colorScheme } = useMantineColorScheme(); + const confirmDelete = () => { const index = steps.findIndex((item) => item.uuid === toDelete); deleteStep(index); @@ -42,9 +44,9 @@ const WorkflowEditor = () => { }; const onDelete = (uuid) => { - const currentStep = steps.find((step) => step.uuid === uuid); + const stepToDelete = steps.find((step) => step.uuid === uuid); - if (!currentStep) { + if (!stepToDelete) { setToDelete(uuid); return; @@ -55,7 +57,7 @@ const WorkflowEditor = () => { step.filters?.find( (filter) => filter.children?.find( - (item) => item.on === FilterPartTypeEnum.PREVIOUS_STEP && item.step === currentStep.uuid + (item) => item.on === FilterPartTypeEnum.PREVIOUS_STEP && item.step === stepToDelete.uuid ) !== undefined ) !== undefined ); @@ -117,21 +119,27 @@ const WorkflowEditor = () => { render={({ field, fieldState }) => { return ( ({ + styles={(theme) => ({ wrapper: { background: 'transparent', width: '100%', }, input: { background: 'transparent', - border: 'none', + borderStyle: 'solid', + borderColor: colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[5], + borderWidth: '1px', fontSize: '20px', fontWeight: 'bolder', - padding: 0, + padding: 9, lineHeight: '28px', minHeight: 'auto', height: 'auto', width: '100%', + '&:not(:placeholder-shown)': { + borderStyle: `none`, + padding: 10, + }, }, })} {...field} @@ -164,12 +172,7 @@ const WorkflowEditor = () => { />
- 0} - confirm={confirmDelete} - cancel={cancelDelete} - /> + 0} confirm={confirmDelete} cancel={cancelDelete} /> ); }; From b8c5b7fd1e533131f515110fbe9c1d84551a0336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Wed, 29 Mar 2023 14:35:36 +0200 Subject: [PATCH 006/152] fix: margins, padding and tour --- .../components/EditorPreviewSwitch.tsx | 2 + .../templates/components/TemplateEditor.tsx | 2 +- .../components/TemplatePushEditor.tsx | 2 + .../components/TemplateSMSEditor.tsx | 1 + .../templates/components/TemplateSettings.tsx | 5 +- .../templates/components/TestWorkflow.tsx | 29 ++--- .../components/WorkflowSettingsTabs.tsx | 1 - .../chat-editor/TemplateChatEditor.tsx | 1 + .../email-editor/EmailContentCard.tsx | 7 +- .../email-editor/EmailInboxContent.tsx | 8 +- .../email-editor/EmailMessagesCards.tsx | 18 +-- .../components/email-editor/TestSendEmail.tsx | 82 ++++++++------ .../in-app-editor/InAppContentCard.tsx | 107 +++++++++++------- .../in-app-editor/TemplateInAppEditor.tsx | 2 +- .../TemplatePreference.tsx | 51 +++++---- .../editor/DigestWorkflowTourTooltip.tsx | 6 +- .../editor/PreviewSegment/MobileIcon.tsx | 25 ++-- .../editor/PreviewSegment/WebIcon.tsx | 56 ++++++--- .../src/pages/templates/editor/PreviewWeb.tsx | 2 - .../editor/useDigestWorkflowTour.tsx | 25 ++-- .../templates/workflow/ReplyCallback.tsx | 9 +- .../workflow/SideBar/StepSettings.tsx | 12 +- 22 files changed, 260 insertions(+), 193 deletions(-) diff --git a/apps/web/src/pages/templates/components/EditorPreviewSwitch.tsx b/apps/web/src/pages/templates/components/EditorPreviewSwitch.tsx index f01c0fc5f3f..d595b159b1a 100644 --- a/apps/web/src/pages/templates/components/EditorPreviewSwitch.tsx +++ b/apps/web/src/pages/templates/components/EditorPreviewSwitch.tsx @@ -13,6 +13,8 @@ export const EditorPreviewSwitch = ({ view, setView }) => { background: 'transparent', border: `1px solid ${theme.colorScheme === 'dark' ? colors.B40 : colors.B70}`, borderRadius: '30px', + width: '100%', + maxWidth: '300px', }, label: { fontSize: '14px', diff --git a/apps/web/src/pages/templates/components/TemplateEditor.tsx b/apps/web/src/pages/templates/components/TemplateEditor.tsx index 6f780b2767f..54c4be576dd 100644 --- a/apps/web/src/pages/templates/components/TemplateEditor.tsx +++ b/apps/web/src/pages/templates/components/TemplateEditor.tsx @@ -20,7 +20,7 @@ import { When } from '../../../components/utils/When'; import { useEffect, useMemo } from 'react'; import { useBasePath } from '../hooks/useBasePath'; -const getPageTitle = (channel: StepTypeEnum) => { +export const getPageTitle = (channel: StepTypeEnum) => { if (channel === StepTypeEnum.EMAIL) { return ( diff --git a/apps/web/src/pages/templates/components/TemplatePushEditor.tsx b/apps/web/src/pages/templates/components/TemplatePushEditor.tsx index 2de91ceda70..1e3a2af7618 100644 --- a/apps/web/src/pages/templates/components/TemplatePushEditor.tsx +++ b/apps/web/src/pages/templates/components/TemplatePushEditor.tsx @@ -36,6 +36,8 @@ export function TemplatePushEditor({ control={control} render={({ field }) => (