Fix orphan views by deferring record table widget view creation to dashboard save (#20006)

This commit is contained in:
Abdul Rahman
2026-04-28 15:36:26 +05:30
committed by GitHub
parent a5cd64daf5
commit a710c105cf
35 changed files with 1130 additions and 183 deletions
@@ -1,6 +1,7 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { RecordTableWidget } from '@/object-record/record-table-widget/components/RecordTableWidget';
import { RecordTableWidgetProvider } from '@/object-record/record-table-widget/components/RecordTableWidgetProvider';
import { RecordTableWidgetViewDraftInitEffect } from '@/page-layout/widgets/record-table/components/RecordTableWidgetViewDraftInitEffect';
type RecordTableWidgetRendererContentProps = {
objectMetadataId: string;
@@ -18,12 +19,18 @@ export const RecordTableWidgetRendererContent = ({
});
return (
<RecordTableWidgetProvider
objectNameSingular={objectMetadataItem.nameSingular}
viewId={viewId}
widgetId={widgetId}
>
<RecordTableWidget />
</RecordTableWidgetProvider>
<>
<RecordTableWidgetViewDraftInitEffect
widgetId={widgetId}
viewId={viewId}
/>
<RecordTableWidgetProvider
objectNameSingular={objectMetadataItem.nameSingular}
viewId={viewId}
widgetId={widgetId}
>
<RecordTableWidget />
</RecordTableWidgetProvider>
</>
);
};
@@ -0,0 +1,23 @@
import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { useInitializeRecordTableWidgetViewDraft } from '@/page-layout/widgets/record-table/hooks/useInitializeRecordTableWidgetViewDraft';
import { useViewById } from '@/views/hooks/useViewById';
type RecordTableWidgetViewDraftInitEffectProps = {
widgetId: string;
viewId: string;
};
export const RecordTableWidgetViewDraftInitEffect = ({
widgetId,
viewId,
}: RecordTableWidgetViewDraftInitEffectProps) => {
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const { view } = useViewById(viewId);
useInitializeRecordTableWidgetViewDraft({
widgetId,
view: isPageLayoutInEditMode ? view : undefined,
});
return null;
};
@@ -0,0 +1,89 @@
import { type FlatView } from '@/metadata-store/types/FlatView';
import { type FlatViewField } from '@/metadata-store/types/FlatViewField';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
import { filterFieldsForRecordTableViewCreation } from '@/page-layout/widgets/record-table/utils/filterFieldsForRecordTableViewCreation';
import { sortFieldsByRelevanceForRecordTableWidget } from '@/page-layout/widgets/record-table/utils/sortFieldsByRelevanceForRecordTableWidget';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { v4 } from 'uuid';
import {
ViewOpenRecordIn,
ViewType,
ViewVisibility,
WidgetConfigurationType,
} from '~/generated-metadata/graphql';
const DEFAULT_VIEW_FIELD_SIZE = 180;
const INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET = 6;
export const useAddDraftViewForRecordTableWidget = (pageLayoutId: string) => {
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState(
recordTableWidgetViewDraftComponentState,
pageLayoutId,
);
const store = useStore();
const addDraftViewForRecordTableWidget = useCallback(
(widgetId: string, objectMetadataItem: EnrichedObjectMetadataItem) => {
const newViewId = v4();
const flatView: FlatView = {
id: newViewId,
name: `${objectMetadataItem.labelPlural} Table`,
icon: objectMetadataItem.icon ?? 'IconTable',
objectMetadataId: objectMetadataItem.id,
type: ViewType.TABLE_WIDGET,
isCompact: false,
position: 0,
openRecordIn: ViewOpenRecordIn.RECORD_PAGE,
visibility: ViewVisibility.UNLISTED,
shouldHideEmptyGroups: false,
};
const eligibleFields = objectMetadataItem.fields.filter(
filterFieldsForRecordTableViewCreation,
);
const sortedFields = eligibleFields.toSorted(
sortFieldsByRelevanceForRecordTableWidget(
objectMetadataItem.labelIdentifierFieldMetadataId,
),
);
const flatViewFields: FlatViewField[] = sortedFields.map(
(field, index) => ({
id: v4(),
viewId: newViewId,
fieldMetadataId: field.id,
position: index,
size: DEFAULT_VIEW_FIELD_SIZE,
isVisible: index < INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET,
isActive: true,
}),
);
store.set(recordTableWidgetViewDraftState, (prev) => ({
...prev,
[widgetId]: { view: flatView, viewFields: flatViewFields },
}));
requestAnimationFrame(() => {
updatePageLayoutWidget(widgetId, {
configuration: {
configurationType: WidgetConfigurationType.RECORD_TABLE,
viewId: newViewId,
},
});
});
},
[store, recordTableWidgetViewDraftState, updatePageLayoutWidget],
);
return { addDraftViewForRecordTableWidget };
};
@@ -1,85 +0,0 @@
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { filterFieldsForRecordTableViewCreation } from '@/page-layout/widgets/record-table/utils/filterFieldsForRecordTableViewCreation';
import { sortFieldsByRelevanceForRecordTableWidget } from '@/page-layout/widgets/record-table/utils/sortFieldsByRelevanceForRecordTableWidget';
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
import { useCallback } from 'react';
import { v4 } from 'uuid';
import {
WidgetConfigurationType,
ViewType,
} from '~/generated-metadata/graphql';
const DEFAULT_VIEW_FIELD_SIZE = 180;
const INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET = 6;
export const useCreateViewForRecordTableWidget = (pageLayoutId: string) => {
const { performViewAPICreate } = usePerformViewAPIPersist();
const { performViewFieldAPICreate } = usePerformViewFieldAPIPersist();
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
const createViewForRecordTableWidget = useCallback(
async (
widgetId: string,
objectMetadataItem: EnrichedObjectMetadataItem,
) => {
const newViewId = v4();
const viewResult = await performViewAPICreate(
{
input: {
id: newViewId,
name: `${objectMetadataItem.labelPlural} Table`,
icon: objectMetadataItem.icon ?? 'IconTable',
objectMetadataId: objectMetadataItem.id,
type: ViewType.TABLE_WIDGET,
},
},
objectMetadataItem.id,
);
if (viewResult.status !== 'successful') {
return;
}
const eligibleFields = objectMetadataItem.fields.filter(
filterFieldsForRecordTableViewCreation,
);
const sortedFields = eligibleFields.toSorted(
sortFieldsByRelevanceForRecordTableWidget(
objectMetadataItem.labelIdentifierFieldMetadataId,
),
);
const viewFieldInputs = sortedFields.map((field, index) => ({
id: v4(),
viewId: newViewId,
fieldMetadataId: field.id,
position: index,
size: DEFAULT_VIEW_FIELD_SIZE,
isVisible: index < INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET,
}));
try {
await performViewFieldAPICreate({ inputs: viewFieldInputs });
updatePageLayoutWidget(widgetId, {
configuration: {
configurationType: WidgetConfigurationType.RECORD_TABLE,
viewId: newViewId,
},
});
} catch (error) {
throw new Error(
'Failed to create view fields for record table widget',
{ cause: error },
);
}
},
[performViewAPICreate, performViewFieldAPICreate, updatePageLayoutWidget],
);
return { createViewForRecordTableWidget };
};
@@ -1,15 +0,0 @@
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
import { useCallback } from 'react';
export const useDeleteViewForRecordTableWidget = () => {
const { performViewAPIDestroy } = usePerformViewAPIPersist();
const deleteViewForRecordTableWidget = useCallback(
async (viewId: string) => {
await performViewAPIDestroy({ id: viewId });
},
[performViewAPIDestroy],
);
return { deleteViewForRecordTableWidget };
};
@@ -0,0 +1,71 @@
import { type FlatViewField } from '@/metadata-store/types/FlatViewField';
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState';
import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { type View } from '@/views/types/View';
import { useStore } from 'jotai';
import { useCallback, useEffect } from 'react';
type UseInitializeRecordTableWidgetViewDraftParams = {
widgetId: string;
view: View | undefined;
};
export const useInitializeRecordTableWidgetViewDraft = ({
widgetId,
view,
}: UseInitializeRecordTableWidgetViewDraftParams) => {
const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState(
recordTableWidgetViewDraftComponentState,
);
const recordTableWidgetViewPersistedState =
useAtomComponentStateCallbackState(
recordTableWidgetViewPersistedComponentState,
);
const store = useStore();
const initializeDraft = useCallback(() => {
const currentDraft = store.get(recordTableWidgetViewDraftState);
if (widgetId in currentDraft) {
return;
}
if (!view || view.viewFields.length === 0) {
return;
}
const { viewFields, ...viewProps } = view;
const flatViewFields: FlatViewField[] = viewFields.map((field) => ({
...field,
viewId: view.id,
}));
const snapshot: RecordTableWidgetViewSnapshot = {
view: viewProps,
viewFields: flatViewFields,
};
store.set(recordTableWidgetViewDraftState, (prev) => ({
...prev,
[widgetId]: snapshot,
}));
store.set(recordTableWidgetViewPersistedState, (prev) => ({
...prev,
[widgetId]: snapshot,
}));
}, [
recordTableWidgetViewDraftState,
recordTableWidgetViewPersistedState,
widgetId,
view,
store,
]);
useEffect(initializeDraft, [initializeDraft]);
};
@@ -1,18 +1,33 @@
import { useMapViewFieldToRecordTableWidgetViewFieldItem } from '@/page-layout/widgets/record-table/hooks/useMapViewFieldToRecordTableWidgetViewFieldItem';
import { useRecordTableWidgetViewForDisplay } from '@/page-layout/widgets/record-table/hooks/useRecordTableWidgetViewForDisplay';
import { type RecordTableWidgetViewFieldItem } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewFieldItem';
import { useViewById } from '@/views/hooks/useViewById';
import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { sortByProperty } from '~/utils/array/sortByProperty';
export const useRecordTableWidgetViewFieldItems = (viewId: string) => {
const { view } = useViewById(viewId);
type UseRecordTableWidgetViewFieldItemsParams = {
viewId: string;
widgetId: string;
pageLayoutId: string;
};
export const useRecordTableWidgetViewFieldItems = ({
viewId,
widgetId,
pageLayoutId,
}: UseRecordTableWidgetViewFieldItemsParams) => {
const { view } = useRecordTableWidgetViewForDisplay({
viewId,
widgetId,
pageLayoutId,
});
const { mapViewFieldToRecordTableWidgetViewFieldItem } =
useMapViewFieldToRecordTableWidgetViewFieldItem();
const recordTableWidgetViewFieldItems: RecordTableWidgetViewFieldItem[] =
useMemo(() => {
if (!view) {
if (!isDefined(view)) {
return [];
}
@@ -0,0 +1,35 @@
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
import { constructViewFromRecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/utils/constructViewFromRecordTableWidgetViewSnapshot';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useViewById } from '@/views/hooks/useViewById';
import { type View } from '@/views/types/View';
import { isDefined } from 'twenty-shared/utils';
type UseRecordTableWidgetViewForDisplayParams = {
viewId: string;
widgetId: string;
pageLayoutId: string;
};
export const useRecordTableWidgetViewForDisplay = ({
viewId,
widgetId,
pageLayoutId,
}: UseRecordTableWidgetViewForDisplayParams): {
view: View | undefined;
} => {
const { view } = useViewById(viewId);
const recordTableWidgetViewDraft = useAtomComponentStateValue(
recordTableWidgetViewDraftComponentState,
pageLayoutId,
);
const draftSnapshot = recordTableWidgetViewDraft[widgetId];
const viewFromDraft = isDefined(draftSnapshot)
? constructViewFromRecordTableWidgetViewSnapshot(draftSnapshot)
: undefined;
return { view: viewFromDraft ?? view };
};
@@ -0,0 +1,27 @@
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
export const useRemoveDraftViewForRecordTableWidget = (
pageLayoutId: string,
) => {
const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState(
recordTableWidgetViewDraftComponentState,
pageLayoutId,
);
const store = useStore();
const removeDraftViewForRecordTableWidget = useCallback(
(widgetId: string) => {
store.set(recordTableWidgetViewDraftState, (prev) => {
const { [widgetId]: _, ...rest } = prev;
return rest;
});
},
[store, recordTableWidgetViewDraftState],
);
return { removeDraftViewForRecordTableWidget };
};
@@ -1,12 +1,28 @@
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
import { type RecordTableWidgetViewFieldItem } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewFieldItem';
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useReorderRecordTableWidgetFields = () => {
const { performViewFieldAPIUpdate } = usePerformViewFieldAPIPersist();
type UseReorderRecordTableWidgetFieldsParams = {
pageLayoutId: string;
widgetId: string;
};
export const useReorderRecordTableWidgetFields = ({
pageLayoutId,
widgetId,
}: UseReorderRecordTableWidgetFieldsParams) => {
const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState(
recordTableWidgetViewDraftComponentState,
pageLayoutId,
);
const store = useStore();
const reorderRecordTableWidgetFields = useCallback(
async (
(
sourceIndex: number,
destinationIndex: number,
visibleFieldItems: RecordTableWidgetViewFieldItem[],
@@ -19,16 +35,35 @@ export const useReorderRecordTableWidgetFields = () => {
const [movedField] = reorderedFields.splice(sourceIndex, 1);
reorderedFields.splice(destinationIndex, 0, movedField);
const updates = reorderedFields.map((fieldItem, index) => ({
input: {
id: fieldItem.viewField.id,
update: { position: index },
},
}));
const updatedPositions = new Map(
reorderedFields.map((fieldItem, index) => [
fieldItem.viewField.id,
index,
]),
);
await performViewFieldAPIUpdate(updates);
store.set(recordTableWidgetViewDraftState, (prev) => {
const widgetViewDraft = prev[widgetId];
if (!isDefined(widgetViewDraft)) {
return prev;
}
return {
...prev,
[widgetId]: {
...widgetViewDraft,
viewFields: widgetViewDraft.viewFields.map((field) => {
const newPosition = updatedPositions.get(field.id);
return newPosition !== undefined
? { ...field, position: newPosition }
: field;
}),
},
};
});
},
[performViewFieldAPIUpdate],
[store, recordTableWidgetViewDraftState, widgetId],
);
return { reorderRecordTableWidgetFields };
@@ -5,6 +5,7 @@ import { currentRecordFiltersComponentState } from '@/object-record/record-filte
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
import { useMapRecordFieldToViewFieldWithCurrentAggregateOperation } from '@/page-layout/widgets/record-table/hooks/useMapRecordFieldToViewFieldWithCurrentAggregateOperation';
import { computeViewFieldsToCreateAndUpdate } from '@/page-layout/widgets/record-table/utils/computeViewFieldsToCreateAndUpdate';
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
@@ -65,6 +66,11 @@ export const useSaveRecordTableWidgetsViewDataOnDashboardSave = () => {
);
const views = store.get(viewsSelector.atom);
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
const recordTableWidgetViewDraft = store.get(
recordTableWidgetViewDraftComponentState.atomFamily({
instanceId: pageLayoutId,
}),
);
const recordTableWidgets = pageLayoutDraft.tabs.flatMap((tab) =>
tab.widgets.filter(
@@ -182,16 +188,21 @@ export const useSaveRecordTableWidgetsViewDataOnDashboardSave = () => {
}),
);
const newViewFields = currentRecordFields.map(
const recordIndexViewFields = currentRecordFields.map(
mapRecordFieldToViewFieldWithCurrentAggregateOperation,
);
const existingViewFields = currentView.viewFields ?? [];
const draftSnapshot = recordTableWidgetViewDraft[widget.id];
const draftViewFields = draftSnapshot?.viewFields ?? [];
const metadataStoreViewFields =
draftViewFields.length > 0
? draftViewFields
: (currentView.viewFields ?? []);
const { viewFieldsToCreate, viewFieldsToUpdate } =
computeViewFieldsToCreateAndUpdate({
newViewFields,
existingViewFields,
newViewFields: metadataStoreViewFields,
existingViewFields: recordIndexViewFields,
viewId,
});
@@ -1,21 +1,46 @@
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useToggleRecordTableWidgetFieldVisibility = () => {
const { performViewFieldAPIUpdate } = usePerformViewFieldAPIPersist();
type UseToggleRecordTableWidgetFieldVisibilityParams = {
pageLayoutId: string;
widgetId: string;
};
export const useToggleRecordTableWidgetFieldVisibility = ({
pageLayoutId,
widgetId,
}: UseToggleRecordTableWidgetFieldVisibilityParams) => {
const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState(
recordTableWidgetViewDraftComponentState,
pageLayoutId,
);
const store = useStore();
const toggleRecordTableWidgetFieldVisibility = useCallback(
async (viewFieldId: string, isVisible: boolean) => {
await performViewFieldAPIUpdate([
{
input: {
id: viewFieldId,
update: { isVisible },
(viewFieldId: string, isVisible: boolean) => {
store.set(recordTableWidgetViewDraftState, (prev) => {
const widgetViewDraft = prev[widgetId];
if (!isDefined(widgetViewDraft)) {
return prev;
}
return {
...prev,
[widgetId]: {
...widgetViewDraft,
viewFields: widgetViewDraft.viewFields.map((field) =>
field.id === viewFieldId ? { ...field, isVisible } : field,
),
},
},
]);
};
});
},
[performViewFieldAPIUpdate],
[store, recordTableWidgetViewDraftState, widgetId],
);
return { toggleRecordTableWidgetFieldVisibility };
@@ -0,0 +1,7 @@
import { type FlatView } from '@/metadata-store/types/FlatView';
import { type FlatViewField } from '@/metadata-store/types/FlatViewField';
export type RecordTableWidgetViewSnapshot = {
view: FlatView;
viewFields: FlatViewField[];
};
@@ -0,0 +1,130 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { sortFieldsByRelevanceForRecordTableWidget } from '@/page-layout/widgets/record-table/utils/sortFieldsByRelevanceForRecordTableWidget';
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
const createField = (
overrides: Partial<FieldMetadataItem> & { id: string },
): FieldMetadataItem =>
({
name: 'field',
label: 'Field',
type: FieldMetadataType.TEXT,
isActive: true,
isSystem: false,
settings: null,
...overrides,
}) as FieldMetadataItem;
describe('sortFieldsByRelevanceForRecordTableWidget', () => {
const labelIdentifierId = 'label-field-id';
const sorter = sortFieldsByRelevanceForRecordTableWidget(labelIdentifierId);
it('should place the label identifier field first', () => {
const labelField = createField({ id: labelIdentifierId });
const textField = createField({ id: 'text-1' });
expect(sorter(labelField, textField)).toBe(-1);
expect(sorter(textField, labelField)).toBe(1);
});
it('should place ONE_TO_MANY relation fields after non-relation fields', () => {
const reverseSide = createField({
id: 'reverse-1',
type: FieldMetadataType.RELATION,
settings: { relationType: RelationType.ONE_TO_MANY },
});
const textField = createField({ id: 'text-1' });
expect(sorter(reverseSide, textField)).toBe(1);
expect(sorter(textField, reverseSide)).toBe(-1);
});
it('should place regular relation fields after non-relation fields', () => {
const relation = createField({
id: 'rel-1',
type: FieldMetadataType.RELATION,
settings: { relationType: RelationType.MANY_TO_ONE },
});
const textField = createField({ id: 'text-1' });
expect(sorter(relation, textField)).toBe(1);
expect(sorter(textField, relation)).toBe(-1);
});
it('should place ONE_TO_MANY relations after MANY_TO_ONE relations', () => {
const reverseSide = createField({
id: 'reverse-1',
type: FieldMetadataType.RELATION,
settings: { relationType: RelationType.ONE_TO_MANY },
});
const manyToOne = createField({
id: 'rel-1',
type: FieldMetadataType.RELATION,
settings: { relationType: RelationType.MANY_TO_ONE },
});
expect(sorter(reverseSide, manyToOne)).toBe(1);
expect(sorter(manyToOne, reverseSide)).toBe(-1);
});
it('should return 0 for two non-relation fields of equal priority', () => {
const fieldA = createField({ id: 'a' });
const fieldB = createField({ id: 'b' });
expect(sorter(fieldA, fieldB)).toBe(0);
});
it('should return 0 for two ONE_TO_MANY relation fields', () => {
const reverseA = createField({
id: 'reverse-a',
type: FieldMetadataType.RELATION,
settings: { relationType: RelationType.ONE_TO_MANY },
});
const reverseB = createField({
id: 'reverse-b',
type: FieldMetadataType.RELATION,
settings: { relationType: RelationType.ONE_TO_MANY },
});
expect(sorter(reverseA, reverseB)).toBe(0);
});
it('should produce a correct full sort order', () => {
const labelField = createField({ id: labelIdentifierId });
const textField = createField({ id: 'text-1' });
const numberField = createField({
id: 'number-1',
type: FieldMetadataType.NUMBER,
});
const manyToOneField = createField({
id: 'rel-1',
type: FieldMetadataType.RELATION,
settings: { relationType: RelationType.MANY_TO_ONE },
});
const oneToManyField = createField({
id: 'reverse-1',
type: FieldMetadataType.RELATION,
settings: { relationType: RelationType.ONE_TO_MANY },
});
const fields = [
oneToManyField,
manyToOneField,
textField,
numberField,
labelField,
];
const sorted = [...fields].sort(sorter);
expect(sorted[0].id).toBe(labelIdentifierId);
expect(sorted[sorted.length - 1].id).toBe('reverse-1');
const labelIdx = sorted.findIndex((f) => f.id === labelIdentifierId);
const textIdx = sorted.findIndex((f) => f.id === 'text-1');
const relIdx = sorted.findIndex((f) => f.id === 'rel-1');
const reverseIdx = sorted.findIndex((f) => f.id === 'reverse-1');
expect(labelIdx).toBeLessThan(textIdx);
expect(textIdx).toBeLessThan(relIdx);
expect(relIdx).toBeLessThan(reverseIdx);
});
});
@@ -0,0 +1,13 @@
import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot';
import { type View } from '@/views/types/View';
export const constructViewFromRecordTableWidgetViewSnapshot = (
snapshot: RecordTableWidgetViewSnapshot,
): View => ({
...snapshot.view,
viewFields: snapshot.viewFields,
viewFilters: [],
viewSorts: [],
viewGroups: [],
viewFilterGroups: [],
});