Batch create, update, and delete navigation menu items (#18882)
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@@ -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!
|
||||
|
||||
@@ -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']} })
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
File diff suppressed because one or more lines are too long
+28
-4
@@ -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 <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
|
||||
+5
-4
@@ -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 <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
|
||||
+27
-11
@@ -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 <Command onClick={handleClick} />;
|
||||
|
||||
+5
-4
@@ -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 <Command onClick={handleClick} />;
|
||||
|
||||
+5
-3
@@ -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
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -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
|
||||
}
|
||||
}
|
||||
+85
@@ -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<NavigationMenuItem[]> => {
|
||||
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 };
|
||||
};
|
||||
-136
@@ -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 };
|
||||
};
|
||||
+38
@@ -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 };
|
||||
};
|
||||
-23
@@ -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 };
|
||||
};
|
||||
+59
@@ -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<NavigationMenuItem[]> => {
|
||||
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 };
|
||||
};
|
||||
-48
@@ -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 };
|
||||
};
|
||||
+3
-7
@@ -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;
|
||||
|
||||
+8
-7
@@ -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 },
|
||||
},
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+3
-3
@@ -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"
|
||||
/>
|
||||
|
||||
+5
-3
@@ -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"
|
||||
/>
|
||||
|
||||
+12
-17
@@ -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 };
|
||||
|
||||
+4
-4
@@ -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 (
|
||||
|
||||
+3
-3
@@ -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<void> => {
|
||||
await deleteNavigationMenuItem(folderId);
|
||||
await deleteManyNavigationMenuItems([folderId]);
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
+5
-6
@@ -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 {
|
||||
|
||||
+55
-177
@@ -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<string, string>();
|
||||
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,
|
||||
]);
|
||||
|
||||
+95
@@ -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<string, NavigationMenuItem>;
|
||||
idsToRecreateSet: Set<string>;
|
||||
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;
|
||||
};
|
||||
+52
@@ -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;
|
||||
};
|
||||
+23
-4
@@ -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);
|
||||
};
|
||||
|
||||
+59
@@ -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<NavigationMenuItemDTO[]> {
|
||||
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<NavigationMenuItemDTO[]> {
|
||||
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<NavigationMenuItemDTO[]> {
|
||||
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(
|
||||
|
||||
+290
-80
@@ -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<NavigationMenuItemDTO> {
|
||||
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<NavigationMenuItemDTO[]> {
|
||||
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<NavigationMenuItemDTO> {
|
||||
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<NavigationMenuItemDTO[]> {
|
||||
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<NavigationMenuItemDTO> {
|
||||
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<NavigationMenuItemDTO[]> {
|
||||
if (ids.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const uniqueOrderedIds: string[] = [];
|
||||
const seenId = new Set<string>();
|
||||
|
||||
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<string>();
|
||||
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({
|
||||
|
||||
+59
@@ -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",
|
||||
}
|
||||
`;
|
||||
|
||||
+25
@@ -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",
|
||||
}
|
||||
`;
|
||||
+40
@@ -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",
|
||||
}
|
||||
`;
|
||||
+1
-1
@@ -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",
|
||||
}
|
||||
`;
|
||||
|
||||
+2
-2
@@ -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",
|
||||
}
|
||||
`;
|
||||
|
||||
+81
@@ -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<TestContext>[] = [
|
||||
{
|
||||
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,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
+100
@@ -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<TestContext>[] = [
|
||||
{
|
||||
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,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
+127
@@ -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<TestContext>[] = [
|
||||
{
|
||||
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,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
+218
@@ -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();
|
||||
});
|
||||
});
|
||||
+182
@@ -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);
|
||||
});
|
||||
});
|
||||
+150
@@ -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);
|
||||
});
|
||||
});
|
||||
+42
@@ -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,
|
||||
},
|
||||
});
|
||||
+47
@@ -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 };
|
||||
};
|
||||
+28
@@ -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,
|
||||
},
|
||||
});
|
||||
+46
@@ -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 };
|
||||
};
|
||||
+37
@@ -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,
|
||||
},
|
||||
});
|
||||
+47
@@ -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 };
|
||||
};
|
||||
Reference in New Issue
Block a user