Add form select field (#14538)
- create a WorkflowFormFieldInput component that takes a fieldMetadataId as input and display a form field input - add a select field for forms https://github.com/user-attachments/assets/42f8ce98-c17a-4809-b1be-032e3066a596
This commit is contained in:
+12
-6
@@ -13,7 +13,6 @@ import { useTheme } from '@emotion/react';
|
||||
import { useId, useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconCircleOff } from 'twenty-ui/display';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
|
||||
type FormSelectFieldInputProps = {
|
||||
@@ -92,12 +91,19 @@ export const FormSelectFieldInput = ({
|
||||
(option) => option.value === draftValue.value,
|
||||
);
|
||||
|
||||
const emptyOption = {
|
||||
label: `No ${label}`,
|
||||
const defaultEmptyOption = {
|
||||
label: `No ${label ?? 'value'}`,
|
||||
value: '',
|
||||
Icon: IconCircleOff,
|
||||
};
|
||||
|
||||
const existingEmptyOption = options.find(
|
||||
(option) => !isDefined(option.value) || option.value === '',
|
||||
);
|
||||
|
||||
const optionsWithEmptyOption = isDefined(existingEmptyOption)
|
||||
? options
|
||||
: [defaultEmptyOption, ...options];
|
||||
|
||||
const handleUnlinkVariable = () => {
|
||||
setDraftValue({
|
||||
type: 'static',
|
||||
@@ -132,10 +138,10 @@ export const FormSelectFieldInput = ({
|
||||
{draftValue.type === 'static' ? (
|
||||
<Select
|
||||
dropdownId={`${instanceId}-select-display`}
|
||||
options={options}
|
||||
options={optionsWithEmptyOption}
|
||||
value={selectedOption?.value}
|
||||
onChange={onSelect}
|
||||
emptyOption={emptyOption}
|
||||
emptyOption={existingEmptyOption ?? defaultEmptyOption}
|
||||
fullWidth
|
||||
hasRightElement={isDefined(VariablePicker) && !readonly}
|
||||
withSearchInput
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
||||
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
|
||||
import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput';
|
||||
import { type VariablePickerComponent } from '@/object-record/record-field/ui/form-types/types/VariablePickerComponent';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
|
||||
type WorkflowFormFieldInputProps = {
|
||||
fieldMetadataId: string;
|
||||
defaultValue: JsonValue;
|
||||
readonly: boolean;
|
||||
onChange: (value: JsonValue) => void;
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
};
|
||||
|
||||
export const WorkflowFormFieldInput = ({
|
||||
fieldMetadataId,
|
||||
defaultValue,
|
||||
readonly,
|
||||
onChange,
|
||||
VariablePicker,
|
||||
}: WorkflowFormFieldInputProps) => {
|
||||
const { fieldMetadataItem, objectMetadataItem } =
|
||||
useFieldMetadataItemById(fieldMetadataId);
|
||||
|
||||
if (!isDefined(fieldMetadataItem) || !isDefined(objectMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fieldDefinition = formatFieldMetadataItemAsFieldDefinition({
|
||||
field: fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
return (
|
||||
<FormFieldInput
|
||||
key={fieldDefinition.metadata.fieldName}
|
||||
defaultValue={defaultValue}
|
||||
field={fieldDefinition}
|
||||
onChange={onChange}
|
||||
VariablePicker={VariablePicker}
|
||||
readonly={readonly}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+24
@@ -9,6 +9,7 @@ import { type WorkflowFormAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { useUpdateWorkflowRunStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowRunStep';
|
||||
import { WorkflowFormFieldInput } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowFormFieldInput';
|
||||
import { useSubmitFormStep } from '@/workflow/workflow-steps/workflow-actions/form-action/hooks/useSubmitFormStep';
|
||||
import { type WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
|
||||
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
|
||||
@@ -142,6 +143,29 @@ export const WorkflowEditActionFormFiller = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === 'SELECT') {
|
||||
const selectedFieldId = field.settings?.selectedFieldId;
|
||||
|
||||
if (!isDefined(selectedFieldId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkflowFormFieldInput
|
||||
key={field.id}
|
||||
fieldMetadataId={selectedFieldId}
|
||||
defaultValue={field.value}
|
||||
readonly={actionOptions.readonly}
|
||||
onChange={(value) => {
|
||||
onFieldUpdate({
|
||||
fieldId: field.id,
|
||||
value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormFieldInput
|
||||
key={field.id}
|
||||
|
||||
+5
@@ -1,5 +1,6 @@
|
||||
import { WorkflowFormFieldSettingsDate } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormFieldSettingsDate';
|
||||
import { WorkflowFormFieldSettingsRecordPicker } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormFieldSettingsRecordPicker';
|
||||
import { WorkflowFormFieldSettingsSelect } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormFieldSettingsSelect';
|
||||
import { type WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
@@ -26,6 +27,10 @@ export const WorkflowFormFieldSettingsByType = ({
|
||||
return (
|
||||
<WorkflowFormFieldSettingsDate field={field} onChange={onChange} />
|
||||
);
|
||||
case FieldMetadataType.SELECT:
|
||||
return (
|
||||
<WorkflowFormFieldSettingsSelect field={field} onChange={onChange} />
|
||||
);
|
||||
case 'RECORD':
|
||||
return (
|
||||
<WorkflowFormFieldSettingsRecordPicker
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { FormFieldInputContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputContainer';
|
||||
import { FormSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormSelectFieldInput';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { type WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
|
||||
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
|
||||
import styled from '@emotion/styled';
|
||||
import camelCase from 'lodash.camelcase';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
type WorkflowFormFieldSettingsSelectProps = {
|
||||
field: WorkflowFormActionField;
|
||||
onChange: (updatedField: WorkflowFormActionField) => void;
|
||||
};
|
||||
|
||||
const StyledFormFieldSettingsSelect = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledRowContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const WorkflowFormFieldSettingsSelect = ({
|
||||
field,
|
||||
onChange,
|
||||
}: WorkflowFormFieldSettingsSelectProps) => {
|
||||
const selectTypeOptions = [
|
||||
{
|
||||
label: 'Existing Field',
|
||||
value: 'EXISTING_FIELD',
|
||||
},
|
||||
];
|
||||
|
||||
const { activeNonSystemObjectMetadataItems } =
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
const selectFieldId = field.settings?.selectedFieldId;
|
||||
|
||||
const fieldOptions = activeNonSystemObjectMetadataItems.reduce(
|
||||
(acc, objectMetadataItem) => {
|
||||
return acc.concat(
|
||||
objectMetadataItem.fields
|
||||
.filter(
|
||||
(field) =>
|
||||
field.isActive &&
|
||||
!field.isSystem &&
|
||||
field.type === FieldMetadataType.SELECT,
|
||||
)
|
||||
.map((field) => ({
|
||||
label: `${objectMetadataItem.labelSingular} > ${field.label}`,
|
||||
value: field.id,
|
||||
})),
|
||||
);
|
||||
},
|
||||
[] as { label: string; value: string }[],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledFormFieldSettingsSelect>
|
||||
<StyledRowContainer>
|
||||
<FormFieldInputContainer>
|
||||
<InputLabel>Label</InputLabel>
|
||||
<FormTextFieldInput
|
||||
onChange={(newLabel: string) => {
|
||||
onChange({
|
||||
...field,
|
||||
label: newLabel,
|
||||
name: camelCase(newLabel),
|
||||
});
|
||||
}}
|
||||
defaultValue={field.label}
|
||||
placeholder={
|
||||
getDefaultFormFieldSettings(FieldMetadataType.SELECT).label
|
||||
}
|
||||
/>
|
||||
</FormFieldInputContainer>
|
||||
<FormFieldInputContainer>
|
||||
<InputLabel>Select Type</InputLabel>
|
||||
<FormSelectFieldInput
|
||||
onChange={(newSelectType: string | null) => {
|
||||
if (newSelectType === null) {
|
||||
return;
|
||||
}
|
||||
onChange({
|
||||
...field,
|
||||
settings: {
|
||||
...field.settings,
|
||||
selectType: newSelectType,
|
||||
},
|
||||
});
|
||||
}}
|
||||
defaultValue={'EXISTING_FIELD'}
|
||||
options={selectTypeOptions}
|
||||
readonly
|
||||
/>
|
||||
</FormFieldInputContainer>
|
||||
</StyledRowContainer>
|
||||
<FormFieldInputContainer>
|
||||
<InputLabel>Field</InputLabel>
|
||||
<FormSelectFieldInput
|
||||
onChange={(newSelectFieldId: string | null) => {
|
||||
if (newSelectFieldId === null) {
|
||||
return;
|
||||
}
|
||||
onChange({
|
||||
...field,
|
||||
settings: {
|
||||
...field.settings,
|
||||
selectedFieldId: newSelectFieldId,
|
||||
},
|
||||
});
|
||||
}}
|
||||
defaultValue={selectFieldId}
|
||||
options={fieldOptions}
|
||||
/>
|
||||
</FormFieldInputContainer>
|
||||
</StyledFormFieldSettingsSelect>
|
||||
);
|
||||
};
|
||||
+28
@@ -145,3 +145,31 @@ export const DateFieldSettings: Story = {
|
||||
expect(args.onClose).toHaveBeenCalled();
|
||||
},
|
||||
};
|
||||
|
||||
export const SelectFieldSettings: Story = {
|
||||
args: {
|
||||
field: {
|
||||
id: 'field-5',
|
||||
name: 'select',
|
||||
label: 'Select Field',
|
||||
type: FieldMetadataType.SELECT,
|
||||
settings: {
|
||||
selectedFieldId: 'field-1',
|
||||
},
|
||||
},
|
||||
onClose: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const fieldTypeSelect = await canvas.findByText('Select');
|
||||
expect(fieldTypeSelect).toBeVisible();
|
||||
|
||||
const selectTypeSelect = await canvas.findByText('Select Type');
|
||||
expect(selectTypeSelect).toBeVisible();
|
||||
|
||||
const closeButton = await canvas.findByTestId('close-button');
|
||||
await userEvent.click(closeButton);
|
||||
expect(args.onClose).toHaveBeenCalled();
|
||||
},
|
||||
};
|
||||
|
||||
+6
@@ -5,6 +5,7 @@ import {
|
||||
IllustrationIconCalendarEvent,
|
||||
IllustrationIconNumbers,
|
||||
IllustrationIconOneToMany,
|
||||
IllustrationIconTag,
|
||||
IllustrationIconText,
|
||||
} from 'twenty-ui/display';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
@@ -31,4 +32,9 @@ export const FORM_SELECT_FIELD_TYPE_OPTIONS: SelectOption<WorkflowFormFieldType>
|
||||
value: 'RECORD',
|
||||
Icon: IllustrationIconOneToMany,
|
||||
},
|
||||
{
|
||||
label: 'Select',
|
||||
value: FieldMetadataType.SELECT,
|
||||
Icon: IllustrationIconTag,
|
||||
},
|
||||
];
|
||||
|
||||
+1
@@ -4,4 +4,5 @@ export type WorkflowFormFieldType =
|
||||
| FieldMetadataType.TEXT
|
||||
| FieldMetadataType.NUMBER
|
||||
| FieldMetadataType.DATE
|
||||
| FieldMetadataType.SELECT
|
||||
| 'RECORD';
|
||||
|
||||
+11
@@ -36,6 +36,17 @@ export const getDefaultFormFieldSettings = (type: WorkflowFormFieldType) => {
|
||||
objectName: 'company',
|
||||
},
|
||||
};
|
||||
case FieldMetadataType.SELECT:
|
||||
return {
|
||||
id: v4(),
|
||||
name: 'select',
|
||||
label: 'Select',
|
||||
placeholder: 'Choose a value',
|
||||
settings: {
|
||||
selectType: 'EXISTING_FIELD',
|
||||
selectedFieldId: undefined,
|
||||
},
|
||||
};
|
||||
default:
|
||||
assertUnreachable(type);
|
||||
}
|
||||
|
||||
@@ -158,6 +158,7 @@ export const workflowFormActionSettingsSchema =
|
||||
z.literal(FieldMetadataType.TEXT),
|
||||
z.literal(FieldMetadataType.NUMBER),
|
||||
z.literal(FieldMetadataType.DATE),
|
||||
z.literal(FieldMetadataType.SELECT),
|
||||
z.literal('RECORD'),
|
||||
]),
|
||||
placeholder: z.string().optional(),
|
||||
|
||||
Reference in New Issue
Block a user