[COMMAND MENU ITEMS] Sync object metadata with navigation command menu items (#19456)
When an object is created or enabled we create a navigation command menu item to that object. And when the object is disabled or deleted, we remove this command menu item.
This commit is contained in:
+3
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
CreateOneObjectMetadataItemDocument,
|
||||
FindManyCommandMenuItemsDocument,
|
||||
FindManyNavigationMenuItemsDocument,
|
||||
FindManyViewsDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
@@ -11,6 +12,8 @@ export const findManyViewsQuery = FindManyViewsDocument;
|
||||
export const findManyNavigationMenuItemsQuery =
|
||||
FindManyNavigationMenuItemsDocument;
|
||||
|
||||
export const findManyCommandMenuItemsQuery = FindManyCommandMenuItemsDocument;
|
||||
|
||||
export const variables = {
|
||||
input: {
|
||||
object: {
|
||||
|
||||
+14
-2
@@ -4,6 +4,7 @@ import { useCreateOneObjectMetadataItem } from '@/object-metadata/hooks/useCreat
|
||||
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import {
|
||||
findManyCommandMenuItemsQuery,
|
||||
findManyNavigationMenuItemsQuery,
|
||||
findManyViewsQuery,
|
||||
query,
|
||||
@@ -12,7 +13,7 @@ import {
|
||||
} from '@/object-metadata/hooks/__mocks__/useCreateOneObjectMetadataItem';
|
||||
|
||||
import { jestExpectSuccessfulMetadataRequestResult } from '@/object-metadata/hooks/__tests__/utils/jest-expect-metadata-request-status.util';
|
||||
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
||||
import { GetCurrentUserDocument } from '~/generated-metadata/graphql';
|
||||
import { mockedUserData } from '~/testing/mock-data/users';
|
||||
import {
|
||||
query as findManyObjectMetadataItemsQuery,
|
||||
@@ -33,7 +34,7 @@ const mocks = [
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: GET_CURRENT_USER,
|
||||
query: GetCurrentUserDocument,
|
||||
variables: {},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
@@ -75,6 +76,17 @@ const mocks = [
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: findManyCommandMenuItemsQuery,
|
||||
variables: {},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
commandMenuItems: [],
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
|
||||
+12
@@ -11,6 +11,7 @@ import {
|
||||
import { jestExpectSuccessfulMetadataRequestResult } from '@/object-metadata/hooks/__tests__/utils/jest-expect-metadata-request-status.util';
|
||||
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
||||
import { FIND_ALL_VIEWS } from '@/views/graphql/queries/findAllViews';
|
||||
import { FindManyCommandMenuItemsDocument } from '~/generated-metadata/graphql';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { mockedUserData } from '~/testing/mock-data/users';
|
||||
import { mockedViews } from '~/testing/mock-data/generated/metadata/views/mock-views-data';
|
||||
@@ -62,6 +63,17 @@ const mocks = [
|
||||
data: findManyObjectMetadataItemsResponseData,
|
||||
})),
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: FindManyCommandMenuItemsDocument,
|
||||
variables: {},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
commandMenuItems: [],
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
|
||||
+26
-11
@@ -2,6 +2,7 @@ import { useApolloClient, useMutation } from '@apollo/client/react';
|
||||
import {
|
||||
type CreateObjectInput,
|
||||
CreateOneObjectMetadataItemDocument,
|
||||
FindManyCommandMenuItemsDocument,
|
||||
FindManyNavigationMenuItemsDocument,
|
||||
FindManyViewsDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
@@ -13,6 +14,7 @@ import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMe
|
||||
import { splitViewWithRelated } from '@/metadata-store/utils/splitViewWithRelated';
|
||||
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser';
|
||||
import { CombinedGraphQLErrors } from '@apollo/client/errors';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { CrudOperationType } from 'twenty-shared/types';
|
||||
@@ -28,6 +30,7 @@ export const useCreateOneObjectMetadataItem = () => {
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const { addToDraft, replaceDraft, applyChanges } =
|
||||
useUpdateMetadataStoreDraft();
|
||||
const { loadCurrentUser } = useLoadCurrentUser();
|
||||
|
||||
const createOneObjectMetadataItem = async (
|
||||
input: CreateObjectInput,
|
||||
@@ -70,17 +73,22 @@ export const useCreateOneObjectMetadataItem = () => {
|
||||
|
||||
applyChanges();
|
||||
|
||||
const [viewsResult, navItemsResult] = await Promise.all([
|
||||
client.query({
|
||||
query: FindManyViewsDocument,
|
||||
variables: { objectMetadataId: createdObject.id },
|
||||
fetchPolicy: 'network-only',
|
||||
}),
|
||||
client.query({
|
||||
query: FindManyNavigationMenuItemsDocument,
|
||||
fetchPolicy: 'network-only',
|
||||
}),
|
||||
]);
|
||||
const [viewsResult, navItemsResult, commandMenuItemsResult] =
|
||||
await Promise.all([
|
||||
client.query({
|
||||
query: FindManyViewsDocument,
|
||||
variables: { objectMetadataId: createdObject.id },
|
||||
fetchPolicy: 'network-only',
|
||||
}),
|
||||
client.query({
|
||||
query: FindManyNavigationMenuItemsDocument,
|
||||
fetchPolicy: 'network-only',
|
||||
}),
|
||||
client.query({
|
||||
query: FindManyCommandMenuItemsDocument,
|
||||
fetchPolicy: 'network-only',
|
||||
}),
|
||||
]);
|
||||
|
||||
const fetchedViews = viewsResult.data?.getViews ?? [];
|
||||
|
||||
@@ -107,7 +115,14 @@ export const useCreateOneObjectMetadataItem = () => {
|
||||
navItemsResult.data?.navigationMenuItems ?? [],
|
||||
);
|
||||
|
||||
replaceDraft(
|
||||
'commandMenuItems',
|
||||
commandMenuItemsResult.data?.commandMenuItems ?? [],
|
||||
);
|
||||
|
||||
applyChanges();
|
||||
|
||||
await loadCurrentUser();
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
+19
-3
@@ -1,5 +1,8 @@
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { DeleteOneObjectMetadataItemDocument } from '~/generated-metadata/graphql';
|
||||
import { useApolloClient, useMutation } from '@apollo/client/react';
|
||||
import {
|
||||
DeleteOneObjectMetadataItemDocument,
|
||||
FindManyCommandMenuItemsDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
|
||||
import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft';
|
||||
@@ -14,9 +17,11 @@ export const useDeleteOneObjectMetadataItem = () => {
|
||||
DeleteOneObjectMetadataItemDocument,
|
||||
);
|
||||
|
||||
const client = useApolloClient();
|
||||
const { handleMetadataError } = useMetadataErrorHandler();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const { removeFromDraft, applyChanges } = useUpdateMetadataStoreDraft();
|
||||
const { removeFromDraft, replaceDraft, applyChanges } =
|
||||
useUpdateMetadataStoreDraft();
|
||||
|
||||
const deleteOneObjectMetadataItem = async (
|
||||
idToDelete: string,
|
||||
@@ -35,6 +40,17 @@ export const useDeleteOneObjectMetadataItem = () => {
|
||||
removeFromDraft({ key: 'objectMetadataItems', itemIds: [idToDelete] });
|
||||
applyChanges();
|
||||
|
||||
const commandMenuItemsResult = await client.query({
|
||||
query: FindManyCommandMenuItemsDocument,
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
replaceDraft(
|
||||
'commandMenuItems',
|
||||
commandMenuItemsResult.data?.commandMenuItems ?? [],
|
||||
);
|
||||
applyChanges();
|
||||
|
||||
return {
|
||||
status: 'successful',
|
||||
response,
|
||||
|
||||
+16
-2
@@ -1,6 +1,7 @@
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { useApolloClient, useMutation } from '@apollo/client/react';
|
||||
import {
|
||||
type UpdateOneObjectInput,
|
||||
FindManyCommandMenuItemsDocument,
|
||||
UpdateOneObjectMetadataItemDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
@@ -20,9 +21,11 @@ export const useUpdateOneObjectMetadataItem = () => {
|
||||
UpdateOneObjectMetadataItemDocument,
|
||||
);
|
||||
|
||||
const client = useApolloClient();
|
||||
const { handleMetadataError } = useMetadataErrorHandler();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const { updateInDraft, applyChanges } = useUpdateMetadataStoreDraft();
|
||||
const { updateInDraft, replaceDraft, applyChanges } =
|
||||
useUpdateMetadataStoreDraft();
|
||||
|
||||
const updateOneObjectMetadataItem = async ({
|
||||
idToUpdate,
|
||||
@@ -52,6 +55,17 @@ export const useUpdateOneObjectMetadataItem = () => {
|
||||
objectData as FlatObjectMetadataItem,
|
||||
]);
|
||||
applyChanges();
|
||||
|
||||
const commandMenuItemsResult = await client.query({
|
||||
query: FindManyCommandMenuItemsDocument,
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
replaceDraft(
|
||||
'commandMenuItems',
|
||||
commandMenuItemsResult.data?.commandMenuItems ?? [],
|
||||
);
|
||||
applyChanges();
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
+262
-17
@@ -11,12 +11,15 @@ import {
|
||||
} from 'twenty-shared/types';
|
||||
import { fromArrayToUniqueKeyRecord, isDefined } from 'twenty-shared/utils';
|
||||
import { FindManyOptions, FindOneOptions, Repository } from 'typeorm';
|
||||
import { v4 as uuidv4, v4 } from 'uuid';
|
||||
import { v4, v5 } from 'uuid';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
|
||||
import {
|
||||
buildNavigationFlatCommandMenuItem,
|
||||
NAVIGATION_COMMAND_UUID_NAMESPACE,
|
||||
} from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
@@ -43,7 +46,6 @@ import {
|
||||
import { computeFlatDefaultRecordPageLayoutToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util';
|
||||
import { computeFlatRecordPageFieldsViewToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util';
|
||||
import { computeFlatViewFieldsToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
@@ -59,10 +61,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
) {
|
||||
super(objectMetadataRepository);
|
||||
}
|
||||
@@ -76,13 +75,13 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
updateObjectInput: UpdateOneObjectInput;
|
||||
ownerFlatApplication?: FlatApplication;
|
||||
}): Promise<FlatObjectMetadata> {
|
||||
const { workspaceCustomFlatApplication, twentyStandardFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const resolvedOwnerFlatApplication =
|
||||
ownerFlatApplication ??
|
||||
(
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
)
|
||||
).workspaceCustomFlatApplication;
|
||||
ownerFlatApplication ?? workspaceCustomFlatApplication;
|
||||
|
||||
const {
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
@@ -90,6 +89,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
||||
flatViewFieldMaps: existingFlatViewFieldMaps,
|
||||
flatViewMaps: existingFlatViewMaps,
|
||||
flatCommandMenuItemMaps: existingFlatCommandMenuItemMaps,
|
||||
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
@@ -99,6 +99,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
'flatFieldMetadataMaps',
|
||||
'flatViewFieldMaps',
|
||||
'flatViewMaps',
|
||||
'flatCommandMenuItemMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -119,6 +120,35 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
flatViewMaps: existingFlatViewMaps,
|
||||
});
|
||||
|
||||
const existingFlatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityMaps: existingFlatObjectMetadataMaps,
|
||||
flatEntityId: updateObjectInput.id,
|
||||
});
|
||||
|
||||
const isActiveChangeDefined = isDefined(updateObjectInput.update.isActive);
|
||||
|
||||
const isBeingEnabled =
|
||||
isActiveChangeDefined &&
|
||||
updateObjectInput.update.isActive === true &&
|
||||
isDefined(existingFlatObjectMetadata) &&
|
||||
!existingFlatObjectMetadata.isActive;
|
||||
|
||||
const isBeingDisabled =
|
||||
isActiveChangeDefined &&
|
||||
updateObjectInput.update.isActive === false &&
|
||||
isDefined(existingFlatObjectMetadata) &&
|
||||
existingFlatObjectMetadata.isActive;
|
||||
|
||||
const { commandMenuItemsToCreate, commandMenuItemsToDelete } =
|
||||
this.computeCommandMenuItemChangesForActiveToggle({
|
||||
isBeingEnabled,
|
||||
isBeingDisabled,
|
||||
existingFlatObjectMetadata,
|
||||
flatCommandMenuItemMaps: existingFlatCommandMenuItemMaps,
|
||||
workspaceId,
|
||||
applicationId: twentyStandardFlatApplication.id,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
@@ -161,6 +191,35 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
);
|
||||
}
|
||||
|
||||
const hasCommandMenuItemChanges =
|
||||
commandMenuItemsToCreate.length > 0 ||
|
||||
commandMenuItemsToDelete.length > 0;
|
||||
|
||||
if (hasCommandMenuItemChanges) {
|
||||
const commandMenuItemMigrationResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
commandMenuItem: {
|
||||
flatEntityToCreate: commandMenuItemsToCreate,
|
||||
flatEntityToDelete: commandMenuItemsToDelete,
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (commandMenuItemMigrationResult.status === 'fail') {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
commandMenuItemMigrationResult,
|
||||
'Multiple validation errors occurred while updating command menu items',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const { flatObjectMetadataMaps: recomputedFlatObjectMetadataMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
@@ -187,10 +246,10 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
]);
|
||||
}
|
||||
|
||||
if (isDefined(updateObjectInput.update.isActive)) {
|
||||
if (isActiveChangeDefined) {
|
||||
await this.flatEntityMapsCacheService.invalidateFlatEntityMaps({
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatNavigationMenuItemMaps'],
|
||||
flatMapsKeys: ['flatNavigationMenuItemMaps', 'flatCommandMenuItemMaps'],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -250,7 +309,12 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
)
|
||||
).workspaceCustomFlatApplication;
|
||||
|
||||
const { flatObjectMetadataMaps, flatFieldMetadataMaps, flatIndexMaps } =
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
flatCommandMenuItemMaps,
|
||||
} =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
@@ -258,6 +322,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
'flatObjectMetadataMaps',
|
||||
'flatIndexMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatCommandMenuItemMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -339,6 +404,16 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
}),
|
||||
);
|
||||
|
||||
const flatCommandMenuItemsToDelete = flatObjectMetadatasToDelete
|
||||
.map((flatObjectMetadataToDelete) =>
|
||||
this.findNavigationCommandMenuItemForObject({
|
||||
objectUniversalIdentifier:
|
||||
flatObjectMetadataToDelete.universalIdentifier,
|
||||
flatCommandMenuItemMaps,
|
||||
}),
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
@@ -358,6 +433,11 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
flatEntityToDelete: flatFieldMetadatasToDelete,
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
commandMenuItem: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: flatCommandMenuItemsToDelete,
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild,
|
||||
@@ -441,6 +521,23 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
});
|
||||
|
||||
const { flatCommandMenuItemMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatCommandMenuItemMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const flatCommandMenuItemToCreate = this.buildFlatNavigationCommandMenuItem(
|
||||
{
|
||||
objectMetadata: flatObjectMetadataToCreate,
|
||||
workspaceId,
|
||||
applicationId: twentyStandardFlatApplication.id,
|
||||
flatCommandMenuItemMaps,
|
||||
},
|
||||
);
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
@@ -497,6 +594,29 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
);
|
||||
}
|
||||
|
||||
const commandMenuItemMigrationResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
commandMenuItem: {
|
||||
flatEntityToCreate: [flatCommandMenuItemToCreate],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (commandMenuItemMigrationResult.status === 'fail') {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
commandMenuItemMigrationResult,
|
||||
'Multiple validation errors occurred while creating command menu item',
|
||||
);
|
||||
}
|
||||
|
||||
if (isRecordPageLayoutEditingEnabled) {
|
||||
const flatRecordPageFieldsViewToCreate =
|
||||
this.computeFlatRecordPageFieldsViewToCreate({
|
||||
@@ -699,7 +819,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
? Math.max(...workspaceLevelItems.map((item) => item.position)) + 1
|
||||
: 0;
|
||||
|
||||
const newId = uuidv4();
|
||||
const newId = v4();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
return {
|
||||
@@ -729,6 +849,131 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
};
|
||||
}
|
||||
|
||||
private buildFlatNavigationCommandMenuItem({
|
||||
objectMetadata,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
flatCommandMenuItemMaps,
|
||||
}: {
|
||||
objectMetadata: {
|
||||
id: string;
|
||||
universalIdentifier: string;
|
||||
labelPlural: string;
|
||||
icon: string | null;
|
||||
nameSingular: string;
|
||||
shortcut: string | null;
|
||||
};
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
flatCommandMenuItemMaps: {
|
||||
byUniversalIdentifier: Record<string, FlatCommandMenuItem | undefined>;
|
||||
};
|
||||
}): FlatCommandMenuItem {
|
||||
const existingItems = Object.values(
|
||||
flatCommandMenuItemMaps.byUniversalIdentifier,
|
||||
).filter(isDefined);
|
||||
|
||||
const nextPosition =
|
||||
existingItems.length > 0
|
||||
? Math.max(...existingItems.map((item) => item.position)) + 1
|
||||
: 0;
|
||||
|
||||
return buildNavigationFlatCommandMenuItem({
|
||||
objectMetadata,
|
||||
commandMenuItemId: v4(),
|
||||
applicationId,
|
||||
workspaceId,
|
||||
position: nextPosition,
|
||||
now: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
private findNavigationCommandMenuItemForObject({
|
||||
objectUniversalIdentifier,
|
||||
flatCommandMenuItemMaps,
|
||||
}: {
|
||||
objectUniversalIdentifier: string;
|
||||
flatCommandMenuItemMaps: {
|
||||
byUniversalIdentifier: Record<string, FlatCommandMenuItem | undefined>;
|
||||
};
|
||||
}): FlatCommandMenuItem | undefined {
|
||||
const commandMenuItemUniversalIdentifier = v5(
|
||||
objectUniversalIdentifier,
|
||||
NAVIGATION_COMMAND_UUID_NAMESPACE,
|
||||
);
|
||||
|
||||
return findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: flatCommandMenuItemMaps,
|
||||
universalIdentifier: commandMenuItemUniversalIdentifier,
|
||||
});
|
||||
}
|
||||
|
||||
private computeCommandMenuItemChangesForActiveToggle({
|
||||
isBeingEnabled,
|
||||
isBeingDisabled,
|
||||
existingFlatObjectMetadata,
|
||||
flatCommandMenuItemMaps,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
}: {
|
||||
isBeingEnabled: boolean;
|
||||
isBeingDisabled: boolean;
|
||||
existingFlatObjectMetadata: FlatObjectMetadata | undefined;
|
||||
flatCommandMenuItemMaps: {
|
||||
byUniversalIdentifier: Record<string, FlatCommandMenuItem | undefined>;
|
||||
};
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
}): {
|
||||
commandMenuItemsToCreate: FlatCommandMenuItem[];
|
||||
commandMenuItemsToDelete: FlatCommandMenuItem[];
|
||||
} {
|
||||
if (!isDefined(existingFlatObjectMetadata)) {
|
||||
return { commandMenuItemsToCreate: [], commandMenuItemsToDelete: [] };
|
||||
}
|
||||
|
||||
if (isBeingEnabled) {
|
||||
const existingCommandMenuItem =
|
||||
this.findNavigationCommandMenuItemForObject({
|
||||
objectUniversalIdentifier:
|
||||
existingFlatObjectMetadata.universalIdentifier,
|
||||
flatCommandMenuItemMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(existingCommandMenuItem)) {
|
||||
return {
|
||||
commandMenuItemsToCreate: [
|
||||
this.buildFlatNavigationCommandMenuItem({
|
||||
objectMetadata: existingFlatObjectMetadata,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
flatCommandMenuItemMaps,
|
||||
}),
|
||||
],
|
||||
commandMenuItemsToDelete: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (isBeingDisabled) {
|
||||
const commandMenuItemToDelete =
|
||||
this.findNavigationCommandMenuItemForObject({
|
||||
objectUniversalIdentifier:
|
||||
existingFlatObjectMetadata.universalIdentifier,
|
||||
flatCommandMenuItemMaps,
|
||||
});
|
||||
|
||||
if (isDefined(commandMenuItemToDelete)) {
|
||||
return {
|
||||
commandMenuItemsToCreate: [],
|
||||
commandMenuItemsToDelete: [commandMenuItemToDelete],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { commandMenuItemsToCreate: [], commandMenuItemsToDelete: [] };
|
||||
}
|
||||
|
||||
public async findOneWithinWorkspace(
|
||||
workspaceId: string,
|
||||
options: FindOneOptions<ObjectMetadataEntity>,
|
||||
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
import { findCommandMenuItems } from 'test/integration/metadata/suites/command-menu-item/utils/find-command-menu-items.util';
|
||||
import { type CreateOneObjectFactoryInput } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata-query-factory.util';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type CommandMenuItemDTO } from 'src/engine/metadata-modules/command-menu-item/dtos/command-menu-item.dto';
|
||||
import { type ObjectMetadataCommandMenuItemPayload } from 'src/engine/metadata-modules/command-menu-item/dtos/types/object-metadata-command-menu-item-payload.type';
|
||||
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
|
||||
|
||||
const findNavigationCommandMenuItemForObject = (
|
||||
commandMenuItems: CommandMenuItemDTO[],
|
||||
objectMetadataId: string,
|
||||
) =>
|
||||
commandMenuItems.find(
|
||||
(item) =>
|
||||
item.engineComponentKey === EngineComponentKey.NAVIGATION &&
|
||||
(item.payload as ObjectMetadataCommandMenuItemPayload | undefined)
|
||||
?.objectMetadataItemId === objectMetadataId,
|
||||
);
|
||||
|
||||
const COMMAND_MENU_ITEM_GQL_FIELDS = `
|
||||
id
|
||||
engineComponentKey
|
||||
label
|
||||
icon
|
||||
payload {
|
||||
... on PathCommandMenuItemPayload {
|
||||
path
|
||||
}
|
||||
... on ObjectMetadataCommandMenuItemPayload {
|
||||
objectMetadataItemId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
describe('Command menu item side effect on object metadata', () => {
|
||||
let createdObjectMetadataId: string | undefined = undefined;
|
||||
|
||||
const uniqueSuffix = Date.now().toString().slice(-8);
|
||||
|
||||
const createObjectInput: CreateOneObjectFactoryInput = {
|
||||
namePlural: `sideEffectItems${uniqueSuffix}`,
|
||||
nameSingular: `sideEffectItem${uniqueSuffix}`,
|
||||
labelPlural: `Side Effect Items ${uniqueSuffix}`,
|
||||
labelSingular: `Side Effect Item ${uniqueSuffix}`,
|
||||
description: 'Object for command menu item side effect tests',
|
||||
icon: 'IconBox',
|
||||
isLabelSyncedWithName: false,
|
||||
};
|
||||
|
||||
afterEach(async () => {
|
||||
if (!isDefined(createdObjectMetadataId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: createdObjectMetadataId,
|
||||
updatePayload: { isActive: false },
|
||||
},
|
||||
});
|
||||
|
||||
await deleteOneObjectMetadata({
|
||||
input: { idToDelete: createdObjectMetadataId },
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
createdObjectMetadataId = undefined;
|
||||
});
|
||||
|
||||
it('should create a navigation command menu item when a custom object is created', async () => {
|
||||
const {
|
||||
data: { createOneObject },
|
||||
} = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: createObjectInput,
|
||||
gqlFields: 'id',
|
||||
});
|
||||
|
||||
createdObjectMetadataId = createOneObject.id;
|
||||
|
||||
const {
|
||||
data: { commandMenuItems },
|
||||
} = await findCommandMenuItems({
|
||||
expectToFail: false,
|
||||
input: undefined,
|
||||
gqlFields: COMMAND_MENU_ITEM_GQL_FIELDS,
|
||||
});
|
||||
|
||||
const navigationItem = findNavigationCommandMenuItemForObject(
|
||||
commandMenuItems,
|
||||
createdObjectMetadataId,
|
||||
);
|
||||
|
||||
expect(navigationItem).toEqual(
|
||||
expect.objectContaining({
|
||||
label: `Go to ${createObjectInput.labelPlural}`,
|
||||
icon: createObjectInput.icon,
|
||||
engineComponentKey: EngineComponentKey.NAVIGATION,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete the navigation command menu item when a custom object is deleted', async () => {
|
||||
const {
|
||||
data: { createOneObject },
|
||||
} = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: createObjectInput,
|
||||
gqlFields: 'id',
|
||||
});
|
||||
|
||||
createdObjectMetadataId = createOneObject.id;
|
||||
const deletedObjectId = createdObjectMetadataId;
|
||||
|
||||
const {
|
||||
data: { commandMenuItems: itemsBeforeDelete },
|
||||
} = await findCommandMenuItems({
|
||||
expectToFail: false,
|
||||
input: undefined,
|
||||
gqlFields: COMMAND_MENU_ITEM_GQL_FIELDS,
|
||||
});
|
||||
|
||||
expect(
|
||||
findNavigationCommandMenuItemForObject(
|
||||
itemsBeforeDelete,
|
||||
deletedObjectId,
|
||||
),
|
||||
).toBeDefined();
|
||||
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: createdObjectMetadataId,
|
||||
updatePayload: { isActive: false },
|
||||
},
|
||||
});
|
||||
|
||||
await deleteOneObjectMetadata({
|
||||
input: { idToDelete: createdObjectMetadataId },
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
createdObjectMetadataId = undefined;
|
||||
|
||||
const {
|
||||
data: { commandMenuItems: itemsAfterDelete },
|
||||
} = await findCommandMenuItems({
|
||||
expectToFail: false,
|
||||
input: undefined,
|
||||
gqlFields: COMMAND_MENU_ITEM_GQL_FIELDS,
|
||||
});
|
||||
|
||||
expect(
|
||||
findNavigationCommandMenuItemForObject(
|
||||
itemsAfterDelete,
|
||||
deletedObjectId,
|
||||
),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should delete the navigation command menu item when a custom object is disabled', async () => {
|
||||
const {
|
||||
data: { createOneObject },
|
||||
} = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: createObjectInput,
|
||||
gqlFields: 'id',
|
||||
});
|
||||
|
||||
createdObjectMetadataId = createOneObject.id;
|
||||
|
||||
const {
|
||||
data: { commandMenuItems: itemsBeforeDisable },
|
||||
} = await findCommandMenuItems({
|
||||
expectToFail: false,
|
||||
input: undefined,
|
||||
gqlFields: COMMAND_MENU_ITEM_GQL_FIELDS,
|
||||
});
|
||||
|
||||
expect(
|
||||
findNavigationCommandMenuItemForObject(
|
||||
itemsBeforeDisable,
|
||||
createdObjectMetadataId,
|
||||
),
|
||||
).toBeDefined();
|
||||
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: createdObjectMetadataId,
|
||||
updatePayload: { isActive: false },
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
data: { commandMenuItems: itemsAfterDisable },
|
||||
} = await findCommandMenuItems({
|
||||
expectToFail: false,
|
||||
input: undefined,
|
||||
gqlFields: COMMAND_MENU_ITEM_GQL_FIELDS,
|
||||
});
|
||||
|
||||
expect(
|
||||
findNavigationCommandMenuItemForObject(
|
||||
itemsAfterDisable,
|
||||
createdObjectMetadataId,
|
||||
),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should recreate the navigation command menu item when a disabled object is re-enabled', async () => {
|
||||
const {
|
||||
data: { createOneObject },
|
||||
} = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: createObjectInput,
|
||||
gqlFields: 'id',
|
||||
});
|
||||
|
||||
createdObjectMetadataId = createOneObject.id;
|
||||
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: createdObjectMetadataId,
|
||||
updatePayload: { isActive: false },
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
data: { commandMenuItems: itemsWhileDisabled },
|
||||
} = await findCommandMenuItems({
|
||||
expectToFail: false,
|
||||
input: undefined,
|
||||
gqlFields: COMMAND_MENU_ITEM_GQL_FIELDS,
|
||||
});
|
||||
|
||||
expect(
|
||||
findNavigationCommandMenuItemForObject(
|
||||
itemsWhileDisabled,
|
||||
createdObjectMetadataId,
|
||||
),
|
||||
).toBeUndefined();
|
||||
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: createdObjectMetadataId,
|
||||
updatePayload: { isActive: true },
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
data: { commandMenuItems: itemsAfterReEnable },
|
||||
} = await findCommandMenuItems({
|
||||
expectToFail: false,
|
||||
input: undefined,
|
||||
gqlFields: COMMAND_MENU_ITEM_GQL_FIELDS,
|
||||
});
|
||||
|
||||
const navigationItem = findNavigationCommandMenuItemForObject(
|
||||
itemsAfterReEnable,
|
||||
createdObjectMetadataId,
|
||||
);
|
||||
|
||||
expect(navigationItem).toEqual(
|
||||
expect.objectContaining({
|
||||
label: `Go to ${createObjectInput.labelPlural}`,
|
||||
icon: createObjectInput.icon,
|
||||
engineComponentKey: EngineComponentKey.NAVIGATION,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user