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

View File

@ -85,7 +85,7 @@ export const FormBooleanFieldInput = ({
<FormFieldInputRowContainer> <FormFieldInputRowContainer>
<FormFieldInputInputContainer <FormFieldInputInputContainer
hasRightElement={isDefined(VariablePicker)} hasRightElement={isDefined(VariablePicker) && !readonly}
> >
{draftValue.type === 'static' ? ( {draftValue.type === 'static' ? (
<StyledBooleanInputContainer> <StyledBooleanInputContainer>
@ -98,12 +98,12 @@ export const FormBooleanFieldInput = ({
) : ( ) : (
<VariableChip <VariableChip
rawVariableName={draftValue.value} rawVariableName={draftValue.value}
onRemove={handleUnlinkVariable} onRemove={readonly ? undefined : handleUnlinkVariable}
/> />
)} )}
</FormFieldInputInputContainer> </FormFieldInputInputContainer>
{VariablePicker ? ( {VariablePicker && !readonly ? (
<VariablePicker <VariablePicker
inputId={inputId} inputId={inputId}
onVariableSelect={handleVariableTagInsert} 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 { 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 { FormNestedFieldInputContainer } from '@/object-record/record-field/form-types/components/FormNestedFieldInputContainer';
import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput'; import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput';
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput'; 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 { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { useMemo } from 'react';
type FormCurrencyFieldInputProps = { type FormCurrencyFieldInputProps = {
label?: string; label?: string;
defaultValue?: FormFieldCurrencyValue | null; defaultValue?: FormFieldCurrencyValue | null;
onPersist: (value: FormFieldCurrencyValue) => void; onPersist: (value: FormFieldCurrencyValue) => void;
VariablePicker?: VariablePickerComponent; VariablePicker?: VariablePickerComponent;
readonly?: boolean;
}; };
export const FormCurrencyFieldInput = ({ export const FormCurrencyFieldInput = ({
@ -21,6 +22,7 @@ export const FormCurrencyFieldInput = ({
defaultValue, defaultValue,
onPersist, onPersist,
VariablePicker, VariablePicker,
readonly,
}: FormCurrencyFieldInputProps) => { }: FormCurrencyFieldInputProps) => {
const currencies = useMemo(() => { const currencies = useMemo(() => {
return Object.entries(SETTINGS_FIELD_CURRENCY_CODES).map( return Object.entries(SETTINGS_FIELD_CURRENCY_CODES).map(
@ -59,6 +61,7 @@ export const FormCurrencyFieldInput = ({
options={currencies} options={currencies}
clearLabel={'Currency Code'} clearLabel={'Currency Code'}
VariablePicker={VariablePicker} VariablePicker={VariablePicker}
readonly={readonly}
/> />
<FormNumberFieldInput <FormNumberFieldInput
label="Amount Micros" label="Amount Micros"
@ -66,6 +69,7 @@ export const FormCurrencyFieldInput = ({
onPersist={handleAmountMicrosChange} onPersist={handleAmountMicrosChange}
VariablePicker={VariablePicker} VariablePicker={VariablePicker}
placeholder="Set 3210000 for 3.21$" placeholder="Set 3210000 for 3.21$"
readonly={readonly}
/> />
</FormNestedFieldInputContainer> </FormNestedFieldInputContainer>
</FormFieldInputContainer> </FormFieldInputContainer>

View File

@ -6,6 +6,7 @@ type FormDateFieldInputProps = {
defaultValue: string | undefined; defaultValue: string | undefined;
onPersist: (value: string | null) => void; onPersist: (value: string | null) => void;
VariablePicker?: VariablePickerComponent; VariablePicker?: VariablePickerComponent;
readonly?: boolean;
}; };
export const FormDateFieldInput = ({ export const FormDateFieldInput = ({
@ -13,6 +14,7 @@ export const FormDateFieldInput = ({
defaultValue, defaultValue,
onPersist, onPersist,
VariablePicker, VariablePicker,
readonly,
}: FormDateFieldInputProps) => { }: FormDateFieldInputProps) => {
return ( return (
<FormDateTimeFieldInput <FormDateTimeFieldInput
@ -21,6 +23,7 @@ export const FormDateFieldInput = ({
defaultValue={defaultValue} defaultValue={defaultValue}
onPersist={onPersist} onPersist={onPersist}
VariablePicker={VariablePicker} VariablePicker={VariablePicker}
readonly={readonly}
/> />
); );
}; };

View File

@ -46,6 +46,10 @@ const StyledDateInputAbsoluteContainer = styled.div`
const StyledDateInput = styled.input<{ hasError?: boolean }>` const StyledDateInput = styled.input<{ hasError?: boolean }>`
${TEXT_INPUT_STYLE} ${TEXT_INPUT_STYLE}
&:disabled {
color: ${({ theme }) => theme.font.color.tertiary};
}
${({ hasError, theme }) => ${({ hasError, theme }) =>
hasError && hasError &&
css` css`
@ -76,6 +80,7 @@ type FormDateTimeFieldInputProps = {
defaultValue: string | undefined; defaultValue: string | undefined;
onPersist: (value: string | null) => void; onPersist: (value: string | null) => void;
VariablePicker?: VariablePickerComponent; VariablePicker?: VariablePickerComponent;
readonly?: boolean;
}; };
export const FormDateTimeFieldInput = ({ export const FormDateTimeFieldInput = ({
@ -84,6 +89,7 @@ export const FormDateTimeFieldInput = ({
defaultValue, defaultValue,
onPersist, onPersist,
VariablePicker, VariablePicker,
readonly,
}: FormDateTimeFieldInputProps) => { }: FormDateTimeFieldInputProps) => {
const { timeZone } = useContext(UserContext); const { timeZone } = useContext(UserContext);
@ -338,6 +344,7 @@ export const FormDateTimeFieldInput = ({
onFocus={handleInputFocus} onFocus={handleInputFocus}
onChange={handleInputChange} onChange={handleInputChange}
onKeyDown={handleInputKeydown} onKeyDown={handleInputKeydown}
disabled={readonly}
/> />
{draftValue.mode === 'edit' ? ( {draftValue.mode === 'edit' ? (
@ -362,12 +369,12 @@ export const FormDateTimeFieldInput = ({
) : ( ) : (
<VariableChip <VariableChip
rawVariableName={draftValue.value} rawVariableName={draftValue.value}
onRemove={handleUnlinkVariable} onRemove={readonly ? undefined : handleUnlinkVariable}
/> />
)} )}
</StyledInputContainer> </StyledInputContainer>
{VariablePicker ? ( {VariablePicker && !readonly ? (
<VariablePicker <VariablePicker
inputId={inputId} inputId={inputId}
onVariableSelect={handleVariableTagInsert} onVariableSelect={handleVariableTagInsert}

View File

@ -24,17 +24,21 @@ type FormMultiSelectFieldInputProps = {
options: SelectOption[]; options: SelectOption[];
onPersist: (value: FieldMultiSelectValue | string) => void; onPersist: (value: FieldMultiSelectValue | string) => void;
VariablePicker?: VariablePickerComponent; VariablePicker?: VariablePickerComponent;
readonly?: boolean;
}; };
const StyledDisplayModeContainer = styled.button` const StyledDisplayModeReadonlyContainer = styled.div`
width: 100%;
align-items: center; align-items: center;
display: flex;
cursor: pointer;
border: none;
background: transparent; background: transparent;
border: none;
display: flex;
font-family: inherit; font-family: inherit;
padding-inline: ${({ theme }) => theme.spacing(2)}; padding-inline: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const StyledDisplayModeContainer = styled(StyledDisplayModeReadonlyContainer)`
cursor: pointer;
&:hover, &:hover,
&[data-open='true'] { &[data-open='true'] {
@ -54,6 +58,7 @@ export const FormMultiSelectFieldInput = ({
options, options,
onPersist, onPersist,
VariablePicker, VariablePicker,
readonly,
}: FormMultiSelectFieldInputProps) => { }: FormMultiSelectFieldInputProps) => {
const inputId = useId(); const inputId = useId();
@ -164,26 +169,37 @@ export const FormMultiSelectFieldInput = ({
<FormFieldInputRowContainer> <FormFieldInputRowContainer>
<FormFieldInputInputContainer <FormFieldInputInputContainer
hasRightElement={isDefined(VariablePicker)} hasRightElement={isDefined(VariablePicker) && !readonly}
> >
{draftValue.type === 'static' ? ( {draftValue.type === 'static' ? (
<StyledDisplayModeContainer readonly ? (
data-open={draftValue.editingMode === 'edit'} <StyledDisplayModeReadonlyContainer>
onClick={handleDisplayModeClick} {isDefined(selectedOptions) && (
> <MultiSelectDisplay
<VisibilityHidden>Edit</VisibilityHidden> values={selectedNames}
options={selectedOptions}
/>
)}
</StyledDisplayModeReadonlyContainer>
) : (
<StyledDisplayModeContainer
data-open={draftValue.editingMode === 'edit'}
onClick={handleDisplayModeClick}
>
<VisibilityHidden>Edit</VisibilityHidden>
{isDefined(selectedOptions) ? ( {isDefined(selectedOptions) && (
<MultiSelectDisplay <MultiSelectDisplay
values={selectedNames} values={selectedNames}
options={selectedOptions} options={selectedOptions}
/> />
) : null} )}
</StyledDisplayModeContainer> </StyledDisplayModeContainer>
)
) : ( ) : (
<VariableChip <VariableChip
rawVariableName={draftValue.value} rawVariableName={draftValue.value}
onRemove={handleUnlinkVariable} onRemove={readonly ? undefined : handleUnlinkVariable}
/> />
)} )}
</FormFieldInputInputContainer> </FormFieldInputInputContainer>
@ -202,7 +218,7 @@ export const FormMultiSelectFieldInput = ({
)} )}
</StyledSelectInputContainer> </StyledSelectInputContainer>
{VariablePicker && ( {VariablePicker && !readonly && (
<VariablePicker <VariablePicker
inputId={inputId} inputId={inputId}
onVariableSelect={handleVariableTagInsert} onVariableSelect={handleVariableTagInsert}

View File

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

View File

@ -26,17 +26,21 @@ type FormSelectFieldInputProps = {
VariablePicker?: VariablePickerComponent; VariablePicker?: VariablePickerComponent;
options: SelectOption[]; options: SelectOption[];
clearLabel?: string; clearLabel?: string;
readonly?: boolean;
}; };
const StyledDisplayModeContainer = styled.button` const StyledDisplayModeReadonlyContainer = styled.div`
width: 100%;
align-items: center; align-items: center;
display: flex;
cursor: pointer;
border: none;
background: transparent; background: transparent;
border: none;
display: flex;
font-family: inherit; font-family: inherit;
padding-inline: ${({ theme }) => theme.spacing(2)}; padding-inline: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const StyledDisplayModeContainer = styled(StyledDisplayModeReadonlyContainer)`
cursor: pointer;
&:hover, &:hover,
&[data-open='true'] { &[data-open='true'] {
@ -57,6 +61,7 @@ export const FormSelectFieldInput = ({
VariablePicker, VariablePicker,
options, options,
clearLabel, clearLabel,
readonly,
}: FormSelectFieldInputProps) => { }: FormSelectFieldInputProps) => {
const inputId = useId(); const inputId = useId();
@ -213,32 +218,42 @@ export const FormSelectFieldInput = ({
hasRightElement={isDefined(VariablePicker)} hasRightElement={isDefined(VariablePicker)}
> >
{draftValue.type === 'static' ? ( {draftValue.type === 'static' ? (
<> readonly ? (
<StyledDisplayModeReadonlyContainer>
{isDefined(selectedOption) && (
<SelectDisplay
color={selectedOption.color ?? 'transparent'}
label={selectedOption.label}
Icon={selectedOption.icon ?? undefined}
/>
)}
</StyledDisplayModeReadonlyContainer>
) : (
<StyledDisplayModeContainer <StyledDisplayModeContainer
data-open={draftValue.editingMode === 'edit'} data-open={draftValue.editingMode === 'edit'}
onClick={handleDisplayModeClick} onClick={handleDisplayModeClick}
> >
<VisibilityHidden>Edit</VisibilityHidden> <VisibilityHidden>Edit</VisibilityHidden>
{isDefined(selectedOption) ? ( {isDefined(selectedOption) && (
<SelectDisplay <SelectDisplay
color={selectedOption.color ?? 'transparent'} color={selectedOption.color ?? 'transparent'}
label={selectedOption.label} label={selectedOption.label}
Icon={selectedOption.icon ?? undefined} Icon={selectedOption.icon ?? undefined}
isUsedInForm
/> />
) : null} )}
</StyledDisplayModeContainer> </StyledDisplayModeContainer>
</> )
) : ( ) : (
<VariableChip <VariableChip
rawVariableName={draftValue.value} rawVariableName={draftValue.value}
onRemove={handleUnlinkVariable} onRemove={readonly ? undefined : handleUnlinkVariable}
/> />
)} )}
</FormFieldInputInputContainer> </FormFieldInputInputContainer>
<StyledSelectInputContainer> <StyledSelectInputContainer>
{draftValue.type === 'static' && {!readonly &&
draftValue.type === 'static' &&
draftValue.editingMode === 'edit' && ( draftValue.editingMode === 'edit' && (
<OverlayContainer> <OverlayContainer>
<SelectInput <SelectInput
@ -258,7 +273,7 @@ export const FormSelectFieldInput = ({
)} )}
</StyledSelectInputContainer> </StyledSelectInputContainer>
{VariablePicker && ( {VariablePicker && !readonly && (
<VariablePicker <VariablePicker
inputId={inputId} inputId={inputId}
onVariableSelect={handleVariableTagInsert} onVariableSelect={handleVariableTagInsert}

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test'; import { expect, userEvent, within } from '@storybook/test';
import { FormBooleanFieldInput } from '../FormBooleanFieldInput'; import { FormBooleanFieldInput } from '../FormBooleanFieldInput';
const meta: Meta<typeof FormBooleanFieldInput> = { const meta: Meta<typeof FormBooleanFieldInput> = {
@ -54,3 +54,37 @@ export const FalseByDefault: Story = {
await canvas.findByText('False'); 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 { 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> = { const meta: Meta<typeof FormCurrencyFieldInput> = {
title: 'UI/Data/Field/Form/Input/FormCurrencyFieldInput', title: 'UI/Data/Field/Form/Input/FormCurrencyFieldInput',
@ -31,3 +31,57 @@ export const Default: Story = {
await canvas.findByText('Amount Micros'); 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 { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate';
import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate'; import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate';
import { parseDateToString } from '@/ui/input/components/internal/date/utils/parseDateToString'; import { parseDateToString } from '@/ui/input/components/internal/date/utils/parseDateToString';
import { expect } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { import {
expect,
fn, fn,
userEvent, userEvent,
waitFor, waitFor,
@ -323,7 +323,9 @@ export const SwitchesToStandaloneVariable: Story = {
const variableTag = await canvas.findByText('test'); const variableTag = await canvas.findByText('test');
expect(variableTag).toBeVisible(); expect(variableTag).toBeVisible();
const removeVariableButton = canvas.getByTestId(/^remove-icon/); const removeVariableButton = canvasElement.querySelector(
'button .tabler-icon-x',
);
await Promise.all([ await Promise.all([
userEvent.click(removeVariableButton), userEvent.click(removeVariableButton),
@ -372,3 +374,33 @@ export const ClickingOutsideDoesNotResetInputState: Story = {
expect(input).toHaveDisplayValue(defaultValueAsDisplayString.slice(0, -2)); 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'); const variableTag = await canvas.findByText('test');
expect(variableTag).toBeVisible(); expect(variableTag).toBeVisible();
const removeVariableButton = canvas.getByTestId(/^remove-icon/); const removeVariableButton = canvasElement.querySelector(
'button .tabler-icon-x',
);
await Promise.all([ await Promise.all([
userEvent.click(removeVariableButton), userEvent.click(removeVariableButton),
@ -401,3 +403,35 @@ export const ClickingOutsideDoesNotResetInputState: Story = {
expect(input).toHaveDisplayValue(defaultValueAsDisplayString.slice(0, -2)); 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 { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test'; import { fn, userEvent, within } from '@storybook/test';
import { FormMultiSelectFieldInput } from '../FormMultiSelectFieldInput'; import { FormMultiSelectFieldInput } from '../FormMultiSelectFieldInput';
const meta: Meta<typeof FormMultiSelectFieldInput> = { const meta: Meta<typeof FormMultiSelectFieldInput> = {
@ -48,3 +49,87 @@ export const Default: Story = {
await canvas.findByText('Work Policy 2'); 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 { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test'; import { within } from '@storybook/test';
import { FormNumberFieldInput } from '../FormNumberFieldInput'; import { FormNumberFieldInput } from '../FormNumberFieldInput';
@ -36,3 +37,36 @@ export const WithLabel: Story = {
await canvas.findByPlaceholderText('Number field...'); 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 { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test'; import { expect, fn, userEvent, within } from '@storybook/test';
import { FormTextFieldInput } from '../FormTextFieldInput'; import { FormTextFieldInput } from '../FormTextFieldInput';
const meta: Meta<typeof FormTextFieldInput> = { const meta: Meta<typeof FormTextFieldInput> = {
@ -43,3 +43,47 @@ export const Multiline: Story = {
await canvas.findByText(/^Text$/); 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([ await Promise.all([
userEvent.click(removeVariableButton), userEvent.click(removeVariableButton),

View File

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

View File

@ -1,15 +1,18 @@
import { useEffect, useState } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useEffect, useState } from 'react';
import { BooleanDisplay } from '@/ui/field/display/components/BooleanDisplay'; import { BooleanDisplay } from '@/ui/field/display/components/BooleanDisplay';
const StyledEditableBooleanFieldContainer = styled.div` const StyledEditableBooleanFieldContainer = styled.div<{ readonly?: boolean }>`
align-items: center; align-items: center;
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'default')}; cursor: ${({ onClick }) => (onClick ? 'pointer' : 'default')};
display: flex; display: flex;
height: 100%; height: 100%;
width: 100%; width: 100%;
color: ${({ theme, readonly }) =>
readonly ? theme.font.color.tertiary : theme.font.color.primary};
`; `;
type BooleanInputProps = { type BooleanInputProps = {
@ -39,6 +42,7 @@ export const BooleanInput = ({
return ( return (
<StyledEditableBooleanFieldContainer <StyledEditableBooleanFieldContainer
onClick={readonly ? undefined : handleClick} onClick={readonly ? undefined : handleClick}
readonly={readonly}
data-testid={testId} data-testid={testId}
> >
<BooleanDisplay value={internalValue} /> <BooleanDisplay value={internalValue} />

View File

@ -9,6 +9,10 @@ export const StyledTextInput = styled.input`
margin: 0; margin: 0;
${TEXT_INPUT_STYLE} ${TEXT_INPUT_STYLE}
width: 100%; width: 100%;
&:disabled {
color: ${({ theme }) => theme.font.color.tertiary};
}
`; `;
type TextInputProps = { type TextInputProps = {
@ -25,6 +29,7 @@ type TextInputProps = {
onChange?: (newText: string) => void; onChange?: (newText: string) => void;
copyButton?: boolean; copyButton?: boolean;
shouldTrim?: boolean; shouldTrim?: boolean;
disabled?: boolean;
}; };
const getValue = (value: string, shouldTrim: boolean) => { const getValue = (value: string, shouldTrim: boolean) => {
@ -49,6 +54,7 @@ export const TextInput = ({
onChange, onChange,
copyButton = true, copyButton = true,
shouldTrim = true, shouldTrim = true,
disabled,
}: TextInputProps) => { }: TextInputProps) => {
const [internalText, setInternalText] = useState(value); const [internalText, setInternalText] = useState(value);
@ -85,6 +91,7 @@ export const TextInput = ({
onChange={handleChange} onChange={handleChange}
autoFocus={autoFocus} autoFocus={autoFocus}
value={internalText} value={internalText}
disabled={disabled}
/> />
{copyButton && ( {copyButton && (
<div ref={copyRef}> <div ref={copyRef}>

View File

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

View File

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