From f25d58b0d903d0270554bad64c94e7d74fc7d810 Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Wed, 10 Apr 2024 19:16:34 +0200 Subject: [PATCH] [feat][FE] Stop persisting new empty records (#4853) ## Context Closes [#4773](https://github.com/twentyhq/twenty/issues/4773) Persisting of new records is delayed to cell escape and not performed for empty records. ## How was it tested? Locally tested + jest --------- Co-authored-by: Charles Bochet --- .../__stories__/DateFieldDisplay.stories.tsx | 1 + .../__stories__/EmailFieldDisplay.stories.tsx | 1 + .../NumberFieldDisplay.stories.tsx | 1 + .../__stories__/TextFieldDisplay.stories.tsx | 1 + .../__stories__/AddressFieldInput.stories.tsx | 1 + .../__stories__/BooleanFieldInput.stories.tsx | 1 + .../__stories__/DateFieldInput.stories.tsx | 1 + .../__stories__/EmailFieldInput.stories.tsx | 1 + .../__stories__/NumberFieldInput.stories.tsx | 1 + .../__stories__/RatingFieldInput.stories.tsx | 1 + .../RelationFieldInput.stories.tsx | 1 + .../__stories__/TextFieldInput.stories.tsx | 1 + .../components/RecordShowContainer.tsx | 1 + .../components/RecordTableBody.tsx | 2 + .../components/RecordTablePendingRow.tsx | 19 +++ .../hooks/internal/useRecordTableStates.ts | 5 + .../record-table/hooks/useRecordTable.ts | 4 + .../components/RecordTableCell.tsx | 17 +- .../hooks/__tests__/useUpsertRecord.test.tsx | 152 ++++++++++++++++++ .../hooks/useCloseRecordTableCell.ts | 7 + .../hooks/useUpsertRecord.ts | 41 +++++ ...ecordTablePendingRecordIdComponentState.ts | 8 + .../pages/object-record/RecordIndexPage.tsx | 24 ++- 23 files changed, 270 insertions(+), 22 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/components/RecordTablePendingRow.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useUpsertRecord.test.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useUpsertRecord.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-table/states/recordTablePendingRecordIdComponentState.ts diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/DateFieldDisplay.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/DateFieldDisplay.stories.tsx index 5e59fa27d..d1840916a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/DateFieldDisplay.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/DateFieldDisplay.stories.tsx @@ -35,6 +35,7 @@ const meta: Meta = { iconName: 'IconCalendarEvent', metadata: { fieldName: 'Date', + objectMetadataNameSingular: 'person', }, }, hotkeyScope: 'hotkey-scope', diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/EmailFieldDisplay.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/EmailFieldDisplay.stories.tsx index cdb48e180..d296465e6 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/EmailFieldDisplay.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/EmailFieldDisplay.stories.tsx @@ -36,6 +36,7 @@ const meta: Meta = { metadata: { fieldName: 'Email', placeHolder: 'Email', + objectMetadataNameSingular: 'person', }, }, hotkeyScope: 'hotkey-scope', diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/NumberFieldDisplay.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/NumberFieldDisplay.stories.tsx index 3c3d8e229..db7fe3c57 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/NumberFieldDisplay.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/NumberFieldDisplay.stories.tsx @@ -35,6 +35,7 @@ const meta: Meta = { fieldName: 'Number', placeHolder: 'Number', isPositive: true, + objectMetadataNameSingular: 'person', }, }, hotkeyScope: 'hotkey-scope', diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/TextFieldDisplay.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/TextFieldDisplay.stories.tsx index e8ae80758..328a6533c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/TextFieldDisplay.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/TextFieldDisplay.stories.tsx @@ -34,6 +34,7 @@ const meta: Meta = { metadata: { fieldName: 'Text', placeHolder: 'Text', + objectMetadataNameSingular: 'person', }, }, hotkeyScope: 'hotkey-scope', diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/AddressFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/AddressFieldInput.stories.tsx index 1ac509d20..7241793cb 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/AddressFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/AddressFieldInput.stories.tsx @@ -58,6 +58,7 @@ const AddressInputWithContext = ({ metadata: { fieldName: 'Address', placeHolder: 'Enter text', + objectMetadataNameSingular: 'person', }, }} entityId={entityId} 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 53b026113..934d7b3e4 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 @@ -47,6 +47,7 @@ const BooleanFieldInputWithContext = ({ type: FieldMetadataType.Boolean, metadata: { fieldName: 'Boolean', + objectMetadataNameSingular: 'person', }, }} entityId={entityId} diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/DateFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/DateFieldInput.stories.tsx index 7b0fd4dca..389e5a6f3 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/DateFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/DateFieldInput.stories.tsx @@ -49,6 +49,7 @@ const DateFieldInputWithContext = ({ iconName: 'IconCalendarEvent', metadata: { fieldName: 'Date', + objectMetadataNameSingular: 'person', }, }} entityId={entityId} diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailFieldInput.stories.tsx index f3b65cec3..b96085ee8 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailFieldInput.stories.tsx @@ -50,6 +50,7 @@ const EmailFieldInputWithContext = ({ metadata: { fieldName: 'email', placeHolder: 'username@email.com', + objectMetadataNameSingular: 'person', }, }} entityId={entityId} 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 cc4626de3..832797657 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 @@ -50,6 +50,7 @@ const NumberFieldInputWithContext = ({ metadata: { fieldName: 'number', placeHolder: 'Enter number', + objectMetadataNameSingular: 'person', }, }} entityId={entityId} 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 022e5f762..2f6217150 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 @@ -50,6 +50,7 @@ const RatingFieldInputWithContext = ({ iconName: 'Icon123', metadata: { fieldName: 'Rating', + objectMetadataNameSingular: 'person', }, }} entityId={entityId} diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx index e3ad58656..a6fca0e4d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx @@ -73,6 +73,7 @@ const RelationFieldInputWithContext = ({ relationObjectMetadataNamePlural: 'workspaceMembers', relationObjectMetadataNameSingular: CoreObjectNameSingular.WorkspaceMember, + objectMetadataNameSingular: 'person', }, }} entityId={entityId} 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 6dc17f40d..09d400028 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 @@ -50,6 +50,7 @@ const TextFieldInputWithContext = ({ metadata: { fieldName: 'Text', placeHolder: 'Enter text', + objectMetadataNameSingular: 'person', }, }} entityId={entityId} diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx index 26a39d366..ac4991b5b 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx @@ -153,6 +153,7 @@ export const RecordShowContainer = ({ metadata: { fieldName: labelIdentifierFieldMetadataItem?.name || '', + objectMetadataNameSingular: objectNameSingular, }, }, useUpdateRecord: useUpdateOneObjectRecordMutation, diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBody.tsx index 3f9ad3da5..3947067f4 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBody.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBody.tsx @@ -1,6 +1,7 @@ import { useRecoilValue } from 'recoil'; import { RecordTableBodyFetchMoreLoader } from '@/object-record/record-table/components/RecordTableBodyFetchMoreLoader'; +import { RecordTablePendingRow } from '@/object-record/record-table/components/RecordTablePendingRow'; import { RecordTableRow } from '@/object-record/record-table/components/RecordTableRow'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; @@ -18,6 +19,7 @@ export const RecordTableBody = ({ return ( <> + {tableRowIds.map((recordId, rowIndex) => ( { + const { pendingRecordIdState } = useRecordTableStates(); + const pendingRecordId = useRecoilValue(pendingRecordIdState); + + if (!pendingRecordId) return; + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts index 924c17a11..1e178da5b 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts @@ -12,6 +12,7 @@ import { onColumnsChangeComponentState } from '@/object-record/record-table/stat import { onEntityCountChangeComponentState } from '@/object-record/record-table/states/onEntityCountChangeComponentState'; import { onToggleColumnFilterComponentState } from '@/object-record/record-table/states/onToggleColumnFilterComponentState'; import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState'; +import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState'; import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState'; import { allRowsSelectedStatusComponentSelector } from '@/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector'; import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector'; @@ -132,5 +133,9 @@ export const useRecordTableStates = (recordTableId?: string) => { visibleTableColumnsComponentSelector, scopeId, ), + pendingRecordIdState: extractComponentState( + recordTablePendingRecordIdComponentState, + scopeId, + ), }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts index 648b0f440..4ff7c6552 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts @@ -44,6 +44,7 @@ export const useRecordTable = (props?: useRecordTableProps) => { selectedRowIdsSelector, onToggleColumnFilterState, onToggleColumnSortState, + pendingRecordIdState, } = useRecordTableStates(recordTableId); const setAvailableTableColumns = useRecoilCallback( @@ -194,6 +195,8 @@ export const useRecordTable = (props?: useRecordTableProps) => { const isSomeCellInEditModeState = useGetIsSomeCellInEditModeState(recordTableId); + const setPendingRecordId = useSetRecoilState(pendingRecordIdState); + return { scopeId, onColumnsChange, @@ -222,5 +225,6 @@ export const useRecordTable = (props?: useRecordTableProps) => { setHasUserSelectedAllRows, setOnToggleColumnFilter, setOnToggleColumnSort, + setPendingRecordId, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCell.tsx index 1c997b125..7aee69ebc 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCell.tsx @@ -7,6 +7,7 @@ import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEv import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus'; import { RecordTableCellContainer } from '@/object-record/record-table/record-table-cell/components/RecordTableCellContainer'; import { useCloseRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCell'; +import { useUpsertRecord } from '@/object-record/record-table/record-table-cell/hooks/useUpsertRecord'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; export const RecordTableCell = ({ @@ -15,19 +16,21 @@ export const RecordTableCell = ({ customHotkeyScope: HotkeyScope; }) => { const { closeTableCell } = useCloseRecordTableCell(); - const { entityId, fieldDefinition } = useContext(FieldContext); + const { upsertRecord } = useUpsertRecord(); const { moveLeft, moveRight, moveDown } = useRecordTableMoveFocus(); + const { entityId, fieldDefinition } = useContext(FieldContext); + const handleEnter: FieldInputEvent = (persistField) => { - persistField(); + upsertRecord(persistField); closeTableCell(); moveDown(); }; const handleSubmit: FieldInputEvent = (persistField) => { - persistField(); + upsertRecord(persistField); closeTableCell(); }; @@ -37,26 +40,26 @@ export const RecordTableCell = ({ }; const handleClickOutside: FieldInputEvent = (persistField) => { - persistField(); + upsertRecord(persistField); closeTableCell(); }; const handleEscape: FieldInputEvent = (persistField) => { - persistField(); + upsertRecord(persistField); closeTableCell(); }; const handleTab: FieldInputEvent = (persistField) => { - persistField(); + upsertRecord(persistField); closeTableCell(); moveRight(); }; const handleShiftTab: FieldInputEvent = (persistField) => { - persistField(); + upsertRecord(persistField); closeTableCell(); moveLeft(); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useUpsertRecord.test.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useUpsertRecord.test.tsx new file mode 100644 index 000000000..291c7407d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useUpsertRecord.test.tsx @@ -0,0 +1,152 @@ +import { ReactNode } from 'react'; +import { act, renderHook } from '@testing-library/react'; +import { RecoilRoot } from 'recoil'; +import { createState } from 'twenty-ui'; + +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; +import { textfieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions'; +import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { useUpsertRecord } from '@/object-record/record-table/record-table-cell/hooks/useUpsertRecord'; +import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; + +const pendingRecordId = 'a7286b9a-c039-4a89-9567-2dfa7953cda9'; +const draftValue = 'updated Name'; + +jest.mock('@/object-record/hooks/useCreateOneRecord', () => ({ + __esModule: true, + useCreateOneRecord: jest.fn(), +})); + +const draftValueState = createState({ + key: 'draftValueState', + defaultValue: null, +}); +jest.mock( + '@/object-record/record-field/hooks/internal/useRecordFieldInputStates', + () => ({ + __esModule: true, + useRecordFieldInputStates: jest.fn(() => ({ + getDraftValueSelector: () => draftValueState, + })), + }), +); + +const pendingRecordIdState = createState({ + key: 'pendingRecordIdState', + defaultValue: null, +}); +jest.mock( + '@/object-record/record-table/hooks/internal/useRecordTableStates', + () => ({ + __esModule: true, + useRecordTableStates: jest.fn(() => ({ + pendingRecordIdState: pendingRecordIdState, + })), + }), +); + +const createOneRecordMock = jest.fn(); +const updateOneRecordMock = jest.fn(); +(useCreateOneRecord as jest.Mock).mockReturnValue({ + createOneRecord: createOneRecordMock, +}); + +const Wrapper = ({ + children, + pendingRecordIdMockedValue, + draftValueMockedValue, +}: { + children: ReactNode; + pendingRecordIdMockedValue: string | null; + draftValueMockedValue: string | null; +}) => ( + { + snapshot.set(pendingRecordIdState, pendingRecordIdMockedValue); + snapshot.set(draftValueState, draftValueMockedValue); + }} + > + + {children} + + +); + +describe('useUpsertRecord', () => { + beforeEach(async () => { + createOneRecordMock.mockClear(); + updateOneRecordMock.mockClear(); + }); + + it('calls update record if there is no pending record', async () => { + const { result } = renderHook(() => useUpsertRecord(), { + wrapper: ({ children }) => + Wrapper({ + pendingRecordIdMockedValue: null, + draftValueMockedValue: null, + children, + }), + }); + + await act(async () => { + await result.current.upsertRecord(updateOneRecordMock); + }); + + expect(createOneRecordMock).not.toHaveBeenCalled(); + expect(updateOneRecordMock).toHaveBeenCalled(); + }); + + it('calls update record if pending record is empty', async () => { + const { result } = renderHook(() => useUpsertRecord(), { + wrapper: ({ children }) => + Wrapper({ + pendingRecordIdMockedValue: null, + draftValueMockedValue: draftValue, + children, + }), + }); + + await act(async () => { + await result.current.upsertRecord(updateOneRecordMock); + }); + + expect(createOneRecordMock).not.toHaveBeenCalled(); + expect(updateOneRecordMock).toHaveBeenCalled(); + }); + + it('calls create record if pending record is not empty', async () => { + const { result } = renderHook(() => useUpsertRecord(), { + wrapper: ({ children }) => + Wrapper({ + pendingRecordIdMockedValue: pendingRecordId, + draftValueMockedValue: draftValue, + children, + }), + }); + + await act(async () => { + await result.current.upsertRecord(updateOneRecordMock); + }); + + expect(createOneRecordMock).toHaveBeenCalledWith({ + id: pendingRecordId, + name: draftValue, + position: 'first', + }); + expect(updateOneRecordMock).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCell.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCell.ts index 9ba618747..029366626 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCell.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCell.ts @@ -1,3 +1,6 @@ +import { useResetRecoilState } from 'recoil'; + +import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; @@ -7,13 +10,17 @@ import { TableHotkeyScope } from '../../types/TableHotkeyScope'; export const useCloseRecordTableCell = () => { const setHotkeyScope = useSetHotkeyScope(); const { setDragSelectionStartEnabled } = useDragSelect(); + const { pendingRecordIdState } = useRecordTableStates(); const closeCurrentTableCellInEditMode = useCloseCurrentTableCellInEditMode(); + const resetRecordTablePendingRecordId = + useResetRecoilState(pendingRecordIdState); const closeTableCell = async () => { setDragSelectionStartEnabled(true); closeCurrentTableCellInEditMode(); setHotkeyScope(TableHotkeyScope.TableSoftFocus); + resetRecordTablePendingRecordId(); }; return { diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useUpsertRecord.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useUpsertRecord.ts new file mode 100644 index 000000000..230e4a5d3 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useUpsertRecord.ts @@ -0,0 +1,41 @@ +import { useContext } from 'react'; +import { useRecoilValue } from 'recoil'; + +import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; +import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { useRecordFieldInputStates } from '@/object-record/record-field/hooks/internal/useRecordFieldInputStates'; +import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; +import { isDefined } from '~/utils/isDefined'; + +export const useUpsertRecord = () => { + const { entityId, fieldDefinition } = useContext(FieldContext); + + const { pendingRecordIdState } = useRecordTableStates(); + + const pendingRecordId = useRecoilValue(pendingRecordIdState); + const fieldName = fieldDefinition.metadata.fieldName; + const { getDraftValueSelector } = useRecordFieldInputStates( + `${entityId}-${fieldName}`, + ); + const draftValue = useRecoilValue(getDraftValueSelector()); + + const objectNameSingular = + fieldDefinition.metadata.objectMetadataNameSingular ?? ''; + const { createOneRecord } = useCreateOneRecord({ + objectNameSingular, + }); + + const upsertRecord = (persistField: () => void) => { + if (isDefined(pendingRecordId) && isDefined(draftValue)) { + createOneRecord({ + id: pendingRecordId, + name: draftValue, + position: 'first', + }); + } else if (!pendingRecordId) { + persistField(); + } + }; + + return { upsertRecord }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/recordTablePendingRecordIdComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/recordTablePendingRecordIdComponentState.ts new file mode 100644 index 000000000..4e594933f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/states/recordTablePendingRecordIdComponentState.ts @@ -0,0 +1,8 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +export const recordTablePendingRecordIdComponentState = createComponentState< + string | null +>({ + key: 'recordTablePendingRecordIdState', + defaultValue: null, +}); diff --git a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx index 286ca0728..dbd2aacba 100644 --- a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx @@ -1,9 +1,9 @@ import { useParams } from 'react-router-dom'; import styled from '@emotion/styled'; +import { v4 } from 'uuid'; -import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; -import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { RecordIndexContainer } from '@/object-record/record-index/components/RecordIndexContainer'; +import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCell'; import { useSelectedTableCellEditMode } from '@/object-record/record-table/record-table-cell/hooks/useSelectedTableCellEditMode'; import { PageBody } from '@/ui/layout/page/PageBody'; @@ -20,14 +20,6 @@ const StyledIndexContainer = styled.div` export const RecordIndexPage = () => { const objectNamePlural = useParams().objectNamePlural ?? ''; - const { objectNameSingular } = useObjectNameSingularFromPlural({ - objectNamePlural, - }); - - const { createOneRecord: createOneObject } = useCreateOneRecord({ - objectNameSingular, - }); - const recordIndexId = objectNamePlural ?? ''; const setHotkeyScope = useSetHotkeyScope(); @@ -35,12 +27,14 @@ export const RecordIndexPage = () => { scopeId: recordIndexId, }); - const handleAddButtonClick = async () => { - await createOneObject?.({ - position: 'first', - }); + const { setPendingRecordId } = useRecordTable({ + recordTableId: recordIndexId, + }); - setSelectedTableCellEditMode(0, 0); + const handleAddButtonClick = async () => { + setPendingRecordId(v4()); + + setSelectedTableCellEditMode(-1, 0); setHotkeyScope(DEFAULT_CELL_SCOPE.scope, DEFAULT_CELL_SCOPE.customScopes); };