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:
@ -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()),
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user