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({
|
baseWorkflowActionSettingsSchema.extend({
|
||||||
input: z.array(
|
input: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
label: z.string(),
|
label: z.string(),
|
||||||
name: z.string(),
|
|
||||||
type: z.nativeEnum(FieldMetadataType),
|
type: z.nativeEnum(FieldMetadataType),
|
||||||
placeholder: z.string().optional(),
|
placeholder: z.string().optional(),
|
||||||
settings: z.record(z.any()),
|
settings: z.record(z.any()),
|
||||||
|
|||||||
@ -9,8 +9,16 @@ import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/
|
|||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { useState } from 'react';
|
||||||
import { IconChevronDown, IconPlus, useIcons } from 'twenty-ui';
|
import { FieldMetadataType, isDefined } from 'twenty-shared';
|
||||||
|
import {
|
||||||
|
IconChevronDown,
|
||||||
|
IconChevronUp,
|
||||||
|
IconPlus,
|
||||||
|
IconTrash,
|
||||||
|
useIcons,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
type WorkflowEditActionFormProps = {
|
type WorkflowEditActionFormProps = {
|
||||||
action: WorkflowFormAction;
|
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;
|
align-items: center;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
@ -47,6 +61,21 @@ const StyledPlaceholder = styled.div`
|
|||||||
width: 100%;
|
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`
|
const StyledAddFieldContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
color: ${({ theme }) => theme.font.color.secondary};
|
color: ${({ theme }) => theme.font.color.secondary};
|
||||||
@ -65,6 +94,19 @@ export const WorkflowEditActionForm = ({
|
|||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const headerTitle = isDefined(action.name) ? action.name : `Form`;
|
const headerTitle = isDefined(action.name) ? action.name : `Form`;
|
||||||
const headerIcon = getActionIcon(action.type);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -87,35 +129,94 @@ export const WorkflowEditActionForm = ({
|
|||||||
/>
|
/>
|
||||||
<WorkflowStepBody>
|
<WorkflowStepBody>
|
||||||
{action.settings.input.map((field) => (
|
{action.settings.input.map((field) => (
|
||||||
<FormFieldInputContainer key={field.name}>
|
<FormFieldInputContainer key={field.id}>
|
||||||
{field.label ? <InputLabel>{field.label}</InputLabel> : null}
|
{field.label ? <InputLabel>{field.label}</InputLabel> : null}
|
||||||
|
|
||||||
<FormFieldInputRowContainer>
|
<StyledRowContainer>
|
||||||
<FormFieldInputInputContainer hasRightElement={false}>
|
<FormFieldInputRowContainer>
|
||||||
<StyledContainer onClick={() => {}}>
|
<FormFieldInputInputContainer
|
||||||
<StyledPlaceholder>{field.placeholder}</StyledPlaceholder>
|
hasRightElement={false}
|
||||||
<IconChevronDown
|
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}
|
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>
|
</StyledIconButtonContainer>
|
||||||
</FormFieldInputInputContainer>
|
)}
|
||||||
</FormFieldInputRowContainer>
|
</StyledRowContainer>
|
||||||
</FormFieldInputContainer>
|
</FormFieldInputContainer>
|
||||||
))}
|
))}
|
||||||
{!actionOptions.readonly && (
|
{!actionOptions.readonly && (
|
||||||
<FormFieldInputContainer>
|
<StyledRowContainer>
|
||||||
<FormFieldInputRowContainer>
|
<FormFieldInputContainer>
|
||||||
<FormFieldInputInputContainer hasRightElement={false}>
|
<FormFieldInputRowContainer>
|
||||||
<StyledContainer onClick={() => {}}>
|
<FormFieldInputInputContainer
|
||||||
<StyledAddFieldContainer>
|
hasRightElement={false}
|
||||||
<IconPlus size={theme.icon.size.sm} />
|
onClick={() => {
|
||||||
{t`Add Field`}
|
actionOptions.onActionUpdate({
|
||||||
</StyledAddFieldContainer>
|
...action,
|
||||||
</StyledContainer>
|
settings: {
|
||||||
</FormFieldInputInputContainer>
|
...action.settings,
|
||||||
</FormFieldInputRowContainer>
|
input: [
|
||||||
</FormFieldInputContainer>
|
...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>
|
</WorkflowStepBody>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -17,14 +17,14 @@ const DEFAULT_ACTION = {
|
|||||||
settings: {
|
settings: {
|
||||||
input: [
|
input: [
|
||||||
{
|
{
|
||||||
name: 'company',
|
id: 'ed00b897-519f-44cd-8201-a6502a3a9dc8',
|
||||||
type: FieldMetadataType.TEXT,
|
type: FieldMetadataType.TEXT,
|
||||||
label: 'Company',
|
label: 'Company',
|
||||||
placeholder: 'Select a company',
|
placeholder: 'Select a company',
|
||||||
settings: {},
|
settings: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'number',
|
id: 'ed00b897-519f-44cd-8201-a6502a3a9dc9',
|
||||||
type: FieldMetadataType.NUMBER,
|
type: FieldMetadataType.NUMBER,
|
||||||
label: 'Number',
|
label: 'Number',
|
||||||
placeholder: '1000',
|
placeholder: '1000',
|
||||||
|
|||||||
@ -6,17 +6,17 @@ describe('generateFakeFormResponse', () => {
|
|||||||
it('should generate fake responses for a form schema', () => {
|
it('should generate fake responses for a form schema', () => {
|
||||||
const schema = [
|
const schema = [
|
||||||
{
|
{
|
||||||
name: 'name',
|
id: '96939213-49ac-4dee-949d-56e6c7be98e6',
|
||||||
type: FieldMetadataType.TEXT,
|
type: FieldMetadataType.TEXT,
|
||||||
label: 'Name',
|
label: 'Name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'age',
|
id: '96939213-49ac-4dee-949d-56e6c7be98e7',
|
||||||
type: FieldMetadataType.NUMBER,
|
type: FieldMetadataType.NUMBER,
|
||||||
label: 'Age',
|
label: 'Age',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'email',
|
id: '96939213-49ac-4dee-949d-56e6c7be98e8',
|
||||||
type: FieldMetadataType.EMAILS,
|
type: FieldMetadataType.EMAILS,
|
||||||
label: 'Email',
|
label: 'Email',
|
||||||
},
|
},
|
||||||
@ -25,7 +25,7 @@ describe('generateFakeFormResponse', () => {
|
|||||||
const result = generateFakeFormResponse(schema);
|
const result = generateFakeFormResponse(schema);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
email: {
|
'96939213-49ac-4dee-949d-56e6c7be98e8': {
|
||||||
isLeaf: false,
|
isLeaf: false,
|
||||||
label: 'Email',
|
label: 'Email',
|
||||||
value: {
|
value: {
|
||||||
@ -44,14 +44,14 @@ describe('generateFakeFormResponse', () => {
|
|||||||
},
|
},
|
||||||
icon: undefined,
|
icon: undefined,
|
||||||
},
|
},
|
||||||
name: {
|
'96939213-49ac-4dee-949d-56e6c7be98e6': {
|
||||||
isLeaf: true,
|
isLeaf: true,
|
||||||
label: 'Name',
|
label: 'Name',
|
||||||
type: FieldMetadataType.TEXT,
|
type: FieldMetadataType.TEXT,
|
||||||
value: 'My text',
|
value: 'My text',
|
||||||
icon: undefined,
|
icon: undefined,
|
||||||
},
|
},
|
||||||
age: {
|
'96939213-49ac-4dee-949d-56e6c7be98e7': {
|
||||||
isLeaf: true,
|
isLeaf: true,
|
||||||
label: 'Age',
|
label: 'Age',
|
||||||
type: FieldMetadataType.NUMBER,
|
type: FieldMetadataType.NUMBER,
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export const generateFakeFormResponse = (
|
|||||||
formMetadata: FormFieldMetadata[],
|
formMetadata: FormFieldMetadata[],
|
||||||
): Record<string, Leaf | Node> => {
|
): Record<string, Leaf | Node> => {
|
||||||
return formMetadata.reduce((acc, formFieldMetadata) => {
|
return formMetadata.reduce((acc, formFieldMetadata) => {
|
||||||
acc[formFieldMetadata.name] = generateFakeField({
|
acc[formFieldMetadata.id] = generateFakeField({
|
||||||
type: formFieldMetadata.type,
|
type: formFieldMetadata.type,
|
||||||
label: formFieldMetadata.label,
|
label: formFieldMetadata.label,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -505,14 +505,14 @@ export class WorkflowVersionStepWorkspaceService {
|
|||||||
...BASE_STEP_DEFINITION,
|
...BASE_STEP_DEFINITION,
|
||||||
input: [
|
input: [
|
||||||
{
|
{
|
||||||
|
id: v4(),
|
||||||
label: 'Company',
|
label: 'Company',
|
||||||
name: 'company',
|
|
||||||
placeholder: 'Select a company',
|
placeholder: 'Select a company',
|
||||||
type: FieldMetadataType.TEXT,
|
type: FieldMetadataType.TEXT,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: v4(),
|
||||||
label: 'Number',
|
label: 'Number',
|
||||||
name: 'number',
|
|
||||||
placeholder: '1000',
|
placeholder: '1000',
|
||||||
type: FieldMetadataType.NUMBER,
|
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';
|
import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
|
||||||
|
|
||||||
export type FormFieldMetadata = {
|
export type FormFieldMetadata = {
|
||||||
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
name: string;
|
|
||||||
type: FieldMetadataType;
|
type: FieldMetadataType;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
settings?: Record<string, any>;
|
settings?: Record<string, any>;
|
||||||
|
|||||||
Reference in New Issue
Block a user