diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/WorkflowEmailEditor.tsx b/packages/twenty-front/src/modules/advanced-text-editor/components/AdvancedTextEditor.tsx similarity index 72% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/WorkflowEmailEditor.tsx rename to packages/twenty-front/src/modules/advanced-text-editor/components/AdvancedTextEditor.tsx index b80adeceb0..da7e9f889d 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/WorkflowEmailEditor.tsx +++ b/packages/twenty-front/src/modules/advanced-text-editor/components/AdvancedTextEditor.tsx @@ -1,15 +1,14 @@ +import { ImageBubbleMenu } from '@/advanced-text-editor/components/ImageBubbleMenu'; +import { LinkBubbleMenu } from '@/advanced-text-editor/components/LinkBubbleMenu'; +import { TextBubbleMenu } from '@/advanced-text-editor/components/TextBubbleMenu'; import { FORM_FIELD_PLACEHOLDER_STYLES } from '@/object-record/record-field/ui/form-types/constants/FormFieldPlaceholderStyles'; -import { ImageBubbleMenu } from '@/workflow/workflow-steps/workflow-actions/email-action/components/image-bubble-menu/ImageBubbleMenu'; -import { LinkBubbleMenu } from '@/workflow/workflow-steps/workflow-actions/email-action/components/link-bubble-menu/LinkBubbleMenu'; -import { TextBubbleMenu } from '@/workflow/workflow-steps/workflow-actions/email-action/components/text-bubble-menu/TextBubbleMenu'; import styled from '@emotion/styled'; import { EditorContent, type Editor } from '@tiptap/react'; -const EMAIL_EDITOR_MIN_HEIGHT = 340; -const EMAIL_EDITOR_MAX_WIDTH = 600; - const StyledEditorContainer = styled.div<{ readonly?: boolean; + minHeight: number; + maxWidth: number; }>` height: 100%; display: flex; @@ -21,8 +20,8 @@ const StyledEditorContainer = styled.div<{ flex-grow: 1; width: 100%; height: 100%; - min-height: ${EMAIL_EDITOR_MIN_HEIGHT}px; - max-width: ${EMAIL_EDITOR_MAX_WIDTH}px; + min-height: ${({ minHeight }) => minHeight}px; + max-width: ${({ maxWidth }) => maxWidth}px; margin: 0 auto; } @@ -78,17 +77,25 @@ const StyledEditorContainer = styled.div<{ } `; -type WorkflowEmailEditorProps = { +type AdvancedTextEditorProps = { readonly: boolean | undefined; editor: Editor; + minHeight: number; + maxWidth: number; }; -export const WorkflowEmailEditor = ({ +export const AdvancedTextEditor = ({ readonly, editor, -}: WorkflowEmailEditorProps) => { + minHeight, + maxWidth, +}: AdvancedTextEditorProps) => { return ( - + diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/text-bubble-menu/BubbleMenuIconButton.tsx b/packages/twenty-front/src/modules/advanced-text-editor/components/BubbleMenuIconButton.tsx similarity index 100% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/text-bubble-menu/BubbleMenuIconButton.tsx rename to packages/twenty-front/src/modules/advanced-text-editor/components/BubbleMenuIconButton.tsx diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/text-bubble-menu/EditLinkPopover.tsx b/packages/twenty-front/src/modules/advanced-text-editor/components/EditLinkPopover.tsx similarity index 94% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/text-bubble-menu/EditLinkPopover.tsx rename to packages/twenty-front/src/modules/advanced-text-editor/components/EditLinkPopover.tsx index c5eb44bf9b..c1a61fff77 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/text-bubble-menu/EditLinkPopover.tsx +++ b/packages/twenty-front/src/modules/advanced-text-editor/components/EditLinkPopover.tsx @@ -1,9 +1,9 @@ +import { BubbleMenuIconButton } from '@/advanced-text-editor/components/BubbleMenuIconButton'; import { TextInput } from '@/ui/input/components/TextInput'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useToggleDropdown } from '@/ui/layout/dropdown/hooks/useToggleDropdown'; -import { BubbleMenuIconButton } from '@/workflow/workflow-steps/workflow-actions/email-action/components/text-bubble-menu/BubbleMenuIconButton'; import { useLingui } from '@lingui/react/macro'; import { isNonEmptyString } from '@sniptt/guards'; import { type Editor } from '@tiptap/core'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/image-bubble-menu/ImageBubbleMenu.tsx b/packages/twenty-front/src/modules/advanced-text-editor/components/ImageBubbleMenu.tsx similarity index 88% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/image-bubble-menu/ImageBubbleMenu.tsx rename to packages/twenty-front/src/modules/advanced-text-editor/components/ImageBubbleMenu.tsx index c7eea22f3b..de389a1184 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/image-bubble-menu/ImageBubbleMenu.tsx +++ b/packages/twenty-front/src/modules/advanced-text-editor/components/ImageBubbleMenu.tsx @@ -1,5 +1,5 @@ -import { BubbleMenuIconButton } from '@/workflow/workflow-steps/workflow-actions/email-action/components/text-bubble-menu/BubbleMenuIconButton'; -import { StyledBubbleMenuContainer } from '@/workflow/workflow-steps/workflow-actions/email-action/components/text-bubble-menu/TextBubbleMenu'; +import { BubbleMenuIconButton } from '@/advanced-text-editor/components/BubbleMenuIconButton'; +import { StyledBubbleMenuContainer } from '@/advanced-text-editor/components/TextBubbleMenu'; import { type Editor } from '@tiptap/core'; import { useEditorState } from '@tiptap/react'; import { BubbleMenu } from '@tiptap/react/menus'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/link-bubble-menu/LinkBubbleMenu.tsx b/packages/twenty-front/src/modules/advanced-text-editor/components/LinkBubbleMenu.tsx similarity index 77% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/link-bubble-menu/LinkBubbleMenu.tsx rename to packages/twenty-front/src/modules/advanced-text-editor/components/LinkBubbleMenu.tsx index 024a0ace7c..01cd753e15 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/link-bubble-menu/LinkBubbleMenu.tsx +++ b/packages/twenty-front/src/modules/advanced-text-editor/components/LinkBubbleMenu.tsx @@ -1,6 +1,6 @@ -import { BubbleMenuIconButton } from '@/workflow/workflow-steps/workflow-actions/email-action/components/text-bubble-menu/BubbleMenuIconButton'; -import { EditLinkPopover } from '@/workflow/workflow-steps/workflow-actions/email-action/components/text-bubble-menu/EditLinkPopover'; -import { StyledBubbleMenuContainer } from '@/workflow/workflow-steps/workflow-actions/email-action/components/text-bubble-menu/TextBubbleMenu'; +import { BubbleMenuIconButton } from '@/advanced-text-editor/components/BubbleMenuIconButton'; +import { EditLinkPopover } from '@/advanced-text-editor/components/EditLinkPopover'; +import { StyledBubbleMenuContainer } from '@/advanced-text-editor/components/TextBubbleMenu'; import { type Editor } from '@tiptap/core'; import { useEditorState } from '@tiptap/react'; import { BubbleMenu } from '@tiptap/react/menus'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/text-bubble-menu/TextBubbleMenu.tsx b/packages/twenty-front/src/modules/advanced-text-editor/components/TextBubbleMenu.tsx similarity index 77% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/text-bubble-menu/TextBubbleMenu.tsx rename to packages/twenty-front/src/modules/advanced-text-editor/components/TextBubbleMenu.tsx index 8705bdb165..872d15a154 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/text-bubble-menu/TextBubbleMenu.tsx +++ b/packages/twenty-front/src/modules/advanced-text-editor/components/TextBubbleMenu.tsx @@ -1,8 +1,8 @@ -import { BubbleMenuIconButton } from '@/workflow/workflow-steps/workflow-actions/email-action/components/text-bubble-menu/BubbleMenuIconButton'; -import { EditLinkPopover } from '@/workflow/workflow-steps/workflow-actions/email-action/components/text-bubble-menu/EditLinkPopover'; -import { TurnIntoBlockDropdown } from '@/workflow/workflow-steps/workflow-actions/email-action/components/text-bubble-menu/TurnIntoBlockDropdown'; -import { useTextBubbleState } from '@/workflow/workflow-steps/workflow-actions/email-action/hooks/useTextBubbleState'; -import { isTextSelected } from '@/workflow/workflow-steps/workflow-actions/email-action/utils/isTextSelected'; +import { BubbleMenuIconButton } from '@/advanced-text-editor/components/BubbleMenuIconButton'; +import { EditLinkPopover } from '@/advanced-text-editor/components/EditLinkPopover'; +import { TurnIntoBlockDropdown } from '@/advanced-text-editor/components/TurnIntoBlockDropdown'; +import { useTextBubbleState } from '@/advanced-text-editor/hooks/useTextBubbleState'; +import { isTextSelected } from '@/advanced-text-editor/utils/isTextSelected'; import styled from '@emotion/styled'; import { type Editor } from '@tiptap/core'; import { BubbleMenu } from '@tiptap/react/menus'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/text-bubble-menu/TurnIntoBlockDropdown.tsx b/packages/twenty-front/src/modules/advanced-text-editor/components/TurnIntoBlockDropdown.tsx similarity index 95% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/text-bubble-menu/TurnIntoBlockDropdown.tsx rename to packages/twenty-front/src/modules/advanced-text-editor/components/TurnIntoBlockDropdown.tsx index c1bfa1653d..03b79277ac 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/text-bubble-menu/TurnIntoBlockDropdown.tsx +++ b/packages/twenty-front/src/modules/advanced-text-editor/components/TurnIntoBlockDropdown.tsx @@ -1,8 +1,8 @@ +import { useTurnIntoBlockOptions } from '@/advanced-text-editor/hooks/useTurnIntoBlockOptions'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useToggleDropdown } from '@/ui/layout/dropdown/hooks/useToggleDropdown'; -import { useTurnIntoBlockOptions } from '@/workflow/workflow-steps/workflow-actions/email-action/hooks/useTurnIntoBlockOptions'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { type Editor } from '@tiptap/react'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/__stories__/WorkflowEmailEditor.stories.tsx b/packages/twenty-front/src/modules/advanced-text-editor/components/__stories__/AdvancedTextEditor.stories.tsx similarity index 83% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/__stories__/WorkflowEmailEditor.stories.tsx rename to packages/twenty-front/src/modules/advanced-text-editor/components/__stories__/AdvancedTextEditor.stories.tsx index 5f2999211b..914592d367 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/__stories__/WorkflowEmailEditor.stories.tsx +++ b/packages/twenty-front/src/modules/advanced-text-editor/components/__stories__/AdvancedTextEditor.stories.tsx @@ -1,5 +1,5 @@ -import { WorkflowEmailEditor } from '@/workflow/workflow-steps/workflow-actions/email-action/components/WorkflowEmailEditor'; -import { useEmailEditor } from '@/workflow/workflow-steps/workflow-actions/email-action/hooks/useEmailEditor'; +import { AdvancedTextEditor } from '@/advanced-text-editor/components/AdvancedTextEditor'; +import { useAdvancedTextEditor } from '@/advanced-text-editor/hooks/useAdvancedTextEditor'; import { type Meta, type StoryObj } from '@storybook/react'; import { fn } from '@storybook/test'; import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing'; @@ -13,16 +13,20 @@ import { graphqlMocks } from '~/testing/graphqlMocks'; const EditorWrapper = ({ readonly = false, - placeholder = 'Enter email content...', + placeholder = 'Enter text content...', defaultValue = null, onUpdate = fn(), + minHeight = 200, + maxWidth = 800, }: { readonly?: boolean; placeholder?: string; defaultValue?: string | null; onUpdate?: (content: string) => void; + minHeight?: number; + maxWidth?: number; }) => { - const editor = useEmailEditor({ + const editor = useAdvancedTextEditor({ placeholder, readonly, defaultValue, @@ -43,11 +47,18 @@ const EditorWrapper = ({ return
Loading editor...
; } - return ; + return ( + + ); }; const meta: Meta = { - title: 'Modules/Workflow/Actions/SendEmail/EmailEditor', + title: 'Modules/AdvancedTextEditor/AdvancedTextEditor', component: EditorWrapper, parameters: { msw: graphqlMocks, @@ -87,7 +98,7 @@ export const WithContent: Story = { { type: 'text', marks: [{ type: 'bold' }], - text: 'John', + text: 'World', }, { type: 'text', @@ -100,7 +111,7 @@ export const WithContent: Story = { content: [ { type: 'text', - text: 'This is a sample email with ', + text: 'This is a sample text with ', }, { type: 'text', @@ -135,30 +146,30 @@ export const WithHeadings: Story = { { type: 'heading', attrs: { level: 1 }, - content: [{ type: 'text', text: 'Welcome Email' }], + content: [{ type: 'text', text: 'Main Title' }], }, { type: 'heading', attrs: { level: 2 }, - content: [{ type: 'text', text: 'Getting Started' }], + content: [{ type: 'text', text: 'Subtitle' }], }, { type: 'paragraph', content: [ { type: 'text', - text: 'Thank you for joining us! Here are some next steps:', + text: 'This is a paragraph with some content.', }, ], }, { type: 'heading', attrs: { level: 3 }, - content: [{ type: 'text', text: 'Next Steps' }], + content: [{ type: 'text', text: 'Smaller Heading' }], }, { type: 'paragraph', - content: [{ type: 'text', text: '1. Complete your profile' }], + content: [{ type: 'text', text: 'Another paragraph here.' }], }, ], }), @@ -224,7 +235,7 @@ export const WithVariableTags: Story = { type: 'variableTag', attrs: { variable: '{{orderNumber}}' }, }, - { type: 'text', text: ' has been shipped!' }, + { type: 'text', text: ' has been processed!' }, ], }, ], @@ -264,7 +275,7 @@ export const ReadOnly: Story = { export const Empty: Story = { args: { - placeholder: 'Start typing your email content...', + placeholder: 'Start typing your content...', }, }; @@ -274,3 +285,11 @@ export const Interactive: Story = { placeholder: 'Try typing, formatting text, or uploading images...', }, }; + +export const CustomSize: Story = { + args: { + minHeight: 300, + maxWidth: 600, + placeholder: 'This editor has custom dimensions...', + }, +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/extensions/resizable-image/ResizableImage.ts b/packages/twenty-front/src/modules/advanced-text-editor/extensions/resizable-image/ResizableImage.ts similarity index 74% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/extensions/resizable-image/ResizableImage.ts rename to packages/twenty-front/src/modules/advanced-text-editor/extensions/resizable-image/ResizableImage.ts index 2ada3295d5..6413ca93d2 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/extensions/resizable-image/ResizableImage.ts +++ b/packages/twenty-front/src/modules/advanced-text-editor/extensions/resizable-image/ResizableImage.ts @@ -1,4 +1,4 @@ -import { ResizableImageView } from '@/workflow/workflow-steps/workflow-actions/email-action/extensions/resizable-image/ResizableImageView'; +import { ResizableImageView } from '@/advanced-text-editor/extensions/resizable-image/ResizableImageView'; import { type ImageOptions, Image as TiptapImage, diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/extensions/resizable-image/ResizableImageView.tsx b/packages/twenty-front/src/modules/advanced-text-editor/extensions/resizable-image/ResizableImageView.tsx similarity index 100% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/extensions/resizable-image/ResizableImageView.tsx rename to packages/twenty-front/src/modules/advanced-text-editor/extensions/resizable-image/ResizableImageView.tsx diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/extensions/resizable-image/UploadImageExtension.ts b/packages/twenty-front/src/modules/advanced-text-editor/extensions/resizable-image/UploadImageExtension.ts similarity index 91% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/extensions/resizable-image/UploadImageExtension.ts rename to packages/twenty-front/src/modules/advanced-text-editor/extensions/resizable-image/UploadImageExtension.ts index 9b4f9de589..0346f01240 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/extensions/resizable-image/UploadImageExtension.ts +++ b/packages/twenty-front/src/modules/advanced-text-editor/extensions/resizable-image/UploadImageExtension.ts @@ -1,7 +1,7 @@ import { UploadImagePlugin, type UploadImagePluginProps, -} from '@/workflow/workflow-steps/workflow-actions/email-action/extensions/resizable-image/UploadImagePlugin'; +} from '@/advanced-text-editor/extensions/resizable-image/UploadImagePlugin'; import { Extension } from '@tiptap/core'; type UploadImageOptions = Omit & {}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/extensions/resizable-image/UploadImagePlugin.ts b/packages/twenty-front/src/modules/advanced-text-editor/extensions/resizable-image/UploadImagePlugin.ts similarity index 100% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/extensions/resizable-image/UploadImagePlugin.ts rename to packages/twenty-front/src/modules/advanced-text-editor/extensions/resizable-image/UploadImagePlugin.ts diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/hooks/useEmailEditor.ts b/packages/twenty-front/src/modules/advanced-text-editor/hooks/useAdvancedTextEditor.ts similarity index 82% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/hooks/useEmailEditor.ts rename to packages/twenty-front/src/modules/advanced-text-editor/hooks/useAdvancedTextEditor.ts index 9bc1172641..b552c5fa5f 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/hooks/useEmailEditor.ts +++ b/packages/twenty-front/src/modules/advanced-text-editor/hooks/useAdvancedTextEditor.ts @@ -1,6 +1,6 @@ -import { ResizableImage } from '@/workflow/workflow-steps/workflow-actions/email-action/extensions/resizable-image/ResizableImage'; -import { UploadImageExtension } from '@/workflow/workflow-steps/workflow-actions/email-action/extensions/resizable-image/UploadImageExtension'; -import { getInitialEmailEditorContent } from '@/workflow/workflow-variables/utils/getInitialEmailEditorContent'; +import { ResizableImage } from '@/advanced-text-editor/extensions/resizable-image/ResizableImage'; +import { UploadImageExtension } from '@/advanced-text-editor/extensions/resizable-image/UploadImageExtension'; +import { getInitialAdvancedTextEditorContent } from '@/workflow/workflow-variables/utils/getInitialAdvancedTextEditorContent'; import { VariableTag } from '@/workflow/workflow-variables/utils/variableTag'; import { Bold } from '@tiptap/extension-bold'; import { Document } from '@tiptap/extension-document'; @@ -17,7 +17,7 @@ import { type Editor, useEditor } from '@tiptap/react'; import { type DependencyList, useMemo } from 'react'; import { isDefined } from 'twenty-shared/utils'; -type UseEmailEditorProps = { +type UseAdvancedTextEditorProps = { placeholder: string | undefined; readonly: boolean | undefined; defaultValue: string | undefined | null; @@ -28,7 +28,7 @@ type UseEmailEditorProps = { onImageUploadError?: (error: Error, file: File) => void; }; -export const useEmailEditor = ( +export const useAdvancedTextEditor = ( { placeholder, readonly, @@ -38,7 +38,7 @@ export const useEmailEditor = ( onBlur, onImageUpload, onImageUploadError, - }: UseEmailEditorProps, + }: UseAdvancedTextEditorProps, dependencies?: DependencyList, ) => { const extensions = useMemo( @@ -78,7 +78,7 @@ export const useEmailEditor = ( { extensions, content: isDefined(defaultValue) - ? getInitialEmailEditorContent(defaultValue) + ? getInitialAdvancedTextEditorContent(defaultValue) : undefined, editable: !readonly, onUpdate: ({ editor }) => { diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/hooks/useTextBubbleState.ts b/packages/twenty-front/src/modules/advanced-text-editor/hooks/useTextBubbleState.ts similarity index 100% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/hooks/useTextBubbleState.ts rename to packages/twenty-front/src/modules/advanced-text-editor/hooks/useTextBubbleState.ts diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/hooks/useTurnIntoBlockOptions.ts b/packages/twenty-front/src/modules/advanced-text-editor/hooks/useTurnIntoBlockOptions.ts similarity index 100% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/hooks/useTurnIntoBlockOptions.ts rename to packages/twenty-front/src/modules/advanced-text-editor/hooks/useTurnIntoBlockOptions.ts diff --git a/packages/twenty-front/src/modules/advanced-text-editor/index.ts b/packages/twenty-front/src/modules/advanced-text-editor/index.ts new file mode 100644 index 0000000000..12b282d7b9 --- /dev/null +++ b/packages/twenty-front/src/modules/advanced-text-editor/index.ts @@ -0,0 +1,5 @@ +export { AdvancedTextEditor } from './components/AdvancedTextEditor'; +export { ImageBubbleMenu } from './components/ImageBubbleMenu'; +export { LinkBubbleMenu } from './components/LinkBubbleMenu'; +export { TextBubbleMenu } from './components/TextBubbleMenu'; +export { useAdvancedTextEditor } from './hooks/useAdvancedTextEditor'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/utils/isTextSelected.ts b/packages/twenty-front/src/modules/advanced-text-editor/utils/isTextSelected.ts similarity index 100% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/utils/isTextSelected.ts rename to packages/twenty-front/src/modules/advanced-text-editor/utils/isTextSelected.ts diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/WorkflowSendEmailBody.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormAdvancedTextFieldInput.tsx similarity index 56% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/WorkflowSendEmailBody.tsx rename to packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormAdvancedTextFieldInput.tsx index ca6cafe6fb..c93537cbb3 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/WorkflowSendEmailBody.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormAdvancedTextFieldInput.tsx @@ -1,8 +1,7 @@ -import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { AdvancedTextEditor } from '@/advanced-text-editor/components/AdvancedTextEditor'; +import { useAdvancedTextEditor } from '@/advanced-text-editor/hooks/useAdvancedTextEditor'; import { FormFieldInputContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputContainer'; import { type VariablePickerComponent } from '@/object-record/record-field/ui/form-types/types/VariablePickerComponent'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { InputErrorHelper } from '@/ui/input/components/InputErrorHelper'; import { InputHint } from '@/ui/input/components/InputHint'; import { InputLabel } from '@/ui/input/components/InputLabel'; @@ -12,12 +11,6 @@ import { type BreadcrumbProps } from '@/ui/navigation/bread-crumb/components/Bre import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById'; import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; -import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; -import { type WorkflowSendEmailAction } from '@/workflow/types/Workflow'; -import { WorkflowEmailEditor } from '@/workflow/workflow-steps/workflow-actions/email-action/components/WorkflowEmailEditor'; -import { useEmailEditor } from '@/workflow/workflow-steps/workflow-actions/email-action/hooks/useEmailEditor'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; @@ -26,11 +19,11 @@ import { isDefined } from 'twenty-shared/utils'; import { IconMaximize } from 'twenty-ui/display'; import { useIsMobile } from 'twenty-ui/utilities'; -const StyledWorkflowSendEmailBodyContainer = styled(FormFieldInputContainer)` +const StyledAdvancedTextFieldContainer = styled(FormFieldInputContainer)` flex-grow: 1; `; -const StyledWorkflowSendEmailFieldContainer = styled.div` +const StyledAdvancedTextFieldFieldContainer = styled.div` position: relative; display: flex; flex-direction: column; @@ -38,7 +31,7 @@ const StyledWorkflowSendEmailFieldContainer = styled.div` flex-grow: 1; `; -const StyledWorkflowSendEmailBodyInnerContainer = styled.div` +const StyledAdvancedTextFieldInnerContainer = styled.div` flex-grow: 1; background-color: ${({ theme }) => theme.background.transparent.lighter}; border: 1px solid ${({ theme }) => theme.border.color.medium}; @@ -50,14 +43,14 @@ const StyledWorkflowSendEmailBodyInnerContainer = styled.div` width: 100%; `; -const StyledEmailEditorActionButtonContainer = styled.div` +const StyledEditorActionButtonContainer = styled.div` position: absolute; top: ${({ theme }) => theme.spacing(0)}; right: ${({ theme }) => theme.spacing(7.5)}; z-index: 1; `; -const StyledFullScreenEmailEditorContainer = styled.div` +const StyledFullScreenEditorContainer = styled.div` background-color: ${({ theme }) => theme.background.secondary}; border: 1px solid ${({ theme }) => theme.border.color.medium}; border-radius: ${({ theme }) => theme.border.radius.sm}; @@ -68,7 +61,7 @@ const StyledFullScreenEmailEditorContainer = styled.div` `; const StyledFullScreenButtonContainer = styled(StyledDropdownButtonContainer)` - background-color: 'transparent'; + background-color: transparent; color: ${({ theme }) => theme.font.color.tertiary}; padding: ${({ theme }) => theme.spacing(2)}; @@ -78,8 +71,7 @@ const StyledFullScreenButtonContainer = styled(StyledDropdownButtonContainer)` } `; -type WorkflowSendEmailBodyProps = { - action: WorkflowSendEmailAction; +type FormAdvancedTextFieldInputProps = { label?: string; error?: string; hint?: string; @@ -88,10 +80,15 @@ type WorkflowSendEmailBodyProps = { readonly?: boolean; placeholder?: string; VariablePicker?: VariablePickerComponent; + onImageUpload?: (file: File) => Promise; + onImageUploadError?: (error: Error, file: File) => void; + enableFullScreen?: boolean; + fullScreenBreadcrumbs?: BreadcrumbProps['links']; + minHeight: number; + maxWidth: number; }; -export const WorkflowSendEmailBody = ({ - action, +export const FormAdvancedTextFieldInput = ({ label, error, hint, @@ -100,47 +97,26 @@ export const WorkflowSendEmailBody = ({ onChange, readonly, VariablePicker, -}: WorkflowSendEmailBodyProps) => { + onImageUpload, + onImageUploadError, + enableFullScreen = true, + fullScreenBreadcrumbs, + minHeight, + maxWidth, +}: FormAdvancedTextFieldInputProps) => { const instanceId = useId(); const isMobile = useIsMobile(); const [isFullScreen, setIsFullScreen] = useState(false); const theme = useTheme(); - const { uploadAttachmentFile } = useUploadAttachmentFile(); - const { enqueueErrorSnackBar } = useSnackBar(); const { t } = useLingui(); const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); const { removeFocusItemFromFocusStackById } = useRemoveFocusItemFromFocusStackById(); - const workflowVisualizerWorkflowId = useRecoilComponentValue( - workflowVisualizerWorkflowIdComponentState, - ); - const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId); - const headerTitle = isDefined(action.name) ? action.name : 'Send Email'; - - const handleUploadAttachment = async (file: File) => { - if (!isDefined(workflowVisualizerWorkflowId)) { - return undefined; - } - - const { attachmentAbsoluteURL } = await uploadAttachmentFile(file, { - id: workflowVisualizerWorkflowId, - targetObjectNameSingular: CoreObjectNameSingular.Workflow, - }); - - return attachmentAbsoluteURL; - }; - - const handleImageUploadError = (error: Error, file: File) => { - enqueueErrorSnackBar({ - message: t`Failed to upload image: `.concat(file.name), - }); - }; - - const editor = useEmailEditor( + const editor = useAdvancedTextEditor( { - placeholder: placeholder ?? 'Enter email', + placeholder: placeholder ?? 'Enter text', readonly, defaultValue, onUpdate: (editor) => { @@ -162,8 +138,8 @@ export const WorkflowSendEmailBody = ({ onBlur: () => { removeFocusItemFromFocusStackById({ focusId: instanceId }); }, - onImageUpload: handleUploadAttachment, - onImageUploadError: handleImageUploadError, + onImageUpload, + onImageUploadError, }, [isFullScreen], ); @@ -186,34 +162,35 @@ export const WorkflowSendEmailBody = ({ editor.commands.insertVariableTag(variableName); }; - const breadcrumbLinks: BreadcrumbProps['links'] = [ + const defaultBreadcrumbs: BreadcrumbProps['links'] = [ { - children: workflow?.name?.trim() || t`Untitled Workflow`, - href: '#', - }, - { - children: headerTitle, - href: '#', - }, - { - children: t`Email Editor`, + children: t`Text Editor`, }, ]; + const breadcrumbLinks = fullScreenBreadcrumbs || defaultBreadcrumbs; + const { renderFullScreenModal } = useFullScreenModal({ links: breadcrumbLinks, onClose: handleExitFullScreen, hasClosePageButton: !isMobile, }); - const fullScreenOverlay = renderFullScreenModal( -
- - - -
, - isFullScreen, - ); + const fullScreenOverlay = enableFullScreen + ? renderFullScreenModal( +
+ + + +
, + isFullScreen, + ) + : null; if (!isDefined(editor)) { return null; @@ -221,26 +198,33 @@ export const WorkflowSendEmailBody = ({ return ( <> - + {label ? {label} : null} - - + + {!isFullScreen && ( - + )} - - {!readonly && !isFullScreen && ( - - - - )} - + {enableFullScreen && ( + + {!readonly && !isFullScreen && ( + + + + )} + + )} {VariablePicker && !readonly ? ( ) : null} - - + + {hint && {hint}} {error && {error}} - + {fullScreenOverlay} diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormAdvancedTextFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormAdvancedTextFieldInput.stories.tsx new file mode 100644 index 0000000000..ae6d1161d9 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormAdvancedTextFieldInput.stories.tsx @@ -0,0 +1,270 @@ +import { FormAdvancedTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormAdvancedTextFieldInput'; +import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; +import { type Meta, type StoryObj } from '@storybook/react'; +import { expect, fn, userEvent, within } from '@storybook/test'; +import { graphql, HttpResponse } from 'msw'; +import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing'; +import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; +import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; +import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; +import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator'; +import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator'; +import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator'; +import { graphqlMocks } from '~/testing/graphqlMocks'; +import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow'; + +const DEFAULT_PROPS = { + label: 'Advanced Text Field', + placeholder: 'Enter your content...', + defaultValue: '', + onChange: fn(), + readonly: false, + maxHeight: 200, + maxWidth: 800, +}; + +const RICH_CONTENT_VALUE = JSON.stringify({ + type: 'doc', + content: [ + { + type: 'heading', + attrs: { level: 1 }, + content: [{ type: 'text', text: 'Welcome!' }], + }, + { + type: 'paragraph', + content: [ + { type: 'text', text: 'Dear ' }, + { + type: 'variableTag', + attrs: { variable: '{{firstName}}' }, + }, + { type: 'text', text: ',' }, + ], + }, + { + type: 'paragraph', + content: [ + { type: 'text', text: 'Thank you for joining us! We are ' }, + { + type: 'text', + marks: [{ type: 'bold' }], + text: 'excited', + }, + { type: 'text', text: ' to have you on board.' }, + ], + }, + { + type: 'paragraph', + content: [ + { type: 'text', text: 'Visit our ' }, + { + type: 'text', + marks: [{ type: 'link', attrs: { href: 'https://twenty.com' } }], + text: 'website', + }, + { type: 'text', text: ' to get started.' }, + ], + }, + ], +}); + +const meta: Meta = { + title: + 'Modules/ObjectRecord/RecordField/FormTypes/FormAdvancedTextFieldInput', + component: FormAdvancedTextFieldInput, + parameters: { + msw: { + handlers: [ + ...graphqlMocks.handlers, + graphql.query('FindManyWorkflows', () => { + return HttpResponse.json({ + data: { + workflows: [ + { + id: getWorkflowNodeIdMock(), + name: 'Test Workflow', + __typename: 'Workflow', + }, + ], + }, + }); + }), + ], + }, + }, + decorators: [ + WorkflowStepActionDrawerDecorator, + WorkflowStepDecorator, + ComponentDecorator, + ObjectMetadataItemsDecorator, + SnackBarDecorator, + RouterDecorator, + WorkspaceDecorator, + I18nFrontDecorator, + ], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + ...DEFAULT_PROPS, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(await canvas.findByText('Advanced Text Field')).toBeVisible(); + expect(await canvas.findByRole('textbox')).toBeVisible(); + }, +}; + +export const WithRichContent: Story = { + args: { + ...DEFAULT_PROPS, + label: 'Rich Text Content', + defaultValue: RICH_CONTENT_VALUE, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(await canvas.findByText('Rich Text Content')).toBeVisible(); + expect(await canvas.findByText('Welcome!')).toBeVisible(); + expect(await canvas.findByText('excited')).toBeVisible(); + expect(await canvas.findByText('website')).toBeVisible(); + }, +}; + +export const ReadOnly: Story = { + args: { + ...DEFAULT_PROPS, + label: 'Read Only Field', + defaultValue: RICH_CONTENT_VALUE, + readonly: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(await canvas.findByText('Read Only Field')).toBeVisible(); + expect(await canvas.findByText('Welcome!')).toBeVisible(); + }, +}; + +export const WithError: Story = { + args: { + ...DEFAULT_PROPS, + label: 'Field with Error', + error: 'This field is required', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(await canvas.findByText('Field with Error')).toBeVisible(); + expect(await canvas.findByText('This field is required')).toBeVisible(); + }, +}; + +export const WithHint: Story = { + args: { + ...DEFAULT_PROPS, + label: 'Field with Hint', + hint: 'You can use variables, format text, and add images to your content.', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(await canvas.findByText('Field with Hint')).toBeVisible(); + expect( + await canvas.findByText( + 'You can use variables, format text, and add images to your content.', + ), + ).toBeVisible(); + }, +}; + +export const Interactive: Story = { + args: { + ...DEFAULT_PROPS, + label: 'Interactive Field', + placeholder: 'Start typing to see the editor in action...', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(await canvas.findByText('Interactive Field')).toBeVisible(); + + const editor = await canvas.findByRole('textbox'); + expect(editor).toBeVisible(); + + await userEvent.click(editor); + await userEvent.type(editor, 'Hello World!'); + }, +}; + +export const WithVariablePicker: Story = { + args: { + ...DEFAULT_PROPS, + label: 'Field with Variable Picker', + VariablePicker: WorkflowVariablePicker, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(await canvas.findByText('Field with Variable Picker')).toBeVisible(); + expect(await canvas.findByRole('textbox')).toBeVisible(); + }, +}; + +export const WithoutVariablePicker: Story = { + args: { + ...DEFAULT_PROPS, + label: 'Field without Variable Picker', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect( + await canvas.findByText('Field without Variable Picker'), + ).toBeVisible(); + expect(await canvas.findByRole('textbox')).toBeVisible(); + }, +}; + +export const WithLongContent: Story = { + args: { + ...DEFAULT_PROPS, + label: 'Field with Long Content', + defaultValue: JSON.stringify({ + type: 'doc', + content: Array.from({ length: 20 }, (_, i) => ({ + type: 'paragraph', + content: [ + { + type: 'text', + text: `This is paragraph ${i + 1}. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.`, + }, + ], + })), + }), + }, +}; + +export const CustomSize: Story = { + args: { + ...DEFAULT_PROPS, + label: 'Custom Size Field', + maxHeight: 300, + maxWidth: 600, + placeholder: 'This field has custom dimensions...', + }, +}; + +export const DisabledFullScreen: Story = { + args: { + ...DEFAULT_PROPS, + label: 'Field without Full Screen', + enableFullScreen: false, + }, +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail.tsx index 0b2fffa7c9..159bd28518 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail.tsx @@ -1,23 +1,28 @@ import { GMAIL_SEND_SCOPE } from '@/accounts/constants/GmailSendScope'; import { MICROSOFT_SEND_SCOPE } from '@/accounts/constants/MicrosoftSendScope'; import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount'; +import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { FormAdvancedTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormAdvancedTextFieldInput'; import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput'; import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Select } from '@/ui/input/components/Select'; import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; import { type WorkflowSendEmailAction } from '@/workflow/types/Workflow'; import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; -import { WorkflowSendEmailBody } from '@/workflow/workflow-steps/workflow-actions/email-action/components/WorkflowSendEmailBody'; import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader'; import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; import { useTheme } from '@emotion/react'; +import { t } from '@lingui/core/macro'; import { useEffect, useState } from 'react'; import { useRecoilValue } from 'recoil'; import { ConnectedAccountProvider, SettingsPath } from 'twenty-shared/types'; @@ -28,6 +33,9 @@ import { type JsonValue } from 'type-fest'; import { useDebouncedCallback } from 'use-debounce'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; +const EMAIL_EDITOR_MIN_HEIGHT = 340; +const EMAIL_EDITOR_MAX_WIDTH = 600; + type WorkflowEditActionSendEmailProps = { action: WorkflowSendEmailAction; actionOptions: @@ -55,10 +63,15 @@ export const WorkflowEditActionSendEmail = ({ const { getIcon } = useIcons(); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const { triggerApisOAuth } = useTriggerApisOAuth(); + const { enqueueErrorSnackBar } = useSnackBar(); + const { uploadAttachmentFile } = useUploadAttachmentFile(); const workflowVisualizerWorkflowId = useRecoilComponentValue( workflowVisualizerWorkflowIdComponentState, ); + + const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId); + const redirectUrl = `/object/workflow/${workflowVisualizerWorkflowId}`; const [formData, setFormData] = useState({ @@ -115,7 +128,6 @@ export const WorkflowEditActionSendEmail = ({ if (actionOptions.readonly === true) { return; } - actionOptions.onActionUpdate({ ...action, settings: { @@ -154,6 +166,25 @@ export const WorkflowEditActionSendEmail = ({ saveAction(newFormData); }; + const handleUploadAttachment = async (file: File) => { + if (!isDefined(workflowVisualizerWorkflowId)) { + return undefined; + } + + const { attachmentAbsoluteURL } = await uploadAttachmentFile(file, { + id: workflowVisualizerWorkflowId, + targetObjectNameSingular: CoreObjectNameSingular.Workflow, + }); + + return attachmentAbsoluteURL; + }; + + const handleImageUploadError = (error: Error, file: File) => { + enqueueErrorSnackBar({ + message: t`Failed to upload image: `.concat(file.name), + }); + }; + const filter: { or: object[] } = { or: [ { @@ -285,16 +316,33 @@ export const WorkflowEditActionSendEmail = ({ }} VariablePicker={WorkflowVariablePicker} /> - { + onChange={(body: string) => { handleFieldChange('body', body); }} VariablePicker={WorkflowVariablePicker} + enableFullScreen={true} + fullScreenBreadcrumbs={[ + { + children: workflow?.name?.trim() || t`Untitled Workflow`, + href: '#', + }, + { + children: headerTitle, + href: '#', + }, + { + children: t`Email Editor`, + }, + ]} + onImageUpload={handleUploadAttachment} + onImageUploadError={handleImageUploadError} + minHeight={EMAIL_EDITOR_MIN_HEIGHT} + maxWidth={EMAIL_EDITOR_MAX_WIDTH} /> {!actionOptions.readonly && } diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/__stories__/WorkflowSendEmailBody.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/__stories__/WorkflowSendEmailBody.stories.tsx deleted file mode 100644 index 82b558123c..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/email-action/components/__stories__/WorkflowSendEmailBody.stories.tsx +++ /dev/null @@ -1,320 +0,0 @@ -import { type WorkflowSendEmailAction } from '@/workflow/types/Workflow'; -import { WorkflowSendEmailBody } from '@/workflow/workflow-steps/workflow-actions/email-action/components/WorkflowSendEmailBody'; -import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; -import { type Meta, type StoryObj } from '@storybook/react'; -import { expect, fn, userEvent, within } from '@storybook/test'; -import { graphql, HttpResponse } from 'msw'; -import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing'; -import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; -import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; -import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; -import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator'; -import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator'; -import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator'; -import { graphqlMocks } from '~/testing/graphqlMocks'; -import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow'; - -const DEFAULT_ACTION: WorkflowSendEmailAction = { - id: getWorkflowNodeIdMock(), - name: 'Send Email', - type: 'SEND_EMAIL', - valid: false, - settings: { - input: { - connectedAccountId: '', - email: '', - subject: '', - body: '', - }, - outputSchema: {}, - errorHandlingOptions: { - retryOnFailure: { value: false }, - continueOnFailure: { value: false }, - }, - }, -}; - -const RICH_CONTENT_ACTION: WorkflowSendEmailAction = { - id: getWorkflowNodeIdMock(), - name: 'Welcome Email', - type: 'SEND_EMAIL', - valid: true, - settings: { - input: { - connectedAccountId: 'test-account-id', - email: 'user@example.com', - subject: 'Welcome to Twenty!', - body: JSON.stringify({ - type: 'doc', - content: [ - { - type: 'heading', - attrs: { level: 1 }, - content: [{ type: 'text', text: 'Welcome to Twenty!' }], - }, - { - type: 'paragraph', - content: [ - { type: 'text', text: 'Dear ' }, - { - type: 'variableTag', - attrs: { variable: '{{firstName}}' }, - }, - { type: 'text', text: ',' }, - ], - }, - { - type: 'paragraph', - content: [ - { type: 'text', text: 'Thank you for joining us! We are ' }, - { - type: 'text', - marks: [{ type: 'bold' }], - text: 'excited', - }, - { type: 'text', text: ' to have you on board.' }, - ], - }, - { - type: 'paragraph', - content: [ - { type: 'text', text: 'Visit our ' }, - { - type: 'text', - marks: [ - { type: 'link', attrs: { href: 'https://twenty.com' } }, - ], - text: 'website', - }, - { type: 'text', text: ' to get started.' }, - ], - }, - ], - }), - }, - outputSchema: {}, - errorHandlingOptions: { - retryOnFailure: { value: true }, - continueOnFailure: { value: false }, - }, - }, -}; - -const meta: Meta = { - title: 'Modules/Workflow/Actions/SendEmail/EmailBody', - component: WorkflowSendEmailBody, - parameters: { - msw: { - handlers: [ - ...graphqlMocks.handlers, - graphql.query('FindManyWorkflows', () => { - return HttpResponse.json({ - data: { - workflows: [ - { - id: getWorkflowNodeIdMock(), - name: 'Test Workflow', - __typename: 'Workflow', - }, - ], - }, - }); - }), - ], - }, - }, - decorators: [ - WorkflowStepActionDrawerDecorator, - WorkflowStepDecorator, - ComponentDecorator, - ObjectMetadataItemsDecorator, - SnackBarDecorator, - RouterDecorator, - WorkspaceDecorator, - I18nFrontDecorator, - ], -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { - action: DEFAULT_ACTION, - label: 'Email Body', - placeholder: 'Enter your email content...', - defaultValue: '', - onChange: fn(), - readonly: false, - VariablePicker: WorkflowVariablePicker, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - expect(await canvas.findByText('Email Body')).toBeVisible(); - expect(await canvas.findByRole('textbox')).toBeVisible(); - }, -}; - -export const WithRichContent: Story = { - args: { - action: RICH_CONTENT_ACTION, - label: 'Email Body', - placeholder: 'Enter your email content...', - defaultValue: RICH_CONTENT_ACTION.settings.input.body, - onChange: fn(), - readonly: false, - VariablePicker: WorkflowVariablePicker, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - expect(await canvas.findByText('Email Body')).toBeVisible(); - expect(await canvas.findByText('Welcome to Twenty!')).toBeVisible(); - expect(await canvas.findByText('excited')).toBeVisible(); - expect(await canvas.findByText('website')).toBeVisible(); - }, -}; - -export const ReadOnly: Story = { - args: { - action: RICH_CONTENT_ACTION, - label: 'Email Body (Read Only)', - defaultValue: RICH_CONTENT_ACTION.settings.input.body, - onChange: fn(), - readonly: true, - VariablePicker: WorkflowVariablePicker, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - expect(await canvas.findByText('Email Body (Read Only)')).toBeVisible(); - expect(await canvas.findByText('Welcome to Twenty!')).toBeVisible(); - }, -}; - -export const WithError: Story = { - args: { - action: DEFAULT_ACTION, - label: 'Email Body', - error: 'Email body is required', - placeholder: 'Enter your email content...', - defaultValue: '', - onChange: fn(), - readonly: false, - VariablePicker: WorkflowVariablePicker, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - expect(await canvas.findByText('Email Body')).toBeVisible(); - expect(await canvas.findByText('Email body is required')).toBeVisible(); - }, -}; - -export const WithHint: Story = { - args: { - action: DEFAULT_ACTION, - label: 'Email Body', - hint: 'You can use variables, format text, and add images to your email content.', - placeholder: 'Enter your email content...', - defaultValue: '', - onChange: fn(), - readonly: false, - VariablePicker: WorkflowVariablePicker, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - expect(await canvas.findByText('Email Body')).toBeVisible(); - expect( - await canvas.findByText( - 'You can use variables, format text, and add images to your email content.', - ), - ).toBeVisible(); - }, -}; - -export const Interactive: Story = { - args: { - action: DEFAULT_ACTION, - label: 'Email Body', - placeholder: 'Start typing to see the editor in action...', - defaultValue: '', - onChange: fn(), - readonly: false, - VariablePicker: WorkflowVariablePicker, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - expect(await canvas.findByText('Email Body')).toBeVisible(); - - const editor = await canvas.findByRole('textbox'); - expect(editor).toBeVisible(); - - await userEvent.click(editor); - await userEvent.type(editor, 'Hello World!'); - }, -}; - -export const WithoutVariablePicker: Story = { - args: { - action: DEFAULT_ACTION, - label: 'Email Body', - placeholder: 'Enter your email content...', - defaultValue: '', - onChange: fn(), - readonly: false, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - expect(await canvas.findByText('Email Body')).toBeVisible(); - expect(await canvas.findByRole('textbox')).toBeVisible(); - }, -}; - -export const WithLongContent: Story = { - args: { - action: { - ...DEFAULT_ACTION, - settings: { - ...DEFAULT_ACTION.settings, - input: { - ...DEFAULT_ACTION.settings.input, - body: JSON.stringify({ - type: 'doc', - content: Array.from({ length: 20 }, (_, i) => ({ - type: 'paragraph', - content: [ - { - type: 'text', - text: `This is paragraph ${i + 1}. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.`, - }, - ], - })), - }), - }, - }, - }, - label: 'Email Body', - placeholder: 'Enter your email content...', - defaultValue: JSON.stringify({ - type: 'doc', - content: Array.from({ length: 20 }, (_, i) => ({ - type: 'paragraph', - content: [ - { - type: 'text', - text: `This is paragraph ${i + 1}. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.`, - }, - ], - })), - }), - onChange: fn(), - readonly: false, - VariablePicker: WorkflowVariablePicker, - }, -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getInitialEmailEditorContent.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getInitialAdvancedTextEditorContent.ts similarity index 93% rename from packages/twenty-front/src/modules/workflow/workflow-variables/utils/getInitialEmailEditorContent.ts rename to packages/twenty-front/src/modules/workflow/workflow-variables/utils/getInitialAdvancedTextEditorContent.ts index c2fd911435..4f99d628fb 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getInitialEmailEditorContent.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getInitialAdvancedTextEditorContent.ts @@ -5,7 +5,7 @@ import { logError } from '~/utils/logError'; // Previous format of the email body was plain text, // but from now on we will save it as JSON. // So it will fail to parse the content, that's why we have this fallback. -export const getInitialEmailEditorContent = ( +export const getInitialAdvancedTextEditorContent = ( rawContent: string, ): JSONContent => { // Handle empty or null content