From 9ebe519e66ff2d2ea288b8e8cb1e799e61d72a25 Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Mon, 13 Jan 2025 15:07:41 +0100 Subject: [PATCH] Finalize the readonly for a few form fields #1 (#9524) There are many fields so I will cut my work in several small PRs. Here, I updated the following fields: - [x] `FormBooleanFieldInput` - [x] `FormCurrencyFieldInput` - [x] `FormNumberFieldInput` - [x] `FormDateFieldInput` - [x] `FormDateTimeFieldInput` - [x] `FormMultiSelectFieldInput` - [x] `FormSelectFieldInput` The updates in the components are relatively small. I wrote Storybook tests, and this is why the PR is quite big. The changes in the components should mostly the same. I added a disabled state to some inputs. I created a specialized `VariableChip` as its styles started diverging from the original `SortOrFilterChip`. --- .../components/FormFieldInput.tsx | 21 ++- .../components/FormBooleanFieldInput.tsx | 6 +- .../components/FormCurrencyFieldInput.tsx | 14 +- .../components/FormDateFieldInput.tsx | 3 + .../components/FormDateTimeFieldInput.tsx | 11 +- .../components/FormMultiSelectFieldInput.tsx | 56 ++++--- .../components/FormNumberFieldInput.tsx | 9 +- .../components/FormSelectFieldInput.tsx | 41 +++-- .../components/FormTextFieldInput.tsx | 4 +- .../form-types/components/VariableChip.tsx | 71 +++++++- .../FormBooleanFieldInput.stories.tsx | 36 +++- .../FormCurrencyFieldInput.stories.tsx | 62 ++++++- .../FormDateFieldInput.stories.tsx | 36 +++- .../FormDateTimeFieldInput.stories.tsx | 36 +++- .../FormMultiSelectFieldInput.stories.tsx | 87 +++++++++- .../FormNumberFieldInput.stories.tsx | 34 ++++ .../FormSelectFieldInput.stories.tsx | 157 ++++++++++++++++++ .../FormTextFieldInput.stories.tsx | 46 ++++- .../FormUuidFieldInput.stories.tsx | 4 +- .../display/components/SelectDisplay.tsx | 18 +- .../field/input/components/BooleanInput.tsx | 8 +- .../ui/field/input/components/TextInput.tsx | 7 + .../WorkflowEditActionFormCreateRecord.tsx | 1 + .../WorkflowEditActionFormUpdateRecord.tsx | 1 + 24 files changed, 684 insertions(+), 85 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormSelectFieldInput.stories.tsx 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 da67d9c74..9f0480927 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,5 +1,6 @@ import { FormAddressFieldInput } from '@/object-record/record-field/form-types/components/FormAddressFieldInput'; import { FormBooleanFieldInput } from '@/object-record/record-field/form-types/components/FormBooleanFieldInput'; +import { FormCurrencyFieldInput } from '@/object-record/record-field/form-types/components/FormCurrencyFieldInput'; import { FormDateFieldInput } from '@/object-record/record-field/form-types/components/FormDateFieldInput'; import { FormDateTimeFieldInput } from '@/object-record/record-field/form-types/components/FormDateTimeFieldInput'; import { FormEmailsFieldInput } from '@/object-record/record-field/form-types/components/FormEmailsFieldInput'; @@ -12,7 +13,6 @@ import { FormRawJsonFieldInput } from '@/object-record/record-field/form-types/c import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput'; import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput'; import { FormUuidFieldInput } from '@/object-record/record-field/form-types/components/FormUuidFieldInput'; -import { FormCurrencyFieldInput } from '@/object-record/record-field/form-types/components/FormCurrencyFieldInput'; import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent'; import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; import { @@ -27,6 +27,7 @@ import { } from '@/object-record/record-field/types/FieldMetadata'; import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress'; import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean'; +import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFieldCurrency'; import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate'; import { isFieldDateTime } from '@/object-record/record-field/types/guards/isFieldDateTime'; import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails'; @@ -39,7 +40,6 @@ import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFiel import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText'; import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid'; -import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFieldCurrency'; import { JsonValue } from 'type-fest'; type FormFieldInputProps = { @@ -47,6 +47,7 @@ type FormFieldInputProps = { defaultValue: JsonValue; onPersist: (value: JsonValue) => void; VariablePicker?: VariablePickerComponent; + readonly?: boolean; }; export const FormFieldInput = ({ @@ -54,6 +55,7 @@ export const FormFieldInput = ({ defaultValue, onPersist, VariablePicker, + readonly, }: FormFieldInputProps) => { return isFieldNumber(field) ? ( ) : isFieldBoolean(field) ? ( ) : isFieldText(field) ? ( ) : isFieldSelect(field) ? ( ) : isFieldFullName(field) ? ( ) : isFieldAddress(field) ? ( ) : isFieldLinks(field) ? ( ) : isFieldEmails(field) ? ( ) : isFieldPhones(field) ? ( ) : isFieldDate(field) ? ( ) : isFieldDateTime(field) ? ( ) : isFieldMultiSelect(field) ? ( ) : isFieldRawJson(field) ? ( ) : isFieldUuid(field) ? ( ) : isFieldCurrency(field) ? ( ) : null; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormBooleanFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormBooleanFieldInput.tsx index bde8b0115..3d170772d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormBooleanFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormBooleanFieldInput.tsx @@ -85,7 +85,7 @@ export const FormBooleanFieldInput = ({ {draftValue.type === 'static' ? ( @@ -98,12 +98,12 @@ export const FormBooleanFieldInput = ({ ) : ( )} - {VariablePicker ? ( + {VariablePicker && !readonly ? ( void; VariablePicker?: VariablePickerComponent; + readonly?: boolean; }; export const FormCurrencyFieldInput = ({ @@ -21,6 +22,7 @@ export const FormCurrencyFieldInput = ({ defaultValue, onPersist, VariablePicker, + readonly, }: FormCurrencyFieldInputProps) => { const currencies = useMemo(() => { return Object.entries(SETTINGS_FIELD_CURRENCY_CODES).map( @@ -59,6 +61,7 @@ export const FormCurrencyFieldInput = ({ options={currencies} clearLabel={'Currency Code'} VariablePicker={VariablePicker} + readonly={readonly} /> diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateFieldInput.tsx index 4f63ffbc6..2e411bacf 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateFieldInput.tsx @@ -6,6 +6,7 @@ type FormDateFieldInputProps = { defaultValue: string | undefined; onPersist: (value: string | null) => void; VariablePicker?: VariablePickerComponent; + readonly?: boolean; }; export const FormDateFieldInput = ({ @@ -13,6 +14,7 @@ export const FormDateFieldInput = ({ defaultValue, onPersist, VariablePicker, + readonly, }: FormDateFieldInputProps) => { return ( ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInput.tsx index 62b61ebe7..909c2f033 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInput.tsx @@ -46,6 +46,10 @@ const StyledDateInputAbsoluteContainer = styled.div` const StyledDateInput = styled.input<{ hasError?: boolean }>` ${TEXT_INPUT_STYLE} + &:disabled { + color: ${({ theme }) => theme.font.color.tertiary}; + } + ${({ hasError, theme }) => hasError && css` @@ -76,6 +80,7 @@ type FormDateTimeFieldInputProps = { defaultValue: string | undefined; onPersist: (value: string | null) => void; VariablePicker?: VariablePickerComponent; + readonly?: boolean; }; export const FormDateTimeFieldInput = ({ @@ -84,6 +89,7 @@ export const FormDateTimeFieldInput = ({ defaultValue, onPersist, VariablePicker, + readonly, }: FormDateTimeFieldInputProps) => { const { timeZone } = useContext(UserContext); @@ -338,6 +344,7 @@ export const FormDateTimeFieldInput = ({ onFocus={handleInputFocus} onChange={handleInputChange} onKeyDown={handleInputKeydown} + disabled={readonly} /> {draftValue.mode === 'edit' ? ( @@ -362,12 +369,12 @@ export const FormDateTimeFieldInput = ({ ) : ( )} - {VariablePicker ? ( + {VariablePicker && !readonly ? ( void; VariablePicker?: VariablePickerComponent; + readonly?: boolean; }; -const StyledDisplayModeContainer = styled.button` - width: 100%; +const StyledDisplayModeReadonlyContainer = styled.div` align-items: center; - display: flex; - cursor: pointer; - border: none; background: transparent; + border: none; + display: flex; font-family: inherit; padding-inline: ${({ theme }) => theme.spacing(2)}; + width: 100%; +`; + +const StyledDisplayModeContainer = styled(StyledDisplayModeReadonlyContainer)` + cursor: pointer; &:hover, &[data-open='true'] { @@ -54,6 +58,7 @@ export const FormMultiSelectFieldInput = ({ options, onPersist, VariablePicker, + readonly, }: FormMultiSelectFieldInputProps) => { const inputId = useId(); @@ -164,26 +169,37 @@ export const FormMultiSelectFieldInput = ({ {draftValue.type === 'static' ? ( - - Edit + readonly ? ( + + {isDefined(selectedOptions) && ( + + )} + + ) : ( + + Edit - {isDefined(selectedOptions) ? ( - - ) : null} - + {isDefined(selectedOptions) && ( + + )} + + ) ) : ( )} @@ -202,7 +218,7 @@ export const FormMultiSelectFieldInput = ({ )} - {VariablePicker && ( + {VariablePicker && !readonly && ( void; VariablePicker?: VariablePickerComponent; hint?: string; + readonly?: boolean; }; export const FormNumberFieldInput = ({ @@ -35,6 +36,7 @@ export const FormNumberFieldInput = ({ onPersist, VariablePicker, hint, + readonly, }: FormNumberFieldInputProps) => { const inputId = useId(); @@ -102,7 +104,7 @@ export const FormNumberFieldInput = ({ {draftValue.type === 'static' ? ( ) : ( )} - {VariablePicker ? ( + {VariablePicker && !readonly ? ( theme.spacing(2)}; + width: 100%; +`; + +const StyledDisplayModeContainer = styled(StyledDisplayModeReadonlyContainer)` + cursor: pointer; &:hover, &[data-open='true'] { @@ -57,6 +61,7 @@ export const FormSelectFieldInput = ({ VariablePicker, options, clearLabel, + readonly, }: FormSelectFieldInputProps) => { const inputId = useId(); @@ -213,32 +218,42 @@ export const FormSelectFieldInput = ({ hasRightElement={isDefined(VariablePicker)} > {draftValue.type === 'static' ? ( - <> + readonly ? ( + + {isDefined(selectedOption) && ( + + )} + + ) : ( Edit - {isDefined(selectedOption) ? ( + {isDefined(selectedOption) && ( - ) : null} + )} - + ) ) : ( )} - {draftValue.type === 'static' && + {!readonly && + draftValue.type === 'static' && draftValue.editingMode === 'edit' && ( - {VariablePicker && ( + {VariablePicker && !readonly && ( - {VariablePicker ? ( + {VariablePicker && !readonly ? ( ` + align-items: center; + background-color: ${({ theme }) => theme.accent.quaternary}; + border: 1px solid ${({ theme }) => theme.accent.tertiary}; + border-radius: 4px; + color: ${({ theme }) => theme.color.blue}; + height: 26px; + box-sizing: border-box; + cursor: pointer; + display: flex; + flex-direction: row; + flex-shrink: 0; + column-gap: ${({ theme }) => theme.spacing(1)}; + font-size: ${({ theme }) => theme.font.size.sm}; + font-weight: ${({ theme }) => theme.font.weight.medium}; + padding: ${({ theme }) => theme.spacing(0.5)}; + padding-left: ${({ theme }) => theme.spacing(1)}; + margin-left: ${({ theme }) => theme.spacing(2)}; + user-select: none; + white-space: nowrap; + + ${({ theme, deletable }) => + !deletable && + css` + padding-right: ${theme.spacing(1)}; + `} +`; + +const StyledDelete = styled.button` + box-sizing: border-box; + height: 20px; + width: 20px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + font-size: ${({ theme }) => theme.font.size.sm}; + user-select: none; + padding: 0; + margin: 0; + background: none; + border: none; + color: inherit; + + &:hover { + background-color: ${({ theme }) => theme.accent.secondary}; + border-radius: ${({ theme }) => theme.border.radius.sm}; + } +`; + type VariableChipProps = { rawVariableName: string; - onRemove: () => void; + onRemove?: () => void; }; export const VariableChip = ({ rawVariableName, onRemove, }: VariableChipProps) => { + const theme = useTheme(); + return ( - + + {extractVariableLabel(rawVariableName)} + + {onRemove ? ( + + + + ) : null} + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormBooleanFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormBooleanFieldInput.stories.tsx index 8ae79ea62..2fabf0854 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormBooleanFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormBooleanFieldInput.stories.tsx @@ -1,5 +1,5 @@ import { Meta, StoryObj } from '@storybook/react'; -import { within } from '@storybook/test'; +import { expect, userEvent, within } from '@storybook/test'; import { FormBooleanFieldInput } from '../FormBooleanFieldInput'; const meta: Meta = { @@ -54,3 +54,37 @@ export const FalseByDefault: Story = { await canvas.findByText('False'); }, }; + +export const WithVariablePicker: Story = { + args: { + VariablePicker: () =>
VariablePicker
, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const variablePicker = await canvas.findByText('VariablePicker'); + + expect(variablePicker).toBeVisible(); + }, +}; + +export const Disabled: Story = { + args: { + readonly: true, + defaultValue: false, + VariablePicker: () =>
VariablePicker
, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const toggle = await canvas.findByText('False'); + expect(toggle).toBeVisible(); + + await userEvent.click(toggle); + + expect(toggle).toHaveTextContent('False'); + + const variablePicker = canvas.queryByText('VariablePicker'); + expect(variablePicker).not.toBeInTheDocument(); + }, +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormCurrencyFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormCurrencyFieldInput.stories.tsx index d139f5aa0..d0962dc37 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormCurrencyFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormCurrencyFieldInput.stories.tsx @@ -1,8 +1,8 @@ -import { FormCurrencyFieldInput } from '../FormCurrencyFieldInput'; -import { Meta, StoryObj } from '@storybook/react'; -import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata'; import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; -import { within } from '@storybook/test'; +import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata'; +import { Meta, StoryObj } from '@storybook/react'; +import { expect, within } from '@storybook/test'; +import { FormCurrencyFieldInput } from '../FormCurrencyFieldInput'; const meta: Meta = { title: 'UI/Data/Field/Form/Input/FormCurrencyFieldInput', @@ -31,3 +31,57 @@ export const Default: Story = { await canvas.findByText('Amount Micros'); }, }; + +export const WithVariable: Story = { + args: { + label: 'Salary', + defaultValue: { + currencyCode: CurrencyCode.USD, + amountMicros: '{{a.b.c}}', + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const currency = await canvas.findByText(/USD/); + expect(currency).toBeVisible(); + + const amountVariable = await canvas.findByText('c'); + expect(amountVariable).toBeVisible(); + }, +}; + +export const WithVariablePicker: Story = { + args: { + VariablePicker: () =>
VariablePicker
, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const variablePickers = await canvas.findAllByText('VariablePicker'); + + expect(variablePickers).toHaveLength(2); + }, +}; + +export const Disabled: Story = { + args: { + label: 'Salary', + defaultValue: defaultSalaryValue, + VariablePicker: () =>
VariablePicker
, + readonly: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const currency = await canvas.findByText(/USD/); + expect(currency).toBeVisible(); + + const amountInput = await canvas.findByDisplayValue('44000000'); + expect(amountInput).toBeVisible(); + expect(amountInput).toBeDisabled(); + + const variablePickers = canvas.queryAllByText('VariablePicker'); + expect(variablePickers).toHaveLength(0); + }, +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateFieldInput.stories.tsx index bd61db22e..100c2ecc8 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateFieldInput.stories.tsx @@ -1,9 +1,9 @@ import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate'; import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate'; import { parseDateToString } from '@/ui/input/components/internal/date/utils/parseDateToString'; -import { expect } from '@storybook/jest'; import { Meta, StoryObj } from '@storybook/react'; import { + expect, fn, userEvent, waitFor, @@ -323,7 +323,9 @@ export const SwitchesToStandaloneVariable: Story = { const variableTag = await canvas.findByText('test'); expect(variableTag).toBeVisible(); - const removeVariableButton = canvas.getByTestId(/^remove-icon/); + const removeVariableButton = canvasElement.querySelector( + 'button .tabler-icon-x', + ); await Promise.all([ userEvent.click(removeVariableButton), @@ -372,3 +374,33 @@ export const ClickingOutsideDoesNotResetInputState: Story = { expect(input).toHaveDisplayValue(defaultValueAsDisplayString.slice(0, -2)); }, }; + +export const Disabled: Story = { + args: { + label: 'Created At', + defaultValue: `${currentYear}-12-09T13:20:19.631Z`, + onPersist: fn(), + readonly: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByDisplayValue('12/09/' + currentYear); + expect(input).toBeDisabled(); + }, +}; + +export const DisabledWithVariable: Story = { + args: { + label: 'Created At', + defaultValue: `{{a.b.c}}`, + onPersist: fn(), + readonly: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const variableChip = await canvas.findByText('c'); + expect(variableChip).toBeVisible(); + }, +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateTimeFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateTimeFieldInput.stories.tsx index ad8c7e677..fdf84ff6f 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateTimeFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateTimeFieldInput.stories.tsx @@ -352,7 +352,9 @@ export const SwitchesToStandaloneVariable: Story = { const variableTag = await canvas.findByText('test'); expect(variableTag).toBeVisible(); - const removeVariableButton = canvas.getByTestId(/^remove-icon/); + const removeVariableButton = canvasElement.querySelector( + 'button .tabler-icon-x', + ); await Promise.all([ userEvent.click(removeVariableButton), @@ -401,3 +403,35 @@ export const ClickingOutsideDoesNotResetInputState: Story = { expect(input).toHaveDisplayValue(defaultValueAsDisplayString.slice(0, -2)); }, }; + +export const Disabled: Story = { + args: { + label: 'Created At', + defaultValue: `${currentYear}-12-09T13:20:19.631Z`, + onPersist: fn(), + readonly: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByDisplayValue( + new RegExp(`12/09/${currentYear} \\d{2}:20`), + ); + expect(input).toBeDisabled(); + }, +}; + +export const DisabledWithVariable: Story = { + args: { + label: 'Created At', + defaultValue: `{{a.b.c}}`, + onPersist: fn(), + readonly: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const variableChip = await canvas.findByText('c'); + expect(variableChip).toBeVisible(); + }, +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormMultiSelectFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormMultiSelectFieldInput.stories.tsx index 2b4df71f5..6f8591568 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormMultiSelectFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormMultiSelectFieldInput.stories.tsx @@ -1,5 +1,6 @@ +import { expect } from '@storybook/jest'; import { Meta, StoryObj } from '@storybook/react'; -import { within } from '@storybook/test'; +import { fn, userEvent, within } from '@storybook/test'; import { FormMultiSelectFieldInput } from '../FormMultiSelectFieldInput'; const meta: Meta = { @@ -48,3 +49,87 @@ export const Default: Story = { await canvas.findByText('Work Policy 2'); }, }; + +export const WithVariablePicker: Story = { + args: { + label: 'Work Policy', + defaultValue: ['WORK_POLICY_1', 'WORK_POLICY_2'], + options: [ + { + label: 'Work Policy 1', + value: 'WORK_POLICY_1', + color: 'blue', + }, + ], + onPersist: fn(), + VariablePicker: () =>
VariablePicker
, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const firstChip = await canvas.findByText('Work Policy 1'); + expect(firstChip).toBeVisible(); + }, +}; + +export const Disabled: Story = { + args: { + label: 'Work Policy', + defaultValue: ['WORK_POLICY_1', 'WORK_POLICY_2'], + options: [ + { + label: 'Work Policy 1', + value: 'WORK_POLICY_1', + color: 'blue', + }, + { + label: 'Work Policy 2', + value: 'WORK_POLICY_2', + color: 'green', + }, + { + label: 'Work Policy 3', + value: 'WORK_POLICY_3', + color: 'red', + }, + { + label: 'Work Policy 4', + value: 'WORK_POLICY_4', + color: 'yellow', + }, + ], + onPersist: fn(), + readonly: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const firstChip = await canvas.findByText('Work Policy 1'); + expect(firstChip).toBeVisible(); + + await userEvent.click(firstChip); + + const searchInputInModal = canvas.queryByPlaceholderText('Search'); + expect(searchInputInModal).not.toBeInTheDocument(); + }, +}; + +export const DisabledWithVariable: Story = { + args: { + label: 'Created At', + defaultValue: `{{a.b.c}}`, + onPersist: fn(), + readonly: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const variableChip = await canvas.findByText('c'); + expect(variableChip).toBeVisible(); + + await userEvent.click(variableChip); + + const searchInputInModal = canvas.queryByPlaceholderText('Search'); + expect(searchInputInModal).not.toBeInTheDocument(); + }, +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormNumberFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormNumberFieldInput.stories.tsx index 86fd52adc..268f901af 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormNumberFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormNumberFieldInput.stories.tsx @@ -1,3 +1,4 @@ +import { expect } from '@storybook/jest'; import { Meta, StoryObj } from '@storybook/react'; import { within } from '@storybook/test'; import { FormNumberFieldInput } from '../FormNumberFieldInput'; @@ -36,3 +37,36 @@ export const WithLabel: Story = { await canvas.findByPlaceholderText('Number field...'); }, }; + +export const WithVariablePicker: Story = { + args: { + placeholder: 'Number field...', + VariablePicker: () =>
VariablePicker
, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const variablePicker = await canvas.findByText('VariablePicker'); + + expect(variablePicker).toBeVisible(); + }, +}; + +export const Disabled: Story = { + args: { + placeholder: 'Number field...', + readonly: true, + VariablePicker: () =>
VariablePicker
, + defaultValue: 123, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByDisplayValue('123'); + + expect(input).toBeDisabled(); + + const variablePicker = canvas.queryByText('VariablePicker'); + expect(variablePicker).not.toBeInTheDocument(); + }, +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormSelectFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormSelectFieldInput.stories.tsx new file mode 100644 index 000000000..93241b0d3 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormSelectFieldInput.stories.tsx @@ -0,0 +1,157 @@ +import { expect } from '@storybook/jest'; +import { Meta, StoryObj } from '@storybook/react'; +import { fn, userEvent, within } from '@storybook/test'; +import { FormSelectFieldInput } from '../FormSelectFieldInput'; + +const meta: Meta = { + title: 'UI/Data/Field/Form/Input/FormSelectFieldInput', + component: FormSelectFieldInput, + args: {}, + argTypes: {}, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + label: 'Work Policy', + defaultValue: 'WORK_POLICY_1', + options: [ + { + label: 'Work Policy 1', + value: 'WORK_POLICY_1', + color: 'blue', + }, + { + label: 'Work Policy 2', + value: 'WORK_POLICY_2', + color: 'green', + }, + { + label: 'Work Policy 3', + value: 'WORK_POLICY_3', + color: 'red', + }, + { + label: 'Work Policy 4', + value: 'WORK_POLICY_4', + color: 'yellow', + }, + ], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const selectedOption = await canvas.findByText('Work Policy'); + + expect(selectedOption).toBeVisible(); + }, +}; + +export const WithVariablePicker: Story = { + args: { + label: 'Work Policy', + defaultValue: 'WORK_POLICY_1', + options: [ + { + label: 'Work Policy 1', + value: 'WORK_POLICY_1', + color: 'blue', + }, + ], + onPersist: fn(), + VariablePicker: () =>
VariablePicker
, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const firstChip = await canvas.findByText('Work Policy 1'); + expect(firstChip).toBeVisible(); + }, +}; + +export const Disabled: Story = { + args: { + label: 'Work Policy', + defaultValue: 'WORK_POLICY_1', + options: [ + { + label: 'Work Policy 1', + value: 'WORK_POLICY_1', + color: 'blue', + }, + { + label: 'Work Policy 2', + value: 'WORK_POLICY_2', + color: 'green', + }, + { + label: 'Work Policy 3', + value: 'WORK_POLICY_3', + color: 'red', + }, + { + label: 'Work Policy 4', + value: 'WORK_POLICY_4', + color: 'yellow', + }, + ], + onPersist: fn(), + readonly: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const firstChip = await canvas.findByText('Work Policy 1'); + expect(firstChip).toBeVisible(); + + await userEvent.click(firstChip); + + const searchInputInModal = canvas.queryByPlaceholderText('Search'); + expect(searchInputInModal).not.toBeInTheDocument(); + }, +}; + +export const DisabledWithVariable: Story = { + args: { + label: 'Created At', + defaultValue: `{{a.b.c}}`, + options: [ + { + label: 'Work Policy 1', + value: 'WORK_POLICY_1', + color: 'blue', + }, + { + label: 'Work Policy 2', + value: 'WORK_POLICY_2', + color: 'green', + }, + { + label: 'Work Policy 3', + value: 'WORK_POLICY_3', + color: 'red', + }, + { + label: 'Work Policy 4', + value: 'WORK_POLICY_4', + color: 'yellow', + }, + ], + onPersist: fn(), + readonly: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const variableChip = await canvas.findByText('c'); + expect(variableChip).toBeVisible(); + + await userEvent.click(variableChip); + + const searchInputInModal = canvas.queryByPlaceholderText('Search'); + expect(searchInputInModal).not.toBeInTheDocument(); + }, +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormTextFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormTextFieldInput.stories.tsx index ec0aada4c..3348085a8 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormTextFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormTextFieldInput.stories.tsx @@ -1,5 +1,5 @@ import { Meta, StoryObj } from '@storybook/react'; -import { within } from '@storybook/test'; +import { expect, fn, userEvent, within } from '@storybook/test'; import { FormTextFieldInput } from '../FormTextFieldInput'; const meta: Meta = { @@ -43,3 +43,47 @@ export const Multiline: Story = { await canvas.findByText(/^Text$/); }, }; + +export const WithVariablePicker: Story = { + args: { + label: 'Text', + placeholder: 'Text field...', + VariablePicker: () =>
VariablePicker
, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const variablePicker = await canvas.findByText('VariablePicker'); + + expect(variablePicker).toBeVisible(); + }, +}; + +export const Disabled: Story = { + args: { + label: 'Text', + placeholder: 'Text field...', + defaultValue: 'Text field', + readonly: true, + VariablePicker: () =>
VariablePicker
, + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const variablePicker = canvas.queryByText('VariablePicker'); + expect(variablePicker).not.toBeInTheDocument(); + + const editor = canvasElement.querySelector('.ProseMirror > p'); + expect(editor).toBeVisible(); + + const defaultValue = await canvas.findByText('Text field'); + expect(defaultValue).toBeVisible(); + + await userEvent.type(editor, 'Hello'); + + expect(args.onPersist).not.toHaveBeenCalled(); + expect(canvas.queryByText('Hello')).not.toBeInTheDocument(); + expect(defaultValue).toBeVisible(); + }, +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormUuidFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormUuidFieldInput.stories.tsx index d4bb1f219..524eff315 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormUuidFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormUuidFieldInput.stories.tsx @@ -194,7 +194,9 @@ export const ReplaceStaticValueWithVariable: Story = { }), ]); - const removeVariableButton = await canvas.findByTestId(/^remove-icon/); + const removeVariableButton = canvasElement.querySelector( + 'button .tabler-icon-x', + ); await Promise.all([ userEvent.click(removeVariableButton), 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 index eb0a87219..13ac37aa3 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/SelectDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/SelectDisplay.tsx @@ -4,22 +4,8 @@ type SelectDisplayProps = { color: ThemeColor | 'transparent'; label: string; Icon?: IconComponent; - isUsedInForm?: boolean; }; -export const SelectDisplay = ({ - color, - label, - Icon, - isUsedInForm, -}: SelectDisplayProps) => { - return ( - - ); +export const SelectDisplay = ({ color, label, Icon }: SelectDisplayProps) => { + return ; }; diff --git a/packages/twenty-front/src/modules/ui/field/input/components/BooleanInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/BooleanInput.tsx index 30d33eecd..8da3b5672 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/BooleanInput.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/BooleanInput.tsx @@ -1,15 +1,18 @@ -import { useEffect, useState } from 'react'; import styled from '@emotion/styled'; +import { useEffect, useState } from 'react'; import { BooleanDisplay } from '@/ui/field/display/components/BooleanDisplay'; -const StyledEditableBooleanFieldContainer = styled.div` +const StyledEditableBooleanFieldContainer = styled.div<{ readonly?: boolean }>` align-items: center; cursor: ${({ onClick }) => (onClick ? 'pointer' : 'default')}; display: flex; height: 100%; width: 100%; + + color: ${({ theme, readonly }) => + readonly ? theme.font.color.tertiary : theme.font.color.primary}; `; type BooleanInputProps = { @@ -39,6 +42,7 @@ export const BooleanInput = ({ return ( diff --git a/packages/twenty-front/src/modules/ui/field/input/components/TextInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/TextInput.tsx index 9699765b2..a71064ada 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/TextInput.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/TextInput.tsx @@ -9,6 +9,10 @@ export const StyledTextInput = styled.input` margin: 0; ${TEXT_INPUT_STYLE} width: 100%; + + &:disabled { + color: ${({ theme }) => theme.font.color.tertiary}; + } `; type TextInputProps = { @@ -25,6 +29,7 @@ type TextInputProps = { onChange?: (newText: string) => void; copyButton?: boolean; shouldTrim?: boolean; + disabled?: boolean; }; const getValue = (value: string, shouldTrim: boolean) => { @@ -49,6 +54,7 @@ export const TextInput = ({ onChange, copyButton = true, shouldTrim = true, + disabled, }: TextInputProps) => { const [internalText, setInternalText] = useState(value); @@ -85,6 +91,7 @@ export const TextInput = ({ onChange={handleChange} autoFocus={autoFocus} value={internalText} + disabled={disabled} /> {copyButton && (
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormCreateRecord.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormCreateRecord.tsx index 032f97ef5..57fc828a4 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormCreateRecord.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormCreateRecord.tsx @@ -215,6 +215,7 @@ export const WorkflowEditActionFormCreateRecord = ({ handleFieldChange(field.metadata.fieldName, value); }} VariablePicker={WorkflowVariablePicker} + readonly={isFormDisabled} /> ); })} diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormUpdateRecord.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormUpdateRecord.tsx index 49c494d80..eb079c636 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormUpdateRecord.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormUpdateRecord.tsx @@ -248,6 +248,7 @@ export const WorkflowEditActionFormUpdateRecord = ({ handleFieldChange(fieldDefinition.metadata.fieldName, value); }} VariablePicker={WorkflowVariablePicker} + readonly={isFormDisabled} /> ); })}