Files
twenty/packages/twenty-server/test/integration/metadata/suites/object-metadata/command-menu-item-side-effect-on-object-metadata.integration-spec.ts
T
Weiko f63f053444 CommandMenuItem overridable entity (#21486)
## Context
Second PR of the overridable-entities track (after #21436 for views):
command menu items become overridable so that edits on non-owned items
are stored as overrides instead of mutating the row, and
deletion/deactivation becomes reversible.

 ## What this does

- `CommandMenuItemEntity` now extends
`OverridableEntity<CommandMenuItemOverrides>` (adds `isActive` +
`overrides`). All editable properties are overridable for now (to
discuss).
- **Update**: mutations on a command item not owned by the caller
(standard items) are written into `overrides`; reads merge them in the
DTO. The command palette edit mode (pin,
reorder, shortLabel) now preserves standard values, "Reset label to
default" gains true post-save semantics.
- **Delete**: protected items are deactivated (`isActive = false`)
instead of deleted; custom items still hard-delete.
- **Object deactivate/enable toggle**: now flips `isActive` on the
command item (merged into the main migration call) instead of
delete/recreate; a create-if-missing fallback covers legacy deactivated
objects.
- **Front**: inactive command items are filtered out of the palette
selector.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21486?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-13 14:21:27 +02:00

278 lines
7.9 KiB
TypeScript

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
isActive
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 deactivate 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,
),
).toEqual(expect.objectContaining({ isActive: true }));
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,
),
).toEqual(expect.objectContaining({ isActive: false }));
});
it('should reactivate 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,
),
).toEqual(expect.objectContaining({ isActive: false }));
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,
isActive: true,
}),
);
});
});