Refactor table columns states and utils (#14048)

This PR refactors table columns state, hooks and utils, it removes them
and replaces them by record fields.
This commit is contained in:
Lucas Bordeau
2025-08-26 13:17:53 +02:00
committed by GitHub
parent 048455bbb6
commit 8cb4058d2d
76 changed files with 1403 additions and 1465 deletions
+16 -5
View File
@@ -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,
});
@@ -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,
};
},
});
@@ -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;
},
});
@@ -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,
};
},
});
@@ -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;
},
});
@@ -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;
},
});
@@ -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 = ({
<>
<DropdownMenuSectionLabel label={t`Visible fields`} />
<DropdownMenuItemsContainer>
{visibleColumnsFieldMetadataItems.map(
{visibleFieldMetadataItems.map(
(visibleFieldMetadataItem, index) => (
<SelectableListItem
itemId={visibleFieldMetadataItem.id}
@@ -174,7 +178,7 @@ export const AdvancedFilterFieldSelectMenu = ({
{shouldShowSeparator && <DropdownMenuSeparator />}
{shouldShowHiddenFields && (
<>
{visibleColumnsFieldMetadataItems.length > 0 && (
{visibleFieldMetadataItems.length > 0 && (
<DropdownMenuSectionLabel label={t`Hidden fields`} />
)}
<DropdownMenuItemsContainer>
@@ -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 (
<HookMockWrapper>
<ObjectNamePluralSetter>
<ViewComponentInstanceContext.Provider
value={{ instanceId: 'instanceId' }}
>
<RecordTableComponentInstance recordTableId={recordTableId}>
<RecordGroupContext.Provider value={{ recordGroupId: 'default' }}>
{children}
</RecordGroupContext.Provider>
</RecordTableComponentInstance>
</ViewComponentInstanceContext.Provider>
</ObjectNamePluralSetter>
<RecordTableContextProvider
objectNameSingular={objectNameSingular}
recordTableId={recordTableId}
viewBarId="instanceId"
>
<ObjectNamePluralSetter>
<ViewComponentInstanceContext.Provider
value={{ instanceId: 'instanceId' }}
>
<RecordComponentInstanceContextsWrapper
componentInstanceId={recordTableId}
>
<RecordTableComponentInstance recordTableId={recordTableId}>
<RecordGroupContext.Provider
value={{ recordGroupId: 'default' }}
>
{children}
</RecordGroupContext.Provider>
</RecordTableComponentInstance>
</RecordComponentInstanceContextsWrapper>
</ViewComponentInstanceContext.Provider>
</ObjectNamePluralSetter>
</RecordTableContextProvider>
</HookMockWrapper>
);
};
@@ -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 {
@@ -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,
};
};
@@ -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,
};
};
@@ -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 = () => {
<>
<DropdownMenuSectionLabel label={t`Hidden fields`} />
<DropdownMenuItemsContainer>
{hiddenFieldMetadataItems.map(
{hiddenFieldMetadataItemsSorted.map(
(hiddenFieldMetadataItem, index) => (
<SelectableListItem
key={hiddenFieldMetadataItem.id}
@@ -0,0 +1,77 @@
import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField';
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 { 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';
export const useChangeRecordFieldVisibility = (
recordFieldComponentInstanceId?: string,
) => {
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,
};
};
@@ -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<any>,
'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,
};
};
@@ -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 };
@@ -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,
);
@@ -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],
@@ -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<ColumnDefinition<any>, '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,
};
};
@@ -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'),
);
},
});
@@ -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'),
);
},
});
@@ -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(
@@ -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', () => {
@@ -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]
: []),
@@ -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,
@@ -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.`,
);
}
@@ -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 (
<RecordTableContextInternalProvider
value={{
viewBarId,
objectMetadataItem,
visibleTableColumns,
recordTableId,
objectNameSingular,
objectPermissions,
recordFieldByFieldMetadataItemId,
labelIdentifierFieldMetadataItem,
visibleRecordFields,
fieldMetadataItemByFieldMetadataItemId,
fieldDefinitionByFieldMetadataItemId,
}}
>
{children}
@@ -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 (
<RecordIndexContextProvider
value={{
@@ -58,86 +139,100 @@ const meta: Meta = {
onIndexRecordsLoaded: () => {},
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',
}}
>
<RecordTableContextProvider
value={{
recordTableId: 'recordTableId',
viewBarId: mockPerformance.recordId,
objectMetadataItem: mockPerformance.objectMetadataItem as any,
visibleTableColumns: mockPerformance.visibleTableColumns as any,
objectNameSingular:
mockPerformance.objectMetadataItem.nameSingular,
objectPermissions: {
objectMetadataId: mockPerformance.objectMetadataItem.id,
},
}}
>
<RecordTableComponentInstance recordTableId="asd">
<RecordTableBodyContextProvider
value={{
onOpenTableCell: () => {},
onMoveFocus: () => {},
onCloseTableCell: () => {},
onMoveHoverToCurrentCell: () => {},
onActionMenuDropdownOpened: () => {},
onCellMouseEnter: () => {},
}}
>
<RecordTableRowContextProvider
<RecordComponentInstanceContextsWrapper componentInstanceId="recordTableId">
<RecordTableContextProvider
value={{
recordTableId: 'recordTableId',
viewBarId: mockPerformance.recordId,
// TODO: update performance mocks with new data, and merge with common mocks if possible
objectMetadataItem: mockPerformance.objectMetadataItem as any,
objectNameSingular:
mockPerformance.objectMetadataItem.nameSingular,
objectPermissions: {
objectMetadataId: mockPerformance.objectMetadataItem.id,
},
fieldDefinitionByFieldMetadataItemId,
fieldMetadataItemByFieldMetadataItemId,
labelIdentifierFieldMetadataItem,
recordFieldByFieldMetadataItemId,
visibleRecordFields,
}}
>
<RecordTableComponentInstance recordTableId={'recordTableId'}>
<RecordTableBodyContextProvider
value={{
objectNameSingular:
mockPerformance.entityValue.__typename.toLocaleLowerCase(),
recordId: mockPerformance.recordId,
rowIndex: 0,
pathToShowPage:
getBasePathToShowPage({
objectNameSingular:
mockPerformance.entityValue.__typename.toLocaleLowerCase(),
}) + mockPerformance.recordId,
isSelected: false,
inView: true,
onOpenTableCell: () => {},
onMoveFocus: () => {},
onCloseTableCell: () => {},
onMoveHoverToCurrentCell: () => {},
onActionMenuDropdownOpened: () => {},
onCellMouseEnter: () => {},
}}
>
<RecordTableRowDraggableContextProvider
<RecordTableRowContextProvider
value={{
isDragging: false,
dragHandleProps: null,
objectNameSingular:
mockPerformance.entityValue.__typename.toLocaleLowerCase(),
recordId: mockPerformance.recordId,
rowIndex: 0,
pathToShowPage:
getBasePathToShowPage({
objectNameSingular:
mockPerformance.entityValue.__typename.toLocaleLowerCase(),
}) + mockPerformance.recordId,
isSelected: false,
inView: true,
}}
>
<RecordTableCellContext.Provider
<RecordTableRowDraggableContextProvider
value={{
columnDefinition: mockPerformance.fieldDefinition,
cellPosition: { row: 0, column: 0 },
isDragging: false,
dragHandleProps: null,
}}
>
<FieldContext.Provider
<RecordTableCellContext.Provider
value={{
recordId: mockPerformance.recordId,
isLabelIdentifier: false,
fieldDefinition: {
...mockPerformance.fieldDefinition,
cellPosition: { row: 0, column: 0 },
recordField: {
fieldMetadataItemId:
mockPerformance.fieldDefinition.fieldMetadataId,
id: 'test',
isVisible:
mockPerformance.fieldDefinition.isVisible,
position: mockPerformance.fieldDefinition.position,
size: mockPerformance.fieldDefinition.size,
},
isRecordFieldReadOnly: false,
}}
>
<RelationFieldValueSetterEffect />
<table>
<tbody>
<tr>
<Story />
</tr>
</tbody>
</table>
</FieldContext.Provider>
</RecordTableCellContext.Provider>
</RecordTableRowDraggableContextProvider>
</RecordTableRowContextProvider>
</RecordTableBodyContextProvider>
</RecordTableComponentInstance>
</RecordTableContextProvider>
<FieldContext.Provider
value={{
recordId: mockPerformance.recordId,
isLabelIdentifier: false,
fieldDefinition: mockPerformance.fieldDefinition,
isRecordFieldReadOnly: false,
}}
>
<RelationFieldValueSetterEffect />
<table>
<tbody>
<tr>
<Story />
</tr>
</tbody>
</table>
</FieldContext.Provider>
</RecordTableCellContext.Provider>
</RecordTableRowDraggableContextProvider>
</RecordTableRowContextProvider>
</RecordTableBodyContextProvider>
</RecordTableComponentInstance>
</RecordTableContextProvider>
</RecordComponentInstanceContextsWrapper>
</RecordIndexContextProvider>
);
},
@@ -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 },
},
@@ -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,
@@ -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<FieldMetadata>;
recordField: RecordField;
cellPosition: TableCellPosition;
};
@@ -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<FieldMetadata>[];
objectPermissions: ObjectPermission;
visibleRecordFields: RecordField[];
recordFieldByFieldMetadataItemId: Record<string, RecordField>;
labelIdentifierFieldMetadataItem: FieldMetadataItem | undefined;
fieldMetadataItemByFieldMetadataItemId: Record<string, FieldMetadataItem>;
fieldDefinitionByFieldMetadataItemId: Record<
string,
ColumnDefinition<FieldMetadata>
>;
};
export const [RecordTableContextProvider, useRecordTableContextOrThrow] =
@@ -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<FieldMetadata>[];
recordTableId: string;
objectMetadataId: string;
}) => {
setTableColumns(columns, recordTableId, objectMetadataId);
};
return {
handleColumnsChange,
};
};
@@ -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,
};
};
@@ -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) => {
@@ -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<FieldMetadata>[],
),
);
},
[saveViewFields],
);
return { saveColumnsToView };
};
@@ -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<FieldMetadata>[]) => {
const availableTableColumns = getSnapshotValue(
snapshot,
availableTableColumnsState,
);
if (isDeeplyEqual(availableTableColumns, columns)) {
return;
}
set(availableTableColumnsState, columns);
},
[availableTableColumnsState],
);
return {
setAvailableTableColumns,
};
};
@@ -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<FieldMetadata>[],
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 };
};
@@ -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<FieldMetadata>,
'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<FieldMetadata>,
) => {
unfocusRecordTableCell();
moveRecordField({
direction: direction === 'left' ? 'before' : 'after',
fieldMetadataItemIdToMove: column.fieldMetadataId,
});
},
[unfocusRecordTableCell, moveRecordField],
);
return {
handleColumnVisibilityChange,
handleMoveTableColumn,
handleColumnsChange,
};
};
@@ -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 (
<tbody>
@@ -41,8 +38,8 @@ export const RecordTableBodyLoading = () => {
>
<RecordTableCellGrip />
<RecordTableCellCheckbox />
{visibleTableColumns.map((column) => (
<RecordTableCellLoading key={column.fieldMetadataId} />
{visibleRecordFields.map((recordField) => (
<RecordTableCellLoading key={recordField.fieldMetadataItemId} />
))}
</RecordTableTr>
</RecordTableRowDraggableContextProvider>
@@ -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 = ({
<FieldContext.Provider
value={{
recordId,
fieldDefinition: columnDefinition,
fieldDefinition: fieldDefinition,
useUpdateRecord: () => [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,
@@ -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 (
<FieldContext.Provider
value={{
recordId,
fieldDefinition: columnDefinition,
fieldDefinition,
useUpdateRecord: () => [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();
@@ -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}
</RecordTableCellFieldContextLabelIdentifier>
) : (
<RecordTableCellFieldContextGeneric key={instanceId}>
<RecordTableCellFieldContextGeneric
key={instanceId}
recordField={recordField}
>
{children}
</RecordTableCellFieldContextGeneric>
)}
@@ -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 = ({
>
<RecordTableCellContext.Provider
value={{
columnDefinition: visibleTableColumns[position.column],
recordField: visibleRecordFields[position.column],
cellPosition: position,
}}
>
<RecordTableCellFieldContextWrapper>
<RecordTableCellFieldContextWrapper
recordField={visibleRecordFields[position.column]}
>
{children}
</RecordTableCellFieldContextWrapper>
</RecordTableCellContext.Provider>
@@ -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<FieldMetadata>;
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 (
<RecordTableCellContext.Provider
value={{
columnDefinition: column,
recordField,
cellPosition: currentTableCellPosition,
}}
key={column.fieldMetadataId}
key={recordField.fieldMetadataItemId}
>
<RecordTableCellFieldContextWrapper>
<RecordTableCellFieldContextWrapper recordField={recordField}>
{children}
</RecordTableCellFieldContextWrapper>
</RecordTableCellContext.Provider>
@@ -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,
@@ -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);
}}
>
<RecordTableComponentInstance recordTableId={recordTableId}>
<RecordTableContextProvider
recordTableId={recordTableId}
viewBarId="viewBarId"
objectNameSingular={CoreObjectNameSingular.Person}
>
<FieldContext.Provider
value={{
fieldDefinition: textfieldDefinition,
recordId: 'recordId',
isLabelIdentifier: false,
isRecordFieldReadOnly: false,
}}
<RecordComponentInstanceContextsWrapper componentInstanceId={recordTableId}>
<RecordTableComponentInstance recordTableId={recordTableId}>
<RecordTableContextProvider
recordTableId={recordTableId}
viewBarId="viewBarId"
objectNameSingular={CoreObjectNameSingular.Person}
>
<RecordTableRowContextProvider value={recordTableRowContextValue}>
<RecordTableRowDraggableContextProvider
value={recordTableRowDraggableContextValue}
>
<RecordTableCellContext.Provider
value={{ ...recordTableCellContextValue }}
<FieldContext.Provider
value={{
fieldDefinition: textfieldDefinition,
recordId: 'recordId',
isLabelIdentifier: false,
isRecordFieldReadOnly: false,
}}
>
<RecordTableRowContextProvider value={recordTableRowContextValue}>
<RecordTableRowDraggableContextProvider
value={recordTableRowDraggableContextValue}
>
{children}
</RecordTableCellContext.Provider>
</RecordTableRowDraggableContextProvider>
</RecordTableRowContextProvider>
</FieldContext.Provider>
</RecordTableContextProvider>
</RecordTableComponentInstance>
<RecordTableCellContext.Provider
value={{ ...recordTableCellContextValue }}
>
{children}
</RecordTableCellContext.Provider>
</RecordTableRowDraggableContextProvider>
</RecordTableRowContextProvider>
</FieldContext.Provider>
</RecordTableContextProvider>
</RecordTableComponentInstance>
</RecordComponentInstanceContextsWrapper>
</RecoilRoot>
);
@@ -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}
>
<FieldContext.Provider
value={{
fieldDefinition: textfieldDefinition,
recordId: 'recordId',
isLabelIdentifier: false,
isRecordFieldReadOnly: false,
}}
<RecordComponentInstanceContextsWrapper
componentInstanceId={recordTableId}
>
<RecordTableRowContextProvider value={recordTableRowContextValue}>
<RecordTableRowDraggableContextProvider
value={recordTableRowDraggableContextValue}
>
<RecordTableCellContext.Provider
value={{ ...recordTableCellContextValue }}
<FieldContext.Provider
value={{
fieldDefinition: textfieldDefinition,
recordId: 'recordId',
isLabelIdentifier: false,
isRecordFieldReadOnly: false,
}}
>
<RecordTableRowContextProvider value={recordTableRowContextValue}>
<RecordTableRowDraggableContextProvider
value={recordTableRowDraggableContextValue}
>
{children}
</RecordTableCellContext.Provider>
</RecordTableRowDraggableContextProvider>
</RecordTableRowContextProvider>
</FieldContext.Provider>
<RecordTableCellContext.Provider
value={{ ...recordTableCellContextValue }}
>
{children}
</RecordTableCellContext.Provider>
</RecordTableRowDraggableContextProvider>
</RecordTableRowContextProvider>
</FieldContext.Provider>
</RecordComponentInstanceContextsWrapper>
</RecordTableContextProvider>
</RecordTableComponentInstance>
</RecoilRoot>
@@ -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 = ({
}
>
<StyledTd />
{visibleTableColumns.map((column, index) => {
{visibleRecordFields.map((recordField, index) => {
return (
<RecordTableColumnAggregateFooterCellContext.Provider
key={`${column.fieldMetadataId}${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`}
key={`${recordField.fieldMetadataItemId}${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`}
value={{
viewFieldId: column.viewFieldId || '',
fieldMetadataId: column.fieldMetadataId,
viewFieldId: recordField.id || '',
fieldMetadataId: recordField.fieldMetadataItemId,
}}
>
<RecordTableAggregateFooterCell
@@ -121,7 +118,7 @@ export const RecordTableAggregateFooter = ({
</RecordTableColumnAggregateFooterCellContext.Provider>
);
})}
<td colSpan={visibleTableColumns.length - 1} />
<td colSpan={visibleRecordFields.length - 1} />
<td />
<td />
</StyledTableRow>
@@ -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 (
<StyledColumnFooterCell
columnWidth={Math.max(
tableColumnsByKey[fieldMetadataId].size + 24,
COLUMN_MIN_WIDTH,
)}
columnWidth={Math.max(recordField.size + 24, COLUMN_MIN_WIDTH)}
colSpan={isFirstCell ? 2 : undefined}
isFirstCell={isFirstCell}
>
@@ -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<FieldMetadata>;
};
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 (
<StyledTitle
hideTitle={!!column.isLabelIdentifier && !isRecordTableScrolledLeft}
>
<StyledTitle hideTitle={isLabelIdentifier && !isRecordTableScrolledLeft}>
<StyledIcon>
<Icon size={theme.icon.size.md} />
</StyledIcon>
<StyledText>{column.label}</StyledText>
<StyledText>
{correspondingFieldMetadataItem.foundFieldMetadataItem?.label}
</StyledText>
</StyledTitle>
);
};
@@ -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<FieldMetadata>;
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 (
<DropdownContent>
@@ -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<FieldMetadata>;
recordField: RecordField;
objectMetadataId: string;
};
export const RecordTableColumnHeadWithDropdown = ({
objectMetadataId,
column,
recordField,
}: RecordTableColumnHeadWithDropdownProps) => {
const { toggleScrollXWrapper, toggleScrollYWrapper } =
useToggleScrollWrapper();
@@ -32,11 +31,11 @@ export const RecordTableColumnHeadWithDropdown = ({
<Dropdown
onOpen={handleDropdownOpen}
onClose={handleDropdownClose}
dropdownId={column.fieldMetadataId + '-header'}
clickableComponent={<RecordTableColumnHead column={column} />}
dropdownId={recordField.fieldMetadataItemId + '-header'}
clickableComponent={<RecordTableColumnHead recordField={recordField} />}
dropdownComponents={
<RecordTableColumnHeadDropdownMenu
column={column}
recordField={recordField}
objectMetadataId={objectMetadataId}
/>
}
@@ -80,15 +80,18 @@ const StyledTableHead = styled.thead`
`;
export const RecordTableHeader = () => {
const { visibleTableColumns } = useRecordTableContextOrThrow();
const { visibleRecordFields } = useRecordTableContextOrThrow();
return (
<StyledTableHead id="record-table-header" data-select-disable>
<tr>
<RecordTableHeaderDragDropColumn />
<RecordTableHeaderCheckboxColumn />
{visibleTableColumns.map((column) => (
<RecordTableHeaderCell key={column.fieldMetadataId} column={column} />
{visibleRecordFields.map((recordField) => (
<RecordTableHeaderCell
key={recordField.fieldMetadataItemId}
recordField={recordField}
/>
))}
<RecordTableHeaderLastColumn />
</tr>
@@ -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<FieldMetadata>;
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 (
<StyledColumnHeaderCell
key={column.fieldMetadataId}
isResizing={resizedFieldMetadataItemId === column.fieldMetadataId}
key={recordField.fieldMetadataItemId}
isResizing={
resizedFieldMetadataItemId === recordField.fieldMetadataItemId
}
columnWidth={Math.max(
tableColumnsByKey[column.fieldMetadataId].size +
(resizedFieldMetadataItemId === column.fieldMetadataId
(recordField?.size ?? 0) +
(resizedFieldMetadataItemId === recordField.fieldMetadataItemId
? resizeFieldOffset
: 0) +
24,
@@ -277,11 +262,11 @@ export const RecordTableHeaderCell = ({
>
<StyledColumnHeadContainer>
<RecordTableColumnHeadWithDropdown
column={column}
recordField={recordField}
objectMetadataId={objectMetadataItem.id}
/>
{(useIsMobile() || iconVisibility) &&
!!column.isLabelIdentifier &&
!!isLabelIdentifier &&
!isReadOnly &&
hasObjectUpdatePermissions && (
<StyledHeaderIcon>
@@ -299,7 +284,7 @@ export const RecordTableHeaderCell = ({
className="cursor-col-resize"
role="separator"
onPointerDown={() => {
setResizedFieldMetadataItemId(column.fieldMetadataId);
setResizedFieldMetadataItemId(recordField.fieldMetadataItemId);
}}
/>
)}
@@ -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<FieldMetadata>) => {
(column: Pick<ColumnDefinition<FieldMetadata>, '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 (
<DropdownContent>
<DropdownMenuItemsContainer>
{hiddenTableColumns.map((column) => (
{availableFieldMetadataItemsToShow.map((fieldMetadataItem) => (
<MenuItem
key={column.fieldMetadataId}
onClick={() => handleAddColumn(column)}
LeftIcon={getIcon(column.iconName)}
text={column.label}
key={fieldMetadataItem.id}
onClick={() =>
handleAddColumn({
fieldMetadataId: fieldMetadataItem.id,
})
}
LeftIcon={getIcon(fieldMetadataItem.icon)}
text={fieldMetadataItem.label}
/>
))}
</DropdownMenuItemsContainer>
@@ -70,7 +70,7 @@ export const RecordTableActionRow = ({
}: RecordTableActionRowProps) => {
const theme = useTheme();
const { visibleTableColumns } = useRecordTableContextOrThrow();
const { visibleRecordFields } = useRecordTableContextOrThrow();
return (
<StyledRecordTableDraggableTr onClick={onClick}>
@@ -85,7 +85,7 @@ export const RecordTableActionRow = ({
<StyledRecordTableTdTextContainer className="disable-shadow">
<StyledText>{text}</StyledText>
</StyledRecordTableTdTextContainer>
<td colSpan={visibleTableColumns.length - 1} aria-hidden />
<td colSpan={visibleRecordFields.length - 1} aria-hidden />
<td aria-hidden />
<td aria-hidden />
</StyledRecordTableDraggableTr>
@@ -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) => (
<RecordTableTd isSelected={isSelected} key={column.fieldMetadataId} />
return visibleRecordFields.map((recordField) => (
<RecordTableTd
isSelected={isSelected}
key={recordField.fieldMetadataItemId}
/>
));
};
@@ -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 (
<>
<RecordTableCellWrapper column={visibleTableColumns[0]} columnIndex={0}>
<RecordTableCellWrapper
recordField={visibleRecordFields[0]}
recordFieldIndex={0}
>
<RecordTableTd
isSelected={isSelected}
isDragging={isDragging}
width={visibleTableColumns[0].size}
width={visibleRecordFields[0].size}
>
<RecordTableCell />
</RecordTableTd>
</RecordTableCellWrapper>
{tableColumnsAfterFirst.map((column, columnIndex) => (
{recordFieldsAfterFirst.map((recordField, recordFieldIndex) => (
<RecordTableCellWrapper
key={column.fieldMetadataId}
column={column}
columnIndex={columnIndex + 1}
key={recordField.fieldMetadataItemId}
recordField={recordField}
recordFieldIndex={recordFieldIndex + 1}
>
<RecordTableTd
isSelected={isSelected}
isDragging={isDragging}
width={column.size}
width={recordField.size}
>
<RecordTableCell />
</RecordTableTd>
@@ -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 = () => {
/>
<RecordTableRecordGroupStickyEffect />
</StyledRecordGroupSection>
<StyledEmptyTd colSpan={visibleColumns.length - 1} />
<StyledEmptyTd colSpan={visibleRecordFields.length - 1} />
<StyledEmptyTd />
<StyledEmptyTd />
</StyledTrContainer>
@@ -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<FieldMetadata>[]
>({
key: 'availableTableColumnsComponentState',
defaultValue: [],
componentInstanceContext: RecordTableComponentInstanceContext,
});
@@ -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;
},
});
@@ -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,
});
@@ -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);
},
});
@@ -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<FieldMetadata>[]
>({
key: 'tableColumnsComponentState',
defaultValue: [],
componentInstanceContext: RecordTableComponentInstanceContext,
});
@@ -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 <></>;
@@ -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<FieldMetadata>[]
).filter(filterAvailableTableColumns);
@@ -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 },
);
@@ -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,
@@ -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(
@@ -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<typeof ViewBarFilterDropdown> = {
)!;
const instanceId = companyObjectMetadataItem.id;
const setTableColumns = useSetRecoilComponentState(
tableColumnsComponentState,
const setCurrentRecordFields = useSetRecoilComponentState(
currentRecordFieldsComponentState,
instanceId,
);
@@ -68,14 +68,16 @@ const meta: Meta<typeof ViewBarFilterDropdown> = {
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 (
<RecordIndexContextProvider
@@ -0,0 +1,15 @@
import { type RecordField } from '@/object-record/record-field/types/RecordField';
import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields';
import { mapRecordFieldToViewField } from '@/views/utils/mapRecordFieldToViewField';
export const useSaveRecordFields = () => {
const { saveViewFields } = useSaveCurrentViewFields();
const saveRecordFields = (recordFields: RecordField[]) => {
saveViewFields(recordFields.map(mapRecordFieldToViewField));
};
return {
saveRecordFields,
};
};
@@ -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 (
<RecordIndexContextProvider
@@ -90,11 +151,15 @@ const InternalTableContextProviders = ({
objectMetadataItem: objectMetadataItem,
recordTableId: objectMetadataItem.namePlural,
viewBarId: 'view-bar',
visibleTableColumns: visibleTableColumns,
objectPermissions: getObjectPermissionsFromMapByObjectMetadataId({
objectPermissionsByObjectMetadataId,
objectMetadataId: objectMetadataItem.id,
}),
labelIdentifierFieldMetadataItem,
recordFieldByFieldMetadataItemId,
visibleRecordFields,
fieldDefinitionByFieldMetadataItemId,
fieldMetadataItemByFieldMetadataItemId,
}}
>
<RecordTableBodyContextProvider
@@ -7,7 +7,7 @@ import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { mockedApiKeys } from '~/testing/mock-data/api-keys';
import {
getCompaniesMock,
getCompaniesRecordConnectionMock,
getCompanyDuplicateMock,
} from '~/testing/mock-data/companies';
import { mockedClientConfig } from '~/testing/mock-data/config';
@@ -37,7 +37,7 @@ import { mockedRemoteServers } from './mock-data/remote-servers';
import { mockedViewFieldsData } from './mock-data/view-fields';
const peopleMock = getPeopleRecordConnectionMock();
const companiesMock = getCompaniesMock();
const companiesMock = getCompaniesRecordConnectionMock();
const duplicateCompanyMock = getCompanyDuplicateMock();
export const metadataGraphql = graphql.link(
@@ -1,3 +1,5 @@
import { type Company } from '@/companies/types/Company';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
@@ -732,11 +734,20 @@ export const companiesQueryResult = {
},
};
const allMockedCompanyRecords = companiesQueryResult.companies.edges.map(
(edge) => 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',
};
};
@@ -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);
@@ -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';
@@ -0,0 +1,8 @@
export const throwIfNotDefined = <T>(
value: T,
variableName: string
): asserts value is NonNullable<T> => {
if (value === null || value === undefined) {
throw new Error(`Value must be defined for variable ${variableName}, this should not happen`);
}
}