f63f053444
## 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>
278 lines
7.9 KiB
TypeScript
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,
|
|
}),
|
|
);
|
|
});
|
|
});
|