77409b6eb2
In this PR (1/3) - introduce view.mainGroupByFieldMetadataId as the new reference determining which fieldMetadataId is used in a grouped view, in order to deprecate viewGroup.fieldMetadataId which creates inconsistencies. view.mainGroupByFieldMetadataId is now filled at every view creation, though not in use yet. - Introduce a command to backfill view.mainGroupByFieldMetadataId for existing views + delete all viewGroup.fieldMetadataId with a fieldMetadataId that is not view.mainGroupByFieldMetadataId. (It should concern 37 active workspaces) - Temporarily disable the option to change a grouped view's fieldMetadataId as for now it creates inconsistencies. This feature can be reintroduced when we have done the full migration. In a next PR - (2/3) use view.mainGroupByFieldMetadataId instead of viewGroup.fieldMetadataId. In FE we may keep viewGroup.fieldMetadataId as a state (TBD). View groups will now be created / deleted as a side effect of view's mainGroupByFieldMetadataId update. - (3/3) remove viewGroup.fieldMetadataId --------- Co-authored-by: Charles Bochet <charles@twenty.com>
308 lines
11 KiB
TypeScript
308 lines
11 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 { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
|
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
|
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
|
import { usePersistView } from '@/views/hooks/internal/usePersistView';
|
|
import { usePersistViewField } from '@/views/hooks/internal/usePersistViewField';
|
|
import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilter';
|
|
import { usePersistViewFilterGroupRecords } from '@/views/hooks/internal/usePersistViewFilterGroup';
|
|
import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroup';
|
|
import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSort';
|
|
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
|
|
import { isPersistingViewFieldsState } from '@/views/states/isPersistingViewFieldsState';
|
|
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
|
|
import { type GraphQLView } from '@/views/types/GraphQLView';
|
|
import { type ViewGroup } from '@/views/types/ViewGroup';
|
|
import { ViewType } from '@/views/types/ViewType';
|
|
import { convertViewOpenRecordInToCore } from '@/views/utils/convertViewOpenRecordInToCore';
|
|
import { convertViewTypeToCore } from '@/views/utils/convertViewTypeToCore';
|
|
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 { useRecoilCallback } from 'recoil';
|
|
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 { createView } = usePersistView();
|
|
|
|
const currentViewIdCallbackState = useRecoilComponentCallbackState(
|
|
contextStoreCurrentViewIdComponentState,
|
|
viewBarComponentId,
|
|
);
|
|
|
|
const anyFieldFilterValue = useRecoilComponentValue(
|
|
anyFieldFilterValueComponentState,
|
|
);
|
|
|
|
const { createViewFields } = usePersistViewField();
|
|
|
|
const { createViewSorts } = usePersistViewSortRecords();
|
|
|
|
const { createViewGroups } = usePersistViewGroupRecords();
|
|
|
|
const { createViewFilters } = usePersistViewFilterRecords();
|
|
|
|
const { createViewFilterGroups } = usePersistViewFilterGroupRecords();
|
|
|
|
const { objectMetadataItem } = useRecordIndexContextOrThrow();
|
|
|
|
const { refreshCoreViewsByObjectMetadataId } =
|
|
useRefreshCoreViewsByObjectMetadataId();
|
|
|
|
const currentRecordFilterGroups = useRecoilComponentValue(
|
|
currentRecordFilterGroupsComponentState,
|
|
);
|
|
|
|
const currentRecordSorts = useRecoilComponentValue(
|
|
currentRecordSortsComponentState,
|
|
);
|
|
|
|
const currentRecordFilters = useRecoilComponentValue(
|
|
currentRecordFiltersComponentState,
|
|
);
|
|
|
|
const createViewFromCurrentView = useRecoilCallback(
|
|
({ snapshot, set }) =>
|
|
async (
|
|
{
|
|
id,
|
|
name,
|
|
icon,
|
|
mainGroupByFieldMetadataId,
|
|
calendarFieldMetadataId,
|
|
type,
|
|
visibility,
|
|
}: Partial<
|
|
Pick<
|
|
GraphQLView,
|
|
| 'id'
|
|
| 'name'
|
|
| 'icon'
|
|
| 'mainGroupByFieldMetadataId'
|
|
| 'calendarFieldMetadataId'
|
|
| 'type'
|
|
| 'visibility'
|
|
>
|
|
>,
|
|
shouldCopyFiltersAndSortsAndAggregate?: boolean,
|
|
): Promise<string | undefined> => {
|
|
const currentViewId = getSnapshotValue(
|
|
snapshot,
|
|
currentViewIdCallbackState,
|
|
);
|
|
|
|
if (!isDefined(currentViewId)) {
|
|
return undefined;
|
|
}
|
|
|
|
const sourceView = snapshot
|
|
.getLoadable(
|
|
coreViewFromViewIdFamilySelector({
|
|
viewId: currentViewId,
|
|
}),
|
|
)
|
|
.getValue();
|
|
|
|
if (!isDefined(sourceView)) {
|
|
return undefined;
|
|
}
|
|
|
|
set(isPersistingViewFieldsState, true);
|
|
|
|
const viewType = type ?? sourceView.type;
|
|
|
|
const result = await createView({
|
|
input: {
|
|
id: id ?? v4(),
|
|
name: name ?? sourceView.name,
|
|
icon: icon ?? sourceView.icon,
|
|
key: null,
|
|
kanbanAggregateOperation: shouldCopyFiltersAndSortsAndAggregate
|
|
? sourceView.kanbanAggregateOperation
|
|
: undefined,
|
|
kanbanAggregateOperationFieldMetadataId:
|
|
shouldCopyFiltersAndSortsAndAggregate
|
|
? sourceView.kanbanAggregateOperationFieldMetadataId
|
|
: undefined,
|
|
mainGroupByFieldMetadataId: shouldCopyFiltersAndSortsAndAggregate
|
|
? sourceView.mainGroupByFieldMetadataId
|
|
: mainGroupByFieldMetadataId,
|
|
type: convertViewTypeToCore(viewType),
|
|
objectMetadataId: sourceView.objectMetadataId,
|
|
openRecordIn: convertViewOpenRecordInToCore(
|
|
sourceView.openRecordIn,
|
|
),
|
|
anyFieldFilterValue: anyFieldFilterValue,
|
|
calendarLayout:
|
|
viewType === ViewType.Calendar
|
|
? ViewCalendarLayout.MONTH
|
|
: undefined,
|
|
calendarFieldMetadataId:
|
|
viewType === ViewType.Calendar
|
|
? calendarFieldMetadataId
|
|
: undefined,
|
|
visibility,
|
|
},
|
|
});
|
|
|
|
if (result.status === 'failed') {
|
|
set(isPersistingViewFieldsState, false);
|
|
return undefined;
|
|
}
|
|
|
|
const newViewId = result.response.data?.createCoreView.id;
|
|
|
|
if (isUndefinedOrNull(newViewId)) {
|
|
set(isPersistingViewFieldsState, false);
|
|
throw new Error('Failed to create view');
|
|
}
|
|
|
|
const fieldResult = await createViewFields({
|
|
inputs: sourceView.viewFields.map(
|
|
({ __typename, id: _id, ...viewField }) => ({
|
|
...viewField,
|
|
id: v4(),
|
|
viewId: newViewId,
|
|
}),
|
|
),
|
|
});
|
|
|
|
if (fieldResult.status === 'failed') {
|
|
set(isPersistingViewFieldsState, false);
|
|
return undefined;
|
|
}
|
|
|
|
if (type === ViewType.Kanban) {
|
|
if (!isDefined(mainGroupByFieldMetadataId)) {
|
|
throw new Error('Kanban view must have a kanban field');
|
|
}
|
|
|
|
const viewGroupsToCreate =
|
|
objectMetadataItem.fields
|
|
?.find((field) => field.id === mainGroupByFieldMetadataId)
|
|
?.options?.map(
|
|
(option, index) =>
|
|
({
|
|
id: v4(),
|
|
__typename: 'ViewGroup',
|
|
fieldMetadataId: mainGroupByFieldMetadataId,
|
|
fieldValue: option.value,
|
|
isVisible: true,
|
|
position: index,
|
|
}) satisfies ViewGroup,
|
|
) ?? [];
|
|
|
|
viewGroupsToCreate.push({
|
|
__typename: 'ViewGroup',
|
|
id: v4(),
|
|
fieldValue: '',
|
|
position: viewGroupsToCreate.length,
|
|
isVisible: true,
|
|
fieldMetadataId: mainGroupByFieldMetadataId,
|
|
} satisfies ViewGroup);
|
|
|
|
const groupResult = await createViewGroups({
|
|
inputs: viewGroupsToCreate.map(({ __typename, ...viewGroup }) => ({
|
|
...viewGroup,
|
|
viewId: newViewId,
|
|
})),
|
|
});
|
|
|
|
if (groupResult.status === 'failed') {
|
|
set(isPersistingViewFieldsState, false);
|
|
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, newViewId))
|
|
.map((viewSort) => ({
|
|
...viewSort,
|
|
id: v4(),
|
|
}));
|
|
|
|
await createViewFilterGroups(viewFilterGroupsToCreate, {
|
|
id: newViewId,
|
|
});
|
|
|
|
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,
|
|
},
|
|
}),
|
|
);
|
|
|
|
const filterResult = await createViewFilters(createViewFilterInputs);
|
|
|
|
if (filterResult.status === 'failed') {
|
|
set(isPersistingViewFieldsState, false);
|
|
return undefined;
|
|
}
|
|
|
|
await createViewSorts(viewSortsToCreate, { id: newViewId });
|
|
}
|
|
|
|
await refreshCoreViewsByObjectMetadataId(objectMetadataItem.id);
|
|
|
|
set(isPersistingViewFieldsState, false);
|
|
return newViewId;
|
|
},
|
|
[
|
|
currentViewIdCallbackState,
|
|
createViewFields,
|
|
createView,
|
|
anyFieldFilterValue,
|
|
objectMetadataItem.fields,
|
|
objectMetadataItem.id,
|
|
createViewGroups,
|
|
currentRecordFilterGroups,
|
|
currentRecordFilters,
|
|
currentRecordSorts,
|
|
createViewFilterGroups,
|
|
createViewFilters,
|
|
createViewSorts,
|
|
refreshCoreViewsByObjectMetadataId,
|
|
],
|
|
);
|
|
|
|
return { createViewFromCurrentView };
|
|
};
|