From 3a1c94147f35074aad6fa445a07c61804adb223b Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Tue, 12 Aug 2025 10:42:13 +0200 Subject: [PATCH] Refactor usePersistField (#13775) This PR introduces refactors and tradeoffs in the API around the events of field input. # Refactored usePersistField The hook `usePersistField` has been refactored to be used anywhere in the app, not just inside a FieldContext. This was meant to solve a bug at the beginning but now it is just used once in `RecordDetailRelationSection` outside of the context, still this is better to have this hook like that for future use cases. We also introduce `usePersistFieldFromFieldInputContext`, for an easier API inside a FieldContext. # Introduced a new `FieldInputEventContext` To remove the drill-down of events, we introduce `FieldInputEventContext`, this allows to set only once the handlers / events. In practice it allows to have an easier time maintaining the events for the many different field inputs, because it matches the pattern we already use of taking everything from a context (`FieldContext`). # Removed drill-down from FieldInput The heavy drill-down in FieldInput has been completely removed, since everything can be derived from `FieldContext` and `FieldInputEventContext`. Also there was some readonly and other specific props, but they were all drilled down from FieldContext, so it was easier to just use FieldContext where needed. # Refactored events of `MultiItemFieldInputProps` The component `MultiItemFieldInputProps` has a contrived API, here we just remove the complex part that was persisting from inside. We now only give a classic API with `onChange` and `onEscape` the rest is left to higher levels, where it should be, because this generic component shouldn't be aware of persisting things. # Extracted the parsing logic of persisted values For each input field component, we now have a clear util that was before bound to the persist call, # Tradeoff with persist times The tradeoff before was that persistField was called anywhere, before exiting the component sometimes, now it is only called by the higher levels like table or show page, which handles this abstraction. This could be challenged, however I think that having a lot of different events, and not just `handleSubmit` and `handleCancel`, convey enough meaning for the higher levels to decide what to do in each case. A `skipPersist` argument was added in events in the rare edge cases where we want to voluntarily skip persisting even with a submit or escape, but that could be challenged because we could also say that we should use cancel for that and stick to that convention. # Handling of the bug in `ActivityRichTextEditor` Initially this refactor was prioritized for solving this bug, which was very annoying for the users. But while fixing it with the new persistField hook I just understood that the problem is not just for record title cells but for anything that is open when we click on a rich text editor. The issue is described here : https://github.com/twentyhq/core-team-issues/issues/1317 So right now I just let it as is. # Stories The stories were checking that a request was sent in some cases where persist was called before a component exiting, now that persist is only called by higher-levels I just removed those tests from the stories, because that should be the responsibility of higher levels. Also a helper `getFieldInputEventContextProviderWithJestMocks` was created that exposes a context and jest mock functions for testing this new API in stories. # Miscellaneous Deactivated tui with nx by default, because it can be annoying. --- nx.json | 191 ++++++++++++--- packages/twenty-front/jest.config.ts | 6 +- .../components/ActivityRichTextEditor.tsx | 18 +- .../record-field/components/FieldInput.tsx | 139 ++--------- .../contexts/FieldInputEventContext.ts | 28 +++ .../hooks/__tests__/usePersistField.test.tsx | 230 ------------------ .../record-field/hooks/usePersistField.ts | 61 +++-- .../usePersistFieldFromFieldInputContext.ts | 32 +++ .../record-field/hooks/useRecordFieldInput.ts | 14 +- .../meta-types/hooks/useAddressField.ts | 13 - .../meta-types/hooks/useArrayField.ts | 22 +- .../meta-types/hooks/useCurrencyField.ts | 28 --- .../meta-types/hooks/useEmailsField.ts | 13 - .../meta-types/hooks/useFullNameField.ts | 13 - .../meta-types/hooks/useJsonField.ts | 14 -- .../meta-types/hooks/useLinksField.ts | 13 - .../meta-types/hooks/useMultiSelectField.ts | 3 - .../meta-types/hooks/useNumberField.ts | 31 --- .../meta-types/hooks/usePhonesField.ts | 13 - .../meta-types/hooks/useRichTextField.ts | 69 ------ .../meta-types/hooks/useRichTextV2Field.ts | 74 ------ .../meta-types/hooks/useSelectField.ts | 3 - .../input/components/AddressFieldInput.tsx | 39 +-- .../input/components/ArrayFieldInput.tsx | 51 ++-- .../input/components/BooleanFieldInput.tsx | 21 +- .../input/components/CurrencyFieldInput.tsx | 86 ++++--- .../input/components/DateFieldInput.tsx | 44 ++-- .../input/components/DateTimeFieldInput.tsx | 44 ++-- .../input/components/EmailsFieldInput.tsx | 57 +++-- .../input/components/FullNameFieldInput.tsx | 41 +--- .../input/components/LinksFieldInput.tsx | 52 ++-- .../input/components/MultiItemFieldInput.tsx | 39 +-- .../components/MultiSelectFieldInput.tsx | 28 ++- .../input/components/NumberFieldInput.tsx | 89 +++++-- .../input/components/PhonesFieldInput.tsx | 50 ++-- .../input/components/RatingFieldInput.tsx | 27 +- .../input/components/RawJsonFieldInput.tsx | 69 +++--- .../components/RelationFromManyFieldInput.tsx | 14 +- .../components/RelationToOneFieldInput.tsx | 18 +- .../input/components/RichTextFieldInput.tsx | 48 ++-- .../input/components/SelectFieldInput.tsx | 21 +- .../input/components/TextFieldInput.tsx | 42 ++-- .../__stories__/ArrayFieldInput.stories.tsx | 57 +---- .../__stories__/BooleanFieldInput.stories.tsx | 42 ++-- .../DateTimeFieldInput.stories.tsx | 76 +++--- .../__stories__/EmailsFieldInput.stories.tsx | 55 ++--- .../__stories__/LinksFieldInput.stories.tsx | 177 ++------------ .../__stories__/NumberFieldInput.stories.tsx | 84 +++---- .../__stories__/PhonesFieldInput.stories.tsx | 72 ++---- .../__stories__/RatingFieldInput.stories.tsx | 28 +-- .../RelationToOneFieldInput.stories.tsx | 45 ++-- .../RichTextFieldInput.stories.tsx | 50 ++-- .../__stories__/TextFieldInput.stories.tsx | 80 +++--- ...InputEventContextProviderWithJestMocks.tsx | 46 ++++ .../record-field/types/FieldInputEvent.ts | 6 - .../components/RecordInlineCell.tsx | 107 +++++--- .../record-show/components/FieldsCard.tsx | 2 +- .../RecordDetailRelationRecordsListItem.tsx | 8 +- .../RecordDetailRelationSection.tsx | 82 +++++-- ...cordDetailRelationSectionDropdownToOne.tsx | 6 +- .../components/RecordTableCellFieldInput.tsx | 79 +++--- .../components/RecordTitleCell.tsx | 85 ++++--- .../components/RecordTitleCellFieldInput.tsx | 28 +-- .../RecordTitleCellTextFieldInput.tsx | 33 +-- .../RecordTitleFullNameFieldInput.tsx | 37 +-- .../SettingsDataModelFieldPreview.tsx | 10 +- 66 files changed, 1325 insertions(+), 1878 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-field/contexts/FieldInputEventContext.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistFieldFromFieldInputContext.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextField.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextV2Field.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks.tsx delete mode 100644 packages/twenty-front/src/modules/object-record/record-field/types/FieldInputEvent.ts diff --git a/nx.json b/nx.json index 695d3acc3f8..e1cd347e778 100644 --- a/nx.json +++ b/nx.json @@ -1,7 +1,12 @@ { - "workspaceLayout": { "appsDir": "packages", "libsDir": "packages" }, + "workspaceLayout": { + "appsDir": "packages", + "libsDir": "packages" + }, "namedInputs": { - "default": ["{projectRoot}/**/*"], + "default": [ + "{projectRoot}/**/*" + ], "excludeStories": [ "default", "!{projectRoot}/.storybook/*", @@ -29,24 +34,42 @@ "targetDefaults": { "build": { "cache": true, - "inputs": ["^production", "production"], - "dependsOn": ["^build"] + "inputs": [ + "^production", + "production" + ], + "dependsOn": [ + "^build" + ] + }, + "start": { + "cache": true, + "dependsOn": [ + "^build" + ] }, - "start": { "cache": true, "dependsOn": ["^build"] }, "lint": { "executor": "@nx/eslint:lint", "cache": true, - "outputs": ["{options.outputFile}"], + "outputs": [ + "{options.outputFile}" + ], "options": { "eslintConfig": "{projectRoot}/eslint.config.js", "cache": true, "cacheLocation": "{workspaceRoot}/.cache/eslint" }, "configurations": { - "ci": { "cacheStrategy": "content" }, - "fix": { "fix": true } + "ci": { + "cacheStrategy": "content" + }, + "fix": { + "fix": true + } }, - "dependsOn": ["^build"] + "dependsOn": [ + "^build" + ] }, "fmt": { "executor": "nx:run-commands", @@ -60,10 +83,16 @@ "write": false }, "configurations": { - "ci": { "cacheStrategy": "content" }, - "fix": { "write": true } + "ci": { + "cacheStrategy": "content" + }, + "fix": { + "write": true + } }, - "dependsOn": ["^build"] + "dependsOn": [ + "^build" + ] }, "typecheck": { "executor": "nx:run-commands", @@ -72,49 +101,85 @@ "cwd": "{projectRoot}", "command": "tsc -b tsconfig.json --incremental" }, - "configurations": { "watch": { "watch": true } }, - "dependsOn": ["^build"] + "configurations": { + "watch": { + "watch": true + } + }, + "dependsOn": [ + "^build" + ] }, "test": { "executor": "@nx/jest:jest", "cache": true, - "dependsOn": ["^build"], + "dependsOn": [ + "^build" + ], "inputs": [ "^default", "excludeStories", "{workspaceRoot}/jest.preset.js" ], - "outputs": ["{projectRoot}/coverage"], + "outputs": [ + "{projectRoot}/coverage" + ], "options": { "jestConfig": "{projectRoot}/jest.config.ts", "coverage": true, - "coverageReporters": ["text-summary"], + "coverageReporters": [ + "text-summary" + ], "cacheDirectory": "../../.cache/jest/{projectRoot}" }, "configurations": { - "ci": { "ci": true, "maxWorkers": 3 }, - "coverage": { "coverageReporters": ["lcov", "text"] }, - "watch": { "watch": true } + "ci": { + "ci": true, + "maxWorkers": 3 + }, + "coverage": { + "coverageReporters": [ + "lcov", + "text" + ] + }, + "watch": { + "watch": true + } } }, - "test:e2e": { "cache": true, "dependsOn": ["^build"] }, + "test:e2e": { + "cache": true, + "dependsOn": [ + "^build" + ] + }, "storybook:build": { "executor": "nx:run-commands", "cache": true, - "inputs": ["^default", "excludeTests"], - "outputs": ["{projectRoot}/{options.output-dir}"], + "inputs": [ + "^default", + "excludeTests" + ], + "outputs": [ + "{projectRoot}/{options.output-dir}" + ], "options": { "cwd": "{projectRoot}", "command": "VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true storybook build --test", "output-dir": "storybook-static", "config-dir": ".storybook" }, - "dependsOn": ["^build"] + "dependsOn": [ + "^build" + ] }, "storybook:serve:dev": { "executor": "nx:run-commands", "cache": true, - "dependsOn": ["^build"], + "dependsOn": [ + "^build" + ], "options": { "cwd": "{projectRoot}", "command": "storybook dev", @@ -123,7 +188,9 @@ }, "storybook:serve:static": { "executor": "nx:run-commands", - "dependsOn": ["storybook:build"], + "dependsOn": [ + "storybook:build" + ], "options": { "cwd": "{projectRoot}", "command": "npx http-server {args.staticDir} -a={args.host} --port={args.port} --silent={args.silent}", @@ -136,8 +203,13 @@ "storybook:test": { "executor": "nx:run-commands", "cache": true, - "inputs": ["^default", "excludeTests"], - "outputs": ["{projectRoot}/coverage/storybook"], + "inputs": [ + "^default", + "excludeTests" + ], + "outputs": [ + "{projectRoot}/coverage/storybook" + ], "options": { "cwd": "{projectRoot}", "commands": [ @@ -153,7 +225,10 @@ }, "storybook:test:no-coverage": { "executor": "nx:run-commands", - "inputs": ["^default", "excludeTests"], + "inputs": [ + "^default", + "excludeTests" + ], "options": { "cwd": "{projectRoot}", "commands": [ @@ -180,7 +255,11 @@ "reporter": "lcov", "checkCoverage": true }, - "configurations": { "text": { "reporter": "text" } } + "configurations": { + "text": { + "reporter": "text" + } + } }, "storybook:serve-and-test:static": { "executor": "nx:run-commands", @@ -206,7 +285,11 @@ ], "parallel": false }, - "configurations": { "ci": { "ci": "--exit-zero-on-changes" } } + "configurations": { + "ci": { + "ci": "--exit-zero-on-changes" + } + } }, "@nx/jest:jest": { "cache": true, @@ -215,8 +298,15 @@ "excludeStories", "{workspaceRoot}/jest.preset.js" ], - "options": { "passWithNoTests": true }, - "configurations": { "ci": { "ci": true, "codeCoverage": true } } + "options": { + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } }, "@nx/eslint:lint": { "cache": true, @@ -226,14 +316,27 @@ "{workspaceRoot}/tools/eslint-rules/**/*" ] }, - "@nx/vite:test": { "cache": true, "inputs": ["default", "^default"] }, + "@nx/vite:test": { + "cache": true, + "inputs": [ + "default", + "^default" + ] + }, "@nx/vite:build": { "cache": true, - "dependsOn": ["^build"], - "inputs": ["default", "^default"] + "dependsOn": [ + "^build" + ], + "inputs": [ + "default", + "^default" + ] } }, - "installation": { "version": "21.3.11" }, + "installation": { + "version": "21.3.11" + }, "generators": { "@nx/react": { "application": { @@ -252,12 +355,20 @@ "unitTestRunner": "jest", "projectNameAndRootFormat": "derived" }, - "component": { "style": "@emotion/styled" } + "component": { + "style": "@emotion/styled" + } } }, "tasksRunnerOptions": { - "default": { "options": { "cacheableOperations": ["storybook:build"] } } + "default": { + "options": { + "cacheableOperations": [ + "storybook:build" + ] + } + } }, "useInferencePlugins": false, "defaultBase": "main" -} +} \ No newline at end of file diff --git a/packages/twenty-front/jest.config.ts b/packages/twenty-front/jest.config.ts index 1581529801e..6ec83c4d5f6 100644 --- a/packages/twenty-front/jest.config.ts +++ b/packages/twenty-front/jest.config.ts @@ -1,6 +1,6 @@ import { readFileSync } from 'fs'; import { dirname, resolve } from 'path'; -import { type JestConfigWithTsJest, pathsToModuleNameMapper } from 'ts-jest'; +import { pathsToModuleNameMapper, type JestConfigWithTsJest } from 'ts-jest'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); @@ -58,8 +58,8 @@ const jestConfig: JestConfigWithTsJest = { extensionsToTreatAsEsm: ['.ts', '.tsx'], coverageThreshold: { global: { - statements: 56, - lines: 55, + statements: 55, + lines: 54, functions: 45, }, }, diff --git a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx index f006e756669..c052c3dfd0d 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx @@ -31,6 +31,7 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useIsRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordFieldReadOnly'; import { isInlineCellInEditModeFamilyState } from '@/object-record/record-inline-cell/states/isInlineCellInEditModeFamilyState'; import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData'; +import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType'; import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId'; import { BlockEditor } from '@/ui/input/editor/components/BlockEditor'; import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; @@ -369,13 +370,26 @@ export const ActivityRichTextEditor = ({ const recordTitleCellId = getRecordFieldInputInstanceId({ recordId: activityId, - fieldName: labelIdentifierFieldMetadataItem?.id, - prefix: 'activity-rich-text-editor', + fieldName: labelIdentifierFieldMetadataItem?.name, + // TODO: see comments below, this is a very temporary fix, + // it won't work for the breadcrumb title input, but that's ok for now. + prefix: RecordTitleCellContainerType.ShowPage, }); + // TODO: Here instead of closing the input, as it was intially planned, we should block if there is anything open, + // This information should be derived from the focus stack + // The problem with this library is that it takes the focus before anything else and does not prevent the event from bubbling + // Because of this, other events listen at the same time, and when we're in luck, the click outside gets triggered, + // but this leaves the door open for unpredicted behavior with click handlers conflicts, + // we recently had a bug which was deleting what the user typed and closed the right drawer if he used backspace key. + // We could maybe use the types of components in the focus stack. const handleBlockEditorFocus = useRecoilCallback( ({ snapshot }) => () => { + // TODO: Here we want to detect anything that is open to avoid conflicts with the library click event + // that is not prevented and propagate to other click handlers in the app. + // Because that is how we do in the app, for example with stacked dropdowns, we always close what's open before + // letting the click being captured by a button or input that can capture it. const isRecordTitleCellOpen = snapshot .getLoadable(isInlineCellInEditModeFamilyState(recordTitleCellId)) .getValue(); diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx index 37e8cf35e88..294304e6671 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx @@ -13,7 +13,6 @@ import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/ import { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones'; import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects'; -import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { ArrayFieldInput } from '@/object-record/record-field/meta-types/input/components/ArrayFieldInput'; import { RichTextFieldInput } from '@/object-record/record-field/meta-types/input/components/RichTextFieldInput'; import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress'; @@ -40,147 +39,51 @@ import { NumberFieldInput } from '../meta-types/input/components/NumberFieldInpu import { RatingFieldInput } from '../meta-types/input/components/RatingFieldInput'; import { RelationToOneFieldInput } from '../meta-types/input/components/RelationToOneFieldInput'; import { TextFieldInput } from '../meta-types/input/components/TextFieldInput'; -import { type FieldInputEvent } from '../types/FieldInputEvent'; -import { type FieldRichTextV2Metadata } from '../types/FieldMetadata'; import { isFieldText } from '../types/guards/isFieldText'; -type FieldInputProps = { - onSubmit?: FieldInputEvent; - onCancel?: () => void; - onClickOutside?: ( - persist: () => void, - event: MouseEvent | TouchEvent, - ) => void; - onEnter?: FieldInputEvent; - onEscape?: FieldInputEvent; - onTab?: FieldInputEvent; - onShiftTab?: FieldInputEvent; - isReadOnly?: boolean; -}; - -export const FieldInput = ({ - onCancel, - onSubmit, - onEnter, - onEscape, - onShiftTab, - onTab, - onClickOutside, - isReadOnly, -}: FieldInputProps) => { - const { fieldDefinition, recordId } = useContext(FieldContext); +export const FieldInput = () => { + const { fieldDefinition } = useContext(FieldContext); return ( <> {isFieldRelationToOneObject(fieldDefinition) ? ( - + ) : isFieldRelationFromManyObjects(fieldDefinition) ? ( - + ) : isFieldPhones(fieldDefinition) ? ( - + ) : isFieldText(fieldDefinition) ? ( - + ) : isFieldEmails(fieldDefinition) ? ( - onClickOutside?.(() => {}, event)} - /> + ) : isFieldFullName(fieldDefinition) ? ( - + ) : isFieldDateTime(fieldDefinition) ? ( - + ) : isFieldDate(fieldDefinition) ? ( - + ) : isFieldNumber(fieldDefinition) ? ( - + ) : isFieldLinks(fieldDefinition) ? ( - onClickOutside?.(() => {}, event)} - /> + ) : isFieldCurrency(fieldDefinition) ? ( - + ) : isFieldBoolean(fieldDefinition) ? ( - + ) : isFieldRating(fieldDefinition) ? ( - + ) : isFieldSelect(fieldDefinition) ? ( - + ) : isFieldMultiSelect(fieldDefinition) ? ( - + ) : isFieldAddress(fieldDefinition) ? ( - + ) : isFieldRawJson(fieldDefinition) ? ( - + ) : isFieldArray(fieldDefinition) ? ( - onClickOutside?.(() => {}, event)} - /> + ) : isFieldRichTextV2(fieldDefinition) ? ( - + ) : ( <> )} diff --git a/packages/twenty-front/src/modules/object-record/record-field/contexts/FieldInputEventContext.ts b/packages/twenty-front/src/modules/object-record/record-field/contexts/FieldInputEventContext.ts new file mode 100644 index 00000000000..383730a291d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/contexts/FieldInputEventContext.ts @@ -0,0 +1,28 @@ +import { createContext } from 'react'; + +export type FieldInputEventArgs = { + newValue?: unknown; + skipPersist?: boolean; +}; + +export type FieldInputEvent = (args: FieldInputEventArgs) => void; + +export type FieldInputClickOutsideEvent = (args: { + newValue?: unknown; + skipPersist?: boolean; + event?: MouseEvent | TouchEvent; +}) => void; + +export type FieldInputEventContextType = { + onEnter?: FieldInputEvent; + onCancel?: () => void; + onEscape?: FieldInputEvent; + onSubmit?: FieldInputEvent; + onTab?: FieldInputEvent; + onShiftTab?: FieldInputEvent; + onClickOutside?: FieldInputClickOutsideEvent; +}; + +export const FieldInputEventContext = createContext( + {} as FieldInputEventContextType, +); diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx deleted file mode 100644 index cabbf769184..00000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments'; -import { responseData } from '@/object-record/hooks/__mocks__/useUpdateOneRecord'; -import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; -import { - phonesFieldDefinition, - relationFieldDefinition, -} from '@/object-record/record-field/__mocks__/fieldDefinitions'; -import { - FieldContext, - type RecordUpdateHook, - type RecordUpdateHookParams, -} from '@/object-record/record-field/contexts/FieldContext'; -import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; -import { type FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; -import { type FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; -import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; -import { gql } from '@apollo/client'; -import { type MockedResponse } from '@apollo/client/testing'; -import { renderHook, waitFor } from '@testing-library/react'; -import { type ReactNode, act } from 'react'; -import { useRecoilValue } from 'recoil'; -import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; - -const query = gql` - mutation UpdateOnePerson($idToUpdate: UUID!, $input: PersonUpdateInput!) { - updatePerson(id: $idToUpdate, data: $input) { - ${PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS} - } - } -`; - -const phoneMock = { - primaryPhoneNumber: '123 456', - primaryPhoneCountryCode: 'US', - primaryPhoneCallingCode: '+1', - additionalPhones: [], -}; - -const mockPersonResponse = { ...responseData, id: 'recordId' }; - -const mocks: MockedResponse[] = [ - { - request: { - query, - variables: { - idToUpdate: 'recordId', - input: { - phones: phoneMock, - }, - }, - }, - result: jest.fn(() => ({ - data: { - updatePerson: mockPersonResponse, - }, - })), - }, - { - request: { - query, - variables: { - idToUpdate: 'recordId', - input: { companyId: 'companyId' }, - }, - }, - result: jest.fn(() => ({ - data: { - updatePerson: mockPersonResponse, - }, - })), - }, -]; - -const recordId = 'recordId'; - -const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({ - apolloMocks: mocks, -}); - -const getWrapper = - (fieldDefinition: FieldDefinition) => - ({ children }: { children: ReactNode }) => { - const useUpdateOneRecordMutation: RecordUpdateHook = () => { - const { updateOneRecord } = useUpdateOneRecord({ - objectNameSingular: CoreObjectNameSingular.Person, - }); - - const updateEntity = ({ variables }: RecordUpdateHookParams) => { - updateOneRecord?.({ - idToUpdate: variables.where.id as string, - updateOneRecordInput: variables.updateOneRecordInput, - }); - }; - - return [updateEntity, { loading: false }]; - }; - - return ( - - - {children} - - - ); - }; - -const PhoneWrapper = getWrapper(phonesFieldDefinition); -const RelationWrapper = getWrapper(relationFieldDefinition); - -describe('usePersistField', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should work as expected', async () => { - const { result } = renderHook( - () => { - const entityFields = useRecoilValue( - recordStoreFamilySelector({ recordId, fieldName: 'phones' }), - ); - - return { - persistField: usePersistField(), - entityFields, - }; - }, - { wrapper: PhoneWrapper }, - ); - - act(() => { - result.current.persistField({ - primaryPhoneNumber: '123 456', - primaryPhoneCountryCode: 'US', - primaryPhoneCallingCode: '+1', - additionalPhones: [], - }); - }); - - await waitFor(() => { - expect(mocks[0].result).toHaveBeenCalled(); - }); - }); - - it('should persist relation field', async () => { - const { result } = renderHook( - () => { - const entityFields = useRecoilValue( - recordStoreFamilySelector({ - recordId, - fieldName: 'company', - }), - ); - - return { - persistField: usePersistField(), - entityFields, - }; - }, - { wrapper: RelationWrapper }, - ); - - act(() => { - result.current.persistField({ id: 'companyId' }); - }); - - await waitFor(() => { - expect(mocks[1].result).toHaveBeenCalled(); - }); - }); - - it('should skip persistence value has not changed', async () => { - const { result } = renderHook( - () => { - const entityFields = useRecoilValue( - recordStoreFamilySelector({ recordId, fieldName: 'phones' }), - ); - - return { - persistField: usePersistField(), - entityFields, - }; - }, - { wrapper: PhoneWrapper }, - ); - - act(() => { - result.current.persistField(phoneMock); - }); - - await waitFor(() => { - expect(mocks[0].result).not.toHaveBeenCalled(); - }); - }); - - it('should skip persistence when relation field value has not changed', async () => { - const { result } = renderHook( - () => { - const entityFields = useRecoilValue( - recordStoreFamilySelector({ - recordId, - fieldName: 'company', - }), - ); - - return { - persistField: usePersistField(), - entityFields, - }; - }, - { wrapper: RelationWrapper }, - ); - - act(() => { - result.current.persistField({ id: 'companyId' }); - }); - - await waitFor(() => { - expect(mocks[1].result).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts index ed1377d3b6f..e9b38e44b9d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts @@ -1,8 +1,10 @@ -import { useContext } from 'react'; import { useRecoilCallback } from 'recoil'; import { type FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; -import { type FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata'; +import { + type FieldMetadata, + type FieldRelationMetadata, +} from '@/object-record/record-field/types/FieldMetadata'; import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress'; import { isFieldAddressValue } from '@/object-record/record-field/types/guards/isFieldAddressValue'; import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate'; @@ -25,6 +27,8 @@ import { isFieldSelect } from '@/object-record/record-field/types/guards/isField import { isFieldSelectValue } from '@/object-record/record-field/types/guards/isFieldSelectValue'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; +import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; +import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { isWorkflowRunJsonField } from '@/object-record/record-field/meta-types/utils/isWorkflowRunJsonField'; import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray'; import { isFieldArrayValue } from '@/object-record/record-field/types/guards/isFieldArrayValue'; @@ -34,7 +38,6 @@ import { isFieldRichTextValue } from '@/object-record/record-field/types/guards/ import { isFieldRichTextV2Value } from '@/object-record/record-field/types/guards/isFieldRichTextValueV2'; import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; -import { FieldContext } from '../contexts/FieldContext'; import { isFieldBoolean } from '../types/guards/isFieldBoolean'; import { isFieldBooleanValue } from '../types/guards/isFieldBooleanValue'; import { isFieldCurrency } from '../types/guards/isFieldCurrency'; @@ -48,18 +51,30 @@ import { isFieldRatingValue } from '../types/guards/isFieldRatingValue'; import { isFieldText } from '../types/guards/isFieldText'; import { isFieldTextValue } from '../types/guards/isFieldTextValue'; -export const usePersistField = () => { - const { - recordId, - fieldDefinition, - useUpdateRecord = () => [], - } = useContext(FieldContext); +export const usePersistField = ({ + objectMetadataItemId, +}: { + objectMetadataItemId: string; +}) => { + const { objectMetadataItem } = useObjectMetadataItemById({ + objectId: objectMetadataItemId, + }); - const [updateRecord] = useUpdateRecord(); + const { updateOneRecord } = useUpdateOneRecord({ + objectNameSingular: objectMetadataItem?.nameSingular ?? '', + }); const persistField = useRecoilCallback( ({ set, snapshot }) => - (valueToPersist: unknown) => { + ({ + recordId, + fieldDefinition, + valueToPersist, + }: { + recordId: string; + fieldDefinition: FieldDefinition; + valueToPersist: unknown; + }) => { const fieldIsRelationToOneObject = isFieldRelationToOneObject( fieldDefinition as FieldDefinition, @@ -183,24 +198,20 @@ export const usePersistField = () => { ); if (fieldIsRelationToOneObject) { - updateRecord?.({ - variables: { - where: { id: recordId }, - updateOneRecordInput: { - [getForeignKeyNameFromRelationFieldName(fieldName)]: - valueToPersist?.id ?? null, - }, + updateOneRecord?.({ + idToUpdate: recordId, + updateOneRecordInput: { + [getForeignKeyNameFromRelationFieldName(fieldName)]: + valueToPersist?.id ?? null, }, }); return; } - updateRecord?.({ - variables: { - where: { id: recordId }, - updateOneRecordInput: { - [fieldName]: valueToPersist, - }, + updateOneRecord?.({ + idToUpdate: recordId, + updateOneRecordInput: { + [fieldName]: valueToPersist, }, }); } else { @@ -213,7 +224,7 @@ export const usePersistField = () => { ); } }, - [recordId, fieldDefinition, updateRecord], + [updateOneRecord], ); return persistField; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistFieldFromFieldInputContext.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistFieldFromFieldInputContext.ts new file mode 100644 index 00000000000..e2cf9e812e9 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistFieldFromFieldInputContext.ts @@ -0,0 +1,32 @@ +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; +import { useContext } from 'react'; + +export const usePersistFieldFromFieldInputContext = () => { + const { fieldDefinition, recordId } = useContext(FieldContext); + + const { objectMetadataItems } = useObjectMetadataItems(); + + const objectMetadataItem = objectMetadataItems.find( + (objectMetadataItemToFind) => + objectMetadataItemToFind.fields.some( + (fieldMetadataItemToFind) => + fieldMetadataItemToFind.id === fieldDefinition.fieldMetadataId, + ), + ); + + const persistField = usePersistField({ + objectMetadataItemId: objectMetadataItem?.id ?? '', + }); + + const persistFieldFromFieldInputContext = (valueToPersist: unknown) => { + persistField({ + recordId, + fieldDefinition, + valueToPersist, + }); + }; + + return { persistFieldFromFieldInputContext }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useRecordFieldInput.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useRecordFieldInput.ts index 2f555e8d726..a7aa011689d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useRecordFieldInput.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useRecordFieldInput.ts @@ -1,10 +1,18 @@ import { recordFieldInputDraftValueComponentState } from '@/object-record/record-field/states/recordFieldInputDraftValueComponentState'; import { type FieldInputDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue'; -import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { useRecoilCallback } from 'recoil'; export const useRecordFieldInput = () => { - const setDraftValue = useSetRecoilComponentState( - recordFieldInputDraftValueComponentState, + const recordFieldInputDraftValueCallbackState = + useRecoilComponentCallbackState(recordFieldInputDraftValueComponentState); + + const setDraftValue = useRecoilCallback( + ({ set }) => + (newValue: unknown) => { + set(recordFieldInputDraftValueCallbackState, newValue); + }, + [recordFieldInputDraftValueCallbackState], ); const isDraftValueEmpty = ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useAddressField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useAddressField.ts index eab63f7d49b..89c36cfd985 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useAddressField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useAddressField.ts @@ -2,14 +2,12 @@ import { useContext } from 'react'; import { useRecoilState } from 'recoil'; import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput'; -import { isFieldAddressValue } from '@/object-record/record-field/types/guards/isFieldAddressValue'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { recordFieldInputDraftValueComponentState } from '@/object-record/record-field/states/recordFieldInputDraftValueComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { FieldContext } from '../../contexts/FieldContext'; -import { usePersistField } from '../../hooks/usePersistField'; import { type FieldAddressValue } from '../../types/FieldMetadata'; import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata'; import { isFieldAddress } from '../../types/guards/isFieldAddress'; @@ -32,16 +30,6 @@ export const useAddressField = () => { }), ); - const persistField = usePersistField(); - - const persistAddressField = (newValue: FieldAddressValue) => { - if (!isFieldAddressValue(newValue)) { - return; - } - - persistField(newValue); - }; - const { setDraftValue } = useRecordFieldInput(); const draftValue = useRecoilComponentValue( @@ -54,6 +42,5 @@ export const useAddressField = () => { setFieldValue, draftValue, setDraftValue, - persistAddressField, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useArrayField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useArrayField.ts index 73f3a9268e6..ae5f68b6eb7 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useArrayField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useArrayField.ts @@ -1,10 +1,11 @@ import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; -import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; +import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput'; +import { recordFieldInputDraftValueComponentState } from '@/object-record/record-field/states/recordFieldInputDraftValueComponentState'; import { type FieldArrayValue } from '@/object-record/record-field/types/FieldMetadata'; import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata'; import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray'; -import { arraySchema } from '@/object-record/record-field/types/guards/isFieldArrayValue'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useContext } from 'react'; import { useRecoilState } from 'recoil'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -23,22 +24,17 @@ export const useArrayField = () => { }), ); - const persistField = usePersistField(); + const { setDraftValue } = useRecordFieldInput(); - const persistArrayField = (nextValue: string[]) => { - if (!nextValue) persistField(null); - - try { - persistField(arraySchema.parse(nextValue)); - } catch { - return; - } - }; + const draftValue = useRecoilComponentValue( + recordFieldInputDraftValueComponentState, + ); return { fieldValue, fieldDefinition, setFieldValue, - persistArrayField, + draftValue, + setDraftValue, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useCurrencyField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useCurrencyField.ts index c65b3868587..155f1717645 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useCurrencyField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useCurrencyField.ts @@ -2,17 +2,14 @@ import { useContext } from 'react'; import { useRecoilState } from 'recoil'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; -import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput'; import { recordFieldInputDraftValueComponentState } from '@/object-record/record-field/states/recordFieldInputDraftValueComponentState'; import { type FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata'; import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata'; import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFieldCurrency'; -import { isFieldCurrencyValue } from '@/object-record/record-field/types/guards/isFieldCurrencyValue'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { FieldMetadataType } from '~/generated-metadata/graphql'; -import { convertCurrencyAmountToCurrencyMicros } from '~/utils/convertCurrencyToCurrencyMicros'; export const useCurrencyField = () => { const { recordId, fieldDefinition } = useContext(FieldContext); @@ -32,30 +29,6 @@ export const useCurrencyField = () => { }), ); - const persistField = usePersistField(); - - const persistCurrencyField = ({ - amountText, - currencyCode, - }: { - amountText: string; - currencyCode: string; - }) => { - const amount = parseFloat(amountText); - - const newCurrencyValue = { - amountMicros: isNaN(amount) - ? null - : convertCurrencyAmountToCurrencyMicros(amount), - currencyCode, - }; - - if (!isFieldCurrencyValue(newCurrencyValue)) { - return; - } - persistField(newCurrencyValue); - }; - const { setDraftValue } = useRecordFieldInput(); const draftValue = useRecoilComponentValue( @@ -70,7 +43,6 @@ export const useCurrencyField = () => { draftValue, setDraftValue, setFieldValue, - persistCurrencyField, defaultValue, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useEmailsField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useEmailsField.ts index 675a4548a87..f4ea3553ef8 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useEmailsField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useEmailsField.ts @@ -1,11 +1,9 @@ import { useContext } from 'react'; import { useRecoilState } from 'recoil'; -import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput'; import { type FieldEmailsValue } from '@/object-record/record-field/types/FieldMetadata'; import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails'; -import { emailsSchema } from '@/object-record/record-field/types/guards/isFieldEmailsValue'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -34,22 +32,11 @@ export const useEmailsField = () => { recordFieldInputDraftValueComponentState, ); - const persistField = usePersistField(); - - const persistEmailsField = (nextValue: FieldEmailsValue) => { - try { - persistField(emailsSchema.parse(nextValue)); - } catch { - return; - } - }; - return { fieldDefinition, fieldValue, draftValue, setDraftValue, setFieldValue, - persistEmailsField, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useFullNameField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useFullNameField.ts index 89b1479d7c8..615f2887578 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useFullNameField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useFullNameField.ts @@ -2,13 +2,11 @@ import { useContext } from 'react'; import { useRecoilState } from 'recoil'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; -import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput'; import { recordFieldInputDraftValueComponentState } from '@/object-record/record-field/states/recordFieldInputDraftValueComponentState'; import { type FieldFullNameValue } from '@/object-record/record-field/types/FieldMetadata'; import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata'; import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName'; -import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -31,16 +29,6 @@ export const useFullNameField = () => { }), ); - const persistField = usePersistField(); - - const persistFullNameField = (newValue: FieldFullNameValue) => { - if (!isFieldFullNameValue(newValue)) { - return; - } - - persistField(newValue); - }; - const { setDraftValue } = useRecordFieldInput(); const draftValue = useRecoilComponentValue( @@ -53,6 +41,5 @@ export const useFullNameField = () => { setFieldValue, draftValue, setDraftValue, - persistFullNameField, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useJsonField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useJsonField.ts index d238c2a976a..41f63a1716d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useJsonField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useJsonField.ts @@ -1,7 +1,6 @@ import { useContext } from 'react'; import { useRecoilState } from 'recoil'; -import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput'; import { type FieldJsonValue } from '@/object-record/record-field/types/FieldMetadata'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; @@ -32,18 +31,6 @@ export const useJsonField = () => { }), ); - const persistField = usePersistField(); - - const persistJsonField = (nextValue: string) => { - if (!nextValue) persistField(null); - - try { - persistField(JSON.parse(nextValue)); - } catch { - // Do nothing - } - }; - const { setDraftValue } = useRecordFieldInput(); const draftValue = useRecoilComponentValue( @@ -62,6 +49,5 @@ export const useJsonField = () => { fieldDefinition, fieldValue, setFieldValue, - persistJsonField, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useLinksField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useLinksField.ts index 4f6d1c3997d..3cfb7d7446d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useLinksField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useLinksField.ts @@ -1,11 +1,9 @@ import { useContext } from 'react'; import { useRecoilState } from 'recoil'; -import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput'; import { type FieldLinksValue } from '@/object-record/record-field/types/FieldMetadata'; import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks'; -import { linksSchema } from '@/object-record/record-field/types/guards/isFieldLinksValue'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -34,22 +32,11 @@ export const useLinksField = () => { recordFieldInputDraftValueComponentState, ); - const persistField = usePersistField(); - - const persistLinksField = (nextValue: FieldLinksValue) => { - try { - persistField(linksSchema.parse(nextValue)); - } catch { - return; - } - }; - return { fieldDefinition, fieldValue, draftValue, setDraftValue, setFieldValue, - persistLinksField, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useMultiSelectField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useMultiSelectField.ts index 9e4eed5192e..d06ea3ee103 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useMultiSelectField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useMultiSelectField.ts @@ -2,7 +2,6 @@ import { useContext } from 'react'; import { useRecoilState } from 'recoil'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; -import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput'; import { recordFieldInputDraftValueComponentState } from '@/object-record/record-field/states/recordFieldInputDraftValueComponentState'; import { type FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata'; @@ -34,7 +33,6 @@ export const useMultiSelectField = () => { const fieldMultiSelectValues = isFieldMultiSelectValue(fieldValues) ? fieldValues : null; - const persistField = usePersistField(); const { setDraftValue } = useRecordFieldInput(); @@ -45,7 +43,6 @@ export const useMultiSelectField = () => { return { recordId, fieldDefinition, - persistField, fieldValues: fieldMultiSelectValues, draftValue, setDraftValue, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useNumberField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useNumberField.ts index b68be9a938f..fae0991254b 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useNumberField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useNumberField.ts @@ -6,16 +6,9 @@ import { type FieldNumberValue } from '@/object-record/record-field/types/FieldM import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { FieldMetadataType } from '~/generated-metadata/graphql'; -import { - canBeCastAsNumberOrNull, - castAsNumberOrNull, -} from '~/utils/cast-as-number-or-null'; - import { recordFieldInputDraftValueComponentState } from '@/object-record/record-field/states/recordFieldInputDraftValueComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { isNull } from '@sniptt/guards'; import { FieldContext } from '../../contexts/FieldContext'; -import { usePersistField } from '../../hooks/usePersistField'; import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata'; import { isFieldNumber } from '../../types/guards/isFieldNumber'; @@ -33,29 +26,6 @@ export const useNumberField = () => { }), ); - const persistField = usePersistField(); - - const persistNumberField = (newValue: string) => { - if (fieldDefinition?.metadata?.settings?.type === 'percentage') { - const newValueEscaped = newValue.replaceAll('%', ''); - if (!canBeCastAsNumberOrNull(newValueEscaped)) { - return; - } - const castedValue = castAsNumberOrNull(newValue); - if (!isNull(castedValue)) { - persistField(castedValue / 100); - return; - } - persistField(null); - return; - } - if (!canBeCastAsNumberOrNull(newValue)) { - return; - } - const castedValue = castAsNumberOrNull(newValue); - persistField(castedValue); - }; - const { setDraftValue } = useRecordFieldInput(); const draftValue = useRecoilComponentValue( @@ -68,6 +38,5 @@ export const useNumberField = () => { draftValue, setDraftValue, setFieldValue, - persistNumberField, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/usePhonesField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/usePhonesField.ts index 9625ccbfdc1..ab3e40f4e74 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/usePhonesField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/usePhonesField.ts @@ -1,11 +1,9 @@ import { useContext } from 'react'; import { useRecoilState } from 'recoil'; -import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput'; import { type FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata'; import { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones'; -import { phonesSchema } from '@/object-record/record-field/types/guards/isFieldPhonesValue'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -34,22 +32,11 @@ export const usePhonesField = () => { recordFieldInputDraftValueComponentState, ); - const persistField = usePersistField(); - - const persistPhonesField = (nextValue: FieldPhonesValue) => { - try { - persistField(phonesSchema.parse(nextValue)); - } catch { - return; - } - }; - return { fieldDefinition, fieldValue, draftValue, setDraftValue, setFieldValue, - persistPhonesField, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextField.ts deleted file mode 100644 index b32ee965a7d..00000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextField.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { useContext } from 'react'; -import { useRecoilState } from 'recoil'; - -import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput'; -import { type FieldRichTextValue } from '@/object-record/record-field/types/FieldMetadata'; -import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; - -import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; -import { recordFieldInputDraftValueComponentState } from '@/object-record/record-field/states/recordFieldInputDraftValueComponentState'; -import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText'; -import { isFieldRichTextValue } from '@/object-record/record-field/types/guards/isFieldRichTextValue'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import type { PartialBlock } from '@blocknote/core'; -import { isNonEmptyString } from '@sniptt/guards'; -import { FieldContext } from '../../contexts/FieldContext'; -import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata'; - -export const useRichTextField = () => { - const { recordId, fieldDefinition, maxWidth } = useContext(FieldContext); - - assertFieldMetadata( - FieldMetadataType.RICH_TEXT, - isFieldRichText, - fieldDefinition, - ); - - const fieldName = fieldDefinition.metadata.fieldName; - - const [fieldValue, setFieldValue] = useRecoilState( - recordStoreFamilySelector({ - recordId, - fieldName: fieldName, - }), - ); - const fieldRichTextValue = isFieldRichTextValue(fieldValue) ? fieldValue : ''; - - const { setDraftValue } = useRecordFieldInput(); - - const draftValue = useRecoilComponentValue( - recordFieldInputDraftValueComponentState, - ); - - const draftValueParsed: PartialBlock[] = isNonEmptyString(draftValue) - ? JSON.parse(draftValue) - : draftValue; - - const persistField = usePersistField(); - - const persistRichTextField = (nextValue: PartialBlock[]) => { - if (!nextValue) { - persistField(null); - } else { - const parsedValueToPersist = JSON.stringify(nextValue); - - persistField(parsedValueToPersist); - } - }; - - return { - draftValue: draftValueParsed, - setDraftValue, - maxWidth, - fieldDefinition, - fieldValue: fieldRichTextValue, - setFieldValue, - persistRichTextField, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextV2Field.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextV2Field.ts deleted file mode 100644 index 5d7d9641297..00000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextV2Field.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { useContext } from 'react'; -import { useRecoilState } from 'recoil'; - -import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput'; -import { - type FieldRichTextV2Value, - type FieldRichTextValue, -} from '@/object-record/record-field/types/FieldMetadata'; -import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; - -import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; -import { recordFieldInputDraftValueComponentState } from '@/object-record/record-field/states/recordFieldInputDraftValueComponentState'; -import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2'; -import { isFieldRichTextV2Value } from '@/object-record/record-field/types/guards/isFieldRichTextValueV2'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import type { PartialBlock } from '@blocknote/core'; -import { isNonEmptyString } from '@sniptt/guards'; -import { FieldContext } from '../../contexts/FieldContext'; -import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata'; - -export const useRichTextV2Field = () => { - const { recordId, fieldDefinition, maxWidth } = useContext(FieldContext); - - assertFieldMetadata( - FieldMetadataType.RICH_TEXT_V2, - isFieldRichTextV2, - fieldDefinition, - ); - - const fieldName = fieldDefinition.metadata.fieldName; - - const [fieldValue, setFieldValue] = useRecoilState( - recordStoreFamilySelector({ - recordId, - fieldName: fieldName, - }), - ); - const fieldRichTextV2Value = isFieldRichTextV2Value(fieldValue) - ? fieldValue - : ({ blocknote: null, markdown: null } as FieldRichTextV2Value); - - const { setDraftValue } = useRecordFieldInput(); - - const draftValue = useRecoilComponentValue( - recordFieldInputDraftValueComponentState, - ); - - const draftValueParsed: PartialBlock[] = isNonEmptyString(draftValue) - ? JSON.parse(draftValue) - : draftValue; - - const persistField = usePersistField(); - - const persistRichTextField = (nextValue: PartialBlock[]) => { - if (!nextValue) { - persistField(null); - } else { - const parsedValueToPersist = JSON.stringify(nextValue); - - persistField(parsedValueToPersist); - } - }; - - return { - draftValue: draftValueParsed, - setDraftValue, - maxWidth, - fieldDefinition, - fieldValue: fieldRichTextV2Value, - setFieldValue, - persistRichTextField, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useSelectField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useSelectField.ts index 3f313cc334e..89144c29b71 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useSelectField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useSelectField.ts @@ -1,7 +1,6 @@ import { useContext } from 'react'; import { useRecoilState } from 'recoil'; -import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -29,7 +28,6 @@ export const useSelectField = () => { ); const fieldSelectValue = isFieldSelectValue(fieldValue) ? fieldValue : null; - const persistField = usePersistField(); const { setDraftValue } = useRecordFieldInput(); @@ -40,7 +38,6 @@ export const useSelectField = () => { return { recordId, fieldDefinition, - persistField, fieldValue: fieldSelectValue, draftValue, setDraftValue, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/AddressFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/AddressFieldInput.tsx index aeeced43c85..9b13ed96591 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/AddressFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/AddressFieldInput.tsx @@ -3,31 +3,17 @@ import { type FieldAddressDraftValue } from '@/object-record/record-field/types/ import { AddressInput } from '@/ui/field/input/components/AddressInput'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; -import { - type FieldInputClickOutsideEvent, - type FieldInputEvent, -} from '@/object-record/record-field/types/FieldInputEvent'; + +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; -import { usePersistField } from '../../../hooks/usePersistField'; +import { useContext } from 'react'; -export type AddressFieldInputProps = { - onClickOutside?: FieldInputClickOutsideEvent; - onEnter?: FieldInputEvent; - onEscape?: FieldInputEvent; - onTab?: FieldInputEvent; - onShiftTab?: FieldInputEvent; -}; - -export const AddressFieldInput = ({ - onEnter, - onEscape, - onClickOutside, - onTab, - onShiftTab, -}: AddressFieldInputProps) => { +export const AddressFieldInput = () => { const { draftValue, setDraftValue, fieldDefinition } = useAddressField(); - const persistField = usePersistField(); + const { onEnter, onTab, onShiftTab, onEscape, onClickOutside } = useContext( + FieldInputEventContext, + ); const convertToAddress = ( newAddress: FieldAddressDraftValue | undefined, @@ -47,27 +33,28 @@ export const AddressFieldInput = ({ const subFields = settings && 'subFields' in settings ? settings.subFields : undefined; + const handleEnter = (newAddress: FieldAddressDraftValue) => { - onEnter?.(() => persistField(convertToAddress(newAddress))); + onEnter?.({ newValue: convertToAddress(newAddress) }); }; const handleTab = (newAddress: FieldAddressDraftValue) => { - onTab?.(() => persistField(convertToAddress(newAddress))); + onTab?.({ newValue: convertToAddress(newAddress) }); }; const handleShiftTab = (newAddress: FieldAddressDraftValue) => { - onShiftTab?.(() => persistField(convertToAddress(newAddress))); + onShiftTab?.({ newValue: convertToAddress(newAddress) }); }; const handleEscape = (newAddress: FieldAddressDraftValue) => { - onEscape?.(() => persistField(convertToAddress(newAddress))); + onEscape?.({ newValue: convertToAddress(newAddress) }); }; const handleClickOutside = ( event: MouseEvent | TouchEvent, newAddress: FieldAddressDraftValue, ) => { - onClickOutside?.(() => persistField(convertToAddress(newAddress)), event); + onClickOutside?.({ newValue: convertToAddress(newAddress), event }); }; const handleChange = (newAddress: FieldAddressDraftValue) => { diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/ArrayFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/ArrayFieldInput.tsx index 288880831ea..087e366ae3e 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/ArrayFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/ArrayFieldInput.tsx @@ -1,35 +1,50 @@ +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; import { useArrayField } from '@/object-record/record-field/meta-types/hooks/useArrayField'; import { ArrayFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/ArrayFieldMenuItem'; import { MultiItemFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiItemFieldInput'; -import { useMemo } from 'react'; +import { arraySchema } from '@/object-record/record-field/types/guards/isFieldArrayValue'; +import { useContext, useMemo } from 'react'; +import { isDefined } from 'twenty-shared/utils'; import { FieldMetadataType } from '~/generated-metadata/graphql'; -type ArrayFieldInputProps = { - onCancel?: () => void; - onClickOutside?: (event: MouseEvent | TouchEvent) => void; -}; +export const ArrayFieldInput = () => { + const { setDraftValue, draftValue, fieldDefinition } = useArrayField(); -export const ArrayFieldInput = ({ - onCancel, - onClickOutside, -}: ArrayFieldInputProps) => { - const { persistArrayField, fieldValue, fieldDefinition } = useArrayField(); + const { onEscape, onClickOutside } = useContext(FieldInputEventContext); const arrayItems = useMemo>( - () => (Array.isArray(fieldValue) ? fieldValue : []), - [fieldValue], + () => (Array.isArray(draftValue) ? draftValue : []), + [draftValue], ); + const handleChange = (newValue: any[]) => { + if (!isDefined(newValue)) setDraftValue(null); + + const parseResponse = arraySchema.safeParse(newValue); + + if (parseResponse.success) { + setDraftValue(parseResponse.data); + } + }; + + const handleClickOutside = ( + _newValue: any, + event: MouseEvent | TouchEvent, + ) => { + onClickOutside?.({ newValue: draftValue, event }); + }; + + const handleEscape = (_newValue: any) => { + onEscape?.({ newValue: draftValue }); + }; + return ( { - onClickOutside?.(event); - persist(); - }} + onChange={handleChange} + onEscape={handleEscape} + onClickOutside={handleClickOutside} placeholder="Enter value" fieldMetadataType={FieldMetadataType.ARRAY} renderItem={({ value, index, handleEdit, handleDelete }) => ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/BooleanFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/BooleanFieldInput.tsx index d4399e18e8a..ecf2e5c513f 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/BooleanFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/BooleanFieldInput.tsx @@ -1,33 +1,30 @@ import { BooleanInput } from '@/ui/field/input/components/BooleanInput'; -import { type FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent'; -import { usePersistField } from '../../../hooks/usePersistField'; +import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; +import { useContext } from 'react'; import { useBooleanField } from '../../hooks/useBooleanField'; export type BooleanFieldInputProps = { - onSubmit?: FieldInputEvent; - readonly?: boolean; testId?: string; }; -export const BooleanFieldInput = ({ - onSubmit, - readonly, - testId, -}: BooleanFieldInputProps) => { +export const BooleanFieldInput = ({ testId }: BooleanFieldInputProps) => { const { fieldValue } = useBooleanField(); - const persistField = usePersistField(); + const { isRecordFieldReadOnly } = useContext(FieldContext); + + const { onSubmit } = useContext(FieldInputEventContext); const handleToggle = (newValue: boolean) => { - onSubmit?.(() => persistField(newValue)); + onSubmit?.({ newValue }); }; return ( ); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx index 7222301fef3..970d54e24e8 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx @@ -6,31 +6,21 @@ import { CurrencyInput } from '@/ui/field/input/components/CurrencyInput'; import { useCurrencyField } from '../../hooks/useCurrencyField'; +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; -import { - type FieldInputClickOutsideEvent, - type FieldInputEvent, -} from '@/object-record/record-field/types/FieldInputEvent'; + +import { isFieldCurrencyValue } from '@/object-record/record-field/types/guards/isFieldCurrencyValue'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useContext } from 'react'; +import { convertCurrencyAmountToCurrencyMicros } from '~/utils/convertCurrencyToCurrencyMicros'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; -type CurrencyFieldInputProps = { - onClickOutside?: FieldInputClickOutsideEvent; - onEnter?: FieldInputEvent; - onEscape?: FieldInputEvent; - onTab?: FieldInputEvent; - onShiftTab?: FieldInputEvent; -}; +export const CurrencyFieldInput = () => { + const { draftValue, setDraftValue, defaultValue } = useCurrencyField(); -export const CurrencyFieldInput = ({ - onEnter, - onEscape, - onClickOutside, - onTab, - onShiftTab, -}: CurrencyFieldInputProps) => { - const { draftValue, persistCurrencyField, setDraftValue, defaultValue } = - useCurrencyField(); + const { onClickOutside, onEnter, onEscape, onShiftTab, onTab } = useContext( + FieldInputEventContext, + ); const instanceId = useAvailableComponentInstanceIdOrThrow( RecordFieldComponentInstanceContext, @@ -55,21 +45,44 @@ export const CurrencyFieldInput = ({ ? defaultCurrencyCodeWithoutSQLQuotes : CurrencyCode.USD; + const getNewCurrencyValue = ({ + amountText, + currencyCode, + }: { + amountText: string; + currencyCode: string; + }) => { + const amount = parseFloat(amountText); + + const newCurrencyValue = { + amountMicros: isNaN(amount) + ? null + : convertCurrencyAmountToCurrencyMicros(amount), + currencyCode, + }; + + if (!isFieldCurrencyValue(newCurrencyValue)) { + return; + } + + return newCurrencyValue; + }; + const handleEnter = (newValue: string) => { - onEnter?.(() => { - persistCurrencyField({ + onEnter?.({ + newValue: getNewCurrencyValue({ amountText: newValue, currencyCode, - }); + }), }); }; const handleEscape = (newValue: string) => { - onEscape?.(() => { - persistCurrencyField({ + onEscape?.({ + newValue: getNewCurrencyValue({ amountText: newValue, currencyCode, - }); + }), }); }; @@ -77,30 +90,31 @@ export const CurrencyFieldInput = ({ event: MouseEvent | TouchEvent, newValue: string, ) => { - onClickOutside?.(() => { - persistCurrencyField({ + onClickOutside?.({ + newValue: getNewCurrencyValue({ amountText: newValue, currencyCode, - }); - }, event); + }), + event, + }); }; const handleTab = (newValue: string) => { - onTab?.(() => { - persistCurrencyField({ + onTab?.({ + newValue: getNewCurrencyValue({ amountText: newValue, currencyCode, - }); + }), }); }; const handleShiftTab = (newValue: string) => { - onShiftTab?.(() => - persistCurrencyField({ + onShiftTab?.({ + newValue: getNewCurrencyValue({ amountText: newValue, currencyCode, }), - ); + }); }; const handleChange = (newValue: string) => { diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateFieldInput.tsx index 1bd49853c2c..680a70241aa 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateFieldInput.tsx @@ -1,41 +1,27 @@ import { useDateField } from '@/object-record/record-field/meta-types/hooks/useDateField'; import { DateInput } from '@/ui/field/input/components/DateInput'; +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; -import { type FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useContext } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { type Nullable } from 'twenty-ui/utilities'; -import { usePersistField } from '../../../hooks/usePersistField'; -type FieldInputEvent = (persist: () => void) => void; - -type DateFieldInputProps = { - onClickOutside?: FieldInputClickOutsideEvent; - onEnter?: FieldInputEvent; - onEscape?: FieldInputEvent; - onClear?: FieldInputEvent; - onSubmit?: FieldInputEvent; -}; - -export const DateFieldInput = ({ - onEnter, - onEscape, - onClickOutside, - onClear, - onSubmit, -}: DateFieldInputProps) => { +export const DateFieldInput = () => { const { fieldValue, setDraftValue } = useDateField(); + const { onEnter, onEscape, onClickOutside, onSubmit } = useContext( + FieldInputEventContext, + ); + const instanceId = useAvailableComponentInstanceIdOrThrow( RecordFieldComponentInstanceContext, ); - const persistField = usePersistField(); - - const persistDate = (newDate: Nullable) => { + const getDateToPersist = (newDate: Nullable) => { if (!isDefined(newDate)) { - persistField(null); + return null; } else { const newDateWithoutTime = `${newDate?.getFullYear()}-${( newDate?.getMonth() + 1 @@ -43,27 +29,27 @@ export const DateFieldInput = ({ .toString() .padStart(2, '0')}-${newDate?.getDate().toString().padStart(2, '0')}`; - persistField(newDateWithoutTime); + return newDateWithoutTime; } }; const handleEnter = (newDate: Nullable) => { - onEnter?.(() => persistDate(newDate)); + onEnter?.({ newValue: getDateToPersist(newDate) }); }; const handleSubmit = (newDate: Nullable) => { - onSubmit?.(() => persistDate(newDate)); + onSubmit?.({ newValue: getDateToPersist(newDate) }); }; const handleEscape = (newDate: Nullable) => { - onEscape?.(() => persistDate(newDate)); + onEscape?.({ newValue: getDateToPersist(newDate) }); }; const handleClickOutside = ( event: MouseEvent | TouchEvent, newDate: Nullable, ) => { - onClickOutside?.(() => persistDate(newDate), event); + onClickOutside?.({ newValue: getDateToPersist(newDate), event }); }; const handleChange = (newDate: Nullable) => { @@ -71,7 +57,7 @@ export const DateFieldInput = ({ }; const handleClear = () => { - onClear?.(() => persistDate(null)); + onSubmit?.({ newValue: null }); }; const dateValue = fieldValue ? new Date(fieldValue) : null; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateTimeFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateTimeFieldInput.tsx index 0b02ce9354e..f4fa9936cf2 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateTimeFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateTimeFieldInput.tsx @@ -1,60 +1,46 @@ import { DateInput } from '@/ui/field/input/components/DateInput'; -import { type FieldInputEvent } from '@/object-record/record-field/meta-types/input/components/NumberFieldInput'; - +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; -import { type FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useContext } from 'react'; import { type Nullable } from 'twenty-ui/utilities'; -import { usePersistField } from '../../../hooks/usePersistField'; import { useDateTimeField } from '../../hooks/useDateTimeField'; -export type DateTimeFieldInputProps = { - onClickOutside?: FieldInputClickOutsideEvent; - onEnter?: FieldInputEvent; - onEscape?: FieldInputEvent; - onClear?: FieldInputEvent; - onSubmit?: FieldInputEvent; -}; - -export const DateTimeFieldInput = ({ - onEnter, - onEscape, - onClickOutside, - onClear, - onSubmit, -}: DateTimeFieldInputProps) => { +export const DateTimeFieldInput = () => { const { fieldValue, setDraftValue } = useDateTimeField(); + const { onEnter, onEscape, onClickOutside, onSubmit } = useContext( + FieldInputEventContext, + ); + const instanceId = useAvailableComponentInstanceIdOrThrow( RecordFieldComponentInstanceContext, ); - const persistField = usePersistField(); - - const persistDate = (newDate: Nullable) => { + const getDateToPersist = (newDate: Nullable) => { if (!newDate) { - persistField(null); + return null; } else { const newDateISO = newDate?.toISOString(); - persistField(newDateISO); + return newDateISO; } }; const handleEnter = (newDate: Nullable) => { - onEnter?.(() => persistDate(newDate)); + onEnter?.({ newValue: getDateToPersist(newDate) }); }; const handleEscape = (newDate: Nullable) => { - onEscape?.(() => persistDate(newDate)); + onEscape?.({ newValue: getDateToPersist(newDate) }); }; const handleClickOutside = ( event: MouseEvent | TouchEvent, newDate: Nullable, ) => { - onClickOutside?.(() => persistDate(newDate), event); + onClickOutside?.({ newValue: getDateToPersist(newDate), event }); }; const handleChange = (newDate: Nullable) => { @@ -62,11 +48,11 @@ export const DateTimeFieldInput = ({ }; const handleClear = () => { - onClear?.(() => persistDate(null)); + onSubmit?.({ newValue: null }); }; const handleSubmit = (newDate: Nullable) => { - onSubmit?.(() => persistDate(newDate)); + onSubmit?.({ newValue: getDateToPersist(newDate) }); }; const dateValue = fieldValue ? new Date(fieldValue) : null; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx index 396673740ca..1c7c3a869b3 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx @@ -1,43 +1,46 @@ +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; import { useEmailsField } from '@/object-record/record-field/meta-types/hooks/useEmailsField'; import { EmailsFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/EmailsFieldMenuItem'; import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/states/recordFieldInputIsFieldInErrorComponentState'; +import { emailsSchema } from '@/object-record/record-field/types/guards/isFieldEmailsValue'; import { emailSchema } from '@/object-record/record-field/validation-schemas/emailSchema'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import { useLingui } from '@lingui/react/macro'; -import { useCallback, useMemo } from 'react'; +import { useCallback, useContext, useMemo } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; import { MultiItemFieldInput } from './MultiItemFieldInput'; -type EmailsFieldInputProps = { - onCancel?: () => void; - onClickOutside?: (event: MouseEvent | TouchEvent) => void; -}; - -export const EmailsFieldInput = ({ - onCancel, - onClickOutside, -}: EmailsFieldInputProps) => { - const { persistEmailsField, fieldValue } = useEmailsField(); +export const EmailsFieldInput = () => { + const { setDraftValue, draftValue } = useEmailsField(); const { copyToClipboard } = useCopyToClipboard(); const { t } = useLingui(); + const { onEscape, onClickOutside } = useContext(FieldInputEventContext); + const emails = useMemo( () => [ - fieldValue?.primaryEmail ? fieldValue?.primaryEmail : null, - ...(fieldValue?.additionalEmails ?? []), + draftValue?.primaryEmail ? draftValue?.primaryEmail : null, + ...(draftValue?.additionalEmails ?? []), ].filter(isDefined), - [fieldValue?.primaryEmail, fieldValue?.additionalEmails], + [draftValue?.primaryEmail, draftValue?.additionalEmails], ); - const handlePersistEmails = (updatedEmails: string[]) => { + const handleChange = (updatedEmails: string[]) => { const [nextPrimaryEmail, ...nextAdditionalEmails] = updatedEmails; - persistEmailsField({ + + const nextValue = { primaryEmail: nextPrimaryEmail ?? '', additionalEmails: nextAdditionalEmails, - }); + }; + + const parseResponse = emailsSchema.safeParse(nextValue); + + if (parseResponse.success) { + setDraftValue(parseResponse.data); + } }; const validateInput = useCallback( @@ -64,15 +67,23 @@ export const EmailsFieldInput = ({ copyToClipboard(email, t`Email copied to clipboard`); }; + const handleClickOutside = ( + _newValue: any, + event: MouseEvent | TouchEvent, + ) => { + onClickOutside?.({ newValue: draftValue, event }); + }; + + const handleEscape = (_newValue: any) => { + onEscape?.({ newValue: draftValue }); + }; + return ( { - onClickOutside?.(event); - persist(); - }} + onChange={handleChange} + onEscape={handleEscape} + onClickOutside={handleClickOutside} placeholder="Email" fieldMetadataType={FieldMetadataType.EMAILS} validateInput={validateInput} diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/FullNameFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/FullNameFieldInput.tsx index ef9d885e435..4fe915c38c3 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/FullNameFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/FullNameFieldInput.tsx @@ -6,29 +6,17 @@ import { FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGER import { LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/LastNamePlaceholder'; import { isDoubleTextFieldEmpty } from '@/object-record/record-field/meta-types/input/utils/isDoubleTextFieldEmpty'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; -import { - type FieldInputClickOutsideEvent, - type FieldInputEvent, -} from '@/object-record/record-field/types/FieldInputEvent'; + +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useContext } from 'react'; -type FullNameFieldInputProps = { - onClickOutside?: FieldInputClickOutsideEvent; - onEnter?: FieldInputEvent; - onEscape?: FieldInputEvent; - onTab?: FieldInputEvent; - onShiftTab?: FieldInputEvent; -}; +export const FullNameFieldInput = () => { + const { draftValue, setDraftValue } = useFullNameField(); -export const FullNameFieldInput = ({ - onEnter, - onEscape, - onClickOutside, - onTab, - onShiftTab, -}: FullNameFieldInputProps) => { - const { draftValue, setDraftValue, persistFullNameField } = - useFullNameField(); + const { onEnter, onEscape, onClickOutside, onTab, onShiftTab } = useContext( + FieldInputEventContext, + ); const convertToFullName = (newDoubleText: FieldDoubleText) => { return { @@ -46,29 +34,26 @@ export const FullNameFieldInput = ({ }; const handleEnter = (newDoubleText: FieldDoubleText) => { - onEnter?.(() => persistFullNameField(convertToFullName(newDoubleText))); + onEnter?.({ newValue: convertToFullName(newDoubleText) }); }; const handleEscape = (newDoubleText: FieldDoubleText) => { - onEscape?.(() => persistFullNameField(convertToFullName(newDoubleText))); + onEscape?.({ newValue: convertToFullName(newDoubleText) }); }; const handleClickOutside = ( event: MouseEvent | TouchEvent, newDoubleText: FieldDoubleText, ) => { - onClickOutside?.( - () => persistFullNameField(convertToFullName(newDoubleText)), - event, - ); + onClickOutside?.({ newValue: convertToFullName(newDoubleText), event }); }; const handleTab = (newDoubleText: FieldDoubleText) => { - onTab?.(() => persistFullNameField(convertToFullName(newDoubleText))); + onTab?.({ newValue: convertToFullName(newDoubleText) }); }; const handleShiftTab = (newDoubleText: FieldDoubleText) => { - onShiftTab?.(() => persistFullNameField(convertToFullName(newDoubleText))); + onShiftTab?.({ newValue: convertToFullName(newDoubleText) }); }; const handleChange = (newDoubleText: FieldDoubleText) => { diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx index caca0aca63b..187f1a7a5bf 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx @@ -1,40 +1,42 @@ +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField'; import { LinksFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/LinksFieldMenuItem'; import { getFieldLinkDefinedLinks } from '@/object-record/record-field/meta-types/input/utils/getFieldLinkDefinedLinks'; import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/states/recordFieldInputIsFieldInErrorComponentState'; +import { linksSchema } from '@/object-record/record-field/types/guards/isFieldLinksValue'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; -import { useMemo } from 'react'; +import { useContext, useMemo } from 'react'; import { absoluteUrlSchema } from 'twenty-shared/utils'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { MultiItemFieldInput } from './MultiItemFieldInput'; -type LinksFieldInputProps = { - onCancel?: () => void; - onClickOutside?: (event: MouseEvent | TouchEvent) => void; -}; +export const LinksFieldInput = () => { + const { draftValue, fieldDefinition, setDraftValue } = useLinksField(); -export const LinksFieldInput = ({ - onCancel, - onClickOutside, -}: LinksFieldInputProps) => { - const { persistLinksField, fieldValue, fieldDefinition } = useLinksField(); + const { onEscape, onClickOutside } = useContext(FieldInputEventContext); const links = useMemo<{ url: string; label: string | null }[]>( - () => getFieldLinkDefinedLinks(fieldValue), - [fieldValue], + () => getFieldLinkDefinedLinks(draftValue), + [draftValue], ); - const handlePersistLinks = ( + const handleChange = ( updatedLinks: { url: string | null; label: string | null }[], ) => { const nextPrimaryLink = updatedLinks.at(0); const nextSecondaryLinks = updatedLinks.slice(1); - persistLinksField({ + const nextValue = { primaryLinkUrl: nextPrimaryLink?.url ?? null, primaryLinkLabel: nextPrimaryLink?.label ?? null, secondaryLinks: nextSecondaryLinks, - }); + }; + + const parseResponse = linksSchema.safeParse(nextValue); + + if (parseResponse.success) { + setDraftValue(parseResponse.data); + } }; const getShowPrimaryIcon = (index: number) => index === 0 && links.length > 1; @@ -48,15 +50,23 @@ export const LinksFieldInput = ({ setIsFieldInError(hasError && values.length === 0); }; + const handleClickOutside = ( + _newValue: any, + event: MouseEvent | TouchEvent, + ) => { + onClickOutside?.({ newValue: draftValue, event }); + }; + + const handleEscape = (_newValue: any) => { + onEscape?.({ newValue: draftValue }); + }; + return ( { - onClickOutside?.(event); - persist(); - }} + onChange={handleChange} + onEscape={handleEscape} + onClickOutside={handleClickOutside} placeholder="URL" fieldMetadataType={FieldMetadataType.LINKS} validateInput={(input) => ({ diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx index ddcffe846a8..9ac5bebfddf 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx @@ -7,7 +7,6 @@ import { type MultiItemBaseInputProps, } from '@/object-record/record-field/meta-types/input/components/MultiItemBaseInput'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; -import { type FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent'; import { type PhoneRecord } from '@/object-record/record-field/types/FieldMetadata'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; @@ -25,8 +24,8 @@ import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmp type MultiItemFieldInputProps = { items: T[]; - onPersist: (updatedItems: T[]) => void; - onCancel?: () => void; + onChange: (newItemsValue: T[]) => void; + onEscape?: (newItemsValue: T[]) => void; placeholder: string; validateInput?: (input: string) => { isValid: boolean; errorMessage: string }; formatInput?: (input: string) => T; @@ -40,7 +39,7 @@ type MultiItemFieldInputProps = { newItemLabel?: string; fieldMetadataType: FieldMetadataType; renderInput?: MultiItemBaseInputProps['renderInput']; - onClickOutside?: FieldInputClickOutsideEvent; + onClickOutside?: (newItemsValue: T[], event: MouseEvent | TouchEvent) => void; onError?: (hasError: boolean, values: any[]) => void; }; @@ -48,8 +47,8 @@ type MultiItemFieldInputProps = { // This should be refactored with a hook instead that exposes those events in a context around this component and its children. export const MultiItemFieldInput = ({ items, - onPersist, - onCancel, + onChange, + onEscape, placeholder, validateInput, formatInput, @@ -61,8 +60,9 @@ export const MultiItemFieldInput = ({ onError, }: MultiItemFieldInputProps) => { const containerRef = useRef(null); - const handleDropdownClose = () => { - onCancel?.(); + + const handleEscape = () => { + onEscape?.(items); }; const instanceId = useAvailableComponentInstanceIdOrThrow( @@ -78,7 +78,8 @@ export const MultiItemFieldInput = ({ if (isEditing && isPrimaryItem) { handleSubmitInput(); } - onClickOutside?.(() => {}, event); + + onClickOutside?.(items, event); }, listenerId: instanceId, }); @@ -86,8 +87,8 @@ export const MultiItemFieldInput = ({ useHotkeysOnFocusedElement({ focusId: instanceId, keys: [Key.Escape], - callback: handleDropdownClose, - dependencies: [handleDropdownClose], + callback: handleEscape, + dependencies: [handleEscape], }); const [isInputDisplayed, setIsInputDisplayed] = useState(false); @@ -99,13 +100,15 @@ export const MultiItemFieldInput = ({ }); const isAddingNewItem = itemToEditIndex === -1; - const handleOnChange = (value: string) => { + const handleInputChange = (value: string) => { setInputValue(value); + if (!validateInput) return; setErrorData( errorData.isValid ? errorData : { isValid: true, errorMessage: '' }, ); + onError?.(false, items); }; @@ -178,19 +181,19 @@ export const MultiItemFieldInput = ({ ? [...items, newItem] : toSpliced(items, itemToEditIndex, 1, newItem); - onPersist(updatedItems); + onChange(updatedItems); setIsInputDisplayed(false); setInputValue(''); }; const handleSetPrimaryItem = (index: number) => { const updatedItems = moveArrayItem(items, { fromIndex: index, toIndex: 0 }); - onPersist(updatedItems); + onChange(updatedItems); }; const handleDeleteItem = (index: number) => { const updatedItems = toSpliced(items, index, 1); - onPersist(updatedItems); + onChange(updatedItems); }; return ( @@ -219,11 +222,11 @@ export const MultiItemFieldInput = ({ value={inputValue} hasError={!errorData.isValid} renderInput={renderInput} - onEscape={handleDropdownClose} + onEscape={handleEscape} onChange={(value) => { value - ? handleOnChange(turnIntoEmptyStringIfWhitespacesOnly(value)) - : handleOnChange(''); + ? handleInputChange(turnIntoEmptyStringIfWhitespacesOnly(value)) + : handleInputChange(''); }} onEnter={handleSubmitInput} hasItem={!!items.length} diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx index 507bffe11d1..b84884d8302 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx @@ -1,22 +1,30 @@ +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; import { useMultiSelectField } from '@/object-record/record-field/meta-types/hooks/useMultiSelectField'; import { SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-field/meta-types/input/constants/SelectFieldInputSelectableListComponentInstanceId'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; +import { type FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata'; + import { MultiSelectInput } from '@/ui/field/input/components/MultiSelectInput'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useContext } from 'react'; -type MultiSelectFieldInputProps = { - onCancel?: () => void; -}; +export const MultiSelectFieldInput = () => { + const { fieldDefinition, draftValue, setDraftValue } = useMultiSelectField(); -export const MultiSelectFieldInput = ({ - onCancel, -}: MultiSelectFieldInputProps) => { - const { persistField, fieldDefinition, fieldValues } = useMultiSelectField(); + const { onSubmit } = useContext(FieldInputEventContext); + + const handleOptionSelected = (newDraftValue: FieldMultiSelectValue) => { + setDraftValue(newDraftValue); + }; const instanceId = useAvailableComponentInstanceIdOrThrow( RecordFieldComponentInstanceContext, ); + const handleCancel = () => { + onSubmit?.({ newValue: draftValue }); + }; + return ( ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/NumberFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/NumberFieldInput.tsx index f74a8f37874..0dcaf136319 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/NumberFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/NumberFieldInput.tsx @@ -1,56 +1,97 @@ import { TextInput } from '@/ui/field/input/components/TextInput'; +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; -import { type FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent'; import { FieldInputContainer } from '@/ui/field/input/components/FieldInputContainer'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { isNull } from '@sniptt/guards'; +import { useContext } from 'react'; +import { + canBeCastAsNumberOrNull, + castAsNumberOrNull, +} from '~/utils/cast-as-number-or-null'; import { useNumberField } from '../../hooks/useNumberField'; -export type FieldInputEvent = (persist: () => void) => void; +export const NumberFieldInput = () => { + const { fieldDefinition, draftValue, setDraftValue } = useNumberField(); -export type NumberFieldInputProps = { - onClickOutside?: FieldInputClickOutsideEvent; - onEnter?: FieldInputEvent; - onEscape?: FieldInputEvent; - onTab?: FieldInputEvent; - onShiftTab?: FieldInputEvent; -}; - -export const NumberFieldInput = ({ - onEnter, - onEscape, - onClickOutside, - onTab, - onShiftTab, -}: NumberFieldInputProps) => { - const { fieldDefinition, draftValue, setDraftValue, persistNumberField } = - useNumberField(); + const { onEnter, onEscape, onClickOutside, onTab, onShiftTab } = useContext( + FieldInputEventContext, + ); const instanceId = useAvailableComponentInstanceIdOrThrow( RecordFieldComponentInstanceContext, ); + const getNumberValueToPersist = ( + newValue: string, + ): { success: boolean; value?: any } => { + if (fieldDefinition?.metadata?.settings?.type === 'percentage') { + const newValueEscaped = newValue.replaceAll('%', ''); + + if (!canBeCastAsNumberOrNull(newValueEscaped)) { + return { success: false }; + } + + const castedValue = castAsNumberOrNull(newValue); + + if (!isNull(castedValue)) { + return { success: true, value: castedValue / 100 }; + } + + return { success: true, value: null }; + } + + if (!canBeCastAsNumberOrNull(newValue)) { + return { success: false }; + } + + const castedValue = castAsNumberOrNull(newValue); + + return { success: true, value: castedValue }; + }; + const handleEnter = (newText: string) => { - onEnter?.(() => persistNumberField(newText)); + const { success, value } = getNumberValueToPersist(newText); + + const shouldNotPersist = !success; + + onEnter?.({ newValue: value, skipPersist: shouldNotPersist }); }; const handleEscape = (newText: string) => { - onEscape?.(() => persistNumberField(newText)); + const { success, value } = getNumberValueToPersist(newText); + + const shouldNotPersist = !success; + + onEscape?.({ newValue: value, skipPersist: shouldNotPersist }); }; const handleClickOutside = ( event: MouseEvent | TouchEvent, newText: string, ) => { - onClickOutside?.(() => persistNumberField(newText), event); + const { success, value } = getNumberValueToPersist(newText); + + const shouldNotPersist = !success; + + onClickOutside?.({ newValue: value, skipPersist: shouldNotPersist, event }); }; const handleTab = (newText: string) => { - onTab?.(() => persistNumberField(newText)); + const { success, value } = getNumberValueToPersist(newText); + + const shouldNotPersist = !success; + + onTab?.({ newValue: value, skipPersist: shouldNotPersist }); }; const handleShiftTab = (newText: string) => { - onShiftTab?.(() => persistNumberField(newText)); + const { success, value } = getNumberValueToPersist(newText); + + const shouldNotPersist = !success; + + onShiftTab?.({ newValue: value, skipPersist: shouldNotPersist }); }; const handleChange = (newText: string) => { diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/PhonesFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/PhonesFieldInput.tsx index 7c6e0dce53b..e5b669b6ebd 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/PhonesFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/PhonesFieldInput.tsx @@ -4,16 +4,18 @@ import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/re import { phoneSchema } from '@/object-record/record-field/validation-schemas/phoneSchema'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import styled from '@emotion/styled'; -import { type E164Number, parsePhoneNumber } from 'libphonenumber-js'; +import { parsePhoneNumber, type E164Number } from 'libphonenumber-js'; import ReactPhoneNumberInput from 'react-phone-number-input'; import 'react-phone-number-input/style.css'; import { MultiItemFieldInput } from './MultiItemFieldInput'; +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; import { createPhonesFromFieldValue } from '@/object-record/record-field/meta-types/input/utils/phonesUtils'; -import { type FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent'; +import { phonesSchema } from '@/object-record/record-field/types/guards/isFieldPhonesValue'; import { PhoneCountryPickerDropdownButton } from '@/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton'; import { css } from '@emotion/react'; +import { useContext } from 'react'; import { TEXT_INPUT_STYLE } from 'twenty-ui/theme'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString'; @@ -69,24 +71,18 @@ const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)` width: calc(100% - ${({ theme }) => theme.spacing(8)}); `; -type PhonesFieldInputProps = { - onCancel?: () => void; - onClickOutside?: FieldInputClickOutsideEvent; -}; +export const PhonesFieldInput = () => { + const { fieldDefinition, setDraftValue, draftValue } = usePhonesField(); -export const PhonesFieldInput = ({ - onCancel, - onClickOutside, -}: PhonesFieldInputProps) => { - const { persistPhonesField, fieldValue, fieldDefinition } = usePhonesField(); + const { onEscape, onClickOutside } = useContext(FieldInputEventContext); - const phones = createPhonesFromFieldValue(fieldValue); + const phones = createPhonesFromFieldValue(draftValue); const defaultCountry = stripSimpleQuotesFromString( fieldDefinition?.defaultValue?.primaryPhoneCountryCode, ); - const handlePersistPhones = ( + const handlePhonesChange = ( updatedPhones: { number: string; countryCode: string; @@ -94,12 +90,19 @@ export const PhonesFieldInput = ({ }[], ) => { const [nextPrimaryPhone, ...nextAdditionalPhones] = updatedPhones; - persistPhonesField({ + + const newValue = { primaryPhoneNumber: nextPrimaryPhone?.number ?? '', primaryPhoneCountryCode: nextPrimaryPhone?.countryCode ?? '', primaryPhoneCallingCode: nextPrimaryPhone?.callingCode ?? '', additionalPhones: nextAdditionalPhones, - }); + }; + + const newValidatedPhoneResponse = phonesSchema.safeParse(newValue); + + if (newValidatedPhoneResponse.success) { + setDraftValue(newValidatedPhoneResponse.data); + } }; const validateInput = (input: string) => ({ @@ -119,12 +122,23 @@ export const PhonesFieldInput = ({ setIsFieldInError(hasError && values.length === 0); }; + const handleClickOutside = ( + _newValue: any, + event: MouseEvent | TouchEvent, + ) => { + onClickOutside?.({ newValue: draftValue, event }); + }; + + const handleEscape = (_newValue: any) => { + onEscape?.({ newValue: draftValue }); + }; + return ( { +export const RatingFieldInput = () => { const { rating } = useRatingField(); - const persistField = usePersistField(); + const { isRecordFieldReadOnly } = useContext(FieldContext); + + const { onSubmit } = useContext(FieldInputEventContext); const handleChange = (newRating: FieldRatingValue) => { - onSubmit?.(() => persistField(newRating)); + onSubmit?.({ newValue: newRating }); }; return ( - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RawJsonFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RawJsonFieldInput.tsx index 1d098c0b8b8..34cd50c08b1 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RawJsonFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RawJsonFieldInput.tsx @@ -1,16 +1,14 @@ import styled from '@emotion/styled'; +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; import { isWorkflowRunJsonField } from '@/object-record/record-field/meta-types/utils/isWorkflowRunJsonField'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; -import { - type FieldInputClickOutsideEvent, - type FieldInputEvent, -} from '@/object-record/record-field/types/FieldInputEvent'; + import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useLingui } from '@lingui/react/macro'; -import { useRef, useState } from 'react'; +import { useContext, useRef, useState } from 'react'; import { Key } from 'ts-key-enum'; import { IconPencil } from 'twenty-ui/display'; import { CodeEditor, FloatingIconButton } from 'twenty-ui/input'; @@ -18,14 +16,6 @@ import { JsonTree, isTwoFirstDepths } from 'twenty-ui/json-visualizer'; import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; import { useJsonField } from '../../hooks/useJsonField'; -type RawJsonFieldInputProps = { - onClickOutside?: FieldInputClickOutsideEvent; - onEnter?: FieldInputEvent; - onEscape?: FieldInputEvent; - onTab?: FieldInputEvent; - onShiftTab?: FieldInputEvent; -}; - const CONTAINER_HEIGHT = 300; const StyledContainer = styled.div` @@ -51,47 +41,66 @@ const StyledJsonTreeContainer = styled.div` width: min-content; `; -export const RawJsonFieldInput = ({ - onEscape, - onClickOutside, - onTab, - onShiftTab, -}: RawJsonFieldInputProps) => { +export const RawJsonFieldInput = () => { const { t } = useLingui(); const { copyToClipboard } = useCopyToClipboard(); - const { - draftValue, - precomputedDraftValue, - setDraftValue, - persistJsonField, - fieldDefinition, - } = useJsonField(); + const { draftValue, precomputedDraftValue, setDraftValue, fieldDefinition } = + useJsonField(); const containerRef = useRef(null); const instanceId = useAvailableComponentInstanceIdOrThrow( RecordFieldComponentInstanceContext, ); + const { onEscape, onClickOutside, onTab, onShiftTab } = useContext( + FieldInputEventContext, + ); + const [isEditing, setIsEditing] = useState(false); + const parseJsonField = ( + nextValue: string, + ): { success: boolean; value?: any } => { + if (!nextValue) { + return { + success: true, + value: null, + }; + } + + try { + return { success: true, value: JSON.parse(nextValue) }; + } catch { + return { success: false }; + } + }; + const handleEscape = (newText: string) => { - onEscape?.(() => persistJsonField(newText)); + const { success, value } = parseJsonField(newText); + + onEscape?.({ newValue: value, skipPersist: !success }); }; const handleClickOutside = ( event: MouseEvent | TouchEvent, newText: string, ) => { - onClickOutside?.(() => persistJsonField(newText), event); + const { success, value } = parseJsonField(newText); + + onClickOutside?.({ newValue: value, skipPersist: !success, event }); }; const handleTab = (newText: string) => { - onTab?.(() => persistJsonField(newText)); + const { success, value } = parseJsonField(newText); + + onTab?.({ newValue: value, skipPersist: !success }); }; const handleShiftTab = (newText: string) => { - onShiftTab?.(() => persistJsonField(newText)); + const { success, value } = parseJsonField(newText); + + onShiftTab?.({ newValue: value, skipPersist: !success }); }; const handleChange = (newText: string) => { diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx index 7bfa67456f4..7d2e449a0be 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx @@ -7,13 +7,13 @@ import { type TaskTarget } from '@/activities/types/TaskTarget'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; import { useRelationField } from '@/object-record/record-field/meta-types/hooks/useRelationField'; import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer'; import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState'; import { type FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; -import { type FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent'; import { type FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker'; import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch'; @@ -24,18 +24,14 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; -type RelationFromManyFieldInputProps = { - onSubmit?: FieldInputEvent; -}; - -export const RelationFromManyFieldInput = ({ - onSubmit, -}: RelationFromManyFieldInputProps) => { +export const RelationFromManyFieldInput = () => { const { fieldDefinition, recordId } = useContext(FieldContext); const instanceId = useAvailableComponentInstanceIdOrThrow( RecordFieldComponentInstanceContext, ); + const { onSubmit } = useContext(FieldInputEventContext); + const { updateRelation } = useUpdateRelationFromManyFieldInput(); const fieldName = fieldDefinition.metadata.fieldName; const objectMetadataNameSingular = @@ -51,7 +47,7 @@ export const RelationFromManyFieldInput = ({ const { fieldValue } = useRelationField(); const handleSubmit = () => { - onSubmit?.(() => {}); + onSubmit?.({ skipPersist: true }); }; const isRelationFromActivityTargets = diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationToOneFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationToOneFieldInput.tsx index 3640b36a7ca..afe325391f9 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationToOneFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationToOneFieldInput.tsx @@ -1,4 +1,4 @@ -import { usePersistField } from '../../../hooks/usePersistField'; +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; import { useRelationField } from '../../hooks/useRelationField'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; @@ -6,7 +6,6 @@ import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/ import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState'; import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState'; -import { type FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent'; import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker'; import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState'; import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; @@ -14,21 +13,14 @@ import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; +import { useContext } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { IconForbid } from 'twenty-ui/display'; -export type RelationToOneFieldInputProps = { - onSubmit?: FieldInputEvent; - onCancel?: () => void; -}; - -export const RelationToOneFieldInput = ({ - onSubmit, - onCancel, -}: RelationToOneFieldInputProps) => { +export const RelationToOneFieldInput = () => { const { fieldDefinition, recordId } = useRelationField(); - const persistField = usePersistField(); + const { onSubmit, onCancel } = useContext(FieldInputEventContext); const instanceId = useAvailableComponentInstanceIdOrThrow( RecordFieldComponentInstanceContext, @@ -36,7 +28,7 @@ export const RelationToOneFieldInput = ({ const handleRecordSelected = ( selectedRecord: SingleRecordPickerRecord | null | undefined, - ) => onSubmit?.(() => persistField(selectedRecord?.record ?? null)); + ) => onSubmit?.({ newValue: selectedRecord?.record ?? null }); const { objectMetadataItem: relationObjectMetadataItem } = useObjectMetadataItem({ diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RichTextFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RichTextFieldInput.tsx index 7a551918c3e..5ad72153e3b 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RichTextFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RichTextFieldInput.tsx @@ -1,17 +1,17 @@ import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; -import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { useRichTextCommandMenu } from '@/command-menu/hooks/useRichTextCommandMenu'; import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; -import { - type FieldInputClickOutsideEvent, - type FieldInputEvent, -} from '@/object-record/record-field/types/FieldInputEvent'; + +import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; + +import { type FieldRichTextV2Metadata } from '@/object-record/record-field/types/FieldMetadata'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { Suspense, lazy, useRef } from 'react'; +import { Suspense, lazy, useContext, useRef } from 'react'; import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; import { IconLayoutSidebarLeftCollapse } from 'twenty-ui/display'; import { FloatingIconButton } from 'twenty-ui/input'; @@ -22,13 +22,6 @@ const ActivityRichTextEditor = lazy(() => })), ); -export type RichTextFieldInputProps = { - onClickOutside?: FieldInputClickOutsideEvent; - onCancel?: () => void; - onEnter?: FieldInputEvent; - onEscape?: FieldInputEvent; -}; - const StyledContainer = styled.div` background-color: ${({ theme }) => theme.background.primary}; width: 480px; @@ -60,29 +53,34 @@ const LoadingSkeleton = () => { ); }; -export const RichTextFieldInput = ({ - targetableObject, - onClickOutside, - onEscape, -}: { - targetableObject: Pick & { - targetObjectNameSingular: +export const RichTextFieldInput = () => { + const { fieldDefinition, recordId } = useContext(FieldContext); + + const targetableObject = { + id: recordId, + targetObjectNameSingular: ( + fieldDefinition as { + metadata: FieldRichTextV2Metadata; + } + ).metadata.objectMetadataNameSingular as | CoreObjectNameSingular.Note - | CoreObjectNameSingular.Task; + | CoreObjectNameSingular.Task, }; -} & RichTextFieldInputProps) => { + const { editRichText } = useRichTextCommandMenu(); const containerRef = useRef(null); const instanceId = useAvailableComponentInstanceIdOrThrow( RecordFieldComponentInstanceContext, ); + const { onEscape, onClickOutside } = useContext(FieldInputEventContext); + const handleClickOutside = (event: MouseEvent | TouchEvent) => { - onClickOutside?.(() => {}, event); + onClickOutside?.({ event, skipPersist: true }); }; const handleEscape = () => { - onEscape?.(() => {}); + onEscape?.({ skipPersist: true }); }; useRegisterInputEvents({ @@ -106,7 +104,7 @@ export const RichTextFieldInput = ({ Icon={IconLayoutSidebarLeftCollapse} size="small" onClick={() => { - onEscape?.(() => {}); + onEscape?.({ skipPersist: true }); editRichText( targetableObject.id, targetableObject.targetObjectNameSingular, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx index 20b73022cb1..2a1069306ea 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx @@ -1,27 +1,21 @@ +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; import { useClearField } from '@/object-record/record-field/hooks/useClearField'; import { useSelectField } from '@/object-record/record-field/meta-types/hooks/useSelectField'; import { SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-field/meta-types/input/constants/SelectFieldInputSelectableListComponentInstanceId'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; -import { type FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent'; import { SelectInput } from '@/ui/field/input/components/SelectInput'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; -import { useState } from 'react'; +import { useContext, useState } from 'react'; import { Key } from 'ts-key-enum'; import { isDefined } from 'twenty-shared/utils'; import { type SelectOption } from 'twenty-ui/input'; -type SelectFieldInputProps = { - onSubmit?: FieldInputEvent; - onCancel?: () => void; -}; +export const SelectFieldInput = () => { + const { fieldDefinition, fieldValue } = useSelectField(); -export const SelectFieldInput = ({ - onSubmit, - onCancel, -}: SelectFieldInputProps) => { - const { persistField, fieldDefinition, fieldValue } = useSelectField(); + const { onCancel, onSubmit } = useContext(FieldInputEventContext); const instanceId = useAvailableComponentInstanceIdOrThrow( RecordFieldComponentInstanceContext, @@ -44,7 +38,7 @@ export const SelectFieldInput = ({ }; const handleSubmit = (option: SelectOption) => { - onSubmit?.(() => persistField(option?.value)); + onSubmit?.({ newValue: option.value }); resetSelectedItem(); }; @@ -76,8 +70,7 @@ export const SelectFieldInput = ({ (option) => option.value === itemId, ); if (isDefined(option)) { - onSubmit?.(() => persistField(option.value)); - resetSelectedItem(); + handleSubmit(option); } }} onOptionSelected={handleSubmit} diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/TextFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/TextFieldInput.tsx index b96a9b5fca1..2fc2c395a2f 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/TextFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/TextFieldInput.tsx @@ -1,60 +1,50 @@ import { TextAreaInput } from '@/ui/field/input/components/TextAreaInput'; -import { usePersistField } from '../../../hooks/usePersistField'; import { useTextField } from '../../hooks/useTextField'; +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; -import { - type FieldInputClickOutsideEvent, - type FieldInputEvent, -} from '@/object-record/record-field/types/FieldInputEvent'; + import { FieldInputContainer } from '@/ui/field/input/components/FieldInputContainer'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useContext } from 'react'; import { turnIntoUndefinedIfWhitespacesOnly } from '~/utils/string/turnIntoUndefinedIfWhitespacesOnly'; -export type TextFieldInputProps = { - onClickOutside?: FieldInputClickOutsideEvent; - onEnter?: FieldInputEvent; - onEscape?: FieldInputEvent; - onTab?: FieldInputEvent; - onShiftTab?: FieldInputEvent; -}; - -export const TextFieldInput = ({ - onEnter, - onEscape, - onClickOutside, - onTab, - onShiftTab, -}: TextFieldInputProps) => { +export const TextFieldInput = () => { const { fieldDefinition, draftValue, setDraftValue } = useTextField(); + const { onEnter, onEscape, onClickOutside, onTab, onShiftTab } = useContext( + FieldInputEventContext, + ); + const instanceId = useAvailableComponentInstanceIdOrThrow( RecordFieldComponentInstanceContext, ); - const persistField = usePersistField(); const handleEnter = (newText: string) => { - onEnter?.(() => persistField(newText.trim())); + onEnter?.({ newValue: newText.trim() }); }; const handleEscape = (newText: string) => { - onEscape?.(() => persistField(newText.trim())); + onEscape?.({ newValue: newText.trim() }); }; const handleClickOutside = ( event: MouseEvent | TouchEvent, newText: string, ) => { - onClickOutside?.(() => persistField(newText.trim()), event); + onClickOutside?.({ + newValue: newText.trim(), + event, + }); }; const handleTab = (newText: string) => { - onTab?.(() => persistField(newText.trim())); + onTab?.({ newValue: newText.trim() }); }; const handleShiftTab = (newText: string) => { - onShiftTab?.(() => persistField(newText.trim())); + onShiftTab?.({ newValue: newText.trim() }); }; const handleChange = (newText: string) => { diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/ArrayFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/ArrayFieldInput.stories.tsx index 35097b331c8..c116ae6a2e1 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/ArrayFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/ArrayFieldInput.stories.tsx @@ -1,9 +1,10 @@ -import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; import { type Meta, type StoryObj } from '@storybook/react'; +import { expect, userEvent, waitFor, within } from '@storybook/test'; import { useEffect } from 'react'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { useArrayField } from '@/object-record/record-field/meta-types/hooks/useArrayField'; +import { getFieldInputEventContextProviderWithJestMocks } from '@/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix'; import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId'; @@ -12,48 +13,34 @@ import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentTyp import { FieldMetadataType } from '~/generated-metadata/graphql'; import { ArrayFieldInput } from '../ArrayFieldInput'; -const updateRecord = fn(); +const { FieldInputEventContextProviderWithJestMocks } = + getFieldInputEventContextProviderWithJestMocks(); const ArrayValueSetterEffect = ({ value }: { value: string[] }) => { - const { setFieldValue } = useArrayField(); + const { setFieldValue, setDraftValue } = useArrayField(); useEffect(() => { setFieldValue(value); - }, [setFieldValue, value]); + setDraftValue(value); + }, [setFieldValue, value, setDraftValue]); return null; }; -type ArrayFieldValueGaterProps = Pick< - ArrayInputWithContextProps, - 'onCancel' | 'onClickOutside' ->; - -const ArrayFieldValueGater = ({ - onCancel, - onClickOutside, -}: ArrayFieldValueGaterProps) => { +const ArrayFieldValueGater = () => { const { fieldValue } = useArrayField(); - return ( - fieldValue && ( - - ) - ); + return fieldValue && ; }; type ArrayInputWithContextProps = { value: string[]; recordId?: string; - onCancel?: () => void; - onClickOutside?: (event: MouseEvent | TouchEvent) => void; }; const ArrayInputWithContext = ({ value, recordId = 'record-id', - onCancel, - onClickOutside, }: ArrayInputWithContextProps) => { const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); @@ -99,14 +86,12 @@ const ArrayInputWithContext = ({ recordId, isLabelIdentifier: false, isRecordFieldReadOnly: false, - useUpdateRecord: () => [updateRecord, { loading: false }], }} > - - + + + + ); @@ -155,21 +140,5 @@ export const TrimInput: Story = { 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); }, }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx index fe9948b1f52..6d937442b17 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx @@ -1,5 +1,5 @@ import { type Meta, type StoryObj } from '@storybook/react'; -import { expect, fn, userEvent, within } from '@storybook/test'; +import { expect, userEvent, within } from '@storybook/test'; import { useEffect } from 'react'; import { useSetRecoilState } from 'recoil'; @@ -7,13 +7,14 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { getFieldInputEventContextProviderWithJestMocks } from '@/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix'; import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId'; -import { - BooleanFieldInput, - type BooleanFieldInputProps, -} from '../BooleanFieldInput'; +import { BooleanFieldInput } from '../BooleanFieldInput'; + +const { FieldInputEventContextProviderWithJestMocks, handleSubmitMocked } = + getFieldInputEventContextProviderWithJestMocks(); const BooleanFieldValueSetterEffect = ({ value, @@ -31,7 +32,7 @@ const BooleanFieldValueSetterEffect = ({ return <>; }; -type BooleanFieldInputWithContextProps = BooleanFieldInputProps & { +type BooleanFieldInputWithContextProps = { value: boolean; recordId?: string; }; @@ -39,7 +40,6 @@ type BooleanFieldInputWithContextProps = BooleanFieldInputProps & { const BooleanFieldInputWithContext = ({ value, recordId, - onSubmit, }: BooleanFieldInputWithContextProps) => { return ( - - + + + + ); @@ -92,19 +94,9 @@ export default meta; type Story = StoryObj; -const submitJestFn = fn(); - export const Default: Story = {}; export const Toggle: Story = { - args: { - onSubmit: submitJestFn, - }, - argTypes: { - onSubmit: { - control: false, - }, - }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -114,18 +106,18 @@ export const Toggle: Story = { await expect(trueText).toBeInTheDocument(); - await expect(submitJestFn).toHaveBeenCalledTimes(0); + await expect(handleSubmitMocked).toHaveBeenCalledTimes(0); await userEvent.click(input); await expect(input).toHaveTextContent('False'); - await expect(submitJestFn).toHaveBeenCalledTimes(1); + await expect(handleSubmitMocked).toHaveBeenCalledTimes(1); await userEvent.click(input); await expect(input).toHaveTextContent('True'); - await expect(submitJestFn).toHaveBeenCalledTimes(2); + await expect(handleSubmitMocked).toHaveBeenCalledTimes(2); }, }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/DateTimeFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/DateTimeFieldInput.stories.tsx index ab71fde9a4b..cebbcfb0b51 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/DateTimeFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/DateTimeFieldInput.stories.tsx @@ -1,21 +1,27 @@ import { type Meta, type StoryObj } from '@storybook/react'; -import { expect, fn, userEvent, within } from '@storybook/test'; +import { expect, userEvent, within } from '@storybook/test'; import { useEffect } from 'react'; import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { getFieldInputEventContextProviderWithJestMocks } from '@/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix'; import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId'; import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect'; import { useDateTimeField } from '../../../hooks/useDateTimeField'; -import { - DateTimeFieldInput, - type DateTimeFieldInputProps, -} from '../DateTimeFieldInput'; +import { DateTimeFieldInput } from '../DateTimeFieldInput'; + +const { + FieldInputEventContextProviderWithJestMocks, + handleEscapeMocked, + handleClickoutsideMocked, + handleEnterMocked, +} = getFieldInputEventContextProviderWithJestMocks(); + const formattedDate = new Date(2022, 0, 1, 2, 0, 0); const DateFieldValueSetterEffect = ({ value }: { value: Date }) => { @@ -28,30 +34,13 @@ const DateFieldValueSetterEffect = ({ value }: { value: Date }) => { return <>; }; -type DateFieldValueGaterProps = Pick< - DateTimeFieldInputProps, - 'onEscape' | 'onEnter' | 'onClickOutside' ->; - -const DateFieldValueGater = ({ - onEscape, - onEnter, - onClickOutside, -}: DateFieldValueGaterProps) => { +const DateFieldValueGater = () => { const { fieldValue } = useDateTimeField(); - return ( - fieldValue && ( - - ) - ); + return fieldValue && ; }; -type DateFieldInputWithContextProps = DateTimeFieldInputProps & { +type DateFieldInputWithContextProps = { value: Date; recordId: string; }; @@ -59,9 +48,6 @@ type DateFieldInputWithContextProps = DateTimeFieldInputProps & { const DateFieldInputWithContext = ({ value, recordId, - onEscape, - onEnter, - onClickOutside, }: DateFieldInputWithContextProps) => { const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); const instanceId = getRecordFieldInputInstanceId({ @@ -104,31 +90,25 @@ const DateFieldInputWithContext = ({ isRecordFieldReadOnly: false, }} > - - - + + + + +
); }; -const escapeJestFn = fn(); -const enterJestFn = fn(); -const clickOutsideJestFn = fn(); - const meta: Meta = { title: 'UI/Data/Field/Input/DateTimeFieldInput', component: DateFieldInputWithContext, args: { value: formattedDate, - onEscape: escapeJestFn, - onEnter: enterJestFn, - onClickOutside: clickOutsideJestFn, + onEscape: handleEscapeMocked, + onEnter: handleEnterMocked, + onClickOutside: handleClickoutsideMocked, }, argTypes: { onEscape: { @@ -160,36 +140,36 @@ export const ClickOutside: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + await expect(handleClickoutsideMocked).toHaveBeenCalledTimes(0); await canvas.findByText('January'); const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); await userEvent.click(emptyDiv); - await expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + await expect(handleClickoutsideMocked).toHaveBeenCalledTimes(1); }, }; export const Escape: Story = { play: async ({ canvasElement }) => { - await expect(escapeJestFn).toHaveBeenCalledTimes(0); + await expect(handleEscapeMocked).toHaveBeenCalledTimes(0); const canvas = within(canvasElement); await canvas.findByText('January'); await userEvent.keyboard('{escape}'); - await expect(escapeJestFn).toHaveBeenCalledTimes(1); + await expect(handleEscapeMocked).toHaveBeenCalledTimes(1); }, }; export const Enter: Story = { play: async ({ canvasElement }) => { - await expect(enterJestFn).toHaveBeenCalledTimes(0); + await expect(handleEnterMocked).toHaveBeenCalledTimes(0); const canvas = within(canvasElement); await canvas.findByText('January'); await userEvent.keyboard('{enter}'); - await expect(enterJestFn).toHaveBeenCalledTimes(1); + await expect(handleEnterMocked).toHaveBeenCalledTimes(1); }, }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailsFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailsFieldInput.stories.tsx index 9621a35cd9c..151279b8f6b 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailsFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailsFieldInput.stories.tsx @@ -1,10 +1,11 @@ -import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; import { type Meta, type StoryObj } from '@storybook/react'; +import { expect, fn, userEvent, within } from '@storybook/test'; import { useEffect } from 'react'; import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { useEmailsField } from '@/object-record/record-field/meta-types/hooks/useEmailsField'; +import { getFieldInputEventContextProviderWithJestMocks } from '@/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { type FieldEmailsValue } from '@/object-record/record-field/types/FieldMetadata'; import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix'; @@ -18,46 +19,34 @@ import { EmailsFieldInput } from '../EmailsFieldInput'; const updateRecord = fn(); +const { FieldInputEventContextProviderWithJestMocks } = + getFieldInputEventContextProviderWithJestMocks(); + const EmailValueSetterEffect = ({ value }: { value: FieldEmailsValue }) => { - const { setFieldValue } = useEmailsField(); + const { setFieldValue, setDraftValue } = useEmailsField(); useEffect(() => { setFieldValue(value); - }, [setFieldValue, value]); + setDraftValue(value); + }, [setFieldValue, value, setDraftValue]); return null; }; -type EmailFieldValueGaterProps = Pick< - EmailInputWithContextProps, - 'onCancel' | 'onClickOutside' ->; - -const EmailFieldValueGater = ({ - onCancel, - onClickOutside, -}: EmailFieldValueGaterProps) => { +const EmailFieldValueGater = () => { const { fieldValue } = useEmailsField(); - return ( - fieldValue && ( - - ) - ); + return fieldValue && ; }; type EmailInputWithContextProps = { value: FieldEmailsValue; recordId?: string; - onCancel?: () => void; - onClickOutside?: (event: MouseEvent | TouchEvent) => void; }; const EmailInputWithContext = ({ value, recordId = 'record-id', - onCancel, - onClickOutside, }: EmailInputWithContextProps) => { const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); const instanceId = getRecordFieldInputInstanceId({ @@ -101,11 +90,10 @@ const EmailInputWithContext = ({ useUpdateRecord: () => [updateRecord, { loading: false }], }} > - - + + + + ); @@ -159,21 +147,6 @@ export const TrimInput: Story = { 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); }, }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/LinksFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/LinksFieldInput.stories.tsx index 0a0c4b0956a..d47216791d9 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/LinksFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/LinksFieldInput.stories.tsx @@ -4,6 +4,7 @@ import { useEffect } from 'react'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField'; +import { getFieldInputEventContextProviderWithJestMocks } from '@/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix'; import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId'; @@ -15,6 +16,12 @@ import { LinksFieldInput } from '../LinksFieldInput'; const updateRecord = fn(); +const { + FieldInputEventContextProviderWithJestMocks, + handleEscapeMocked, + handleClickoutsideMocked, +} = getFieldInputEventContextProviderWithJestMocks(); + const LinksValueSetterEffect = ({ value, }: { @@ -24,11 +31,12 @@ const LinksValueSetterEffect = ({ secondaryLinks: Array<{ url: string | null; label: string | null }> | null; }; }) => { - const { setFieldValue } = useLinksField(); + const { setFieldValue, setDraftValue } = useLinksField(); useEffect(() => { setFieldValue(value); - }, [setFieldValue, value]); + setDraftValue(value); + }, [setFieldValue, value, setDraftValue]); return null; }; @@ -40,33 +48,17 @@ type LinksInputWithContextProps = { secondaryLinks: Array<{ url: string | null; label: string | null }> | null; }; recordId: string; - onCancel?: () => void; - onClickOutside?: (event: MouseEvent | TouchEvent) => void; }; -type LinksFieldValueGaterProps = Pick< - LinksInputWithContextProps, - 'onCancel' | 'onClickOutside' ->; - -const LinksFieldValueGater = ({ - onCancel, - onClickOutside, -}: LinksFieldValueGaterProps) => { +const LinksFieldValueGater = () => { const { fieldValue } = useLinksField(); - return ( - fieldValue && ( - - ) - ); + return fieldValue && ; }; const LinksInputWithContext = ({ value, recordId, - onCancel, - onClickOutside, }: LinksInputWithContextProps) => { const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); const instanceId = getRecordFieldInputInstanceId({ @@ -111,11 +103,10 @@ const LinksInputWithContext = ({ useUpdateRecord: () => [updateRecord, { loading: false }], }} > - - + + + +
@@ -127,9 +118,6 @@ const getPrimaryLinkBookmarkIcon = (canvasElement: HTMLElement) => // It would be better to use an aria-label on the icon, but we'll do this for now canvasElement.querySelector('svg[class*="tabler-icon-bookmark"]'); -const cancelJestFn = fn(); -const clickOutsideJestFn = fn(); - const meta: Meta = { title: 'UI/Data/Field/Input/LinksFieldInput', component: LinksInputWithContext, @@ -140,8 +128,6 @@ const meta: Meta = { secondaryLinks: null, }, recordId: '123', - onCancel: cancelJestFn, - onClickOutside: clickOutsideJestFn, }, argTypes: { onCancel: { control: false }, @@ -242,22 +228,6 @@ export const CreatePrimaryLink: Story = { 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(); }, }; @@ -272,22 +242,6 @@ export const TrimInput: Story = { 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(); }, }; @@ -316,27 +270,6 @@ export const AddSecondaryLink: Story = { const secondaryLink = await canvas.findByText('docs.twenty.com'); expect(secondaryLink).toBeVisible(); - - await waitFor(() => { - expect(updateRecord).toHaveBeenCalledWith({ - variables: { - where: { id: '123' }, - updateOneRecordInput: { - links: { - primaryLinkUrl: 'https://www.twenty.com', - primaryLinkLabel: 'Twenty Website', - secondaryLinks: [ - { - url: 'https://docs.twenty.com', - label: null, - }, - ], - }, - }, - }, - }); - }); - expect(updateRecord).toHaveBeenCalledTimes(1); }, }; @@ -369,22 +302,6 @@ export const DeletePrimaryLink: Story = { const input = await canvas.findByPlaceholderText('URL'); expect(input).toBeVisible(); expect(input).toHaveValue(''); - - await waitFor(() => { - expect(updateRecord).toHaveBeenCalledWith({ - variables: { - where: { id: '123' }, - updateOneRecordInput: { - links: { - primaryLinkUrl: null, - primaryLinkLabel: null, - secondaryLinks: [], - }, - }, - }, - }); - }); - expect(updateRecord).toHaveBeenCalledTimes(1); }, }; @@ -427,22 +344,6 @@ export const DeletePrimaryLinkAndUseSecondaryLinkAsTheNewPrimaryLink: Story = { expect(oldPrimaryLink).not.toBeInTheDocument(); expect(getPrimaryLinkBookmarkIcon(canvasElement)).not.toBeInTheDocument(); - - await waitFor(() => { - expect(updateRecord).toHaveBeenCalledWith({ - variables: { - where: { id: '123' }, - updateOneRecordInput: { - links: { - primaryLinkUrl: 'https://docs.twenty.com', - primaryLinkLabel: 'Documentation', - secondaryLinks: [], - }, - }, - }, - }); - }); - expect(updateRecord).toHaveBeenCalledTimes(1); }, }; @@ -485,22 +386,6 @@ export const DeleteSecondaryLink: Story = { expect(secondaryLink).not.toBeInTheDocument(); expect(getPrimaryLinkBookmarkIcon(canvasElement)).not.toBeInTheDocument(); - - await waitFor(() => { - expect(updateRecord).toHaveBeenCalledWith({ - variables: { - where: { id: '123' }, - updateOneRecordInput: { - links: { - primaryLinkUrl: 'https://www.twenty.com', - primaryLinkLabel: 'Twenty Website', - secondaryLinks: [], - }, - }, - }, - }); - }); - expect(updateRecord).toHaveBeenCalledTimes(1); }, }; @@ -511,14 +396,14 @@ export const ClickOutside: Story = { const input = await canvas.findByPlaceholderText('URL'); await userEvent.click(input); - expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + expect(handleClickoutsideMocked).toHaveBeenCalledTimes(0); const outsideDiv = await canvas.findByTestId( 'links-field-input-click-outside-div', ); await userEvent.click(outsideDiv); - expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + expect(handleClickoutsideMocked).toHaveBeenCalledTimes(1); }, }; @@ -526,14 +411,14 @@ export const Cancel: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - expect(cancelJestFn).toHaveBeenCalledTimes(0); + expect(handleEscapeMocked).toHaveBeenCalledTimes(0); const input = await canvas.findByPlaceholderText('URL'); await userEvent.click(input); await userEvent.keyboard('{escape}'); - expect(cancelJestFn).toHaveBeenCalledTimes(1); + expect(handleEscapeMocked).toHaveBeenCalledTimes(1); }, }; @@ -602,28 +487,6 @@ export const MakeSecondaryLinkPrimary: Story = { getCanvasElementForDropdownTesting(), ).findByText('Set as Primary'); await userEvent.click(setPrimaryOption); - - // Documentation should now be the primary link - await waitFor(() => { - expect(updateRecord).toHaveBeenCalledWith({ - variables: { - where: { id: '123' }, - updateOneRecordInput: { - links: { - primaryLinkUrl: 'https://docs.twenty.com', - primaryLinkLabel: 'Documentation', - secondaryLinks: [ - { - url: 'https://www.twenty.com', - label: 'Twenty Website', - }, - ], - }, - }, - }, - }); - }); - expect(updateRecord).toHaveBeenCalledTimes(1); }, }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx index 0676affc590..510c6201dfa 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx @@ -1,5 +1,5 @@ import { type Decorator, type Meta, type StoryObj } from '@storybook/react'; -import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; +import { expect, userEvent, waitFor, within } from '@storybook/test'; import { useEffect, useState } from 'react'; import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; @@ -7,6 +7,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { getFieldInputEventContextProviderWithJestMocks } from '@/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix'; import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId'; @@ -14,10 +15,16 @@ import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentTyp import { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect'; import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; import { useNumberField } from '../../../hooks/useNumberField'; -import { - NumberFieldInput, - type NumberFieldInputProps, -} from '../NumberFieldInput'; +import { NumberFieldInput } from '../NumberFieldInput'; + +const { + FieldInputEventContextProviderWithJestMocks, + handleEnterMocked, + handleEscapeMocked, + handleClickoutsideMocked, + handleTabMocked, + handleShiftTabMocked, +} = getFieldInputEventContextProviderWithJestMocks(); const NumberFieldValueSetterEffect = ({ value }: { value: number }) => { const { setFieldValue } = useNumberField(); @@ -29,7 +36,7 @@ const NumberFieldValueSetterEffect = ({ value }: { value: number }) => { return <>; }; -type NumberFieldInputWithContextProps = NumberFieldInputProps & { +type NumberFieldInputWithContextProps = { value: number; recordId: string; }; @@ -37,11 +44,6 @@ type NumberFieldInputWithContextProps = NumberFieldInputProps & { const NumberFieldInputWithContext = ({ recordId, value, - onEnter, - onEscape, - onClickOutside, - onTab, - onShiftTab, }: NumberFieldInputWithContextProps) => { const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); @@ -94,15 +96,11 @@ const NumberFieldInputWithContext = ({ isRecordFieldReadOnly: false, }} > - {isReady && } - - + + {isReady && } + + + {isReady &&
}
@@ -110,19 +108,13 @@ const NumberFieldInputWithContext = ({ ); }; -const enterJestFn = fn(); -const escapeJestfn = fn(); -const clickOutsideJestFn = fn(); -const tabJestFn = fn(); -const shiftTabJestFn = fn(); - const clearMocksDecorator: Decorator = (Story, context) => { if (context.parameters.clearMocks === true) { - enterJestFn.mockClear(); - escapeJestfn.mockClear(); - clickOutsideJestFn.mockClear(); - tabJestFn.mockClear(); - shiftTabJestFn.mockClear(); + handleEnterMocked.mockClear(); + handleEscapeMocked.mockClear(); + handleClickoutsideMocked.mockClear(); + handleTabMocked.mockClear(); + handleShiftTabMocked.mockClear(); } return ; }; @@ -133,11 +125,11 @@ const meta: Meta = { args: { value: 1000, isPositive: true, - onEnter: enterJestFn, - onEscape: escapeJestfn, - onClickOutside: clickOutsideJestFn, - onTab: tabJestFn, - onShiftTab: shiftTabJestFn, + onEnter: handleEnterMocked, + onEscape: handleEscapeMocked, + onClickOutside: handleClickoutsideMocked, + onTab: handleTabMocked, + onShiftTab: handleShiftTabMocked, }, argTypes: { onEnter: { control: false }, @@ -162,13 +154,13 @@ export const Enter: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - expect(enterJestFn).toHaveBeenCalledTimes(0); + expect(handleEnterMocked).toHaveBeenCalledTimes(0); await canvas.findByTestId('is-ready-marker'); await userEvent.keyboard('{enter}'); await waitFor(() => { - expect(enterJestFn).toHaveBeenCalledTimes(1); + expect(handleEnterMocked).toHaveBeenCalledTimes(1); }); }, }; @@ -177,13 +169,13 @@ export const Escape: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - expect(escapeJestfn).toHaveBeenCalledTimes(0); + expect(handleEscapeMocked).toHaveBeenCalledTimes(0); await canvas.findByTestId('is-ready-marker'); await userEvent.keyboard('{esc}'); await waitFor(() => { - expect(escapeJestfn).toHaveBeenCalledTimes(1); + expect(handleEscapeMocked).toHaveBeenCalledTimes(1); }); }, }; @@ -192,7 +184,7 @@ export const ClickOutside: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + expect(handleClickoutsideMocked).toHaveBeenCalledTimes(0); const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); @@ -200,7 +192,7 @@ export const ClickOutside: Story = { await userEvent.click(emptyDiv); await waitFor(() => { - expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + expect(handleClickoutsideMocked).toHaveBeenCalledTimes(1); }); }, }; @@ -209,13 +201,13 @@ export const Tab: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - expect(tabJestFn).toHaveBeenCalledTimes(0); + expect(handleTabMocked).toHaveBeenCalledTimes(0); await canvas.findByTestId('is-ready-marker'); await userEvent.keyboard('{tab}'); await waitFor(() => { - expect(tabJestFn).toHaveBeenCalledTimes(1); + expect(handleTabMocked).toHaveBeenCalledTimes(1); }); }, }; @@ -224,13 +216,13 @@ export const ShiftTab: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - expect(shiftTabJestFn).toHaveBeenCalledTimes(0); + expect(handleShiftTabMocked).toHaveBeenCalledTimes(0); await canvas.findByTestId('is-ready-marker'); await userEvent.keyboard('{shift>}{tab}'); await waitFor(() => { - expect(shiftTabJestFn).toHaveBeenCalledTimes(1); + expect(handleShiftTabMocked).toHaveBeenCalledTimes(1); }); }, }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhonesFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhonesFieldInput.stories.tsx index c902fc17e59..4cfa6834110 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhonesFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhonesFieldInput.stories.tsx @@ -1,12 +1,12 @@ -import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; import { type Meta, type StoryObj } from '@storybook/react'; +import { expect, userEvent, within } from '@storybook/test'; import { useEffect } from 'react'; import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { usePhonesField } from '@/object-record/record-field/meta-types/hooks/usePhonesField'; +import { getFieldInputEventContextProviderWithJestMocks } from '@/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; -import { type FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent'; import { type FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata'; import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix'; import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId'; @@ -15,48 +15,34 @@ import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentTyp import { FieldMetadataType } from '~/generated-metadata/graphql'; import { PhonesFieldInput } from '../PhonesFieldInput'; -const updateRecord = fn(); +const { FieldInputEventContextProviderWithJestMocks } = + getFieldInputEventContextProviderWithJestMocks(); const PhoneValueSetterEffect = ({ value }: { value: FieldPhonesValue }) => { - const { setFieldValue } = usePhonesField(); + const { setFieldValue, setDraftValue } = usePhonesField(); useEffect(() => { setFieldValue(value); - }, [setFieldValue, value]); + setDraftValue(value); + }, [setFieldValue, value, setDraftValue]); return null; }; -type PhoneFieldValueGaterProps = Pick< - PhoneInputWithContextProps, - 'onCancel' | 'onClickOutside' ->; - -const PhoneFieldValueGater = ({ - onCancel, - onClickOutside, -}: PhoneFieldValueGaterProps) => { +const PhoneFieldValueGater = () => { const { fieldValue } = usePhonesField(); - return ( - fieldValue && ( - - ) - ); + return fieldValue && ; }; type PhoneInputWithContextProps = { value: FieldPhonesValue; recordId?: string; - onCancel?: () => void; - onClickOutside?: FieldInputClickOutsideEvent; }; const PhoneInputWithContext = ({ value, recordId = 'record-id', - onCancel, - onClickOutside, }: PhoneInputWithContextProps) => { const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); const instanceId = getRecordFieldInputInstanceId({ @@ -97,14 +83,13 @@ const PhoneInputWithContext = ({ recordId, isLabelIdentifier: false, isRecordFieldReadOnly: false, - useUpdateRecord: () => [updateRecord, { loading: false }], + useUpdateRecord: () => [() => {}, { loading: false }], }} > - - + + + + ); @@ -173,35 +158,6 @@ export const TrimInput: Story = { 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); }, }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RatingFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RatingFieldInput.stories.tsx index 859416103db..5056618d69f 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RatingFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RatingFieldInput.stories.tsx @@ -1,10 +1,11 @@ import { type Decorator, type Meta, type StoryObj } from '@storybook/react'; -import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; +import { expect, userEvent, waitFor, within } from '@storybook/test'; import { useEffect } from 'react'; import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { getFieldInputEventContextProviderWithJestMocks } from '@/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix'; import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId'; @@ -12,10 +13,10 @@ import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentTyp import { FieldMetadataType } from 'twenty-shared/types'; import { type FieldRatingValue } from '../../../../types/FieldMetadata'; import { useRatingField } from '../../../hooks/useRatingField'; -import { - RatingFieldInput, - type RatingFieldInputProps, -} from '../RatingFieldInput'; +import { RatingFieldInput } from '../RatingFieldInput'; + +const { FieldInputEventContextProviderWithJestMocks, handleSubmitMocked } = + getFieldInputEventContextProviderWithJestMocks(); const RatingFieldValueSetterEffect = ({ value, @@ -31,7 +32,7 @@ const RatingFieldValueSetterEffect = ({ return <>; }; -type RatingFieldInputWithContextProps = RatingFieldInputProps & { +type RatingFieldInputWithContextProps = { value: FieldRatingValue; recordId: string; }; @@ -39,7 +40,6 @@ type RatingFieldInputWithContextProps = RatingFieldInputProps & { const RatingFieldInputWithContext = ({ recordId, value, - onSubmit, }: RatingFieldInputWithContextProps) => { const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); @@ -83,17 +83,17 @@ const RatingFieldInputWithContext = ({ }} > - + + + ); }; -const submitJestFn = fn(); - const clearMocksDecorator: Decorator = (Story, context) => { if (context.parameters.clearMocks === true) { - submitJestFn.mockClear(); + handleSubmitMocked.mockClear(); } return ; }; @@ -103,7 +103,7 @@ const meta: Meta = { component: RatingFieldInputWithContext, args: { value: '3', - onSubmit: submitJestFn, + onSubmit: handleSubmitMocked, }, argTypes: { onSubmit: { control: false }, @@ -124,7 +124,7 @@ export const Submit: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - expect(submitJestFn).toHaveBeenCalledTimes(0); + expect(handleSubmitMocked).toHaveBeenCalledTimes(0); const input = canvas.getByRole('slider', { name: 'Rating' }); const firstStar = input.firstElementChild; @@ -136,7 +136,7 @@ export const Submit: Story = { await userEvent.click(firstStar); await waitFor(() => { - expect(submitJestFn).toHaveBeenCalledTimes(1); + expect(handleSubmitMocked).toHaveBeenCalledTimes(1); }); }, }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx index ad5a477f725..4020cd798ce 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx @@ -1,5 +1,5 @@ import { type Decorator, type Meta, type StoryObj } from '@storybook/react'; -import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; +import { expect, userEvent, waitFor, within } from '@storybook/test'; import { useEffect } from 'react'; import { useSetRecoilState } from 'recoil'; @@ -22,10 +22,9 @@ import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentTyp import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import { FieldMetadataType } from 'twenty-shared/types'; import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing'; -import { - RelationToOneFieldInput, - type RelationToOneFieldInputProps, -} from '../RelationToOneFieldInput'; +import { RelationToOneFieldInput } from '../RelationToOneFieldInput'; + +import { getFieldInputEventContextProviderWithJestMocks } from './utils/getFieldInputEventContextProviderWithJestMocks'; const RelationWorkspaceSetterEffect = () => { const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState); @@ -50,15 +49,19 @@ const RelationWorkspaceSetterEffect = () => { return <>; }; -type RelationToOneFieldInputWithContextProps = RelationToOneFieldInputProps & { +const { + FieldInputEventContextProviderWithJestMocks, + handleSubmitMocked, + handleCancelMocked, +} = getFieldInputEventContextProviderWithJestMocks(); + +type RelationToOneFieldInputWithContextProps = { value: number; recordId: string; }; const RelationToOneFieldInputWithContext = ({ recordId, - onSubmit, - onCancel, }: RelationToOneFieldInputWithContextProps) => { const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); @@ -101,8 +104,11 @@ const RelationToOneFieldInputWithContext = ({ instanceId: 'relation-to-one-field-input-123-Relation', }} > - - + + + + +
@@ -110,13 +116,10 @@ const RelationToOneFieldInputWithContext = ({ ); }; -const submitJestFn = fn(); -const cancelJestFn = fn(); - const clearMocksDecorator: Decorator = (Story, context) => { if (context.parameters.clearMocks === true) { - submitJestFn.mockClear(); - cancelJestFn.mockClear(); + handleSubmitMocked.mockClear(); + handleCancelMocked.mockClear(); } return ; }; @@ -126,8 +129,8 @@ const meta: Meta = { component: RelationToOneFieldInputWithContext, args: { useEditButton: true, - onSubmit: submitJestFn, - onCancel: cancelJestFn, + onSubmit: handleSubmitMocked, + onCancel: handleCancelMocked, }, argTypes: { onSubmit: { control: false }, @@ -154,7 +157,7 @@ export const Submit: Story = { play: async () => { const canvas = within(getCanvasElementForDropdownTesting()); - expect(submitJestFn).toHaveBeenCalledTimes(0); + expect(handleSubmitMocked).toHaveBeenCalledTimes(0); const item = await canvas.findByText('Linkedin', undefined, { timeout: 3000, @@ -163,7 +166,7 @@ export const Submit: Story = { await userEvent.click(item); await waitFor(() => { - expect(submitJestFn).toHaveBeenCalledTimes(1); + expect(handleSubmitMocked).toHaveBeenCalledTimes(1); }); }, }; @@ -172,12 +175,12 @@ export const Cancel: Story = { play: async () => { const canvas = within(getCanvasElementForDropdownTesting()); - expect(cancelJestFn).toHaveBeenCalledTimes(0); + expect(handleCancelMocked).toHaveBeenCalledTimes(0); await canvas.findByText('Linkedin', undefined, { timeout: 3000 }); const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); await userEvent.click(emptyDiv); - expect(cancelJestFn).toHaveBeenCalledTimes(1); + expect(handleCancelMocked).toHaveBeenCalledTimes(1); }, }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RichTextFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RichTextFieldInput.stories.tsx index 6af5b11f6ba..f479bf012e3 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RichTextFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RichTextFieldInput.stories.tsx @@ -1,7 +1,6 @@ -import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; +import { expect, userEvent, waitFor, within } from '@storybook/test'; import { useEffect } from 'react'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix'; @@ -14,29 +13,25 @@ import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { RichTextFieldInput } from '../RichTextFieldInput'; +import { getFieldInputEventContextProviderWithJestMocks } from './utils/getFieldInputEventContextProviderWithJestMocks'; -const clickOutsideJestFn = fn(); -const escapeJestFn = fn(); +const targetableObjectId = 'test-id'; -type RichTextFieldInputWithContextProps = { - targetableObjectId?: string; - onClickOutside?: typeof clickOutsideJestFn; - onEscape?: typeof escapeJestFn; -}; +const { + FieldInputEventContextProviderWithJestMocks, + handleEscapeMocked, + handleClickoutsideMocked, +} = getFieldInputEventContextProviderWithJestMocks(); const clearMocksDecorator: Decorator = (Story, context) => { if (context.parameters.clearMocks !== false) { - clickOutsideJestFn.mockClear(); - escapeJestFn.mockClear(); + handleClickoutsideMocked.mockClear(); + handleEscapeMocked.mockClear(); } return ; }; -const RichTextFieldInputWithContext = ({ - targetableObjectId = 'test-id', - onClickOutside, - onEscape, -}: RichTextFieldInputWithContextProps) => { +const RichTextFieldInputWithContext = () => { const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); const instanceId = getRecordFieldInputInstanceId({ @@ -78,14 +73,9 @@ const RichTextFieldInputWithContext = ({ isRecordFieldReadOnly: false, }} > - + + +
@@ -97,8 +87,8 @@ const meta: Meta = { component: RichTextFieldInputWithContext, args: { targetableObjectId: 'test-id', - onClickOutside: clickOutsideJestFn, - onEscape: escapeJestFn, + onClickOutside: handleClickoutsideMocked, + onEscape: handleEscapeMocked, }, argTypes: { onClickOutside: { control: false }, @@ -124,13 +114,13 @@ export const Default: Story = {}; export const Escape: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - expect(escapeJestFn).toHaveBeenCalledTimes(0); + expect(handleEscapeMocked).toHaveBeenCalledTimes(0); await canvas.findByTestId('click-outside-element'); await userEvent.keyboard('{esc}'); await waitFor(() => { - expect(escapeJestFn).toHaveBeenCalledTimes(1); + expect(handleEscapeMocked).toHaveBeenCalledTimes(1); }); }, }; @@ -138,14 +128,14 @@ export const Escape: Story = { export const ClickOutside: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + expect(handleClickoutsideMocked).toHaveBeenCalledTimes(0); const outsideElement = await canvas.findByTestId('click-outside-element'); await userEvent.click(outsideElement); await waitFor(() => { - expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + expect(handleClickoutsideMocked).toHaveBeenCalledTimes(1); }); }, }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx index 4c4372605e1..88c84c30861 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx @@ -1,9 +1,10 @@ -import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; +import { expect, userEvent, waitFor, within } from '@storybook/test'; import { useEffect, useState } from 'react'; import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { getFieldInputEventContextProviderWithJestMocks } from '@/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix'; import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId'; @@ -13,7 +14,8 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { useTextField } from '../../../hooks/useTextField'; -import { TextFieldInput, type TextFieldInputProps } from '../TextFieldInput'; +import { TextFieldInput } from '../TextFieldInput'; + const TextFieldValueSetterEffect = ({ value }: { value: string }) => { const { setFieldValue } = useTextField(); @@ -24,7 +26,16 @@ const TextFieldValueSetterEffect = ({ value }: { value: string }) => { return <>; }; -type TextFieldInputWithContextProps = TextFieldInputProps & { +const { + FieldInputEventContextProviderWithJestMocks, + handleEscapeMocked, + handleClickoutsideMocked, + handleEnterMocked, + handleShiftTabMocked, + handleTabMocked, +} = getFieldInputEventContextProviderWithJestMocks(); + +type TextFieldInputWithContextProps = { value: string; recordId: string; }; @@ -32,11 +43,6 @@ type TextFieldInputWithContextProps = TextFieldInputProps & { const TextFieldInputWithContext = ({ recordId, value, - onEnter, - onEscape, - onClickOutside, - onTab, - onShiftTab, }: TextFieldInputWithContextProps) => { const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); const [isReady, setIsReady] = useState(false); @@ -85,14 +91,10 @@ const TextFieldInputWithContext = ({ isRecordFieldReadOnly: false, }} > - - + + + + {isReady &&
}
@@ -100,19 +102,13 @@ const TextFieldInputWithContext = ({ ); }; -const enterJestFn = fn(); -const escapeJestfn = fn(); -const clickOutsideJestFn = fn(); -const tabJestFn = fn(); -const shiftTabJestFn = fn(); - const clearMocksDecorator: Decorator = (Story, context) => { if (context.parameters.clearMocks === true) { - enterJestFn.mockClear(); - escapeJestfn.mockClear(); - clickOutsideJestFn.mockClear(); - tabJestFn.mockClear(); - shiftTabJestFn.mockClear(); + handleEnterMocked.mockClear(); + handleEscapeMocked.mockClear(); + handleClickoutsideMocked.mockClear(); + handleTabMocked.mockClear(); + handleShiftTabMocked.mockClear(); } return ; }; @@ -122,11 +118,11 @@ const meta: Meta = { component: TextFieldInputWithContext, args: { value: 'text', - onEnter: enterJestFn, - onEscape: escapeJestfn, - onClickOutside: clickOutsideJestFn, - onTab: tabJestFn, - onShiftTab: shiftTabJestFn, + onEnter: handleEnterMocked, + onEscape: handleEscapeMocked, + onClickOutside: handleClickoutsideMocked, + onTab: handleTabMocked, + onShiftTab: handleShiftTabMocked, }, argTypes: { onEnter: { control: false }, @@ -151,13 +147,13 @@ export const Enter: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - expect(enterJestFn).toHaveBeenCalledTimes(0); + expect(handleEnterMocked).toHaveBeenCalledTimes(0); await canvas.findByTestId('is-ready-marker'); await userEvent.keyboard('{enter}'); await waitFor(() => { - expect(enterJestFn).toHaveBeenCalledTimes(1); + expect(handleEnterMocked).toHaveBeenCalledTimes(1); }); }, }; @@ -166,13 +162,13 @@ export const Escape: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - expect(escapeJestfn).toHaveBeenCalledTimes(0); + expect(handleEscapeMocked).toHaveBeenCalledTimes(0); await canvas.findByTestId('is-ready-marker'); await userEvent.keyboard('{esc}'); await waitFor(() => { - expect(escapeJestfn).toHaveBeenCalledTimes(1); + expect(handleEscapeMocked).toHaveBeenCalledTimes(1); }); }, }; @@ -181,7 +177,7 @@ export const ClickOutside: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + expect(handleClickoutsideMocked).toHaveBeenCalledTimes(0); const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); await canvas.findByTestId('is-ready-marker'); @@ -189,7 +185,7 @@ export const ClickOutside: Story = { await userEvent.click(emptyDiv); await waitFor(() => { - expect(clickOutsideJestFn).toHaveBeenCalled(); + expect(handleClickoutsideMocked).toHaveBeenCalled(); }); }, }; @@ -200,12 +196,12 @@ export const Tab: Story = { await canvas.findByTestId('is-ready-marker'); - expect(tabJestFn).toHaveBeenCalledTimes(0); + expect(handleTabMocked).toHaveBeenCalledTimes(0); await userEvent.keyboard('{tab}'); await waitFor(() => { - expect(tabJestFn).toHaveBeenCalledTimes(1); + expect(handleTabMocked).toHaveBeenCalledTimes(1); }); }, }; @@ -216,12 +212,12 @@ export const ShiftTab: Story = { await canvas.findByTestId('is-ready-marker'); - expect(shiftTabJestFn).toHaveBeenCalledTimes(0); + expect(handleShiftTabMocked).toHaveBeenCalledTimes(0); await userEvent.keyboard('{shift>}{tab}'); await waitFor(() => { - expect(shiftTabJestFn).toHaveBeenCalledTimes(1); + expect(handleShiftTabMocked).toHaveBeenCalledTimes(1); }); }, }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks.tsx new file mode 100644 index 00000000000..e681515b188 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks.tsx @@ -0,0 +1,46 @@ +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; +import { fn } from '@storybook/test'; +import { type PropsWithChildren } from 'react'; + +type FieldInputEventContextProviderWithJestMocksProps = PropsWithChildren; + +export const getFieldInputEventContextProviderWithJestMocks = () => { + const handleSubmitMocked = fn(); + const handleCancelMocked = fn(); + const handleClickoutsideMocked = fn(); + const handleEnterMocked = fn(); + const handleEscapeMocked = fn(); + const handleShiftTabMocked = fn(); + const handleTabMocked = fn(); + + const FieldInputEventContextProviderWithJestMocks = ({ + children, + }: FieldInputEventContextProviderWithJestMocksProps) => { + return ( + + {children} + + ); + }; + + return { + FieldInputEventContextProviderWithJestMocks, + handleSubmitMocked, + handleCancelMocked, + handleClickoutsideMocked, + handleEnterMocked, + handleEscapeMocked, + handleShiftTabMocked, + handleTabMocked, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputEvent.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputEvent.ts deleted file mode 100644 index d8f033259ce..00000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputEvent.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type FieldInputEvent = (persist: () => void) => void; - -export type FieldInputClickOutsideEvent = ( - persist: () => void, - event: MouseEvent | TouchEvent, -) => void; diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx index 865a70a2457..20a43d4fc61 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx @@ -5,14 +5,16 @@ import { FieldInput } from '@/object-record/record-field/components/FieldInput'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider'; import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon'; -import { - type FieldInputClickOutsideEvent, - type FieldInputEvent, -} from '@/object-record/record-field/types/FieldInputEvent'; import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly'; import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode'; +import { + FieldInputEventContext, + type FieldInputClickOutsideEvent, + type FieldInputEvent, +} from '@/object-record/record-field/contexts/FieldInputEventContext'; +import { usePersistFieldFromFieldInputContext } from '@/object-record/record-field/hooks/usePersistFieldFromFieldInputContext'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { isInlineCellInEditModeFamilyState } from '@/object-record/record-inline-cell/states/isInlineCellInEditModeFamilyState'; import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField'; @@ -98,13 +100,22 @@ export const RecordInlineCell = ({ goBackToPreviousDropdownFocusId, ]); - const handleEnter: FieldInputEvent = (persistField) => { - persistField(); + const { persistFieldFromFieldInputContext } = + usePersistFieldFromFieldInputContext(); + + const handleEnter: FieldInputEvent = ({ newValue, skipPersist }) => { + if (skipPersist !== true) { + persistFieldFromFieldInputContext(newValue); + } + closeInlineCell(); }; - const handleSubmit: FieldInputEvent = (persistField) => { - persistField(); + const handleSubmit: FieldInputEvent = ({ newValue, skipPersist }) => { + if (skipPersist !== true) { + persistFieldFromFieldInputContext(newValue); + } + closeInlineCell(); }; @@ -112,23 +123,37 @@ export const RecordInlineCell = ({ closeInlineCell(); }; - const handleEscape = () => { + const handleEscape: FieldInputEvent = ({ newValue, skipPersist }) => { + if (skipPersist !== true) { + persistFieldFromFieldInputContext(newValue); + } + closeInlineCell(); }; - const handleTab: FieldInputEvent = (persistField) => { - persistField(); + const handleTab: FieldInputEvent = ({ newValue, skipPersist }) => { + if (skipPersist !== true) { + persistFieldFromFieldInputContext(newValue); + } + closeInlineCell(); }; - const handleShiftTab: FieldInputEvent = (persistField) => { - persistField(); + const handleShiftTab: FieldInputEvent = ({ newValue, skipPersist }) => { + if (skipPersist !== true) { + persistFieldFromFieldInputContext(newValue); + } + closeInlineCell(); }; - const handleClickOutside: FieldInputClickOutsideEvent = useRecoilCallback( + const handleClickOutside = useRecoilCallback( ({ snapshot }) => - (persistField, event) => { + ({ + event, + newValue, + skipPersist, + }: Parameters[0]) => { const currentDropdownFocusId = snapshot .getLoadable(activeDropdownFocusIdState) .getValue(); @@ -142,13 +167,22 @@ export const RecordInlineCell = ({ if (currentDropdownFocusId !== expectedDropdownFocusId) { return; } - event.preventDefault(); - event.stopImmediatePropagation(); - persistField(); + event?.preventDefault(); + event?.stopImmediatePropagation(); + + if (skipPersist !== true) { + persistFieldFromFieldInputContext(newValue); + } + closeInlineCell(); }, - [closeInlineCell, recordId, fieldDefinition.fieldMetadataId], + [ + closeInlineCell, + recordId, + fieldDefinition.fieldMetadataId, + persistFieldFromFieldInputContext, + ], ); const { getIcon } = useIcons(); @@ -163,18 +197,7 @@ export const RecordInlineCell = ({ labelWidth: fieldDefinition.labelWidth, showLabel: fieldDefinition.showLabel, isCentered, - editModeContent: ( - - ), + editModeContent: , displayModeContent: , isDisplayModeFixHeight: isDisplayModeFixHeight, editModeContentOnly: isFieldInputOnly, @@ -184,10 +207,22 @@ export const RecordInlineCell = ({ }; return ( - - - - - + + + + + + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx index 6a5c3175441..7d45c094da9 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx @@ -20,10 +20,10 @@ import { RecordDetailDuplicatesSection } from '@/object-record/record-show/recor import { RecordDetailRelationSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSection'; import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId'; import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported'; +import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId'; import { useIsInRightDrawerOrThrow } from '@/ui/layout/right-drawer/contexts/RightDrawerContext'; import { isDefined } from 'twenty-shared/utils'; import { FieldMetadataType } from '~/generated-metadata/graphql'; -import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId'; type FieldsCardProps = { objectNameSingular: string; diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx index 7c324e43afd..2eb86269c16 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx @@ -17,9 +17,9 @@ import { type RecordUpdateHook, type RecordUpdateHookParams, } from '@/object-record/record-field/contexts/FieldContext'; +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordReadOnly'; import { isRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isRecordFieldReadOnly'; -import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { type FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; @@ -113,6 +113,8 @@ export const RecordDetailRelationRecordsListItem = ({ isRecordFieldReadOnly: parentIsRecordFieldReadOnly, } = useContext(FieldContext); + const { onSubmit } = useContext(FieldInputEventContext); + const { openModal } = useModal(); const { @@ -137,8 +139,6 @@ export const RecordDetailRelationRecordsListItem = ({ relationObjectMetadataItem.id, ); - const persistField = usePersistField(); - const { updateOneRecord: updateOneRelationRecord } = useUpdateOneRecord({ objectNameSingular: relationObjectMetadataNameSingular, }); @@ -189,7 +189,7 @@ export const RecordDetailRelationRecordsListItem = ({ if (!relationFieldMetadataItem?.name) return; if (isToOneObject) { - persistField(null); + onSubmit?.({ newValue: null }); } else { updateOneRelationRecord({ idToUpdate: relationRecord.id, diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx index 9775de4f2de..a62ce5eec10 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx @@ -2,9 +2,15 @@ import { useContext } from 'react'; import { useRecoilValue } from 'recoil'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { type RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { + FieldInputEventContext, + type FieldInputEvent, +} from '@/object-record/record-field/contexts/FieldInputEventContext'; +import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; import { type FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { RecordDetailRelationRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsList'; import { RecordDetailRelationSectionDropdown } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdown'; @@ -40,6 +46,7 @@ export const RecordDetailRelationSection = ({ relationFieldMetadataId, relationObjectMetadataNameSingular, relationType, + objectMetadataNameSingular, } = fieldDefinition.metadata as FieldRelationMetadata; const isMobile = useIsMobile(); @@ -119,34 +126,61 @@ export const RecordDetailRelationSection = ({ }, }); + // TODO: refactor this when we have refactored columnDefinitions and field definitions because + // we should be able to get the objectMetadataItem from a context way more easily + const { objectMetadataItems } = useObjectMetadataItems(); + + const objectMetadataItem = objectMetadataItems.find( + (objectMetadataItemToFind) => + objectMetadataItemToFind.nameSingular === objectMetadataNameSingular, + ); + + const persistField = usePersistField({ + objectMetadataItemId: objectMetadataItem?.id ?? '', + }); + + const handleSubmit: FieldInputEvent = ({ newValue }) => { + persistField({ + recordId: recordId, + fieldDefinition, + valueToPersist: newValue, + }); + }; + if (loading) return null; const relationRecordsCount = relationAggregateResult?.id?.COUNT ?? 0; return ( - - 0 - ? t`All (${relationRecordsCount})` - : '', - } - : undefined - } - hideRightAdornmentOnMouseLeave={!isDropdownOpen && !isMobile} - areRecordsAvailable={relationRecords.length > 0} - rightAdornment={ - - } - /> - {relationRecords.length > 0 && ( - - )} - + + + 0 + ? t`All (${relationRecordsCount})` + : '', + } + : undefined + } + hideRightAdornmentOnMouseLeave={!isDropdownOpen && !isMobile} + areRecordsAvailable={relationRecords.length > 0} + rightAdornment={ + + } + /> + {relationRecords.length > 0 && ( + + )} + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdownToOne.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdownToOne.tsx index 9007b4570b3..c1cfa6b06be 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdownToOne.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdownToOne.tsx @@ -4,7 +4,7 @@ import { useRecoilValue } from 'recoil'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; -import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer'; import { type FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker'; @@ -74,7 +74,7 @@ export const RecordDetailRelationSectionDropdownToOne = () => { setSingleRecordPickerSearchFilter(''); }, [setSingleRecordPickerSearchFilter]); - const persistField = usePersistField(); + const { onSubmit } = useContext(FieldInputEventContext); const handleRelationPickerEntitySelected = ( selectedRelationEntity?: SingleRecordPickerRecord, @@ -83,7 +83,7 @@ export const RecordDetailRelationSectionDropdownToOne = () => { if (!selectedRelationEntity?.id || !relationFieldMetadataItem?.name) return; - persistField(selectedRelationEntity.record); + onSubmit?.({ newValue: selectedRelationEntity.record }); }; const { createNewRecordAndOpenRightDrawer } = diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx index c686d45ee76..00bba1942aa 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx @@ -1,33 +1,41 @@ import { FieldInput } from '@/object-record/record-field/components/FieldInput'; -import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { usePersistFieldFromFieldInputContext } from '@/object-record/record-field/hooks/usePersistFieldFromFieldInputContext'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; + import { + FieldInputEventContext, type FieldInputClickOutsideEvent, type FieldInputEvent, -} from '@/object-record/record-field/types/FieldInputEvent'; +} from '@/object-record/record-field/contexts/FieldInputEventContext'; import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext'; import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector'; import { useAvailableComponentInstanceId } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceId'; -import { useContext } from 'react'; import { useRecoilCallback } from 'recoil'; export const RecordTableCellFieldInput = () => { const { onMoveFocus, onCloseTableCell } = useRecordTableBodyContextOrThrow(); - const { isRecordFieldReadOnly: isReadOnly } = useContext(FieldContext); + const instanceId = useAvailableComponentInstanceId( RecordFieldComponentInstanceContext, ); - const handleEnter: FieldInputEvent = (persistField) => { - persistField(); + const { persistFieldFromFieldInputContext } = + usePersistFieldFromFieldInputContext(); + + const handleEnter: FieldInputEvent = ({ newValue, skipPersist }) => { + if (skipPersist !== true) { + persistFieldFromFieldInputContext(newValue); + } onCloseTableCell(); onMoveFocus('down'); }; - const handleSubmit: FieldInputEvent = (persistField) => { - persistField(); + const handleSubmit: FieldInputEvent = ({ newValue, skipPersist }) => { + if (skipPersist !== true) { + persistFieldFromFieldInputContext(newValue); + } onCloseTableCell(); }; @@ -38,7 +46,7 @@ export const RecordTableCellFieldInput = () => { const handleClickOutside: FieldInputClickOutsideEvent = useRecoilCallback( ({ snapshot }) => - (persistField, event) => { + ({ newValue, event, skipPersist }) => { const currentFocusId = snapshot .getLoadable(currentFocusIdSelector) .getValue(); @@ -46,44 +54,57 @@ export const RecordTableCellFieldInput = () => { if (currentFocusId !== instanceId) { return; } - event.preventDefault(); - event.stopImmediatePropagation(); - persistField(); + event?.preventDefault(); + event?.stopImmediatePropagation(); + + if (skipPersist !== true) { + persistFieldFromFieldInputContext(newValue); + } + onCloseTableCell(); }, - [onCloseTableCell, instanceId], + [onCloseTableCell, instanceId, persistFieldFromFieldInputContext], ); - const handleEscape: FieldInputEvent = (persistField) => { - persistField(); + const handleEscape: FieldInputEvent = ({ newValue, skipPersist }) => { + if (skipPersist !== true) { + persistFieldFromFieldInputContext(newValue); + } onCloseTableCell(); }; - const handleTab: FieldInputEvent = (persistField) => { - persistField(); + const handleTab: FieldInputEvent = ({ newValue, skipPersist }) => { + if (skipPersist !== true) { + persistFieldFromFieldInputContext(newValue); + } onCloseTableCell(); onMoveFocus('right'); }; - const handleShiftTab: FieldInputEvent = (persistField) => { - persistField(); + const handleShiftTab: FieldInputEvent = ({ newValue, skipPersist }) => { + if (skipPersist !== true) { + persistFieldFromFieldInputContext(newValue); + } onCloseTableCell(); onMoveFocus('left'); }; return ( - + + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleCell.tsx b/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleCell.tsx index 44d5ab1208a..e4a5c114d03 100644 --- a/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleCell.tsx @@ -1,13 +1,15 @@ -import { useContext } from 'react'; +import { useCallback, useContext } from 'react'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider'; import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly'; + import { + FieldInputEventContext, type FieldInputClickOutsideEvent, type FieldInputEvent, -} from '@/object-record/record-field/types/FieldInputEvent'; - +} from '@/object-record/record-field/contexts/FieldInputEventContext'; +import { usePersistFieldFromFieldInputContext } from '@/object-record/record-field/hooks/usePersistFieldFromFieldInputContext'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { RecordTitleCellContainer } from '@/object-record/record-title-cell/components/RecordTitleCellContainer'; import { @@ -19,6 +21,7 @@ import { RecordTitleCellFieldInput } from '@/object-record/record-title-cell/com import { useRecordTitleCell } from '@/object-record/record-title-cell/hooks/useRecordTitleCell'; import { type RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType'; import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId'; +import { useRecoilCallback } from 'recoil'; type RecordTitleCellProps = { loading?: boolean; @@ -38,53 +41,77 @@ export const RecordTitleCell = ({ const { closeRecordTitleCell } = useRecordTitleCell(); - const closeCell = () => { + const closeCell = useCallback(() => { closeRecordTitleCell({ recordId, fieldName: fieldDefinition.metadata.fieldName, containerType, }); - }; + }, [closeRecordTitleCell, containerType, fieldDefinition, recordId]); - const handleEnter: FieldInputEvent = (persistField) => { - closeCell(); - persistField(); - }; + const { persistFieldFromFieldInputContext } = + usePersistFieldFromFieldInputContext(); + + const handleEnter: FieldInputEvent = ({ newValue, skipPersist }) => { + if (skipPersist !== true) { + persistFieldFromFieldInputContext(newValue); + } - const handleEscape = () => { closeCell(); }; - const handleTab: FieldInputEvent = (persistField) => { + const handleClickOutside: FieldInputClickOutsideEvent = useRecoilCallback( + () => + ({ newValue, skipPersist }) => { + if (skipPersist !== true) { + persistFieldFromFieldInputContext(newValue); + } + + closeCell(); + }, + [closeCell, persistFieldFromFieldInputContext], + ); + + const handleEscape: FieldInputEvent = () => { closeCell(); - persistField(); }; - const handleShiftTab: FieldInputEvent = (persistField) => { + const handleTab: FieldInputEvent = ({ newValue, skipPersist }) => { + if (skipPersist !== true) { + persistFieldFromFieldInputContext(newValue); + } + closeCell(); - persistField(); }; - const handleClickOutside: FieldInputClickOutsideEvent = (persistField) => { + const handleShiftTab: FieldInputEvent = ({ newValue, skipPersist }) => { + if (skipPersist !== true) { + persistFieldFromFieldInputContext(newValue); + } + closeCell(); - persistField(); }; const recordTitleCellContextValue: RecordTitleCellContextProps = { editModeContent: ( - + + + ), displayModeContent: ( diff --git a/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleCellFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleCellFieldInput.tsx index b19447fea3a..7871f7f4e12 100644 --- a/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleCellFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleCellFieldInput.tsx @@ -1,7 +1,6 @@ import { useContext } from 'react'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; -import { type FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent'; import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName'; import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText'; import { RecordTitleCellTextFieldInput } from '@/object-record/record-title-cell/components/RecordTitleCellTextFieldInput'; @@ -9,25 +8,12 @@ import { RecordTitleFullNameFieldInput } from '@/object-record/record-title-cell type RecordTitleCellFieldInputProps = { instanceId: string; - onClickOutside?: ( - persist: () => void, - event: MouseEvent | TouchEvent, - ) => void; - onEnter?: FieldInputEvent; - onEscape?: FieldInputEvent; - onTab?: FieldInputEvent; - onShiftTab?: FieldInputEvent; sizeVariant?: 'xs' | 'md'; }; export const RecordTitleCellFieldInput = ({ instanceId, sizeVariant, - onEnter, - onEscape, - onShiftTab, - onTab, - onClickOutside, }: RecordTitleCellFieldInputProps) => { const { fieldDefinition } = useContext(FieldContext); @@ -40,22 +26,10 @@ export const RecordTitleCellFieldInput = ({ {isFieldText(fieldDefinition) ? ( ) : isFieldFullName(fieldDefinition) ? ( - + ) : null} ); diff --git a/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleCellTextFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleCellTextFieldInput.tsx index 1552af56d89..d6d53baa57b 100644 --- a/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleCellTextFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleCellTextFieldInput.tsx @@ -1,33 +1,20 @@ -import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; import { useTextField } from '@/object-record/record-field/meta-types/hooks/useTextField'; import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents'; -import { - type FieldInputClickOutsideEvent, - type FieldInputEvent, -} from '@/object-record/record-field/types/FieldInputEvent'; + import { TextInput } from '@/ui/input/components/TextInput'; -import { useRef } from 'react'; +import { useContext, useRef } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { turnIntoUndefinedIfWhitespacesOnly } from '~/utils/string/turnIntoUndefinedIfWhitespacesOnly'; type RecordTitleCellTextFieldInputProps = { instanceId: string; - onClickOutside?: FieldInputClickOutsideEvent; - onEnter?: FieldInputEvent; - onEscape?: FieldInputEvent; - onTab?: FieldInputEvent; - onShiftTab?: FieldInputEvent; sizeVariant?: 'xs' | 'md'; }; export const RecordTitleCellTextFieldInput = ({ instanceId, sizeVariant, - onEnter, - onEscape, - onClickOutside, - onTab, - onShiftTab, }: RecordTitleCellTextFieldInputProps) => { const { fieldDefinition, draftValue, setDraftValue } = useTextField(); @@ -37,26 +24,28 @@ export const RecordTitleCellTextFieldInput = ({ setDraftValue(turnIntoUndefinedIfWhitespacesOnly(newText)); }; - const persistField = usePersistField(); + const { onEnter, onEscape, onClickOutside, onTab, onShiftTab } = useContext( + FieldInputEventContext, + ); useRegisterInputEvents({ focusId: instanceId, inputRef: wrapperRef, inputValue: draftValue ?? '', onEnter: (inputValue) => { - onEnter?.(() => persistField(inputValue)); + onEnter?.({ newValue: inputValue }); }, onEscape: (inputValue) => { - onEscape?.(() => persistField(inputValue)); + onEscape?.({ newValue: inputValue }); }, onClickOutside: (event, inputValue) => { - onClickOutside?.(() => persistField(inputValue), event); + onClickOutside?.({ newValue: inputValue, event }); }, onTab: (inputValue) => { - onTab?.(() => persistField(inputValue)); + onTab?.({ newValue: inputValue }); }, onShiftTab: (inputValue) => { - onShiftTab?.(() => persistField(inputValue)); + onShiftTab?.({ newValue: inputValue }); }, }); diff --git a/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleFullNameFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleFullNameFieldInput.tsx index 00264f0a4b9..4842fe84e2f 100644 --- a/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleFullNameFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleFullNameFieldInput.tsx @@ -1,33 +1,25 @@ +import { FieldInputEventContext } from '@/object-record/record-field/contexts/FieldInputEventContext'; import { useFullNameField } from '@/object-record/record-field/meta-types/hooks/useFullNameField'; import { FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/FirstNamePlaceholder'; import { LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/LastNamePlaceholder'; import { isDoubleTextFieldEmpty } from '@/object-record/record-field/meta-types/input/utils/isDoubleTextFieldEmpty'; import { type FieldDoubleText } from '@/object-record/record-field/types/FieldDoubleText'; -import { - type FieldInputClickOutsideEvent, - type FieldInputEvent, -} from '@/object-record/record-field/types/FieldInputEvent'; + +import { useContext } from 'react'; import { RecordTitleDoubleTextInput } from './RecordTitleDoubleTextInput'; type RecordTitleFullNameFieldInputProps = { - onClickOutside?: FieldInputClickOutsideEvent; - onEnter?: FieldInputEvent; - onEscape?: FieldInputEvent; - onTab?: FieldInputEvent; - onShiftTab?: FieldInputEvent; sizeVariant?: 'xs' | 'md'; }; export const RecordTitleFullNameFieldInput = ({ - onEnter, - onEscape, - onClickOutside, - onTab, - onShiftTab, sizeVariant, }: RecordTitleFullNameFieldInputProps) => { - const { draftValue, setDraftValue, persistFullNameField } = - useFullNameField(); + const { draftValue, setDraftValue } = useFullNameField(); + + const { onEnter, onEscape, onClickOutside, onTab, onShiftTab } = useContext( + FieldInputEventContext, + ); const convertToFullName = (newDoubleText: FieldDoubleText) => { return { @@ -45,29 +37,26 @@ export const RecordTitleFullNameFieldInput = ({ }; const handleEnter = (newDoubleText: FieldDoubleText) => { - onEnter?.(() => persistFullNameField(convertToFullName(newDoubleText))); + onEnter?.({ newValue: convertToFullName(newDoubleText) }); }; const handleEscape = (newDoubleText: FieldDoubleText) => { - onEscape?.(() => persistFullNameField(convertToFullName(newDoubleText))); + onEscape?.({ newValue: convertToFullName(newDoubleText) }); }; const handleClickOutside = ( event: MouseEvent | TouchEvent, newDoubleText: FieldDoubleText, ) => { - onClickOutside?.( - () => persistFullNameField(convertToFullName(newDoubleText)), - event, - ); + onClickOutside?.({ newValue: convertToFullName(newDoubleText), event }); }; const handleTab = (newDoubleText: FieldDoubleText) => { - onTab?.(() => persistFullNameField(convertToFullName(newDoubleText))); + onTab?.({ newValue: convertToFullName(newDoubleText) }); }; const handleShiftTab = (newDoubleText: FieldDoubleText) => { - onShiftTab?.(() => persistFullNameField(convertToFullName(newDoubleText))); + onShiftTab?.({ newValue: convertToFullName(newDoubleText) }); }; const handleChange = (newDoubleText: FieldDoubleText) => { diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx index c63ea436190..89c91bc10ab 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx @@ -150,14 +150,18 @@ export const SettingsDataModelFieldPreview = ({ }, defaultValue: fieldMetadataItem.defaultValue, }, - isRecordFieldReadOnly: false, + isRecordFieldReadOnly: + fieldMetadataItem.type === FieldMetadataType.BOOLEAN || + fieldMetadataItem.type === FieldMetadataType.RATING + ? true + : false, disableChipClick: true, }} > {fieldMetadataItem.type === FieldMetadataType.BOOLEAN ? ( - + ) : fieldMetadataItem.type === FieldMetadataType.RATING ? ( - + ) : ( )}