diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 21714aafe0..744a9655af 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -3395,8 +3395,11 @@ input UsageAnalyticsInput { type Mutation { addQueryToEventStream(input: AddQuerySubscriptionInput!): Boolean! removeQueryFromEventStream(input: RemoveQueryFromEventStreamInput!): Boolean! + createManyNavigationMenuItems(inputs: [CreateNavigationMenuItemInput!]!): [NavigationMenuItem!]! createNavigationMenuItem(input: CreateNavigationMenuItemInput!): NavigationMenuItem! + updateManyNavigationMenuItems(inputs: [UpdateOneNavigationMenuItemInput!]!): [NavigationMenuItem!]! updateNavigationMenuItem(input: UpdateOneNavigationMenuItemInput!): NavigationMenuItem! + deleteManyNavigationMenuItems(ids: [UUID!]!): [NavigationMenuItem!]! deleteNavigationMenuItem(id: UUID!): NavigationMenuItem! uploadAIChatFile(file: Upload!): FileWithSignedUrl! uploadWorkflowFile(file: Upload!): FileWithSignedUrl! diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index 73ad7970dd..223aba8d0b 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -2849,8 +2849,11 @@ export type EventLogTable = 'WORKSPACE_EVENT' | 'PAGEVIEW' | 'OBJECT_EVENT' | 'U export interface Mutation { addQueryToEventStream: Scalars['Boolean'] removeQueryFromEventStream: Scalars['Boolean'] + createManyNavigationMenuItems: NavigationMenuItem[] createNavigationMenuItem: NavigationMenuItem + updateManyNavigationMenuItems: NavigationMenuItem[] updateNavigationMenuItem: NavigationMenuItem + deleteManyNavigationMenuItems: NavigationMenuItem[] deleteNavigationMenuItem: NavigationMenuItem uploadAIChatFile: FileWithSignedUrl uploadWorkflowFile: FileWithSignedUrl @@ -6099,8 +6102,11 @@ export interface UsageAnalyticsInput {periodStart?: (Scalars['DateTime'] | null) export interface MutationGenqlSelection{ addQueryToEventStream?: { __args: {input: AddQuerySubscriptionInput} } removeQueryFromEventStream?: { __args: {input: RemoveQueryFromEventStreamInput} } + createManyNavigationMenuItems?: (NavigationMenuItemGenqlSelection & { __args: {inputs: CreateNavigationMenuItemInput[]} }) createNavigationMenuItem?: (NavigationMenuItemGenqlSelection & { __args: {input: CreateNavigationMenuItemInput} }) + updateManyNavigationMenuItems?: (NavigationMenuItemGenqlSelection & { __args: {inputs: UpdateOneNavigationMenuItemInput[]} }) updateNavigationMenuItem?: (NavigationMenuItemGenqlSelection & { __args: {input: UpdateOneNavigationMenuItemInput} }) + deleteManyNavigationMenuItems?: (NavigationMenuItemGenqlSelection & { __args: {ids: Scalars['UUID'][]} }) deleteNavigationMenuItem?: (NavigationMenuItemGenqlSelection & { __args: {id: Scalars['UUID']} }) uploadAIChatFile?: (FileWithSignedUrlGenqlSelection & { __args: {file: Scalars['Upload']} }) uploadWorkflowFile?: (FileWithSignedUrlGenqlSelection & { __args: {file: Scalars['Upload']} }) diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts index 5b8d32848c..2eacb10116 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/types.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts @@ -7161,6 +7161,15 @@ export default { ] } ], + "createManyNavigationMenuItems": [ + 145, + { + "inputs": [ + 369, + "[CreateNavigationMenuItemInput!]!" + ] + } + ], "createNavigationMenuItem": [ 145, { @@ -7170,6 +7179,15 @@ export default { ] } ], + "updateManyNavigationMenuItems": [ + 145, + { + "inputs": [ + 370, + "[UpdateOneNavigationMenuItemInput!]!" + ] + } + ], "updateNavigationMenuItem": [ 145, { @@ -7179,6 +7197,15 @@ export default { ] } ], + "deleteManyNavigationMenuItems": [ + 145, + { + "ids": [ + 3, + "[UUID!]!" + ] + } + ], "deleteNavigationMenuItem": [ 145, { diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 9d217b7ebf..101dee58dd 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -2569,6 +2569,7 @@ export type Mutation = { createDevelopmentApplication: DevelopmentApplication; createEmailingDomain: EmailingDomain; createFrontComponent: FrontComponent; + createManyNavigationMenuItems: Array; createManyViewFieldGroups: Array; createManyViewFields: Array; createManyViewGroups: Array; @@ -2606,6 +2607,7 @@ export type Mutation = { deleteEmailingDomain: Scalars['Boolean']; deleteFrontComponent: FrontComponent; deleteJobs: DeleteJobsResponse; + deleteManyNavigationMenuItems: Array; deleteNavigationMenuItem: NavigationMenuItem; deleteOneAgent: Agent; deleteOneField: Field; @@ -2699,6 +2701,7 @@ export type Mutation = { updateDatabaseConfigVariable: Scalars['Boolean']; updateFrontComponent: FrontComponent; updateLabPublicFeatureFlag: FeatureFlag; + updateManyNavigationMenuItems: Array; updateMessageChannel: MessageChannel; updateMessageFolder: MessageFolder; updateMessageFolders: Array; @@ -2859,6 +2862,11 @@ export type MutationCreateFrontComponentArgs = { }; +export type MutationCreateManyNavigationMenuItemsArgs = { + inputs: Array; +}; + + export type MutationCreateManyViewFieldGroupsArgs = { inputs: Array; }; @@ -3043,6 +3051,11 @@ export type MutationDeleteJobsArgs = { }; +export type MutationDeleteManyNavigationMenuItemsArgs = { + ids: Array; +}; + + export type MutationDeleteNavigationMenuItemArgs = { id: Scalars['UUID']; }; @@ -3494,6 +3507,11 @@ export type MutationUpdateLabPublicFeatureFlagArgs = { }; +export type MutationUpdateManyNavigationMenuItemsArgs = { + inputs: Array; +}; + + export type MutationUpdateMessageChannelArgs = { input: UpdateMessageChannelInput; }; @@ -6734,26 +6752,26 @@ export type NavigationMenuItemFieldsFragment = { __typename?: 'NavigationMenuIte export type NavigationMenuItemQueryFieldsFragment = { __typename?: 'NavigationMenuItem', id: string, type: NavigationMenuItemType, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, color?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null }; -export type CreateNavigationMenuItemMutationVariables = Exact<{ - input: CreateNavigationMenuItemInput; +export type CreateManyNavigationMenuItemsMutationVariables = Exact<{ + inputs: Array | CreateNavigationMenuItemInput; }>; -export type CreateNavigationMenuItemMutation = { __typename?: 'Mutation', createNavigationMenuItem: { __typename?: 'NavigationMenuItem', id: string, type: NavigationMenuItemType, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, color?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null } }; +export type CreateManyNavigationMenuItemsMutation = { __typename?: 'Mutation', createManyNavigationMenuItems: Array<{ __typename?: 'NavigationMenuItem', id: string, type: NavigationMenuItemType, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, color?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null }> }; -export type DeleteNavigationMenuItemMutationVariables = Exact<{ - id: Scalars['UUID']; +export type DeleteManyNavigationMenuItemsMutationVariables = Exact<{ + ids: Array | Scalars['UUID']; }>; -export type DeleteNavigationMenuItemMutation = { __typename?: 'Mutation', deleteNavigationMenuItem: { __typename?: 'NavigationMenuItem', id: string, type: NavigationMenuItemType, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, color?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null } }; +export type DeleteManyNavigationMenuItemsMutation = { __typename?: 'Mutation', deleteManyNavigationMenuItems: Array<{ __typename?: 'NavigationMenuItem', id: string, type: NavigationMenuItemType, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, color?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null }> }; -export type UpdateNavigationMenuItemMutationVariables = Exact<{ - input: UpdateOneNavigationMenuItemInput; +export type UpdateManyNavigationMenuItemsMutationVariables = Exact<{ + inputs: Array | UpdateOneNavigationMenuItemInput; }>; -export type UpdateNavigationMenuItemMutation = { __typename?: 'Mutation', updateNavigationMenuItem: { __typename?: 'NavigationMenuItem', id: string, type: NavigationMenuItemType, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, color?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null } }; +export type UpdateManyNavigationMenuItemsMutation = { __typename?: 'Mutation', updateManyNavigationMenuItems: Array<{ __typename?: 'NavigationMenuItem', id: string, type: NavigationMenuItemType, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, color?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null }> }; export type FindManyNavigationMenuItemsQueryVariables = Exact<{ [key: string]: never; }>; @@ -8185,9 +8203,9 @@ export const InstallMarketplaceAppDocument = {"kind":"Document","definitions":[{ export const UpgradeApplicationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpgradeApplication"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appRegistrationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"targetVersion"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"upgradeApplication"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appRegistrationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appRegistrationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"targetVersion"},"value":{"kind":"Variable","name":{"kind":"Name","value":"targetVersion"}}}]}]}}]} as unknown as DocumentNode; export const FindManyMarketplaceAppsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyMarketplaceApps"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findManyMarketplaceApps"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"MarketplaceAppFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MarketplaceAppFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"MarketplaceApp"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"author"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"screenshots"}},{"kind":"Field","name":{"kind":"Name","value":"aboutDescription"}},{"kind":"Field","name":{"kind":"Name","value":"providers"}},{"kind":"Field","name":{"kind":"Name","value":"websiteUrl"}},{"kind":"Field","name":{"kind":"Name","value":"termsUrl"}},{"kind":"Field","name":{"kind":"Name","value":"objects"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"fields"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"fields"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"objectUniversalIdentifier"}}]}},{"kind":"Field","name":{"kind":"Name","value":"logicFunctions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"timeoutSeconds"}}]}},{"kind":"Field","name":{"kind":"Name","value":"frontComponents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"isFeatured"}},{"kind":"Field","name":{"kind":"Name","value":"defaultRole"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"canReadAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canSoftDeleteAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canDestroyAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateAllSettings"}},{"kind":"Field","name":{"kind":"Name","value":"canAccessAllTools"}},{"kind":"Field","name":{"kind":"Name","value":"objectPermissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objectUniversalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"canReadObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canSoftDeleteObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canDestroyObjectRecords"}}]}},{"kind":"Field","name":{"kind":"Name","value":"fieldPermissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objectUniversalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"fieldUniversalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"canReadFieldValue"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateFieldValue"}}]}},{"kind":"Field","name":{"kind":"Name","value":"permissionFlags"}}]}}]}}]} as unknown as DocumentNode; export const FindOneMarketplaceAppDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneMarketplaceApp"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findOneMarketplaceApp"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"universalIdentifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"MarketplaceAppFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MarketplaceAppFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"MarketplaceApp"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"author"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"screenshots"}},{"kind":"Field","name":{"kind":"Name","value":"aboutDescription"}},{"kind":"Field","name":{"kind":"Name","value":"providers"}},{"kind":"Field","name":{"kind":"Name","value":"websiteUrl"}},{"kind":"Field","name":{"kind":"Name","value":"termsUrl"}},{"kind":"Field","name":{"kind":"Name","value":"objects"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"fields"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"fields"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"objectUniversalIdentifier"}}]}},{"kind":"Field","name":{"kind":"Name","value":"logicFunctions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"timeoutSeconds"}}]}},{"kind":"Field","name":{"kind":"Name","value":"frontComponents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"isFeatured"}},{"kind":"Field","name":{"kind":"Name","value":"defaultRole"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"canReadAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canSoftDeleteAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canDestroyAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateAllSettings"}},{"kind":"Field","name":{"kind":"Name","value":"canAccessAllTools"}},{"kind":"Field","name":{"kind":"Name","value":"objectPermissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objectUniversalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"canReadObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canSoftDeleteObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canDestroyObjectRecords"}}]}},{"kind":"Field","name":{"kind":"Name","value":"fieldPermissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objectUniversalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"fieldUniversalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"canReadFieldValue"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateFieldValue"}}]}},{"kind":"Field","name":{"kind":"Name","value":"permissionFlags"}}]}}]}}]} as unknown as DocumentNode; -export const CreateNavigationMenuItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateNavigationMenuItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateNavigationMenuItemInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createNavigationMenuItem"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NavigationMenuItemQueryFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NavigationMenuItemFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NavigationMenuItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"targetRecordId"}},{"kind":"Field","name":{"kind":"Name","value":"targetObjectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"viewId"}},{"kind":"Field","name":{"kind":"Name","value":"folderId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NavigationMenuItemQueryFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NavigationMenuItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NavigationMenuItemFields"}},{"kind":"Field","name":{"kind":"Name","value":"targetRecordIdentifier"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifier"}}]}}]}}]} as unknown as DocumentNode; -export const DeleteNavigationMenuItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteNavigationMenuItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteNavigationMenuItem"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NavigationMenuItemQueryFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NavigationMenuItemFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NavigationMenuItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"targetRecordId"}},{"kind":"Field","name":{"kind":"Name","value":"targetObjectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"viewId"}},{"kind":"Field","name":{"kind":"Name","value":"folderId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NavigationMenuItemQueryFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NavigationMenuItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NavigationMenuItemFields"}},{"kind":"Field","name":{"kind":"Name","value":"targetRecordIdentifier"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifier"}}]}}]}}]} as unknown as DocumentNode; -export const UpdateNavigationMenuItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateNavigationMenuItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateOneNavigationMenuItemInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateNavigationMenuItem"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NavigationMenuItemQueryFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NavigationMenuItemFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NavigationMenuItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"targetRecordId"}},{"kind":"Field","name":{"kind":"Name","value":"targetObjectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"viewId"}},{"kind":"Field","name":{"kind":"Name","value":"folderId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NavigationMenuItemQueryFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NavigationMenuItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NavigationMenuItemFields"}},{"kind":"Field","name":{"kind":"Name","value":"targetRecordIdentifier"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifier"}}]}}]}}]} as unknown as DocumentNode; +export const CreateManyNavigationMenuItemsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateManyNavigationMenuItems"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"inputs"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateNavigationMenuItemInput"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createManyNavigationMenuItems"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"inputs"},"value":{"kind":"Variable","name":{"kind":"Name","value":"inputs"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NavigationMenuItemQueryFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NavigationMenuItemFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NavigationMenuItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"targetRecordId"}},{"kind":"Field","name":{"kind":"Name","value":"targetObjectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"viewId"}},{"kind":"Field","name":{"kind":"Name","value":"folderId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NavigationMenuItemQueryFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NavigationMenuItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NavigationMenuItemFields"}},{"kind":"Field","name":{"kind":"Name","value":"targetRecordIdentifier"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifier"}}]}}]}}]} as unknown as DocumentNode; +export const DeleteManyNavigationMenuItemsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteManyNavigationMenuItems"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"ids"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteManyNavigationMenuItems"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"ids"},"value":{"kind":"Variable","name":{"kind":"Name","value":"ids"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NavigationMenuItemQueryFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NavigationMenuItemFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NavigationMenuItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"targetRecordId"}},{"kind":"Field","name":{"kind":"Name","value":"targetObjectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"viewId"}},{"kind":"Field","name":{"kind":"Name","value":"folderId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NavigationMenuItemQueryFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NavigationMenuItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NavigationMenuItemFields"}},{"kind":"Field","name":{"kind":"Name","value":"targetRecordIdentifier"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifier"}}]}}]}}]} as unknown as DocumentNode; +export const UpdateManyNavigationMenuItemsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateManyNavigationMenuItems"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"inputs"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateOneNavigationMenuItemInput"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateManyNavigationMenuItems"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"inputs"},"value":{"kind":"Variable","name":{"kind":"Name","value":"inputs"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NavigationMenuItemQueryFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NavigationMenuItemFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NavigationMenuItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"targetRecordId"}},{"kind":"Field","name":{"kind":"Name","value":"targetObjectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"viewId"}},{"kind":"Field","name":{"kind":"Name","value":"folderId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NavigationMenuItemQueryFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NavigationMenuItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NavigationMenuItemFields"}},{"kind":"Field","name":{"kind":"Name","value":"targetRecordIdentifier"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifier"}}]}}]}}]} as unknown as DocumentNode; export const FindManyNavigationMenuItemsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyNavigationMenuItems"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"navigationMenuItems"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NavigationMenuItemQueryFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NavigationMenuItemFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NavigationMenuItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"targetRecordId"}},{"kind":"Field","name":{"kind":"Name","value":"targetObjectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"viewId"}},{"kind":"Field","name":{"kind":"Name","value":"folderId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NavigationMenuItemQueryFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NavigationMenuItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NavigationMenuItemFields"}},{"kind":"Field","name":{"kind":"Name","value":"targetRecordIdentifier"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifier"}}]}}]}}]} as unknown as DocumentNode; export const FindOneNavigationMenuItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneNavigationMenuItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"navigationMenuItem"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NavigationMenuItemQueryFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NavigationMenuItemFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NavigationMenuItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"targetRecordId"}},{"kind":"Field","name":{"kind":"Name","value":"targetObjectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"viewId"}},{"kind":"Field","name":{"kind":"Name","value":"folderId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NavigationMenuItemQueryFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NavigationMenuItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NavigationMenuItemFields"}},{"kind":"Field","name":{"kind":"Name","value":"targetRecordIdentifier"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifier"}}]}}]}}]} as unknown as DocumentNode; export const CreateOneObjectMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneObjectMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateOneObjectInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneObject"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSearchable"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"isLabelSyncedWithName"}},{"kind":"Field","name":{"kind":"Name","value":"fieldsList"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"isUIReadOnly"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"isUnique"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"defaultValue"}},{"kind":"Field","name":{"kind":"Name","value":"options"}},{"kind":"Field","name":{"kind":"Name","value":"settings"}},{"kind":"Field","name":{"kind":"Name","value":"isLabelSyncedWithName"}},{"kind":"Field","name":{"kind":"Name","value":"morphId"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"relation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"sourceObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}}]}},{"kind":"Field","name":{"kind":"Name","value":"targetObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}}]}},{"kind":"Field","name":{"kind":"Name","value":"sourceFieldMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"targetFieldMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"morphRelations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"sourceObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}}]}},{"kind":"Field","name":{"kind":"Name","value":"targetObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}}]}},{"kind":"Field","name":{"kind":"Name","value":"sourceFieldMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"targetFieldMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/AddToFavoritesSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/AddToFavoritesSingleRecordCommand.tsx index 945b622716..3ab7a2ea8c 100644 --- a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/AddToFavoritesSingleRecordCommand.tsx +++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/AddToFavoritesSingleRecordCommand.tsx @@ -1,7 +1,11 @@ +import { NavigationMenuItemType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { v4 as uuidv4 } from 'uuid'; + import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect'; import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi'; -import { useCreateNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useCreateNavigationMenuItem'; -import { isDefined } from 'twenty-shared/utils'; +import { useCreateManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useCreateManyNavigationMenuItems'; +import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData'; export const AddToFavoritesSingleRecordCommand = () => { const { objectMetadataItem, selectedRecords } = @@ -13,14 +17,34 @@ export const AddToFavoritesSingleRecordCommand = () => { throw new Error('Object metadata item is required to add to favorites'); } - const { createNavigationMenuItem } = useCreateNavigationMenuItem(); + const { createManyNavigationMenuItems } = useCreateManyNavigationMenuItems(); + const { navigationMenuItems, currentWorkspaceMemberId } = + useNavigationMenuItemsData(); const handleExecute = () => { if (!isDefined(selectedRecord)) { return; } - createNavigationMenuItem(selectedRecord, objectMetadataItem.nameSingular); + const relevantItems = navigationMenuItems.filter( + (item) => !isDefined(item.folderId) && isDefined(item.userWorkspaceId), + ); + + const maxPosition = Math.max( + ...relevantItems.map((item) => item.position), + 0, + ); + + createManyNavigationMenuItems([ + { + id: uuidv4(), + type: NavigationMenuItemType.RECORD, + targetRecordId: selectedRecord.id, + targetObjectMetadataId: objectMetadataItem.id, + userWorkspaceId: currentWorkspaceMemberId, + position: maxPosition + 1, + }, + ]); }; return ; diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/RemoveFromFavoritesSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/RemoveFromFavoritesSingleRecordCommand.tsx index d0983b2918..8e3e60314a 100644 --- a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/RemoveFromFavoritesSingleRecordCommand.tsx +++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/RemoveFromFavoritesSingleRecordCommand.tsx @@ -1,8 +1,9 @@ +import { isDefined } from 'twenty-shared/utils'; + import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect'; import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi'; -import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem'; +import { useDeleteManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useDeleteManyNavigationMenuItems'; import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData'; -import { isDefined } from 'twenty-shared/utils'; export const RemoveFromFavoritesSingleRecordCommand = () => { const { selectedRecords, objectMetadataItem } = @@ -19,7 +20,7 @@ export const RemoveFromFavoritesSingleRecordCommand = () => { const { navigationMenuItems, workspaceNavigationMenuItems } = useNavigationMenuItemsData(); - const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem(); + const { deleteManyNavigationMenuItems } = useDeleteManyNavigationMenuItems(); const foundNavigationMenuItem = [ ...navigationMenuItems, @@ -35,7 +36,7 @@ export const RemoveFromFavoritesSingleRecordCommand = () => { return; } - deleteNavigationMenuItem(foundNavigationMenuItem.id); + deleteManyNavigationMenuItems([foundNavigationMenuItem.id]); }; return ; diff --git a/packages/twenty-front/src/modules/command-menu-item/record/single-record/components/AddToFavoritesSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/record/single-record/components/AddToFavoritesSingleRecordCommand.tsx index 45e8ef3b4e..1ced342b23 100644 --- a/packages/twenty-front/src/modules/command-menu-item/record/single-record/components/AddToFavoritesSingleRecordCommand.tsx +++ b/packages/twenty-front/src/modules/command-menu-item/record/single-record/components/AddToFavoritesSingleRecordCommand.tsx @@ -1,26 +1,42 @@ +import { NavigationMenuItemType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { v4 as uuidv4 } from 'uuid'; + import { Command } from '@/command-menu-item/display/components/Command'; import { useSelectedRecordIdOrThrow } from '@/command-menu-item/record/single-record/hooks/useSelectedRecordIdOrThrow'; import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; -import { useCreateNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useCreateNavigationMenuItem'; -import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; -import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue'; -import { isDefined } from 'twenty-shared/utils'; +import { useCreateManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useCreateManyNavigationMenuItems'; +import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData'; export const AddToFavoritesSingleRecordCommand = () => { const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow(); const recordId = useSelectedRecordIdOrThrow(); - const { createNavigationMenuItem } = useCreateNavigationMenuItem(); - - const recordStore = useAtomFamilyStateValue(recordStoreFamilyState, recordId); + const { createManyNavigationMenuItems } = useCreateManyNavigationMenuItems(); + const { navigationMenuItems, currentWorkspaceMemberId } = + useNavigationMenuItemsData(); const handleClick = () => { - if (!isDefined(recordStore)) { - return; - } + const relevantItems = navigationMenuItems.filter( + (item) => !isDefined(item.folderId) && isDefined(item.userWorkspaceId), + ); - createNavigationMenuItem(recordStore, objectMetadataItem.nameSingular); + const maxPosition = Math.max( + ...relevantItems.map((item) => item.position), + 0, + ); + + createManyNavigationMenuItems([ + { + id: uuidv4(), + type: NavigationMenuItemType.RECORD, + targetRecordId: recordId, + targetObjectMetadataId: objectMetadataItem.id, + userWorkspaceId: currentWorkspaceMemberId, + position: maxPosition + 1, + }, + ]); }; return ; diff --git a/packages/twenty-front/src/modules/command-menu-item/record/single-record/components/RemoveFromFavoritesSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/record/single-record/components/RemoveFromFavoritesSingleRecordCommand.tsx index 9cab614aa4..479386c344 100644 --- a/packages/twenty-front/src/modules/command-menu-item/record/single-record/components/RemoveFromFavoritesSingleRecordCommand.tsx +++ b/packages/twenty-front/src/modules/command-menu-item/record/single-record/components/RemoveFromFavoritesSingleRecordCommand.tsx @@ -1,9 +1,10 @@ +import { isDefined } from 'twenty-shared/utils'; + import { Command } from '@/command-menu-item/display/components/Command'; import { useSelectedRecordIdOrThrow } from '@/command-menu-item/record/single-record/hooks/useSelectedRecordIdOrThrow'; import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; -import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem'; +import { useDeleteManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useDeleteManyNavigationMenuItems'; import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData'; -import { isDefined } from 'twenty-shared/utils'; export const RemoveFromFavoritesSingleRecordCommand = () => { const recordId = useSelectedRecordIdOrThrow(); @@ -12,7 +13,7 @@ export const RemoveFromFavoritesSingleRecordCommand = () => { const { navigationMenuItems, workspaceNavigationMenuItems } = useNavigationMenuItemsData(); - const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem(); + const { deleteManyNavigationMenuItems } = useDeleteManyNavigationMenuItems(); const foundNavigationMenuItem = [ ...navigationMenuItems, @@ -28,7 +29,7 @@ export const RemoveFromFavoritesSingleRecordCommand = () => { return; } - deleteNavigationMenuItem(foundNavigationMenuItem.id); + deleteManyNavigationMenuItems([foundNavigationMenuItem.id]); }; return ; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/graphql/mutations/createNavigationMenuItem.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/graphql/mutations/createManyNavigationMenuItems.ts similarity index 56% rename from packages/twenty-front/src/modules/navigation-menu-item/common/graphql/mutations/createNavigationMenuItem.ts rename to packages/twenty-front/src/modules/navigation-menu-item/common/graphql/mutations/createManyNavigationMenuItems.ts index fd05f7eb34..ece5f152cc 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/graphql/mutations/createNavigationMenuItem.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/graphql/mutations/createManyNavigationMenuItems.ts @@ -1,10 +1,12 @@ import { gql } from '@apollo/client'; import { NAVIGATION_MENU_ITEM_QUERY_FRAGMENT } from '@/navigation-menu-item/common/graphql/fragments/navigationMenuItemQueryFragment'; -export const CREATE_NAVIGATION_MENU_ITEM = gql` +export const CREATE_MANY_NAVIGATION_MENU_ITEMS = gql` ${NAVIGATION_MENU_ITEM_QUERY_FRAGMENT} - mutation CreateNavigationMenuItem($input: CreateNavigationMenuItemInput!) { - createNavigationMenuItem(input: $input) { + mutation CreateManyNavigationMenuItems( + $inputs: [CreateNavigationMenuItemInput!]! + ) { + createManyNavigationMenuItems(inputs: $inputs) { ...NavigationMenuItemQueryFields } } diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/graphql/mutations/deleteNavigationMenuItem.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/graphql/mutations/deleteManyNavigationMenuItems.ts similarity index 62% rename from packages/twenty-front/src/modules/navigation-menu-item/common/graphql/mutations/deleteNavigationMenuItem.ts rename to packages/twenty-front/src/modules/navigation-menu-item/common/graphql/mutations/deleteManyNavigationMenuItems.ts index 2ac307764b..889476735f 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/graphql/mutations/deleteNavigationMenuItem.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/graphql/mutations/deleteManyNavigationMenuItems.ts @@ -1,10 +1,10 @@ import { gql } from '@apollo/client'; import { NAVIGATION_MENU_ITEM_QUERY_FRAGMENT } from '@/navigation-menu-item/common/graphql/fragments/navigationMenuItemQueryFragment'; -export const DELETE_NAVIGATION_MENU_ITEM = gql` +export const DELETE_MANY_NAVIGATION_MENU_ITEMS = gql` ${NAVIGATION_MENU_ITEM_QUERY_FRAGMENT} - mutation DeleteNavigationMenuItem($id: UUID!) { - deleteNavigationMenuItem(id: $id) { + mutation DeleteManyNavigationMenuItems($ids: [UUID!]!) { + deleteManyNavigationMenuItems(ids: $ids) { ...NavigationMenuItemQueryFields } } diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/graphql/mutations/updateNavigationMenuItem.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/graphql/mutations/updateManyNavigationMenuItems.ts similarity index 56% rename from packages/twenty-front/src/modules/navigation-menu-item/common/graphql/mutations/updateNavigationMenuItem.ts rename to packages/twenty-front/src/modules/navigation-menu-item/common/graphql/mutations/updateManyNavigationMenuItems.ts index 6a2d18fb04..a7976f0f2c 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/graphql/mutations/updateNavigationMenuItem.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/graphql/mutations/updateManyNavigationMenuItems.ts @@ -1,10 +1,12 @@ import { gql } from '@apollo/client'; import { NAVIGATION_MENU_ITEM_QUERY_FRAGMENT } from '@/navigation-menu-item/common/graphql/fragments/navigationMenuItemQueryFragment'; -export const UPDATE_NAVIGATION_MENU_ITEM = gql` +export const UPDATE_MANY_NAVIGATION_MENU_ITEMS = gql` ${NAVIGATION_MENU_ITEM_QUERY_FRAGMENT} - mutation UpdateNavigationMenuItem($input: UpdateOneNavigationMenuItemInput!) { - updateNavigationMenuItem(input: $input) { + mutation UpdateManyNavigationMenuItems( + $inputs: [UpdateOneNavigationMenuItemInput!]! + ) { + updateManyNavigationMenuItems(inputs: $inputs) { ...NavigationMenuItemQueryFields } } diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useCreateManyNavigationMenuItems.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useCreateManyNavigationMenuItems.ts new file mode 100644 index 0000000000..8e4ae01200 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useCreateManyNavigationMenuItems.ts @@ -0,0 +1,85 @@ +import { useMutation } from '@apollo/client/react'; +import { isDefined } from 'twenty-shared/utils'; +import { + type CreateNavigationMenuItemInput, + CreateManyNavigationMenuItemsDocument, + type NavigationMenuItem, +} from '~/generated-metadata/graphql'; + +import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; + +const buildOptimisticNavigationMenuItem = ( + input: CreateNavigationMenuItemInput & { id: string }, +): NavigationMenuItem => ({ + id: input.id, + type: input.type, + position: input.position ?? 0, + userWorkspaceId: input.userWorkspaceId ?? null, + targetRecordId: input.targetRecordId ?? null, + targetObjectMetadataId: input.targetObjectMetadataId ?? null, + viewId: input.viewId ?? null, + folderId: input.folderId ?? null, + name: input.name ?? null, + link: input.link ?? null, + icon: input.icon ?? null, + color: input.color ?? null, + applicationId: null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), +}); + +export const useCreateManyNavigationMenuItems = () => { + const { addToDraft, removeFromDraft, applyChanges } = + useUpdateMetadataStoreDraft(); + + const [createManyNavigationMenuItemsMutation] = useMutation( + CreateManyNavigationMenuItemsDocument, + ); + + const createManyNavigationMenuItems = async ( + inputs: CreateNavigationMenuItemInput[], + ): Promise => { + if (inputs.length === 0) { + return []; + } + + const inputsWithIds = inputs.filter( + (input): input is CreateNavigationMenuItemInput & { id: string } => + isDefined(input.id), + ); + + if (inputsWithIds.length > 0) { + addToDraft({ + key: 'navigationMenuItems', + items: inputsWithIds.map(buildOptimisticNavigationMenuItem), + }); + applyChanges(); + } + + try { + const result = await createManyNavigationMenuItemsMutation({ + variables: { inputs }, + }); + + const createdItems = result.data?.createManyNavigationMenuItems ?? []; + + if (createdItems.length > 0) { + addToDraft({ key: 'navigationMenuItems', items: createdItems }); + applyChanges(); + } + + return createdItems; + } catch (error) { + if (inputsWithIds.length > 0) { + removeFromDraft({ + key: 'navigationMenuItems', + itemIds: inputsWithIds.map((input) => input.id), + }); + applyChanges(); + } + throw error; + } + }; + + return { createManyNavigationMenuItems }; +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useCreateNavigationMenuItem.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useCreateNavigationMenuItem.ts deleted file mode 100644 index 96a133cbb1..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useCreateNavigationMenuItem.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { useMutation } from '@apollo/client/react'; -import { NavigationMenuItemType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; -import { v4 as uuidv4 } from 'uuid'; -import { - type CreateNavigationMenuItemInput, - CreateNavigationMenuItemDocument, - type NavigationMenuItem, -} from '~/generated-metadata/graphql'; - -import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; -import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData'; -import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; -import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; - -const buildOptimisticNavigationMenuItem = ( - input: CreateNavigationMenuItemInput & { id: string }, -): NavigationMenuItem => ({ - id: input.id, - type: input.type, - position: input.position ?? 0, - userWorkspaceId: input.userWorkspaceId ?? null, - targetRecordId: input.targetRecordId ?? null, - targetObjectMetadataId: input.targetObjectMetadataId ?? null, - viewId: input.viewId ?? null, - folderId: input.folderId ?? null, - name: input.name ?? null, - link: input.link ?? null, - icon: input.icon ?? null, - color: input.color ?? null, - applicationId: null, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), -}); - -export const useCreateNavigationMenuItem = () => { - const { navigationMenuItems, currentWorkspaceMemberId } = - useNavigationMenuItemsData(); - const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector); - const { addToDraft, applyChanges } = useUpdateMetadataStoreDraft(); - - const [createNavigationMenuItemMutation] = useMutation( - CreateNavigationMenuItemDocument, - ); - - const createNavigationMenuItem = async ( - targetRecord: ObjectRecord, - targetObjectNameSingular: string, - folderId?: string, - ) => { - const isView = targetObjectNameSingular === 'view'; - const id = uuidv4(); - - const relevantItems = folderId - ? navigationMenuItems.filter((item) => item.folderId === folderId) - : navigationMenuItems.filter( - (item) => - !isDefined(item.folderId) && isDefined(item.userWorkspaceId), - ); - - const maxPosition = Math.max( - ...relevantItems.map((item) => item.position), - 0, - ); - - const position = maxPosition + 1; - - if (isView) { - const input: CreateNavigationMenuItemInput = { - id, - type: NavigationMenuItemType.VIEW, - viewId: targetRecord.id, - userWorkspaceId: currentWorkspaceMemberId, - folderId, - position, - }; - - addToDraft({ - key: 'navigationMenuItems', - items: [buildOptimisticNavigationMenuItem({ ...input, id })], - }); - applyChanges(); - - const result = await createNavigationMenuItemMutation({ - variables: { input }, - }); - - const created = result.data?.createNavigationMenuItem; - - if (isDefined(created)) { - addToDraft({ key: 'navigationMenuItems', items: [created] }); - applyChanges(); - } - } else { - const objectMetadataItem = objectMetadataItems.find( - (item) => item.nameSingular === targetObjectNameSingular, - ); - - if (!isDefined(objectMetadataItem)) { - throw new Error( - `Object metadata item not found for nameSingular: ${targetObjectNameSingular}`, - ); - } - - const input: CreateNavigationMenuItemInput = { - id, - type: NavigationMenuItemType.RECORD, - targetRecordId: targetRecord.id, - targetObjectMetadataId: objectMetadataItem.id, - userWorkspaceId: currentWorkspaceMemberId, - folderId, - position, - }; - - addToDraft({ - key: 'navigationMenuItems', - items: [buildOptimisticNavigationMenuItem({ ...input, id })], - }); - applyChanges(); - - const result = await createNavigationMenuItemMutation({ - variables: { input }, - }); - - const created = result.data?.createNavigationMenuItem; - - if (isDefined(created)) { - addToDraft({ key: 'navigationMenuItems', items: [created] }); - applyChanges(); - } - } - }; - - return { createNavigationMenuItem }; -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useDeleteManyNavigationMenuItems.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useDeleteManyNavigationMenuItems.ts new file mode 100644 index 0000000000..7618071b2c --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useDeleteManyNavigationMenuItems.ts @@ -0,0 +1,38 @@ +import { useMutation } from '@apollo/client/react'; +import { DeleteManyNavigationMenuItemsDocument } from '~/generated-metadata/graphql'; + +import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; +import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector'; +import { useStore } from 'jotai'; + +export const useDeleteManyNavigationMenuItems = () => { + const { removeFromDraft, replaceDraft, applyChanges } = + useUpdateMetadataStoreDraft(); + + const [deleteManyNavigationMenuItemsMutation] = useMutation( + DeleteManyNavigationMenuItemsDocument, + ); + + const store = useStore(); + + const deleteManyNavigationMenuItems = async (ids: string[]) => { + if (ids.length === 0) { + return; + } + + const previousItems = store.get(navigationMenuItemsSelector.atom); + + removeFromDraft({ key: 'navigationMenuItems', itemIds: ids }); + applyChanges(); + + try { + await deleteManyNavigationMenuItemsMutation({ variables: { ids } }); + } catch (error) { + replaceDraft('navigationMenuItems', previousItems); + applyChanges(); + throw error; + } + }; + + return { deleteManyNavigationMenuItems }; +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem.ts deleted file mode 100644 index 2e25bd0ee7..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useMutation } from '@apollo/client/react'; -import { DeleteNavigationMenuItemDocument } from '~/generated-metadata/graphql'; - -import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; - -export const useDeleteNavigationMenuItem = () => { - const { removeFromDraft, applyChanges } = useUpdateMetadataStoreDraft(); - - const [deleteNavigationMenuItemMutation] = useMutation( - DeleteNavigationMenuItemDocument, - ); - - const deleteNavigationMenuItem = async (id: string) => { - removeFromDraft({ key: 'navigationMenuItems', itemIds: [id] }); - applyChanges(); - - await deleteNavigationMenuItemMutation({ - variables: { id }, - }); - }; - - return { deleteNavigationMenuItem }; -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useUpdateManyNavigationMenuItems.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useUpdateManyNavigationMenuItems.ts new file mode 100644 index 0000000000..f7a0016122 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useUpdateManyNavigationMenuItems.ts @@ -0,0 +1,59 @@ +import { useMutation } from '@apollo/client/react'; +import { + type NavigationMenuItem, + UpdateManyNavigationMenuItemsDocument, + type UpdateOneNavigationMenuItemInput, +} from '~/generated-metadata/graphql'; + +import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; +import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector'; +import { useStore } from 'jotai'; + +export const useUpdateManyNavigationMenuItems = () => { + const { updateInDraft, addToDraft, replaceDraft, applyChanges } = + useUpdateMetadataStoreDraft(); + + const [updateManyNavigationMenuItemsMutation] = useMutation( + UpdateManyNavigationMenuItemsDocument, + ); + + const store = useStore(); + + const updateManyNavigationMenuItems = async ( + inputs: UpdateOneNavigationMenuItemInput[], + ): Promise => { + if (inputs.length === 0) { + return []; + } + + const previousItems = store.get(navigationMenuItemsSelector.atom); + + const optimisticItems = inputs.map( + ({ id, update }) => ({ id, ...update }) as NavigationMenuItem, + ); + + updateInDraft('navigationMenuItems', optimisticItems); + applyChanges(); + + try { + const result = await updateManyNavigationMenuItemsMutation({ + variables: { inputs }, + }); + + const updatedItems = result.data?.updateManyNavigationMenuItems ?? []; + + if (updatedItems.length > 0) { + addToDraft({ key: 'navigationMenuItems', items: updatedItems }); + applyChanges(); + } + + return updatedItems; + } catch (error) { + replaceDraft('navigationMenuItems', previousItems); + applyChanges(); + throw error; + } + }; + + return { updateManyNavigationMenuItems }; +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useUpdateNavigationMenuItem.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useUpdateNavigationMenuItem.ts deleted file mode 100644 index 26cfa6328e..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useUpdateNavigationMenuItem.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { useMutation } from '@apollo/client/react'; -import { isDefined } from 'twenty-shared/utils'; -import { - type NavigationMenuItem, - type UpdateNavigationMenuItemInput, - type UpdateOneNavigationMenuItemInput, - UpdateNavigationMenuItemDocument, -} from '~/generated-metadata/graphql'; - -import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; - -export const useUpdateNavigationMenuItem = () => { - const { addToDraft, applyChanges } = useUpdateMetadataStoreDraft(); - - const [updateNavigationMenuItemMutation] = useMutation( - UpdateNavigationMenuItemDocument, - ); - - const updateNavigationMenuItem = async ( - input: UpdateNavigationMenuItemInput & { id: string }, - ) => { - const { id, ...update } = input; - - addToDraft({ - key: 'navigationMenuItems', - items: [{ id, ...update } as NavigationMenuItem], - }); - applyChanges(); - - const updateOneInput: UpdateOneNavigationMenuItemInput = { - id, - update, - }; - - const result = await updateNavigationMenuItemMutation({ - variables: { input: updateOneInput }, - }); - - const updated = result.data?.updateNavigationMenuItem; - - if (isDefined(updated)) { - addToDraft({ key: 'navigationMenuItems', items: [updated] }); - applyChanges(); - } - }; - - return { updateNavigationMenuItem }; -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/buildCreateNavigationMenuItemInput.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/buildCreateNavigationMenuItemInput.ts index d586044633..9bcbfc8d88 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/buildCreateNavigationMenuItemInput.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/buildCreateNavigationMenuItemInput.ts @@ -3,7 +3,7 @@ import type { NavigationMenuItem, } from '~/generated-metadata/graphql'; -import { isDefined } from 'twenty-shared/utils'; +import { ensureAbsoluteUrl, isDefined } from 'twenty-shared/utils'; import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder'; import { isNavigationMenuItemLink } from '@/navigation-menu-item/common/utils/isNavigationMenuItemLink'; @@ -14,6 +14,7 @@ export const buildCreateNavigationMenuItemInput = ( resolveFolderId: (draftFolderId: string) => string, ): CreateNavigationMenuItemInput => { const input: CreateNavigationMenuItemInput = { + id: draftItem.id, type: draftItem.type, position: draftItem.position, }; @@ -24,12 +25,7 @@ export const buildCreateNavigationMenuItemInput = ( } else if (isNavigationMenuItemLink(draftItem)) { input.name = draftItem.name ?? 'Link'; const linkUrl = (draftItem.link ?? '').trim(); - input.link = - linkUrl.startsWith('http://') || linkUrl.startsWith('https://') - ? linkUrl - : linkUrl - ? `https://${linkUrl}` - : undefined; + input.link = linkUrl ? ensureAbsoluteUrl(linkUrl) : undefined; } else if (isNavigationMenuItemObject(draftItem)) { input.targetObjectMetadataId = draftItem.targetObjectMetadataId ?? undefined; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleNavigationMenuItemDragAndDrop.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleNavigationMenuItemDragAndDrop.ts index 9ca21fc1f7..92d418ae62 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleNavigationMenuItemDragAndDrop.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleNavigationMenuItemDragAndDrop.ts @@ -4,7 +4,7 @@ import { type NavigationMenuItem } from '~/generated-metadata/graphql'; import { isDefined } from 'twenty-shared/utils'; import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState'; -import { useUpdateNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useUpdateNavigationMenuItem'; +import { useUpdateManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useUpdateManyNavigationMenuItems'; import { NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG } from '@/navigation-menu-item/common/constants/NavigationMenuItemSectionDroppableConfig'; import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState'; import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState'; @@ -29,7 +29,7 @@ export const useHandleNavigationMenuItemDragAndDrop = ( const { navigationMenuItems, workspaceNavigationMenuItems } = useNavigationMenuItemsData(); const { navigationMenuItemsSorted } = useSortedNavigationMenuItems(); - const { updateNavigationMenuItem } = useUpdateNavigationMenuItem(); + const { updateManyNavigationMenuItems } = useUpdateManyNavigationMenuItems(); const setNavigationMenuItemsDraft = useSetAtomState( navigationMenuItemsDraftState, ); @@ -73,11 +73,12 @@ export const useHandleNavigationMenuItemDragAndDrop = ( }), ); } else { - await updateNavigationMenuItem({ - id: draggableId, - position: newPosition, - ...folderUpdate, - }); + await updateManyNavigationMenuItems([ + { + id: draggableId, + update: { position: newPosition, ...folderUpdate }, + }, + ]); } }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderContent.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderContent.tsx index 7826c188ba..201c279a44 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderContent.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderContent.tsx @@ -6,7 +6,7 @@ import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/co import { NavigationMenuItemBackButton } from '@/navigation-menu-item/edit/components/NavigationMenuItemBackButton'; import { NavigationMenuItemIcon } from '@/navigation-menu-item/display/components/NavigationMenuItemIcon'; import { NavigationMenuItemSortableItem } from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemSortableItem'; -import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem'; +import { useDeleteManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useDeleteManyNavigationMenuItems'; import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink'; import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel'; import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/display/object/utils/getNavigationMenuItemObjectNameSingular'; @@ -29,7 +29,7 @@ export const NavigationMenuItemFolderContent = ({ }: NavigationMenuItemFolderContentProps) => { const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector); const views = useAtomStateValue(viewsSelector); - const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem(); + const { deleteManyNavigationMenuItems } = useDeleteManyNavigationMenuItems(); const folderDroppableId = `${NavigationMenuItemDroppableIds.FAVORITE_FOLDER_PREFIX}${folderId}`; @@ -76,7 +76,7 @@ export const NavigationMenuItemFolderContent = ({ Icon={IconHeartOff} onClick={(e) => { e.stopPropagation(); - deleteNavigationMenuItem(navigationMenuItem.id); + deleteManyNavigationMenuItems([navigationMenuItem.id]); }} accent="tertiary" /> diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderDnd.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderDnd.tsx index aead0072cf..176ec576af 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderDnd.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderDnd.tsx @@ -24,7 +24,7 @@ import { NavigationSections } from '@/navigation-menu-item/common/constants/Navi import { NavigationDropTargetContext } from '@/navigation-menu-item/common/contexts/NavigationDropTargetContext'; import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext'; import { SortableDropTargetRefContext } from '@/navigation-menu-item/common/contexts/SortableDropTargetRefContext'; -import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem'; +import { useDeleteManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useDeleteManyNavigationMenuItems'; import { addMenuItemInsertionContextState } from '@/navigation-menu-item/common/states/addMenuItemInsertionContextState'; import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState'; import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection'; @@ -133,7 +133,7 @@ export const NavigationMenuItemFolderDnd = ({ NavigationDropTargetContext, ); - const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem(); + const { deleteManyNavigationMenuItems } = useDeleteManyNavigationMenuItems(); const favoritesEdit = useFavoritesFolderEdit({ folderId, folderName, @@ -361,7 +361,9 @@ export const NavigationMenuItemFolderDnd = ({ Icon={IconHeartOff} onClick={(event) => { event.stopPropagation(); - deleteNavigationMenuItem(navigationMenuItem.id); + deleteManyNavigationMenuItems([ + navigationMenuItem.id, + ]); }} accent="tertiary" /> diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/hooks/useCreateNavigationMenuItemFolder.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/hooks/useCreateNavigationMenuItemFolder.ts index dee68c6a6c..1dc9024c91 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/hooks/useCreateNavigationMenuItemFolder.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/hooks/useCreateNavigationMenuItemFolder.ts @@ -1,17 +1,14 @@ import { NavigationMenuItemType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { useMutation } from '@apollo/client/react'; -import { CreateNavigationMenuItemDocument } from '~/generated-metadata/graphql'; +import { useCreateManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useCreateManyNavigationMenuItems'; import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData'; export const useCreateNavigationMenuItemFolder = () => { const { navigationMenuItems, currentWorkspaceMemberId } = useNavigationMenuItemsData(); - const [createNavigationMenuItemMutation] = useMutation( - CreateNavigationMenuItemDocument, - ); + const { createManyNavigationMenuItems } = useCreateManyNavigationMenuItems(); const createNewNavigationMenuItemFolder = async ( name: string, @@ -31,19 +28,17 @@ export const useCreateNavigationMenuItemFolder = () => { ? Math.min(...topLevelItems.map((item) => item.position)) : 1; - await createNavigationMenuItemMutation({ - variables: { - input: { - type: NavigationMenuItemType.FOLDER, - name, - targetRecordId: null, - targetObjectMetadataId: null, - userWorkspaceId: currentWorkspaceMemberId, - folderId: null, - position: minPosition - 1, - }, + await createManyNavigationMenuItems([ + { + type: NavigationMenuItemType.FOLDER, + name, + targetRecordId: null, + targetObjectMetadataId: null, + userWorkspaceId: currentWorkspaceMemberId, + folderId: null, + position: minPosition - 1, }, - }); + ]); }; return { createNewNavigationMenuItemFolder }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSection.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSection.tsx index 256857b5bc..935e199c9e 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSection.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSection.tsx @@ -9,7 +9,7 @@ import { type NavigationMenuItem } from '~/generated-metadata/graphql'; import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds'; import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants'; import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext'; -import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem'; +import { useDeleteManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useDeleteManyNavigationMenuItems'; import { isNavigationMenuItemFolderCreatingState } from '@/navigation-menu-item/common/states/isNavigationMenuItemFolderCreatingState'; import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder'; import { NavigationMenuItemDisplay } from '@/navigation-menu-item/display/components/NavigationMenuItemDisplay'; @@ -51,7 +51,7 @@ const ORPHAN_DROPPABLE_ID = export const FavoritesSection = () => { const { navigationMenuItemsSorted } = useSortedNavigationMenuItems(); const { userNavigationMenuItemsByFolder } = useNavigationMenuItemsByFolder(); - const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem(); + const { deleteManyNavigationMenuItems } = useDeleteManyNavigationMenuItems(); const { isDragging } = useContext(NavigationMenuItemDragContext); const favoritesDropDisabled = useIsDropDisabledForSection(false); @@ -128,12 +128,12 @@ export const FavoritesSection = () => { Icon={IconHeartOff} onClick={(event) => { event.stopPropagation(); - deleteNavigationMenuItem(item.id); + deleteManyNavigationMenuItems([item.id]); }} accent="tertiary" /> ), - [deleteNavigationMenuItem], + [deleteManyNavigationMenuItems], ); if ( diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/folder/hooks/useDeleteNavigationMenuItemFolder.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/folder/hooks/useDeleteNavigationMenuItemFolder.ts index 2620f1ba62..8c325a18b1 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/folder/hooks/useDeleteNavigationMenuItemFolder.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/folder/hooks/useDeleteNavigationMenuItemFolder.ts @@ -1,12 +1,12 @@ -import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem'; +import { useDeleteManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useDeleteManyNavigationMenuItems'; export const useDeleteNavigationMenuItemFolder = () => { - const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem(); + const { deleteManyNavigationMenuItems } = useDeleteManyNavigationMenuItems(); const deleteNavigationMenuItemFolder = async ( folderId: string, ): Promise => { - await deleteNavigationMenuItem(folderId); + await deleteManyNavigationMenuItems([folderId]); }; return { diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/folder/hooks/useRenameNavigationMenuItemFolder.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/folder/hooks/useRenameNavigationMenuItemFolder.ts index 62133b2c7b..fb596cd900 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/folder/hooks/useRenameNavigationMenuItemFolder.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/folder/hooks/useRenameNavigationMenuItemFolder.ts @@ -1,7 +1,7 @@ -import { useUpdateNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useUpdateNavigationMenuItem'; +import { useUpdateManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useUpdateManyNavigationMenuItems'; export const useRenameNavigationMenuItemFolder = () => { - const { updateNavigationMenuItem } = useUpdateNavigationMenuItem(); + const { updateManyNavigationMenuItems } = useUpdateManyNavigationMenuItems(); const renameNavigationMenuItemFolder = async ( folderId: string, @@ -11,10 +11,9 @@ export const useRenameNavigationMenuItemFolder = () => { return; } - await updateNavigationMenuItem({ - id: folderId, - name: newName, - }); + await updateManyNavigationMenuItems([ + { id: folderId, update: { name: newName } }, + ]); }; return { diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useSaveNavigationMenuItemsDraft.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useSaveNavigationMenuItemsDraft.ts index ebe49fd91e..d272218a95 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useSaveNavigationMenuItemsDraft.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useSaveNavigationMenuItemsDraft.ts @@ -1,29 +1,22 @@ import { useCallback } from 'react'; -import { NavigationMenuItemType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; -import { useMutation } from '@apollo/client/react'; -import { CreateNavigationMenuItemDocument } from '~/generated-metadata/graphql'; - -import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem'; -import { useUpdateNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useUpdateNavigationMenuItem'; +import { useCreateManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useCreateManyNavigationMenuItems'; +import { useDeleteManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useDeleteManyNavigationMenuItems'; +import { useUpdateManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useUpdateManyNavigationMenuItems'; import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState'; +import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector'; import { buildCreateNavigationMenuItemInput } from '@/navigation-menu-item/common/utils/buildCreateNavigationMenuItemInput'; import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/common/utils/filterWorkspaceNavigationMenuItems'; -import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder'; -import { isNavigationMenuItemLink } from '@/navigation-menu-item/common/utils/isNavigationMenuItemLink'; -import { orderFoldersForCreation } from '@/navigation-menu-item/common/utils/orderFoldersForCreation'; -import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector'; +import { buildUpdateInputsFromDraft } from '@/navigation-menu-item/edit/utils/buildUpdateInputsFromDraft'; +import { getObjectMetadataColorUpdates } from '@/navigation-menu-item/edit/utils/getObjectMetadataColorUpdates'; import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; import { useStore } from 'jotai'; export const useSaveNavigationMenuItemsDraft = () => { - const { updateNavigationMenuItem } = useUpdateNavigationMenuItem(); - const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem(); - const [createNavigationMenuItemMutation] = useMutation( - CreateNavigationMenuItemDocument, - ); + const { createManyNavigationMenuItems } = useCreateManyNavigationMenuItems(); + const { deleteManyNavigationMenuItems } = useDeleteManyNavigationMenuItems(); + const { updateManyNavigationMenuItems } = useUpdateManyNavigationMenuItems(); const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem(); const store = useStore(); @@ -32,71 +25,47 @@ export const useSaveNavigationMenuItemsDraft = () => { const draft = store.get(navigationMenuItemsDraftState.atom); const currentItems = store.get(navigationMenuItemsSelector.atom); - if (!draft) return; + if (!draft) { + return; + } const objectMetadataItems = store.get(objectMetadataItemsSelector.atom); - for (const draftItem of draft) { - if (draftItem.type !== NavigationMenuItemType.OBJECT) { - continue; - } - if (!isDefined(draftItem.targetObjectMetadataId)) { - continue; - } - if (!isDefined(draftItem.color)) { - continue; - } - - const objectMetadataItem = objectMetadataItems.find( - (item) => item.id === draftItem.targetObjectMetadataId, - ); - - if (!isDefined(objectMetadataItem)) { - continue; - } - - if (objectMetadataItem.color === draftItem.color) { - continue; - } + const colorUpdates = getObjectMetadataColorUpdates({ + draft, + objectMetadataItems, + }); + for (const { idToUpdate, color } of colorUpdates) { await updateOneObjectMetadataItem({ - idToUpdate: draftItem.targetObjectMetadataId, - updatePayload: { color: draftItem.color }, + idToUpdate, + updatePayload: { color }, }); } const workspaceItems = filterWorkspaceNavigationMenuItems(currentItems); - const topLevelWorkspace = workspaceItems.filter( - (item) => !isDefined(item.folderId), - ); - const draftIds = new Set(draft.map((i) => i.id)); + const draftIds = new Set(draft.map((item) => item.id)); - const topLevelToDelete = topLevelWorkspace.filter( - (item) => !draftIds.has(item.id), - ); - const folderIdsToDelete = new Set( - topLevelToDelete - .filter(isNavigationMenuItemFolder) - .map((item) => item.id), - ); - const folderChildrenToDelete = currentItems.filter( - (item) => - isDefined(item.folderId) && folderIdsToDelete.has(item.folderId), + const deleteAfterLayoutChangeIds = workspaceItems + .filter((workspaceItem) => !draftIds.has(workspaceItem.id)) + .map((workspaceItem) => workspaceItem.id); + + await deleteManyNavigationMenuItems(deleteAfterLayoutChangeIds); + + const currentIds = new Set(workspaceItems.map((item) => item.id)); + const workspaceItemsById = new Map( + workspaceItems.map((item) => [item.id, item]), ); - for (const item of folderChildrenToDelete) { - await deleteNavigationMenuItem(item.id); - } - for (const item of topLevelToDelete) { - await deleteNavigationMenuItem(item.id); - } + const itemsToCreate = draft.filter((item) => !currentIds.has(item.id)); - const currentIds = new Set(workspaceItems.map((i) => i.id)); - const workspaceItemsById = new Map(workspaceItems.map((i) => [i.id, i])); - const idsToCreate = draft.filter((item) => !currentIds.has(item.id)); const idsToRecreate = draft.filter((item) => { const original = workspaceItemsById.get(item.id); - if (!original) return false; + + if (!original) { + return false; + } + return ( original.viewId !== item.viewId || original.targetObjectMetadataId !== item.targetObjectMetadataId || @@ -104,124 +73,33 @@ export const useSaveNavigationMenuItemsDraft = () => { ); }); - for (const draftItem of idsToRecreate) { - await deleteNavigationMenuItem(draftItem.id); - } + const recreateIds = idsToRecreate.map((item) => item.id); - const itemsToCreate = [...idsToCreate, ...idsToRecreate]; - const foldersToCreate = itemsToCreate.filter(isNavigationMenuItemFolder); - const nonFoldersToCreate = itemsToCreate.filter( - (item) => !isNavigationMenuItemFolder(item), + await deleteManyNavigationMenuItems(recreateIds); + + const allItemsToCreate = [...itemsToCreate, ...idsToRecreate]; + const resolveFolderId = (draftFolderId: string): string => draftFolderId; + + const createInputs = allItemsToCreate.map((draftItem) => + buildCreateNavigationMenuItemInput(draftItem, resolveFolderId), ); - const createdFolderIdByDraftId = new Map(); - const resolveFolderId = (draftFolderId: string): string => - createdFolderIdByDraftId.get(draftFolderId) ?? draftFolderId; + await createManyNavigationMenuItems(createInputs); - const orderedFolders = orderFoldersForCreation(foldersToCreate, currentIds); - for (const draftItem of orderedFolders) { - const input = buildCreateNavigationMenuItemInput( - draftItem, - resolveFolderId, - ); - const result = await createNavigationMenuItemMutation({ - variables: { input }, - }); - const created = result.data?.createNavigationMenuItem; - if (isDefined(created?.id)) { - createdFolderIdByDraftId.set(draftItem.id, created.id); - } - } + const idsToRecreateSet = new Set(recreateIds); - for (const draftItem of nonFoldersToCreate) { - const input = buildCreateNavigationMenuItemInput( - draftItem, - resolveFolderId, - ); - await createNavigationMenuItemMutation({ - variables: { input }, - }); - } + const updateInputs = buildUpdateInputsFromDraft({ + draft, + workspaceItemsById, + idsToRecreateSet, + resolveFolderId, + }); - const idsToRecreateSet = new Set(idsToRecreate.map((i) => i.id)); - for (const draftItem of draft) { - if (idsToRecreateSet.has(draftItem.id)) continue; - - const original = workspaceItemsById.get(draftItem.id); - if (!original) continue; - - const positionChanged = original.position !== draftItem.position; - const folderIdChanged = - (original.folderId ?? null) !== (draftItem.folderId ?? null); - const nameChanged = - (isNavigationMenuItemFolder(draftItem) || - isNavigationMenuItemLink(draftItem)) && - (original.name ?? null) !== (draftItem.name ?? null); - const linkChanged = - isNavigationMenuItemLink(draftItem) && - (original.link ?? null) !== (draftItem.link ?? null); - const iconChanged = - isNavigationMenuItemFolder(draftItem) && - (original.icon ?? null) !== (draftItem.icon ?? null); - const colorChanged = - isNavigationMenuItemFolder(draftItem) && - (original.color ?? null) !== (draftItem.color ?? null); - - if ( - positionChanged || - folderIdChanged || - nameChanged || - linkChanged || - iconChanged || - colorChanged - ) { - const updateInput: { - id: string; - position?: number; - folderId?: string | null; - name?: string; - link?: string | null; - icon?: string | null; - color?: string | null; - } = { id: draftItem.id }; - - if (positionChanged) { - updateInput.position = draftItem.position; - } - if (folderIdChanged) { - updateInput.folderId = - draftItem.folderId != null - ? resolveFolderId(draftItem.folderId) - : null; - } - if (nameChanged && isNavigationMenuItemFolder(draftItem)) { - updateInput.name = draftItem.name ?? undefined; - } - if (nameChanged && isNavigationMenuItemLink(draftItem)) { - updateInput.name = draftItem.name ?? undefined; - } - if (linkChanged && isNavigationMenuItemLink(draftItem)) { - const linkUrl = (draftItem.link ?? '').trim(); - updateInput.link = linkUrl - ? linkUrl.startsWith('http://') || linkUrl.startsWith('https://') - ? linkUrl - : `https://${linkUrl}` - : null; - } - if (iconChanged && isNavigationMenuItemFolder(draftItem)) { - updateInput.icon = draftItem.icon ?? null; - } - if (colorChanged) { - updateInput.color = draftItem.color ?? null; - } - - await updateNavigationMenuItem(updateInput); - } - } + await updateManyNavigationMenuItems(updateInputs); }, [ - updateNavigationMenuItem, - deleteNavigationMenuItem, - createNavigationMenuItemMutation, + createManyNavigationMenuItems, + deleteManyNavigationMenuItems, + updateManyNavigationMenuItems, updateOneObjectMetadataItem, store, ]); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/utils/buildUpdateInputsFromDraft.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/utils/buildUpdateInputsFromDraft.ts new file mode 100644 index 0000000000..260868a288 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/utils/buildUpdateInputsFromDraft.ts @@ -0,0 +1,95 @@ +import { ensureAbsoluteUrl, isDefined } from 'twenty-shared/utils'; +import { + type NavigationMenuItem, + type UpdateOneNavigationMenuItemInput, +} from '~/generated-metadata/graphql'; + +import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder'; +import { isNavigationMenuItemLink } from '@/navigation-menu-item/common/utils/isNavigationMenuItemLink'; + +export const buildUpdateInputsFromDraft = ({ + draft, + workspaceItemsById, + idsToRecreateSet, + resolveFolderId, +}: { + draft: NavigationMenuItem[]; + workspaceItemsById: Map; + idsToRecreateSet: Set; + resolveFolderId: (draftFolderId: string) => string; +}): UpdateOneNavigationMenuItemInput[] => { + const updateInputs = []; + + for (const draftItem of draft) { + if (idsToRecreateSet.has(draftItem.id)) { + continue; + } + + const original = workspaceItemsById.get(draftItem.id); + if (!original) { + continue; + } + + const positionChanged = original.position !== draftItem.position; + const folderIdChanged = + (original.folderId ?? null) !== (draftItem.folderId ?? null); + const nameChanged = + (isNavigationMenuItemFolder(draftItem) || + isNavigationMenuItemLink(draftItem)) && + (original.name ?? null) !== (draftItem.name ?? null); + const linkChanged = + isNavigationMenuItemLink(draftItem) && + (original.link ?? null) !== (draftItem.link ?? null); + const iconChanged = + isNavigationMenuItemFolder(draftItem) && + (original.icon ?? null) !== (draftItem.icon ?? null); + const colorChanged = + isNavigationMenuItemFolder(draftItem) && + (original.color ?? null) !== (draftItem.color ?? null); + + if ( + !positionChanged && + !folderIdChanged && + !nameChanged && + !linkChanged && + !iconChanged && + !colorChanged + ) { + continue; + } + + const updatePayload: UpdateOneNavigationMenuItemInput['update'] = {}; + + if (positionChanged) { + updatePayload.position = draftItem.position; + } + if (folderIdChanged) { + updatePayload.folderId = isDefined(draftItem.folderId) + ? resolveFolderId(draftItem.folderId) + : null; + } + if (nameChanged && isNavigationMenuItemFolder(draftItem)) { + updatePayload.name = draftItem.name; + } + if (nameChanged && isNavigationMenuItemLink(draftItem)) { + updatePayload.name = draftItem.name; + } + if (linkChanged && isNavigationMenuItemLink(draftItem)) { + const linkUrl = (draftItem.link ?? '').trim(); + updatePayload.link = linkUrl ? ensureAbsoluteUrl(linkUrl) : null; + } + if (iconChanged && isNavigationMenuItemFolder(draftItem)) { + updatePayload.icon = draftItem.icon ?? null; + } + if (colorChanged) { + updatePayload.color = draftItem.color ?? null; + } + + updateInputs.push({ + id: draftItem.id, + update: updatePayload, + }); + } + + return updateInputs; +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/utils/getObjectMetadataColorUpdates.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/utils/getObjectMetadataColorUpdates.ts new file mode 100644 index 0000000000..a9ad074b41 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/utils/getObjectMetadataColorUpdates.ts @@ -0,0 +1,52 @@ +import { NavigationMenuItemType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { type NavigationMenuItem } from '~/generated-metadata/graphql'; + +import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; + +type ObjectMetadataColorUpdate = { + idToUpdate: string; + color: string; +}; + +export const getObjectMetadataColorUpdates = ({ + draft, + objectMetadataItems, +}: { + draft: NavigationMenuItem[]; + objectMetadataItems: EnrichedObjectMetadataItem[]; +}): ObjectMetadataColorUpdate[] => { + const updates: ObjectMetadataColorUpdate[] = []; + + for (const draftItem of draft) { + if (draftItem.type !== NavigationMenuItemType.OBJECT) { + continue; + } + if (!isDefined(draftItem.targetObjectMetadataId)) { + continue; + } + if (!isDefined(draftItem.color)) { + continue; + } + + const objectMetadataItem = objectMetadataItems.find( + (item) => item.id === draftItem.targetObjectMetadataId, + ); + + if (!isDefined(objectMetadataItem)) { + continue; + } + + if (objectMetadataItem.color === draftItem.color) { + continue; + } + + updates.push({ + idToUpdate: draftItem.targetObjectMetadataId, + color: draftItem.color, + }); + } + + return updates; +}; diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerOptionDropdown.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerOptionDropdown.tsx index 961d12a4d6..8a7a80ada4 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerOptionDropdown.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerOptionDropdown.tsx @@ -1,6 +1,5 @@ -import { useCreateNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useCreateNavigationMenuItem'; +import { useCreateManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useCreateManyNavigationMenuItems'; import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData'; -import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; @@ -11,6 +10,9 @@ import { type View } from '@/views/types/View'; import { useDestroyViewFromCurrentState } from '@/views/view-picker/hooks/useDestroyViewFromCurrentState'; import { viewPickerReferenceViewIdComponentState } from '@/views/view-picker/states/viewPickerReferenceViewIdComponentState'; import { useLingui } from '@lingui/react/macro'; +import { NavigationMenuItemType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { v4 as uuidv4 } from 'uuid'; import { IconHeart, IconLock, @@ -53,7 +55,7 @@ export const ViewPickerOptionDropdown = ({ ); const hasViewsPermission = useHasPermissionFlag(PermissionFlagType.VIEWS); - const { createNavigationMenuItem } = useCreateNavigationMenuItem(); + const { createManyNavigationMenuItems } = useCreateManyNavigationMenuItems(); const { navigationMenuItems, currentWorkspaceMemberId } = useNavigationMenuItemsData(); @@ -76,7 +78,24 @@ export const ViewPickerOptionDropdown = ({ const handleAddToFavorites = () => { if (!isFavorite) { - createNavigationMenuItem(view as unknown as ObjectRecord, 'view'); + const relevantItems = navigationMenuItems.filter( + (item) => !isDefined(item.folderId) && isDefined(item.userWorkspaceId), + ); + + const maxPosition = Math.max( + ...relevantItems.map((item) => item.position), + 0, + ); + + createManyNavigationMenuItems([ + { + id: uuidv4(), + type: NavigationMenuItemType.VIEW, + viewId: view.id, + userWorkspaceId: currentWorkspaceMemberId, + position: maxPosition + 1, + }, + ]); } closeDropdown(dropdownId); }; diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.resolver.ts index 74ea1329ee..2214fb1343 100644 --- a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.resolver.ts @@ -65,6 +65,26 @@ export class NavigationMenuItemResolver { }); } + @Mutation(() => [NavigationMenuItemDTO]) + @UseGuards(NoPermissionGuard) + async createManyNavigationMenuItems( + @Args('inputs', { type: () => [CreateNavigationMenuItemInput] }) + inputs: CreateNavigationMenuItemInput[], + @AuthWorkspace() workspace: WorkspaceEntity, + @AuthUserWorkspaceId({ allowUndefined: true }) + userWorkspaceId: string | undefined, + @AuthApiKey() apiKey: ApiKeyEntity | undefined, + @Context() context: { req: { application?: ApplicationEntity } }, + ): Promise { + return await this.navigationMenuItemService.createMany({ + inputs, + workspaceId: workspace.id, + authUserWorkspaceId: userWorkspaceId, + authApiKeyId: apiKey?.id, + authApplicationId: context.req.application?.id, + }); + } + @Mutation(() => NavigationMenuItemDTO) @UseGuards(NoPermissionGuard) async createNavigationMenuItem( @@ -84,6 +104,26 @@ export class NavigationMenuItemResolver { }); } + @Mutation(() => [NavigationMenuItemDTO]) + @UseGuards(NoPermissionGuard) + async updateManyNavigationMenuItems( + @Args('inputs', { type: () => [UpdateOneNavigationMenuItemInput] }) + inputs: UpdateOneNavigationMenuItemInput[], + @AuthWorkspace() workspace: WorkspaceEntity, + @AuthUserWorkspaceId({ allowUndefined: true }) + userWorkspaceId: string | undefined, + @AuthApiKey() apiKey: ApiKeyEntity | undefined, + @Context() context: { req: { application?: ApplicationEntity } }, + ): Promise { + return await this.navigationMenuItemService.updateMany({ + inputs, + workspaceId: workspace.id, + authUserWorkspaceId: userWorkspaceId, + authApiKeyId: apiKey?.id, + authApplicationId: context.req.application?.id, + }); + } + @Mutation(() => NavigationMenuItemDTO) @UseGuards(NoPermissionGuard) async updateNavigationMenuItem( @@ -103,6 +143,25 @@ export class NavigationMenuItemResolver { }); } + @Mutation(() => [NavigationMenuItemDTO]) + @UseGuards(NoPermissionGuard) + async deleteManyNavigationMenuItems( + @Args('ids', { type: () => [UUIDScalarType] }) ids: string[], + @AuthWorkspace() workspace: WorkspaceEntity, + @AuthUserWorkspaceId({ allowUndefined: true }) + userWorkspaceId: string | undefined, + @AuthApiKey() apiKey: ApiKeyEntity | undefined, + @Context() context: { req: { application?: ApplicationEntity } }, + ): Promise { + return await this.navigationMenuItemService.deleteMany({ + ids, + workspaceId: workspace.id, + authUserWorkspaceId: userWorkspaceId, + authApiKeyId: apiKey?.id, + authApplicationId: context.req.application?.id, + }); + } + @Mutation(() => NavigationMenuItemDTO) @UseGuards(NoPermissionGuard) async deleteNavigationMenuItem( diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service.ts index 60664ca452..9d6126d993 100644 --- a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service.ts @@ -7,6 +7,8 @@ import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/wo import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type'; +import { addFlatNavigationMenuItemToMapsAndUpdateIndex } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/add-flat-navigation-menu-item-to-maps-and-update-index.util'; import { fromCreateNavigationMenuItemInputToFlatNavigationMenuItemToCreate } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/from-create-navigation-menu-item-input-to-flat-navigation-menu-item-to-create.util'; import { fromDeleteNavigationMenuItemInputToFlatNavigationMenuItemOrThrow } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/from-delete-navigation-menu-item-input-to-flat-navigation-menu-item-or-throw.util'; import { fromFlatNavigationMenuItemToNavigationMenuItemDto } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/from-flat-navigation-menu-item-to-navigation-menu-item-dto.util'; @@ -14,7 +16,10 @@ import { fromUpdateNavigationMenuItemInputToFlatNavigationMenuItemToUpdateOrThro import { type CreateNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/create-navigation-menu-item.input'; import { type NavigationMenuItemDTO } from 'src/engine/metadata-modules/navigation-menu-item/dtos/navigation-menu-item.dto'; import { RecordIdentifierDTO } from 'src/engine/metadata-modules/navigation-menu-item/dtos/record-identifier.dto'; -import { type UpdateNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/update-navigation-menu-item.input'; +import { + type UpdateNavigationMenuItemInput, + type UpdateOneNavigationMenuItemInput, +} from 'src/engine/metadata-modules/navigation-menu-item/dtos/update-navigation-menu-item.input'; import { NavigationMenuItemType } from 'src/engine/metadata-modules/navigation-menu-item/enums/navigation-menu-item-type.enum'; import { NavigationMenuItemException, @@ -123,13 +128,55 @@ export class NavigationMenuItemService { authApiKeyId?: string; authApplicationId?: string; }): Promise { - await this.navigationMenuItemAccessService.canUserCreateNavigationMenuItem({ - userWorkspaceId: authUserWorkspaceId, + const createdItems = await this.createMany({ + inputs: [input], workspaceId, - apiKeyId: authApiKeyId, - applicationId: authApplicationId, - inputUserWorkspaceId: input.userWorkspaceId, + authUserWorkspaceId, + authApiKeyId, + authApplicationId, }); + + const created = createdItems[0]; + + if (!isDefined(created)) { + throw new NavigationMenuItemException( + 'Failed to create navigation menu item', + NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT, + ); + } + + return created; + } + + async createMany({ + inputs, + workspaceId, + authUserWorkspaceId, + authApiKeyId, + authApplicationId, + }: { + inputs: CreateNavigationMenuItemInput[]; + workspaceId: string; + authUserWorkspaceId?: string; + authApiKeyId?: string; + authApplicationId?: string; + }): Promise { + if (inputs.length === 0) { + return []; + } + + for (const input of inputs) { + await this.navigationMenuItemAccessService.canUserCreateNavigationMenuItem( + { + userWorkspaceId: authUserWorkspaceId, + workspaceId, + apiKeyId: authApiKeyId, + applicationId: authApplicationId, + inputUserWorkspaceId: input.userWorkspaceId, + }, + ); + } + const { workspaceCustomFlatApplication } = await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { workspaceId }, @@ -150,30 +197,57 @@ export class NavigationMenuItemService { }, ); - const normalizedInput: CreateNavigationMenuItemInput = { - ...input, - userWorkspaceId: - isDefined(input.userWorkspaceId) && isDefined(authUserWorkspaceId) - ? authUserWorkspaceId - : input.userWorkspaceId, - }; + const optimisticFlatNavigationMenuItemMaps = structuredClone( + existingFlatNavigationMenuItemMaps, + ); - const flatNavigationMenuItemToCreate = - fromCreateNavigationMenuItemInputToFlatNavigationMenuItemToCreate({ - createNavigationMenuItemInput: normalizedInput, - workspaceId, - flatApplication: workspaceCustomFlatApplication, - flatNavigationMenuItemMaps: existingFlatNavigationMenuItemMaps, - flatObjectMetadataMaps, - flatViewMaps, + const foldersFirst = [...inputs].sort((a, b) => { + const aIsFolder = a.type === NavigationMenuItemType.FOLDER ? 0 : 1; + const bIsFolder = b.type === NavigationMenuItemType.FOLDER ? 0 : 1; + + return aIsFolder - bIsFolder; + }); + + const flatEntityByInput = new Map< + CreateNavigationMenuItemInput, + FlatNavigationMenuItem + >(); + + const flatNavigationMenuItemsToCreate = foldersFirst.map((input) => { + const normalizedInput: CreateNavigationMenuItemInput = { + ...input, + userWorkspaceId: + isDefined(input.userWorkspaceId) && isDefined(authUserWorkspaceId) + ? authUserWorkspaceId + : input.userWorkspaceId, + }; + + const flatNavigationMenuItemToCreate = + fromCreateNavigationMenuItemInputToFlatNavigationMenuItemToCreate({ + createNavigationMenuItemInput: normalizedInput, + workspaceId, + flatApplication: workspaceCustomFlatApplication, + flatNavigationMenuItemMaps: optimisticFlatNavigationMenuItemMaps, + flatObjectMetadataMaps, + flatViewMaps, + }); + + addFlatNavigationMenuItemToMapsAndUpdateIndex({ + flatNavigationMenuItem: flatNavigationMenuItemToCreate, + flatNavigationMenuItemMaps: optimisticFlatNavigationMenuItemMaps, }); + flatEntityByInput.set(input, flatNavigationMenuItemToCreate); + + return flatNavigationMenuItemToCreate; + }); + const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { allFlatEntityOperationByMetadataName: { navigationMenuItem: { - flatEntityToCreate: [flatNavigationMenuItemToCreate], + flatEntityToCreate: flatNavigationMenuItemsToCreate, flatEntityToDelete: [], flatEntityToUpdate: [], }, @@ -188,7 +262,7 @@ export class NavigationMenuItemService { if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, - 'Multiple validation errors occurred while creating navigation menu item', + 'Multiple validation errors occurred while creating navigation menu items', ); } @@ -200,11 +274,13 @@ export class NavigationMenuItemService { }, ); - return fromFlatNavigationMenuItemToNavigationMenuItemDto( - findFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityId: flatNavigationMenuItemToCreate.id, - flatEntityMaps: recomputedFlatNavigationMenuItemMaps, - }), + return inputs.map((input) => + fromFlatNavigationMenuItemToNavigationMenuItemDto( + findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: flatEntityByInput.get(input)!.id, + flatEntityMaps: recomputedFlatNavigationMenuItemMaps, + }), + ), ); } @@ -221,6 +297,45 @@ export class NavigationMenuItemService { authApiKeyId?: string; authApplicationId?: string; }): Promise { + const { id, ...update } = input; + + const updatedItems = await this.updateMany({ + inputs: [{ id, update }], + workspaceId, + authUserWorkspaceId, + authApiKeyId, + authApplicationId, + }); + + const updated = updatedItems[0]; + + if (!isDefined(updated)) { + throw new NavigationMenuItemException( + 'Failed to update navigation menu item', + NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT, + ); + } + + return updated; + } + + async updateMany({ + inputs, + workspaceId, + authUserWorkspaceId, + authApiKeyId, + authApplicationId, + }: { + inputs: UpdateOneNavigationMenuItemInput[]; + workspaceId: string; + authUserWorkspaceId?: string; + authApiKeyId?: string; + authApplicationId?: string; + }): Promise { + if (inputs.length === 0) { + return []; + } + const { workspaceCustomFlatApplication } = await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { workspaceId }, @@ -234,29 +349,41 @@ export class NavigationMenuItemService { }, ); - const existingNavigationMenuItem = findFlatEntityByIdInFlatEntityMaps({ - flatEntityId: input.id, - flatEntityMaps: existingFlatNavigationMenuItemMaps, - }); + const flatNavigationMenuItemsToUpdate: FlatNavigationMenuItem[] = []; - if (isDefined(existingNavigationMenuItem)) { - await this.navigationMenuItemAccessService.canUserUpdateNavigationMenuItem( - { - userWorkspaceId: authUserWorkspaceId, - workspaceId, - apiKeyId: authApiKeyId, - applicationId: authApplicationId, - existingUserWorkspaceId: existingNavigationMenuItem.userWorkspaceId, - }, + for (const { id, update } of inputs) { + const updateInput: UpdateNavigationMenuItemInput & { id: string } = { + ...update, + id, + }; + + const existingNavigationMenuItem = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: id, + flatEntityMaps: existingFlatNavigationMenuItemMaps, + }); + + if (isDefined(existingNavigationMenuItem)) { + await this.navigationMenuItemAccessService.canUserUpdateNavigationMenuItem( + { + userWorkspaceId: authUserWorkspaceId, + workspaceId, + apiKeyId: authApiKeyId, + applicationId: authApplicationId, + existingUserWorkspaceId: existingNavigationMenuItem.userWorkspaceId, + }, + ); + } + + flatNavigationMenuItemsToUpdate.push( + fromUpdateNavigationMenuItemInputToFlatNavigationMenuItemToUpdateOrThrow( + { + flatNavigationMenuItemMaps: existingFlatNavigationMenuItemMaps, + updateNavigationMenuItemInput: updateInput, + }, + ), ); } - const flatNavigationMenuItemToUpdate = - fromUpdateNavigationMenuItemInputToFlatNavigationMenuItemToUpdateOrThrow({ - flatNavigationMenuItemMaps: existingFlatNavigationMenuItemMaps, - updateNavigationMenuItemInput: input, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { @@ -264,7 +391,7 @@ export class NavigationMenuItemService { navigationMenuItem: { flatEntityToCreate: [], flatEntityToDelete: [], - flatEntityToUpdate: [flatNavigationMenuItemToUpdate], + flatEntityToUpdate: flatNavigationMenuItemsToUpdate, }, }, workspaceId, @@ -277,7 +404,7 @@ export class NavigationMenuItemService { if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, - 'Multiple validation errors occurred while updating navigation menu item', + 'Multiple validation errors occurred while updating navigation menu items', ); } @@ -289,11 +416,13 @@ export class NavigationMenuItemService { }, ); - return fromFlatNavigationMenuItemToNavigationMenuItemDto( - findFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityId: input.id, - flatEntityMaps: recomputedFlatNavigationMenuItemMaps, - }), + return inputs.map(({ id }) => + fromFlatNavigationMenuItemToNavigationMenuItemDto( + findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: id, + flatEntityMaps: recomputedFlatNavigationMenuItemMaps, + }), + ), ); } @@ -310,6 +439,53 @@ export class NavigationMenuItemService { authApiKeyId?: string; authApplicationId?: string; }): Promise { + const deletedItems = await this.deleteMany({ + ids: [id], + workspaceId, + authUserWorkspaceId, + authApiKeyId, + authApplicationId, + }); + + const deleted = deletedItems[0]; + + if (!isDefined(deleted)) { + throw new NavigationMenuItemException( + 'Failed to delete navigation menu item', + NavigationMenuItemExceptionCode.NAVIGATION_MENU_ITEM_NOT_FOUND, + ); + } + + return deleted; + } + + async deleteMany({ + ids, + workspaceId, + authUserWorkspaceId, + authApiKeyId, + authApplicationId, + }: { + ids: string[]; + workspaceId: string; + authUserWorkspaceId?: string; + authApiKeyId?: string; + authApplicationId?: string; + }): Promise { + if (ids.length === 0) { + return []; + } + + const uniqueOrderedIds: string[] = []; + const seenId = new Set(); + + for (const id of ids) { + if (!seenId.has(id)) { + seenId.add(id); + uniqueOrderedIds.push(id); + } + } + const { workspaceCustomFlatApplication } = await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { workspaceId }, @@ -323,30 +499,55 @@ export class NavigationMenuItemService { }, ); - const flatNavigationMenuItemToDelete = - fromDeleteNavigationMenuItemInputToFlatNavigationMenuItemOrThrow({ - flatNavigationMenuItemMaps: existingFlatNavigationMenuItemMaps, - navigationMenuItemId: id, - }); + const flatEntitiesToDeleteOrdered: FlatNavigationMenuItem[] = []; + const seenDeleteId = new Set(); + const deletedNavigationMenuItemDtoByRequestedId = new Map< + string, + NavigationMenuItemDTO + >(); - await this.navigationMenuItemAccessService.canUserDeleteNavigationMenuItem({ - userWorkspaceId: authUserWorkspaceId, - workspaceId, - apiKeyId: authApiKeyId, - applicationId: authApplicationId, - existingUserWorkspaceId: flatNavigationMenuItemToDelete.userWorkspaceId, - }); + for (const requestedId of uniqueOrderedIds) { + const flatNavigationMenuItemRoot = + fromDeleteNavigationMenuItemInputToFlatNavigationMenuItemOrThrow({ + flatNavigationMenuItemMaps: existingFlatNavigationMenuItemMaps, + navigationMenuItemId: requestedId, + }); - const flatEntitiesToDelete = [flatNavigationMenuItemToDelete]; + await this.navigationMenuItemAccessService.canUserDeleteNavigationMenuItem( + { + userWorkspaceId: authUserWorkspaceId, + workspaceId, + apiKeyId: authApiKeyId, + applicationId: authApplicationId, + existingUserWorkspaceId: flatNavigationMenuItemRoot.userWorkspaceId, + }, + ); - if (flatNavigationMenuItemToDelete.type === NavigationMenuItemType.FOLDER) { - const userWorkspaceIdKey = - flatNavigationMenuItemToDelete.userWorkspaceId ?? 'null'; - const folderChildren = - existingFlatNavigationMenuItemMaps.byUserWorkspaceIdAndFolderId[ - userWorkspaceIdKey - ]?.[id] ?? []; - flatEntitiesToDelete.unshift(...folderChildren); + deletedNavigationMenuItemDtoByRequestedId.set( + requestedId, + fromFlatNavigationMenuItemToNavigationMenuItemDto( + flatNavigationMenuItemRoot, + ), + ); + + const flatEntitiesForRoot = [flatNavigationMenuItemRoot]; + + if (flatNavigationMenuItemRoot.type === NavigationMenuItemType.FOLDER) { + const userWorkspaceIdKey = + flatNavigationMenuItemRoot.userWorkspaceId ?? 'null'; + const folderChildren = + existingFlatNavigationMenuItemMaps.byUserWorkspaceIdAndFolderId[ + userWorkspaceIdKey + ]?.[requestedId] ?? []; + flatEntitiesForRoot.unshift(...folderChildren); + } + + for (const flatEntity of flatEntitiesForRoot) { + if (!seenDeleteId.has(flatEntity.id)) { + seenDeleteId.add(flatEntity.id); + flatEntitiesToDeleteOrdered.push(flatEntity); + } + } } const validateAndBuildResult = @@ -355,7 +556,7 @@ export class NavigationMenuItemService { allFlatEntityOperationByMetadataName: { navigationMenuItem: { flatEntityToCreate: [], - flatEntityToDelete: flatEntitiesToDelete, + flatEntityToDelete: flatEntitiesToDeleteOrdered, flatEntityToUpdate: [], }, }, @@ -369,13 +570,22 @@ export class NavigationMenuItemService { if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, - 'Multiple validation errors occurred while deleting navigation menu item', + 'Multiple validation errors occurred while deleting navigation menu items', ); } - return fromFlatNavigationMenuItemToNavigationMenuItemDto( - flatNavigationMenuItemToDelete, - ); + return uniqueOrderedIds.map((requestedId) => { + const dto = deletedNavigationMenuItemDtoByRequestedId.get(requestedId); + + if (!isDefined(dto)) { + throw new NavigationMenuItemException( + 'Failed to resolve deleted navigation menu item in batch', + NavigationMenuItemExceptionCode.NAVIGATION_MENU_ITEM_NOT_FOUND, + ); + } + + return dto; + }); } async findTargetRecord({ diff --git a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-batch-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-batch-creation.integration-spec.ts.snap new file mode 100644 index 0000000000..52bf2365fd --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-batch-creation.integration-spec.ts.snap @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`NavigationMenuItem batch creation should fail when batch contains item with invalid folderId 1`] = ` +{ + "extensions": { + "code": "GRAPHQL_VALIDATION_FAILED", + "http": { + "status": 400, + }, + "userFriendlyMessage": "An error occurred.", + "value": "not-a-valid-uuid", + }, + "message": "Invalid UUID: 'not-a-valid-uuid'", + "name": "ValidationError", +} +`; + +exports[`NavigationMenuItem batch creation should fail when batch contains item with invalid targetObjectMetadataId 1`] = ` +{ + "extensions": { + "code": "GRAPHQL_VALIDATION_FAILED", + "http": { + "status": 400, + }, + "userFriendlyMessage": "An error occurred.", + "value": "not-a-valid-uuid", + }, + "message": "Invalid UUID: 'not-a-valid-uuid'", + "name": "ValidationError", +} +`; + +exports[`NavigationMenuItem batch creation should fail when batch contains item with invalid targetRecordId 1`] = ` +{ + "extensions": { + "code": "GRAPHQL_VALIDATION_FAILED", + "http": { + "status": 400, + }, + "userFriendlyMessage": "An error occurred.", + "value": "not-a-valid-uuid", + }, + "message": "Invalid UUID: 'not-a-valid-uuid'", + "name": "ValidationError", +} +`; + +exports[`NavigationMenuItem batch creation should fail when batch contains item with missing targetRecordId 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "subCode": "RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND", + "userFriendlyMessage": "An error occurred.", + }, + "message": "Could not find objectMetadata for given targetObjectMetadataId", + "name": "NotFoundError", +} +`; + diff --git a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-batch-deletion.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-batch-deletion.integration-spec.ts.snap new file mode 100644 index 0000000000..d6b3513b69 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-batch-deletion.integration-spec.ts.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`NavigationMenuItem batch deletion should fail when batch contains a non-existent id among valid ones 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "subCode": "NAVIGATION_MENU_ITEM_NOT_FOUND", + "userFriendlyMessage": "Navigation menu item not found.", + }, + "message": "Navigation menu item not found", + "name": "NotFoundError", +} +`; + +exports[`NavigationMenuItem batch deletion should fail when deleting with non-existent id 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "subCode": "NAVIGATION_MENU_ITEM_NOT_FOUND", + "userFriendlyMessage": "Navigation menu item not found.", + }, + "message": "Navigation menu item not found", + "name": "NotFoundError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-batch-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-batch-update.integration-spec.ts.snap new file mode 100644 index 0000000000..1a9ead5319 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-batch-update.integration-spec.ts.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`NavigationMenuItem batch update should fail when batch contains an update for non-existent item 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "subCode": "NAVIGATION_MENU_ITEM_NOT_FOUND", + "userFriendlyMessage": "Navigation menu item not found.", + }, + "message": "Navigation menu item not found", + "name": "NotFoundError", +} +`; + +exports[`NavigationMenuItem batch update should fail when updating with invalid folderId 1`] = ` +{ + "extensions": { + "code": "GRAPHQL_VALIDATION_FAILED", + "http": { + "status": 400, + }, + "userFriendlyMessage": "An error occurred.", + "value": "not-a-valid-uuid", + }, + "message": "Invalid UUID: 'not-a-valid-uuid'", + "name": "ValidationError", +} +`; + +exports[`NavigationMenuItem batch update should fail when updating with non-existent id 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "subCode": "NAVIGATION_MENU_ITEM_NOT_FOUND", + "userFriendlyMessage": "Navigation menu item not found.", + }, + "message": "Navigation menu item not found", + "name": "NotFoundError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-creation.integration-spec.ts.snap index f23475405d..d56e8de8d9 100644 --- a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-creation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-creation.integration-spec.ts.snap @@ -120,7 +120,7 @@ exports[`NavigationMenuItem creation should fail when creating with missing targ }, "userFriendlyMessage": "Metadata validation failed", }, - "message": "Multiple validation errors occurred while creating navigation menu item", + "message": "Multiple validation errors occurred while creating navigation menu items", "name": "GraphQLError", } `; diff --git a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-update-circular-dependency.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-update-circular-dependency.integration-spec.ts.snap index 36e7942485..becb4f1187 100644 --- a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-update-circular-dependency.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-update-circular-dependency.integration-spec.ts.snap @@ -30,7 +30,7 @@ exports[`Navigation Menu Item update should fail with circular dependency when f }, "userFriendlyMessage": "Metadata validation failed", }, - "message": "Multiple validation errors occurred while updating navigation menu item", + "message": "Multiple validation errors occurred while updating navigation menu items", "name": "GraphQLError", } `; @@ -65,7 +65,7 @@ exports[`Navigation Menu Item update should fail with circular dependency when u }, "userFriendlyMessage": "Metadata validation failed", }, - "message": "Multiple validation errors occurred while updating navigation menu item", + "message": "Multiple validation errors occurred while updating navigation menu items", "name": "GraphQLError", } `; diff --git a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/failing-navigation-menu-item-batch-creation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/failing-navigation-menu-item-batch-creation.integration-spec.ts new file mode 100644 index 0000000000..690c183dc1 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/failing-navigation-menu-item-batch-creation.integration-spec.ts @@ -0,0 +1,81 @@ +import { faker } from '@faker-js/faker'; +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { createManyNavigationMenuItems } from 'test/integration/metadata/suites/navigation-menu-item/utils/create-many-navigation-menu-items.util'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; +import { NavigationMenuItemType } from 'twenty-shared/types'; + +import { type CreateNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/create-navigation-menu-item.input'; + +type TestContext = { + inputs: CreateNavigationMenuItemInput[]; +}; + +const failingBatchCreationTestCases: EachTestingContext[] = [ + { + title: 'when batch contains item with missing targetRecordId', + context: { + inputs: [ + { + type: NavigationMenuItemType.RECORD, + targetObjectMetadataId: faker.string.uuid(), + } as CreateNavigationMenuItemInput, + ], + }, + }, + { + title: 'when batch contains item with invalid targetRecordId', + context: { + inputs: [ + { + type: NavigationMenuItemType.RECORD, + targetRecordId: 'not-a-valid-uuid', + targetObjectMetadataId: faker.string.uuid(), + }, + ], + }, + }, + { + title: 'when batch contains item with invalid targetObjectMetadataId', + context: { + inputs: [ + { + type: NavigationMenuItemType.RECORD, + targetRecordId: faker.string.uuid(), + targetObjectMetadataId: 'not-a-valid-uuid', + }, + ], + }, + }, + { + title: 'when batch contains item with invalid folderId', + context: { + inputs: [ + { + type: NavigationMenuItemType.RECORD, + targetRecordId: faker.string.uuid(), + targetObjectMetadataId: faker.string.uuid(), + folderId: 'not-a-valid-uuid', + }, + ], + }, + }, +]; + +describe('NavigationMenuItem batch creation should fail', () => { + it.each(eachTestingContextFilter(failingBatchCreationTestCases))( + '$title', + async ({ context }) => { + const { errors } = await createManyNavigationMenuItems({ + expectToFail: true, + inputs: context.inputs, + }); + + expectOneNotInternalServerErrorSnapshot({ + errors, + }); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/failing-navigation-menu-item-batch-deletion.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/failing-navigation-menu-item-batch-deletion.integration-spec.ts new file mode 100644 index 0000000000..8845c13874 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/failing-navigation-menu-item-batch-deletion.integration-spec.ts @@ -0,0 +1,100 @@ +import { faker } from '@faker-js/faker'; +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { createNavigationMenuItem } from 'test/integration/metadata/suites/navigation-menu-item/utils/create-navigation-menu-item.util'; +import { deleteNavigationMenuItem } from 'test/integration/metadata/suites/navigation-menu-item/utils/delete-navigation-menu-item.util'; +import { deleteManyNavigationMenuItems } from 'test/integration/metadata/suites/navigation-menu-item/utils/delete-many-navigation-menu-items.util'; +import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; +import { NavigationMenuItemType } from 'twenty-shared/types'; +import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; + +type TestContext = { + buildIds: (realItemId: string) => string[]; +}; + +const failingBatchDeletionTestCases: EachTestingContext[] = [ + { + title: 'when deleting with non-existent id', + context: { + buildIds: () => [faker.string.uuid()], + }, + }, + { + title: 'when batch contains a non-existent id among valid ones', + context: { + buildIds: (realItemId: string) => [realItemId, faker.string.uuid()], + }, + }, +]; + +describe('NavigationMenuItem batch deletion should fail', () => { + let createdNavigationMenuItemId: string; + let personObjectMetadataId: string; + + beforeAll(async () => { + const { objects } = await findManyObjectMetadata({ + expectToFail: false, + input: { + filter: {}, + paging: { first: 1000 }, + }, + gqlFields: ` + id + nameSingular + `, + }); + + jestExpectToBeDefined(objects); + + const personObjectMetadata = objects.find( + (object: { nameSingular: string }) => object.nameSingular === 'person', + ); + + jestExpectToBeDefined(personObjectMetadata); + + personObjectMetadataId = personObjectMetadata.id; + }); + + beforeEach(async () => { + const { data } = await createNavigationMenuItem({ + expectToFail: false, + input: { + type: NavigationMenuItemType.RECORD, + targetRecordId: faker.string.uuid(), + targetObjectMetadataId: personObjectMetadataId, + position: 1, + }, + }); + + createdNavigationMenuItemId = data.createNavigationMenuItem.id; + }); + + afterEach(async () => { + if (createdNavigationMenuItemId) { + await deleteNavigationMenuItem({ + expectToFail: false, + input: { id: createdNavigationMenuItemId }, + }); + createdNavigationMenuItemId = undefined as unknown as string; + } + }); + + it.each(eachTestingContextFilter(failingBatchDeletionTestCases))( + '$title', + async ({ context }) => { + const ids = context.buildIds(createdNavigationMenuItemId); + + const { errors } = await deleteManyNavigationMenuItems({ + expectToFail: true, + ids, + }); + + expectOneNotInternalServerErrorSnapshot({ + errors, + }); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/failing-navigation-menu-item-batch-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/failing-navigation-menu-item-batch-update.integration-spec.ts new file mode 100644 index 0000000000..6433837061 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/failing-navigation-menu-item-batch-update.integration-spec.ts @@ -0,0 +1,127 @@ +import { faker } from '@faker-js/faker'; +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { createNavigationMenuItem } from 'test/integration/metadata/suites/navigation-menu-item/utils/create-navigation-menu-item.util'; +import { deleteNavigationMenuItem } from 'test/integration/metadata/suites/navigation-menu-item/utils/delete-navigation-menu-item.util'; +import { updateManyNavigationMenuItems } from 'test/integration/metadata/suites/navigation-menu-item/utils/update-many-navigation-menu-items.util'; +import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; +import { NavigationMenuItemType } from 'twenty-shared/types'; +import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; + +import { type UpdateOneNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/update-navigation-menu-item.input'; + +type TestContext = { + buildInputs: (itemId: string) => UpdateOneNavigationMenuItemInput[]; +}; + +const failingBatchUpdateTestCases: EachTestingContext[] = [ + { + title: 'when updating with non-existent id', + context: { + buildInputs: () => [ + { + id: faker.string.uuid(), + update: { position: 5 }, + }, + ], + }, + }, + { + title: 'when updating with invalid folderId', + context: { + buildInputs: (itemId: string) => [ + { + id: itemId, + update: { folderId: 'not-a-valid-uuid' }, + }, + ], + }, + }, + { + title: 'when batch contains an update for non-existent item', + context: { + buildInputs: (itemId: string) => [ + { + id: itemId, + update: { position: 10 }, + }, + { + id: faker.string.uuid(), + update: { position: 20 }, + }, + ], + }, + }, +]; + +describe('NavigationMenuItem batch update should fail', () => { + let createdNavigationMenuItemId: string; + let personObjectMetadataId: string; + + beforeAll(async () => { + const { objects } = await findManyObjectMetadata({ + expectToFail: false, + input: { + filter: {}, + paging: { first: 1000 }, + }, + gqlFields: ` + id + nameSingular + `, + }); + + jestExpectToBeDefined(objects); + + const personObjectMetadata = objects.find( + (object: { nameSingular: string }) => object.nameSingular === 'person', + ); + + jestExpectToBeDefined(personObjectMetadata); + + personObjectMetadataId = personObjectMetadata.id; + }); + + beforeEach(async () => { + const { data } = await createNavigationMenuItem({ + expectToFail: false, + input: { + type: NavigationMenuItemType.RECORD, + targetRecordId: faker.string.uuid(), + targetObjectMetadataId: personObjectMetadataId, + position: 1, + }, + }); + + createdNavigationMenuItemId = data.createNavigationMenuItem.id; + }); + + afterEach(async () => { + if (createdNavigationMenuItemId) { + await deleteNavigationMenuItem({ + expectToFail: false, + input: { id: createdNavigationMenuItemId }, + }); + createdNavigationMenuItemId = undefined as unknown as string; + } + }); + + it.each(eachTestingContextFilter(failingBatchUpdateTestCases))( + '$title', + async ({ context }) => { + const inputs = context.buildInputs(createdNavigationMenuItemId); + + const { errors } = await updateManyNavigationMenuItems({ + expectToFail: true, + inputs, + }); + + expectOneNotInternalServerErrorSnapshot({ + errors, + }); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/successful-navigation-menu-item-batch-creation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/successful-navigation-menu-item-batch-creation.integration-spec.ts new file mode 100644 index 0000000000..f0a182b4df --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/successful-navigation-menu-item-batch-creation.integration-spec.ts @@ -0,0 +1,218 @@ +import { faker } from '@faker-js/faker'; +import { NavigationMenuItemType } from 'twenty-shared/types'; +import { createManyNavigationMenuItems } from 'test/integration/metadata/suites/navigation-menu-item/utils/create-many-navigation-menu-items.util'; +import { deleteManyNavigationMenuItems } from 'test/integration/metadata/suites/navigation-menu-item/utils/delete-many-navigation-menu-items.util'; +import { findNavigationMenuItems } from 'test/integration/metadata/suites/navigation-menu-item/utils/find-navigation-menu-items.util'; +import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util'; +import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; + +describe('NavigationMenuItem batch creation should succeed', () => { + let personObjectMetadataId: string; + let companyObjectMetadataId: string; + let createdIds: string[] = []; + + beforeAll(async () => { + const { objects } = await findManyObjectMetadata({ + expectToFail: false, + input: { + filter: {}, + paging: { first: 1000 }, + }, + gqlFields: ` + id + nameSingular + `, + }); + + jestExpectToBeDefined(objects); + + const personObjectMetadata = objects.find( + (object: { nameSingular: string }) => object.nameSingular === 'person', + ); + const companyObjectMetadata = objects.find( + (object: { nameSingular: string }) => object.nameSingular === 'company', + ); + + jestExpectToBeDefined(personObjectMetadata); + jestExpectToBeDefined(companyObjectMetadata); + + personObjectMetadataId = personObjectMetadata.id; + companyObjectMetadataId = companyObjectMetadata.id; + }); + + afterEach(async () => { + if (createdIds.length > 0) { + await deleteManyNavigationMenuItems({ + ids: createdIds, + expectToFail: false, + }); + createdIds = []; + } + }); + + it('should create multiple navigation menu items in a single batch', async () => { + const targetRecordId1 = faker.string.uuid(); + const targetRecordId2 = faker.string.uuid(); + + const { data } = await createManyNavigationMenuItems({ + expectToFail: false, + inputs: [ + { + type: NavigationMenuItemType.RECORD, + targetRecordId: targetRecordId1, + targetObjectMetadataId: personObjectMetadataId, + }, + { + type: NavigationMenuItemType.RECORD, + targetRecordId: targetRecordId2, + targetObjectMetadataId: companyObjectMetadataId, + }, + ], + }); + + jestExpectToBeDefined(data?.createManyNavigationMenuItems); + + const items = data.createManyNavigationMenuItems; + + createdIds = items.map((item) => item.id); + + expect(items).toHaveLength(2); + + expect(items[0]).toMatchObject({ + id: expect.any(String), + targetRecordId: targetRecordId1, + targetObjectMetadataId: personObjectMetadataId, + }); + + expect(items[1]).toMatchObject({ + id: expect.any(String), + targetRecordId: targetRecordId2, + targetObjectMetadataId: companyObjectMetadataId, + }); + }); + + it('should create a folder and a child item in a single batch', async () => { + const folderId = faker.string.uuid(); + const targetRecordId = faker.string.uuid(); + + const { data } = await createManyNavigationMenuItems({ + expectToFail: false, + inputs: [ + { + id: folderId, + type: NavigationMenuItemType.FOLDER, + name: 'Test Folder', + position: 1, + }, + { + type: NavigationMenuItemType.RECORD, + targetRecordId, + targetObjectMetadataId: personObjectMetadataId, + folderId, + position: 1, + }, + ], + }); + + jestExpectToBeDefined(data?.createManyNavigationMenuItems); + + const items = data.createManyNavigationMenuItems; + + createdIds = items.map((item) => item.id); + + expect(items).toHaveLength(2); + + expect(items[0]).toMatchObject({ + id: folderId, + type: NavigationMenuItemType.FOLDER, + name: 'Test Folder', + }); + + expect(items[1]).toMatchObject({ + targetRecordId, + folderId, + }); + }); + + it('should preserve input order in the response', async () => { + const targetRecordId1 = faker.string.uuid(); + const targetRecordId2 = faker.string.uuid(); + const targetRecordId3 = faker.string.uuid(); + + const { data } = await createManyNavigationMenuItems({ + expectToFail: false, + inputs: [ + { + type: NavigationMenuItemType.RECORD, + targetRecordId: targetRecordId1, + targetObjectMetadataId: personObjectMetadataId, + position: 3, + }, + { + type: NavigationMenuItemType.RECORD, + targetRecordId: targetRecordId2, + targetObjectMetadataId: personObjectMetadataId, + position: 1, + }, + { + type: NavigationMenuItemType.RECORD, + targetRecordId: targetRecordId3, + targetObjectMetadataId: personObjectMetadataId, + position: 2, + }, + ], + }); + + jestExpectToBeDefined(data?.createManyNavigationMenuItems); + + const items = data.createManyNavigationMenuItems; + + createdIds = items.map((item) => item.id); + + expect(items).toHaveLength(3); + expect(items[0].targetRecordId).toBe(targetRecordId1); + expect(items[1].targetRecordId).toBe(targetRecordId2); + expect(items[2].targetRecordId).toBe(targetRecordId3); + }); + + it('should return empty array for empty inputs', async () => { + const { data } = await createManyNavigationMenuItems({ + expectToFail: false, + inputs: [], + }); + + jestExpectToBeDefined(data?.createManyNavigationMenuItems); + expect(data.createManyNavigationMenuItems).toHaveLength(0); + }); + + it('should create items visible in findAll', async () => { + const targetRecordId = faker.string.uuid(); + + const { data: createData } = await createManyNavigationMenuItems({ + expectToFail: false, + inputs: [ + { + type: NavigationMenuItemType.RECORD, + targetRecordId, + targetObjectMetadataId: personObjectMetadataId, + }, + ], + }); + + jestExpectToBeDefined(createData?.createManyNavigationMenuItems); + createdIds = createData.createManyNavigationMenuItems.map( + (item) => item.id, + ); + + const { data: findData } = await findNavigationMenuItems({ + expectToFail: false, + input: undefined, + }); + + const foundItem = findData.navigationMenuItems.find( + (item) => item.id === createdIds[0], + ); + + expect(foundItem).toBeDefined(); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/successful-navigation-menu-item-batch-deletion.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/successful-navigation-menu-item-batch-deletion.integration-spec.ts new file mode 100644 index 0000000000..71780d1673 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/successful-navigation-menu-item-batch-deletion.integration-spec.ts @@ -0,0 +1,182 @@ +import { faker } from '@faker-js/faker'; +import { NavigationMenuItemType } from 'twenty-shared/types'; +import { createManyNavigationMenuItems } from 'test/integration/metadata/suites/navigation-menu-item/utils/create-many-navigation-menu-items.util'; +import { deleteManyNavigationMenuItems } from 'test/integration/metadata/suites/navigation-menu-item/utils/delete-many-navigation-menu-items.util'; +import { findNavigationMenuItems } from 'test/integration/metadata/suites/navigation-menu-item/utils/find-navigation-menu-items.util'; +import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util'; +import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; + +describe('NavigationMenuItem batch deletion should succeed', () => { + let personObjectMetadataId: string; + let pendingCleanupIds: string[] = []; + + afterEach(async () => { + if (pendingCleanupIds.length > 0) { + await deleteManyNavigationMenuItems({ + ids: pendingCleanupIds, + expectToFail: false, + }); + pendingCleanupIds = []; + } + }); + + beforeAll(async () => { + const { objects } = await findManyObjectMetadata({ + expectToFail: false, + input: { + filter: {}, + paging: { first: 1000 }, + }, + gqlFields: ` + id + nameSingular + `, + }); + + jestExpectToBeDefined(objects); + + const personObjectMetadata = objects.find( + (object: { nameSingular: string }) => object.nameSingular === 'person', + ); + + jestExpectToBeDefined(personObjectMetadata); + + personObjectMetadataId = personObjectMetadata.id; + }); + + it('should delete multiple items in a single batch', async () => { + const { data: createData } = await createManyNavigationMenuItems({ + expectToFail: false, + inputs: [ + { + type: NavigationMenuItemType.RECORD, + targetRecordId: faker.string.uuid(), + targetObjectMetadataId: personObjectMetadataId, + }, + { + type: NavigationMenuItemType.RECORD, + targetRecordId: faker.string.uuid(), + targetObjectMetadataId: personObjectMetadataId, + }, + ], + }); + + jestExpectToBeDefined(createData?.createManyNavigationMenuItems); + const items = createData.createManyNavigationMenuItems; + const idsToDelete = items.map((item) => item.id); + + pendingCleanupIds = [...idsToDelete]; + + const { data: deleteData } = await deleteManyNavigationMenuItems({ + ids: idsToDelete, + expectToFail: false, + }); + + pendingCleanupIds = []; + + jestExpectToBeDefined(deleteData?.deleteManyNavigationMenuItems); + expect(deleteData.deleteManyNavigationMenuItems).toHaveLength(2); + + const { data: findData } = await findNavigationMenuItems({ + expectToFail: false, + input: undefined, + }); + + for (const deletedId of idsToDelete) { + const foundItem = findData.navigationMenuItems.find( + (item) => item.id === deletedId, + ); + expect(foundItem).toBeUndefined(); + } + }); + + it('should delete a folder and cascade to its children', async () => { + const folderId = faker.string.uuid(); + + const { data: createData } = await createManyNavigationMenuItems({ + expectToFail: false, + inputs: [ + { + id: folderId, + type: NavigationMenuItemType.FOLDER, + name: 'Cascade Folder', + position: 1, + }, + { + type: NavigationMenuItemType.RECORD, + targetRecordId: faker.string.uuid(), + targetObjectMetadataId: personObjectMetadataId, + folderId, + position: 1, + }, + ], + }); + + jestExpectToBeDefined(createData?.createManyNavigationMenuItems); + const items = createData.createManyNavigationMenuItems; + const childId = items[1].id; + + pendingCleanupIds = items.map((item) => item.id); + + const { data: deleteData } = await deleteManyNavigationMenuItems({ + ids: [folderId], + expectToFail: false, + }); + + pendingCleanupIds = []; + + jestExpectToBeDefined(deleteData?.deleteManyNavigationMenuItems); + expect(deleteData.deleteManyNavigationMenuItems).toHaveLength(1); + + const { data: findData } = await findNavigationMenuItems({ + expectToFail: false, + input: undefined, + }); + + expect( + findData.navigationMenuItems.find((item) => item.id === folderId), + ).toBeUndefined(); + + expect( + findData.navigationMenuItems.find((item) => item.id === childId), + ).toBeUndefined(); + }); + + it('should handle deduplication of ids', async () => { + const { data: createData } = await createManyNavigationMenuItems({ + expectToFail: false, + inputs: [ + { + type: NavigationMenuItemType.RECORD, + targetRecordId: faker.string.uuid(), + targetObjectMetadataId: personObjectMetadataId, + }, + ], + }); + + jestExpectToBeDefined(createData?.createManyNavigationMenuItems); + const itemId = createData.createManyNavigationMenuItems[0].id; + + pendingCleanupIds = [itemId]; + + const { data: deleteData } = await deleteManyNavigationMenuItems({ + ids: [itemId, itemId], + expectToFail: false, + }); + + pendingCleanupIds = []; + + jestExpectToBeDefined(deleteData?.deleteManyNavigationMenuItems); + expect(deleteData.deleteManyNavigationMenuItems).toHaveLength(1); + }); + + it('should return empty array for empty ids', async () => { + const { data } = await deleteManyNavigationMenuItems({ + ids: [], + expectToFail: false, + }); + + jestExpectToBeDefined(data?.deleteManyNavigationMenuItems); + expect(data.deleteManyNavigationMenuItems).toHaveLength(0); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/successful-navigation-menu-item-batch-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/successful-navigation-menu-item-batch-update.integration-spec.ts new file mode 100644 index 0000000000..fde1b1b763 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/successful-navigation-menu-item-batch-update.integration-spec.ts @@ -0,0 +1,150 @@ +import { faker } from '@faker-js/faker'; +import { NavigationMenuItemType } from 'twenty-shared/types'; +import { createManyNavigationMenuItems } from 'test/integration/metadata/suites/navigation-menu-item/utils/create-many-navigation-menu-items.util'; +import { deleteManyNavigationMenuItems } from 'test/integration/metadata/suites/navigation-menu-item/utils/delete-many-navigation-menu-items.util'; +import { updateManyNavigationMenuItems } from 'test/integration/metadata/suites/navigation-menu-item/utils/update-many-navigation-menu-items.util'; +import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util'; +import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; + +describe('NavigationMenuItem batch update should succeed', () => { + let personObjectMetadataId: string; + let createdIds: string[] = []; + + beforeAll(async () => { + const { objects } = await findManyObjectMetadata({ + expectToFail: false, + input: { + filter: {}, + paging: { first: 1000 }, + }, + gqlFields: ` + id + nameSingular + `, + }); + + jestExpectToBeDefined(objects); + + const personObjectMetadata = objects.find( + (object: { nameSingular: string }) => object.nameSingular === 'person', + ); + + jestExpectToBeDefined(personObjectMetadata); + + personObjectMetadataId = personObjectMetadata.id; + }); + + afterEach(async () => { + if (createdIds.length > 0) { + await deleteManyNavigationMenuItems({ + ids: createdIds, + expectToFail: false, + }); + createdIds = []; + } + }); + + it('should update positions of multiple items at once', async () => { + const { data: createData } = await createManyNavigationMenuItems({ + expectToFail: false, + inputs: [ + { + type: NavigationMenuItemType.RECORD, + targetRecordId: faker.string.uuid(), + targetObjectMetadataId: personObjectMetadataId, + position: 1, + }, + { + type: NavigationMenuItemType.RECORD, + targetRecordId: faker.string.uuid(), + targetObjectMetadataId: personObjectMetadataId, + position: 2, + }, + ], + }); + + jestExpectToBeDefined(createData?.createManyNavigationMenuItems); + const items = createData.createManyNavigationMenuItems; + createdIds = items.map((item) => item.id); + + const { data: updateData } = await updateManyNavigationMenuItems({ + expectToFail: false, + inputs: [ + { id: items[0].id, update: { position: 10 } }, + { id: items[1].id, update: { position: 20 } }, + ], + }); + + jestExpectToBeDefined(updateData?.updateManyNavigationMenuItems); + + const updatedItems = updateData.updateManyNavigationMenuItems; + + expect(updatedItems).toHaveLength(2); + expect(updatedItems[0]).toMatchObject({ + id: items[0].id, + position: 10, + }); + expect(updatedItems[1]).toMatchObject({ + id: items[1].id, + position: 20, + }); + }); + + it('should update folderIds of multiple items at once', async () => { + const folderId = faker.string.uuid(); + + const { data: createData } = await createManyNavigationMenuItems({ + expectToFail: false, + inputs: [ + { + id: folderId, + type: NavigationMenuItemType.FOLDER, + name: 'Batch Folder', + position: 1, + }, + { + type: NavigationMenuItemType.RECORD, + targetRecordId: faker.string.uuid(), + targetObjectMetadataId: personObjectMetadataId, + position: 1, + }, + { + type: NavigationMenuItemType.RECORD, + targetRecordId: faker.string.uuid(), + targetObjectMetadataId: personObjectMetadataId, + position: 2, + }, + ], + }); + + jestExpectToBeDefined(createData?.createManyNavigationMenuItems); + const items = createData.createManyNavigationMenuItems; + createdIds = items.map((item) => item.id); + + const { data: updateData } = await updateManyNavigationMenuItems({ + expectToFail: false, + inputs: [ + { id: items[1].id, update: { folderId } }, + { id: items[2].id, update: { folderId } }, + ], + }); + + jestExpectToBeDefined(updateData?.updateManyNavigationMenuItems); + + const updatedItems = updateData.updateManyNavigationMenuItems; + + expect(updatedItems).toHaveLength(2); + expect(updatedItems[0]).toMatchObject({ id: items[1].id, folderId }); + expect(updatedItems[1]).toMatchObject({ id: items[2].id, folderId }); + }); + + it('should return empty array for empty inputs', async () => { + const { data } = await updateManyNavigationMenuItems({ + expectToFail: false, + inputs: [], + }); + + jestExpectToBeDefined(data?.updateManyNavigationMenuItems); + expect(data.updateManyNavigationMenuItems).toHaveLength(0); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/utils/create-many-navigation-menu-items-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/utils/create-many-navigation-menu-items-query-factory.util.ts new file mode 100644 index 0000000000..ac716ed22a --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/utils/create-many-navigation-menu-items-query-factory.util.ts @@ -0,0 +1,42 @@ +import gql from 'graphql-tag'; + +import { type CreateNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/create-navigation-menu-item.input'; + +const DEFAULT_NAVIGATION_MENU_ITEM_GQL_FIELDS = ` + id + type + userWorkspaceId + targetRecordId + targetObjectMetadataId + viewId + folderId + position + name + link + icon + color + applicationId + createdAt + updatedAt +`; + +export const createManyNavigationMenuItemsQueryFactory = ({ + inputs, + gqlFields = DEFAULT_NAVIGATION_MENU_ITEM_GQL_FIELDS, +}: { + inputs: CreateNavigationMenuItemInput[]; + gqlFields?: string; +}) => ({ + query: gql` + mutation CreateManyNavigationMenuItems( + $inputs: [CreateNavigationMenuItemInput!]! + ) { + createManyNavigationMenuItems(inputs: $inputs) { + ${gqlFields} + } + } + `, + variables: { + inputs, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/utils/create-many-navigation-menu-items.util.ts b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/utils/create-many-navigation-menu-items.util.ts new file mode 100644 index 0000000000..396d1981d9 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/utils/create-many-navigation-menu-items.util.ts @@ -0,0 +1,47 @@ +import { createManyNavigationMenuItemsQueryFactory } from 'test/integration/metadata/suites/navigation-menu-item/utils/create-many-navigation-menu-items-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.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 CreateNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/create-navigation-menu-item.input'; +import { type NavigationMenuItemDTO } from 'src/engine/metadata-modules/navigation-menu-item/dtos/navigation-menu-item.dto'; + +export const createManyNavigationMenuItems = async ({ + inputs, + gqlFields, + expectToFail = false, + token, +}: { + inputs: CreateNavigationMenuItemInput[]; + gqlFields?: string; + expectToFail?: boolean; + token?: string; +}): CommonResponseBody<{ + createManyNavigationMenuItems: NavigationMenuItemDTO[]; +}> => { + const graphqlOperation = createManyNavigationMenuItemsQueryFactory({ + inputs, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: + 'NavigationMenuItems batch creation should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: + 'NavigationMenuItems batch creation has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/utils/delete-many-navigation-menu-items-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/utils/delete-many-navigation-menu-items-query-factory.util.ts new file mode 100644 index 0000000000..adda53697c --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/utils/delete-many-navigation-menu-items-query-factory.util.ts @@ -0,0 +1,28 @@ +import gql from 'graphql-tag'; + +const DEFAULT_NAVIGATION_MENU_ITEM_GQL_FIELDS = ` + id + type + targetRecordId + targetObjectMetadataId + position +`; + +export const deleteManyNavigationMenuItemsQueryFactory = ({ + ids, + gqlFields = DEFAULT_NAVIGATION_MENU_ITEM_GQL_FIELDS, +}: { + ids: string[]; + gqlFields?: string; +}) => ({ + query: gql` + mutation DeleteManyNavigationMenuItems($ids: [UUID!]!) { + deleteManyNavigationMenuItems(ids: $ids) { + ${gqlFields} + } + } + `, + variables: { + ids, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/utils/delete-many-navigation-menu-items.util.ts b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/utils/delete-many-navigation-menu-items.util.ts new file mode 100644 index 0000000000..ffa71c9d18 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/utils/delete-many-navigation-menu-items.util.ts @@ -0,0 +1,46 @@ +import { deleteManyNavigationMenuItemsQueryFactory } from 'test/integration/metadata/suites/navigation-menu-item/utils/delete-many-navigation-menu-items-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.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 NavigationMenuItemDTO } from 'src/engine/metadata-modules/navigation-menu-item/dtos/navigation-menu-item.dto'; + +export const deleteManyNavigationMenuItems = async ({ + ids, + gqlFields, + expectToFail = false, + token, +}: { + ids: string[]; + gqlFields?: string; + expectToFail?: boolean; + token?: string; +}): CommonResponseBody<{ + deleteManyNavigationMenuItems: NavigationMenuItemDTO[]; +}> => { + const graphqlOperation = deleteManyNavigationMenuItemsQueryFactory({ + ids, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: + 'NavigationMenuItems batch deletion should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: + 'NavigationMenuItems batch deletion has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/utils/update-many-navigation-menu-items-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/utils/update-many-navigation-menu-items-query-factory.util.ts new file mode 100644 index 0000000000..1b4a20c063 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/utils/update-many-navigation-menu-items-query-factory.util.ts @@ -0,0 +1,37 @@ +import gql from 'graphql-tag'; + +import { type UpdateOneNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/update-navigation-menu-item.input'; + +const DEFAULT_NAVIGATION_MENU_ITEM_GQL_FIELDS = ` + id + type + userWorkspaceId + targetRecordId + targetObjectMetadataId + folderId + position + applicationId + createdAt + updatedAt +`; + +export const updateManyNavigationMenuItemsQueryFactory = ({ + inputs, + gqlFields = DEFAULT_NAVIGATION_MENU_ITEM_GQL_FIELDS, +}: { + inputs: UpdateOneNavigationMenuItemInput[]; + gqlFields?: string; +}) => ({ + query: gql` + mutation UpdateManyNavigationMenuItems( + $inputs: [UpdateOneNavigationMenuItemInput!]! + ) { + updateManyNavigationMenuItems(inputs: $inputs) { + ${gqlFields} + } + } + `, + variables: { + inputs, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/utils/update-many-navigation-menu-items.util.ts b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/utils/update-many-navigation-menu-items.util.ts new file mode 100644 index 0000000000..42bff75c76 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/utils/update-many-navigation-menu-items.util.ts @@ -0,0 +1,47 @@ +import { updateManyNavigationMenuItemsQueryFactory } from 'test/integration/metadata/suites/navigation-menu-item/utils/update-many-navigation-menu-items-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.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 NavigationMenuItemDTO } from 'src/engine/metadata-modules/navigation-menu-item/dtos/navigation-menu-item.dto'; +import { type UpdateOneNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/update-navigation-menu-item.input'; + +export const updateManyNavigationMenuItems = async ({ + inputs, + gqlFields, + expectToFail = false, + token, +}: { + inputs: UpdateOneNavigationMenuItemInput[]; + gqlFields?: string; + expectToFail?: boolean; + token?: string; +}): CommonResponseBody<{ + updateManyNavigationMenuItems: NavigationMenuItemDTO[]; +}> => { + const graphqlOperation = updateManyNavigationMenuItemsQueryFactory({ + inputs, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: + 'NavigationMenuItems batch update should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: + 'NavigationMenuItems batch update has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +};