Files
twenty/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts
T
30c42345f0 Core views frontend (#13932)
Parallel code path to read and write core views when
IS_CORE_VIEW_ENABLED.

Migrated view key to an enum.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
2025-08-20 19:04:58 +02:00

277 lines
11 KiB
TypeScript

import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
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 { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
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 { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistViewFieldRecords';
import { usePersistViewFilterGroupRecords } from '@/views/hooks/internal/usePersistViewFilterGroupRecords';
import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilterRecords';
import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroupRecords';
import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords';
import { isPersistingViewFieldsState } from '@/views/states/isPersistingViewFieldsState';
import { type GraphQLView } from '@/views/types/GraphQLView';
import { type View } from '@/views/types/View';
import { type ViewGroup } from '@/views/types/ViewGroup';
import { type ViewSort } from '@/views/types/ViewSort';
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 { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { FeatureFlagKey, useCreateCoreViewMutation } from '~/generated/graphql';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
const featureFlags = useFeatureFlagsMap();
const isCoreViewEnabled = featureFlags[FeatureFlagKey.IS_CORE_VIEW_ENABLED];
const [createCoreViewMutation] = useCreateCoreViewMutation();
const currentViewIdCallbackState = useRecoilComponentCallbackState(
contextStoreCurrentViewIdComponentState,
viewBarComponentId,
);
const { createOneRecord } = useCreateOneRecord<View>({
objectNameSingular: CoreObjectNameSingular.View,
});
const anyFieldFilterValue = useRecoilComponentValue(
anyFieldFilterValueComponentState,
);
const { createViewFieldRecords } = usePersistViewFieldRecords();
const { createViewSortRecords } = usePersistViewSortRecords();
const { createViewGroupRecords } = usePersistViewGroupRecords();
const { createViewFilterRecords } = usePersistViewFilterRecords();
const { createViewFilterGroupRecords } = usePersistViewFilterGroupRecords();
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const { findManyRecordsLazy } = useLazyFindManyRecords({
objectNameSingular: CoreObjectNameSingular.View,
fetchPolicy: 'network-only',
});
const currentRecordFilterGroups = useRecoilComponentValue(
currentRecordFilterGroupsComponentState,
);
const currentRecordSorts = useRecoilComponentValue(
currentRecordSortsComponentState,
);
const currentRecordFilters = useRecoilComponentValue(
currentRecordFiltersComponentState,
);
const createViewFromCurrentView = useRecoilCallback(
({ snapshot, set }) =>
async (
{
id,
name,
icon,
kanbanFieldMetadataId,
type,
}: Partial<
Pick<
GraphQLView,
'id' | 'name' | 'icon' | 'kanbanFieldMetadataId' | 'type'
>
>,
shouldCopyFiltersAndSortsAndAggregate?: boolean,
): Promise<string | undefined> => {
const currentViewId = getSnapshotValue(
snapshot,
currentViewIdCallbackState,
);
if (!isDefined(currentViewId)) {
return undefined;
}
const sourceView = snapshot
.getLoadable(
prefetchViewFromViewIdFamilySelector({
viewId: currentViewId,
}),
)
.getValue();
if (!isDefined(sourceView)) {
return undefined;
}
set(isPersistingViewFieldsState, true);
let newViewId: string | undefined;
if (isCoreViewEnabled) {
const result = await createCoreViewMutation({
variables: {
input: {
name: name ?? sourceView.name,
icon: icon ?? sourceView.icon,
key: null,
kanbanAggregateOperation: shouldCopyFiltersAndSortsAndAggregate
? sourceView.kanbanAggregateOperation
: undefined,
kanbanAggregateOperationFieldMetadataId:
shouldCopyFiltersAndSortsAndAggregate
? sourceView.kanbanAggregateOperationFieldMetadataId
: undefined,
type: convertViewTypeToCore(type ?? sourceView.type),
objectMetadataId: sourceView.objectMetadataId,
openRecordIn: convertViewOpenRecordInToCore(
sourceView.openRecordIn,
),
anyFieldFilterValue: anyFieldFilterValue,
},
},
});
newViewId = result.data?.createCoreView?.id ?? undefined;
} else {
const createdView = await createOneRecord({
id: id ?? v4(),
name: name ?? sourceView.name,
icon: icon ?? sourceView.icon,
key: null,
kanbanFieldMetadataId:
kanbanFieldMetadataId ?? sourceView.kanbanFieldMetadataId,
kanbanAggregateOperation: shouldCopyFiltersAndSortsAndAggregate
? sourceView.kanbanAggregateOperation
: undefined,
kanbanAggregateOperationFieldMetadataId:
shouldCopyFiltersAndSortsAndAggregate
? sourceView.kanbanAggregateOperationFieldMetadataId
: undefined,
type: type ?? sourceView.type,
objectMetadataId: sourceView.objectMetadataId,
openRecordIn: sourceView.openRecordIn,
anyFieldFilterValue: anyFieldFilterValue,
});
newViewId = createdView.id;
}
if (isUndefinedOrNull(newViewId)) {
throw new Error('Failed to create view');
}
await createViewFieldRecords(sourceView.viewFields, { id: newViewId });
if (type === ViewType.Kanban) {
if (!isDefined(kanbanFieldMetadataId)) {
throw new Error('Kanban view must have a kanban field');
}
const viewGroupsToCreate =
objectMetadataItem.fields
?.find((field) => field.id === kanbanFieldMetadataId)
?.options?.map(
(option, index) =>
({
id: v4(),
__typename: 'ViewGroup',
fieldMetadataId: kanbanFieldMetadataId,
fieldValue: option.value,
isVisible: true,
position: index,
}) satisfies ViewGroup,
) ?? [];
viewGroupsToCreate.push({
__typename: 'ViewGroup',
id: v4(),
fieldValue: '',
position: viewGroupsToCreate.length,
isVisible: true,
fieldMetadataId: kanbanFieldMetadataId,
} satisfies ViewGroup);
await createViewGroupRecords({
viewGroupsToCreate,
viewId: newViewId,
});
}
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(mapRecordSortToViewSort)
.map(
(viewSort) =>
({
...viewSort,
id: v4(),
}) satisfies ViewSort,
);
await createViewFilterGroupRecords(viewFilterGroupsToCreate, {
id: newViewId,
});
await createViewFilterRecords(viewFiltersToCreate, { id: newViewId });
await createViewSortRecords(viewSortsToCreate, { id: newViewId });
}
await findManyRecordsLazy();
set(isPersistingViewFieldsState, false);
return newViewId;
},
[
anyFieldFilterValue,
currentViewIdCallbackState,
createOneRecord,
createViewFieldRecords,
findManyRecordsLazy,
objectMetadataItem.fields,
createViewGroupRecords,
createViewSortRecords,
createViewFilterRecords,
createViewFilterGroupRecords,
currentRecordFilters,
currentRecordSorts,
currentRecordFilterGroups,
isCoreViewEnabled,
createCoreViewMutation,
],
);
return { createViewFromCurrentView };
};