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:
Vishnu
2025-05-27 21:01:54 +05:30
committed by GitHub
parent 13d13144b7
commit 651ad38e79
5 changed files with 169 additions and 10 deletions

View File

@ -132,8 +132,9 @@ export const MultiItemFieldInput = <T,>({
};
const handleSubmitInput = () => {
const sanitizedInput = inputValue.trim();
if (validateInput !== undefined) {
const validationData = validateInput(inputValue) ?? { isValid: true };
const validationData = validateInput(sanitizedInput) ?? { isValid: true };
if (!validationData.isValid) {
onError?.(true, items);
setErrorData(validationData);
@ -141,18 +142,18 @@ export const MultiItemFieldInput = <T,>({
}
}
if (inputValue === '' && isAddingNewItem) {
if (sanitizedInput === '' && isAddingNewItem) {
return;
}
if (inputValue === '' && !isAddingNewItem) {
if (sanitizedInput === '' && !isAddingNewItem) {
handleDeleteItem(itemToEditIndex);
return;
}
const newItem = formatInput
? formatInput(inputValue)
: (inputValue as unknown as T);
? formatInput(sanitizedInput)
: (sanitizedInput as unknown as T);
if (!isAddingNewItem && newItem === items[itemToEditIndex]) {
setIsInputDisplayed(false);

View File

@ -1,6 +1,6 @@
import { expect } from '@storybook/jest';
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 { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
@ -125,3 +125,39 @@ export const Default: Story = {
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);
},
};

View File

@ -1,6 +1,6 @@
import { expect } from '@storybook/jest';
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 { 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 = {
args: {
value: {

View File

@ -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 = {
args: {
value: {

View File

@ -1,6 +1,6 @@
import { expect } from '@storybook/jest';
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 { 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 = {
args: {
value: {