From 8dd43be9a2c5780ef94960ff7fbb97dbc8f07a3d Mon Sep 17 00:00:00 2001
From: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
Date: Tue, 4 Nov 2025 15:17:55 +0100
Subject: [PATCH] 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
### v1
---
packages/twenty-front/jest.config.mjs | 2 +-
.../src/generated-metadata/graphql.ts | 46 +++
.../twenty-front/src/generated/graphql.ts | 46 +++
.../useSetViewTypeFromLayoutOptionsMenu.ts | 12 +-
.../hooks/useHandleRecordGroupField.ts | 12 +-
.../mutations/createManyCoreViewGroups.ts | 11 +
.../hooks/internal/usePersistViewGroup.ts | 86 +++++-
.../hooks/useCreateViewFromCurrentView.ts | 12 +-
.../views/hooks/useSaveCurrentViewGroups.ts | 12 +-
.../resolvers/view-group.resolver.ts | 29 ++
.../services/view-group-v2.service.ts | 51 +++-
...ny-view-groups-v2.integration-spec.ts.snap | 94 +++++++
...te-many-view-groups-v2.integration-spec.ts | 152 ++++++++++
...te-many-view-groups-v2.integration-spec.ts | 265 ++++++++++++++++++
...any-core-view-groups-query-factory.util.ts | 23 ++
.../create-many-core-view-groups.util.ts | 43 +++
.../view-group.integration-spec.ts.snap | 2 +-
17 files changed, 857 insertions(+), 41 deletions(-)
create mode 100644 packages/twenty-front/src/modules/views/graphql/mutations/createManyCoreViewGroups.ts
create mode 100644 packages/twenty-server/test/integration/graphql/suites/view/view-group/__snapshots__/failing-create-many-view-groups-v2.integration-spec.ts.snap
create mode 100644 packages/twenty-server/test/integration/graphql/suites/view/view-group/failing-create-many-view-groups-v2.integration-spec.ts
create mode 100644 packages/twenty-server/test/integration/graphql/suites/view/view-group/successful-create-many-view-groups-v2.integration-spec.ts
create mode 100644 packages/twenty-server/test/integration/metadata/suites/view-group/utils/create-many-core-view-groups-query-factory.util.ts
create mode 100644 packages/twenty-server/test/integration/metadata/suites/view-group/utils/create-many-core-view-groups.util.ts
diff --git a/packages/twenty-front/jest.config.mjs b/packages/twenty-front/jest.config.mjs
index 48a251a68e..e37fd1dde0 100644
--- a/packages/twenty-front/jest.config.mjs
+++ b/packages/twenty-front/jest.config.mjs
@@ -63,7 +63,7 @@ const jestConfig = {
global: {
statements: 52,
// Temporarily decreasing to 50.97 as introduced v1 code that aims to be deleted
- lines: 50.97,
+ lines: 50.95,
functions: 41,
},
},
diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts
index 98d130d97d..535c59768f 100644
--- a/packages/twenty-front/src/generated-metadata/graphql.ts
+++ b/packages/twenty-front/src/generated-metadata/graphql.ts
@@ -1758,6 +1758,7 @@ export type Mutation = {
createEmailingDomain: EmailingDomain;
createFile: File;
createManyCoreViewFields: Array;
+ createManyCoreViewGroups: Array;
createOIDCIdentityProvider: SetupSsoOutput;
createObjectEvent: Analytics;
createOneAgent: Agent;
@@ -2036,6 +2037,11 @@ export type MutationCreateManyCoreViewFieldsArgs = {
};
+export type MutationCreateManyCoreViewGroupsArgs = {
+ inputs: Array;
+};
+
+
export type MutationCreateOidcIdentityProviderArgs = {
input: SetupOidcSsoInput;
};
@@ -5979,6 +5985,13 @@ export type CreateManyCoreViewFieldsMutationVariables = Exact<{
export type CreateManyCoreViewFieldsMutation = { __typename?: 'Mutation', createManyCoreViewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }> };
+export type CreateManyCoreViewGroupsMutationVariables = Exact<{
+ inputs: Array | CreateViewGroupInput;
+}>;
+
+
+export type CreateManyCoreViewGroupsMutation = { __typename?: 'Mutation', createManyCoreViewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> };
+
export type DeleteCoreViewMutationVariables = Exact<{
id: Scalars['String'];
}>;
@@ -12668,6 +12681,39 @@ export function useCreateManyCoreViewFieldsMutation(baseOptions?: Apollo.Mutatio
export type CreateManyCoreViewFieldsMutationHookResult = ReturnType;
export type CreateManyCoreViewFieldsMutationResult = Apollo.MutationResult;
export type CreateManyCoreViewFieldsMutationOptions = Apollo.BaseMutationOptions;
+export const CreateManyCoreViewGroupsDocument = gql`
+ mutation CreateManyCoreViewGroups($inputs: [CreateViewGroupInput!]!) {
+ createManyCoreViewGroups(inputs: $inputs) {
+ ...ViewGroupFragment
+ }
+}
+ ${ViewGroupFragmentFragmentDoc}`;
+export type CreateManyCoreViewGroupsMutationFn = Apollo.MutationFunction;
+
+/**
+ * __useCreateManyCoreViewGroupsMutation__
+ *
+ * To run a mutation, you first call `useCreateManyCoreViewGroupsMutation` within a React component and pass it any options that fit your needs.
+ * When your component renders, `useCreateManyCoreViewGroupsMutation` returns a tuple that includes:
+ * - A mutate function that you can call at any time to execute the mutation
+ * - An object with fields that represent the current status of the mutation's execution
+ *
+ * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
+ *
+ * @example
+ * const [createManyCoreViewGroupsMutation, { data, loading, error }] = useCreateManyCoreViewGroupsMutation({
+ * variables: {
+ * inputs: // value for 'inputs'
+ * },
+ * });
+ */
+export function useCreateManyCoreViewGroupsMutation(baseOptions?: Apollo.MutationHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useMutation(CreateManyCoreViewGroupsDocument, options);
+ }
+export type CreateManyCoreViewGroupsMutationHookResult = ReturnType;
+export type CreateManyCoreViewGroupsMutationResult = Apollo.MutationResult;
+export type CreateManyCoreViewGroupsMutationOptions = Apollo.BaseMutationOptions;
export const DeleteCoreViewDocument = gql`
mutation DeleteCoreView($id: String!) {
deleteCoreView(id: $id)
diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts
index 148fdffe15..9cbd9a2caf 100644
--- a/packages/twenty-front/src/generated/graphql.ts
+++ b/packages/twenty-front/src/generated/graphql.ts
@@ -1715,6 +1715,7 @@ export type Mutation = {
createEmailingDomain: EmailingDomain;
createFile: File;
createManyCoreViewFields: Array;
+ createManyCoreViewGroups: Array;
createOIDCIdentityProvider: SetupSsoOutput;
createObjectEvent: Analytics;
createOneAgent: Agent;
@@ -1987,6 +1988,11 @@ export type MutationCreateManyCoreViewFieldsArgs = {
};
+export type MutationCreateManyCoreViewGroupsArgs = {
+ inputs: Array;
+};
+
+
export type MutationCreateOidcIdentityProviderArgs = {
input: SetupOidcSsoInput;
};
@@ -4789,6 +4795,13 @@ export type CreateManyCoreViewFieldsMutationVariables = Exact<{
export type CreateManyCoreViewFieldsMutation = { __typename?: 'Mutation', createManyCoreViewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }> };
+export type CreateManyCoreViewGroupsMutationVariables = Exact<{
+ inputs: Array | CreateViewGroupInput;
+}>;
+
+
+export type CreateManyCoreViewGroupsMutation = { __typename?: 'Mutation', createManyCoreViewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }> };
+
export type DeleteCoreViewMutationVariables = Exact<{
id: Scalars['String'];
}>;
@@ -5588,6 +5601,39 @@ export function useCreateManyCoreViewFieldsMutation(baseOptions?: Apollo.Mutatio
export type CreateManyCoreViewFieldsMutationHookResult = ReturnType;
export type CreateManyCoreViewFieldsMutationResult = Apollo.MutationResult;
export type CreateManyCoreViewFieldsMutationOptions = Apollo.BaseMutationOptions;
+export const CreateManyCoreViewGroupsDocument = gql`
+ mutation CreateManyCoreViewGroups($inputs: [CreateViewGroupInput!]!) {
+ createManyCoreViewGroups(inputs: $inputs) {
+ ...ViewGroupFragment
+ }
+}
+ ${ViewGroupFragmentFragmentDoc}`;
+export type CreateManyCoreViewGroupsMutationFn = Apollo.MutationFunction;
+
+/**
+ * __useCreateManyCoreViewGroupsMutation__
+ *
+ * To run a mutation, you first call `useCreateManyCoreViewGroupsMutation` within a React component and pass it any options that fit your needs.
+ * When your component renders, `useCreateManyCoreViewGroupsMutation` returns a tuple that includes:
+ * - A mutate function that you can call at any time to execute the mutation
+ * - An object with fields that represent the current status of the mutation's execution
+ *
+ * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
+ *
+ * @example
+ * const [createManyCoreViewGroupsMutation, { data, loading, error }] = useCreateManyCoreViewGroupsMutation({
+ * variables: {
+ * inputs: // value for 'inputs'
+ * },
+ * });
+ */
+export function useCreateManyCoreViewGroupsMutation(baseOptions?: Apollo.MutationHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useMutation(CreateManyCoreViewGroupsDocument, options);
+ }
+export type CreateManyCoreViewGroupsMutationHookResult = ReturnType;
+export type CreateManyCoreViewGroupsMutationResult = Apollo.MutationResult;
+export type CreateManyCoreViewGroupsMutationOptions = Apollo.BaseMutationOptions;
export const DeleteCoreViewDocument = gql`
mutation DeleteCoreView($id: String!) {
deleteCoreView(id: $id)
diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts
index 835ba24afa..4cee83a213 100644
--- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts
+++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts
@@ -58,14 +58,12 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => {
fieldMetadataId: randomFieldForKanban,
} satisfies ViewGroup);
- await createViewGroups(
- viewGroupsToCreate.map(({ __typename, ...viewGroup }) => ({
- input: {
- ...viewGroup,
- viewId: currentViewId,
- },
+ await createViewGroups({
+ inputs: viewGroupsToCreate.map(({ __typename, ...viewGroup }) => ({
+ ...viewGroup,
+ viewId: currentViewId,
})),
- );
+ });
return viewGroupsToCreate;
},
diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts
index 4f59f67fc9..6c510530a2 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts
+++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts
@@ -105,14 +105,12 @@ export const useHandleRecordGroupField = () => {
);
if (viewGroupsToCreate.length > 0) {
- await createViewGroups(
- viewGroupsToCreate.map(({ __typename, ...viewGroup }) => ({
- input: {
- ...viewGroup,
- viewId: view.id,
- },
+ await createViewGroups({
+ inputs: viewGroupsToCreate.map(({ __typename, ...viewGroup }) => ({
+ ...viewGroup,
+ viewId: view.id,
})),
- );
+ });
}
if (viewGroupsToDelete.length > 0) {
diff --git a/packages/twenty-front/src/modules/views/graphql/mutations/createManyCoreViewGroups.ts b/packages/twenty-front/src/modules/views/graphql/mutations/createManyCoreViewGroups.ts
new file mode 100644
index 0000000000..5c25116765
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/graphql/mutations/createManyCoreViewGroups.ts
@@ -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
+ }
+ }
+`;
diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroup.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroup.ts
index f2528af025..13919b7fb1 100644
--- a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroup.ts
+++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroup.ts
@@ -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
+ > | null>
+ | MetadataRequestResult<
+ Awaited>[]
+ >
+ > => {
+ 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[],
diff --git a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts
index fd471348e6..349403fe11 100644
--- a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts
+++ b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts
@@ -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);
diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts
index 07e1eccea9..042ba9bc1d 100644
--- a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts
+++ b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts
@@ -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),
]);
},
diff --git a/packages/twenty-server/src/engine/metadata-modules/view-group/resolvers/view-group.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/view-group/resolvers/view-group.resolver.ts
index 5b7ee80ed9..56090f2ac8 100644
--- a/packages/twenty-server/src/engine/metadata-modules/view-group/resolvers/view-group.resolver.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/view-group/resolvers/view-group.resolver.ts
@@ -11,6 +11,10 @@ import { DeleteViewGroupInput } from 'src/engine/metadata-modules/view-group/dto
import { DestroyViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/destroy-view-group.input';
import { UpdateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/update-view-group.input';
import { ViewGroupDTO } from 'src/engine/metadata-modules/view-group/dtos/view-group.dto';
+import {
+ ViewGroupException,
+ ViewGroupExceptionCode,
+} from 'src/engine/metadata-modules/view-group/exceptions/view-group.exception';
import { ViewGroupV2Service } from 'src/engine/metadata-modules/view-group/services/view-group-v2.service';
import { ViewGroupService } from 'src/engine/metadata-modules/view-group/services/view-group.service';
import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/utils/view-graphql-api-exception.filter';
@@ -70,6 +74,31 @@ export class ViewGroupResolver {
});
}
+ @Mutation(() => [ViewGroupDTO])
+ async createManyCoreViewGroups(
+ @Args('inputs', { type: () => [CreateViewGroupInput] })
+ createViewGroupInputs: CreateViewGroupInput[],
+ @AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
+ ): Promise {
+ const isWorkspaceMigrationV2Enabled =
+ await this.featureFlagService.isFeatureEnabled(
+ FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
+ workspaceId,
+ );
+
+ if (!isWorkspaceMigrationV2Enabled) {
+ throw new ViewGroupException(
+ 'Not implemented in v1, please active IS_WORKSPACE_MIGRATION_V2_ENABLED',
+ ViewGroupExceptionCode.INVALID_VIEW_GROUP_DATA,
+ );
+ }
+
+ return await this.viewGroupV2Service.createMany({
+ createViewGroupInputs,
+ workspaceId,
+ });
+ }
+
@Mutation(() => ViewGroupDTO)
async updateCoreViewGroup(
@Args('input') updateViewGroupInput: UpdateViewGroupInput,
diff --git a/packages/twenty-server/src/engine/metadata-modules/view-group/services/view-group-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-group/services/view-group-v2.service.ts
index d904eb0e6f..188a16e8d1 100644
--- a/packages/twenty-server/src/engine/metadata-modules/view-group/services/view-group-v2.service.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/view-group/services/view-group-v2.service.ts
@@ -5,6 +5,7 @@ import { isDefined } from 'twenty-shared/utils';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
+import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { fromCreateViewGroupInputToFlatViewGroupToCreate } from 'src/engine/metadata-modules/flat-view-group/utils/from-create-view-group-input-to-flat-view-group-to-create.util';
import { fromDeleteViewGroupInputToFlatViewGroupOrThrow } from 'src/engine/metadata-modules/flat-view-group/utils/from-delete-view-group-input-to-flat-view-group-or-throw.util';
import { fromDestroyViewGroupInputToFlatViewGroupOrThrow } from 'src/engine/metadata-modules/flat-view-group/utils/from-destroy-view-group-input-to-flat-view-group-or-throw.util';
@@ -14,6 +15,10 @@ import { DeleteViewGroupInput } from 'src/engine/metadata-modules/view-group/dto
import { DestroyViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/destroy-view-group.input';
import { UpdateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/update-view-group.input';
import { ViewGroupDTO } from 'src/engine/metadata-modules/view-group/dtos/view-group.dto';
+import {
+ ViewGroupException,
+ ViewGroupExceptionCode,
+} from 'src/engine/metadata-modules/view-group/exceptions/view-group.exception';
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
@@ -31,6 +36,32 @@ export class ViewGroupV2Service {
createViewGroupInput: CreateViewGroupInput;
workspaceId: string;
}): Promise {
+ const [createdViewGroup] = await this.createMany({
+ workspaceId,
+ createViewGroupInputs: [createViewGroupInput],
+ });
+
+ if (!isDefined(createdViewGroup)) {
+ throw new ViewGroupException(
+ 'Failed to create view group',
+ ViewGroupExceptionCode.INVALID_VIEW_GROUP_DATA,
+ );
+ }
+
+ return createdViewGroup;
+ }
+
+ async createMany({
+ createViewGroupInputs,
+ workspaceId,
+ }: {
+ createViewGroupInputs: CreateViewGroupInput[];
+ workspaceId: string;
+ }): Promise {
+ if (createViewGroupInputs.length === 0) {
+ return [];
+ }
+
const {
flatViewGroupMaps: existingFlatViewGroupMaps,
flatViewMaps,
@@ -46,11 +77,13 @@ export class ViewGroupV2Service {
},
);
- const flatViewGroupToCreate =
- fromCreateViewGroupInputToFlatViewGroupToCreate({
- createViewGroupInput,
- workspaceId,
- });
+ const flatViewGroupsToCreate = createViewGroupInputs.map(
+ (createViewGroupInput) =>
+ fromCreateViewGroupInputToFlatViewGroupToCreate({
+ createViewGroupInput,
+ workspaceId,
+ }),
+ );
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
@@ -58,7 +91,7 @@ export class ViewGroupV2Service {
fromToAllFlatEntityMaps: {
flatViewGroupMaps: computeFlatEntityMapsFromTo({
flatEntityMaps: existingFlatViewGroupMaps,
- flatEntityToCreate: [flatViewGroupToCreate],
+ flatEntityToCreate: flatViewGroupsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
}),
@@ -77,7 +110,7 @@ export class ViewGroupV2Service {
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
- 'Multiple validation errors occurred while creating view group',
+ 'Multiple validation errors occurred while creating view groups',
);
}
@@ -89,8 +122,8 @@ export class ViewGroupV2Service {
},
);
- return findFlatEntityByIdInFlatEntityMapsOrThrow({
- flatEntityId: flatViewGroupToCreate.id,
+ return findManyFlatEntityByIdInFlatEntityMapsOrThrow({
+ flatEntityIds: flatViewGroupsToCreate.map((el) => el.id),
flatEntityMaps: recomputedExistingFlatViewGroupMaps,
});
}
diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-group/__snapshots__/failing-create-many-view-groups-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/view/view-group/__snapshots__/failing-create-many-view-groups-v2.integration-spec.ts.snap
new file mode 100644
index 0000000000..ddf83e9535
--- /dev/null
+++ b/packages/twenty-server/test/integration/graphql/suites/view/view-group/__snapshots__/failing-create-many-view-groups-v2.integration-spec.ts.snap
@@ -0,0 +1,94 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`View Group Resolver - Failing Create Many Operations - v2 should accumulate multiple validation errors when some inputs are invalid 1`] = `
+{
+ "extensions": {
+ "code": "METADATA_VALIDATION_FAILED",
+ "errors": {
+ "cronTrigger": [],
+ "databaseEventTrigger": [],
+ "fieldMetadata": [],
+ "index": [],
+ "objectMetadata": [],
+ "routeTrigger": [],
+ "serverlessFunction": [],
+ "view": [],
+ "viewField": [],
+ "viewFilter": [],
+ "viewGroup": [
+ {
+ "errors": [
+ {
+ "code": "INVALID_VIEW_DATA",
+ "message": "Field metadata not found",
+ "userFriendlyMessage": "Field metadata not found",
+ },
+ ],
+ "flatEntityMinimalInformation": {
+ "fieldMetadataId": Any,
+ "id": Any,
+ "viewId": Any,
+ },
+ "status": "fail",
+ "type": "create_view_group",
+ },
+ {
+ "errors": [
+ {
+ "code": "INVALID_VIEW_DATA",
+ "message": "View not found",
+ "userFriendlyMessage": "View not found",
+ },
+ ],
+ "flatEntityMinimalInformation": {
+ "fieldMetadataId": Any,
+ "id": Any,
+ "viewId": Any,
+ },
+ "status": "fail",
+ "type": "create_view_group",
+ },
+ {
+ "errors": [
+ {
+ "code": "INVALID_VIEW_DATA",
+ "message": "Field metadata not found",
+ "userFriendlyMessage": "Field metadata not found",
+ },
+ {
+ "code": "INVALID_VIEW_DATA",
+ "message": "View not found",
+ "userFriendlyMessage": "View not found",
+ },
+ ],
+ "flatEntityMinimalInformation": {
+ "fieldMetadataId": Any,
+ "id": Any,
+ "viewId": Any,
+ },
+ "status": "fail",
+ "type": "create_view_group",
+ },
+ ],
+ },
+ "message": "Validation failed for 0 object(s) and 0 field(s)",
+ "summary": {
+ "invalidCronTrigger": 0,
+ "invalidDatabaseEventTrigger": 0,
+ "invalidFieldMetadata": 0,
+ "invalidIndex": 0,
+ "invalidObjectMetadata": 0,
+ "invalidRouteTrigger": 0,
+ "invalidServerlessFunction": 0,
+ "invalidView": 0,
+ "invalidViewField": 0,
+ "invalidViewFilter": 0,
+ "invalidViewGroup": 0,
+ "totalErrors": 0,
+ },
+ "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
+ },
+ "message": "Multiple validation errors occurred while creating view groups",
+ "name": "GraphQLError",
+}
+`;
diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-group/failing-create-many-view-groups-v2.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-group/failing-create-many-view-groups-v2.integration-spec.ts
new file mode 100644
index 0000000000..5a817b5cf5
--- /dev/null
+++ b/packages/twenty-server/test/integration/graphql/suites/view/view-group/failing-create-many-view-groups-v2.integration-spec.ts
@@ -0,0 +1,152 @@
+import { createManyCoreViewGroups } from 'test/integration/metadata/suites/view-group/utils/create-many-core-view-groups.util';
+import { v4 as uuidv4 } from 'uuid';
+import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
+import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
+import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
+import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
+import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
+import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util';
+import { FieldMetadataType } from 'twenty-shared/types';
+
+import { type CreateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/create-view-group.input';
+
+describe('View Group Resolver - Failing Create Many Operations - v2', () => {
+ let testSetup: {
+ testViewId: string;
+ testObjectMetadataId: string;
+ firstTestFieldMetadataId: string;
+ secondTestFieldMetadataId: string;
+ };
+
+ beforeAll(async () => {
+ const {
+ data: {
+ createOneObject: { id: objectMetadataId },
+ },
+ } = await createOneObjectMetadata({
+ expectToFail: false,
+ input: {
+ nameSingular: 'myGroupTestObjectV2',
+ namePlural: 'myGroupTestObjectsV2',
+ labelSingular: 'My Group Test Object v2',
+ labelPlural: 'My Group Test Objects v2',
+ icon: 'Icon123',
+ },
+ });
+
+ const {
+ data: {
+ createOneField: { id: firstTestFieldMetadataId },
+ },
+ } = await createOneFieldMetadata({
+ expectToFail: false,
+ input: {
+ name: 'testField',
+ label: 'Test Field',
+ type: FieldMetadataType.TEXT,
+ objectMetadataId,
+ isLabelSyncedWithName: true,
+ },
+ gqlFields: `
+ id
+ name
+ label
+ isLabelSyncedWithName
+ `,
+ });
+
+ const {
+ data: {
+ createOneField: { id: secondTestFieldMetadataId },
+ },
+ } = await createOneFieldMetadata({
+ expectToFail: false,
+ input: {
+ name: 'secondTestField',
+ label: 'Test Field',
+ type: FieldMetadataType.TEXT,
+ objectMetadataId,
+ isLabelSyncedWithName: false,
+ },
+ gqlFields: `
+ id
+ name
+ label
+ isLabelSyncedWithName
+ `,
+ });
+
+ const {
+ data: {
+ createCoreView: { id: testViewId },
+ },
+ } = await createOneCoreView({
+ input: {
+ icon: 'icon123',
+ objectMetadataId,
+ name: 'TestViewForGroups',
+ },
+ expectToFail: false,
+ });
+
+ testSetup = {
+ testViewId,
+ testObjectMetadataId: objectMetadataId,
+ firstTestFieldMetadataId,
+ secondTestFieldMetadataId,
+ };
+ });
+
+ afterAll(async () => {
+ await updateOneObjectMetadata({
+ input: {
+ idToUpdate: testSetup.testObjectMetadataId,
+ updatePayload: {
+ isActive: false,
+ },
+ },
+ });
+ await deleteOneObjectMetadata({
+ expectToFail: false,
+ input: { idToDelete: testSetup.testObjectMetadataId },
+ });
+ });
+
+ it('should accumulate multiple validation errors when some inputs are invalid', async () => {
+ const invalidViewId = uuidv4();
+ const invalidFieldMetadataId = uuidv4();
+
+ const inputs: CreateViewGroupInput[] = [
+ {
+ fieldMetadataId: invalidFieldMetadataId,
+ viewId: testSetup.testViewId,
+ position: 0,
+ isVisible: true,
+ fieldValue: 'Invalid Group A',
+ },
+ {
+ fieldMetadataId: testSetup.firstTestFieldMetadataId,
+ viewId: invalidViewId,
+ position: 1,
+ isVisible: true,
+ fieldValue: 'Invalid Group B',
+ },
+ {
+ fieldMetadataId: invalidFieldMetadataId,
+ viewId: invalidViewId,
+ position: 2,
+ isVisible: true,
+ fieldValue: 'Invalid Group C',
+ },
+ ];
+
+ const { errors } = await createManyCoreViewGroups({
+ inputs,
+ expectToFail: true,
+ });
+
+ expectOneNotInternalServerErrorSnapshot({
+ errors,
+ });
+ });
+});
diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-group/successful-create-many-view-groups-v2.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-group/successful-create-many-view-groups-v2.integration-spec.ts
new file mode 100644
index 0000000000..fc9cf21149
--- /dev/null
+++ b/packages/twenty-server/test/integration/graphql/suites/view/view-group/successful-create-many-view-groups-v2.integration-spec.ts
@@ -0,0 +1,265 @@
+import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
+import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
+import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
+import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
+import { createManyCoreViewGroups } from 'test/integration/metadata/suites/view-group/utils/create-many-core-view-groups.util';
+import { deleteOneCoreViewGroup } from 'test/integration/metadata/suites/view-group/utils/delete-one-core-view-group.util';
+import { destroyOneCoreViewGroup } from 'test/integration/metadata/suites/view-group/utils/destroy-one-core-view-group.util';
+import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util';
+import { FieldMetadataType } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
+
+import { type CreateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/create-view-group.input';
+
+describe('View Group Resolver - Successful Create Many Operations - v2', () => {
+ let testSetup: {
+ testViewId: string;
+ testObjectMetadataId: string;
+ firstTestFieldMetadataId: string;
+ secondTestFieldMetadataId: string;
+ thirdTestFieldMetadataId: string;
+ };
+ let createdViewGroupIds: string[] = [];
+
+ beforeAll(async () => {
+ const {
+ data: {
+ createOneObject: { id: objectMetadataId },
+ },
+ } = await createOneObjectMetadata({
+ expectToFail: false,
+ input: {
+ nameSingular: 'myGroupTestObjectV2',
+ namePlural: 'myGroupTestObjectsV2',
+ labelSingular: 'My Group Test Object v2',
+ labelPlural: 'My Group Test Objects v2',
+ icon: 'Icon123',
+ },
+ });
+
+ const {
+ data: {
+ createOneField: { id: firstTestFieldMetadataId },
+ },
+ } = await createOneFieldMetadata({
+ expectToFail: false,
+ input: {
+ name: 'testField',
+ label: 'Test Field',
+ type: FieldMetadataType.TEXT,
+ objectMetadataId,
+ isLabelSyncedWithName: true,
+ },
+ gqlFields: `
+ id
+ name
+ label
+ isLabelSyncedWithName
+ `,
+ });
+
+ const {
+ data: {
+ createOneField: { id: secondTestFieldMetadataId },
+ },
+ } = await createOneFieldMetadata({
+ expectToFail: false,
+ input: {
+ name: 'secondTestField',
+ label: 'Test Field',
+ type: FieldMetadataType.TEXT,
+ objectMetadataId,
+ isLabelSyncedWithName: false,
+ },
+ gqlFields: `
+ id
+ name
+ label
+ isLabelSyncedWithName
+ `,
+ });
+
+ const {
+ data: {
+ createOneField: { id: thirdTestFieldMetadataId },
+ },
+ } = await createOneFieldMetadata({
+ expectToFail: false,
+ input: {
+ name: 'thirdTestField',
+ label: 'Test Field',
+ type: FieldMetadataType.TEXT,
+ objectMetadataId,
+ isLabelSyncedWithName: false,
+ },
+ gqlFields: `
+ id
+ name
+ label
+ isLabelSyncedWithName
+ `,
+ });
+
+ const {
+ data: {
+ createCoreView: { id: testViewId },
+ },
+ } = await createOneCoreView({
+ input: {
+ icon: 'icon123',
+ objectMetadataId,
+ name: 'TestViewForGroups',
+ },
+ expectToFail: false,
+ });
+
+ testSetup = {
+ testViewId,
+ testObjectMetadataId: objectMetadataId,
+ firstTestFieldMetadataId,
+ secondTestFieldMetadataId,
+ thirdTestFieldMetadataId,
+ };
+ });
+
+ afterAll(async () => {
+ await updateOneObjectMetadata({
+ input: {
+ idToUpdate: testSetup.testObjectMetadataId,
+ updatePayload: {
+ isActive: false,
+ },
+ },
+ });
+ await deleteOneObjectMetadata({
+ expectToFail: false,
+ input: { idToDelete: testSetup.testObjectMetadataId },
+ });
+ });
+
+ afterEach(async () => {
+ for (const viewGroupId of createdViewGroupIds) {
+ if (isDefined(viewGroupId)) {
+ const {
+ data: { deleteCoreViewGroup },
+ } = await deleteOneCoreViewGroup({
+ expectToFail: false,
+ input: {
+ id: viewGroupId,
+ },
+ });
+
+ expect(deleteCoreViewGroup.deletedAt).not.toBeNull();
+ await destroyOneCoreViewGroup({
+ expectToFail: false,
+ input: {
+ id: viewGroupId,
+ },
+ });
+ }
+ }
+ createdViewGroupIds = [];
+ });
+
+ it('should successfully create multiple view groups in batch', async () => {
+ const inputs: CreateViewGroupInput[] = [
+ {
+ fieldMetadataId: testSetup.firstTestFieldMetadataId,
+ viewId: testSetup.testViewId,
+ position: 0,
+ isVisible: true,
+ fieldValue: 'Group A',
+ },
+ {
+ fieldMetadataId: testSetup.secondTestFieldMetadataId,
+ viewId: testSetup.testViewId,
+ position: 1,
+ isVisible: false,
+ fieldValue: 'Group B',
+ },
+ {
+ fieldMetadataId: testSetup.thirdTestFieldMetadataId,
+ viewId: testSetup.testViewId,
+ position: 2,
+ isVisible: true,
+ fieldValue: 'Group C',
+ },
+ ];
+
+ const {
+ data: { createManyCoreViewGroups: createdViewGroups },
+ errors,
+ } = await createManyCoreViewGroups({
+ inputs,
+ expectToFail: false,
+ });
+
+ expect(errors).toBeUndefined();
+ expect(createdViewGroups).toBeDefined();
+ expect(createdViewGroups).toHaveLength(3);
+
+ createdViewGroups.forEach((viewGroup, index) => {
+ expect(viewGroup).toMatchObject({
+ fieldMetadataId: inputs[index].fieldMetadataId,
+ viewId: testSetup.testViewId,
+ position: inputs[index].position,
+ isVisible: inputs[index].isVisible,
+ fieldValue: inputs[index].fieldValue,
+ });
+
+ createdViewGroupIds.push(viewGroup.id);
+ });
+ });
+
+ it('should successfully create single view group using batch endpoint', async () => {
+ const inputs: CreateViewGroupInput[] = [
+ {
+ fieldMetadataId: testSetup.firstTestFieldMetadataId,
+ viewId: testSetup.testViewId,
+ position: 5,
+ isVisible: true,
+ fieldValue: 'Single Group',
+ },
+ ];
+
+ const {
+ data: { createManyCoreViewGroups: createdViewGroups },
+ errors,
+ } = await createManyCoreViewGroups({
+ inputs,
+ expectToFail: false,
+ });
+
+ expect(errors).toBeUndefined();
+ expect(createdViewGroups).toBeDefined();
+ expect(createdViewGroups).toHaveLength(1);
+
+ const viewGroup = createdViewGroups[0];
+
+ expect(viewGroup).toMatchObject({
+ fieldMetadataId: testSetup.firstTestFieldMetadataId,
+ viewId: testSetup.testViewId,
+ position: 5,
+ isVisible: true,
+ fieldValue: 'Single Group',
+ });
+
+ createdViewGroupIds.push(viewGroup.id);
+ });
+
+ it('should return empty array when creating zero view groups', async () => {
+ const inputs: CreateViewGroupInput[] = [];
+
+ const {
+ data: { createManyCoreViewGroups: createdViewGroups },
+ errors,
+ } = await createManyCoreViewGroups({
+ inputs,
+ expectToFail: false,
+ });
+
+ expect(errors).toBeUndefined();
+ expect(createdViewGroups).toBeDefined();
+ expect(createdViewGroups).toHaveLength(0);
+ });
+});
diff --git a/packages/twenty-server/test/integration/metadata/suites/view-group/utils/create-many-core-view-groups-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-group/utils/create-many-core-view-groups-query-factory.util.ts
new file mode 100644
index 0000000000..730ea7b6de
--- /dev/null
+++ b/packages/twenty-server/test/integration/metadata/suites/view-group/utils/create-many-core-view-groups-query-factory.util.ts
@@ -0,0 +1,23 @@
+import gql from 'graphql-tag';
+import { VIEW_GROUP_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
+
+import { type CreateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/create-view-group.input';
+
+export const createManyCoreViewGroupsQueryFactory = ({
+ gqlFields = VIEW_GROUP_GQL_FIELDS,
+ inputs,
+}: {
+ gqlFields?: string;
+ inputs: CreateViewGroupInput[];
+}) => ({
+ query: gql`
+ mutation CreateManyCoreViewGroups($inputs: [CreateViewGroupInput!]!) {
+ createManyCoreViewGroups(inputs: $inputs) {
+ ${gqlFields}
+ }
+ }
+ `,
+ variables: {
+ inputs,
+ },
+});
diff --git a/packages/twenty-server/test/integration/metadata/suites/view-group/utils/create-many-core-view-groups.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-group/utils/create-many-core-view-groups.util.ts
new file mode 100644
index 0000000000..2a3ca804e3
--- /dev/null
+++ b/packages/twenty-server/test/integration/metadata/suites/view-group/utils/create-many-core-view-groups.util.ts
@@ -0,0 +1,43 @@
+import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
+import { createManyCoreViewGroupsQueryFactory } from 'test/integration/metadata/suites/view-group/utils/create-many-core-view-groups-query-factory.util';
+import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
+import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
+import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
+
+import { type CreateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/create-view-group.input';
+import { type ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
+
+export const createManyCoreViewGroups = async ({
+ inputs,
+ gqlFields,
+ expectToFail,
+}: {
+ inputs: CreateViewGroupInput[];
+ gqlFields?: string;
+ expectToFail?: boolean;
+}): CommonResponseBody<{
+ createManyCoreViewGroups: ViewGroupEntity[];
+}> => {
+ const graphqlOperation = createManyCoreViewGroupsQueryFactory({
+ inputs,
+ gqlFields,
+ });
+
+ const response = await makeMetadataAPIRequest(graphqlOperation);
+
+ if (expectToFail === true) {
+ warnIfNoErrorButExpectedToFail({
+ response,
+ errorMessage: 'View Groups batch creation should have failed but did not',
+ });
+ }
+
+ if (expectToFail === false) {
+ warnIfErrorButNotExpectedToFail({
+ response,
+ errorMessage: 'View Groups batch creation has failed but should not',
+ });
+ }
+
+ return { data: response.body.data, errors: response.body.errors };
+};
diff --git a/packages/twenty-server/test/integration/rest/suites/__snapshots__/view-group.integration-spec.ts.snap b/packages/twenty-server/test/integration/rest/suites/__snapshots__/view-group.integration-spec.ts.snap
index 6e3cc69a0a..e181b6bac8 100644
--- a/packages/twenty-server/test/integration/rest/suites/__snapshots__/view-group.integration-spec.ts.snap
+++ b/packages/twenty-server/test/integration/rest/suites/__snapshots__/view-group.integration-spec.ts.snap
@@ -32,7 +32,7 @@ exports[`View Group REST API POST /metadata/viewGroups should fail to create vie
},
],
},
- "message": "Multiple validation errors occurred while creating view group",
+ "message": "Multiple validation errors occurred while creating view groups",
"statusCode": 400,
"summary": {
"invalidCronTrigger": 0,