diff --git a/apps/api/src/app/workflows-v2/shared/schemas/in-app-control.schema.ts b/apps/api/src/app/workflows-v2/shared/schemas/in-app-control.schema.ts index bf2650f7c05..587e9c41e86 100644 --- a/apps/api/src/app/workflows-v2/shared/schemas/in-app-control.schema.ts +++ b/apps/api/src/app/workflows-v2/shared/schemas/in-app-control.schema.ts @@ -1,5 +1,5 @@ import { JSONSchema } from 'json-schema-to-ts'; -import { UiComponentEnum, UiSchema, UiSchemaGroupEnum } from '@novu/shared'; +import { UiComponentEnum, UiSchema, UiSchemaGroupEnum, UiSchemaProperty } from '@novu/shared'; const ABSOLUTE_AND_RELATIVE_URL_REGEX = '^(?!mailto:)(?:(https?):\\/\\/[^\\s/$.?#].[^\\s]*)|^(\\/[^\\s]*)$'; @@ -44,26 +44,42 @@ export const inAppControlSchema = { required: ['body'], additionalProperties: false, } as const satisfies JSONSchema; + +const redirectPlaceholder = { + url: { + placeholder: '', + }, + target: { + placeholder: '_self', + }, +}; + export const InAppUiSchema: UiSchema = { group: UiSchemaGroupEnum.IN_APP, properties: { body: { component: UiComponentEnum.IN_APP_BODY, + placeholder: '', }, avatar: { component: UiComponentEnum.IN_APP_AVATAR, + placeholder: '', }, subject: { component: UiComponentEnum.IN_APP_SUBJECT, + placeholder: '', }, primaryAction: { component: UiComponentEnum.IN_APP_BUTTON_DROPDOWN, + placeholder: null, }, secondaryAction: { component: UiComponentEnum.IN_APP_BUTTON_DROPDOWN, + placeholder: null, }, redirect: { component: UiComponentEnum.URL_TEXT_BOX, + placeholder: redirectPlaceholder, }, }, }; diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 3b52879aae4..61f03fc29fb 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -76,6 +76,7 @@ "devDependencies": { "@clerk/types": "^4.6.1", "@eslint/js": "^9.9.0", + "@hookform/devtools": "^4.3.0", "@playwright/test": "^1.44.0", "@sentry/vite-plugin": "^2.22.6", "@types/lodash.debounce": "^4.0.9", diff --git a/apps/dashboard/src/api/steps.ts b/apps/dashboard/src/api/steps.ts new file mode 100644 index 00000000000..10af3c76415 --- /dev/null +++ b/apps/dashboard/src/api/steps.ts @@ -0,0 +1,14 @@ +import { getV2 } from './api.client'; +import type { StepDataDto } from '@novu/shared'; + +export const fetchStep = async ({ + workflowSlug, + stepSlug, +}: { + workflowSlug: string; + stepSlug: string; +}): Promise => { + const { data } = await getV2<{ data: StepDataDto }>(`/workflows/${workflowSlug}/steps/${stepSlug}`); + + return data; +}; diff --git a/apps/dashboard/src/components/primitives/form/avatar-picker.tsx b/apps/dashboard/src/components/primitives/form/avatar-picker.tsx index f5c36d87e5e..b89c4a3bd64 100644 --- a/apps/dashboard/src/components/primitives/form/avatar-picker.tsx +++ b/apps/dashboard/src/components/primitives/form/avatar-picker.tsx @@ -1,15 +1,14 @@ -'use client'; - import { Avatar, AvatarImage } from '@/components/primitives/avatar'; import { Button } from '@/components/primitives/button'; -import { FormControl, FormMessage } from '@/components/primitives/form/form'; +import { FormMessage } from '@/components/primitives/form/form'; import { Input, InputField } from '@/components/primitives/input'; import { Label } from '@/components/primitives/label'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/primitives/popover'; import { Separator } from '@/components/primitives/separator'; import TextSeparator from '@/components/primitives/text-separator'; import { useState, forwardRef } from 'react'; -import { RiEdit2Line, RiImageEditFill } from 'react-icons/ri'; +import { RiEdit2Line, RiErrorWarningFill, RiImageEditFill } from 'react-icons/ri'; +import { useFormField } from './form-context'; const predefinedAvatars = [ `${window.location.origin}/images/avatar.svg`, @@ -30,6 +29,7 @@ type AvatarPickerProps = React.InputHTMLAttributes; export const AvatarPicker = forwardRef(({ id, ...props }, ref) => { const [isOpen, setIsOpen] = useState(false); + const { error } = useFormField(); const handlePredefinedAvatarClick = (url: string) => { props.onChange?.({ target: { value: url } } as React.ChangeEvent); @@ -40,14 +40,17 @@ export const AvatarPicker = forwardRef(({ i
- @@ -59,11 +62,9 @@ export const AvatarPicker = forwardRef(({ i
- - - - - + + +
@@ -80,7 +81,6 @@ export const AvatarPicker = forwardRef(({ i - ); }); diff --git a/apps/dashboard/src/components/primitives/form/form.tsx b/apps/dashboard/src/components/primitives/form/form.tsx index a54d3250235..9da0b29f8a7 100644 --- a/apps/dashboard/src/components/primitives/form/form.tsx +++ b/apps/dashboard/src/components/primitives/form/form.tsx @@ -101,28 +101,35 @@ const formMessageVariants = cva('flex items-center gap-1', { }, }); -const FormMessage = React.forwardRef>( - ({ className, children, ...props }, ref) => { - const { error, formMessageId } = useFormField(); - const body = error ? String(error?.message) : children; +const FormMessagePure = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes & { error?: string } +>(({ className, children, error, id, ...props }, ref) => { + const body = error ? error : children; + + if (!body) { + return null; + } - if (!body) { - return null; - } + return ( +

+ {error ? : } + {body} +

+ ); +}); +FormMessagePure.displayName = 'FormMessagePure'; - return ( -

- {error ? : } - {body} -

- ); - } -); +const FormMessage = React.forwardRef>((props, ref) => { + const { error, formMessageId } = useFormField(); + + return ; +}); FormMessage.displayName = 'FormMessage'; -export { Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField }; +export { Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormMessagePure, FormField }; diff --git a/apps/dashboard/src/components/primitives/sonner.tsx b/apps/dashboard/src/components/primitives/sonner.tsx index cb5601952b5..1e122464fbf 100644 --- a/apps/dashboard/src/components/primitives/sonner.tsx +++ b/apps/dashboard/src/components/primitives/sonner.tsx @@ -91,7 +91,7 @@ const Toaster = ({ ...props }: ToasterProps) => { toastOptions={{ classNames: { toast: - 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg text-foreground-950', + 'group toast group-[.toaster]:bg-transparent group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg text-foreground-950', description: 'group-[.toast]:text-foreground-600', actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground', cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground', diff --git a/apps/dashboard/src/components/primitives/url-input.tsx b/apps/dashboard/src/components/primitives/url-input.tsx deleted file mode 100644 index 688a8d86aed..00000000000 --- a/apps/dashboard/src/components/primitives/url-input.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { forwardRef } from 'react'; -import { liquid } from '@codemirror/lang-liquid'; -import { EditorView } from '@uiw/react-codemirror'; -import { RedirectTargetEnum } from '@novu/shared'; -import { Input, InputField, InputFieldProps, InputProps } from '@/components/primitives/input'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/primitives/select'; -import { Editor } from './editor'; - -type URLValue = { - type: string; - url: string; -}; - -type URLInputProps = Omit & { - options: string[]; - value: URLValue; - onChange: (value: URLValue) => void; - asEditor?: boolean; -} & Pick; - -export const URLInput = forwardRef((props, ref) => { - const { options, value, onChange, size = 'default', asEditor = false, placeholder, ...rest } = props; - - return ( -
-
- - {asEditor ? ( - onChange({ ...value, url: val })} - height={size === 'md' ? '38px' : '30px'} - extensions={[ - liquid({ - variables: [{ type: 'variable', label: 'asdf' }], - }), - EditorView.lineWrapping, - ]} - /> - ) : ( - onChange({ ...value, url: e.target.value })} - {...rest} - /> - )} - - -
-
- ); -}); diff --git a/apps/dashboard/src/components/workflow-editor/action-picker.tsx b/apps/dashboard/src/components/workflow-editor/action-picker.tsx deleted file mode 100644 index 6a41c7c397c..00000000000 --- a/apps/dashboard/src/components/workflow-editor/action-picker.tsx +++ /dev/null @@ -1,247 +0,0 @@ -import { ComponentProps } from 'react'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; -import { RiEdit2Line, RiExpandUpDownLine, RiForbid2Line } from 'react-icons/ri'; -import { z } from 'zod'; -import { liquid } from '@codemirror/lang-liquid'; -import { EditorView } from '@uiw/react-codemirror'; -import { RedirectTargetEnum } from '@novu/shared'; -import { Button, buttonVariants } from '@/components/primitives/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/primitives/dropdown-menu'; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/primitives/form/form'; -import { InputField } from '@/components/primitives/input'; -import { Popover, PopoverContent, PopoverTrigger } from '@/components/primitives/popover'; -import { Separator } from '@/components/primitives/separator'; -import { URLInput } from '@/components/primitives/url-input'; -import { cn } from '@/utils/ui'; -import { urlTargetTypes } from '@/utils/url'; -import { Editor } from '../primitives/editor'; - -type Action = { - label: string; - redirect: { - url: string; - type: string; - }; -}; - -type Actions = { - primaryAction?: Action; - secondaryAction?: Action; -}; - -type ActionPickerProps = { - className?: string; - value: Actions | undefined; - onChange: (value: Actions) => void; -}; - -export const ActionPicker = (props: ActionPickerProps) => { - const { className, value, onChange } = props; - const primaryAction = value?.primaryAction; - const secondaryAction = value?.secondaryAction; - - return ( -
-
- {!primaryAction && !secondaryAction && ( -
- - No action -
- )} - {primaryAction && ( - { - onChange({ primaryAction, secondaryAction }); - }} - > - - - )} - {secondaryAction && ( - { - onChange({ primaryAction, secondaryAction }); - }} - > - - - )} -
- - - - - - { - onChange({}); - }} - > -
- - No action -
-
- { - onChange({ - primaryAction: value?.primaryAction || { - label: 'Primary action', - redirect: { type: '_self', url: '' }, - }, - secondaryAction: undefined, - }); - }} - > -
- Primary action -
-
- { - onChange({ - primaryAction: value?.primaryAction || { - label: 'Primary action', - redirect: { type: '_self', url: '' }, - }, - secondaryAction: value?.secondaryAction || { - label: 'Secondary action', - redirect: { type: '_self', url: '' }, - }, - }); - }} - > -
- Primary action -
-
- Secondary action -
-
-
-
-
- ); -}; - -const formSchema = z.object({ - label: z.string(), - redirect: z.object({ - url: z.string(), - type: z.union([ - z.literal(RedirectTargetEnum.BLANK), - z.literal(RedirectTargetEnum.PARENT), - z.literal(RedirectTargetEnum.SELF), - z.literal(RedirectTargetEnum.TOP), - z.literal(RedirectTargetEnum.UNFENCED_TOP), - ]), - }), -}); - -const ConfigureActionPopover = ( - props: ComponentProps & { action: Action; setAction: (action: Action) => void } -) => { - const { setAction, action, ...rest } = props; - - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - label: action.label, - redirect: action.redirect, - }, - }); - - return ( - { - if (!open) { - form.handleSubmit((values) => { - setAction(values); - })(); - } - }} - > - - -
- -
-
- Customize button -
- - ( - -
- Button text -
- - - - - - -
- )} - /> - - ( - -
- Redirect URL -
- - field.onChange(val)} - asEditor - /> - - -
- )} - /> -
-
- -
-
- ); -}; diff --git a/apps/dashboard/src/components/workflow-editor/steps/component-utils.tsx b/apps/dashboard/src/components/workflow-editor/steps/component-utils.tsx new file mode 100644 index 00000000000..338c335a381 --- /dev/null +++ b/apps/dashboard/src/components/workflow-editor/steps/component-utils.tsx @@ -0,0 +1,30 @@ +import { UiComponentEnum } from '@novu/shared'; + +import { InAppAction } from '@/components/workflow-editor/steps/in-app/in-app-action'; +import { InAppSubject } from '@/components/workflow-editor/steps/in-app/in-app-subject'; +import { InAppBody } from '@/components/workflow-editor/steps/in-app/in-app-body'; +import { InAppAvatar } from '@/components/workflow-editor/steps/in-app/in-app-avatar'; +import { InAppRedirect } from '@/components/workflow-editor/steps/in-app/in-app-redirect'; + +export const getComponentByType = ({ component }: { component?: UiComponentEnum }) => { + switch (component) { + case UiComponentEnum.IN_APP_AVATAR: { + return ; + } + case UiComponentEnum.IN_APP_SUBJECT: { + return ; + } + case UiComponentEnum.IN_APP_BODY: { + return ; + } + case UiComponentEnum.IN_APP_BUTTON_DROPDOWN: { + return ; + } + case UiComponentEnum.URL_TEXT_BOX: { + return ; + } + default: { + return null; + } + } +}; diff --git a/apps/dashboard/src/components/workflow-editor/steps/configure-in-app-template/configure-in-app-step-template-tabs.tsx b/apps/dashboard/src/components/workflow-editor/steps/configure-in-app-template/configure-in-app-step-template-tabs.tsx deleted file mode 100644 index 545e123ac45..00000000000 --- a/apps/dashboard/src/components/workflow-editor/steps/configure-in-app-template/configure-in-app-step-template-tabs.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { useState } from 'react'; -import { RiEdit2Line, RiInformationFill, RiPencilRuler2Line } from 'react-icons/ri'; -import { Cross2Icon } from '@radix-ui/react-icons'; -import { useNavigate } from 'react-router-dom'; -import { useFormContext } from 'react-hook-form'; -import * as z from 'zod'; -import { liquid } from '@codemirror/lang-liquid'; -import { EditorView } from '@uiw/react-codemirror'; -import { RedirectTargetEnum } from '@novu/shared'; - -import { Notification5Fill } from '@/components/icons'; -import { Button } from '@/components/primitives/button'; -import { Editor } from '@/components/primitives/editor'; -import { AvatarPicker } from '@/components/primitives/form/avatar-picker'; -import { InputField } from '@/components/primitives/input'; -import { Separator } from '@/components/primitives/separator'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/primitives/tabs'; -import { URLInput } from '@/components/primitives/url-input'; -import { urlTargetTypes } from '@/utils/url'; -import { ActionPicker } from '../../action-picker'; -import { workflowSchema } from '../../schema'; -import { ConfigureInAppStepTemplatePreview } from '@/components/workflow-editor/steps/configure-in-app-template/configure-in-app-step-template-preview'; - -const tabsContentClassName = 'h-full w-full px-3 py-3.5'; - -export const ConfigureInAppStepTemplateTabs = () => { - const navigate = useNavigate(); - const { formState } = useFormContext>(); - - const [subject, setSubject] = useState(''); - const [body, setBody] = useState(''); - - return ( - -
-
- - Configure Template -
- - - - Editor - - - - Preview - - - - -
- - -
-
- - In-app Template -
-
-
- - - - -
- - - -
- - - {'This supports markdown and variables, type { for more.'} - -
- {}} - className="mt-3" - /> -
-
- - console.log(val)} - placeholder="Redirect URL" - size="md" - asEditor - /> -
-
-
- - - - -
- -
-
- ); -}; diff --git a/apps/dashboard/src/components/workflow-editor/steps/configure-step.tsx b/apps/dashboard/src/components/workflow-editor/steps/configure-step.tsx index 9fecf97aa0c..64393aae649 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/configure-step.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/configure-step.tsx @@ -8,7 +8,7 @@ import { useEnvironment } from '@/context/environment/hooks'; import { StepTypeEnum } from '@/utils/enums'; import { buildRoute, ROUTES } from '@/utils/routes'; import { motion } from 'framer-motion'; -import { InApp } from './in-app/in-app'; +import { ConfigureInApp } from './in-app/configure-in-app'; import { useStep } from './use-step'; import Chat from './chat'; import { useState } from 'react'; @@ -102,7 +102,7 @@ export function ConfigureStep() { const Step = ({ stepType }: { stepType?: StepTypeEnum }) => { switch (stepType) { case StepTypeEnum.IN_APP: - return ; + return ; /** * TODO: Add other step types here diff --git a/apps/dashboard/src/components/workflow-editor/steps/edit-step-sidebar.tsx b/apps/dashboard/src/components/workflow-editor/steps/edit-step-sidebar.tsx index 603d25fa187..1c7bc926a5d 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/edit-step-sidebar.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/edit-step-sidebar.tsx @@ -1,59 +1,23 @@ -import { Form } from '@/components/primitives/form/form'; -import { Sheet, SheetOverlay, SheetPortal } from '@/components/primitives/sheet'; -import { useFetchWorkflow, useUpdateWorkflow } from '@/hooks'; -import { handleValidationIssues } from '@/utils/handleValidationIssues'; -import { zodResolver } from '@hookform/resolvers/zod'; +import { useMemo } from 'react'; import { motion } from 'framer-motion'; -import { useLayoutEffect, useMemo } from 'react'; -import { useForm } from 'react-hook-form'; -import { useNavigate, useParams } from 'react-router-dom'; -import * as z from 'zod'; -import { workflowSchema } from '../schema'; -import { StepEditor } from './step-editor'; +import { useParams } from 'react-router-dom'; + +import { Sheet, SheetOverlay, SheetPortal } from '@/components/primitives/sheet'; +import { useFetchWorkflow } from '@/hooks/use-fetch-workflow'; +import { StepEditor } from '@/components/workflow-editor/steps/step-editor'; +import { useFetchStep } from '@/hooks/use-fetch-step'; const transitionSetting = { ease: [0.29, 0.83, 0.57, 0.99], duration: 0.4 }; export const EditStepSidebar = () => { const { workflowSlug = '', stepSlug = '' } = useParams<{ workflowSlug: string; stepSlug: string }>(); - const navigate = useNavigate(); - const form = useForm>({ mode: 'onSubmit', resolver: zodResolver(workflowSchema) }); - const { reset, setError } = form; - const { workflow, error } = useFetchWorkflow({ + const { workflow } = useFetchWorkflow({ workflowSlug, }); - const step = useMemo(() => workflow?.steps.find((el) => el.slug === stepSlug), [stepSlug, workflow]); - - useLayoutEffect(() => { - if (!workflow) { - return; - } - - reset({ ...workflow, steps: workflow.steps.map((step) => ({ ...step })) }); - }, [workflow, error, navigate, reset]); - - const { updateWorkflow } = useUpdateWorkflow({ - onSuccess: (data) => { - reset({ ...data, steps: data.steps.map((step) => ({ ...step })) }); - - if (data.issues) { - // TODO: remove the as any cast when BE issues are typed - handleValidationIssues({ fields: form.getValues(), issues: data.issues as any, setError }); - } - - // TODO: show the toast - navigate(`../`, { relative: 'path' }); - }, - }); - - const onSubmit = (data: z.infer) => { - if (!workflow) { - return; - } - - updateWorkflow({ id: workflow._id, workflow: { ...workflow, ...data } as any }); - }; + const { step } = useFetchStep({ workflowSlug, stepSlug }); + const stepType = useMemo(() => workflow?.steps.find((el) => el.slug === stepSlug)?.type, [stepSlug, workflow]); return ( @@ -87,18 +51,8 @@ export const EditStepSidebar = () => { 'bg-background fixed inset-y-0 right-0 z-50 flex h-full w-3/4 flex-col border-l shadow-lg sm:max-w-[600px]' } > -
- { - event.preventDefault(); - event.stopPropagation(); - form.handleSubmit(onSubmit)(event); - }} - > - {step && } - - + {/* TODO: show loading indicator */} + {workflow && step && stepType && }
diff --git a/apps/dashboard/src/components/workflow-editor/steps/in-app/edit-step-in-app-preview.tsx b/apps/dashboard/src/components/workflow-editor/steps/in-app/configure-in-app-preview.tsx similarity index 93% rename from apps/dashboard/src/components/workflow-editor/steps/in-app/edit-step-in-app-preview.tsx rename to apps/dashboard/src/components/workflow-editor/steps/in-app/configure-in-app-preview.tsx index a27e2479e87..bac9a5eedab 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/in-app/edit-step-in-app-preview.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/in-app/configure-in-app-preview.tsx @@ -3,7 +3,7 @@ import { useParams } from 'react-router-dom'; import { usePreviewStep } from '@/hooks'; import { InAppPreview } from '@/components/workflow-editor/in-app-preview'; -export function EditStepInAppPreview() { +export function ConfigureInAppPreview() { const { previewStep, data } = usePreviewStep(); const { workflowSlug, stepSlug } = useParams<{ workflowSlug: string; diff --git a/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app.tsx b/apps/dashboard/src/components/workflow-editor/steps/in-app/configure-in-app.tsx similarity index 84% rename from apps/dashboard/src/components/workflow-editor/steps/in-app/in-app.tsx rename to apps/dashboard/src/components/workflow-editor/steps/in-app/configure-in-app.tsx index 51f5ef007ea..31b099f7e3e 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/in-app/configure-in-app.tsx @@ -4,9 +4,9 @@ import { Button } from '../../../primitives/button'; import { Separator } from '../../../primitives/separator'; import { CommonFields } from '../common-fields'; import { SidebarContent } from '@/components/side-navigation/Sidebar'; -import { EditStepInAppPreview } from '@/components/workflow-editor/steps/in-app/edit-step-in-app-preview'; +import { ConfigureInAppPreview } from './configure-in-app-preview'; -export function InApp() { +export function ConfigureInApp() { return ( <> @@ -21,7 +21,7 @@ export function InApp() { - + ); diff --git a/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-action.tsx b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-action.tsx new file mode 100644 index 00000000000..a962661513f --- /dev/null +++ b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-action.tsx @@ -0,0 +1,203 @@ +import { ComponentProps } from 'react'; +import { useFormContext, useWatch } from 'react-hook-form'; +import { RiEdit2Line, RiExpandUpDownLine, RiForbid2Line } from 'react-icons/ri'; +import { liquid } from '@codemirror/lang-liquid'; +import { EditorView } from '@uiw/react-codemirror'; + +import { Button, buttonVariants } from '@/components/primitives/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/primitives/dropdown-menu'; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + FormMessagePure, +} from '@/components/primitives/form/form'; +import { InputField } from '@/components/primitives/input'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/primitives/popover'; +import { Separator } from '@/components/primitives/separator'; +import { URLInput } from '@/components/workflow-editor/url-input'; +import { cn } from '@/utils/ui'; +import { urlTargetTypes } from '@/utils/url'; +import { Editor } from '@/components/primitives/editor'; + +const primaryActionKey = 'primaryAction'; +const secondaryActionKey = 'secondaryAction'; + +export const InAppAction = () => { + const { control, setValue, getFieldState } = useFormContext(); + const primaryAction = useWatch({ control, name: primaryActionKey }); + const secondaryAction = useWatch({ control, name: secondaryActionKey }); + const primaryActionLabel = getFieldState(`${primaryActionKey}.label`); + const primaryActionRedirectUrl = getFieldState(`${primaryActionKey}.redirect.url`); + const secondaryActionLabel = getFieldState(`${secondaryActionKey}.label`); + const secondaryActionRedirectUrl = getFieldState(`${secondaryActionKey}.redirect.url`); + const error = + primaryActionLabel.error || + primaryActionRedirectUrl.error || + secondaryActionLabel.error || + secondaryActionRedirectUrl.error; + + return ( + <> +
+
+ {!primaryAction && !secondaryAction && ( +
+ + No action +
+ )} + {primaryAction && ( + + + + )} + {secondaryAction && ( + + + + )} +
+ + + + + + { + setValue(primaryActionKey, undefined, { shouldDirty: true, shouldValidate: false }); + setValue(secondaryActionKey, undefined, { shouldDirty: true, shouldValidate: false }); + }} + > +
+ + No action +
+
+ { + setValue( + primaryActionKey, + { + label: 'Primary action', + redirect: { target: '_self', url: '' }, + }, + { shouldDirty: true, shouldValidate: false } + ); + setValue(secondaryActionKey, undefined, { shouldDirty: true, shouldValidate: false }); + }} + > +
+ Primary action +
+
+ { + setValue( + primaryActionKey, + { + label: 'Primary action', + redirect: { target: '_self', url: '' }, + }, + { shouldDirty: true, shouldValidate: false } + ); + setValue( + secondaryActionKey, + { + label: 'Secondary action', + redirect: { target: '_self', url: '' }, + }, + { shouldDirty: true, shouldValidate: false } + ); + }} + > +
+ Primary action +
+
+ Secondary action +
+
+
+
+
+ + + ); +}; + +const ConfigureActionPopover = (props: ComponentProps & { fields: { actionKey: string } }) => { + const { + fields: { actionKey }, + ...rest + } = props; + const { control } = useFormContext(); + + return ( + + + +
+
+ Customize button +
+ + ( + +
+ Button text +
+ + + + + + +
+ )} + /> +
+ Redirect URL + +
+
+
+
+ ); +}; diff --git a/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-avatar.tsx b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-avatar.tsx new file mode 100644 index 00000000000..9ac39abaf41 --- /dev/null +++ b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-avatar.tsx @@ -0,0 +1,24 @@ +import { useFormContext } from 'react-hook-form'; + +import { FormControl, FormField, FormItem } from '@/components/primitives/form/form'; +import { AvatarPicker } from '@/components/primitives/form/avatar-picker'; + +const avatarKey = 'avatar'; + +export const InAppAvatar = () => { + const { control } = useFormContext(); + + return ( + ( + + + + + + )} + /> + ); +}; diff --git a/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-body.tsx b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-body.tsx new file mode 100644 index 00000000000..b43e4e801da --- /dev/null +++ b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-body.tsx @@ -0,0 +1,46 @@ +import { useFormContext } from 'react-hook-form'; +import { liquid } from '@codemirror/lang-liquid'; +import { EditorView } from '@uiw/react-codemirror'; + +import { FormControl, FormField, FormItem, FormMessage } from '@/components/primitives/form/form'; +import { InputField } from '@/components/primitives/input'; +import { Editor } from '@/components/primitives/editor'; +import { capitalize } from '@/utils/string'; + +const bodyKey = 'body'; + +export const InAppBody = () => { + const { + control, + formState: { errors }, + } = useFormContext(); + + return ( + ( + + + + field.onChange(val)} + /> + + + + + )} + /> + ); +}; diff --git a/apps/dashboard/src/components/workflow-editor/steps/configure-in-app-template/configure-in-app-step-template-preview.tsx b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-editor-preview.tsx similarity index 98% rename from apps/dashboard/src/components/workflow-editor/steps/configure-in-app-template/configure-in-app-step-template-preview.tsx rename to apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-editor-preview.tsx index 4ab3669af05..4fff3697a7f 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/configure-in-app-template/configure-in-app-step-template-preview.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-editor-preview.tsx @@ -11,7 +11,7 @@ import { useParams } from 'react-router-dom'; import { InAppPreview } from '@/components/workflow-editor/in-app-preview'; import { loadLanguage } from '@uiw/codemirror-extensions-langs'; -export const ConfigureInAppStepTemplatePreview = () => { +export const InAppEditorPreview = () => { const [editorValue, setEditorValue] = useState('{}'); const [isEditorOpen, setIsEditorOpen] = useState(true); const { previewStep, data } = usePreviewStep(); diff --git a/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-editor.tsx b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-editor.tsx new file mode 100644 index 00000000000..dbe546dc7b2 --- /dev/null +++ b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-editor.tsx @@ -0,0 +1,52 @@ +import { RiPencilRuler2Line } from 'react-icons/ri'; +import { UiSchemaGroupEnum, type UiSchema } from '@novu/shared'; + +import { getComponentByType } from '@/components/workflow-editor/steps/component-utils'; + +const avatarKey = 'avatar'; +const subjectKey = 'subject'; +const bodyKey = 'body'; +const redirectKey = 'redirect'; +const primaryActionKey = 'primaryAction'; +const secondaryActionKey = 'secondaryAction'; + +export const InAppEditor = ({ uiSchema }: { uiSchema?: UiSchema }) => { + if (!uiSchema || uiSchema?.group !== UiSchemaGroupEnum.IN_APP) { + return null; + } + + const { + [avatarKey]: avatar, + [subjectKey]: subject, + [bodyKey]: body, + [redirectKey]: redirect, + [primaryActionKey]: primaryAction, + [secondaryActionKey]: secondaryAction, + } = uiSchema.properties ?? {}; + + return ( +
+
+ + In-app Template +
+
+ {(avatar || subject) && ( +
+ {avatar && getComponentByType({ component: avatar.component })} + {subject && getComponentByType({ component: subject.component })} +
+ )} + {body && getComponentByType({ component: body.component })} + {(primaryAction || secondaryAction) && + getComponentByType({ + component: primaryAction.component || secondaryAction.component, + })} +
+ {redirect && + getComponentByType({ + component: redirect.component, + })} +
+ ); +}; diff --git a/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-redirect.tsx b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-redirect.tsx new file mode 100644 index 00000000000..1dddbd7f4fc --- /dev/null +++ b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-redirect.tsx @@ -0,0 +1,21 @@ +import { FormLabel } from '@/components/primitives/form/form'; +import { URLInput } from '../../url-input'; +import { urlTargetTypes } from '@/utils/url'; + +export const InAppRedirect = () => { + return ( +
+ Redirect URL + +
+ ); +}; diff --git a/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-subject.tsx b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-subject.tsx new file mode 100644 index 00000000000..0daf67acd71 --- /dev/null +++ b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-subject.tsx @@ -0,0 +1,46 @@ +import { useFormContext } from 'react-hook-form'; +import { liquid } from '@codemirror/lang-liquid'; +import { EditorView } from '@uiw/react-codemirror'; + +import { FormControl, FormField, FormItem, FormMessage } from '@/components/primitives/form/form'; +import { InputField } from '@/components/primitives/input'; +import { Editor } from '@/components/primitives/editor'; +import { capitalize } from '@/utils/string'; + +const subjectKey = 'subject'; + +export const InAppSubject = () => { + const { + control, + formState: { errors }, + } = useFormContext(); + + return ( + ( + + + + field.onChange(val)} + /> + + + + + )} + /> + ); +}; diff --git a/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-tabs.tsx b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-tabs.tsx new file mode 100644 index 00000000000..d257a1acacf --- /dev/null +++ b/apps/dashboard/src/components/workflow-editor/steps/in-app/in-app-tabs.tsx @@ -0,0 +1,143 @@ +import { RiEdit2Line, RiPencilRuler2Line } from 'react-icons/ri'; +import { Cross2Icon } from '@radix-ui/react-icons'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { type WorkflowResponseDto, type StepDataDto, type StepUpdateDto } from '@novu/shared'; + +import { Form } from '@/components/primitives/form/form'; +import { Notification5Fill } from '@/components/icons'; +import { Button } from '@/components/primitives/button'; +import { Separator } from '@/components/primitives/separator'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/primitives/tabs'; +import { InAppEditorPreview } from '@/components/workflow-editor/steps/in-app/in-app-editor-preview'; +import { useUpdateWorkflow } from '@/hooks/use-update-workflow'; +import { buildDynamicZodSchema, buildDefaultValues } from '@/utils/schema'; +import { InAppEditor } from '@/components/workflow-editor/steps/in-app/in-app-editor'; +import { showToast } from '@/components/primitives/sonner-helpers'; +import { ToastIcon } from '@/components/primitives/sonner'; + +const tabsContentClassName = 'h-full w-full px-3 py-3.5'; + +export const InAppTabs = ({ workflow, step }: { workflow: WorkflowResponseDto; step: StepDataDto }) => { + const { stepSlug = '' } = useParams<{ stepSlug: string }>(); + const { dataSchema, uiSchema } = step.controls; + const navigate = useNavigate(); + const schema = buildDynamicZodSchema(dataSchema ?? {}); + const form = useForm({ + mode: 'onSubmit', + resolver: zodResolver(schema), + resetOptions: { keepDirtyValues: true }, + defaultValues: buildDefaultValues(uiSchema ?? {}), + values: step.controls.values, + }); + const { reset, formState } = form; + + const { updateWorkflow } = useUpdateWorkflow({ + onSuccess: () => { + showToast({ + children: () => ( + <> + + Saved + + ), + options: { + position: 'bottom-right', + classNames: { + toast: 'ml-10 mb-4', + }, + }, + }); + }, + onError: () => { + showToast({ + children: () => ( + <> + + Failed to save + + ), + options: { + position: 'bottom-right', + classNames: { + toast: 'ml-10 mb-4', + }, + }, + }); + }, + }); + + const onSubmit = async (data: any) => { + await updateWorkflow({ + id: workflow._id, + workflow: { + ...workflow, + steps: workflow.steps.map((step) => + step.slug === stepSlug ? ({ ...step, controlValues: { ...data } } as StepUpdateDto) : step + ), + }, + }); + reset({ ...data }); + }; + + return ( +
+ { + event.preventDefault(); + event.stopPropagation(); + form.handleSubmit(onSubmit)(event); + }} + > + +
+
+ + Configure Template +
+ + + + Editor + + + + Preview + + + + +
+ + + + + + + + +
+ +
+
+
+ + ); +}; diff --git a/apps/dashboard/src/components/workflow-editor/steps/step-editor.tsx b/apps/dashboard/src/components/workflow-editor/steps/step-editor.tsx index 1394068af6d..b6dda55c497 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/step-editor.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/step-editor.tsx @@ -1,10 +1,13 @@ -import { ConfigureInAppStepTemplateTabs } from '@/components/workflow-editor/steps/configure-in-app-template/configure-in-app-step-template-tabs'; -import { StepTypeEnum } from '@novu/shared'; +import { type StepDataDto, StepTypeEnum, type WorkflowResponseDto } from '@novu/shared'; +import { InAppTabs } from '@/components/workflow-editor/steps/in-app/in-app-tabs'; -const STEP_TYPE_TO_EDITOR: Record React.JSX.Element> = { +const STEP_TYPE_TO_EDITOR: Record< + StepTypeEnum, + (args: { workflow: WorkflowResponseDto; step: StepDataDto }) => React.JSX.Element +> = { [StepTypeEnum.EMAIL]: () =>
EMAIL Editor
, [StepTypeEnum.CHAT]: () =>
CHAT Editor
, - [StepTypeEnum.IN_APP]: ConfigureInAppStepTemplateTabs, + [StepTypeEnum.IN_APP]: InAppTabs, [StepTypeEnum.SMS]: () =>
SMS Editor
, [StepTypeEnum.PUSH]: () =>
PUSH Editor
, [StepTypeEnum.DIGEST]: () =>
DIGEST Editor
, @@ -13,7 +16,15 @@ const STEP_TYPE_TO_EDITOR: Record React.JSX.Element> = { [StepTypeEnum.CUSTOM]: () =>
CUSTOM Editor
, }; -export const StepEditor = ({ stepType }: { stepType: StepTypeEnum }) => { +export const StepEditor = ({ + workflow, + step, + stepType, +}: { + workflow: WorkflowResponseDto; + step: StepDataDto; + stepType: StepTypeEnum; +}) => { const Editor = STEP_TYPE_TO_EDITOR[stepType]; - return ; + return ; }; diff --git a/apps/dashboard/src/components/workflow-editor/url-input.tsx b/apps/dashboard/src/components/workflow-editor/url-input.tsx new file mode 100644 index 00000000000..4b28f712856 --- /dev/null +++ b/apps/dashboard/src/components/workflow-editor/url-input.tsx @@ -0,0 +1,103 @@ +import { liquid } from '@codemirror/lang-liquid'; +import { EditorView } from '@uiw/react-codemirror'; +import { useFormContext } from 'react-hook-form'; + +import { Input, InputField, InputFieldProps, InputProps } from '@/components/primitives/input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/primitives/select'; +import { Editor } from '@/components/primitives/editor'; +import { FormControl, FormField, FormItem, FormMessagePure } from '@/components/primitives/form/form'; +import { cn } from '@/utils/ui'; + +type URLInputProps = Omit & { + options: string[]; + asEditor?: boolean; + withHint?: boolean; + fields: { + urlKey: string; + targetKey: string; + }; +} & Pick; + +export const URLInput = ({ + options, + size = 'default', + asEditor = false, + placeholder, + fields: { urlKey, targetKey }, + withHint = true, +}: URLInputProps) => { + const { control, getFieldState } = useFormContext(); + const url = getFieldState(`${urlKey}`); + const target = getFieldState(`${targetKey}`); + const error = url.error || target.error; + + return ( +
+
+
+ + ( + + + {asEditor ? ( + + ) : ( + + )} + + + )} + /> + ( + + + + + + )} + /> + +
+
+ + {withHint && 'This support variables and relative URLs i.e /tasks/{{taskId}}'} + +
+ ); +}; diff --git a/apps/dashboard/src/components/workflow-editor/workflow-editor-provider.tsx b/apps/dashboard/src/components/workflow-editor/workflow-editor-provider.tsx index 718a4353c8b..9540d1fcdb8 100644 --- a/apps/dashboard/src/components/workflow-editor/workflow-editor-provider.tsx +++ b/apps/dashboard/src/components/workflow-editor/workflow-editor-provider.tsx @@ -47,7 +47,7 @@ const STEP_NAME_BY_TYPE: Record = { const createStep = (type: StepTypeEnum): Step => ({ name: STEP_NAME_BY_TYPE[type], stepId: '', - slug: '_stp_', + slug: '_st_', type, _id: crypto.randomUUID(), }); diff --git a/apps/dashboard/src/hooks/use-fetch-step.tsx b/apps/dashboard/src/hooks/use-fetch-step.tsx new file mode 100644 index 00000000000..f0fc1b99377 --- /dev/null +++ b/apps/dashboard/src/hooks/use-fetch-step.tsx @@ -0,0 +1,20 @@ +import { useQuery } from '@tanstack/react-query'; +import type { StepDataDto } from '@novu/shared'; +import { QueryKeys } from '@/utils/query-keys'; +import { useEnvironment } from '@/context/environment/hooks'; +import { fetchStep } from '@/api/steps'; + +export const useFetchStep = ({ workflowSlug, stepSlug }: { workflowSlug: string; stepSlug: string }) => { + const { currentEnvironment } = useEnvironment(); + const { data, isPending, error } = useQuery({ + queryKey: [QueryKeys.fetchWorkflow, currentEnvironment?._id, workflowSlug, stepSlug], + queryFn: () => fetchStep({ workflowSlug, stepSlug }), + enabled: !!currentEnvironment?._id && !!stepSlug, + }); + + return { + step: data, + isPending, + error, + }; +}; diff --git a/apps/dashboard/src/hooks/use-update-workflow.ts b/apps/dashboard/src/hooks/use-update-workflow.ts index a646cd3e990..e2827ccaa9e 100644 --- a/apps/dashboard/src/hooks/use-update-workflow.ts +++ b/apps/dashboard/src/hooks/use-update-workflow.ts @@ -2,11 +2,15 @@ import { useMutation } from '@tanstack/react-query'; import type { UpdateWorkflowDto, WorkflowResponseDto } from '@novu/shared'; import { updateWorkflow } from '@/api/workflows'; -export const useUpdateWorkflow = ({ onSuccess }: { onSuccess?: (data: WorkflowResponseDto) => void } = {}) => { +export const useUpdateWorkflow = ({ + onSuccess, + onError, +}: { onSuccess?: (data: WorkflowResponseDto) => void; onError?: (error: unknown) => void } = {}) => { const { mutateAsync, isPending, error, data } = useMutation({ mutationFn: async ({ id, workflow }: { id: string; workflow: UpdateWorkflowDto }) => updateWorkflow({ id, workflow }), onSuccess, + onError, }); return { diff --git a/apps/dashboard/src/utils/schema.ts b/apps/dashboard/src/utils/schema.ts new file mode 100644 index 00000000000..a3aa6928290 --- /dev/null +++ b/apps/dashboard/src/utils/schema.ts @@ -0,0 +1,167 @@ +import * as z from 'zod'; +import { UiSchema, WorkflowTestDataResponseDto } from '@novu/shared'; +import { capitalize } from './string'; + +type JSONSchema = WorkflowTestDataResponseDto['to']; + +type ZodValue = + | z.AnyZodObject + | z.ZodString + | z.ZodNumber + | z.ZodEffects + | z.ZodDefault + | z.ZodEnum<[string, ...string[]]> + | z.ZodOptional; + +const handleStringFormat = ({ value, key, format }: { value: z.ZodString; key: string; format: string }) => { + if (format === 'email') { + return value.email(`${capitalize(key)} must be a valid email`); + } else if (format === 'uri') { + return value + .transform((val) => (val === '' ? undefined : val)) + .refine((val) => !val || z.string().url().safeParse(val).success, { + message: `${capitalize(key)} must be a valid URI`, + }); + } + + return value; +}; + +const handleStringPattern = ({ value, key, pattern }: { value: z.ZodString; key: string; pattern: string }) => { + return value + .transform((val) => (val === '' ? undefined : val)) + .refine((val) => !val || z.string().regex(new RegExp(pattern)).safeParse(val).success, { + message: `${capitalize(key)} must be a valid value`, + }); +}; + +const handleStringEnum = ({ key, enumValues }: { key: string; enumValues: [string, ...string[]] }) => { + return z.enum(enumValues, { message: `${capitalize(key)} must be one of ${enumValues.join(', ')}` }); +}; + +const handleStringType = ({ + key, + format, + pattern, + enumValues, + defaultValue, + requiredFields, +}: { + key: string; + format?: string; + pattern?: string; + enumValues?: unknown; + defaultValue?: unknown; + requiredFields: Readonly>; +}) => { + const isRequired = requiredFields.includes(key); + + let stringValue: + | z.ZodString + | z.ZodEffects + | z.ZodEnum<[string, ...string[]]> + | z.ZodDefault = z.string(); + + if (format) { + stringValue = handleStringFormat({ + value: stringValue, + key, + format, + }); + } else if (pattern) { + stringValue = handleStringPattern({ + value: stringValue, + key, + pattern, + }); + } else if (enumValues) { + stringValue = handleStringEnum({ + key, + enumValues: enumValues as [string, ...string[]], + }); + } else if (isRequired) { + stringValue = stringValue.min(1, `${capitalize(key)} is required`); + } + + if (defaultValue) { + stringValue = stringValue.default(defaultValue as string); + } + + // remove empty strings + return stringValue.transform((val) => (val === '' ? undefined : val)); +}; + +/** + * Transform JSONSchema to Zod schema. + * The function will recursively build the schema based on the JSONSchema object. + * It removes empty strings and objects with empty required fields during the transformation phase after parsing. + */ +export const buildDynamicZodSchema = (obj: JSONSchema): z.AnyZodObject => { + const properties = typeof obj === 'object' ? (obj.properties ?? {}) : {}; + const requiredFields = typeof obj === 'object' ? (obj.required ?? []) : []; + + const keys: Record = Object.keys(properties).reduce((acc, key) => { + const jsonSchemaProp = properties[key]; + if (typeof jsonSchemaProp !== 'object') { + return acc; + } + + let zodValue: ZodValue; + const { type, format, pattern, enum: enumValues, default: defaultValue, required } = jsonSchemaProp; + const isRequired = requiredFields.includes(key); + + if (type === 'object') { + zodValue = buildDynamicZodSchema(jsonSchemaProp); + if (defaultValue) { + zodValue = zodValue.default(defaultValue); + } + zodValue = zodValue.transform((val) => { + const hasAnyRequiredEmpty = required?.some((field) => val[field] === '' || val[field] === undefined); + // remove object if any required field is empty or undefined + return hasAnyRequiredEmpty ? undefined : val; + }); + } else if (type === 'string') { + zodValue = handleStringType({ key, requiredFields, format, pattern, enumValues, defaultValue }); + } else { + zodValue = z.number(isRequired ? { message: `${capitalize(key)} is required` } : undefined); + if (defaultValue) { + zodValue = zodValue.default(defaultValue as number); + } + } + + if (!isRequired) { + zodValue = zodValue.optional() as ZodValue; + } + + return { ...acc, [key]: zodValue }; + }, {}); + + return z.object({ ...keys }); +}; + +/** + * Build default values based on the UI Schema object. + */ +export const buildDefaultValues = (uiSchema: UiSchema): object => { + const properties = typeof uiSchema === 'object' ? (uiSchema.properties ?? {}) : {}; + + const keys: Record = Object.keys(properties).reduce((acc, key) => { + const property = properties[key]; + if (typeof property !== 'object') { + return acc; + } + + const { placeholder: defaultValue } = property; + if (defaultValue === null || typeof defaultValue === 'undefined') { + return acc; + } + + if (typeof defaultValue === 'object') { + return { ...acc, [key]: buildDefaultValues({ properties: { ...defaultValue } }) }; + } + + return { ...acc, [key]: defaultValue }; + }, {}); + + return keys; +}; diff --git a/packages/shared/src/dto/workflows/step-data.dto.ts b/packages/shared/src/dto/workflows/step-data.dto.ts index a922899d049..6a8563762f0 100644 --- a/packages/shared/src/dto/workflows/step-data.dto.ts +++ b/packages/shared/src/dto/workflows/step-data.dto.ts @@ -25,6 +25,7 @@ export enum UiComponentEnum { } export class UiSchemaProperty { + placeholder?: unknown; component: UiComponentEnum; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 89276a512da..54020f41c27 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -807,6 +807,9 @@ importers: '@eslint/js': specifier: ^9.9.0 version: 9.9.1 + '@hookform/devtools': + specifier: ^4.3.0 + version: 4.3.1(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@playwright/test': specifier: ^1.44.0 version: 1.46.1 @@ -43674,6 +43677,19 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@emotion/styled@11.10.6(@emotion/react@11.11.1(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.23.2 + '@emotion/babel-plugin': 11.10.6 + '@emotion/is-prop-valid': 1.2.0 + '@emotion/react': 11.11.1(@types/react@18.3.3)(react@18.3.1) + '@emotion/serialize': 1.1.1 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.0(react@18.3.1) + '@emotion/utils': 1.2.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + '@emotion/unitless@0.7.5': {} '@emotion/unitless@0.8.0': {} @@ -44478,8 +44494,8 @@ snapshots: '@hookform/devtools@4.3.1(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@emotion/react': 11.10.6(@types/react@18.3.3)(react@18.3.1) - '@emotion/styled': 11.10.6(@emotion/react@11.10.6(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) + '@emotion/react': 11.11.1(@types/react@18.3.3)(react@18.3.1) + '@emotion/styled': 11.10.6(@emotion/react@11.11.1(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) '@types/lodash': 4.14.192 little-state-machine: 4.8.0(react@18.3.1) lodash: 4.17.21