From c17e18b1e928eac02afaee12ff04cbf51f48e712 Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Mon, 18 Nov 2024 18:23:46 +0100 Subject: [PATCH] Add Record Create action in the frontend (#8514) In this PR: - Updated the front-end types for workflows to include CRUD actions and global naming changes - Allow users to create a Record Create action - Scaffold the edit for Record Create action; for now, I render a `` component for every editable field; it's likely we'll change it soon Closes https://github.com/twentyhq/private-issues/issues/142 Demo: https://github.com/user-attachments/assets/6f0b207a-b7d2-46d9-b5ab-9e32bde55d76 --- .../components/FormFieldInput.tsx | 26 +++ .../WorkflowDiagramStepNodeBase.tsx | 23 ++- .../WorkflowEditActionFormRecordCreate.tsx | 167 ++++++++++++++++++ .../WorkflowEditActionFormSendEmail.tsx | 56 +++--- ...rkflowEditActionFormServerlessFunction.tsx | 75 ++++---- .../WorkflowEditTriggerDatabaseEventForm.tsx | 38 ++-- .../WorkflowEditTriggerManualForm.tsx | 38 ++-- .../components/WorkflowStepDetail.tsx | 26 ++- .../src/modules/workflow/constants/Actions.ts | 11 +- .../modules/workflow/hooks/useCreateStep.ts | 10 +- .../src/modules/workflow/types/Workflow.ts | 76 ++++++-- .../workflow/utils/generateWorkflowDiagram.ts | 24 ++- .../utils/getStepDefaultDefinition.ts | 39 +++- .../utils/isWorkflowRecordCreateAction.ts | 12 ++ .../icon/components/IconAddressBook.tsx | 14 +- 15 files changed, 503 insertions(+), 132 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx create mode 100644 packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormRecordCreate.tsx create mode 100644 packages/twenty-front/src/modules/workflow/utils/isWorkflowRecordCreateAction.ts diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx new file mode 100644 index 0000000000..993dd28634 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx @@ -0,0 +1,26 @@ +import VariableTagInput from '@/workflow/search-variables/components/VariableTagInput'; + +type FormFieldInputProps = { + recordFieldInputdId: string; + label: string; + value: string; + onChange: (value: string) => void; + isReadOnly?: boolean; +}; + +export const FormFieldInput = ({ + recordFieldInputdId, + label, + onChange, + value, +}: FormFieldInputProps) => { + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeBase.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeBase.tsx index 04c6a62be9..cccb23a12a 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeBase.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeBase.tsx @@ -3,7 +3,13 @@ import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram'; import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconCode, IconHandMove, IconMail, IconPlaylistAdd } from 'twenty-ui'; +import { + IconAddressBook, + IconCode, + IconHandMove, + IconMail, + IconPlaylistAdd, +} from 'twenty-ui'; const StyledStepNodeLabelIconContainer = styled.div` align-items: center; @@ -70,6 +76,21 @@ export const WorkflowDiagramStepNodeBase = ({ ); } + case 'RECORD_CRUD.CREATE': { + return ( + + + + ); + } + case 'RECORD_CRUD.DELETE': + case 'RECORD_CRUD.UPDATE': { + return null; + } } } } diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormRecordCreate.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormRecordCreate.tsx new file mode 100644 index 0000000000..647dfe240a --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormRecordCreate.tsx @@ -0,0 +1,167 @@ +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; +import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput'; +import { Select, SelectOption } from '@/ui/input/components/Select'; +import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase'; +import { WorkflowRecordCreateAction } from '@/workflow/types/Workflow'; +import { useTheme } from '@emotion/react'; +import { useEffect, useState } from 'react'; +import { + HorizontalSeparator, + IconAddressBook, + isDefined, + useIcons, +} from 'twenty-ui'; +import { useDebouncedCallback } from 'use-debounce'; +import { FieldMetadataType } from '~/generated/graphql'; + +type WorkflowEditActionFormRecordCreateProps = { + action: WorkflowRecordCreateAction; + actionOptions: + | { + readonly: true; + } + | { + readonly?: false; + onActionUpdate: (action: WorkflowRecordCreateAction) => void; + }; +}; + +type SendEmailFormData = { + objectName: string; + [field: string]: unknown; +}; + +export const WorkflowEditActionFormRecordCreate = ({ + action, + actionOptions, +}: WorkflowEditActionFormRecordCreateProps) => { + const theme = useTheme(); + const { getIcon } = useIcons(); + + const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); + + const availableMetadata: Array> = + activeObjectMetadataItems.map((item) => ({ + Icon: getIcon(item.icon), + label: item.labelPlural, + value: item.nameSingular, + })); + + const [formData, setFormData] = useState({ + objectName: action.settings.input.objectName, + ...action.settings.input.objectRecord, + }); + const isFormDisabled = actionOptions.readonly; + + const handleFieldChange = ( + fieldName: keyof SendEmailFormData, + updatedValue: string, + ) => { + const newFormData: SendEmailFormData = { + ...formData, + [fieldName]: updatedValue, + }; + + setFormData(newFormData); + + saveAction(newFormData); + }; + + useEffect(() => { + setFormData({ + objectName: action.settings.input.objectName, + ...action.settings.input.objectRecord, + }); + }, [action.settings.input]); + + const selectedObjectMetadataItemNameSingular = formData.objectName; + + const selectedObjectMetadataItem = activeObjectMetadataItems.find( + (item) => item.nameSingular === selectedObjectMetadataItemNameSingular, + ); + if (!isDefined(selectedObjectMetadataItem)) { + throw new Error('Should have found the metadata item'); + } + + const editableFields = selectedObjectMetadataItem.fields.filter( + (field) => + field.type !== FieldMetadataType.Relation && + !field.isSystem && + field.isActive, + ); + + const saveAction = useDebouncedCallback( + async (formData: SendEmailFormData) => { + if (actionOptions.readonly === true) { + return; + } + + const { objectName: updatedObjectName, ...updatedOtherFields } = formData; + + actionOptions.onActionUpdate({ + ...action, + settings: { + ...action.settings, + input: { + type: 'CREATE', + objectName: updatedObjectName, + objectRecord: updatedOtherFields, + }, + }, + }); + }, + 1_000, + ); + + useEffect(() => { + return () => { + saveAction.flush(); + }; + }, [saveAction]); + + return ( + + } + headerTitle="Record Create" + headerType="Action" + > +