diff --git a/packages/twenty-front/.storybook/preview.tsx b/packages/twenty-front/.storybook/preview.tsx index 9f763aa8ed..7f14f6ff7a 100644 --- a/packages/twenty-front/.storybook/preview.tsx +++ b/packages/twenty-front/.storybook/preview.tsx @@ -21,15 +21,26 @@ initialize({ return; } - if (request.url.startsWith('http://localhost:3000/files/data:image')) { + if (request.url.startsWith('http://localhost:3000/files/')) { return; } - const requestBody = await request.json(); + try { + const requestBody = await request.json(); + + // eslint-disable-next-line no-console + console.warn(`Unhandled ${request.method} request to ${request.url} + with payload ${JSON.stringify(requestBody)}\n + This request should be mocked with MSW`); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Cannot parse msw request body : ${error}`); + } + // eslint-disable-next-line no-console - console.warn(`Unhandled ${request.method} request to ${request.url} - with payload ${JSON.stringify(requestBody)}\n - This request should be mocked with MSW`); + console.warn( + `Unhandled ${request.method} request to ${request.url} \n This request should be mocked with MSW`, + ); }, quiet: true, }); diff --git a/packages/twenty-front/src/modules/object-metadata/states/fieldMetadataItemByIdSelector.ts b/packages/twenty-front/src/modules/object-metadata/states/fieldMetadataItemByIdSelector.ts new file mode 100644 index 0000000000..b960594730 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/states/fieldMetadataItemByIdSelector.ts @@ -0,0 +1,38 @@ +import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { selectorFamily } from 'recoil'; +import { findById, isDefined } from 'twenty-shared/utils'; + +export const fieldMetadataItemByIdSelector = selectorFamily({ + key: 'fieldMetadataItemByIdSelector', + get: + ({ fieldMetadataItemId }: { fieldMetadataItemId: string }) => + ({ get }) => { + const objectMetadataItems = get(objectMetadataItemsState); + const flattenedFieldMetadataItems = get( + flattenedFieldMetadataItemsSelector, + ); + + const foundObjectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.fields.some(findById(fieldMetadataItemId)), + ); + + if (!isDefined(foundObjectMetadataItem)) { + return {}; + } + + const foundFieldMetadataItem = flattenedFieldMetadataItems.find( + findById(fieldMetadataItemId), + ); + + if (!isDefined(foundFieldMetadataItem)) { + return {}; + } + + return { + foundFieldMetadataItem, + foundObjectMetadataItem, + }; + }, +}); diff --git a/packages/twenty-front/src/modules/object-metadata/states/flattenedFieldMetadataItemsSelector.ts b/packages/twenty-front/src/modules/object-metadata/states/flattenedFieldMetadataItemsSelector.ts new file mode 100644 index 0000000000..ebc99956d0 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/states/flattenedFieldMetadataItemsSelector.ts @@ -0,0 +1,18 @@ +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { selector } from 'recoil'; + +export const flattenedFieldMetadataItemsSelector = selector< + FieldMetadataItem[] +>({ + key: 'flattenedFieldMetadataItemsSelector', + get: ({ get }) => { + const objectMetadataItems = get(objectMetadataItemsState); + + const flattenedFieldMetadataItems = objectMetadataItems.flatMap( + (objectMetadataItem) => objectMetadataItem.fields, + ); + + return flattenedFieldMetadataItems; + }, +}); diff --git a/packages/twenty-front/src/modules/object-metadata/states/isFieldMetadataItemFilterableAndSortableSelector.ts b/packages/twenty-front/src/modules/object-metadata/states/isFieldMetadataItemFilterableAndSortableSelector.ts new file mode 100644 index 0000000000..5d5466047d --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/states/isFieldMetadataItemFilterableAndSortableSelector.ts @@ -0,0 +1,66 @@ +import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector'; +import { availableFieldMetadataItemsForSortFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector'; +import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { selectorFamily } from 'recoil'; +import { findById, isDefined } from 'twenty-shared/utils'; + +export const isFieldMetadataItemFilterableAndSortableSelector = selectorFamily({ + key: 'isFieldMetadataItemFilterableAndSortableSelector', + get: + ({ fieldMetadataItemId }: { fieldMetadataItemId: string }) => + ({ get }) => { + const objectMetadataItems = get(objectMetadataItemsState); + const flattenedFieldMetadataItems = get( + flattenedFieldMetadataItemsSelector, + ); + + const foundObjectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.fields.some(findById(fieldMetadataItemId)), + ); + + if (!isDefined(foundObjectMetadataItem)) { + return { + isFilterable: false, + isSortable: false, + }; + } + + const foundFieldMetadataItem = flattenedFieldMetadataItems.find( + findById(fieldMetadataItemId), + ); + + if (!isDefined(foundFieldMetadataItem)) { + return { + isFilterable: false, + isSortable: false, + }; + } + + const filterableFieldMetadataItems = get( + availableFieldMetadataItemsForFilterFamilySelector({ + objectMetadataItemId: foundObjectMetadataItem.id, + }), + ); + + const sortableFieldMetadataItems = get( + availableFieldMetadataItemsForSortFamilySelector({ + objectMetadataItemId: foundObjectMetadataItem.id, + }), + ); + + const isFilterable = filterableFieldMetadataItems.some( + findById(fieldMetadataItemId), + ); + + const isSortable = sortableFieldMetadataItems.some( + findById(fieldMetadataItemId), + ); + + return { + isFilterable, + isSortable, + }; + }, +}); diff --git a/packages/twenty-front/src/modules/object-metadata/states/isFieldMetadataItemLabelIdentifierSelector.ts b/packages/twenty-front/src/modules/object-metadata/states/isFieldMetadataItemLabelIdentifierSelector.ts new file mode 100644 index 0000000000..c55b0ae0b0 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/states/isFieldMetadataItemLabelIdentifierSelector.ts @@ -0,0 +1,37 @@ +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; +import { selectorFamily } from 'recoil'; +import { findById, isDefined } from 'twenty-shared/utils'; + +export const isFieldMetadataItemLabelIdentifierSelector = selectorFamily({ + key: 'isFieldMetadataItemLabelIdentifierSelector', + get: + ({ fieldMetadataItemId }: { fieldMetadataItemId: string }) => + ({ get }) => { + const objectMetadataItems = get(objectMetadataItemsState); + + const foundObjectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.fields.some(findById(fieldMetadataItemId)), + ); + + if (!isDefined(foundObjectMetadataItem)) { + return false; + } + + const foundFieldMetadataItem = foundObjectMetadataItem.fields.find( + findById(fieldMetadataItemId), + ); + + if (!isDefined(foundFieldMetadataItem)) { + return false; + } + + const fieldIsLabelIdentifier = isLabelIdentifierField({ + fieldMetadataItem: foundFieldMetadataItem, + objectMetadataItem: foundObjectMetadataItem, + }); + + return fieldIsLabelIdentifier; + }, +}); diff --git a/packages/twenty-front/src/modules/object-metadata/states/labelIdentifierFieldMetadataItemSelector.ts b/packages/twenty-front/src/modules/object-metadata/states/labelIdentifierFieldMetadataItemSelector.ts new file mode 100644 index 0000000000..30840bf25a --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/states/labelIdentifierFieldMetadataItemSelector.ts @@ -0,0 +1,31 @@ +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; +import { selectorFamily } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; + +export const labelIdentifierFieldMetadataItemSelector = selectorFamily({ + key: 'labelIdentifierFieldMetadataItemSelector', + get: + ({ objectMetadataItemId }: { objectMetadataItemId: string }) => + ({ get }) => { + const objectMetadataItems = get(objectMetadataItemsState); + + const objectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => objectMetadataItem.id === objectMetadataItemId, + ); + + if (!isDefined(objectMetadataItem)) { + return undefined; + } + + const labelIdentifierFieldMetadataItem = objectMetadataItem.fields.find( + (fieldMetadataItemToFind) => + isLabelIdentifierField({ + fieldMetadataItem: fieldMetadataItemToFind, + objectMetadataItem: objectMetadataItem, + }), + ); + + return labelIdentifierFieldMetadataItem; + }, +}); diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectMenu.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectMenu.tsx index 9d752c3d14..b0a6aa6f96 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterFieldSelectMenu.tsx @@ -3,7 +3,6 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; -import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; @@ -20,6 +19,7 @@ import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState'; import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; +import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector'; import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/DropdownMenuSectionLabel'; @@ -49,12 +49,12 @@ export const AdvancedFilterFieldSelectMenu = ({ const { filterableFieldMetadataItems } = useFilterableFieldMetadataItemsInRecordIndexContext(); - const visibleTableColumns = useRecoilComponentValue( - visibleTableColumnsComponentSelector, + const visibleRecordFields = useRecoilComponentValue( + visibleRecordFieldsComponentSelector, recordIndexId, ); - const visibleColumnsIds = visibleTableColumns.map( - (column) => column.fieldMetadataId, + const visibleFieldMetadataItemIds = visibleRecordFields.map( + (recordField) => recordField.fieldMetadataItemId, ); const filteredSearchInputFieldMetadataItems = @@ -64,18 +64,22 @@ export const AdvancedFilterFieldSelectMenu = ({ .includes(objectFilterDropdownSearchInput.toLocaleLowerCase()), ); - const visibleColumnsFieldMetadataItems = filteredSearchInputFieldMetadataItems - .sort((a, b) => { - return visibleColumnsIds.indexOf(a.id) - visibleColumnsIds.indexOf(b.id); + const visibleFieldMetadataItems = filteredSearchInputFieldMetadataItems + .toSorted((a, b) => { + return ( + visibleFieldMetadataItemIds.indexOf(a.id) - + visibleFieldMetadataItemIds.indexOf(b.id) + ); }) .filter((fieldMetadataItem) => - visibleColumnsIds.includes(fieldMetadataItem.id), + visibleFieldMetadataItemIds.includes(fieldMetadataItem.id), ); const hiddenColumnsFieldMetadataItems = filteredSearchInputFieldMetadataItems .sort((a, b) => a.label.localeCompare(b.label)) .filter( - (fieldMetadataItem) => !visibleColumnsIds.includes(fieldMetadataItem.id), + (fieldMetadataItem) => + !visibleFieldMetadataItemIds.includes(fieldMetadataItem.id), ); const { resetSelectedItem } = useSelectableList( @@ -122,15 +126,15 @@ export const AdvancedFilterFieldSelectMenu = ({ } }; - const shouldShowVisibleFields = visibleColumnsFieldMetadataItems.length > 0; + const shouldShowVisibleFields = visibleFieldMetadataItems.length > 0; const shouldShowHiddenFields = hiddenColumnsFieldMetadataItems.length > 0; const shouldShowSeparator = - visibleColumnsFieldMetadataItems.length > 0 && + visibleFieldMetadataItems.length > 0 && hiddenColumnsFieldMetadataItems.length > 0; const selectableItemIdArray = [ - ...visibleColumnsFieldMetadataItems.map( + ...visibleFieldMetadataItems.map( (fieldMetadataItem) => fieldMetadataItem.id, ), ...hiddenColumnsFieldMetadataItems.map( @@ -152,7 +156,7 @@ export const AdvancedFilterFieldSelectMenu = ({ <> - {visibleColumnsFieldMetadataItems.map( + {visibleFieldMetadataItems.map( (visibleFieldMetadataItem, index) => ( } {shouldShowHiddenFields && ( <> - {visibleColumnsFieldMetadataItems.length > 0 && ( + {visibleFieldMetadataItems.length > 0 && ( )} diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRecordIndexTableQuery.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRecordIndexTableQuery.test.tsx index 9aafc0f98c..d735bd5255 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRecordIndexTableQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRecordIndexTableQuery.test.tsx @@ -1,9 +1,12 @@ import { renderHook, waitFor } from '@testing-library/react'; import { type ReactNode } from 'react'; +import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext'; import { useRecordIndexTableQuery } from '@/object-record/record-index/hooks/useRecordIndexTableQuery'; import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; + +import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; import { type MockedResponse } from '@apollo/client/testing'; import gql from 'graphql-tag'; @@ -657,17 +660,29 @@ jest.mock('react-router-dom', () => ({ const Wrapper = ({ children }: { children: ReactNode }) => { return ( - - - - - {children} - - - - + + + + + + + {children} + + + + + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdownSelectableFieldMetadataItems.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdownSelectableFieldMetadataItems.ts index 8992beb096..d5d79768de 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdownSelectableFieldMetadataItems.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdownSelectableFieldMetadataItems.ts @@ -1,7 +1,7 @@ import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; +import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector'; import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; -import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; export const useFilterDropdownSelectableFieldMetadataItems = () => { @@ -14,13 +14,13 @@ export const useFilterDropdownSelectableFieldMetadataItems = () => { const { filterableFieldMetadataItems } = useFilterableFieldMetadataItemsInRecordIndexContext(); - const visibleTableColumns = useRecoilComponentValue( - visibleTableColumnsComponentSelector, + const visibleRecordFields = useRecoilComponentValue( + visibleRecordFieldsComponentSelector, recordIndexId, ); - const visibleColumnsIds = visibleTableColumns.map( - (column) => column.fieldMetadataId, + const visibleFieldMetadataItemIds = visibleRecordFields.map( + (recordField) => recordField.fieldMetadataItemId, ); const filteredSearchInputFieldMetadataItems = @@ -34,11 +34,12 @@ export const useFilterDropdownSelectableFieldMetadataItems = () => { filteredSearchInputFieldMetadataItems .sort((a, b) => { return ( - visibleColumnsIds.indexOf(a.id) - visibleColumnsIds.indexOf(b.id) + visibleFieldMetadataItemIds.indexOf(a.id) - + visibleFieldMetadataItemIds.indexOf(b.id) ); }) .filter((fieldMetadataItem) => - visibleColumnsIds.includes(fieldMetadataItem.id), + visibleFieldMetadataItemIds.includes(fieldMetadataItem.id), ); const selectableHiddenFieldMetadataItems = @@ -46,7 +47,7 @@ export const useFilterDropdownSelectableFieldMetadataItems = () => { .sort((a, b) => a.label.localeCompare(b.label)) .filter( (fieldMetadataItem) => - !visibleColumnsIds.includes(fieldMetadataItem.id), + !visibleFieldMetadataItemIds.includes(fieldMetadataItem.id), ); return { diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForTable.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForTable.ts deleted file mode 100644 index 3752d3d94b..0000000000 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForTable.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { type OnDragEndResponder } from '@hello-pangea/dnd'; - -import { useFilterVisibleAndReadableRecordField } from '@/object-record/record-field/hooks/useFilterVisibleAndReadableRecordField'; -import { useReorderVisibleRecordFields } from '@/object-record/record-field/hooks/useReorderVisibleRecordFields'; -import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; -import { useSetTableColumns } from '@/object-record/record-table/hooks/useSetTableColumns'; -import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns'; -import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector'; -import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields'; -import { mapRecordFieldToViewField } from '@/views/utils/mapRecordFieldToViewField'; -import { produce } from 'immer'; -import { useRecoilCallback } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; -import { sortByProperty } from '~/utils/array/sortByProperty'; - -export const useObjectOptionsForTable = ( - recordTableId: string, - objectMetadataId: string, -) => { - const hiddenTableColumns = useRecoilComponentValue( - hiddenTableColumnsComponentSelector, - recordTableId, - ); - - const tableColumns = useRecoilComponentValue( - tableColumnsComponentState, - recordTableId, - ); - - const currentRecordFields = useRecoilComponentValue( - currentRecordFieldsComponentState, - recordTableId, - ); - - const { filterVisibleAndReadableRecordField } = - useFilterVisibleAndReadableRecordField(); - - const visibleRecordFields = currentRecordFields - .filter(filterVisibleAndReadableRecordField) - .toSorted(sortByProperty('position')); - - const { handleColumnVisibilityChange } = useTableColumns({ - recordTableId, - }); - - const { reorderVisibleRecordFields } = - useReorderVisibleRecordFields(recordTableId); - - const { saveViewFields } = useSaveCurrentViewFields(); - - const { setTableColumns } = useSetTableColumns(); - - const handleReorderColumns: OnDragEndResponder = useRecoilCallback( - () => async (result) => { - if ( - !result.destination || - result.destination.index === 1 || - result.source.index === 1 - ) { - return; - } - - const updatedRecordField = reorderVisibleRecordFields({ - fromIndex: result.source.index - 1, - toIndex: result.destination.index - 1, - }); - - saveViewFields([mapRecordFieldToViewField(updatedRecordField)]); - - // TODO: remove this after refactor - const modifiedVisibleTableColumns = produce( - tableColumns, - (draftTableColumns) => { - const indexToModify = draftTableColumns.findIndex( - (tableColumnToFind) => - tableColumnToFind.fieldMetadataId === - updatedRecordField.fieldMetadataItemId, - ); - - if (isDefined(draftTableColumns[indexToModify])) { - draftTableColumns[indexToModify].position = - updatedRecordField.position; - } else { - throw new Error( - `Undefined draftTableColumns this should not happen`, - ); - } - }, - ); - - // TODO: remove after refactor - setTableColumns( - modifiedVisibleTableColumns, - recordTableId, - objectMetadataId, - ); - }, - [ - reorderVisibleRecordFields, - saveViewFields, - objectMetadataId, - recordTableId, - setTableColumns, - tableColumns, - ], - ); - - return { - handleReorderColumns, - handleColumnVisibilityChange, - visibleTableColumns: visibleRecordFields, - hiddenTableColumns, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useProcessOptionDropdownDragEnd.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useProcessOptionDropdownDragEnd.ts new file mode 100644 index 0000000000..23e85e0ec7 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useProcessOptionDropdownDragEnd.ts @@ -0,0 +1,38 @@ +import { type OnDragEndResponder } from '@hello-pangea/dnd'; + +import { useReorderVisibleRecordFields } from '@/object-record/record-field/hooks/useReorderVisibleRecordFields'; + +import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields'; +import { mapRecordFieldToViewField } from '@/views/utils/mapRecordFieldToViewField'; +import { useRecoilCallback } from 'recoil'; + +export const useProcessOptionDropdownDragEnd = (recordTableId: string) => { + const { reorderVisibleRecordFields } = + useReorderVisibleRecordFields(recordTableId); + + const { saveViewFields } = useSaveCurrentViewFields(); + + const processOptionDropdownDragEnd: OnDragEndResponder = useRecoilCallback( + () => async (result) => { + if ( + !result.destination || + result.destination.index === 1 || + result.source.index === 1 + ) { + return; + } + + const updatedRecordField = reorderVisibleRecordFields({ + fromIndex: result.source.index - 1, + toIndex: result.destination.index - 1, + }); + + saveViewFields([mapRecordFieldToViewField(updatedRecordField)]); + }, + [reorderVisibleRecordFields, saveViewFields], + ); + + return { + processOptionDropdownDragEnd, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx index 2ff649c45a..910cde6476 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx @@ -7,14 +7,13 @@ import { useResetSortDropdown } from '@/object-record/object-sort-dropdown/hooks import { isRecordSortDirectionDropdownMenuUnfoldedComponentState } from '@/object-record/object-sort-dropdown/states/isRecordSortDirectionDropdownMenuUnfoldedComponentState'; import { objectSortDropdownSearchInputComponentState } from '@/object-record/object-sort-dropdown/states/objectSortDropdownSearchInputComponentState'; import { selectedRecordSortDirectionComponentState } from '@/object-record/object-sort-dropdown/states/selectedRecordSortDirectionComponentState'; +import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useUpsertRecordSort } from '@/object-record/record-sort/hooks/useUpsertRecordSort'; import { RECORD_SORT_DIRECTIONS, type RecordSortDirection, } from '@/object-record/record-sort/types/RecordSortDirection'; -import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector'; -import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; @@ -35,6 +34,7 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import { Trans, useLingui } from '@lingui/react/macro'; import { useRecoilValue } from 'recoil'; +import { findByProperty } from 'twenty-shared/utils'; import { IconX, useIcons } from 'twenty-ui/display'; import { MenuItem } from 'twenty-ui/navigation'; import { v4 } from 'uuid'; @@ -63,19 +63,13 @@ export const ObjectSortDropdownButton = () => { const { getIcon } = useIcons(); - const visibleTableColumns = useRecoilComponentValue( - visibleTableColumnsComponentSelector, + const visibleRecordFields = useRecoilComponentValue( + visibleRecordFieldsComponentSelector, recordIndexId, ); - const visibleColumnsFieldMetadataIds = visibleTableColumns.map( - (column) => column.fieldMetadataId, - ); - const hiddenTableColumns = useRecoilComponentValue( - hiddenTableColumnsComponentSelector, - recordIndexId, - ); - const hiddenColumnFieldMetadataIds = hiddenTableColumns.map( - (column) => column.fieldMetadataId, + + const visibleFieldMetadataItemIds = visibleRecordFields.map( + (recordField) => recordField.fieldMetadataItemId, ); const filteredSearchInputFieldMetadataItems = @@ -88,24 +82,28 @@ export const ObjectSortDropdownButton = () => { const visibleFieldMetadataItems = filteredSearchInputFieldMetadataItems .sort((fieldMetadataItemA, fieldMetadataItemB) => { return ( - visibleColumnsFieldMetadataIds.indexOf(fieldMetadataItemA.id) - - visibleColumnsFieldMetadataIds.indexOf(fieldMetadataItemB.id) + visibleFieldMetadataItemIds.indexOf(fieldMetadataItemA.id) - + visibleFieldMetadataItemIds.indexOf(fieldMetadataItemB.id) ); }) .filter((fieldMetadataItem) => - visibleColumnsFieldMetadataIds.includes(fieldMetadataItem.id), + visibleFieldMetadataItemIds.includes(fieldMetadataItem.id), ); - const hiddenFieldMetadataItems = filteredSearchInputFieldMetadataItems + const hiddenFieldMetadataItemsSorted = filteredSearchInputFieldMetadataItems .sort((fieldMetadataItemA, fieldMetadataItemB) => fieldMetadataItemA.label.localeCompare(fieldMetadataItemB.label), ) - .filter((fieldMetadataItem) => - hiddenColumnFieldMetadataIds.includes(fieldMetadataItem.id), + .filter( + (fieldMetadataItem) => + !visibleRecordFields.some( + findByProperty('fieldMetadataItemId', fieldMetadataItem.id), + ), ); const shouldShowSeparator = - visibleFieldMetadataItems.length > 0 && hiddenFieldMetadataItems.length > 0; + visibleFieldMetadataItems.length > 0 && + hiddenFieldMetadataItemsSorted.length > 0; const handleDropdownButtonClose = () => { resetRecordSortDropdownSearchInput(); @@ -152,7 +150,7 @@ export const ObjectSortDropdownButton = () => { const selectableItemIdArray = [ ...visibleFieldMetadataItems.map((item) => item.id), - ...hiddenFieldMetadataItems.map((item) => item.id), + ...hiddenFieldMetadataItemsSorted.map((item) => item.id), ]; const selectedItemId = useRecoilComponentValue( @@ -165,7 +163,7 @@ export const ObjectSortDropdownButton = () => { OBJECT_SORT_DROPDOWN_ID, ); - const shouldShowHiddenFields = hiddenFieldMetadataItems.length > 0; + const shouldShowHiddenFields = hiddenFieldMetadataItemsSorted.length > 0; const shouldShowVisibleFields = visibleFieldMetadataItems.length > 0; return ( @@ -257,7 +255,7 @@ export const ObjectSortDropdownButton = () => { <> - {hiddenFieldMetadataItems.map( + {hiddenFieldMetadataItemsSorted.map( (hiddenFieldMetadataItem, index) => ( { + const currentRecordFields = useRecoilComponentValue( + currentRecordFieldsComponentState, + recordFieldComponentInstanceId, + ); + + const { updateRecordField } = useUpdateRecordField( + recordFieldComponentInstanceId, + ); + const { upsertRecordField } = useUpsertRecordField( + recordFieldComponentInstanceId, + ); + + const { saveViewFields } = useSaveCurrentViewFields(); + + const changeRecordFieldVisibility = ({ + fieldMetadataId, + isVisible, + }: { + fieldMetadataId: string; + isVisible: boolean; + }) => { + const lastPosition = + currentRecordFields.toSorted(sortByProperty('position', 'desc'))?.[0] + ?.position ?? 0; + + const shouldShowFieldMetadataItem = isVisible === true; + const correspondingRecordField = currentRecordFields.find( + (recordFieldToFind) => + recordFieldToFind.fieldMetadataItemId === fieldMetadataId, + ); + + const noExistingRecordField = !isDefined(correspondingRecordField); + + if (noExistingRecordField) { + const recordFieldToUpsert: RecordField = { + id: v4(), + fieldMetadataItemId: fieldMetadataId, + size: 100, + isVisible: shouldShowFieldMetadataItem, + position: lastPosition + 1, + }; + + upsertRecordField(recordFieldToUpsert); + + saveViewFields([mapRecordFieldToViewField(recordFieldToUpsert)]); + } else { + updateRecordField(fieldMetadataId, { + isVisible: shouldShowFieldMetadataItem, + }); + + const updatedRecordField: RecordField = { + ...correspondingRecordField, + isVisible: shouldShowFieldMetadataItem, + }; + + saveViewFields([mapRecordFieldToViewField(updatedRecordField)]); + } + }; + + return { + changeRecordFieldVisibility, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useCreateTableColumn.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useCreateTableColumn.ts deleted file mode 100644 index 11536ae65f..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useCreateTableColumn.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { availableTableColumnsComponentState } from '@/object-record/record-table/states/availableTableColumnsComponentState'; -import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; -import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; -import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; -import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; -import { produce } from 'immer'; -import { useRecoilCallback } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; - -// TODO: remove after refactor -export const useCreateTableColumn = (recordTableId?: string) => { - const tableColumnsCallbackState = useRecoilComponentCallbackState( - tableColumnsComponentState, - recordTableId, - ); - - const availableTableColumnsCallbackState = useRecoilComponentCallbackState( - availableTableColumnsComponentState, - recordTableId, - ); - - const createTableColumn = useRecoilCallback( - ({ set, snapshot }) => - ( - fieldMetadataItemId: string, - tableColumnToCreateInfo: Pick< - ColumnDefinition, - 'isVisible' | 'position' | 'size' - >, - ) => { - const tableColumns = getSnapshotValue( - snapshot, - tableColumnsCallbackState, - ); - - const availableTableColumns = getSnapshotValue( - snapshot, - availableTableColumnsCallbackState, - ); - - const modifiedTableColumns = produce( - tableColumns, - (draftTableColumns) => { - const indexToModify = draftTableColumns.findIndex( - (tableColumnToFind) => - tableColumnToFind.fieldMetadataId === fieldMetadataItemId, - ); - - const correspondingAvailableTableColumn = - availableTableColumns.find( - (tableColumnToFind) => - tableColumnToFind.fieldMetadataId === fieldMetadataItemId, - ); - - if ( - indexToModify === -1 && - isDefined(correspondingAvailableTableColumn) - ) { - draftTableColumns.push({ - ...correspondingAvailableTableColumn, - fieldMetadataId: fieldMetadataItemId, - position: tableColumnToCreateInfo.position, - isVisible: tableColumnToCreateInfo.isVisible, - }); - } - }, - ); - - set(tableColumnsCallbackState, modifiedTableColumns); - }, - [tableColumnsCallbackState, availableTableColumnsCallbackState], - ); - - return { - createTableColumn, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useMoveRecordField.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useMoveRecordField.ts index bda62ea713..a6b37dd52a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useMoveRecordField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useMoveRecordField.ts @@ -1,5 +1,4 @@ import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField'; -import { useUpdateTableColumn } from '@/object-record/record-field/hooks/useUpdateTableColumn'; import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields'; @@ -17,8 +16,8 @@ export const useMoveRecordField = (recordTableId?: string) => { const { updateRecordField } = useUpdateRecordField(recordTableId); - const { updateTableColumn } = useUpdateTableColumn(recordTableId); - + // TODO: fix this we want to move left and right of VISIBLE record fields, + // because otherwise it will just do nothing while moving left and right of non visible record fields const moveRecordField = useRecoilCallback( ({ snapshot }) => ({ @@ -84,22 +83,9 @@ export const useMoveRecordField = (recordTableId?: string) => { position: currentRecordFieldNewPosition, }), ]); - - updateTableColumn(targetRecordField.fieldMetadataItemId, { - position: targetRecordFieldNewPosition, - }); - - updateTableColumn(currentRecordField.fieldMetadataItemId, { - position: currentRecordFieldNewPosition, - }); } }, - [ - currentRecordFieldsCallbackState, - updateTableColumn, - saveViewFields, - updateRecordField, - ], + [currentRecordFieldsCallbackState, saveViewFields, updateRecordField], ); return { moveRecordField }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useReorderVisibleRecordFields.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useReorderVisibleRecordFields.ts index 5fb06c2451..7e5bb42da1 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useReorderVisibleRecordFields.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useReorderVisibleRecordFields.ts @@ -1,6 +1,6 @@ import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField'; import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; -import { visibleAndReadableRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleAndReadableRecordFieldsComponentSelector'; +import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector'; import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { computeNewPositionOfRecordWithPosition } from '@/object-record/utils/computeNewPositionOfRecordWithPosition'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; @@ -8,7 +8,7 @@ import { useRecoilCallback } from 'recoil'; export const useReorderVisibleRecordFields = (recordTableId: string) => { const visibleRecordFieldsCallbackState = useRecoilComponentCallbackState( - visibleAndReadableRecordFieldsComponentSelector, + visibleRecordFieldsComponentSelector, recordTableId, ); diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpdateRecordField.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpdateRecordField.ts index 090d70dd50..3a805d81ee 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpdateRecordField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpdateRecordField.ts @@ -3,11 +3,14 @@ import { type RecordField } from '@/object-record/record-field/types/RecordField import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; -export const useUpdateRecordField = (recordTableId?: string) => { +export const useUpdateRecordField = ( + recordFieldComponentInstanceId?: string, +) => { const currentRecordFieldsCallbackState = useRecoilComponentCallbackState( currentRecordFieldsComponentState, - recordTableId, + recordFieldComponentInstanceId, ); const updateRecordField = useRecoilCallback( @@ -23,12 +26,12 @@ export const useUpdateRecordField = (recordTableId?: string) => { currentRecordFieldsCallbackState, ); - const foundRecordFieldInCurrentRecordFields = currentRecordFields.some( + const foundRecordFieldInCurrentRecordFields = currentRecordFields.find( (existingRecordField) => existingRecordField.fieldMetadataItemId === fieldMetadataItemId, ); - if (!foundRecordFieldInCurrentRecordFields) { + if (!isDefined(foundRecordFieldInCurrentRecordFields)) { throw new Error( `Cannot find record field to update with field metadata item id : ${fieldMetadataItemId}`, ); @@ -48,6 +51,11 @@ export const useUpdateRecordField = (recordTableId?: string) => { return newCurrentRecordFields; }); + + return { + ...foundRecordFieldInCurrentRecordFields, + ...partialRecordField, + } satisfies RecordField as RecordField; } }, [currentRecordFieldsCallbackState], diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpdateTableColumn.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpdateTableColumn.ts deleted file mode 100644 index 1bdfdf2c5f..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpdateTableColumn.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; -import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; -import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; -import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; -import { produce } from 'immer'; -import { useRecoilCallback } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; - -export const useUpdateTableColumn = (recordTableId?: string) => { - const tableColumnsCallbackState = useRecoilComponentCallbackState( - tableColumnsComponentState, - recordTableId, - ); - - const updateTableColumn = useRecoilCallback( - ({ set, snapshot }) => - ( - fieldMetadataItemId: string, - partiableTableColumn: Partial< - Pick, 'isVisible' | 'position'> - >, - ) => { - const tableColumns = getSnapshotValue( - snapshot, - tableColumnsCallbackState, - ); - - const modifiedTableColumns = produce( - tableColumns, - (draftTableColumns) => { - const indexToModify = draftTableColumns.findIndex( - (tableColumnToFind) => - tableColumnToFind.fieldMetadataId === fieldMetadataItemId, - ); - - if (isDefined(partiableTableColumn.position)) { - draftTableColumns[indexToModify].position = - partiableTableColumn.position; - } - - if (isDefined(partiableTableColumn.isVisible)) { - draftTableColumns[indexToModify].isVisible = - partiableTableColumn.isVisible; - } - }, - ); - - set(tableColumnsCallbackState, modifiedTableColumns); - }, - [tableColumnsCallbackState], - ); - - return { - updateTableColumn, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/states/visibleAndReadableRecordFieldsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-field/states/visibleAndReadableRecordFieldsComponentSelector.ts deleted file mode 100644 index dece01cfa6..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/states/visibleAndReadableRecordFieldsComponentSelector.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { flattenedReadableFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedReadableFieldMetadataItemIdsSelector'; -import { RecordFieldsComponentInstanceContext } from '@/object-record/record-field/states/context/RecordFieldsComponentInstanceContext'; -import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; -import { createComponentSelector } from '@/ui/utilities/state/component-state/utils/createComponentSelector'; -import { sortByProperty } from '~/utils/array/sortByProperty'; - -export const visibleAndReadableRecordFieldsComponentSelector = - createComponentSelector({ - key: 'visibleAndReadableRecordFieldsComponentSelector', - componentInstanceContext: RecordFieldsComponentInstanceContext, - get: - ({ instanceId }) => - ({ get }) => { - const currentRecordFields = get( - currentRecordFieldsComponentState.atomFamily({ - instanceId, - }), - ); - - const readableFieldMetadataItems = get( - flattenedReadableFieldMetadataItemsSelector, - ); - - const filteredVisibleAndReadableRecordFields = - currentRecordFields.filter( - (recordFieldToFilter) => - recordFieldToFilter.isVisible === true && - readableFieldMetadataItems.some( - (fieldMetadataItemToFilter) => - fieldMetadataItemToFilter.id === - recordFieldToFilter.fieldMetadataItemId && - fieldMetadataItemToFilter.isActive === true && - fieldMetadataItemToFilter.isSystem !== true, - ), - ); - - return filteredVisibleAndReadableRecordFields.toSorted( - sortByProperty('position'), - ); - }, - }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/states/visibleRecordFieldsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-field/states/visibleRecordFieldsComponentSelector.ts new file mode 100644 index 0000000000..3417f3a352 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/states/visibleRecordFieldsComponentSelector.ts @@ -0,0 +1,39 @@ +import { flattenedReadableFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedReadableFieldMetadataItemIdsSelector'; +import { RecordFieldsComponentInstanceContext } from '@/object-record/record-field/states/context/RecordFieldsComponentInstanceContext'; +import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; +import { createComponentSelector } from '@/ui/utilities/state/component-state/utils/createComponentSelector'; +import { sortByProperty } from '~/utils/array/sortByProperty'; + +export const visibleRecordFieldsComponentSelector = createComponentSelector({ + key: 'visibleRecordFieldsComponentSelector', + componentInstanceContext: RecordFieldsComponentInstanceContext, + get: + ({ instanceId }) => + ({ get }) => { + const currentRecordFields = get( + currentRecordFieldsComponentState.atomFamily({ + instanceId, + }), + ); + + const readableFieldMetadataItems = get( + flattenedReadableFieldMetadataItemsSelector, + ); + + const filteredVisibleAndReadableRecordFields = currentRecordFields.filter( + (recordFieldToFilter) => + recordFieldToFilter.isVisible === true && + readableFieldMetadataItems.some( + (fieldMetadataItemToFilter) => + fieldMetadataItemToFilter.id === + recordFieldToFilter.fieldMetadataItemId && + fieldMetadataItemToFilter.isActive === true && + fieldMetadataItemToFilter.isSystem !== true, + ), + ); + + return filteredVisibleAndReadableRecordFields.toSorted( + sortByProperty('position'), + ); + }, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx index e8522d2b50..3e1e0f0175 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx @@ -3,7 +3,6 @@ import { useEffect } from 'react'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; -import { useSetAvailableTableColumns } from '@/object-record/record-table/hooks/useSetAvailableTableColumns'; import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState'; import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation'; import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; @@ -12,10 +11,7 @@ import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; export const RecordIndexTableContainerEffect = () => { - const { recordIndexId, objectNameSingular } = useRecordIndexContextOrThrow(); - - const { setAvailableTableColumns } = - useSetAvailableTableColumns(recordIndexId); + const { objectNameSingular } = useRecordIndexContextOrThrow(); const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, @@ -24,10 +20,6 @@ export const RecordIndexTableContainerEffect = () => { const { columnDefinitions } = useColumnDefinitionsFromFieldMetadata(objectMetadataItem); - useEffect(() => { - setAvailableTableColumns(columnDefinitions); - }, [columnDefinitions, setAvailableTableColumns]); - const { currentView } = useGetCurrentViewOnly(); const setViewFieldAggregateOperation = useRecoilCallback( diff --git a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/__tests__/useRecordIndexLazyFetchRecords.test.ts b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/__tests__/useRecordIndexLazyFetchRecords.test.ts index 46c46dddda..52643bf9dd 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/__tests__/useRecordIndexLazyFetchRecords.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/__tests__/useRecordIndexLazyFetchRecords.test.ts @@ -6,10 +6,8 @@ import { useRecordIndexLazyFetchRecords, } from '../useRecordIndexLazyFetchRecords'; +import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords'; -import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; -import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; -import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; import { ViewType } from '@/views/types/ViewType'; import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper'; import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems'; @@ -131,179 +129,39 @@ describe('useRecordData', () => { it('should call the callback function with fetched data', async () => { const callback = jest.fn(); mockFetchAllRecords.mockReturnValue([mockPerson]); + const { result } = renderHook( - () => - useRecordIndexLazyFetchRecords({ + () => { + const { columnDefinitions } = + useColumnDefinitionsFromFieldMetadata(objectMetadataItem); + + const lazyFetchResult = useRecordIndexLazyFetchRecords({ recordIndexId, objectMetadataItem, callback, pageSize: 30, delayMs: 0, - }), + }); + + return { + columnDefinitions, + lazyFetchResult, + }; + }, { wrapper: Wrapper }, ); await act(async () => { - result.current.getTableData(); - }); - - await waitFor(() => { - expect(callback).toHaveBeenCalledWith([mockPerson], []); - }); - }); - - it('should call the callback function with kanban field included as column if view type is kanban', async () => { - const callback = jest.fn(); - - mockFetchAllRecords.mockReturnValue([mockPerson]); - - const { result } = renderHook( - () => { - const [recordGroupFieldMetadata, setRecordGroupFieldMetadata] = - useRecoilComponentState( - recordGroupFieldMetadataComponentState, - recordIndexId, - ); - - return { - tableData: useRecordIndexLazyFetchRecords({ - recordIndexId, - objectMetadataItem, - callback, - pageSize: 30, - maximumRequests: 100, - delayMs: 0, - viewType: ViewType.Kanban, - }), - kanbanFieldName: recordGroupFieldMetadata?.name, - setRecordGroupFieldMetadata, - kanbanData: useObjectOptionsForBoard({ - objectNameSingular: objectMetadataItem.nameSingular, - recordBoardId: recordIndexId, - viewBarId: recordIndexId, - }), - }; - }, - { - wrapper: Wrapper, - }, - ); - - const personObjectMetadataItem = generatedMockObjectMetadataItems.find( - (item) => item.nameSingular === 'person', - ); - - const updatedAtFieldMetadataItem = personObjectMetadataItem?.fields.find( - (field) => field.name === 'updatedAt', - ); - - await act(async () => { - result.current.setRecordGroupFieldMetadata(updatedAtFieldMetadataItem); - }); - - await act(async () => { - result.current.tableData.getTableData(); + result.current.lazyFetchResult.getTableData(); }); await waitFor(() => { expect(callback).toHaveBeenCalledWith( [mockPerson], - [ - { - defaultValue: 'now', - editButtonIcon: undefined, - fieldMetadataId: updatedAtFieldMetadataItem?.id, - iconName: 'IconCalendarClock', - isFilterable: true, - isLabelIdentifier: false, - isSortable: true, - isVisible: false, - label: 'Last update', - labelWidth: undefined, - metadata: { - fieldName: 'updatedAt', - isCustom: false, - isNullable: false, - isUIReadOnly: false, - objectMetadataNameSingular: 'person', - options: null, - placeHolder: 'Last update', - relationFieldMetadataId: undefined, - relationObjectMetadataId: '', - relationObjectMetadataNamePlural: '', - relationObjectMetadataNameSingular: '', - relationType: undefined, - morphRelations: [], - targetFieldMetadataName: '', - settings: { - displayFormat: 'RELATIVE', - }, - }, - position: 9, - showLabel: undefined, - size: 100, - type: 'DATE_TIME', - }, - ], + result.current.columnDefinitions, ); }); }); - - it('should not call the callback function with kanban field included as column if view type is table', async () => { - const callback = jest.fn(); - mockFetchAllRecords.mockReturnValue([mockPerson]); - const { result } = renderHook( - () => { - const [recordGroupFieldMetadata, setRecordGroupFieldMetadata] = - useRecoilComponentState( - recordGroupFieldMetadataComponentState, - recordIndexId, - ); - - return { - tableData: useRecordIndexLazyFetchRecords({ - recordIndexId, - objectMetadataItem, - callback, - pageSize: 30, - maximumRequests: 100, - delayMs: 0, - viewType: ViewType.Table, - }), - objectMetadataItem, - kanbanFieldName: recordGroupFieldMetadata?.name, - setRecordGroupFieldMetadata, - kanbanData: useObjectOptionsForBoard({ - objectNameSingular: objectMetadataItem.nameSingular, - recordBoardId: recordIndexId, - viewBarId: recordIndexId, - }), - }; - }, - { - wrapper: Wrapper, - }, - ); - - await act(async () => { - const fieldMetadataItem = - result.current.objectMetadataItem?.fields.find( - (fieldMetadata) => - fieldMetadata.id === - result.current.kanbanData.hiddenBoardFields[0].fieldMetadataId, - ); - - result.current.setRecordGroupFieldMetadata(fieldMetadataItem); - }); - - await act(async () => { - result.current.tableData.getTableData(); - }); - - await waitFor(() => { - expect(callback).toHaveBeenCalledWith([mockPerson], []); - }); - }); }); describe('utils', () => { diff --git a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords.ts b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords.ts index cc2ee57034..46fc0378ac 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords.ts @@ -7,6 +7,7 @@ import { contextStoreFilterGroupsComponentState } from '@/context-store/states/c import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; +import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords'; import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/object-options-dropdown/constants/ExportTableDataDefaultPageSize'; @@ -14,7 +15,6 @@ import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdow import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams'; -import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { ViewType } from '@/views/types/ViewType'; @@ -61,10 +61,6 @@ export const useRecordIndexLazyFetchRecords = ({ const hiddenKanbanFieldColumn = hiddenBoardFields.find( (column) => column.metadata.fieldName === recordGroupFieldMetadata?.name, ); - const columns = useRecoilComponentValue( - visibleTableColumnsComponentSelector, - recordIndexId, - ); const contextStoreTargetedRecordsRule = useRecoilComponentValue( contextStoreTargetedRecordsRuleComponentState, @@ -97,8 +93,11 @@ export const useRecordIndexLazyFetchRecords = ({ contextStoreAnyFieldFilterValue, }); + const { columnDefinitions } = + useColumnDefinitionsFromFieldMetadata(objectMetadataItem); + const finalColumns = [ - ...columns, + ...columnDefinitions, ...(hiddenKanbanFieldColumn && viewType === ViewType.Kanban ? [hiddenKanbanFieldColumn] : []), diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts index 184a08df88..1a9349d590 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts @@ -11,12 +11,10 @@ import { recordIndexKanbanAggregateOperationState } from '@/object-record/record import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState'; import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState'; import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState'; -import { useSetTableColumns } from '@/object-record/record-table/hooks/useSetTableColumns'; import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState'; import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation'; import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns'; -import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import { type View } from '@/views/types/View'; import { type ViewField } from '@/views/types/ViewField'; @@ -45,15 +43,9 @@ export const useLoadRecordIndexStates = () => { ); const { setRecordGroupsFromViewGroups } = useSetRecordGroups(); - const { setTableColumns } = useSetTableColumns(); - const onViewFieldsChange = useRecoilCallback( ({ set, snapshot }) => - ( - viewFields: ViewField[], - objectMetadataItem: ObjectMetadataItem, - recordIndexId: string, - ) => { + (viewFields: ViewField[], objectMetadataItem: ObjectMetadataItem) => { const activeFieldMetadataItems = objectMetadataItem.fields.filter( ({ isActive, isSystem }) => isActive && !isSystem, ); @@ -108,12 +100,6 @@ export const useLoadRecordIndexStates = () => { columnDefinitions, }); - setTableColumns( - newFieldDefinitions, - recordIndexId, - objectMetadataItem.id, - ); - const existingRecordIndexFieldDefinitions = snapshot .getLoadable(recordIndexFieldDefinitionsState) .getValue(); @@ -161,17 +147,12 @@ export const useLoadRecordIndexStates = () => { } } }, - [setTableColumns], + [], ); const loadRecordIndexStates = useRecoilCallback( ({ snapshot }) => async (view: View, objectMetadataItem: ObjectMetadataItem) => { - const recordIndexId = getRecordIndexIdFromObjectNamePluralAndViewId( - objectMetadataItem.namePlural, - view.id, - ); - const filterableFieldMetadataItems = snapshot .getLoadable( availableFieldMetadataItemsForFilterFamilySelector({ @@ -180,7 +161,7 @@ export const useLoadRecordIndexStates = () => { ) .getValue(); - onViewFieldsChange(view.viewFields, objectMetadataItem, recordIndexId); + onViewFieldsChange(view.viewFields, objectMetadataItem); setRecordGroupsFromViewGroups( view.id, diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordTableRecordGqlFields.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordTableRecordGqlFields.ts index 5c217ffa75..02ab21e1df 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordTableRecordGqlFields.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordTableRecordGqlFields.ts @@ -3,7 +3,8 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { generateDepthOneWithoutRelationsRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneWithoutRelationsRecordGqlFields'; -import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; +import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { FieldMetadataType } from 'twenty-shared/types'; import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; @@ -13,10 +14,13 @@ export const useRecordTableRecordGqlFields = ({ }: { objectMetadataItem: ObjectMetadataItem; }) => { - const visibleTableColumns = useRecoilComponentValue( - visibleTableColumnsComponentSelector, + const visibleRecordFields = useRecoilComponentValue( + visibleRecordFieldsComponentSelector, ); + const { fieldMetadataItemByFieldMetadataItemId } = + useRecordTableContextOrThrow(); + const { objectMetadataItem: noteTargetObjectMetadataItem } = useObjectMetadataItem({ objectNameSingular: CoreObjectNameSingular.NoteTarget, @@ -33,22 +37,23 @@ export const useRecordTableRecordGqlFields = ({ }); const gqlFieldsList = Object.fromEntries( - visibleTableColumns.flatMap((column) => { - const isMorphRelation = column.type === FieldMetadataType.MORPH_RELATION; + visibleRecordFields.flatMap((recordField) => { + const fieldMetadataItem = + fieldMetadataItemByFieldMetadataItemId[recordField.fieldMetadataItemId]; + + const isMorphRelation = + fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION; if (!isMorphRelation) { - return [[column.metadata.fieldName, true]]; + return [[fieldMetadataItem.name, true]]; } - const fieldMetadataItem = objectMetadataItem.fields.find( - (field) => field.id === column.fieldMetadataId, - ); if ( !isDefined(fieldMetadataItem) || !isDefined(fieldMetadataItem.morphRelations) ) { throw new Error( - `Field ${column.metadata.fieldName} is missing, please refresh the page. If the problem persists, please contact support.`, + `Field ${fieldMetadataItem.name} is missing, please refresh the page. If the problem persists, please contact support.`, ); } diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx index f90316af60..cfa33ea4cd 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx @@ -3,9 +3,13 @@ import { type ReactNode } from 'react'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { RecordTableContextProvider as RecordTableContextInternalProvider } from '@/object-record/record-table/contexts/RecordTableContext'; +import { labelIdentifierFieldMetadataItemSelector } from '@/object-metadata/states/labelIdentifierFieldMetadataItemSelector'; +import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject'; -import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; +import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; +import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useRecoilValue } from 'recoil'; type RecordTableContextProviderProps = { viewBarId: string; @@ -28,20 +32,65 @@ export const RecordTableContextProvider = ({ objectMetadataItem.id, ); - const visibleTableColumns = useRecoilComponentValue( - visibleTableColumnsComponentSelector, + const visibleRecordFields = useRecoilComponentValue( + visibleRecordFieldsComponentSelector, recordTableId, ); + const currentRecordFields = useRecoilComponentValue( + currentRecordFieldsComponentState, + recordTableId, + ); + + const fieldMetadataItems = objectMetadataItem.fields; + + const fieldMetadataItemByFieldMetadataItemId = Object.fromEntries( + fieldMetadataItems.map((fieldMetadataItem) => [ + fieldMetadataItem.id, + fieldMetadataItem, + ]), + ); + + const recordFieldByFieldMetadataItemId = Object.fromEntries( + currentRecordFields.map((recordField) => [ + recordField.fieldMetadataItemId, + recordField, + ]), + ); + + const fieldDefinitionByFieldMetadataItemId = Object.fromEntries( + fieldMetadataItems.map((fieldMetadataItem) => [ + fieldMetadataItem.id, + formatFieldMetadataItemAsColumnDefinition({ + field: fieldMetadataItem, + objectMetadataItem, + position: + recordFieldByFieldMetadataItemId[fieldMetadataItem.id]?.position ?? 0, + labelWidth: + recordFieldByFieldMetadataItemId[fieldMetadataItem.id]?.size ?? 0, + }), + ]), + ); + + const labelIdentifierFieldMetadataItem = useRecoilValue( + labelIdentifierFieldMetadataItemSelector({ + objectMetadataItemId: objectMetadataItem.id, + }), + ); + return ( {children} diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx index 946539602d..985a249271 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx @@ -1,6 +1,6 @@ import { type Meta, type StoryObj } from '@storybook/react'; import { useEffect } from 'react'; -import { useRecoilState, useSetRecoilState } from 'recoil'; +import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage'; @@ -13,15 +13,25 @@ import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDeco import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory'; +import { labelIdentifierFieldMetadataItemSelector } from '@/object-metadata/states/labelIdentifierFieldMetadataItemSelector'; +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; +import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; +import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; +import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector'; +import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext'; +import { mockPerformance } from '@/object-record/record-table/components/__stories__/perf/mock'; import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext'; import { RecordTableContextProvider } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { RecordTableRowDraggableContextProvider } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext'; import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import { ComponentDecorator } from 'twenty-ui/testing'; + import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems'; -import { mockPerformance } from './mock'; const RelationFieldValueSetterEffect = () => { const setEntity = useSetRecoilState( @@ -32,14 +42,36 @@ const RelationFieldValueSetterEffect = () => { recordStoreFamilyState(mockPerformance.relationRecordId), ); + const setCurrentRecordFields = useSetRecoilComponentState( + currentRecordFieldsComponentState, + 'recordTableId', + ); + const [, setObjectMetadataItems] = useRecoilState(objectMetadataItemsState); useEffect(() => { setEntity(mockPerformance.entityValue); setRelationEntity(mockPerformance.relationFieldValue); + setCurrentRecordFields( + mockPerformance.visibleTableColumns.map( + (tableColumn) => + ({ + fieldMetadataItemId: tableColumn.fieldMetadataId, + id: tableColumn.fieldMetadataId, + isVisible: tableColumn.isVisible, + position: tableColumn.position, + size: tableColumn.size, + }) satisfies RecordField, + ), + ); setObjectMetadataItems(generatedMockObjectMetadataItems); - }, [setEntity, setRelationEntity, setObjectMetadataItems]); + }, [ + setEntity, + setRelationEntity, + setObjectMetadataItems, + setCurrentRecordFields, + ]); return null; }; @@ -50,6 +82,55 @@ const meta: Meta = { MemoryRouterDecorator, ChipGeneratorsDecorator, (Story) => { + const currentRecordFields = useRecoilComponentValue( + currentRecordFieldsComponentState, + 'recordTableId', + ); + + const visibleRecordFields = useRecoilComponentValue( + visibleRecordFieldsComponentSelector, + 'recordTableId', + ); + + const fieldMetadataItems = mockPerformance.objectMetadataItem.fields; + + const fieldMetadataItemByFieldMetadataItemId = Object.fromEntries( + fieldMetadataItems.map((fieldMetadataItem) => [ + fieldMetadataItem.id, + // TODO: update performance mocks with new data, and merge with common mocks if possible + fieldMetadataItem as unknown as FieldMetadataItem, + ]), + ); + + const recordFieldByFieldMetadataItemId = Object.fromEntries( + currentRecordFields.map((recordField) => [ + recordField.fieldMetadataItemId, + recordField, + ]), + ); + + const fieldDefinitionByFieldMetadataItemId = Object.fromEntries( + fieldMetadataItems.map((fieldMetadataItem) => [ + fieldMetadataItem.id, + formatFieldMetadataItemAsColumnDefinition({ + // TODO: update performance mocks with new data, and merge with common mocks if possible + field: fieldMetadataItem as any, + objectMetadataItem: mockPerformance.objectMetadataItem as any, + position: + recordFieldByFieldMetadataItemId[fieldMetadataItem.id] + ?.position ?? 0, + labelWidth: + recordFieldByFieldMetadataItemId[fieldMetadataItem.id]?.size ?? 0, + }), + ]), + ); + + const labelIdentifierFieldMetadataItem = useRecoilValue( + labelIdentifierFieldMetadataItemSelector({ + objectMetadataItemId: mockPerformance.objectMetadataItem.id, + }), + ); + return ( {}, objectNamePlural: 'companies', objectNameSingular: 'company', + // TODO: update performance mocks with new data, and merge with common mocks if possible objectMetadataItem: mockPerformance.objectMetadataItem as any, recordIndexId: 'recordIndexId', }} > - - - {}, - onMoveFocus: () => {}, - onCloseTableCell: () => {}, - onMoveHoverToCurrentCell: () => {}, - onActionMenuDropdownOpened: () => {}, - onCellMouseEnter: () => {}, - }} - > - + + + {}, + onMoveFocus: () => {}, + onCloseTableCell: () => {}, + onMoveHoverToCurrentCell: () => {}, + onActionMenuDropdownOpened: () => {}, + onCellMouseEnter: () => {}, }} > - - - - - - - - - - -
-
-
-
-
-
-
-
+ + + + + + + + +
+
+ + + + + + +
); }, @@ -145,7 +240,15 @@ const meta: Meta = { ], component: RecordTableCellFieldContextWrapper, argTypes: { value: { control: 'date' } }, - args: {}, + args: { + recordField: { + fieldMetadataItemId: mockPerformance.fieldDefinition.fieldMetadataId, + id: 'test', + isVisible: mockPerformance.fieldDefinition.isVisible, + position: mockPerformance.fieldDefinition.position, + size: mockPerformance.fieldDefinition.size, + }, + }, parameters: { chromatic: { disableSnapshot: true }, }, diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/mock.ts b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/mock.ts index 81c9716467..da090267fa 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/mock.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/mock.ts @@ -1,4 +1,4 @@ -import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { FieldMetadataType } from 'twenty-shared/types'; export const mockPerformance = { objectMetadataItem: { @@ -708,26 +708,26 @@ export const mockPerformance = { }, }, fieldDefinition: { - fieldMetadataId: '4e79f0b7-d100-4e89-a07b-315a710b8059', + fieldMetadataId: '9058056e-36b3-4a3f-9037-f0bca9744296', primaryLinkLabel: 'Company', - label: 'Company', metadata: { fieldName: 'company', placeHolder: 'Company', relationType: 'TO_ONE_OBJECT', - relationFieldMetadataId: '01fa2247-7937-4493-b7e2-3d72f05d6d25', + relationFieldMetadataId: '7b281010-5f47-4771-b3f5-f4bcd24ed1b5', relationObjectMetadataNameSingular: 'company', relationObjectMetadataNamePlural: 'companies', objectMetadataNameSingular: 'person', options: null, }, + label: 'Company', iconName: 'IconBuildingSkyscraper', type: FieldMetadataType.RELATION, position: 2, size: 150, isLabelIdentifier: false, isVisible: true, - viewFieldId: '924f4c94-cbcd-4de5-b7a2-ebae2f0b2c3b', + viewFieldId: '9a479a97-deaa-4ddb-9d59-96f05875ac09', isSortable: false, isFilterable: true, defaultValue: null, diff --git a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableCellContext.ts b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableCellContext.ts index 41b72cdbcd..33da8cdbda 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableCellContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableCellContext.ts @@ -1,11 +1,10 @@ import { createContext } from 'react'; -import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; -import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; +import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { type TableCellPosition } from '@/object-record/record-table/types/TableCellPosition'; export type RecordTableCellContextValue = { - columnDefinition: ColumnDefinition; + recordField: RecordField; cellPosition: TableCellPosition; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts index 11e7f542e2..d5b45b2a4e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts @@ -1,4 +1,6 @@ +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { type ObjectPermission } from '~/generated/graphql'; @@ -9,8 +11,15 @@ type RecordTableContextValue = { viewBarId: string; objectNameSingular: string; objectMetadataItem: ObjectMetadataItem; - visibleTableColumns: ColumnDefinition[]; objectPermissions: ObjectPermission; + visibleRecordFields: RecordField[]; + recordFieldByFieldMetadataItemId: Record; + labelIdentifierFieldMetadataItem: FieldMetadataItem | undefined; + fieldMetadataItemByFieldMetadataItemId: Record; + fieldDefinitionByFieldMetadataItemId: Record< + string, + ColumnDefinition + >; }; export const [RecordTableContextProvider, useRecordTableContextOrThrow] = diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useHandleColumnsChange.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useHandleColumnsChange.ts deleted file mode 100644 index c3dc3447e7..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useHandleColumnsChange.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; -import { useSetTableColumns } from '@/object-record/record-table/hooks/useSetTableColumns'; -import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; - -// TODO: see how we can better abstract this and set the correct interaction between view and table -// but for now it allows to have a cleaner API globally. -// TODO: should be solved with new RecordField abstraction -export const useHandleColumnsChange = () => { - const { setTableColumns } = useSetTableColumns(); - - const handleColumnsChange = async ({ - columns, - recordTableId, - objectMetadataId, - }: { - columns: ColumnDefinition[]; - recordTableId: string; - objectMetadataId: string; - }) => { - setTableColumns(columns, recordTableId, objectMetadataId); - }; - - return { - handleColumnsChange, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useMoveTableColumn.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useMoveTableColumn.ts new file mode 100644 index 0000000000..85d18af254 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useMoveTableColumn.ts @@ -0,0 +1,31 @@ +import { useCallback } from 'react'; + +import { useUnfocusRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useUnfocusRecordTableCell'; + +import { useMoveRecordField } from '@/object-record/record-field/hooks/useMoveRecordField'; + +type useRecordTableProps = { + recordTableId: string; +}; + +export const useMoveTableColumn = ({ recordTableId }: useRecordTableProps) => { + const { unfocusRecordTableCell } = useUnfocusRecordTableCell(recordTableId); + + const { moveRecordField } = useMoveRecordField(recordTableId); + + const moveTableColumn = useCallback( + async (direction: 'left' | 'right', fieldMetadataItemId: string) => { + unfocusRecordTableCell(); + + moveRecordField({ + direction: direction === 'left' ? 'before' : 'after', + fieldMetadataItemIdToMove: fieldMetadataItemId, + }); + }, + [unfocusRecordTableCell, moveRecordField], + ); + + return { + moveTableColumn, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocusedCell.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocusedCell.ts index 4fe7ca3546..285103d410 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocusedCell.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocusedCell.ts @@ -3,10 +3,10 @@ import { useRecoilCallback } from 'recoil'; import { type MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; import { useFocusRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useFocusRecordTableCell'; import { recordTableFocusPositionComponentState } from '@/object-record/record-table/states/recordTableFocusPositionComponentState'; -import { numberOfTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/numberOfTableColumnsComponentSelector'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; export const useRecordTableMoveFocusedCell = (recordTableId?: string) => { @@ -64,8 +64,8 @@ export const useRecordTableMoveFocusedCell = (recordTableId?: string) => { [recordIndexAllRecordIdsSelector, focusRecordTableCell, focusPositionState], ); - const numberOfTableColumnsSelector = useRecoilComponentCallbackState( - numberOfTableColumnsComponentSelector, + const currentRecordFieldsCallbackState = useRecoilComponentCallbackState( + currentRecordFieldsComponentState, recordTableId, ); @@ -78,23 +78,23 @@ export const useRecordTableMoveFocusedCell = (recordTableId?: string) => { ); const focusPosition = getSnapshotValue(snapshot, focusPositionState); - const numberOfTableColumns = getSnapshotValue( + const numberOfRecordFields = getSnapshotValue( snapshot, - numberOfTableColumnsSelector, - ); + currentRecordFieldsCallbackState, + ).length; const currentColumnIndex = focusPosition.column; const currentRowIndex = focusPosition.row; const isLastRowAndLastColumn = - currentColumnIndex === numberOfTableColumns - 1 && + currentColumnIndex === numberOfRecordFields - 1 && currentRowIndex === allRecordIds.length - 1; const isLastColumnButNotLastRow = - currentColumnIndex === numberOfTableColumns - 1 && + currentColumnIndex === numberOfRecordFields - 1 && currentRowIndex !== allRecordIds.length - 1; - const isNotLastColumn = currentColumnIndex !== numberOfTableColumns - 1; + const isNotLastColumn = currentColumnIndex !== numberOfRecordFields - 1; if (isLastRowAndLastColumn) { return; @@ -115,7 +115,7 @@ export const useRecordTableMoveFocusedCell = (recordTableId?: string) => { [ recordIndexAllRecordIdsSelector, focusPositionState, - numberOfTableColumnsSelector, + currentRecordFieldsCallbackState, focusRecordTableCell, ], ); @@ -125,10 +125,10 @@ export const useRecordTableMoveFocusedCell = (recordTableId?: string) => { () => { const focusPosition = getSnapshotValue(snapshot, focusPositionState); - const numberOfTableColumns = getSnapshotValue( + const numberOfRecordFields = getSnapshotValue( snapshot, - numberOfTableColumnsSelector, - ); + currentRecordFieldsCallbackState, + ).length; const currentColumnIndex = focusPosition.column; const currentRowIndex = focusPosition.row; @@ -153,11 +153,15 @@ export const useRecordTableMoveFocusedCell = (recordTableId?: string) => { } else if (isFirstColumnButNotFirstRow) { focusRecordTableCell({ row: currentRowIndex - 1, - column: numberOfTableColumns - 1, + column: numberOfRecordFields - 1, }); } }, - [numberOfTableColumnsSelector, focusPositionState, focusRecordTableCell], + [ + currentRecordFieldsCallbackState, + focusPositionState, + focusRecordTableCell, + ], ); const moveFocus = (direction: MoveFocusDirection) => { diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useSaveColumnsToView.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useSaveColumnsToView.ts deleted file mode 100644 index b64cfe008d..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useSaveColumnsToView.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; -import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; -import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields'; -import { mapColumnDefinitionsToViewFields } from '@/views/utils/mapColumnDefinitionToViewField'; -import { useRecoilCallback } from 'recoil'; - -export const useSaveColumnsToView = () => { - const { saveViewFields } = useSaveCurrentViewFields(); - - const saveColumnsToView = useRecoilCallback( - () => async (columns) => { - await saveViewFields( - mapColumnDefinitionsToViewFields( - columns as ColumnDefinition[], - ), - ); - }, - [saveViewFields], - ); - - return { saveColumnsToView }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useSetAvailableTableColumns.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useSetAvailableTableColumns.ts deleted file mode 100644 index 4509436c74..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useSetAvailableTableColumns.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; -import { availableTableColumnsComponentState } from '@/object-record/record-table/states/availableTableColumnsComponentState'; -import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; -import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; -import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; -import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; -import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; -import { useRecoilCallback } from 'recoil'; -import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; - -export const useSetAvailableTableColumns = ( - recordTableIdFromProps?: string, -) => { - const recordTableId = useAvailableComponentInstanceIdOrThrow( - RecordTableComponentInstanceContext, - recordTableIdFromProps, - ); - - const availableTableColumnsState = useRecoilComponentCallbackState( - availableTableColumnsComponentState, - recordTableId, - ); - - const setAvailableTableColumns = useRecoilCallback( - ({ snapshot, set }) => - (columns: ColumnDefinition[]) => { - const availableTableColumns = getSnapshotValue( - snapshot, - availableTableColumnsState, - ); - - if (isDeeplyEqual(availableTableColumns, columns)) { - return; - } - set(availableTableColumnsState, columns); - }, - [availableTableColumnsState], - ); - - return { - setAvailableTableColumns, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useSetTableColumns.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useSetTableColumns.ts deleted file mode 100644 index cdd4e64841..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useSetTableColumns.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; -import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; -import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; -import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; -import { useRecoilCallback } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; -import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; - -export const useSetTableColumns = () => { - const setTableColumns = useRecoilCallback( - ({ snapshot, set }) => - ( - columns: ColumnDefinition[], - recordTableId: string, - objectMetadataId: string, - ) => { - const objectMetadataItems = getSnapshotValue( - snapshot, - objectMetadataItemsState, - ); - - const objectMetadataItem = objectMetadataItems.find( - (item) => item.id === objectMetadataId, - ); - - if (!isDefined(objectMetadataItem)) { - return; - } - - const tableColumns = getSnapshotValue( - snapshot, - tableColumnsComponentState.atomFamily({ - instanceId: recordTableId, - }), - ); - - const columnsToSet = columns.filter((column) => - objectMetadataItem.readableFields - .map((field) => field.name) - .includes(column.metadata.fieldName), - ); - - if (isDeeplyEqual(tableColumns, columnsToSet)) { - return; - } - set( - tableColumnsComponentState.atomFamily({ - instanceId: recordTableId, - }), - columnsToSet, - ); - }, - [], - ); - - return { setTableColumns }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts deleted file mode 100644 index c726c6d140..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { useCallback } from 'react'; - -import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; -import { useUnfocusRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useUnfocusRecordTableCell'; - -import { useCreateTableColumn } from '@/object-record/record-field/hooks/useCreateTableColumn'; -import { useMoveRecordField } from '@/object-record/record-field/hooks/useMoveRecordField'; -import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField'; -import { useUpdateTableColumn } from '@/object-record/record-field/hooks/useUpdateTableColumn'; -import { useUpsertRecordField } from '@/object-record/record-field/hooks/useUpsertRecordField'; -import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; -import { type RecordField } from '@/object-record/record-field/types/RecordField'; -import { useHandleColumnsChange } from '@/object-record/record-table/hooks/useHandleColumnsChange'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields'; -import { mapRecordFieldToViewField } from '@/views/utils/mapRecordFieldToViewField'; -import { isDefined } from 'twenty-shared/utils'; -import { v4 } from 'uuid'; -import { sortByProperty } from '~/utils/array/sortByProperty'; -import { type ColumnDefinition } from '../types/ColumnDefinition'; - -type useRecordTableProps = { - recordTableId: string; -}; - -export const useTableColumns = ({ recordTableId }: useRecordTableProps) => { - const currentRecordFields = useRecoilComponentValue( - currentRecordFieldsComponentState, - recordTableId, - ); - - const { unfocusRecordTableCell } = useUnfocusRecordTableCell(recordTableId); - - const { handleColumnsChange } = useHandleColumnsChange(); - - const { updateRecordField } = useUpdateRecordField(recordTableId); - const { upsertRecordField } = useUpsertRecordField(recordTableId); - - const { saveViewFields } = useSaveCurrentViewFields(); - - const { updateTableColumn } = useUpdateTableColumn(recordTableId); - const { createTableColumn } = useCreateTableColumn(recordTableId); - - const handleColumnVisibilityChange = useCallback( - async ( - viewField: Pick< - ColumnDefinition, - 'fieldMetadataId' | 'isVisible' - >, - ) => { - const lastPosition = currentRecordFields.toSorted( - sortByProperty('position', 'desc'), - )[0].position; - - const shouldShowFieldMetadataItem = viewField.isVisible === true; - const corresponingRecordField = currentRecordFields.find( - (recordFieldToFind) => - recordFieldToFind.fieldMetadataItemId === viewField.fieldMetadataId, - ); - - const noExistingRecordField = !isDefined(corresponingRecordField); - - if (noExistingRecordField) { - const recordFieldToUpsert: RecordField = { - id: v4(), - fieldMetadataItemId: viewField.fieldMetadataId, - size: 100, - isVisible: shouldShowFieldMetadataItem, - position: lastPosition + 1, - }; - - upsertRecordField(recordFieldToUpsert); - - createTableColumn(viewField.fieldMetadataId, { - position: lastPosition + 1, - size: 100, - isVisible: shouldShowFieldMetadataItem, - }); - - saveViewFields([mapRecordFieldToViewField(recordFieldToUpsert)]); - } else { - updateRecordField(viewField.fieldMetadataId, { - isVisible: shouldShowFieldMetadataItem, - }); - - const updatedRecordField: RecordField = { - ...corresponingRecordField, - isVisible: shouldShowFieldMetadataItem, - }; - - updateTableColumn(viewField.fieldMetadataId, { - isVisible: shouldShowFieldMetadataItem, - }); - - saveViewFields([mapRecordFieldToViewField(updatedRecordField)]); - } - }, - [ - updateTableColumn, - saveViewFields, - currentRecordFields, - upsertRecordField, - updateRecordField, - createTableColumn, - ], - ); - - const { moveRecordField } = useMoveRecordField(recordTableId); - - const handleMoveTableColumn = useCallback( - async ( - direction: 'left' | 'right', - column: ColumnDefinition, - ) => { - unfocusRecordTableCell(); - - moveRecordField({ - direction: direction === 'left' ? 'before' : 'after', - fieldMetadataItemIdToMove: column.fieldMetadataId, - }); - }, - [unfocusRecordTableCell, moveRecordField], - ); - - return { - handleColumnVisibilityChange, - handleMoveTableColumn, - handleColumnsChange, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyLoading.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyLoading.tsx index 3a3d4c54a1..2765ebb37c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyLoading.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyLoading.tsx @@ -1,16 +1,13 @@ +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { RecordTableRowDraggableContextProvider } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext'; import { RecordTableCellCheckbox } from '@/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox'; import { RecordTableCellGrip } from '@/object-record/record-table/record-table-cell/components/RecordTableCellGrip'; import { RecordTableCellLoading } from '@/object-record/record-table/record-table-cell/components/RecordTableCellLoading'; import { RecordTableTr } from '@/object-record/record-table/record-table-row/components/RecordTableTr'; -import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; export const RecordTableBodyLoading = () => { - const visibleTableColumns = useRecoilComponentValue( - visibleTableColumnsComponentSelector, - ); + const { visibleRecordFields } = useRecordTableContextOrThrow(); return ( @@ -41,8 +38,8 @@ export const RecordTableBodyLoading = () => { > - {visibleTableColumns.map((column) => ( - + {visibleRecordFields.map((recordField) => ( + ))} diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextGeneric.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextGeneric.tsx index 76830780f2..a66d086441 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextGeneric.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextGeneric.tsx @@ -1,41 +1,48 @@ import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly'; +import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; import { isFieldRelationFromManyObjects } from '@/object-record/record-field/ui/types/guards/isFieldRelationFromManyObjects'; import { isFieldRelationToOneObject } from '@/object-record/record-field/ui/types/guards/isFieldRelationToOneObject'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext'; -import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { useContext, type ReactNode } from 'react'; type RecordTableCellFieldContextGenericProps = { + recordField: RecordField; children: ReactNode; }; export const RecordTableCellFieldContextGeneric = ({ + recordField, children, }: RecordTableCellFieldContextGenericProps) => { const { recordId, isRecordReadOnly } = useRecordTableRowContextOrThrow(); - const { objectMetadataItem, objectPermissions } = - useRecordTableContextOrThrow(); + const { + objectMetadataItem, + objectPermissions, + fieldDefinitionByFieldMetadataItemId, + } = useRecordTableContextOrThrow(); const { indexIdentifierUrl, objectPermissionsByObjectMetadataId } = useRecordIndexContextOrThrow(); - const { columnDefinition } = useContext(RecordTableCellContext); + + const fieldDefinition = + fieldDefinitionByFieldMetadataItemId[recordField.fieldMetadataItemId]; const updateRecord = useContext(RecordUpdateContext); let hasObjectReadPermissions = objectPermissions.canReadObjectRecords; // todo @guillim : adjust this to handle morph relations permissions display if ( - isFieldRelationToOneObject(columnDefinition) || - isFieldRelationFromManyObjects(columnDefinition) + isFieldRelationToOneObject(fieldDefinition) || + isFieldRelationFromManyObjects(fieldDefinition) ) { const relationObjectMetadataId = - columnDefinition.metadata.relationObjectMetadataId; + fieldDefinition.metadata.relationObjectMetadataId; const relationObjectPermissions = getObjectPermissionsForObject( objectPermissionsByObjectMetadataId, @@ -49,13 +56,13 @@ export const RecordTableCellFieldContextGeneric = ({ [updateRecord, {}], labelIdentifierLink: indexIdentifierUrl(recordId), isLabelIdentifier: isLabelIdentifierField({ fieldMetadataItem: { - id: columnDefinition.fieldMetadataId, - name: columnDefinition.metadata.fieldName, + id: fieldDefinition.fieldMetadataId, + name: fieldDefinition.metadata.fieldName, }, objectMetadataItem, }), @@ -64,8 +71,8 @@ export const RecordTableCellFieldContextGeneric = ({ isRecordReadOnly: isRecordReadOnly ?? false, objectPermissions, fieldMetadataItem: { - id: columnDefinition.fieldMetadataId, - isUIReadOnly: columnDefinition.metadata.isUIReadOnly ?? false, + id: fieldDefinition.fieldMetadataId, + isUIReadOnly: fieldDefinition.metadata.isUIReadOnly ?? false, }, }), isForbidden: !hasObjectReadPermissions, diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextLabelIdentifier.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextLabelIdentifier.tsx index fa03483291..0fda493f41 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextLabelIdentifier.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextLabelIdentifier.tsx @@ -27,8 +27,10 @@ export const RecordTableCellFieldContextLabelIdentifier = ({ const { indexIdentifierUrl, objectPermissionsByObjectMetadataId } = useRecordIndexContextOrThrow(); const { recordId, isRecordReadOnly } = useRecordTableRowContextOrThrow(); + const { fieldDefinitionByFieldMetadataItemId } = + useRecordTableContextOrThrow(); - const { columnDefinition } = useContext(RecordTableCellContext); + const { recordField } = useContext(RecordTableCellContext); const { objectMetadataItem, recordTableId } = useRecordTableContextOrThrow(); const { rowIndex } = useRecordTableRowContextOrThrow(); const { activateRecordTableRow } = useActiveRecordTableRow(recordTableId); @@ -59,11 +61,14 @@ export const RecordTableCellFieldContextLabelIdentifier = ({ ? 'CLICK' : 'MOUSE_DOWN'; + const fieldDefinition = + fieldDefinitionByFieldMetadataItemId[recordField.fieldMetadataItemId]; + return ( [updateRecord, {}], labelIdentifierLink: indexIdentifierUrl(recordId), isLabelIdentifier: true, @@ -73,11 +78,11 @@ export const RecordTableCellFieldContextLabelIdentifier = ({ isRecordReadOnly: isRecordReadOnly ?? false, objectPermissions, fieldMetadataItem: { - id: columnDefinition.fieldMetadataId, - isUIReadOnly: columnDefinition.metadata.isUIReadOnly ?? false, + id: recordField.fieldMetadataItemId, + isUIReadOnly: fieldDefinition.metadata.isUIReadOnly ?? false, }, }), - maxWidth: columnDefinition.size, + maxWidth: recordField.size, onRecordChipClick: () => { activateRecordTableRow(rowIndex); unfocusRecordTableRow(); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx index dc3fe15a8a..e9c2cc5324 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx @@ -1,40 +1,40 @@ import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; +import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext'; import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix'; -import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { RecordTableCellFieldContextGeneric } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextGeneric'; import { RecordTableCellFieldContextLabelIdentifier } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextLabelIdentifier'; import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId'; -import { useContext, type ReactNode } from 'react'; -import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; +import { type ReactNode } from 'react'; type RecordTableCellFieldContextWrapperProps = { children: ReactNode; + recordField: RecordField; }; export const RecordTableCellFieldContextWrapper = ({ + recordField, children, }: RecordTableCellFieldContextWrapperProps) => { - const { columnDefinition } = useContext(RecordTableCellContext); const { recordId } = useRecordTableRowContextOrThrow(); - const { objectMetadataItem } = useRecordTableContextOrThrow(); + const { objectMetadataItem, fieldMetadataItemByFieldMetadataItemId } = + useRecordTableContextOrThrow(); - if (isUndefinedOrNull(columnDefinition)) { - return null; - } + const fieldMetadataItem = + fieldMetadataItemByFieldMetadataItemId[recordField.fieldMetadataItemId]; const instanceId = getRecordFieldInputInstanceId({ recordId, - fieldName: columnDefinition.metadata.fieldName, + fieldName: fieldMetadataItem.name, prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX, }); const isLabelIdentifier = isLabelIdentifierField({ fieldMetadataItem: { - id: columnDefinition.fieldMetadataId, - name: columnDefinition.metadata.fieldName, + id: recordField.fieldMetadataItemId, + name: fieldMetadataItem.name, }, objectMetadataItem, }); @@ -46,7 +46,10 @@ export const RecordTableCellFieldContextWrapper = ({ {children} ) : ( - + {children} )} diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortalWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortalWrapper.tsx index eae21201f2..71893c5f5c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortalWrapper.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortalWrapper.tsx @@ -1,11 +1,11 @@ import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage'; import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly'; +import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper'; -import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { type TableCellPosition } from '@/object-record/record-table/types/TableCellPosition'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { createPortal } from 'react-dom'; @@ -28,8 +28,8 @@ export const RecordTableCellPortalWrapper = ({ const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow(); - const visibleTableColumns = useRecoilComponentValue( - visibleTableColumnsComponentSelector, + const visibleRecordFields = useRecoilComponentValue( + visibleRecordFieldsComponentSelector, ); const recordId = allRecordIds.at(position.row); @@ -62,11 +62,13 @@ export const RecordTableCellPortalWrapper = ({ > - + {children} diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellWrapper.tsx index d679d9420b..e4d967626e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellWrapper.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellWrapper.tsx @@ -1,39 +1,38 @@ -import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; +import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper'; -import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { type TableCellPosition } from '@/object-record/record-table/types/TableCellPosition'; import { useMemo } from 'react'; export const RecordTableCellWrapper = ({ children, - column, - columnIndex, + recordField, + recordFieldIndex, }: { - column: ColumnDefinition; - columnIndex: number; + recordField: RecordField; + recordFieldIndex: number; children: React.ReactNode; }) => { const { rowIndex } = useRecordTableRowContextOrThrow(); const currentTableCellPosition: TableCellPosition = useMemo( () => ({ - column: columnIndex, + column: recordFieldIndex, row: rowIndex, }), - [columnIndex, rowIndex], + [recordFieldIndex, rowIndex], ); return ( - + {children} diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__mocks__/cell.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__mocks__/cell.ts index 37809cda48..a09b668b8a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__mocks__/cell.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__mocks__/cell.ts @@ -1,7 +1,6 @@ import { RecordTableCellContextValue } from '@/object-record/record-table/contexts/RecordTableCellContext'; import { RecordTableRowContextValue } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { RecordTableRowDraggableContextValue } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; export const recordTableRowContextValue: RecordTableRowContextValue = { rowIndex: 2, @@ -18,17 +17,12 @@ export const recordTableRowDraggableContextValue: RecordTableRowDraggableContext }; export const recordTableCellContextValue: RecordTableCellContextValue = { - columnDefinition: { + recordField: { size: 1, position: 1, - fieldMetadataId: 'fieldMetadataId', - label: 'label', - iconName: 'iconName', - type: FieldMetadataType.TEXT, - metadata: { - placeHolder: 'placeHolder', - fieldName: 'fieldName', - }, + fieldMetadataItemId: 'fieldMetadataId', + id: 'id', + isVisible: true, }, cellPosition: { row: 2, diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellInGroup.test.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellInGroup.test.tsx index 0d42fae0bc..5a07f0ad5b 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellInGroup.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellInGroup.test.tsx @@ -3,6 +3,7 @@ import { RecoilRoot } from 'recoil'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; import { textfieldDefinition } from '@/object-record/record-field/ui/__mocks__/fieldDefinitions'; import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; @@ -29,34 +30,36 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => ( snapshot.set(objectMetadataItemsState, generatedMockObjectMetadataItems); }} > - - - + + - - - + + - {children} - - - - - - + + {children} + + + + + + + ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellNoGroup.test.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellNoGroup.test.tsx index f6cb3cad54..ceafd8e230 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellNoGroup.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellNoGroup.test.tsx @@ -3,6 +3,7 @@ import { RecoilRoot } from 'recoil'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; import { textfieldDefinition } from '@/object-record/record-field/ui/__mocks__/fieldDefinitions'; import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; @@ -35,26 +36,30 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => ( viewBarId="viewBarId" objectNameSingular={CoreObjectNameSingular.Person} > - - - - + + - {children} - - - - + + {children} + + + + + diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx index 37620bc9b9..5338d6adc3 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx @@ -1,12 +1,11 @@ import styled from '@emotion/styled'; import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableAggregateFooterCell } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell'; import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext'; import { FIRST_TH_WIDTH } from '@/object-record/record-table/record-table-header/components/RecordTableHeader'; -import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { isUndefined } from '@sniptt/guards'; import { MOBILE_VIEWPORT } from 'twenty-ui/theme'; @@ -86,9 +85,7 @@ export const RecordTableAggregateFooter = ({ }: { currentRecordGroupId?: string; }) => { - const visibleTableColumns = useRecoilComponentValue( - visibleTableColumnsComponentSelector, - ); + const { visibleRecordFields } = useRecordTableContextOrThrow(); const { scrollWrapperHTMLElement } = useScrollWrapperElement(); @@ -105,13 +102,13 @@ export const RecordTableAggregateFooter = ({ } > - {visibleTableColumns.map((column, index) => { + {visibleRecordFields.map((recordField, index) => { return ( ); })} - + diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx index 5d21fa47ad..88ab0ed589 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx @@ -1,11 +1,10 @@ import styled from '@emotion/styled'; -import { useContext, useMemo } from 'react'; +import { useContext } from 'react'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext'; import { RecordTableColumnFooterWithDropdown } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterWithDropdown'; -import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { mapArrayToObject } from '~/utils/array/mapArrayToObject'; +import { findById, isDefined } from 'twenty-shared/utils'; const COLUMN_MIN_WIDTH = 104; @@ -59,22 +58,21 @@ export const RecordTableAggregateFooterCell = ({ isFirstCell?: boolean; currentRecordGroupId?: string; }) => { - const tableColumns = useRecoilComponentValue(tableColumnsComponentState); - const tableColumnsByKey = useMemo( - () => - mapArrayToObject(tableColumns, ({ fieldMetadataId }) => fieldMetadataId), - [tableColumns], - ); + const { visibleRecordFields } = useRecordTableContextOrThrow(); + const { fieldMetadataId } = useContext( RecordTableColumnAggregateFooterCellContext, ); + const recordField = visibleRecordFields.find(findById(fieldMetadataId)); + + if (!isDefined(recordField)) { + return null; + } + return ( diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHead.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHead.tsx index a0654bffe2..05d2b3b0b9 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHead.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHead.tsx @@ -1,17 +1,15 @@ import { css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState'; +import { fieldMetadataItemByIdSelector } from '@/object-metadata/states/fieldMetadataItemByIdSelector'; +import { isFieldMetadataItemLabelIdentifierSelector } from '@/object-metadata/states/isFieldMetadataItemLabelIdentifierSelector'; +import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useRecoilValue } from 'recoil'; import { useIcons } from 'twenty-ui/display'; import { MOBILE_VIEWPORT } from 'twenty-ui/theme'; -import { type ColumnDefinition } from '../../types/ColumnDefinition'; - -type RecordTableColumnHeadProps = { - column: ColumnDefinition; -}; const StyledTitle = styled.div<{ hideTitle?: boolean }>` align-items: center; @@ -48,26 +46,44 @@ const StyledText = styled.span` white-space: nowrap; `; +type RecordTableColumnHeadProps = { + recordField: RecordField; +}; + export const RecordTableColumnHead = ({ - column, + recordField, }: RecordTableColumnHeadProps) => { const theme = useTheme(); - const { getIcon } = useIcons(); - const Icon = getIcon(column.iconName); + const correspondingFieldMetadataItem = useRecoilValue( + fieldMetadataItemByIdSelector({ + fieldMetadataItemId: recordField.fieldMetadataItemId, + }), + ); const isRecordTableScrolledLeft = useRecoilComponentValue( isRecordTableScrolledLeftComponentState, ); + const { getIcon } = useIcons(); + const Icon = getIcon( + correspondingFieldMetadataItem.foundFieldMetadataItem?.icon, + ); + + const isLabelIdentifier = useRecoilValue( + isFieldMetadataItemLabelIdentifierSelector({ + fieldMetadataItemId: recordField.fieldMetadataItemId, + }), + ); + return ( - + - {column.label} + + {correspondingFieldMetadataItem.foundFieldMetadataItem?.label} + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx index 3e9e308792..05270a5ebb 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx @@ -1,17 +1,20 @@ -import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; +import { isFieldMetadataItemFilterableAndSortableSelector } from '@/object-metadata/states/isFieldMetadataItemFilterableAndSortableSelector'; +import { isFieldMetadataItemLabelIdentifierSelector } from '@/object-metadata/states/isFieldMetadataItemLabelIdentifierSelector'; +import { useChangeRecordFieldVisibility } from '@/object-record/record-field/hooks/useChangeRecordFieldVisibility'; +import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { useHandleToggleColumnSort } from '@/object-record/record-index/hooks/useHandleToggleColumnSort'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useMoveTableColumn } from '@/object-record/record-table/hooks/useMoveTableColumn'; import { useOpenRecordFilterChipFromTableHeader } from '@/object-record/record-table/record-table-header/hooks/useOpenRecordFilterChipFromTableHeader'; -import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; import { useToggleScrollWrapper } from '@/ui/utilities/scroll/hooks/useToggleScrollWrapper'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; +import { useRecoilValue } from 'recoil'; import { IconArrowLeft, IconArrowRight, @@ -20,11 +23,9 @@ import { IconSortDescending, } from 'twenty-ui/display'; import { MenuItem } from 'twenty-ui/navigation'; -import { useTableColumns } from '../../hooks/useTableColumns'; -import { type ColumnDefinition } from '../../types/ColumnDefinition'; export type RecordTableColumnHeadDropdownMenuProps = { - column: ColumnDefinition; + recordField: RecordField; objectMetadataId: string; }; @@ -33,7 +34,7 @@ const StyledDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)` `; export const RecordTableColumnHeadDropdownMenu = ({ - column, + recordField, objectMetadataId, }: RecordTableColumnHeadDropdownMenuProps) => { const { t } = useLingui(); @@ -41,25 +42,37 @@ export const RecordTableColumnHeadDropdownMenu = ({ const { toggleScrollXWrapper, toggleScrollYWrapper } = useToggleScrollWrapper(); - const visibleTableColumns = useRecoilComponentValue( - visibleTableColumnsComponentSelector, + const { visibleRecordFields } = useRecordTableContextOrThrow(); + + const isLabelIdentifier = useRecoilValue( + isFieldMetadataItemLabelIdentifierSelector({ + fieldMetadataItemId: recordField.fieldMetadataItemId, + }), ); - const secondVisibleColumn = visibleTableColumns[1]; - const canMove = column.isLabelIdentifier !== true; + const secondVisibleRecordField = visibleRecordFields[1]; + const canMove = isLabelIdentifier !== true; const canMoveLeft = - column.fieldMetadataId !== secondVisibleColumn?.fieldMetadataId && canMove; + recordField.fieldMetadataItemId !== + secondVisibleRecordField?.fieldMetadataItemId && canMove; + + const lastVisibleRecordField = + visibleRecordFields[visibleRecordFields.length - 1]; - const lastVisibleColumn = visibleTableColumns[visibleTableColumns.length - 1]; const canMoveRight = - column.fieldMetadataId !== lastVisibleColumn?.fieldMetadataId && canMove; + recordField.fieldMetadataItemId !== + lastVisibleRecordField?.fieldMetadataItemId && canMove; const { recordTableId } = useRecordTableContextOrThrow(); - const { handleColumnVisibilityChange, handleMoveTableColumn } = - useTableColumns({ recordTableId }); + const { moveTableColumn } = useMoveTableColumn({ + recordTableId, + }); - const dropdownId = column.fieldMetadataId + '-header'; + const { changeRecordFieldVisibility } = + useChangeRecordFieldVisibility(recordTableId); + + const dropdownId = recordField.fieldMetadataItemId + '-header'; const { closeDropdown } = useCloseDropdown(); @@ -71,9 +84,10 @@ export const RecordTableColumnHeadDropdownMenu = ({ const handleColumnMoveLeft = () => { closeDropdownAndToggleScroll(); + if (!canMoveLeft) return; - handleMoveTableColumn('left', column); + moveTableColumn('left', recordField.fieldMetadataItemId); }; const handleColumnMoveRight = () => { @@ -81,13 +95,13 @@ export const RecordTableColumnHeadDropdownMenu = ({ if (!canMoveRight) return; - handleMoveTableColumn('right', column); + moveTableColumn('right', recordField.fieldMetadataItemId); }; const handleColumnVisibility = () => { closeDropdownAndToggleScroll(); - handleColumnVisibilityChange({ - ...column, + changeRecordFieldVisibility({ + fieldMetadataId: recordField.fieldMetadataItemId, isVisible: false, }); }; @@ -99,7 +113,7 @@ export const RecordTableColumnHeadDropdownMenu = ({ const handleSortClick = () => { closeDropdownAndToggleScroll(); - handleToggleColumnSort(column.fieldMetadataId); + handleToggleColumnSort(recordField.fieldMetadataItemId); }; const { openRecordFilterChipFromTableHeader } = @@ -108,14 +122,18 @@ export const RecordTableColumnHeadDropdownMenu = ({ const handleFilterClick = () => { closeDropdownAndToggleScroll(); - openRecordFilterChipFromTableHeader(column.fieldMetadataId); + openRecordFilterChipFromTableHeader(recordField.fieldMetadataItemId); }; - const isSortable = column.isSortable === true; - const isFilterable = column.isFilterable === true; + const { isFilterable, isSortable } = useRecoilValue( + isFieldMetadataItemFilterableAndSortableSelector({ + fieldMetadataItemId: recordField.fieldMetadataItemId, + }), + ); + const showSeparator = - (isFilterable || isSortable) && column.isLabelIdentifier !== true; - const canHide = column.isLabelIdentifier !== true; + (isFilterable || isSortable) && isLabelIdentifier !== true; + const canHide = isLabelIdentifier !== true; return ( diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown.tsx index e7fda38f1a..17ad06324e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown.tsx @@ -1,5 +1,4 @@ -import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; -import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; +import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { useToggleScrollWrapper } from '@/ui/utilities/scroll/hooks/useToggleScrollWrapper'; import { useCallback } from 'react'; @@ -7,13 +6,13 @@ import { RecordTableColumnHead } from './RecordTableColumnHead'; import { RecordTableColumnHeadDropdownMenu } from './RecordTableColumnHeadDropdownMenu'; type RecordTableColumnHeadWithDropdownProps = { - column: ColumnDefinition; + recordField: RecordField; objectMetadataId: string; }; export const RecordTableColumnHeadWithDropdown = ({ objectMetadataId, - column, + recordField, }: RecordTableColumnHeadWithDropdownProps) => { const { toggleScrollXWrapper, toggleScrollYWrapper } = useToggleScrollWrapper(); @@ -32,11 +31,11 @@ export const RecordTableColumnHeadWithDropdown = ({ } + dropdownId={recordField.fieldMetadataItemId + '-header'} + clickableComponent={} dropdownComponents={ } diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx index 9e8c024b17..563be9bbd8 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx @@ -80,15 +80,18 @@ const StyledTableHead = styled.thead` `; export const RecordTableHeader = () => { - const { visibleTableColumns } = useRecordTableContextOrThrow(); + const { visibleRecordFields } = useRecordTableContextOrThrow(); return ( - {visibleTableColumns.map((column) => ( - + {visibleRecordFields.map((recordField) => ( + ))} diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx index 4fc61b1c7b..f86f09109d 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx @@ -1,20 +1,18 @@ import styled from '@emotion/styled'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useState } from 'react'; import { useRecoilCallback } from 'recoil'; import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField'; -import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; + +import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord'; -import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns'; import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown'; import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState'; import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState'; import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState'; import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState'; -import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; -import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer'; import { type PointerEventListener } from '@/ui/utilities/pointer-event/types/PointerEventListener'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; @@ -23,9 +21,10 @@ import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-st import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { useSaveRecordFields } from '@/views/hooks/useSaveRecordFields'; +import { throwIfNotDefined } from 'twenty-shared/utils'; import { IconPlus } from 'twenty-ui/display'; import { LightIconButton } from 'twenty-ui/input'; -import { mapArrayToObject } from '~/utils/array/mapArrayToObject'; const COLUMN_MIN_WIDTH = 104; @@ -109,13 +108,13 @@ const StyledHeaderIcon = styled.div` `; type RecordTableHeaderCellProps = { - column: ColumnDefinition; + recordField: RecordField; }; export const RecordTableHeaderCell = ({ - column, + recordField, }: RecordTableHeaderCellProps) => { - const { objectMetadataItem, objectPermissions, recordTableId } = + const { objectMetadataItem, objectPermissions } = useRecordTableContextOrThrow(); const resizeFieldOffsetState = useRecoilComponentCallbackState( @@ -126,13 +125,6 @@ export const RecordTableHeaderCell = ({ resizeFieldOffsetComponentState, ); - const tableColumns = useRecoilComponentValue(tableColumnsComponentState); - const tableColumnsByKey = useMemo( - () => - mapArrayToObject(tableColumns, ({ fieldMetadataId }) => fieldMetadataId), - [tableColumns], - ); - const [initialPointerPositionX, setInitialPointerPositionX] = useState< number | null >(null); @@ -140,9 +132,7 @@ export const RecordTableHeaderCell = ({ string | null >(null); - const { handleColumnsChange } = useTableColumns({ - recordTableId, - }); + const { saveRecordFields } = useSaveRecordFields(); const { updateRecordField } = useUpdateRecordField(); @@ -166,6 +156,8 @@ export const RecordTableHeaderCell = ({ const handleResizeHandlerEnd = useRecoilCallback( ({ snapshot, set }) => async () => { + throwIfNotDefined(recordField, 'recordField'); + if (!resizedFieldMetadataItemId) return; const resizeFieldOffset = getSnapshotValue( @@ -174,44 +166,30 @@ export const RecordTableHeaderCell = ({ ); const nextWidth = Math.round( - Math.max( - tableColumnsByKey[resizedFieldMetadataItemId].size + - resizeFieldOffset, - COLUMN_MIN_WIDTH, - ), + Math.max(recordField.size + resizeFieldOffset, COLUMN_MIN_WIDTH), ); set(resizeFieldOffsetState, 0); setInitialPointerPositionX(null); setResizedFieldMetadataItemId(null); - if (nextWidth !== tableColumnsByKey[resizedFieldMetadataItemId].size) { - const nextColumns = tableColumns.map((column) => - column.fieldMetadataId === resizedFieldMetadataItemId - ? { ...column, size: nextWidth } - : column, + if (nextWidth !== recordField.size) { + const updatedRecordField = updateRecordField( + resizedFieldMetadataItemId, + { + size: nextWidth, + }, ); - updateRecordField(resizedFieldMetadataItemId, { - size: nextWidth, - }); - - await handleColumnsChange({ - columns: nextColumns, - objectMetadataId: objectMetadataItem.id, - recordTableId, - }); + saveRecordFields([updatedRecordField]); } }, [ + recordField, + saveRecordFields, resizedFieldMetadataItemId, resizeFieldOffsetState, - tableColumnsByKey, setResizedFieldMetadataItemId, - tableColumns, - handleColumnsChange, - objectMetadataItem, - recordTableId, updateRecordField, ], ); @@ -229,8 +207,13 @@ export const RecordTableHeaderCell = ({ const isMobile = useIsMobile(); + const { labelIdentifierFieldMetadataItem } = useRecordTableContextOrThrow(); + + const isLabelIdentifier = + recordField.fieldMetadataItemId === labelIdentifierFieldMetadataItem?.id; + const disableColumnResize = - column.isLabelIdentifier && isMobile && !isRecordTableScrolledLeft; + isLabelIdentifier && isMobile && !isRecordTableScrolledLeft; const { createNewIndexRecord } = useCreateNewIndexRecord({ objectMetadataItem, @@ -261,11 +244,13 @@ export const RecordTableHeaderCell = ({ return ( {(useIsMobile() || iconVisibility) && - !!column.isLabelIdentifier && + !!isLabelIdentifier && !isReadOnly && hasObjectUpdatePermissions && ( @@ -299,7 +284,7 @@ export const RecordTableHeaderCell = ({ className="cursor-col-resize" role="separator" onPointerDown={() => { - setResizedFieldMetadataItemId(column.fieldMetadataId); + setResizedFieldMetadataItemId(recordField.fieldMetadataItemId); }} /> )} diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx index c0b104a8b6..23ac2bbfa0 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx @@ -2,10 +2,10 @@ import { useCallback } from 'react'; import { useLocation } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; +import { useActiveFieldMetadataItems } from '@/object-metadata/hooks/useActiveFieldMetadataItems'; +import { useChangeRecordFieldVisibility } from '@/object-record/record-field/hooks/useChangeRecordFieldVisibility'; import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; -import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns'; -import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector'; import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { SettingsPath } from '@/types/SettingsPath'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; @@ -13,7 +13,6 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useLingui } from '@lingui/react/macro'; import { IconSettings, useIcons } from 'twenty-ui/display'; import { MenuItem, UndecoratedLink } from 'twenty-ui/navigation'; @@ -21,25 +20,22 @@ import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; export const RecordTableHeaderPlusButtonContent = () => { const { t } = useLingui(); - const { objectMetadataItem, recordTableId } = useRecordTableContextOrThrow(); + const { objectMetadataItem, recordTableId, visibleRecordFields } = + useRecordTableContextOrThrow(); const { closeDropdown } = useCloseDropdown(); - const hiddenTableColumns = useRecoilComponentValue( - hiddenTableColumnsComponentSelector, - ); - const { getIcon } = useIcons(); - const { handleColumnVisibilityChange } = useTableColumns({ - recordTableId, - }); + + const { changeRecordFieldVisibility } = + useChangeRecordFieldVisibility(recordTableId); const handleAddColumn = useCallback( - (column: ColumnDefinition) => { + (column: Pick, 'fieldMetadataId'>) => { closeDropdown(); - handleColumnVisibilityChange({ ...column, isVisible: true }); + changeRecordFieldVisibility({ ...column, isVisible: true }); }, - [handleColumnVisibilityChange, closeDropdown], + [changeRecordFieldVisibility, closeDropdown], ); const location = useLocation(); @@ -47,15 +43,30 @@ export const RecordTableHeaderPlusButtonContent = () => { navigationMemorizedUrlState, ); + const { activeFieldMetadataItems } = useActiveFieldMetadataItems({ + objectMetadataItem, + }); + + const availableFieldMetadataItemsToShow = activeFieldMetadataItems.filter( + (fieldMetadataItemToFilter) => + !visibleRecordFields + .map((recordField) => recordField.fieldMetadataItemId) + .includes(fieldMetadataItemToFilter.id), + ); + return ( - {hiddenTableColumns.map((column) => ( + {availableFieldMetadataItemsToShow.map((fieldMetadataItem) => ( handleAddColumn(column)} - LeftIcon={getIcon(column.iconName)} - text={column.label} + key={fieldMetadataItem.id} + onClick={() => + handleAddColumn({ + fieldMetadataId: fieldMetadataItem.id, + }) + } + LeftIcon={getIcon(fieldMetadataItem.icon)} + text={fieldMetadataItem.label} /> ))} diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableActionRow.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableActionRow.tsx index 63e3d04f2b..b0174ee882 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableActionRow.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableActionRow.tsx @@ -70,7 +70,7 @@ export const RecordTableActionRow = ({ }: RecordTableActionRowProps) => { const theme = useTheme(); - const { visibleTableColumns } = useRecordTableContextOrThrow(); + const { visibleRecordFields } = useRecordTableContextOrThrow(); return ( @@ -85,7 +85,7 @@ export const RecordTableActionRow = ({ {text} - + diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCellsEmpty.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCellsEmpty.tsx index 016ff200a8..8cd57a8cb4 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCellsEmpty.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCellsEmpty.tsx @@ -1,16 +1,16 @@ +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd'; -import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; export const RecordTableCellsEmpty = () => { const { isSelected } = useRecordTableRowContextOrThrow(); - const visibleTableColumns = useRecoilComponentValue( - visibleTableColumnsComponentSelector, - ); + const { visibleRecordFields } = useRecordTableContextOrThrow(); - return visibleTableColumns.map((column) => ( - + return visibleRecordFields.map((recordField) => ( + )); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCellsVisible.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCellsVisible.tsx index 801d2fe49b..baf22cc37f 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCellsVisible.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableCellsVisible.tsx @@ -1,10 +1,9 @@ +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { useRecordTableRowDraggableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext'; import { RecordTableCell } from '@/object-record/record-table/record-table-cell/components/RecordTableCell'; import { RecordTableCellWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellWrapper'; import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd'; -import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { isNonEmptyArray } from '~/utils/isNonEmptyArray'; export const RecordTableCellsVisible = () => { @@ -12,37 +11,38 @@ export const RecordTableCellsVisible = () => { const { isDragging } = useRecordTableRowDraggableContextOrThrow(); - const visibleTableColumns = useRecoilComponentValue( - visibleTableColumnsComponentSelector, - ); + const { visibleRecordFields } = useRecordTableContextOrThrow(); - if (!isNonEmptyArray(visibleTableColumns)) { + if (!isNonEmptyArray(visibleRecordFields)) { return null; } - const tableColumnsAfterFirst = visibleTableColumns.slice(1); + const recordFieldsAfterFirst = visibleRecordFields.slice(1); return ( <> - + - {tableColumnsAfterFirst.map((column, columnIndex) => ( + {recordFieldsAfterFirst.map((recordField, recordFieldIndex) => ( diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx index d8f065c6ef..e2a44a10ef 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx @@ -11,9 +11,7 @@ import { RecordTableTd } from '@/object-record/record-table/record-table-cell/co import { RecordTableRecordGroupStickyEffect } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupStickyEffect'; import { useAggregateRecordsForRecordTableSection } from '@/object-record/record-table/record-table-section/hooks/useAggregateRecordsForRecordTableSection'; import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState'; -import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { useRecoilComponentFamilyState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyState'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { Tag } from 'twenty-ui/components'; @@ -57,11 +55,8 @@ export const RecordTableRecordGroupSection = () => { const currentRecordGroupId = useCurrentRecordGroupId(); - const visibleColumns = useRecoilComponentValue( - visibleTableColumnsComponentSelector, - ); - - const { objectMetadataItem } = useRecordTableContextOrThrow(); + const { visibleRecordFields, objectMetadataItem } = + useRecordTableContextOrThrow(); const { aggregateValue, aggregateLabel } = useAggregateRecordsForRecordTableSection(); @@ -121,7 +116,7 @@ export const RecordTableRecordGroupSection = () => { /> - + diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/availableTableColumnsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/availableTableColumnsComponentState.ts deleted file mode 100644 index e92f9e7e3b..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/states/availableTableColumnsComponentState.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; - -import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; -import { type ColumnDefinition } from '../types/ColumnDefinition'; - -export const availableTableColumnsComponentState = createComponentState< - ColumnDefinition[] ->({ - key: 'availableTableColumnsComponentState', - defaultValue: [], - componentInstanceContext: RecordTableComponentInstanceContext, -}); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector.ts deleted file mode 100644 index e688ee07dd..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { availableTableColumnsComponentState } from '@/object-record/record-table/states/availableTableColumnsComponentState'; -import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; -import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; -import { createComponentSelector } from '@/ui/utilities/state/component-state/utils/createComponentSelector'; -import { mapArrayToObject } from '~/utils/array/mapArrayToObject'; - -export const hiddenTableColumnsComponentSelector = createComponentSelector({ - key: 'hiddenTableColumnsComponentSelector', - componentInstanceContext: RecordTableComponentInstanceContext, - get: - ({ instanceId }) => - ({ get }) => { - const tableColumns = get( - tableColumnsComponentState.atomFamily({ instanceId }), - ); - const availableColumns = get( - availableTableColumnsComponentState.atomFamily({ instanceId }), - ); - const tableColumnsByKey = mapArrayToObject( - tableColumns, - ({ fieldMetadataId }) => fieldMetadataId, - ); - - const hiddenColumns = availableColumns - .filter( - ({ fieldMetadataId }) => - !tableColumnsByKey[fieldMetadataId]?.isVisible, - ) - .map((availableColumn) => { - const { fieldMetadataId } = availableColumn; - const existingTableColumn = tableColumnsByKey[fieldMetadataId]; - - return { - ...(existingTableColumn || availableColumn), - isVisible: false, - }; - }); - - return hiddenColumns; - }, -}); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/numberOfTableColumnsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/numberOfTableColumnsComponentSelector.ts deleted file mode 100644 index abb426860e..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/numberOfTableColumnsComponentSelector.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; -import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; -import { createComponentSelector } from '@/ui/utilities/state/component-state/utils/createComponentSelector'; - -export const numberOfTableColumnsComponentSelector = createComponentSelector({ - key: 'numberOfTableColumnsComponentSelector', - componentInstanceContext: RecordTableComponentInstanceContext, - get: - ({ instanceId }) => - ({ get }) => - get(tableColumnsComponentState.atomFamily({ instanceId })).length, -}); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector.ts deleted file mode 100644 index 0329915585..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; -import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; -import { createComponentSelector } from '@/ui/utilities/state/component-state/utils/createComponentSelector'; - -export const visibleTableColumnsComponentSelector = createComponentSelector({ - key: 'visibleTableColumnsComponentSelector', - componentInstanceContext: RecordTableComponentInstanceContext, - get: - ({ instanceId }) => - ({ get }) => { - const columns = get( - tableColumnsComponentState.atomFamily({ instanceId }), - ); - - return columns - .filter((column) => column.isVisible) - .sort((columnA, columnB) => columnA.position - columnB.position); - }, -}); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/tableColumnsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/tableColumnsComponentState.ts deleted file mode 100644 index fdc063a836..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/states/tableColumnsComponentState.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; - -import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; -import { type ColumnDefinition } from '../types/ColumnDefinition'; - -export const tableColumnsComponentState = createComponentState< - ColumnDefinition[] ->({ - key: 'tableColumnsComponentState', - defaultValue: [], - componentInstanceContext: RecordTableComponentInstanceContext, -}); diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx index 786b5b3edb..1b7d763c14 100644 --- a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx +++ b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx @@ -4,13 +4,13 @@ import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainCo import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; -import { useSetAvailableTableColumns } from '@/object-record/record-table/hooks/useSetAvailableTableColumns'; -import { useSetTableColumns } from '@/object-record/record-table/hooks/useSetTableColumns'; + +import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; +import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS } from '@/sign-in-background-mock/constants/SignInBackgroundMockColumnDefinitions'; -import { SIGN_IN_BACKGROUND_MOCK_VIEW_FIELDS } from '@/sign-in-background-mock/constants/SignInBackgroundMockViewFields'; import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import { useInitViewBar } from '@/views/hooks/useInitViewBar'; -import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions'; type SignInBackgroundMockContainerEffectProps = { objectNamePlural: string; @@ -31,10 +31,10 @@ export const SignInBackgroundMockContainerEffect = ({ MAIN_CONTEXT_STORE_INSTANCE_ID, ); - const { setAvailableTableColumns } = - useSetAvailableTableColumns(recordTableId); - - const { setTableColumns } = useSetTableColumns(); + const setCurrentRecordFields = useSetRecoilComponentState( + currentRecordFieldsComponentState, + recordTableId, + ); const { objectNameSingular } = useObjectNameSingularFromPlural({ objectNamePlural, @@ -52,17 +52,21 @@ export const SignInBackgroundMockContainerEffect = ({ setAvailableFieldDefinitions?.(SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS); - setAvailableTableColumns(SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS); - - setTableColumns( - mapViewFieldsToColumnDefinitions({ - viewFields: SIGN_IN_BACKGROUND_MOCK_VIEW_FIELDS, - columnDefinitions: SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS, - }), - recordTableId, - objectMetadataItem.id, + const recordFields = SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS.filter( + (fieldDefinition) => fieldDefinition.fieldMetadataId !== '', + ).map( + (columnDefinitionMock) => + ({ + fieldMetadataItemId: columnDefinitionMock.fieldMetadataId, + id: columnDefinitionMock.fieldMetadataId, + isVisible: columnDefinitionMock.isVisible, + position: columnDefinitionMock.position, + size: columnDefinitionMock.size, + }) satisfies RecordField as RecordField, ); + setCurrentRecordFields(recordFields); + if (contextStoreCurrentObjectMetadataItemId !== objectMetadataItem.id) { setContextStoreCurrentObjectMetadataItemId(objectMetadataItem.id); } @@ -70,11 +74,10 @@ export const SignInBackgroundMockContainerEffect = ({ setViewObjectMetadataId, setAvailableFieldDefinitions, objectMetadataItem, - setAvailableTableColumns, - setTableColumns, recordTableId, setContextStoreCurrentObjectMetadataItemId, contextStoreCurrentObjectMetadataItemId, + setCurrentRecordFields, ]); return <>; diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockColumnDefinitions.ts b/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockColumnDefinitions.ts index 4882b9f665..f1112f4526 100644 --- a/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockColumnDefinitions.ts +++ b/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockColumnDefinitions.ts @@ -1,47 +1,20 @@ +/* eslint-disable @nx/workspace-max-consts-per-file */ import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns'; +import { findByProperty } from 'twenty-shared/utils'; import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql'; +import { getMockCompanyObjectMetadataItem } from '~/testing/mock-data/companies'; + +const COMPANY_MOCK_OBJECT = getMockCompanyObjectMetadataItem(); export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( [ { position: 0, - fieldMetadataId: '20202020-5e4e-4007-a630-8a2617914889', - label: 'Domain Name', - size: 100, - type: FieldMetadataType.LINKS, - metadata: { - fieldName: 'domainName', - placeHolder: 'Domain Name', - relationObjectMetadataNameSingular: '', - relationObjectMetadataNamePlural: '', - objectMetadataNameSingular: 'company', - }, - iconName: 'IconLink', - isVisible: true, - defaultValue: '', - }, - { - position: 1, - fieldMetadataId: '20202020-7fbd-41ad-b64d-25a15ff62f04', - label: 'Employees', - size: 100, - type: FieldMetadataType.NUMBER, - metadata: { - fieldName: 'employees', - placeHolder: 'Employees', - relationObjectMetadataNameSingular: '', - relationObjectMetadataNamePlural: '', - objectMetadataNameSingular: 'company', - }, - iconName: 'IconUsers', - isVisible: true, - defaultValue: 0, - }, - { - position: 2, - fieldMetadataId: 'REPLACE_ME', + fieldMetadataId: + COMPANY_MOCK_OBJECT.fields.find(findByProperty('name', 'name'))?.id ?? + '', label: 'Name', size: 100, type: FieldMetadataType.TEXT, @@ -56,26 +29,68 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( isVisible: true, defaultValue: '', }, + { + position: 1, + fieldMetadataId: + COMPANY_MOCK_OBJECT.fields.find(findByProperty('name', 'domainName')) + ?.id ?? '', + label: 'Domain Name', + size: 100, + type: FieldMetadataType.LINKS, + metadata: { + fieldName: 'domainName', + placeHolder: 'Domain Name', + relationObjectMetadataNameSingular: '', + relationObjectMetadataNamePlural: '', + objectMetadataNameSingular: 'company', + }, + iconName: 'IconLink', + isVisible: true, + defaultValue: '', + }, + { + position: 2, + fieldMetadataId: + COMPANY_MOCK_OBJECT.fields.find(findByProperty('name', 'employees')) + ?.id ?? '', + label: 'Employees', + size: 100, + type: FieldMetadataType.NUMBER, + metadata: { + fieldName: 'employees', + placeHolder: 'Employees', + relationObjectMetadataNameSingular: '', + relationObjectMetadataNamePlural: '', + objectMetadataNameSingular: 'company', + }, + iconName: 'IconUsers', + isVisible: true, + defaultValue: 0, + }, { position: 3, - fieldMetadataId: '20202020-e7c8-4771-8cc4-ce0e8c36a3c0', - label: 'Favorites', + fieldMetadataId: + COMPANY_MOCK_OBJECT.fields.find(findByProperty('name', 'people'))?.id ?? + '', + label: 'People', size: 100, type: FieldMetadataType.RELATION, metadata: { - fieldName: 'favorites', + fieldName: 'people', relationType: RelationType.ONE_TO_MANY, relationObjectMetadataNameSingular: '', relationObjectMetadataNamePlural: '', objectMetadataNameSingular: 'company', }, - iconName: 'IconHeart', + iconName: 'IconUsers', isVisible: true, defaultValue: [], }, { position: 4, - fieldMetadataId: '20202020-ad10-4117-a039-3f04b7a5f939', + fieldMetadataId: + COMPANY_MOCK_OBJECT.fields.find(findByProperty('name', 'address')) + ?.id ?? '', label: 'Address', size: 100, type: FieldMetadataType.ADDRESS, @@ -92,7 +107,9 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( }, { position: 5, - fieldMetadataId: '20202020-0739-495d-8e70-c0807f6b2268', + fieldMetadataId: + COMPANY_MOCK_OBJECT.fields.find(findByProperty('name', 'accountOwner')) + ?.id ?? '', label: 'Account Owner', size: 100, type: FieldMetadataType.RELATION, @@ -109,24 +126,9 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( }, { position: 6, - fieldMetadataId: '20202020-68b4-4c8e-af19-738eba2a42a5', - label: 'People', - size: 100, - type: FieldMetadataType.RELATION, - metadata: { - fieldName: 'people', - relationType: RelationType.ONE_TO_MANY, - relationObjectMetadataNameSingular: '', - relationObjectMetadataNamePlural: '', - objectMetadataNameSingular: 'company', - }, - iconName: 'IconUsers', - isVisible: true, - defaultValue: [], - }, - { - position: 7, - fieldMetadataId: '20202020-61af-4ffd-b79b-baed6db8ad11', + fieldMetadataId: + COMPANY_MOCK_OBJECT.fields.find(findByProperty('name', 'attachments')) + ?.id ?? '', label: 'Attachments', size: 100, type: FieldMetadataType.RELATION, @@ -142,8 +144,10 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( defaultValue: [], }, { - position: 8, - fieldMetadataId: '20202020-4dc2-47c9-bb15-6e6f19ba9e46', + position: 7, + fieldMetadataId: + COMPANY_MOCK_OBJECT.fields.find(findByProperty('name', 'createdAt')) + ?.id ?? '', label: 'Creation date', size: 100, type: FieldMetadataType.DATE_TIME, @@ -159,8 +163,11 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( defaultValue: '', }, { - position: 9, - fieldMetadataId: '20202020-9e9f-4235-98b2-c76f3e2d281e', + position: 8, + fieldMetadataId: + COMPANY_MOCK_OBJECT.fields.find( + findByProperty('name', 'idealCustomerProfile'), + )?.id ?? '', label: 'ICP', size: 100, type: FieldMetadataType.BOOLEAN, @@ -176,8 +183,10 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( defaultValue: false, }, { - position: 10, - fieldMetadataId: '20202020-a61d-4b78-b998-3fd88b4f73a1', + position: 9, + fieldMetadataId: + COMPANY_MOCK_OBJECT.fields.find(findByProperty('name', 'linkedinLink')) + ?.id ?? '', label: 'Linkedin', size: 100, type: FieldMetadataType.LINKS, @@ -193,8 +202,10 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( defaultValue: '', }, { - position: 11, - fieldMetadataId: '20202020-e3fc-46ff-b552-3e757843f06e', + position: 10, + fieldMetadataId: + COMPANY_MOCK_OBJECT.fields.find(findByProperty('name', 'opportunities')) + ?.id ?? '', label: 'Opportunities', size: 100, type: FieldMetadataType.RELATION, @@ -210,8 +221,10 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( defaultValue: [], }, { - position: 12, - fieldMetadataId: '20202020-46e3-479a-b8f4-77137c74daa6', + position: 11, + fieldMetadataId: + COMPANY_MOCK_OBJECT.fields.find(findByProperty('name', 'xLink'))?.id ?? + '', label: 'X', size: 100, type: FieldMetadataType.LINKS, @@ -227,8 +240,11 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( defaultValue: '', }, { - position: 13, - fieldMetadataId: '20202020-4a2e-4b41-8562-279963e8947e', + position: 12, + fieldMetadataId: + COMPANY_MOCK_OBJECT.fields.find( + findByProperty('name', 'activityTargets'), + )?.id ?? '', label: 'Activities', size: 100, type: FieldMetadataType.RELATION, @@ -244,8 +260,11 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( defaultValue: [], }, { - position: 14, - fieldMetadataId: '20202020-4a5a-466f-92d9-c3870d9502a9', + position: 13, + fieldMetadataId: + COMPANY_MOCK_OBJECT.fields.find( + findByProperty('name', 'annualRecurringRevenue'), + )?.id ?? '', label: 'ARR', size: 100, type: FieldMetadataType.CURRENCY, @@ -260,5 +279,24 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = ( isVisible: true, defaultValue: 0, }, + { + position: 14, + fieldMetadataId: + COMPANY_MOCK_OBJECT.fields.find(findByProperty('name', 'favorites')) + ?.id ?? '', + label: 'Favorites', + size: 100, + type: FieldMetadataType.RELATION, + metadata: { + fieldName: 'favorites', + relationType: RelationType.ONE_TO_MANY, + relationObjectMetadataNameSingular: '', + relationObjectMetadataNamePlural: '', + objectMetadataNameSingular: 'company', + }, + iconName: 'IconHeart', + isVisible: true, + defaultValue: [], + }, ] satisfies ColumnDefinition[] ).filter(filterAvailableTableColumns); diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockCompanies.ts b/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockCompanies.ts index 01ebf0a470..d2cf949ee1 100644 --- a/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockCompanies.ts +++ b/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockCompanies.ts @@ -1,4 +1,9 @@ -export const SIGN_IN_BACKGROUND_MOCK_COMPANIES = [ +import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; +import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection'; +import { type RecordGqlEdge } from '@/object-record/graphql/types/RecordGqlEdge'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +const baseMock = [ { __typename: 'Company', id: '04b2e9f5-0713-40a5-8216-82802401d33e', @@ -1360,3 +1365,22 @@ export const SIGN_IN_BACKGROUND_MOCK_COMPANIES = [ }, }, ]; + +// eslint-disable-next-line @nx/workspace-max-consts-per-file, @typescript-eslint/naming-convention +const baseMockToRecordConnection = { + edges: baseMock.map( + (mockCompany) => + ({ + node: mockCompany, + cursor: mockCompany.id, + __typename: 'CompanyEdge', + }) as RecordGqlEdge, + ) as any, + pageInfo: {}, + __typename: 'CompanyConnection', +} as RecordGqlConnection; + +// eslint-disable-next-line @nx/workspace-max-consts-per-file +export const SIGN_IN_BACKGROUND_MOCK_COMPANIES = getRecordsFromRecordConnection( + { recordConnection: baseMockToRecordConnection }, +); diff --git a/packages/twenty-front/src/modules/views/components/ViewFieldsHiddenDropdownSection.tsx b/packages/twenty-front/src/modules/views/components/ViewFieldsHiddenDropdownSection.tsx index d4950abae5..8447f01d4a 100644 --- a/packages/twenty-front/src/modules/views/components/ViewFieldsHiddenDropdownSection.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewFieldsHiddenDropdownSection.tsx @@ -1,7 +1,7 @@ import { useActiveFieldMetadataItems } from '@/object-metadata/hooks/useActiveFieldMetadataItems'; import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; -import { useObjectOptionsForTable } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForTable'; import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext'; +import { useChangeRecordFieldVisibility } from '@/object-record/record-field/hooks/useChangeRecordFieldVisibility'; import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; @@ -15,10 +15,8 @@ export const ViewFieldsHiddenDropdownSection = () => { ObjectOptionsDropdownContext, ); - const { handleColumnVisibilityChange } = useObjectOptionsForTable( - recordIndexId, - objectMetadataItem.id, - ); + const { changeRecordFieldVisibility } = + useChangeRecordFieldVisibility(recordIndexId); const { handleBoardFieldVisibilityChange } = useObjectOptionsForBoard({ objectNameSingular: objectMetadataItem.nameSingular, @@ -29,7 +27,7 @@ export const ViewFieldsHiddenDropdownSection = () => { const handleChangeFieldVisibility = viewType === ViewType.Kanban ? handleBoardFieldVisibilityChange - : handleColumnVisibilityChange; + : changeRecordFieldVisibility; const currentRecordFields = useRecoilComponentValue( currentRecordFieldsComponentState, diff --git a/packages/twenty-front/src/modules/views/components/ViewFieldsVisibleDropdownSection.tsx b/packages/twenty-front/src/modules/views/components/ViewFieldsVisibleDropdownSection.tsx index 411b2bf4a3..e1110ea096 100644 --- a/packages/twenty-front/src/modules/views/components/ViewFieldsVisibleDropdownSection.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewFieldsVisibleDropdownSection.tsx @@ -3,9 +3,10 @@ import { type DropResult, type ResponderProvided } from '@hello-pangea/dnd'; import { useGetFieldMetadataItemByIdOrThrow } from '@/object-metadata/hooks/useGetFieldMetadataItemById'; import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem'; import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; -import { useObjectOptionsForTable } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForTable'; +import { useProcessOptionDropdownDragEnd } from '@/object-record/object-options-dropdown/hooks/useProcessOptionDropdownDragEnd'; import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext'; -import { visibleAndReadableRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleAndReadableRecordFieldsComponentSelector'; +import { useChangeRecordFieldVisibility } from '@/object-record/record-field/hooks/useChangeRecordFieldVisibility'; +import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector'; import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem'; import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; @@ -22,8 +23,8 @@ export const ViewFieldsVisibleDropdownSection = () => { ObjectOptionsDropdownContext, ); - const { handleReorderColumns, handleColumnVisibilityChange } = - useObjectOptionsForTable(recordIndexId, objectMetadataItem.id); + const { processOptionDropdownDragEnd } = + useProcessOptionDropdownDragEnd(recordIndexId); const { handleReorderBoardFields, handleBoardFieldVisibilityChange } = useObjectOptionsForBoard({ @@ -38,12 +39,15 @@ export const ViewFieldsVisibleDropdownSection = () => { const handleReorderFields = viewType === ViewType.Kanban ? handleReorderBoardFields - : handleReorderColumns; + : processOptionDropdownDragEnd; + + const { changeRecordFieldVisibility } = + useChangeRecordFieldVisibility(recordIndexId); const handleChangeFieldVisibility = viewType === ViewType.Kanban ? handleBoardFieldVisibilityChange - : handleColumnVisibilityChange; + : changeRecordFieldVisibility; const handleDragEnd = (result: DropResult, provided: ResponderProvided) => { handleReorderFields(result, provided); @@ -55,7 +59,7 @@ export const ViewFieldsVisibleDropdownSection = () => { getLabelIdentifierFieldMetadataItem(objectMetadataItem); const visibleRecordFields = useRecoilComponentValue( - visibleAndReadableRecordFieldsComponentSelector, + visibleRecordFieldsComponentSelector, ); const nonDraggableRecordField = visibleRecordFields.find( diff --git a/packages/twenty-front/src/modules/views/components/__stories__/ViewBarFilterDropdown.stories.tsx b/packages/twenty-front/src/modules/views/components/__stories__/ViewBarFilterDropdown.stories.tsx index 856c117df5..d9e423100f 100644 --- a/packages/twenty-front/src/modules/views/components/__stories__/ViewBarFilterDropdown.stories.tsx +++ b/packages/twenty-front/src/modules/views/components/__stories__/ViewBarFilterDropdown.stories.tsx @@ -4,11 +4,9 @@ import { type TaskGroups } from '@/activities/tasks/components/TaskGroups'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; -import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; import { prefetchViewsState } from '@/prefetch/states/prefetchViewsState'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import { ViewBarFilterDropdown } from '@/views/components/ViewBarFilterDropdown'; @@ -16,6 +14,8 @@ import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewCompon import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; +import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; +import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { VIEW_BAR_FILTER_DROPDOWN_ID } from '@/views/constants/ViewBarFilterDropdownId'; import { coreViewsState } from '@/views/states/coreViewState'; import { within } from '@storybook/test'; @@ -45,8 +45,8 @@ const meta: Meta = { )!; const instanceId = companyObjectMetadataItem.id; - const setTableColumns = useSetRecoilComponentState( - tableColumnsComponentState, + const setCurrentRecordFields = useSetRecoilComponentState( + currentRecordFieldsComponentState, instanceId, ); @@ -68,14 +68,16 @@ const meta: Meta = { const columns = companyObjectMetadataItem.fields.map( (fieldMetadataItem, index) => - formatFieldMetadataItemAsColumnDefinition({ - field: fieldMetadataItem, - objectMetadataItem: companyObjectMetadataItem, + ({ + id: fieldMetadataItem.id, + fieldMetadataItemId: fieldMetadataItem.id, + isVisible: true, position: index, - }), + size: 100, + }) satisfies RecordField, ); - setTableColumns(columns); + setCurrentRecordFields(columns); return ( { + const { saveViewFields } = useSaveCurrentViewFields(); + + const saveRecordFields = (recordFields: RecordField[]) => { + saveViewFields(recordFields.map(mapRecordFieldToViewField)); + }; + + return { + saveRecordFields, + }; +}; diff --git a/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx b/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx index ce794ac3ce..92f6c58f47 100644 --- a/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx @@ -3,10 +3,14 @@ import { useRecoilValue } from 'recoil'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId'; +import { labelIdentifierFieldMetadataItemSelector } from '@/object-metadata/states/labelIdentifierFieldMetadataItemSelector'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; +import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; +import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector'; import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates'; import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext'; @@ -16,12 +20,13 @@ import { } from '@/object-record/record-table/contexts/RecordTableContext'; import { useSetRecordTableData } from '@/object-record/record-table/hooks/internal/useSetRecordTableData'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; -import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId'; import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; import { type View } from '@/views/types/View'; +import { mapViewFieldToRecordField } from '@/views/utils/mapViewFieldToRecordField'; import { useEffect, useMemo } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { getCompaniesMock } from '~/testing/mock-data/companies'; @@ -35,6 +40,9 @@ const InternalTableStateLoaderEffect = ({ }) => { const { recordTableId } = useRecordTableContextOrThrow(); const { loadRecordIndexStates } = useLoadRecordIndexStates(); + const setCurrentRecordFields = useSetRecoilComponentState( + currentRecordFieldsComponentState, + ); const setRecordTableData = useSetRecordTableData({ recordTableId, @@ -54,7 +62,18 @@ const InternalTableStateLoaderEffect = ({ setRecordTableData({ records: getCompaniesMock(), }); - }, [loadRecordIndexStates, objectMetadataItem, setRecordTableData, view]); + const recordFields = view.viewFields + .map(mapViewFieldToRecordField) + .filter(isDefined); + + setCurrentRecordFields(recordFields); + }, [ + loadRecordIndexStates, + objectMetadataItem, + setRecordTableData, + view, + setCurrentRecordFields, + ]); return null; }; @@ -66,11 +85,53 @@ const InternalTableContextProviders = ({ children: React.ReactNode; objectMetadataItem: ObjectMetadataItem; }) => { - const visibleTableColumns = useRecoilComponentValue( - visibleTableColumnsComponentSelector, + const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + + const currentRecordFields = useRecoilComponentValue( + currentRecordFieldsComponentState, + 'record-index', ); - const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + const visibleRecordFields = useRecoilComponentValue( + visibleRecordFieldsComponentSelector, + 'record-index', + ); + + const fieldMetadataItems = objectMetadataItem.fields; + + const fieldMetadataItemByFieldMetadataItemId = Object.fromEntries( + fieldMetadataItems.map((fieldMetadataItem) => [ + fieldMetadataItem.id, + fieldMetadataItem, + ]), + ); + + const recordFieldByFieldMetadataItemId = Object.fromEntries( + currentRecordFields.map((recordField) => [ + recordField.fieldMetadataItemId, + recordField, + ]), + ); + + const fieldDefinitionByFieldMetadataItemId = Object.fromEntries( + fieldMetadataItems.map((fieldMetadataItem) => [ + fieldMetadataItem.id, + formatFieldMetadataItemAsColumnDefinition({ + field: fieldMetadataItem, + objectMetadataItem: objectMetadataItem, + position: + recordFieldByFieldMetadataItemId[fieldMetadataItem.id]?.position ?? 0, + labelWidth: + recordFieldByFieldMetadataItemId[fieldMetadataItem.id]?.size ?? 0, + }), + ]), + ); + + const labelIdentifierFieldMetadataItem = useRecoilValue( + labelIdentifierFieldMetadataItemSelector({ + objectMetadataItemId: objectMetadataItem.id, + }), + ); return ( edge.node, -); +const allMockedCompanyRecords = getRecordsFromRecordConnection({ + recordConnection: companiesQueryResult.companies, +}) as ObjectRecord[]; + export const getCompaniesMock = () => { - return [...allMockedCompanyRecords]; + return [...allMockedCompanyRecords] as Company[]; +}; + +export const getCompaniesRecordConnectionMock = () => { + const companiesMock = companiesQueryResult.companies.edges.map( + (edge) => edge.node, + ); + + return companiesMock; }; export const getMockCompanyObjectMetadataItem = () => { @@ -744,9 +755,10 @@ export const getMockCompanyObjectMetadataItem = () => { return companyObjectMetadataItem; }; + export const getCompanyDuplicateMock = () => { return { - ...companiesQueryResult.companies.edges[0].node, + ...allMockedCompanyRecords[0], id: '8b40856a-2ec9-4c03-8bc0-c032c89e1824', }; }; diff --git a/packages/twenty-front/src/testing/mock-data/people.ts b/packages/twenty-front/src/testing/mock-data/people.ts index fa1b12722a..46f91898a6 100644 --- a/packages/twenty-front/src/testing/mock-data/people.ts +++ b/packages/twenty-front/src/testing/mock-data/people.ts @@ -1,6 +1,7 @@ -import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode'; +import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; + import { type FieldMetadataType } from 'twenty-shared/types'; import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow'; @@ -1747,9 +1748,9 @@ export const peopleQueryResult = { }, } satisfies { people: RecordGqlConnection }; -export const allMockPersonRecords = peopleQueryResult.people.edges.map((edge) => - getRecordFromRecordNode({ recordNode: edge.node }), -); +export const allMockPersonRecords = getRecordsFromRecordConnection({ + recordConnection: peopleQueryResult.people, +}) as ObjectRecord[]; export const getPeopleRecordConnectionMock = () => { const peopleMock = peopleQueryResult.people.edges.map((edge) => edge.node); diff --git a/packages/twenty-shared/src/utils/index.ts b/packages/twenty-shared/src/utils/index.ts index 07d1e1358a..19f1ae9dc5 100644 --- a/packages/twenty-shared/src/utils/index.ts +++ b/packages/twenty-shared/src/utils/index.ts @@ -34,6 +34,7 @@ export { capitalize } from './strings/capitalize'; export type { StringPropertyKeys } from './trim-and-remove-duplicated-whitespaces-from-object-string-properties'; export { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from './trim-and-remove-duplicated-whitespaces-from-object-string-properties'; export { trimAndRemoveDuplicatedWhitespacesFromString } from './trim-and-remove-duplicated-whitespaces-from-string'; +export { throwIfNotDefined } from './typeguard/throwIfNotDefined'; export { absoluteUrlSchema } from './url/absoluteUrlSchema'; export { buildSignedPath } from './url/buildSignedPath'; export { getAbsoluteUrl } from './url/getAbsoluteUrl'; diff --git a/packages/twenty-shared/src/utils/typeguard/throwIfNotDefined.ts b/packages/twenty-shared/src/utils/typeguard/throwIfNotDefined.ts new file mode 100644 index 0000000000..a4bf0a98e4 --- /dev/null +++ b/packages/twenty-shared/src/utils/typeguard/throwIfNotDefined.ts @@ -0,0 +1,8 @@ +export const throwIfNotDefined = ( + value: T, + variableName: string +): asserts value is NonNullable => { + if (value === null || value === undefined) { + throw new Error(`Value must be defined for variable ${variableName}, this should not happen`); + } +}