mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-22 19:41:53 +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 { 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 { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
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 { 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 { JsonValue } from 'type-fest';
|
||||
|
||||
type FormFieldInputProps = {
|
||||
@ -128,5 +130,13 @@ export const FormFieldInput = ({
|
||||
placeholder={field.label}
|
||||
VariablePicker={VariablePicker}
|
||||
/>
|
||||
) : isFieldUuid(field) ? (
|
||||
<FormUuidFieldInput
|
||||
label={field.label}
|
||||
defaultValue={defaultValue as string | null | undefined}
|
||||
onPersist={onPersist}
|
||||
placeholder={field.label}
|
||||
VariablePicker={VariablePicker}
|
||||
/>
|
||||
) : 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