987995636f
## Summary - View persist hooks (`usePerformView(Sort|Field|Filter|Group|FieldGroup|FilterGroup|)APIPersist`) now write successful mutation results back to the metadata store (`addToDraft` / `updateInDraft` / `removeFromDraft` + `applyChanges`), following the existing pattern from `usePerformViewAPIUpdate`. - Previously the store only updated via SSE, so until that landed, save flows diffed against stale view data and re-sent creates with the same id — failing server-side with a duplicate key error. Fixes TWENTY-SERVER-HQM
282 lines
9.8 KiB
TypeScript
282 lines
9.8 KiB
TypeScript
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
|
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
|
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
|
|
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
|
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
|
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
|
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
|
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
|
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
|
|
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
|
|
import { usePerformViewFilterAPIPersist } from '@/views/hooks/internal/usePerformViewFilterAPIPersist';
|
|
import { usePerformViewFilterGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFilterGroupAPIPersist';
|
|
import { usePerformViewSortAPIPersist } from '@/views/hooks/internal/usePerformViewSortAPIPersist';
|
|
import { viewFromViewIdFamilySelector } from '@/views/states/selectors/viewFromViewIdFamilySelector';
|
|
import { type GraphQLView } from '@/views/types/GraphQLView';
|
|
import { ViewType } from '@/views/types/ViewType';
|
|
import { duplicateViewFiltersAndViewFilterGroups } from '@/views/utils/duplicateViewFiltersAndViewFilterGroups';
|
|
import { mapRecordFilterGroupToViewFilterGroup } from '@/views/utils/mapRecordFilterGroupToViewFilterGroup';
|
|
import { mapRecordFilterToViewFilter } from '@/views/utils/mapRecordFilterToViewFilter';
|
|
import { mapRecordSortToViewSort } from '@/views/utils/mapRecordSortToViewSort';
|
|
import { useStore } from 'jotai';
|
|
import { useCallback } from 'react';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import { v4 } from 'uuid';
|
|
import { ViewCalendarLayout } from '~/generated-metadata/graphql';
|
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
|
|
|
export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
|
|
const { performViewAPICreate } = usePerformViewAPIPersist();
|
|
|
|
const { objectMetadataItem, recordIndexId } = useRecordIndexContextOrThrow();
|
|
|
|
const currentViewId = useAtomComponentStateCallbackState(
|
|
contextStoreCurrentViewIdComponentState,
|
|
viewBarComponentId,
|
|
);
|
|
|
|
const anyFieldFilterValue = useAtomComponentStateValue(
|
|
anyFieldFilterValueComponentState,
|
|
recordIndexId,
|
|
);
|
|
|
|
const { performViewFieldAPICreate } = usePerformViewFieldAPIPersist();
|
|
|
|
const { performViewSortAPICreate } = usePerformViewSortAPIPersist();
|
|
|
|
const { performViewFilterAPICreate } = usePerformViewFilterAPIPersist();
|
|
|
|
const { performViewFilterGroupAPICreate } =
|
|
usePerformViewFilterGroupAPIPersist();
|
|
|
|
const store = useStore();
|
|
|
|
const currentRecordFilterGroups = useAtomComponentStateValue(
|
|
currentRecordFilterGroupsComponentState,
|
|
recordIndexId,
|
|
);
|
|
|
|
const currentRecordSorts = useAtomComponentStateValue(
|
|
currentRecordSortsComponentState,
|
|
recordIndexId,
|
|
);
|
|
|
|
const currentRecordFilters = useAtomComponentStateValue(
|
|
currentRecordFiltersComponentState,
|
|
recordIndexId,
|
|
);
|
|
|
|
const createViewFromCurrentView = useCallback(
|
|
async (
|
|
{
|
|
id,
|
|
name,
|
|
icon,
|
|
mainGroupByFieldMetadataId,
|
|
calendarFieldMetadataId,
|
|
calendarEndFieldMetadataId,
|
|
type,
|
|
visibility,
|
|
}: Partial<
|
|
Pick<
|
|
GraphQLView,
|
|
| 'id'
|
|
| 'name'
|
|
| 'icon'
|
|
| 'mainGroupByFieldMetadataId'
|
|
| 'calendarFieldMetadataId'
|
|
| 'calendarEndFieldMetadataId'
|
|
| 'type'
|
|
| 'visibility'
|
|
>
|
|
>,
|
|
shouldCopyFiltersAndSortsAndAggregate?: boolean,
|
|
): Promise<string | undefined> => {
|
|
const existingCurrentViewId = store.get(currentViewId);
|
|
|
|
if (!isDefined(existingCurrentViewId)) {
|
|
return undefined;
|
|
}
|
|
|
|
const sourceView = store.get(
|
|
viewFromViewIdFamilySelector.selectorFamily({
|
|
viewId: existingCurrentViewId,
|
|
}),
|
|
);
|
|
|
|
if (!isDefined(sourceView)) {
|
|
return undefined;
|
|
}
|
|
|
|
const viewType = type ?? sourceView.type;
|
|
|
|
const result = await performViewAPICreate(
|
|
{
|
|
input: {
|
|
id: id ?? v4(),
|
|
name: name ?? sourceView.name,
|
|
icon: icon ?? sourceView.icon,
|
|
key: null,
|
|
kanbanAggregateOperation: shouldCopyFiltersAndSortsAndAggregate
|
|
? sourceView.kanbanAggregateOperation
|
|
: undefined,
|
|
kanbanAggregateOperationFieldMetadataId:
|
|
shouldCopyFiltersAndSortsAndAggregate
|
|
? sourceView.kanbanAggregateOperationFieldMetadataId
|
|
: undefined,
|
|
kanbanColumnWidth: shouldCopyFiltersAndSortsAndAggregate
|
|
? sourceView.kanbanColumnWidth
|
|
: undefined,
|
|
mainGroupByFieldMetadataId: shouldCopyFiltersAndSortsAndAggregate
|
|
? sourceView.mainGroupByFieldMetadataId
|
|
: mainGroupByFieldMetadataId,
|
|
type: viewType,
|
|
objectMetadataId: sourceView.objectMetadataId,
|
|
openRecordIn: sourceView.openRecordIn,
|
|
anyFieldFilterValue: anyFieldFilterValue,
|
|
calendarLayout:
|
|
viewType === ViewType.CALENDAR
|
|
? ViewCalendarLayout.MONTH
|
|
: undefined,
|
|
calendarFieldMetadataId:
|
|
viewType === ViewType.CALENDAR
|
|
? calendarFieldMetadataId
|
|
: undefined,
|
|
calendarEndFieldMetadataId:
|
|
viewType === ViewType.CALENDAR
|
|
? calendarEndFieldMetadataId
|
|
: undefined,
|
|
visibility,
|
|
},
|
|
},
|
|
objectMetadataItem.id,
|
|
);
|
|
|
|
if (result.status === 'failed') {
|
|
return undefined;
|
|
}
|
|
|
|
const newViewId = result.response.data?.createView.id;
|
|
|
|
if (isUndefinedOrNull(newViewId)) {
|
|
throw new Error('Failed to create view');
|
|
}
|
|
|
|
const fieldResult = await performViewFieldAPICreate({
|
|
inputs: sourceView.viewFields.map((viewField) => ({
|
|
id: v4(),
|
|
fieldMetadataId: viewField.fieldMetadataId,
|
|
position: viewField.position,
|
|
isVisible: viewField.isVisible,
|
|
size: viewField.size,
|
|
aggregateOperation: viewField.aggregateOperation,
|
|
viewFieldGroupId: viewField.viewFieldGroupId,
|
|
viewId: newViewId,
|
|
})),
|
|
});
|
|
|
|
if (fieldResult.status === 'failed') {
|
|
return undefined;
|
|
}
|
|
|
|
if (shouldCopyFiltersAndSortsAndAggregate === true) {
|
|
const viewFilterGroupsToCopy = currentRecordFilterGroups.map(
|
|
(recordFilterGroup) =>
|
|
mapRecordFilterGroupToViewFilterGroup({
|
|
recordFilterGroup,
|
|
view: { id: newViewId },
|
|
}),
|
|
);
|
|
|
|
const viewFiltersToCopy = currentRecordFilters.map(
|
|
mapRecordFilterToViewFilter,
|
|
);
|
|
|
|
const {
|
|
duplicatedViewFilterGroups: viewFilterGroupsToCreate,
|
|
duplicatedViewFilters: viewFiltersToCreate,
|
|
} = duplicateViewFiltersAndViewFilterGroups({
|
|
viewFilterGroupsToDuplicate: viewFilterGroupsToCopy,
|
|
viewFiltersToDuplicate: viewFiltersToCopy,
|
|
});
|
|
|
|
const viewSortsToCreate = currentRecordSorts
|
|
.map((recordSort) => mapRecordSortToViewSort(recordSort))
|
|
.map((viewSort) => ({
|
|
...viewSort,
|
|
id: v4(),
|
|
}));
|
|
|
|
const filterGroupResult = await performViewFilterGroupAPICreate(
|
|
viewFilterGroupsToCreate,
|
|
{
|
|
id: newViewId,
|
|
},
|
|
);
|
|
|
|
if (filterGroupResult.status === 'failed') {
|
|
return undefined;
|
|
}
|
|
|
|
const createViewFilterInputs = viewFiltersToCreate.map(
|
|
(viewFilter) => ({
|
|
input: {
|
|
id: viewFilter.id,
|
|
fieldMetadataId: viewFilter.fieldMetadataId,
|
|
viewId: newViewId,
|
|
value: viewFilter.value,
|
|
operand: viewFilter.operand,
|
|
viewFilterGroupId: viewFilter.viewFilterGroupId,
|
|
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
|
|
subFieldName: viewFilter.subFieldName ?? null,
|
|
relationTargetFieldMetadataId:
|
|
viewFilter.relationTargetFieldMetadataId ?? null,
|
|
},
|
|
}),
|
|
);
|
|
|
|
const filterResult = await performViewFilterAPICreate(
|
|
createViewFilterInputs,
|
|
);
|
|
|
|
if (filterResult.status === 'failed') {
|
|
return undefined;
|
|
}
|
|
|
|
const createViewSortInputs = viewSortsToCreate.map((viewSort) => ({
|
|
input: {
|
|
id: viewSort.id,
|
|
fieldMetadataId: viewSort.fieldMetadataId,
|
|
viewId: newViewId,
|
|
direction: viewSort.direction,
|
|
},
|
|
}));
|
|
|
|
const sortResult = await performViewSortAPICreate(createViewSortInputs);
|
|
|
|
if (sortResult.status === 'failed') {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
return newViewId;
|
|
},
|
|
[
|
|
currentViewId,
|
|
performViewAPICreate,
|
|
anyFieldFilterValue,
|
|
objectMetadataItem,
|
|
performViewFieldAPICreate,
|
|
store,
|
|
currentRecordFilterGroups,
|
|
currentRecordFilters,
|
|
currentRecordSorts,
|
|
performViewFilterGroupAPICreate,
|
|
performViewFilterAPICreate,
|
|
performViewSortAPICreate,
|
|
],
|
|
);
|
|
|
|
return { createViewFromCurrentView };
|
|
};
|