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 index ba0aa8195..5a99fd7d3 100644 --- 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 @@ -1,15 +1,18 @@ -import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FormBooleanFieldInput } from '@/object-record/record-field/form-types/components/FormBooleanFieldInput'; import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput'; +import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput'; import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput'; import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent'; +import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; +import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean'; import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber'; +import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText'; import { JsonValue } from 'type-fest'; type FormFieldInputProps = { - field: FieldMetadataItem; + field: FieldDefinition; defaultValue: JsonValue; onPersist: (value: JsonValue) => void; VariablePicker?: VariablePickerComponent; @@ -23,7 +26,6 @@ export const FormFieldInput = ({ }: FormFieldInputProps) => { return isFieldNumber(field) ? ( ) : isFieldBoolean(field) ? ( + ) : isFieldSelect(field) ? ( + ) : null; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSelectFieldInput.tsx new file mode 100644 index 000000000..1be174704 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSelectFieldInput.tsx @@ -0,0 +1,264 @@ +import { StyledFormFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputContainer'; +import { StyledFormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputInputContainer'; +import { StyledFormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputRowContainer'; +import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip'; +import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent'; +import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; +import { FieldSelectMetadata } from '@/object-record/record-field/types/FieldMetadata'; +import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope'; +import { SINGLE_RECORD_SELECT_BASE_LIST } from '@/object-record/relation-picker/constants/SingleRecordSelectBaseList'; +import { SelectOption } from '@/spreadsheet-import/types'; +import { SelectDisplay } from '@/ui/field/display/components/SelectDisplay'; +import { SelectInput } from '@/ui/field/input/components/SelectInput'; +import { InputLabel } from '@/ui/input/components/InputLabel'; +import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; +import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString'; +import styled from '@emotion/styled'; +import { useId, useState } from 'react'; +import { Key } from 'ts-key-enum'; +import { isDefined, VisibilityHidden } from 'twenty-ui'; + +type FormSelectFieldInputProps = { + field: FieldDefinition; + label?: string; + defaultValue: string | undefined; + onPersist: (value: number | null | string) => void; + VariablePicker?: VariablePickerComponent; +}; + +const StyledDisplayModeContainer = styled.button` + width: 100%; + align-items: center; + display: flex; + cursor: pointer; + border: none; + background: transparent; + font-family: inherit; + padding-inline: ${({ theme }) => theme.spacing(2)}; + + &:hover, + &[data-open='true'] { + background-color: ${({ theme }) => theme.background.transparent.lighter}; + } +`; + +export const FormSelectFieldInput = ({ + label, + field, + defaultValue, + onPersist, + VariablePicker, +}: FormSelectFieldInputProps) => { + const inputId = useId(); + + const hotkeyScope = InlineCellHotkeyScope.InlineCell; + + const { + setHotkeyScopeAndMemorizePreviousScope, + goBackToPreviousHotkeyScope, + } = usePreviousHotkeyScope(); + + const [draftValue, setDraftValue] = useState< + | { + type: 'static'; + value: string; + editingMode: 'view' | 'edit'; + } + | { + type: 'variable'; + value: string; + } + >( + isStandaloneVariableString(defaultValue) + ? { + type: 'variable', + value: defaultValue, + } + : { + type: 'static', + value: isDefined(defaultValue) ? String(defaultValue) : '', + editingMode: 'view', + }, + ); + + const onSubmit = (option: string) => { + setDraftValue({ + type: 'static', + value: option, + editingMode: 'view', + }); + + goBackToPreviousHotkeyScope(); + + onPersist(option); + }; + + const onCancel = () => { + if (draftValue.type !== 'static') { + throw new Error('Can only be called when editing a static value'); + } + + setDraftValue({ + ...draftValue, + editingMode: 'view', + }); + + goBackToPreviousHotkeyScope(); + }; + + const [selectWrapperRef, setSelectWrapperRef] = + useState(null); + + const [filteredOptions, setFilteredOptions] = useState([]); + + const { resetSelectedItem } = useSelectableList( + SINGLE_RECORD_SELECT_BASE_LIST, + ); + + const clearField = () => { + setDraftValue({ + type: 'static', + editingMode: 'view', + value: '', + }); + + onPersist(null); + }; + + const selectedOption = field.metadata.options.find( + (option) => option.value === draftValue.value, + ); + + const handleClearField = () => { + clearField(); + + goBackToPreviousHotkeyScope(); + }; + + const handleSubmit = (option: SelectOption) => { + onSubmit(option.value); + + resetSelectedItem(); + }; + + const handleUnlinkVariable = () => { + setDraftValue({ + type: 'static', + value: '', + editingMode: 'view', + }); + + onPersist(null); + }; + + const handleVariableTagInsert = (variableName: string) => { + setDraftValue({ + type: 'variable', + value: variableName, + }); + + onPersist(variableName); + }; + + const handleDisplayModeClick = () => { + if (draftValue.type !== 'static') { + throw new Error( + 'This function can only be called when editing a static value.', + ); + } + + setDraftValue({ + ...draftValue, + editingMode: 'edit', + }); + + setHotkeyScopeAndMemorizePreviousScope(hotkeyScope); + }; + + const handleSelectEnter = (itemId: string) => { + const option = filteredOptions.find((option) => option.value === itemId); + if (isDefined(option)) { + onSubmit(option.value); + resetSelectedItem(); + } + }; + + useScopedHotkeys( + Key.Escape, + () => { + onCancel(); + resetSelectedItem(); + }, + hotkeyScope, + [onCancel, resetSelectedItem], + ); + + const optionIds = [ + `No ${field.label}`, + ...filteredOptions.map((option) => option.value), + ]; + + return ( + + {label ? {label} : null} + + + + {draftValue.type === 'static' ? ( + <> + + Edit + + {isDefined(selectedOption) ? ( + + ) : null} + + + {draftValue.editingMode === 'edit' ? ( + + ) : null} + + ) : ( + + )} + + + {VariablePicker ? ( + + ) : null} + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/SelectFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/SelectFieldDisplay.tsx index d4492cac0..74ec57afc 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/SelectFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/SelectFieldDisplay.tsx @@ -1,6 +1,5 @@ -import { Tag } from 'twenty-ui'; - import { useSelectFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useSelectFieldDisplay'; +import { SelectDisplay } from '@/ui/field/display/components/SelectDisplay'; import { isDefined } from '~/utils/isDefined'; export const SelectFieldDisplay = () => { @@ -15,10 +14,6 @@ export const SelectFieldDisplay = () => { } return ( - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx index 3c3cf67ba..4f62ed895 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx @@ -3,8 +3,7 @@ import { useSelectField } from '@/object-record/record-field/meta-types/hooks/us import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent'; import { SINGLE_RECORD_SELECT_BASE_LIST } from '@/object-record/relation-picker/constants/SingleRecordSelectBaseList'; import { SelectOption } from '@/spreadsheet-import/types'; -import { SelectInput } from '@/ui/input/components/SelectInput'; -import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; +import { SelectInput } from '@/ui/field/input/components/SelectInput'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useState } from 'react'; @@ -64,7 +63,7 @@ export const SelectFieldInput = ({ return (
- - - + selectWrapperRef={selectWrapperRef} + onOptionSelected={handleSubmit} + options={fieldDefinition.metadata.options} + onCancel={onCancel} + defaultOption={selectedOption} + onFilterChange={setFilteredOptions} + onClear={ + fieldDefinition.metadata.isNullable ? handleClearField : undefined + } + clearLabel={fieldDefinition.label} + />
); }; diff --git a/packages/twenty-front/src/modules/ui/field/display/components/SelectDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/SelectDisplay.tsx new file mode 100644 index 000000000..c9ed54603 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/field/display/components/SelectDisplay.tsx @@ -0,0 +1,10 @@ +import { Tag, ThemeColor } from 'twenty-ui'; + +type SelectDisplayProps = { + color: ThemeColor; + label: string; +}; + +export const SelectDisplay = ({ color, label }: SelectDisplayProps) => { + return ; +}; diff --git a/packages/twenty-front/src/modules/ui/field/input/components/SelectInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/SelectInput.tsx new file mode 100644 index 000000000..56d62331b --- /dev/null +++ b/packages/twenty-front/src/modules/ui/field/input/components/SelectInput.tsx @@ -0,0 +1,55 @@ +import { SelectOption } from '@/spreadsheet-import/types'; +import { SelectInput as SelectBaseInput } from '@/ui/input/components/SelectInput'; +import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; +import { ReferenceType } from '@floating-ui/react'; + +type SelectInputProps = { + selectableListId: string; + selectableItemIdArray: string[]; + hotkeyScope: string; + onEnter: (itemId: string) => void; + selectWrapperRef?: ReferenceType | null | undefined; + onOptionSelected: (selectedOption: SelectOption) => void; + options: SelectOption[]; + onCancel?: () => void; + defaultOption?: SelectOption | undefined; + onFilterChange?: ((filteredOptions: SelectOption[]) => void) | undefined; + onClear?: (() => void) | undefined; + clearLabel?: string; +}; + +export const SelectInput = ({ + selectableListId, + selectableItemIdArray, + hotkeyScope, + onEnter, + selectWrapperRef, + onOptionSelected, + options, + onCancel, + defaultOption, + onFilterChange, + onClear, + clearLabel, +}: SelectInputProps) => { + return ( + + + + ); +}; diff --git a/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx b/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx index 86c413798..48324f5a5 100644 --- a/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx +++ b/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx @@ -18,10 +18,8 @@ export const EditableFilterChip = ({ diff --git a/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx b/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx index 55c2a77f3..de40713b6 100644 --- a/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx +++ b/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx @@ -33,35 +33,44 @@ const StyledChip = styled.div<{ variant: SortOrFitlerChipVariant }>` return theme.color.blue; } }}; + height: 26px; + box-sizing: border-box; cursor: pointer; display: flex; flex-direction: row; flex-shrink: 0; font-size: ${({ theme }) => theme.font.size.sm}; font-weight: ${({ theme }) => theme.font.weight.medium}; - padding: ${({ theme }) => theme.spacing(0.5) + ' ' + theme.spacing(2)}; - margin-left: ${({ theme }) => theme.spacing(2)}; + padding: ${({ theme }) => theme.spacing(0.5)}; + padding-left: ${({ theme }) => theme.spacing(1)}; + column-gap: ${({ theme }) => theme.spacing(1)}; user-select: none; white-space: nowrap; - max-height: ${({ theme }) => theme.spacing(4.5)}; + margin-left: ${({ theme }) => theme.spacing(2)}; `; const StyledIcon = styled.div` align-items: center; display: flex; - margin-right: ${({ theme }) => theme.spacing(1)}; `; -const StyledDelete = styled.div<{ variant: SortOrFitlerChipVariant }>` +const StyledDelete = styled.button<{ variant: SortOrFitlerChipVariant }>` + box-sizing: border-box; + height: 20px; + width: 20px; + display: flex; + justify-content: center; align-items: center; cursor: pointer; - padding: ${({ theme }) => theme.spacing(0.5)}; - display: flex; font-size: ${({ theme }) => theme.font.size.sm}; - margin-left: ${({ theme }) => theme.spacing(2)}; - margin-top: 1px; user-select: none; + padding: 0; + margin: 0; + background: none; + border: none; + color: inherit; + &:hover { background-color: ${({ theme, variant }) => { switch (variant) { diff --git a/packages/twenty-front/src/modules/workflow/workflow-actions/components/WorkflowEditActionFormRecordCreate.tsx b/packages/twenty-front/src/modules/workflow/workflow-actions/components/WorkflowEditActionFormRecordCreate.tsx index 81e40379b..2e60adc42 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-actions/components/WorkflowEditActionFormRecordCreate.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-actions/components/WorkflowEditActionFormRecordCreate.tsx @@ -1,4 +1,6 @@ import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition'; import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput'; import { Select, SelectOption } from '@/ui/input/components/Select'; import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase'; @@ -55,6 +57,33 @@ export const WorkflowEditActionFormRecordCreate = ({ }); const isFormDisabled = actionOptions.readonly; + const objectNameSingular = formData.objectName; + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const inlineFieldMetadataItems = objectMetadataItem.fields + .filter( + (fieldMetadataItem) => + fieldMetadataItem.type !== FieldMetadataType.Relation && + !fieldMetadataItem.isSystem && + fieldMetadataItem.isActive, + ) + .sort((fieldMetadataItemA, fieldMetadataItemB) => + fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name), + ); + + const inlineFieldDefinitions = inlineFieldMetadataItems.map( + (fieldMetadataItem) => + formatFieldMetadataItemAsFieldDefinition({ + field: fieldMetadataItem, + objectMetadataItem, + showLabel: true, + labelWidth: 90, + }), + ); + const handleFieldChange = ( fieldName: keyof CreateRecordFormData, updatedValue: JsonValue, @@ -76,23 +105,6 @@ export const WorkflowEditActionFormRecordCreate = ({ }); }, [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: CreateRecordFormData) => { if (actionOptions.readonly === true) { @@ -162,16 +174,16 @@ export const WorkflowEditActionFormRecordCreate = ({ - {editableFields.map((field) => { - const currentValue = formData[field.name] as JsonValue; + {inlineFieldDefinitions.map((field) => { + const currentValue = formData[field.metadata.fieldName] as JsonValue; return ( { - handleFieldChange(field.name, value); + handleFieldChange(field.metadata.fieldName, value); }} VariablePicker={WorkflowVariablePicker} />