Add UUID form field input (#9121)

This commit is contained in:
Baptiste Devessier 2024-12-18 14:26:35 +01:00 committed by GitHub
parent ae044c2fc8
commit b6508cc615
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 347 additions and 0 deletions

View File

@ -9,6 +9,7 @@ import { FormNumberFieldInput } from '@/object-record/record-field/form-types/co
import { FormRawJsonFieldInput } from '@/object-record/record-field/form-types/components/FormRawJsonFieldInput'; import { FormRawJsonFieldInput } from '@/object-record/record-field/form-types/components/FormRawJsonFieldInput';
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 { 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 {
@ -30,6 +31,7 @@ import { isFieldNumber } from '@/object-record/record-field/types/guards/isField
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson'; import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
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 { JsonValue } from 'type-fest'; import { JsonValue } from 'type-fest';
type FormFieldInputProps = { type FormFieldInputProps = {
@ -128,5 +130,13 @@ export const FormFieldInput = ({
placeholder={field.label} placeholder={field.label}
VariablePicker={VariablePicker} VariablePicker={VariablePicker}
/> />
) : isFieldUuid(field) ? (
<FormUuidFieldInput
label={field.label}
defaultValue={defaultValue as string | null | undefined}
onPersist={onPersist}
placeholder={field.label}
VariablePicker={VariablePicker}
/>
) : null; ) : null;
}; };

View File

@ -0,0 +1,125 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { TextInput } from '@/ui/field/input/components/TextInput';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import styled from '@emotion/styled';
import { useId, useState } from 'react';
import { isDefined } from 'twenty-ui';
const StyledInput = styled(TextInput)`
padding: ${({ theme }) => `${theme.spacing(1)} ${theme.spacing(2)}`};
`;
type FormUuidFieldInputProps = {
label?: string;
defaultValue: string | null | undefined;
placeholder: string;
onPersist: (value: string | null) => void;
readonly?: boolean;
VariablePicker?: VariablePickerComponent;
};
export const FormUuidFieldInput = ({
label,
defaultValue,
placeholder,
onPersist,
VariablePicker,
}: FormUuidFieldInputProps) => {
const inputId = useId();
const [draftValue, setDraftValue] = useState<
| {
type: 'static';
value: string;
}
| {
type: 'variable';
value: string;
}
>(
isStandaloneVariableString(defaultValue)
? {
type: 'variable',
value: defaultValue,
}
: {
type: 'static',
value: isDefined(defaultValue) ? String(defaultValue) : '',
},
);
const handleChange = (newText: string) => {
setDraftValue({
type: 'static',
value: newText,
});
const trimmedNewText = newText.trim();
if (trimmedNewText === '') {
onPersist(null);
return;
}
onPersist(trimmedNewText);
};
const handleUnlinkVariable = () => {
setDraftValue({
type: 'static',
value: '',
});
onPersist(null);
};
const handleVariableTagInsert = (variableName: string) => {
setDraftValue({
type: 'variable',
value: variableName,
});
onPersist(variableName);
};
return (
<FormFieldInputContainer>
{label ? <InputLabel htmlFor={inputId}>{label}</InputLabel> : null}
<FormFieldInputRowContainer>
<FormFieldInputInputContainer
hasRightElement={isDefined(VariablePicker)}
>
{draftValue.type === 'static' ? (
<StyledInput
inputId={inputId}
placeholder={placeholder}
value={draftValue.value}
copyButton={false}
hotkeyScope="record-create"
onChange={handleChange}
/>
) : (
<VariableChip
rawVariableName={draftValue.value}
onRemove={handleUnlinkVariable}
/>
)}
</FormFieldInputInputContainer>
{VariablePicker ? (
<VariablePicker
inputId={inputId}
onVariableSelect={handleVariableTagInsert}
/>
) : null}
</FormFieldInputRowContainer>
</FormFieldInputContainer>
);
};

View File

@ -0,0 +1,212 @@
import { expect } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import {
fn,
userEvent,
waitFor,
waitForElementToBeRemoved,
within,
} from '@storybook/test';
import { FormUuidFieldInput } from '../FormUuidFieldInput';
const meta: Meta<typeof FormUuidFieldInput> = {
title: 'UI/Data/Field/Form/Input/FormUuidFieldInput',
component: FormUuidFieldInput,
args: {},
argTypes: {},
};
export default meta;
type Story = StoryObj<typeof FormUuidFieldInput>;
export const Default: Story = {
args: {
label: 'UUID field',
placeholder: 'Enter UUID',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('UUID field');
},
};
export const SetUuidWithDashes: Story = {
args: {
label: 'UUID field',
placeholder: 'Enter UUID',
onPersist: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const uuid = 'fc50139a-9047-467e-a313-700fd75700ac';
const input = await canvas.findByPlaceholderText('Enter UUID');
expect(input).toBeVisible();
await userEvent.type(input, uuid);
await waitFor(() => {
expect(args.onPersist).toHaveBeenCalledWith(uuid);
});
},
};
export const SetUuidWithoutDashes: Story = {
args: {
label: 'UUID field',
placeholder: 'Enter UUID',
onPersist: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const uuid = 'fc50139a9047467ea313700fd75700ac';
const input = await canvas.findByPlaceholderText('Enter UUID');
expect(input).toBeVisible();
await userEvent.type(input, uuid);
await waitFor(() => {
expect(args.onPersist).toHaveBeenCalledWith(uuid);
});
},
};
export const SetInvalidUuidWithNoValidation: Story = {
args: {
label: 'UUID field',
placeholder: 'Enter UUID',
onPersist: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const uuid = 'invalid';
const input = await canvas.findByPlaceholderText('Enter UUID');
expect(input).toBeVisible();
await userEvent.type(input, uuid);
await waitFor(() => {
expect(args.onPersist).toHaveBeenCalledWith(uuid);
});
},
};
export const TrimInputBeforePersisting: Story = {
args: {
label: 'UUID field',
placeholder: 'Enter UUID',
onPersist: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const uuid = 'fc50139a9047467ea313700fd75700ac';
const input = await canvas.findByPlaceholderText('Enter UUID');
expect(input).toBeVisible();
await userEvent.type(input, `{Space>2}${uuid}{Space>3}`);
await waitFor(() => {
expect(args.onPersist).toHaveBeenCalledWith(uuid);
});
},
};
export const ClearField: Story = {
args: {
label: 'UUID field',
placeholder: 'Enter UUID',
onPersist: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const input = await canvas.findByPlaceholderText('Enter UUID');
expect(input).toBeVisible();
const uuid = 'test';
await userEvent.type(input, uuid);
await waitFor(() => {
expect(args.onPersist).toHaveBeenCalledWith(uuid);
});
await Promise.all([
userEvent.clear(input),
waitFor(() => {
expect(args.onPersist).toHaveBeenCalledWith(null);
}),
]);
},
};
export const ReplaceStaticValueWithVariable: Story = {
args: {
label: 'UUID field',
placeholder: 'Enter UUID',
onPersist: fn(),
VariablePicker: ({ onVariableSelect }) => {
return (
<button
onClick={() => {
onVariableSelect('{{test}}');
}}
>
Add variable
</button>
);
},
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const input = await canvas.findByPlaceholderText('Enter UUID');
expect(input).toBeVisible();
expect(input).toHaveDisplayValue('');
const addVariableButton = await canvas.findByRole('button', {
name: 'Add variable',
});
const [, , variableTag] = await Promise.all([
userEvent.click(addVariableButton),
waitForElementToBeRemoved(input),
waitFor(() => {
const variableTag = canvas.getByText('test');
expect(variableTag).toBeVisible();
return variableTag;
}),
waitFor(() => {
expect(args.onPersist).toHaveBeenCalledWith('{{test}}');
}),
]);
const removeVariableButton = await canvas.findByTestId(/^remove-icon/);
await Promise.all([
userEvent.click(removeVariableButton),
waitForElementToBeRemoved(variableTag),
waitFor(() => {
const input = canvas.getByPlaceholderText('Enter UUID');
expect(input).toBeVisible();
}),
waitFor(() => {
expect(args.onPersist).toHaveBeenCalledWith(null);
}),
]);
},
};