Fix orphan views by deferring record table widget view creation to dashboard save (#20006)
This commit is contained in:
+14
-7
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+23
@@ -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;
|
||||
};
|
||||
+89
@@ -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 };
|
||||
};
|
||||
-85
@@ -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 };
|
||||
};
|
||||
-15
@@ -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 };
|
||||
};
|
||||
+71
@@ -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]);
|
||||
};
|
||||
+19
-4
@@ -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 [];
|
||||
}
|
||||
|
||||
|
||||
+35
@@ -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 };
|
||||
};
|
||||
+27
@@ -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 };
|
||||
};
|
||||
+47
-12
@@ -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 };
|
||||
|
||||
+15
-4
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
+37
-12
@@ -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 };
|
||||
|
||||
+7
@@ -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[];
|
||||
};
|
||||
+130
@@ -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);
|
||||
});
|
||||
});
|
||||
+13
@@ -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: [],
|
||||
});
|
||||
Reference in New Issue
Block a user