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`.
This commit is contained in:
Baptiste Devessier
2025-01-13 15:07:41 +01:00
committed by GitHub
parent b81879dead
commit 9ebe519e66
24 changed files with 684 additions and 85 deletions

View File

@ -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) ? (
<FormNumberFieldInput
@ -62,6 +64,7 @@ export const FormFieldInput = ({
onPersist={onPersist}
placeholder={field.label}
VariablePicker={VariablePicker}
readonly={readonly}
/>
) : isFieldBoolean(field) ? (
<FormBooleanFieldInput
@ -69,6 +72,7 @@ export const FormFieldInput = ({
defaultValue={defaultValue as string | boolean | undefined}
onPersist={onPersist}
VariablePicker={VariablePicker}
readonly={readonly}
/>
) : isFieldText(field) ? (
<FormTextFieldInput
@ -77,6 +81,7 @@ export const FormFieldInput = ({
onPersist={onPersist}
placeholder={field.label}
VariablePicker={VariablePicker}
readonly={readonly}
/>
) : isFieldSelect(field) ? (
<FormSelectFieldInput
@ -86,6 +91,7 @@ export const FormFieldInput = ({
VariablePicker={VariablePicker}
options={field.metadata.options}
clearLabel={field.label}
readonly={readonly}
/>
) : isFieldFullName(field) ? (
<FormFullNameFieldInput
@ -93,6 +99,7 @@ export const FormFieldInput = ({
defaultValue={defaultValue as FieldFullNameValue | undefined}
onPersist={onPersist}
VariablePicker={VariablePicker}
readonly={readonly}
/>
) : isFieldAddress(field) ? (
<FormAddressFieldInput
@ -100,6 +107,7 @@ export const FormFieldInput = ({
defaultValue={defaultValue as FieldAddressValue | undefined}
onPersist={onPersist}
VariablePicker={VariablePicker}
readonly={readonly}
/>
) : isFieldLinks(field) ? (
<FormLinksFieldInput
@ -107,6 +115,7 @@ export const FormFieldInput = ({
defaultValue={defaultValue as FieldLinksValue | undefined}
onPersist={onPersist}
VariablePicker={VariablePicker}
readonly={readonly}
/>
) : isFieldEmails(field) ? (
<FormEmailsFieldInput
@ -114,6 +123,7 @@ export const FormFieldInput = ({
defaultValue={defaultValue as FieldEmailsValue | undefined}
onPersist={onPersist}
VariablePicker={VariablePicker}
readonly={readonly}
/>
) : isFieldPhones(field) ? (
<FormPhoneFieldInput
@ -121,6 +131,7 @@ export const FormFieldInput = ({
defaultValue={defaultValue as FieldPhonesValue | undefined}
onPersist={onPersist}
VariablePicker={VariablePicker}
readonly={readonly}
/>
) : isFieldDate(field) ? (
<FormDateFieldInput
@ -128,6 +139,7 @@ export const FormFieldInput = ({
defaultValue={defaultValue as string | undefined}
onPersist={onPersist}
VariablePicker={VariablePicker}
readonly={readonly}
/>
) : isFieldDateTime(field) ? (
<FormDateTimeFieldInput
@ -135,6 +147,7 @@ export const FormFieldInput = ({
defaultValue={defaultValue as string | undefined}
onPersist={onPersist}
VariablePicker={VariablePicker}
readonly={readonly}
/>
) : isFieldMultiSelect(field) ? (
<FormMultiSelectFieldInput
@ -143,6 +156,7 @@ export const FormFieldInput = ({
onPersist={onPersist}
VariablePicker={VariablePicker}
options={field.metadata.options}
readonly={readonly}
/>
) : isFieldRawJson(field) ? (
<FormRawJsonFieldInput
@ -151,6 +165,7 @@ export const FormFieldInput = ({
onPersist={onPersist}
placeholder={field.label}
VariablePicker={VariablePicker}
readonly={readonly}
/>
) : isFieldUuid(field) ? (
<FormUuidFieldInput
@ -159,6 +174,7 @@ export const FormFieldInput = ({
onPersist={onPersist}
placeholder={field.label}
VariablePicker={VariablePicker}
readonly={readonly}
/>
) : isFieldCurrency(field) ? (
<FormCurrencyFieldInput
@ -166,6 +182,7 @@ export const FormFieldInput = ({
defaultValue={defaultValue as FormFieldCurrencyValue | null}
onPersist={onPersist}
VariablePicker={VariablePicker}
readonly={readonly}
/>
) : null;
};

View File

@ -85,7 +85,7 @@ export const FormBooleanFieldInput = ({
<FormFieldInputRowContainer>
<FormFieldInputInputContainer
hasRightElement={isDefined(VariablePicker)}
hasRightElement={isDefined(VariablePicker) && !readonly}
>
{draftValue.type === 'static' ? (
<StyledBooleanInputContainer>
@ -98,12 +98,12 @@ export const FormBooleanFieldInput = ({
) : (
<VariableChip
rawVariableName={draftValue.value}
onRemove={handleUnlinkVariable}
onRemove={readonly ? undefined : handleUnlinkVariable}
/>
)}
</FormFieldInputInputContainer>
{VariablePicker ? (
{VariablePicker && !readonly ? (
<VariablePicker
inputId={inputId}
onVariableSelect={handleVariableTagInsert}

View File

@ -1,19 +1,20 @@
import { useMemo } from 'react';
import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
import { FormFieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { FormNestedFieldInputContainer } from '@/object-record/record-field/form-types/components/FormNestedFieldInputContainer';
import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput';
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
import { FormFieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata';
import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { useMemo } from 'react';
type FormCurrencyFieldInputProps = {
label?: string;
defaultValue?: FormFieldCurrencyValue | null;
onPersist: (value: FormFieldCurrencyValue) => 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}
/>
<FormNumberFieldInput
label="Amount Micros"
@ -66,6 +69,7 @@ export const FormCurrencyFieldInput = ({
onPersist={handleAmountMicrosChange}
VariablePicker={VariablePicker}
placeholder="Set 3210000 for 3.21$"
readonly={readonly}
/>
</FormNestedFieldInputContainer>
</FormFieldInputContainer>

View File

@ -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 (
<FormDateTimeFieldInput
@ -21,6 +23,7 @@ export const FormDateFieldInput = ({
defaultValue={defaultValue}
onPersist={onPersist}
VariablePicker={VariablePicker}
readonly={readonly}
/>
);
};

View File

@ -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 = ({
) : (
<VariableChip
rawVariableName={draftValue.value}
onRemove={handleUnlinkVariable}
onRemove={readonly ? undefined : handleUnlinkVariable}
/>
)}
</StyledInputContainer>
{VariablePicker ? (
{VariablePicker && !readonly ? (
<VariablePicker
inputId={inputId}
onVariableSelect={handleVariableTagInsert}

View File

@ -24,17 +24,21 @@ type FormMultiSelectFieldInputProps = {
options: SelectOption[];
onPersist: (value: FieldMultiSelectValue | string) => 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 = ({
<FormFieldInputRowContainer>
<FormFieldInputInputContainer
hasRightElement={isDefined(VariablePicker)}
hasRightElement={isDefined(VariablePicker) && !readonly}
>
{draftValue.type === 'static' ? (
<StyledDisplayModeContainer
data-open={draftValue.editingMode === 'edit'}
onClick={handleDisplayModeClick}
>
<VisibilityHidden>Edit</VisibilityHidden>
readonly ? (
<StyledDisplayModeReadonlyContainer>
{isDefined(selectedOptions) && (
<MultiSelectDisplay
values={selectedNames}
options={selectedOptions}
/>
)}
</StyledDisplayModeReadonlyContainer>
) : (
<StyledDisplayModeContainer
data-open={draftValue.editingMode === 'edit'}
onClick={handleDisplayModeClick}
>
<VisibilityHidden>Edit</VisibilityHidden>
{isDefined(selectedOptions) ? (
<MultiSelectDisplay
values={selectedNames}
options={selectedOptions}
/>
) : null}
</StyledDisplayModeContainer>
{isDefined(selectedOptions) && (
<MultiSelectDisplay
values={selectedNames}
options={selectedOptions}
/>
)}
</StyledDisplayModeContainer>
)
) : (
<VariableChip
rawVariableName={draftValue.value}
onRemove={handleUnlinkVariable}
onRemove={readonly ? undefined : handleUnlinkVariable}
/>
)}
</FormFieldInputInputContainer>
@ -202,7 +218,7 @@ export const FormMultiSelectFieldInput = ({
)}
</StyledSelectInputContainer>
{VariablePicker && (
{VariablePicker && !readonly && (
<VariablePicker
inputId={inputId}
onVariableSelect={handleVariableTagInsert}

View File

@ -26,6 +26,7 @@ type FormNumberFieldInputProps = {
onPersist: (value: number | null | string) => 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 = ({
<FormFieldInputRowContainer>
<FormFieldInputInputContainer
hasRightElement={isDefined(VariablePicker)}
hasRightElement={isDefined(VariablePicker) && !readonly}
>
{draftValue.type === 'static' ? (
<StyledInput
@ -112,16 +114,17 @@ export const FormNumberFieldInput = ({
copyButton={false}
hotkeyScope="record-create"
onChange={handleChange}
disabled={readonly}
/>
) : (
<VariableChip
rawVariableName={draftValue.value}
onRemove={handleUnlinkVariable}
onRemove={readonly ? undefined : handleUnlinkVariable}
/>
)}
</FormFieldInputInputContainer>
{VariablePicker ? (
{VariablePicker && !readonly ? (
<VariablePicker
inputId={inputId}
onVariableSelect={handleVariableTagInsert}

View File

@ -26,17 +26,21 @@ type FormSelectFieldInputProps = {
VariablePicker?: VariablePickerComponent;
options: SelectOption[];
clearLabel?: string;
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'] {
@ -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 ? (
<StyledDisplayModeReadonlyContainer>
{isDefined(selectedOption) && (
<SelectDisplay
color={selectedOption.color ?? 'transparent'}
label={selectedOption.label}
Icon={selectedOption.icon ?? undefined}
/>
)}
</StyledDisplayModeReadonlyContainer>
) : (
<StyledDisplayModeContainer
data-open={draftValue.editingMode === 'edit'}
onClick={handleDisplayModeClick}
>
<VisibilityHidden>Edit</VisibilityHidden>
{isDefined(selectedOption) ? (
{isDefined(selectedOption) && (
<SelectDisplay
color={selectedOption.color ?? 'transparent'}
label={selectedOption.label}
Icon={selectedOption.icon ?? undefined}
isUsedInForm
/>
) : null}
)}
</StyledDisplayModeContainer>
</>
)
) : (
<VariableChip
rawVariableName={draftValue.value}
onRemove={handleUnlinkVariable}
onRemove={readonly ? undefined : handleUnlinkVariable}
/>
)}
</FormFieldInputInputContainer>
<StyledSelectInputContainer>
{draftValue.type === 'static' &&
{!readonly &&
draftValue.type === 'static' &&
draftValue.editingMode === 'edit' && (
<OverlayContainer>
<SelectInput
@ -258,7 +273,7 @@ export const FormSelectFieldInput = ({
)}
</StyledSelectInputContainer>
{VariablePicker && (
{VariablePicker && !readonly && (
<VariablePicker
inputId={inputId}
onVariableSelect={handleVariableTagInsert}

View File

@ -63,7 +63,7 @@ export const FormTextFieldInput = ({
<FormFieldInputRowContainer multiline={multiline}>
<FormFieldInputInputContainer
hasRightElement={isDefined(VariablePicker)}
hasRightElement={isDefined(VariablePicker) && !readonly}
multiline={multiline}
>
<TextVariableEditor
@ -73,7 +73,7 @@ export const FormTextFieldInput = ({
/>
</FormFieldInputInputContainer>
{VariablePicker ? (
{VariablePicker && !readonly ? (
<VariablePicker
inputId={inputId}
multiline={multiline}

View File

@ -1,27 +1,86 @@
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
import { extractVariableLabel } from '@/workflow/workflow-variables/utils/extractVariableLabel';
import styled from '@emotion/styled';
import { css, useTheme } from '@emotion/react';
import { IconX, isDefined } from 'twenty-ui';
export const StyledContainer = styled.div`
align-items: center;
display: flex;
`;
const StyledChip = styled.div<{ deletable: boolean }>`
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 (
<StyledContainer>
<SortOrFilterChip
labelValue={extractVariableLabel(rawVariableName)}
onRemove={onRemove}
/>
<StyledChip deletable={isDefined(onRemove)}>
{extractVariableLabel(rawVariableName)}
{onRemove ? (
<StyledDelete onClick={onRemove}>
<IconX size={theme.icon.size.sm} stroke={theme.icon.stroke.sm} />
</StyledDelete>
) : null}
</StyledChip>
</StyledContainer>
);
};

View File

@ -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<typeof FormBooleanFieldInput> = {
@ -54,3 +54,37 @@ export const FalseByDefault: Story = {
await canvas.findByText('False');
},
};
export const WithVariablePicker: Story = {
args: {
VariablePicker: () => <div>VariablePicker</div>,
},
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: () => <div>VariablePicker</div>,
},
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();
},
};

View File

@ -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<typeof FormCurrencyFieldInput> = {
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: () => <div>VariablePicker</div>,
},
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: () => <div>VariablePicker</div>,
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);
},
};

View File

@ -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();
},
};

View File

@ -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();
},
};

View File

@ -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<typeof FormMultiSelectFieldInput> = {
@ -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: () => <div>VariablePicker</div>,
},
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();
},
};

View File

@ -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: () => <div>VariablePicker</div>,
},
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: () => <div>VariablePicker</div>,
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();
},
};

View File

@ -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<typeof FormSelectFieldInput> = {
title: 'UI/Data/Field/Form/Input/FormSelectFieldInput',
component: FormSelectFieldInput,
args: {},
argTypes: {},
};
export default meta;
type Story = StoryObj<typeof FormSelectFieldInput>;
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: () => <div>VariablePicker</div>,
},
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();
},
};

View File

@ -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<typeof FormTextFieldInput> = {
@ -43,3 +43,47 @@ export const Multiline: Story = {
await canvas.findByText(/^Text$/);
},
};
export const WithVariablePicker: Story = {
args: {
label: 'Text',
placeholder: 'Text field...',
VariablePicker: () => <div>VariablePicker</div>,
},
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: () => <div>VariablePicker</div>,
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();
},
};

View File

@ -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),

View File

@ -4,22 +4,8 @@ type SelectDisplayProps = {
color: ThemeColor | 'transparent';
label: string;
Icon?: IconComponent;
isUsedInForm?: boolean;
};
export const SelectDisplay = ({
color,
label,
Icon,
isUsedInForm,
}: SelectDisplayProps) => {
return (
<Tag
preventShrink
color={color}
text={label}
Icon={Icon}
preventPadding={isUsedInForm}
/>
);
export const SelectDisplay = ({ color, label, Icon }: SelectDisplayProps) => {
return <Tag preventShrink color={color} text={label} Icon={Icon} />;
};

View File

@ -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 (
<StyledEditableBooleanFieldContainer
onClick={readonly ? undefined : handleClick}
readonly={readonly}
data-testid={testId}
>
<BooleanDisplay value={internalValue} />

View File

@ -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 && (
<div ref={copyRef}>

View File

@ -215,6 +215,7 @@ export const WorkflowEditActionFormCreateRecord = ({
handleFieldChange(field.metadata.fieldName, value);
}}
VariablePicker={WorkflowVariablePicker}
readonly={isFormDisabled}
/>
);
})}

View File

@ -248,6 +248,7 @@ export const WorkflowEditActionFormUpdateRecord = ({
handleFieldChange(fieldDefinition.metadata.fieldName, value);
}}
VariablePicker={WorkflowVariablePicker}
readonly={isFormDisabled}
/>
);
})}