feat: trim empty space (#12293)
Trim trailing spaces of the input, instead of manually trimming, thus improving the user experience. Fixes #12279 ### Screencast [Screencast from 2025-05-26 21-03-54.webm](https://github.com/user-attachments/assets/cc40be5a-d260-4a20-bbc8-c0b21ddbbd9b) --------- Co-authored-by: Devessier <baptiste@devessier.fr>
This commit is contained in:
@ -132,8 +132,9 @@ export const MultiItemFieldInput = <T,>({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmitInput = () => {
|
const handleSubmitInput = () => {
|
||||||
|
const sanitizedInput = inputValue.trim();
|
||||||
if (validateInput !== undefined) {
|
if (validateInput !== undefined) {
|
||||||
const validationData = validateInput(inputValue) ?? { isValid: true };
|
const validationData = validateInput(sanitizedInput) ?? { isValid: true };
|
||||||
if (!validationData.isValid) {
|
if (!validationData.isValid) {
|
||||||
onError?.(true, items);
|
onError?.(true, items);
|
||||||
setErrorData(validationData);
|
setErrorData(validationData);
|
||||||
@ -141,18 +142,18 @@ export const MultiItemFieldInput = <T,>({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inputValue === '' && isAddingNewItem) {
|
if (sanitizedInput === '' && isAddingNewItem) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inputValue === '' && !isAddingNewItem) {
|
if (sanitizedInput === '' && !isAddingNewItem) {
|
||||||
handleDeleteItem(itemToEditIndex);
|
handleDeleteItem(itemToEditIndex);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newItem = formatInput
|
const newItem = formatInput
|
||||||
? formatInput(inputValue)
|
? formatInput(sanitizedInput)
|
||||||
: (inputValue as unknown as T);
|
: (sanitizedInput as unknown as T);
|
||||||
|
|
||||||
if (!isAddingNewItem && newItem === items[itemToEditIndex]) {
|
if (!isAddingNewItem && newItem === items[itemToEditIndex]) {
|
||||||
setIsInputDisplayed(false);
|
setIsInputDisplayed(false);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { expect } from '@storybook/jest';
|
import { expect } from '@storybook/jest';
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import { fn, userEvent, within } from '@storybook/test';
|
import { fn, userEvent, waitFor, within } from '@storybook/test';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||||
@ -125,3 +125,39 @@ export const Default: Story = {
|
|||||||
expect(tag3Element).toBeVisible();
|
expect(tag3Element).toBeVisible();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const TrimInput: Story = {
|
||||||
|
args: {
|
||||||
|
value: ['tag1', 'tag2'],
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const addButton = await canvas.findByText('Add Item');
|
||||||
|
await userEvent.click(addButton);
|
||||||
|
|
||||||
|
const input = await canvas.findByPlaceholderText('Enter value');
|
||||||
|
await userEvent.type(input, ' tag2 {enter}');
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const tag2Elements = canvas.queryAllByText('tag2');
|
||||||
|
expect(tag2Elements).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(updateRecord).toHaveBeenCalledWith({
|
||||||
|
variables: {
|
||||||
|
where: { id: 'record-id' },
|
||||||
|
updateOneRecordInput: {
|
||||||
|
tags: [
|
||||||
|
'tag1',
|
||||||
|
'tag2',
|
||||||
|
'tag2', // The second tag2 is not trimmed, so it remains as a duplicate
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(updateRecord).toHaveBeenCalledTimes(1);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { expect } from '@storybook/jest';
|
import { expect } from '@storybook/jest';
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import { fn, userEvent, within } from '@storybook/test';
|
import { fn, userEvent, waitFor, within } from '@storybook/test';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing';
|
import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing';
|
||||||
|
|
||||||
@ -131,7 +131,42 @@ export const Default: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// FIXME: We will have to fix that behavior, we should only be able to set a secondary email as the primary email
|
export const TrimInput: Story = {
|
||||||
|
args: {
|
||||||
|
value: {
|
||||||
|
primaryEmail: 'john@example.com',
|
||||||
|
additionalEmails: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const addButton = await canvas.findByText('Add Email');
|
||||||
|
await userEvent.click(addButton);
|
||||||
|
|
||||||
|
const input = await canvas.findByPlaceholderText('Email');
|
||||||
|
await userEvent.type(input, ' new.email@example.com {enter}');
|
||||||
|
|
||||||
|
const newEmailElement = await canvas.findByText('new.email@example.com');
|
||||||
|
expect(newEmailElement).toBeVisible();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(updateRecord).toHaveBeenCalledWith({
|
||||||
|
variables: {
|
||||||
|
where: { id: 'record-id' },
|
||||||
|
updateOneRecordInput: {
|
||||||
|
emails: {
|
||||||
|
primaryEmail: 'john@example.com',
|
||||||
|
additionalEmails: ['new.email@example.com'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(updateRecord).toHaveBeenCalledTimes(1);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const CanNotSetPrimaryLinkAsPrimaryLink: Story = {
|
export const CanNotSetPrimaryLinkAsPrimaryLink: Story = {
|
||||||
args: {
|
args: {
|
||||||
value: {
|
value: {
|
||||||
|
|||||||
@ -253,6 +253,36 @@ export const CreatePrimaryLink: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const TrimInput: Story = {
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const input = await canvas.findByPlaceholderText('URL');
|
||||||
|
await userEvent.type(input, ' https://www.twenty.com {enter}');
|
||||||
|
|
||||||
|
const linkDisplay = await canvas.findByText('twenty.com');
|
||||||
|
expect(linkDisplay).toBeVisible();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(updateRecord).toHaveBeenCalledWith({
|
||||||
|
variables: {
|
||||||
|
where: { id: '123' },
|
||||||
|
updateOneRecordInput: {
|
||||||
|
links: {
|
||||||
|
primaryLinkUrl: 'https://www.twenty.com',
|
||||||
|
primaryLinkLabel: null,
|
||||||
|
secondaryLinks: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(updateRecord).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
expect(getPrimaryLinkBookmarkIcon(canvasElement)).not.toBeInTheDocument();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const AddSecondaryLink: Story = {
|
export const AddSecondaryLink: Story = {
|
||||||
args: {
|
args: {
|
||||||
value: {
|
value: {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { expect } from '@storybook/jest';
|
import { expect } from '@storybook/jest';
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import { fn, userEvent, within } from '@storybook/test';
|
import { fn, userEvent, waitFor, within } from '@storybook/test';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing';
|
import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing';
|
||||||
|
|
||||||
@ -140,7 +140,64 @@ export const Default: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// FIXME: We will have to fix that behavior, we should only be able to set an additional phone as the primary phone
|
export const TrimInput: Story = {
|
||||||
|
args: {
|
||||||
|
value: {
|
||||||
|
primaryPhoneCountryCode: 'FR',
|
||||||
|
primaryPhoneNumber: '642646272',
|
||||||
|
primaryPhoneCallingCode: '+33',
|
||||||
|
additionalPhones: [
|
||||||
|
{
|
||||||
|
countryCode: 'FR',
|
||||||
|
number: '642646273',
|
||||||
|
callingCode: '+33',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const addButton = await canvas.findByText('Add Phone');
|
||||||
|
await userEvent.click(addButton);
|
||||||
|
|
||||||
|
const input = await canvas.findByPlaceholderText('Phone');
|
||||||
|
await userEvent.type(input, '+33642646274 {enter}');
|
||||||
|
|
||||||
|
const newPhoneElement = await canvas.findByText('+33 6 42 64 62 74');
|
||||||
|
expect(newPhoneElement).toBeVisible();
|
||||||
|
|
||||||
|
// Verify the update was called with swapped phones
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(updateRecord).toHaveBeenCalledWith({
|
||||||
|
variables: {
|
||||||
|
where: { id: 'record-id' },
|
||||||
|
updateOneRecordInput: {
|
||||||
|
phones: {
|
||||||
|
primaryPhoneCallingCode: '+33',
|
||||||
|
primaryPhoneCountryCode: 'FR',
|
||||||
|
primaryPhoneNumber: '642646272',
|
||||||
|
additionalPhones: [
|
||||||
|
{
|
||||||
|
countryCode: 'FR',
|
||||||
|
number: '642646273',
|
||||||
|
callingCode: '+33',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
countryCode: 'FR',
|
||||||
|
number: '642646274',
|
||||||
|
callingCode: '+33',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(updateRecord).toHaveBeenCalledTimes(1);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const CanNotSetPrimaryLinkAsPrimaryLink: Story = {
|
export const CanNotSetPrimaryLinkAsPrimaryLink: Story = {
|
||||||
args: {
|
args: {
|
||||||
value: {
|
value: {
|
||||||
|
|||||||
Reference in New Issue
Block a user