Create many view groups (#15591)

# Introduction
Same as https://github.com/twentyhq/twenty/pull/15576 but for view
groups creation

When creating a kanban view with v2 flag activated in production result
in race condition due to request being slow and //.
That's why we're introducing a batch create on view group here
closing https://github.com/twentyhq/core-team-issues/issues/1847

## In v2
- batch create view group endpoint is available
- frontend will target the new endpoint

## In v1
- batch create view group endpoint is not available
- frontend will stick to old fake batch view group creation loop


## Gallery
### v2
<img width="1796" height="486" alt="image"
src="https://github.com/user-attachments/assets/932cfe9f-85f1-41cc-a1c4-72a4b5d5a256"
/>

### v1
<img width="1852" height="966" alt="image"
src="https://github.com/user-attachments/assets/8aa9df11-cdea-4b12-ae60-118cdf5e257b"
/>
This commit is contained in:
Paul Rastoin
2025-11-04 15:17:55 +01:00
committed by GitHub
parent 558990fbdd
commit 8dd43be9a2
17 changed files with 857 additions and 41 deletions
@@ -0,0 +1,11 @@
import { VIEW_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewGroupFragment';
import { gql } from '@apollo/client';
export const CREATE_MANY_CORE_VIEW_GROUPS = gql`
${VIEW_GROUP_FRAGMENT}
mutation CreateManyCoreViewGroups($inputs: [CreateViewGroupInput!]!) {
createManyCoreViewGroups(inputs: $inputs) {
...ViewGroupFragment
}
}
`;
@@ -4,15 +4,19 @@ import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetad
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useTriggerViewGroupOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewGroupOptimisticEffect';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { ApolloError } from '@apollo/client';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import {
type CreateCoreViewGroupMutationVariables,
type CreateManyCoreViewGroupsMutationVariables,
type DeleteCoreViewGroupMutationVariables,
type DestroyCoreViewGroupMutationVariables,
FeatureFlagKey,
type UpdateCoreViewGroupMutationVariables,
useCreateCoreViewGroupMutation,
useCreateManyCoreViewGroupsMutation,
useDeleteCoreViewGroupMutation,
useDestroyCoreViewGroupMutation,
useUpdateCoreViewGroupMutation,
@@ -21,7 +25,14 @@ import {
export const usePersistViewGroupRecords = () => {
const { triggerViewGroupOptimisticEffect } =
useTriggerViewGroupOptimisticEffect();
const isWorkspaceMigrationV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
);
const [createCoreViewGroupMutation] = useCreateCoreViewGroupMutation();
const [createManyCoreViewGroupsMutation] =
useCreateManyCoreViewGroupsMutation();
const [updateCoreViewGroupMutation] = useUpdateCoreViewGroupMutation();
const [deleteCoreViewGroupMutation] = useDeleteCoreViewGroupMutation();
const [destroyCoreViewGroupMutation] = useDestroyCoreViewGroupMutation();
@@ -29,7 +40,7 @@ export const usePersistViewGroupRecords = () => {
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
const createViewGroups = useCallback(
const oldCreateViewGroups = useCallback(
async (
createCoreViewGroupInputs: CreateCoreViewGroupMutationVariables[],
): Promise<
@@ -46,7 +57,7 @@ export const usePersistViewGroupRecords = () => {
try {
const results = await Promise.all(
createCoreViewGroupInputs.map((variables) =>
createCoreViewGroupInputs.map(async (variables) =>
createCoreViewGroupMutation({
variables,
update: (_cache, { data }) => {
@@ -90,6 +101,77 @@ export const usePersistViewGroupRecords = () => {
],
);
const createViewGroups = useCallback(
async (
createCoreViewGroupInputs: CreateManyCoreViewGroupsMutationVariables,
): Promise<
| MetadataRequestResult<Awaited<
ReturnType<typeof createManyCoreViewGroupsMutation>
> | null>
| MetadataRequestResult<
Awaited<ReturnType<typeof createCoreViewGroupMutation>>[]
>
> => {
if (
!Array.isArray(createCoreViewGroupInputs.inputs) ||
createCoreViewGroupInputs.inputs.length === 0
) {
return {
status: 'successful',
response: null,
};
}
if (!isWorkspaceMigrationV2Enabled) {
const oldFormatInputs: CreateCoreViewGroupMutationVariables[] =
createCoreViewGroupInputs.inputs.map((input) => ({ input }));
return await oldCreateViewGroups(oldFormatInputs);
}
try {
const result = await createManyCoreViewGroupsMutation({
variables: createCoreViewGroupInputs,
update: (_cache, { data }) => {
const createdViewGroups = data?.createManyCoreViewGroups;
if (!isDefined(createdViewGroups)) {
return;
}
triggerViewGroupOptimisticEffect({
createdViewGroups,
});
},
});
return {
status: 'successful',
response: result,
};
} catch (error) {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'viewGroup',
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
}
return {
status: 'failed',
error,
};
}
},
[
isWorkspaceMigrationV2Enabled,
oldCreateViewGroups,
triggerViewGroupOptimisticEffect,
createManyCoreViewGroupsMutation,
handleMetadataError,
enqueueErrorSnackBar,
],
);
const updateViewGroups = useCallback(
async (
updateCoreViewGroupInputs: UpdateCoreViewGroupMutationVariables[],
@@ -204,14 +204,12 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
fieldMetadataId: kanbanFieldMetadataId,
} satisfies ViewGroup);
const groupResult = await createViewGroups(
viewGroupsToCreate.map(({ __typename, ...viewGroup }) => ({
input: {
...viewGroup,
viewId: newViewId,
},
const groupResult = await createViewGroups({
inputs: viewGroupsToCreate.map(({ __typename, ...viewGroup }) => ({
...viewGroup,
viewId: newViewId,
})),
);
});
if (groupResult.status === 'failed') {
set(isPersistingViewFieldsState, false);
@@ -146,14 +146,12 @@ export const useSaveCurrentViewGroups = () => {
);
await Promise.all([
createViewGroups(
viewGroupsToCreate.map(({ __typename, ...viewGroup }) => ({
input: {
...viewGroup,
viewId: view.id,
},
createViewGroups({
inputs: viewGroupsToCreate.map(({ __typename, ...viewGroup }) => ({
...viewGroup,
viewId: view.id,
})),
),
}),
updateViewGroups(viewGroupsToUpdate),
]);
},