Allow to add and delete fields (#10990)

- Allow to add a new field
- On field click, display a delete button
- Use id instead of names for fields



https://github.com/user-attachments/assets/4ebffe22-225a-4bae-aa49-99e66170181a
This commit is contained in:
Thomas Trompette
2025-03-18 17:24:52 +01:00
committed by GitHub
parent 6d517360d1
commit 0ce91d84c1
7 changed files with 139 additions and 38 deletions

View File

@ -86,8 +86,8 @@ export const workflowFormActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.array(
z.object({
id: z.string().uuid(),
label: z.string(),
name: z.string(),
type: z.nativeEnum(FieldMetadataType),
placeholder: z.string().optional(),
settings: z.record(z.any()),

View File

@ -9,8 +9,16 @@ import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared';
import { IconChevronDown, IconPlus, useIcons } from 'twenty-ui';
import { useState } from 'react';
import { FieldMetadataType, isDefined } from 'twenty-shared';
import {
IconChevronDown,
IconChevronUp,
IconPlus,
IconTrash,
useIcons,
} from 'twenty-ui';
import { v4 } from 'uuid';
type WorkflowEditActionFormProps = {
action: WorkflowFormAction;
@ -24,7 +32,13 @@ type WorkflowEditActionFormProps = {
};
};
const StyledContainer = styled.div`
const StyledRowContainer = styled.div`
column-gap: ${({ theme }) => theme.spacing(1)};
display: grid;
grid-template-columns: 1fr 16px;
`;
const StyledFieldContainer = styled.div`
align-items: center;
background: transparent;
border: none;
@ -47,6 +61,21 @@ const StyledPlaceholder = styled.div`
width: 100%;
`;
const StyledIconButtonContainer = styled.div`
align-items: center;
border-radius: ${({ theme }) => theme.border.radius.md};
display: flex;
justify-content: center;
width: 24px;
cursor: pointer;
&:hover,
&[data-open='true'] {
background-color: ${({ theme }) => theme.background.transparent.lighter};
}
`;
const StyledAddFieldContainer = styled.div`
display: flex;
color: ${({ theme }) => theme.font.color.secondary};
@ -65,6 +94,19 @@ export const WorkflowEditActionForm = ({
const { t } = useLingui();
const headerTitle = isDefined(action.name) ? action.name : `Form`;
const headerIcon = getActionIcon(action.type);
const [selectedField, setSelectedField] = useState<string | null>(null);
const isFieldSelected = (fieldName: string) => selectedField === fieldName;
const handleFieldClick = (fieldName: string) => {
if (actionOptions.readonly === true) {
return;
}
if (isFieldSelected(fieldName)) {
setSelectedField(null);
} else {
setSelectedField(fieldName);
}
};
return (
<>
@ -87,35 +129,94 @@ export const WorkflowEditActionForm = ({
/>
<WorkflowStepBody>
{action.settings.input.map((field) => (
<FormFieldInputContainer key={field.name}>
<FormFieldInputContainer key={field.id}>
{field.label ? <InputLabel>{field.label}</InputLabel> : null}
<FormFieldInputRowContainer>
<FormFieldInputInputContainer hasRightElement={false}>
<StyledContainer onClick={() => {}}>
<StyledPlaceholder>{field.placeholder}</StyledPlaceholder>
<IconChevronDown
<StyledRowContainer>
<FormFieldInputRowContainer>
<FormFieldInputInputContainer
hasRightElement={false}
onClick={() => {
handleFieldClick(field.id);
}}
>
<StyledFieldContainer>
<StyledPlaceholder>{field.placeholder}</StyledPlaceholder>
{isFieldSelected(field.id) ? (
<IconChevronUp
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
) : (
<IconChevronDown
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
)}
</StyledFieldContainer>
</FormFieldInputInputContainer>
</FormFieldInputRowContainer>
{isFieldSelected(field.id) && (
<StyledIconButtonContainer>
<IconTrash
size={theme.icon.size.md}
color={theme.font.color.tertiary}
color={theme.font.color.secondary}
onClick={() => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate({
...action,
settings: {
...action.settings,
input: action.settings.input.filter(
(f) => f.id !== field.id,
),
},
});
}}
/>
</StyledContainer>
</FormFieldInputInputContainer>
</FormFieldInputRowContainer>
</StyledIconButtonContainer>
)}
</StyledRowContainer>
</FormFieldInputContainer>
))}
{!actionOptions.readonly && (
<FormFieldInputContainer>
<FormFieldInputRowContainer>
<FormFieldInputInputContainer hasRightElement={false}>
<StyledContainer onClick={() => {}}>
<StyledAddFieldContainer>
<IconPlus size={theme.icon.size.sm} />
{t`Add Field`}
</StyledAddFieldContainer>
</StyledContainer>
</FormFieldInputInputContainer>
</FormFieldInputRowContainer>
</FormFieldInputContainer>
<StyledRowContainer>
<FormFieldInputContainer>
<FormFieldInputRowContainer>
<FormFieldInputInputContainer
hasRightElement={false}
onClick={() => {
actionOptions.onActionUpdate({
...action,
settings: {
...action.settings,
input: [
...action.settings.input,
{
id: v4(),
type: FieldMetadataType.TEXT,
label: 'New Field',
placeholder: 'New Field',
settings: {},
},
],
},
});
}}
>
<StyledFieldContainer>
<StyledAddFieldContainer>
<IconPlus size={theme.icon.size.sm} />
{t`Add Field`}
</StyledAddFieldContainer>
</StyledFieldContainer>
</FormFieldInputInputContainer>
</FormFieldInputRowContainer>
</FormFieldInputContainer>
</StyledRowContainer>
)}
</WorkflowStepBody>
</>

View File

@ -17,14 +17,14 @@ const DEFAULT_ACTION = {
settings: {
input: [
{
name: 'company',
id: 'ed00b897-519f-44cd-8201-a6502a3a9dc8',
type: FieldMetadataType.TEXT,
label: 'Company',
placeholder: 'Select a company',
settings: {},
},
{
name: 'number',
id: 'ed00b897-519f-44cd-8201-a6502a3a9dc9',
type: FieldMetadataType.NUMBER,
label: 'Number',
placeholder: '1000',

View File

@ -6,17 +6,17 @@ describe('generateFakeFormResponse', () => {
it('should generate fake responses for a form schema', () => {
const schema = [
{
name: 'name',
id: '96939213-49ac-4dee-949d-56e6c7be98e6',
type: FieldMetadataType.TEXT,
label: 'Name',
},
{
name: 'age',
id: '96939213-49ac-4dee-949d-56e6c7be98e7',
type: FieldMetadataType.NUMBER,
label: 'Age',
},
{
name: 'email',
id: '96939213-49ac-4dee-949d-56e6c7be98e8',
type: FieldMetadataType.EMAILS,
label: 'Email',
},
@ -25,7 +25,7 @@ describe('generateFakeFormResponse', () => {
const result = generateFakeFormResponse(schema);
expect(result).toEqual({
email: {
'96939213-49ac-4dee-949d-56e6c7be98e8': {
isLeaf: false,
label: 'Email',
value: {
@ -44,14 +44,14 @@ describe('generateFakeFormResponse', () => {
},
icon: undefined,
},
name: {
'96939213-49ac-4dee-949d-56e6c7be98e6': {
isLeaf: true,
label: 'Name',
type: FieldMetadataType.TEXT,
value: 'My text',
icon: undefined,
},
age: {
'96939213-49ac-4dee-949d-56e6c7be98e7': {
isLeaf: true,
label: 'Age',
type: FieldMetadataType.NUMBER,

View File

@ -9,7 +9,7 @@ export const generateFakeFormResponse = (
formMetadata: FormFieldMetadata[],
): Record<string, Leaf | Node> => {
return formMetadata.reduce((acc, formFieldMetadata) => {
acc[formFieldMetadata.name] = generateFakeField({
acc[formFieldMetadata.id] = generateFakeField({
type: formFieldMetadata.type,
label: formFieldMetadata.label,
});

View File

@ -505,14 +505,14 @@ export class WorkflowVersionStepWorkspaceService {
...BASE_STEP_DEFINITION,
input: [
{
id: v4(),
label: 'Company',
name: 'company',
placeholder: 'Select a company',
type: FieldMetadataType.TEXT,
},
{
id: v4(),
label: 'Number',
name: 'number',
placeholder: '1000',
type: FieldMetadataType.NUMBER,
},

View File

@ -3,8 +3,8 @@ import { FieldMetadataType } from 'twenty-shared';
import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
export type FormFieldMetadata = {
id: string;
label: string;
name: string;
type: FieldMetadataType;
placeholder?: string;
settings?: Record<string, any>;