Refactor field definition and column definition manipulation with record field (#13992)
This PR introduces the usage of record fields in the manipulation of table columns and board fields. Since all of the actual system relies on states like tableColumns, recordIndexFieldDefinitions and the likes, it was required to implement temporary utils that modifies those states in parallel of the new generic currentRecordFields, to keep a working product. With this PR though, currentRecordFields becomes the single source of truth that gets saved to DB, and the remaining work is just to make the switch with this new state on all components that are plugged to tableColumns and the like. This will be done in another PR.
This commit is contained in:
-119
@@ -1,119 +0,0 @@
|
||||
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
|
||||
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
||||
import { type DropResult, type ResponderProvided } from '@hello-pangea/dnd';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
|
||||
|
||||
jest.mock('@/views/hooks/useSaveCurrentViewFields', () => ({
|
||||
useSaveCurrentViewFields: jest.fn(() => ({
|
||||
saveViewFields: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('@/views/hooks/useUpdateCurrentView', () => ({
|
||||
useUpdateCurrentView: jest.fn(() => ({
|
||||
updateCurrentView: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
const objectNameSingular = 'company';
|
||||
|
||||
describe('useObjectOptionsForBoard', () => {
|
||||
const mockObjectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.nameSingular === objectNameSingular,
|
||||
);
|
||||
|
||||
if (!mockObjectMetadataItem) {
|
||||
throw new Error('Mock object metadata item not found');
|
||||
}
|
||||
|
||||
const mockFieldMetadataItem1 = mockObjectMetadataItem.fields.find(
|
||||
(field) => field.name === 'name',
|
||||
);
|
||||
|
||||
if (!mockFieldMetadataItem1) {
|
||||
throw new Error('Mock field metadata item not found for "name"');
|
||||
}
|
||||
|
||||
const mockFieldMetadataItem2 = mockObjectMetadataItem.fields.find(
|
||||
(field) => field.name === 'createdAt',
|
||||
);
|
||||
|
||||
if (!mockFieldMetadataItem2) {
|
||||
throw new Error('Mock field metadata item not found for "createdAt"');
|
||||
}
|
||||
|
||||
const initialRecoilState = [
|
||||
{
|
||||
fieldMetadataId: mockFieldMetadataItem1.id,
|
||||
isVisible: true,
|
||||
position: 0,
|
||||
},
|
||||
{
|
||||
fieldMetadataId: mockFieldMetadataItem2.id,
|
||||
isVisible: true,
|
||||
position: 1,
|
||||
},
|
||||
];
|
||||
|
||||
const renderWithRecoil = () =>
|
||||
renderHook(
|
||||
() =>
|
||||
useObjectOptionsForBoard({
|
||||
objectNameSingular,
|
||||
recordBoardId: 'boardId',
|
||||
viewBarId: 'viewBarId',
|
||||
}),
|
||||
{
|
||||
wrapper: getJestMetadataAndApolloMocksAndActionMenuWrapper({
|
||||
apolloMocks: [],
|
||||
onInitializeRecoilSnapshot: (snapshot) => {
|
||||
snapshot.set(
|
||||
recordIndexFieldDefinitionsState,
|
||||
initialRecoilState as any,
|
||||
);
|
||||
},
|
||||
componentInstanceId: 'test',
|
||||
contextStoreCurrentObjectMetadataNameSingular: objectNameSingular,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
it('reorders fields correctly', () => {
|
||||
const { result } = renderWithRecoil();
|
||||
|
||||
const dropResult: DropResult = {
|
||||
source: { droppableId: 'droppable', index: 1 },
|
||||
destination: { droppableId: 'droppable', index: 2 },
|
||||
draggableId: mockFieldMetadataItem1.id,
|
||||
type: 'TYPE',
|
||||
mode: 'FLUID',
|
||||
reason: 'DROP',
|
||||
combine: null,
|
||||
};
|
||||
|
||||
const responderProvided: ResponderProvided = {
|
||||
announce: jest.fn(),
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.handleReorderBoardFields(dropResult, responderProvided);
|
||||
});
|
||||
|
||||
expect(result.current.visibleBoardFields).toEqual([
|
||||
{
|
||||
fieldMetadataId: mockFieldMetadataItem2.id,
|
||||
isVisible: true,
|
||||
position: 0,
|
||||
},
|
||||
{
|
||||
fieldMetadataId: mockFieldMetadataItem1.id,
|
||||
isVisible: true,
|
||||
position: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
+122
-50
@@ -5,17 +5,25 @@ import { useRecoilState } from 'recoil';
|
||||
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
|
||||
import { useReorderVisibleRecordFields } from '@/object-record/record-field/hooks/useReorderVisibleRecordFields';
|
||||
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 { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
||||
import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields';
|
||||
import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
|
||||
import { type GraphQLView } from '@/views/types/GraphQLView';
|
||||
import { mapBoardFieldDefinitionsToViewFields } from '@/views/utils/mapBoardFieldDefinitionsToViewFields';
|
||||
import { mapRecordFieldToViewField } from '@/views/utils/mapRecordFieldToViewField';
|
||||
import { produce } from 'immer';
|
||||
import { findByProperty, isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
|
||||
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import { sortByProperty } from '~/utils/array/sortByProperty';
|
||||
|
||||
type useObjectOptionsForBoardParams = {
|
||||
objectNameSingular: string;
|
||||
@@ -71,6 +79,9 @@ export const useObjectOptionsForBoard = ({
|
||||
[recordIndexFieldDefinitions],
|
||||
);
|
||||
|
||||
const { reorderVisibleRecordFields } =
|
||||
useReorderVisibleRecordFields(recordBoardId);
|
||||
|
||||
const hiddenBoardFields = useMemo(
|
||||
() =>
|
||||
availableColumnDefinitions
|
||||
@@ -97,24 +108,46 @@ export const useObjectOptionsForBoard = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const reorderedVisibleBoardFields = moveArrayItem(visibleBoardFields, {
|
||||
const updatedRecordField = reorderVisibleRecordFields({
|
||||
fromIndex: result.source.index - 1,
|
||||
toIndex: result.destination.index - 1,
|
||||
});
|
||||
|
||||
if (isDeeplyEqual(visibleBoardFields, reorderedVisibleBoardFields))
|
||||
return;
|
||||
saveViewFields([mapRecordFieldToViewField(updatedRecordField)]);
|
||||
|
||||
const updatedFields = [...reorderedVisibleBoardFields].map(
|
||||
(field, index) => ({ ...field, position: index }),
|
||||
const modifiedRecordIndexFieldDefinitions = produce(
|
||||
recordIndexFieldDefinitions,
|
||||
(draftRecordIndexFieldDefinitions) => {
|
||||
const indexToModify = draftRecordIndexFieldDefinitions.findIndex(
|
||||
(recordIndexFieldDefinitionToModify) =>
|
||||
recordIndexFieldDefinitionToModify.fieldMetadataId ===
|
||||
updatedRecordField.fieldMetadataItemId,
|
||||
);
|
||||
|
||||
draftRecordIndexFieldDefinitions[indexToModify].position =
|
||||
updatedRecordField.position;
|
||||
},
|
||||
);
|
||||
|
||||
setRecordIndexFieldDefinitions(updatedFields);
|
||||
saveViewFields(mapBoardFieldDefinitionsToViewFields(updatedFields));
|
||||
// TODO: remove after refactor
|
||||
setRecordIndexFieldDefinitions(modifiedRecordIndexFieldDefinitions);
|
||||
},
|
||||
[saveViewFields, setRecordIndexFieldDefinitions, visibleBoardFields],
|
||||
[
|
||||
saveViewFields,
|
||||
setRecordIndexFieldDefinitions,
|
||||
recordIndexFieldDefinitions,
|
||||
reorderVisibleRecordFields,
|
||||
],
|
||||
);
|
||||
|
||||
const currentRecordFields = useRecoilComponentValue(
|
||||
currentRecordFieldsComponentState,
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
const { updateRecordField } = useUpdateRecordField(recordBoardId);
|
||||
const { upsertRecordField } = useUpsertRecordField(recordBoardId);
|
||||
|
||||
// Todo : this seems over complex and should at least be extracted to an util with unit test.
|
||||
// Let's refactor this as we introduce the new viewBar
|
||||
const handleBoardFieldVisibilityChange = useCallback(
|
||||
@@ -124,59 +157,98 @@ export const useObjectOptionsForBoard = ({
|
||||
'fieldMetadataId' | 'isVisible'
|
||||
>,
|
||||
) => {
|
||||
const isNewViewField = !(
|
||||
updatedFieldDefinition.fieldMetadataId in
|
||||
recordIndexFieldDefinitionsByKey
|
||||
const lastPosition = currentRecordFields.toSorted(
|
||||
sortByProperty('position', 'desc'),
|
||||
)[0].position;
|
||||
|
||||
const shouldShowFieldMetadataItem =
|
||||
updatedFieldDefinition.isVisible === true;
|
||||
const corresponingRecordField = currentRecordFields.find(
|
||||
(recordFieldToFind) =>
|
||||
recordFieldToFind.fieldMetadataItemId ===
|
||||
updatedFieldDefinition.fieldMetadataId,
|
||||
);
|
||||
|
||||
let updatedFieldsDefinitions: ColumnDefinition<FieldMetadata>[];
|
||||
const noExistingRecordField = !isDefined(corresponingRecordField);
|
||||
|
||||
if (isNewViewField) {
|
||||
const correspondingFieldDefinition = availableColumnDefinitions.find(
|
||||
(availableColumnDefinition) =>
|
||||
availableColumnDefinition.fieldMetadataId ===
|
||||
updatedFieldDefinition.fieldMetadataId,
|
||||
);
|
||||
if (noExistingRecordField) {
|
||||
const recordFieldToUpsert: RecordField = {
|
||||
id: v4(),
|
||||
fieldMetadataItemId: updatedFieldDefinition.fieldMetadataId,
|
||||
size: 100,
|
||||
isVisible: shouldShowFieldMetadataItem,
|
||||
position: lastPosition + 1,
|
||||
};
|
||||
|
||||
if (!correspondingFieldDefinition) return;
|
||||
upsertRecordField(recordFieldToUpsert);
|
||||
|
||||
const lastVisibleBoardField =
|
||||
visibleBoardFields[visibleBoardFields.length - 1];
|
||||
saveViewFields([mapRecordFieldToViewField(recordFieldToUpsert)]);
|
||||
|
||||
updatedFieldsDefinitions = [
|
||||
...recordIndexFieldDefinitions,
|
||||
{
|
||||
...correspondingFieldDefinition,
|
||||
position: (lastVisibleBoardField?.position || 0) + 1,
|
||||
isVisible: true,
|
||||
const correspondingAvailableColumnDefinition =
|
||||
availableColumnDefinitions.find(
|
||||
findByProperty(
|
||||
'fieldMetadataId',
|
||||
updatedFieldDefinition.fieldMetadataId,
|
||||
),
|
||||
);
|
||||
|
||||
const modifiedRecordIndexFieldDefinitions = produce(
|
||||
recordIndexFieldDefinitions,
|
||||
(draftRecordIndexFieldDefinitions) => {
|
||||
if (!isDefined(correspondingAvailableColumnDefinition)) {
|
||||
throw new Error(
|
||||
`correspondingAvailableColumnDefinition is not defined this should not happen.`,
|
||||
);
|
||||
}
|
||||
|
||||
draftRecordIndexFieldDefinitions.push({
|
||||
...correspondingAvailableColumnDefinition,
|
||||
fieldMetadataId: updatedFieldDefinition.fieldMetadataId,
|
||||
isVisible: shouldShowFieldMetadataItem,
|
||||
});
|
||||
},
|
||||
];
|
||||
} else {
|
||||
updatedFieldsDefinitions = recordIndexFieldDefinitions.map(
|
||||
(existingFieldDefinition) =>
|
||||
existingFieldDefinition.fieldMetadataId ===
|
||||
updatedFieldDefinition.fieldMetadataId
|
||||
? {
|
||||
...existingFieldDefinition,
|
||||
isVisible: !existingFieldDefinition.isVisible,
|
||||
}
|
||||
: existingFieldDefinition,
|
||||
);
|
||||
|
||||
// TODO: remove after refactor
|
||||
setRecordIndexFieldDefinitions(modifiedRecordIndexFieldDefinitions);
|
||||
} else {
|
||||
updateRecordField(updatedFieldDefinition.fieldMetadataId, {
|
||||
isVisible: shouldShowFieldMetadataItem,
|
||||
});
|
||||
|
||||
const updatedRecordField: RecordField = {
|
||||
...corresponingRecordField,
|
||||
isVisible: shouldShowFieldMetadataItem,
|
||||
};
|
||||
|
||||
saveViewFields([mapRecordFieldToViewField(updatedRecordField)]);
|
||||
|
||||
const modifiedRecordIndexFieldDefinitions = produce(
|
||||
recordIndexFieldDefinitions,
|
||||
(draftRecordIndexFieldDefinitions) => {
|
||||
const indexToModify = draftRecordIndexFieldDefinitions.findIndex(
|
||||
(recordIndexFieldDefinitionToModify) =>
|
||||
recordIndexFieldDefinitionToModify.fieldMetadataId ===
|
||||
updatedRecordField.fieldMetadataItemId,
|
||||
);
|
||||
|
||||
draftRecordIndexFieldDefinitions[indexToModify].isVisible =
|
||||
shouldShowFieldMetadataItem;
|
||||
},
|
||||
);
|
||||
|
||||
// TODO: remove after refactor
|
||||
setRecordIndexFieldDefinitions(modifiedRecordIndexFieldDefinitions);
|
||||
}
|
||||
|
||||
setRecordIndexFieldDefinitions(updatedFieldsDefinitions);
|
||||
|
||||
saveViewFields(
|
||||
mapBoardFieldDefinitionsToViewFields(updatedFieldsDefinitions),
|
||||
);
|
||||
},
|
||||
[
|
||||
recordIndexFieldDefinitionsByKey,
|
||||
currentRecordFields,
|
||||
updateRecordField,
|
||||
upsertRecordField,
|
||||
setRecordIndexFieldDefinitions,
|
||||
saveViewFields,
|
||||
availableColumnDefinitions,
|
||||
visibleBoardFields,
|
||||
recordIndexFieldDefinitions,
|
||||
availableColumnDefinitions,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
+74
-19
@@ -1,12 +1,19 @@
|
||||
import { type OnDragEndResponder } from '@hello-pangea/dnd';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useReorderRecordFields } from '@/object-record/record-field/hooks/useReorderRecordFields';
|
||||
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 { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
||||
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
||||
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,
|
||||
@@ -16,19 +23,37 @@ export const useObjectOptionsForTable = (
|
||||
hiddenTableColumnsComponentSelector,
|
||||
recordTableId,
|
||||
);
|
||||
const visibleTableColumns = useRecoilComponentValue(
|
||||
visibleTableColumnsComponentSelector,
|
||||
|
||||
const tableColumns = useRecoilComponentValue(
|
||||
tableColumnsComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const { handleColumnVisibilityChange, handleColumnReorder } = useTableColumns(
|
||||
{ recordTableId, objectMetadataId },
|
||||
const currentRecordFields = useRecoilComponentValue(
|
||||
currentRecordFieldsComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const { reorderRecordFields } = useReorderRecordFields();
|
||||
const { filterVisibleAndReadableRecordField } =
|
||||
useFilterVisibleAndReadableRecordField();
|
||||
|
||||
const handleReorderColumns: OnDragEndResponder = useCallback(
|
||||
async (result) => {
|
||||
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 ||
|
||||
@@ -37,25 +62,55 @@ export const useObjectOptionsForTable = (
|
||||
return;
|
||||
}
|
||||
|
||||
const reorderedFields = moveArrayItem(visibleTableColumns, {
|
||||
const updatedRecordField = reorderVisibleRecordFields({
|
||||
fromIndex: result.source.index - 1,
|
||||
toIndex: result.destination.index - 1,
|
||||
});
|
||||
|
||||
reorderRecordFields({
|
||||
fromIndex: result.source.index - 1,
|
||||
toIndex: result.destination.index - 1,
|
||||
});
|
||||
saveViewFields([mapRecordFieldToViewField(updatedRecordField)]);
|
||||
|
||||
handleColumnReorder(reorderedFields);
|
||||
// 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,
|
||||
);
|
||||
},
|
||||
[visibleTableColumns, handleColumnReorder, reorderRecordFields],
|
||||
[
|
||||
reorderVisibleRecordFields,
|
||||
saveViewFields,
|
||||
objectMetadataId,
|
||||
recordTableId,
|
||||
setTableColumns,
|
||||
tableColumns,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
handleReorderColumns,
|
||||
handleColumnVisibilityChange,
|
||||
visibleTableColumns,
|
||||
visibleTableColumns: visibleRecordFields,
|
||||
hiddenTableColumns,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user