mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-23 03:51:36 +03:00
Add UUID form field input (#9121)
This commit is contained in:
parent
ae044c2fc8
commit
b6508cc615
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user