diff --git a/packages/twenty-front/src/modules/ui/input/components/TextArea.tsx b/packages/twenty-front/src/modules/ui/input/components/TextArea.tsx index 9b50504c757b..b6bb9d545ab2 100644 --- a/packages/twenty-front/src/modules/ui/input/components/TextArea.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/TextArea.tsx @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import { FocusEventHandler } from 'react'; +import { FocusEventHandler, useId } from 'react'; import TextareaAutosize from 'react-textarea-autosize'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; @@ -10,6 +10,7 @@ import { InputHotkeyScope } from '../types/InputHotkeyScope'; const MAX_ROWS = 5; export type TextAreaProps = { + label?: string; disabled?: boolean; minRows?: number; onChange?: (value: string) => void; @@ -18,6 +19,20 @@ export type TextAreaProps = { className?: string; }; +const StyledContainer = styled.div` + display: flex; + flex-direction: column; + width: 100%; +`; + +const StyledLabel = styled.label` + color: ${({ theme }) => theme.font.color.light}; + display: block; + font-size: ${({ theme }) => theme.font.size.xs}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; + margin-bottom: ${({ theme }) => theme.spacing(1)}; +`; + const StyledTextArea = styled(TextareaAutosize)` background-color: ${({ theme }) => theme.background.transparent.lighter}; border: 1px solid ${({ theme }) => theme.border.color.medium}; @@ -48,6 +63,7 @@ const StyledTextArea = styled(TextareaAutosize)` `; export const TextArea = ({ + label, disabled, placeholder, minRows = 1, @@ -57,6 +73,8 @@ export const TextArea = ({ }: TextAreaProps) => { const computedMinRows = Math.min(minRows, MAX_ROWS); + const inputId = useId(); + const { goBackToPreviousHotkeyScope, setHotkeyScopeAndMemorizePreviousScope, @@ -71,18 +89,23 @@ export const TextArea = ({ }; return ( - - onChange?.(turnIntoEmptyStringIfWhitespacesOnly(event.target.value)) - } - onFocus={handleFocus} - onBlur={handleBlur} - disabled={disabled} - className={className} - /> + + {label && {label}} + + + onChange?.(turnIntoEmptyStringIfWhitespacesOnly(event.target.value)) + } + onFocus={handleFocus} + onBlur={handleBlur} + disabled={disabled} + className={className} + /> + ); }; diff --git a/packages/twenty-front/src/modules/ui/input/components/__stories__/TextArea.stories.tsx b/packages/twenty-front/src/modules/ui/input/components/__stories__/TextArea.stories.tsx index 425ba4f4d835..6583f9cbec24 100644 --- a/packages/twenty-front/src/modules/ui/input/components/__stories__/TextArea.stories.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/__stories__/TextArea.stories.tsx @@ -1,7 +1,9 @@ -import { useState } from 'react'; import { Meta, StoryObj } from '@storybook/react'; +import { useState } from 'react'; import { ComponentDecorator } from 'twenty-ui'; +import { expect } from '@storybook/jest'; +import { userEvent, within } from '@storybook/test'; import { TextArea, TextAreaProps } from '../TextArea'; type RenderProps = TextAreaProps; @@ -37,3 +39,20 @@ export const Filled: Story = { export const Disabled: Story = { args: { disabled: true, value: 'Lorem Ipsum' }, }; + +export const WithLabel: Story = { + args: { label: 'My Textarea' }, + play: async () => { + const canvas = within(document.body); + + const label = await canvas.findByText('My Textarea'); + + expect(label).toBeVisible(); + + await userEvent.click(label); + + const input = await canvas.findByRole('textbox'); + + expect(input).toHaveFocus(); + }, +}; diff --git a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowEditStepContent.tsx b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowEditStepContent.tsx index 9fb36225678c..3f3734b077ed 100644 --- a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowEditStepContent.tsx +++ b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowEditStepContent.tsx @@ -1,10 +1,12 @@ -import { WorkflowEditActionForm } from '@/workflow/components/WorkflowEditActionForm'; +import { WorkflowEditActionFormSendEmail } from '@/workflow/components/WorkflowEditActionFormSendEmail'; +import { WorkflowEditActionFormServerlessFunction } from '@/workflow/components/WorkflowEditActionFormServerlessFunction'; import { WorkflowEditTriggerForm } from '@/workflow/components/WorkflowEditTriggerForm'; import { TRIGGER_STEP_ID } from '@/workflow/constants/TriggerStepId'; import { useUpdateWorkflowVersionStep } from '@/workflow/hooks/useUpdateWorkflowVersionStep'; import { useUpdateWorkflowVersionTrigger } from '@/workflow/hooks/useUpdateWorkflowVersionTrigger'; import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState'; import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow'; +import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; import { findStepPositionOrThrow } from '@/workflow/utils/findStepPositionOrThrow'; import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-ui'; @@ -75,19 +77,39 @@ export const RightDrawerWorkflowEditStepContent = ({ workflow, }); - if (stepDefinition.type === 'trigger') { - return ( - - ); + switch (stepDefinition.type) { + case 'trigger': { + return ( + + ); + } + case 'action': { + switch (stepDefinition.definition.type) { + case 'CODE': { + return ( + + ); + } + case 'SEND_EMAIL': { + return ( + + ); + } + } + } } - return ( - + return assertUnreachable( + stepDefinition, + `Unsupported step: ${JSON.stringify(stepDefinition)}`, ); }; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNode.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNode.tsx index 0c0e6ce945ea..cc5f1a94eb3d 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNode.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNode.tsx @@ -1,8 +1,9 @@ import { WorkflowDiagramBaseStepNode } from '@/workflow/components/WorkflowDiagramBaseStepNode'; import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram'; +import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconCode, IconPlaylistAdd } from 'twenty-ui'; +import { IconCode, IconMail, IconPlaylistAdd } from 'twenty-ui'; const StyledStepNodeLabelIconContainer = styled.div` align-items: center; @@ -32,16 +33,33 @@ export const WorkflowDiagramStepNode = ({ ); } + case 'condition': { + return null; + } case 'action': { - return ( - - - - ); + switch (data.actionType) { + case 'CODE': { + return ( + + + + ); + } + case 'SEND_EMAIL': { + return ( + + + + ); + } + } } } - return null; + return assertUnreachable(data); }; return ( diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormBase.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormBase.tsx new file mode 100644 index 000000000000..77a50896c015 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormBase.tsx @@ -0,0 +1,61 @@ +import styled from '@emotion/styled'; +import React from 'react'; + +const StyledTriggerHeader = styled.div` + background-color: ${({ theme }) => theme.background.secondary}; + border-bottom: 1px solid ${({ theme }) => theme.border.color.medium}; + display: flex; + flex-direction: column; + padding: ${({ theme }) => theme.spacing(6)}; +`; + +const StyledTriggerHeaderTitle = styled.p` + color: ${({ theme }) => theme.font.color.primary}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; + font-size: ${({ theme }) => theme.font.size.xl}; + + margin: ${({ theme }) => theme.spacing(3)} 0; +`; + +const StyledTriggerHeaderType = styled.p` + color: ${({ theme }) => theme.font.color.tertiary}; + margin: 0; +`; + +const StyledTriggerHeaderIconContainer = styled.div` + align-self: flex-start; + display: flex; + justify-content: center; + align-items: center; + background-color: ${({ theme }) => theme.background.transparent.light}; + border-radius: ${({ theme }) => theme.border.radius.xs}; + padding: ${({ theme }) => theme.spacing(1)}; +`; + +export const WorkflowEditActionFormBase = ({ + ActionIcon, + actionTitle, + actionType, + children, +}: { + ActionIcon: React.ReactNode; + actionTitle: string; + actionType: string; + children: React.ReactNode; +}) => { + return ( + <> + + + {ActionIcon} + + + {actionTitle} + + {actionType} + + + {children} + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormSendEmail.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormSendEmail.tsx new file mode 100644 index 000000000000..0760e7d522ce --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormSendEmail.tsx @@ -0,0 +1,109 @@ +import { TextArea } from '@/ui/input/components/TextArea'; +import { TextInput } from '@/ui/input/components/TextInput'; +import { WorkflowEditActionFormBase } from '@/workflow/components/WorkflowEditActionFormBase'; +import { WorkflowSendEmailStep } from '@/workflow/types/Workflow'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { useEffect } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { IconMail } from 'twenty-ui'; +import { useDebouncedCallback } from 'use-debounce'; + +const StyledTriggerSettings = styled.div` + padding: ${({ theme }) => theme.spacing(6)}; + display: flex; + flex-direction: column; + row-gap: ${({ theme }) => theme.spacing(4)}; +`; + +type SendEmailFormData = { + subject: string; + body: string; +}; + +export const WorkflowEditActionFormSendEmail = ({ + action, + onActionUpdate, +}: { + action: WorkflowSendEmailStep; + onActionUpdate: (action: WorkflowSendEmailStep) => void; +}) => { + const theme = useTheme(); + + const form = useForm({ + defaultValues: { + subject: '', + body: '', + }, + }); + + useEffect(() => { + form.setValue('subject', action.settings.subject ?? ''); + form.setValue('body', action.settings.template ?? ''); + }, [action.settings.subject, action.settings.template, form]); + + const saveAction = useDebouncedCallback((formData: SendEmailFormData) => { + onActionUpdate({ + ...action, + settings: { + ...action.settings, + title: formData.subject, + subject: formData.subject, + template: formData.body, + }, + }); + }, 1_000); + + useEffect(() => { + return () => { + saveAction.flush(); + }; + }, [saveAction]); + + const handleSave = form.handleSubmit(saveAction); + + return ( + } + actionTitle="Send Email" + actionType="Email" + > + + ( + { + field.onChange(email); + + handleSave(); + }} + /> + )} + /> + + ( +