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>
This commit is contained in:
+3
@@ -100,6 +100,9 @@ export const buildNavigationFlatCommandMenuItem = ({
|
||||
availabilityObjectMetadataUniversalIdentifier: null,
|
||||
pageLayoutId: null,
|
||||
pageLayoutUniversalIdentifier: null,
|
||||
isActive: true,
|
||||
overrides: null,
|
||||
universalOverrides: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
+17
@@ -1,5 +1,6 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { fromCommandMenuItemOverridesToUniversalOverrides } from 'src/engine/metadata-modules/flat-command-menu-item/utils/from-command-menu-item-overrides-to-universal-overrides.util';
|
||||
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
|
||||
import {
|
||||
FlatEntityMapsException,
|
||||
@@ -74,6 +75,19 @@ export const fromCommandMenuItemEntityToFlatCommandMenuItem = ({
|
||||
}
|
||||
}
|
||||
|
||||
const universalOverrides = isDefined(commandMenuItemEntity.overrides)
|
||||
? fromCommandMenuItemOverridesToUniversalOverrides({
|
||||
overrides: commandMenuItemEntity.overrides,
|
||||
objectMetadataUniversalIdentifierById: Object.fromEntries(
|
||||
objectMetadataIdToUniversalIdentifierMap.entries(),
|
||||
),
|
||||
pageLayoutUniversalIdentifierById: Object.fromEntries(
|
||||
pageLayoutIdToUniversalIdentifierMap.entries(),
|
||||
),
|
||||
shouldThrowOnMissingIdentifier: false,
|
||||
})
|
||||
: null;
|
||||
|
||||
return {
|
||||
id: commandMenuItemEntity.id,
|
||||
workflowVersionId: commandMenuItemEntity.workflowVersionId,
|
||||
@@ -101,5 +115,8 @@ export const fromCommandMenuItemEntityToFlatCommandMenuItem = ({
|
||||
frontComponentUniversalIdentifier,
|
||||
pageLayoutId: commandMenuItemEntity.pageLayoutId,
|
||||
pageLayoutUniversalIdentifier,
|
||||
isActive: commandMenuItemEntity.isActive,
|
||||
overrides: commandMenuItemEntity.overrides,
|
||||
universalOverrides,
|
||||
};
|
||||
};
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import { type FormatRecordSerializedRelationProperties } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type CommandMenuItemOverrides } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
|
||||
import {
|
||||
FlatEntityMapsException,
|
||||
FlatEntityMapsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
|
||||
|
||||
type UniversalCommandMenuItemOverrides =
|
||||
FormatRecordSerializedRelationProperties<CommandMenuItemOverrides>;
|
||||
|
||||
const COMMAND_MENU_ITEM_OVERRIDES_FOREIGN_KEYS = [
|
||||
{
|
||||
foreignKey: 'availabilityObjectMetadataId',
|
||||
universalProperty: 'availabilityObjectMetadataUniversalIdentifier',
|
||||
mapName: 'objectMetadata',
|
||||
},
|
||||
{
|
||||
foreignKey: 'pageLayoutId',
|
||||
universalProperty: 'pageLayoutUniversalIdentifier',
|
||||
mapName: 'pageLayout',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const fromCommandMenuItemOverridesToUniversalOverrides = ({
|
||||
overrides,
|
||||
objectMetadataUniversalIdentifierById,
|
||||
pageLayoutUniversalIdentifierById,
|
||||
shouldThrowOnMissingIdentifier = true,
|
||||
}: {
|
||||
overrides: CommandMenuItemOverrides;
|
||||
objectMetadataUniversalIdentifierById: Partial<Record<string, string>>;
|
||||
pageLayoutUniversalIdentifierById: Partial<Record<string, string>>;
|
||||
shouldThrowOnMissingIdentifier?: boolean;
|
||||
}): UniversalCommandMenuItemOverrides => {
|
||||
const {
|
||||
availabilityObjectMetadataId: _availabilityObjectMetadataId,
|
||||
pageLayoutId: _pageLayoutId,
|
||||
...scalarOverrides
|
||||
} = overrides;
|
||||
|
||||
const universalIdentifierByIdByMapName = {
|
||||
objectMetadata: objectMetadataUniversalIdentifierById,
|
||||
pageLayout: pageLayoutUniversalIdentifierById,
|
||||
};
|
||||
|
||||
return COMMAND_MENU_ITEM_OVERRIDES_FOREIGN_KEYS.reduce<UniversalCommandMenuItemOverrides>(
|
||||
(acc, { foreignKey, universalProperty, mapName }) => {
|
||||
const foreignKeyValue = overrides[foreignKey];
|
||||
|
||||
if (foreignKeyValue === undefined) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (foreignKeyValue === null) {
|
||||
return { ...acc, [universalProperty]: null };
|
||||
}
|
||||
|
||||
const universalIdentifier =
|
||||
universalIdentifierByIdByMapName[mapName][foreignKeyValue];
|
||||
|
||||
if (!isDefined(universalIdentifier)) {
|
||||
if (shouldThrowOnMissingIdentifier) {
|
||||
throw new FlatEntityMapsException(
|
||||
`${mapName} universal identifier not found for id: ${foreignKeyValue}`,
|
||||
FlatEntityMapsExceptionCode.RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return { ...acc, [universalProperty]: null };
|
||||
}
|
||||
|
||||
return { ...acc, [universalProperty]: universalIdentifier };
|
||||
},
|
||||
scalarOverrides,
|
||||
);
|
||||
};
|
||||
+3
@@ -76,6 +76,9 @@ export const fromCreateCommandMenuItemInputToFlatCommandMenuItemToCreate = ({
|
||||
workspaceId,
|
||||
applicationId: flatApplication.id,
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
isActive: true,
|
||||
overrides: null,
|
||||
universalOverrides: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
+34
-23
@@ -3,26 +3,37 @@ import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-comma
|
||||
|
||||
export const fromFlatCommandMenuItemToCommandMenuItemDto = (
|
||||
flatCommandMenuItem: FlatCommandMenuItem,
|
||||
): CommandMenuItemDTO => ({
|
||||
id: flatCommandMenuItem.id,
|
||||
workflowVersionId: flatCommandMenuItem.workflowVersionId ?? undefined,
|
||||
frontComponentId: flatCommandMenuItem.frontComponentId ?? undefined,
|
||||
engineComponentKey: flatCommandMenuItem.engineComponentKey,
|
||||
label: flatCommandMenuItem.label,
|
||||
icon: flatCommandMenuItem.icon ?? undefined,
|
||||
shortLabel: flatCommandMenuItem.shortLabel ?? undefined,
|
||||
position: flatCommandMenuItem.position,
|
||||
isPinned: flatCommandMenuItem.isPinned,
|
||||
payload: flatCommandMenuItem.payload ?? undefined,
|
||||
hotKeys: flatCommandMenuItem.hotKeys ?? undefined,
|
||||
availabilityType: flatCommandMenuItem.availabilityType,
|
||||
conditionalAvailabilityExpression:
|
||||
flatCommandMenuItem.conditionalAvailabilityExpression ?? undefined,
|
||||
availabilityObjectMetadataId:
|
||||
flatCommandMenuItem.availabilityObjectMetadataId ?? undefined,
|
||||
pageLayoutId: flatCommandMenuItem.pageLayoutId ?? undefined,
|
||||
workspaceId: flatCommandMenuItem.workspaceId,
|
||||
applicationId: flatCommandMenuItem.applicationId ?? undefined,
|
||||
createdAt: new Date(flatCommandMenuItem.createdAt),
|
||||
updatedAt: new Date(flatCommandMenuItem.updatedAt),
|
||||
});
|
||||
): CommandMenuItemDTO => {
|
||||
const effectiveFlatCommandMenuItem = {
|
||||
...flatCommandMenuItem,
|
||||
...(flatCommandMenuItem.overrides ?? {}),
|
||||
};
|
||||
|
||||
return {
|
||||
id: effectiveFlatCommandMenuItem.id,
|
||||
workflowVersionId:
|
||||
effectiveFlatCommandMenuItem.workflowVersionId ?? undefined,
|
||||
frontComponentId:
|
||||
effectiveFlatCommandMenuItem.frontComponentId ?? undefined,
|
||||
engineComponentKey: effectiveFlatCommandMenuItem.engineComponentKey,
|
||||
label: effectiveFlatCommandMenuItem.label,
|
||||
icon: effectiveFlatCommandMenuItem.icon ?? undefined,
|
||||
shortLabel: effectiveFlatCommandMenuItem.shortLabel ?? undefined,
|
||||
position: effectiveFlatCommandMenuItem.position,
|
||||
isPinned: effectiveFlatCommandMenuItem.isPinned,
|
||||
payload: effectiveFlatCommandMenuItem.payload ?? undefined,
|
||||
hotKeys: effectiveFlatCommandMenuItem.hotKeys ?? undefined,
|
||||
availabilityType: effectiveFlatCommandMenuItem.availabilityType,
|
||||
conditionalAvailabilityExpression:
|
||||
effectiveFlatCommandMenuItem.conditionalAvailabilityExpression ??
|
||||
undefined,
|
||||
availabilityObjectMetadataId:
|
||||
effectiveFlatCommandMenuItem.availabilityObjectMetadataId ?? undefined,
|
||||
pageLayoutId: effectiveFlatCommandMenuItem.pageLayoutId ?? undefined,
|
||||
workspaceId: effectiveFlatCommandMenuItem.workspaceId,
|
||||
applicationId: effectiveFlatCommandMenuItem.applicationId ?? undefined,
|
||||
isActive: effectiveFlatCommandMenuItem.isActive,
|
||||
createdAt: new Date(effectiveFlatCommandMenuItem.createdAt),
|
||||
updatedAt: new Date(effectiveFlatCommandMenuItem.updatedAt),
|
||||
};
|
||||
};
|
||||
|
||||
+50
-10
@@ -5,12 +5,16 @@ import {
|
||||
CommandMenuItemExceptionCode,
|
||||
} from 'src/engine/metadata-modules/command-menu-item/command-menu-item.exception';
|
||||
import { type UpdateCommandMenuItemInput } from 'src/engine/metadata-modules/command-menu-item/dtos/update-command-menu-item.input';
|
||||
import { type CommandMenuItemOverrides } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
|
||||
import { FLAT_COMMAND_MENU_ITEM_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-command-menu-item/constants/flat-command-menu-item-editable-properties.constant';
|
||||
import { type FlatCommandMenuItemMaps } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item-maps.type';
|
||||
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
|
||||
import { fromCommandMenuItemOverridesToUniversalOverrides } from 'src/engine/metadata-modules/flat-command-menu-item/utils/from-command-menu-item-overrides-to-universal-overrides.util';
|
||||
import { isCallerOverridingEntity } from 'src/engine/metadata-modules/utils/is-caller-overriding-entity.util';
|
||||
import { sanitizeOverridableEntityInput } from 'src/engine/metadata-modules/utils/sanitize-overridable-entity-input.util';
|
||||
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
|
||||
|
||||
export const fromUpdateCommandMenuItemInputToFlatCommandMenuItemToUpdateOrThrow =
|
||||
@@ -19,9 +23,13 @@ export const fromUpdateCommandMenuItemInputToFlatCommandMenuItemToUpdateOrThrow
|
||||
updateCommandMenuItemInput,
|
||||
flatObjectMetadataMaps,
|
||||
flatPageLayoutMaps,
|
||||
callerApplicationUniversalIdentifier,
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
}: {
|
||||
flatCommandMenuItemMaps: FlatCommandMenuItemMaps;
|
||||
updateCommandMenuItemInput: UpdateCommandMenuItemInput;
|
||||
callerApplicationUniversalIdentifier: string;
|
||||
workspaceCustomApplicationUniversalIdentifier: string;
|
||||
} & Pick<
|
||||
AllFlatEntityMaps,
|
||||
'flatObjectMetadataMaps' | 'flatPageLayoutMaps'
|
||||
@@ -40,22 +48,41 @@ export const fromUpdateCommandMenuItemInputToFlatCommandMenuItemToUpdateOrThrow
|
||||
|
||||
const { id: _id, ...updates } = updateCommandMenuItemInput;
|
||||
|
||||
const flatCommandMenuItemToUpdate = {
|
||||
...mergeUpdateInExistingRecord({
|
||||
existing: existingFlatCommandMenuItem,
|
||||
properties: [...FLAT_COMMAND_MENU_ITEM_EDITABLE_PROPERTIES],
|
||||
update: updates,
|
||||
}),
|
||||
const shouldOverride = isCallerOverridingEntity({
|
||||
callerApplicationUniversalIdentifier,
|
||||
entityApplicationUniversalIdentifier:
|
||||
existingFlatCommandMenuItem.applicationUniversalIdentifier,
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
const { overrides, updatedEditableProperties } =
|
||||
sanitizeOverridableEntityInput({
|
||||
metadataName: 'commandMenuItem',
|
||||
existingFlatEntity: existingFlatCommandMenuItem,
|
||||
updatedEditableProperties: updates,
|
||||
shouldOverride,
|
||||
});
|
||||
|
||||
const mergedRecord = mergeUpdateInExistingRecord({
|
||||
existing: existingFlatCommandMenuItem,
|
||||
properties: [...FLAT_COMMAND_MENU_ITEM_EDITABLE_PROPERTIES],
|
||||
update: updatedEditableProperties,
|
||||
});
|
||||
|
||||
const flatCommandMenuItemToUpdate: FlatCommandMenuItem = {
|
||||
...mergedRecord,
|
||||
// sanitizeOverridableEntityInput returns a loosely-typed Record overrides
|
||||
overrides: overrides as FlatCommandMenuItem['overrides'],
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (updates.availabilityObjectMetadataId !== undefined) {
|
||||
if (updatedEditableProperties.availabilityObjectMetadataId !== undefined) {
|
||||
const { availabilityObjectMetadataUniversalIdentifier } =
|
||||
resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'commandMenuItem',
|
||||
foreignKeyValues: {
|
||||
availabilityObjectMetadataId:
|
||||
flatCommandMenuItemToUpdate.availabilityObjectMetadataId,
|
||||
mergedRecord.availabilityObjectMetadataId,
|
||||
},
|
||||
flatEntityMaps: { flatObjectMetadataMaps },
|
||||
});
|
||||
@@ -64,12 +91,12 @@ export const fromUpdateCommandMenuItemInputToFlatCommandMenuItemToUpdateOrThrow
|
||||
availabilityObjectMetadataUniversalIdentifier;
|
||||
}
|
||||
|
||||
if (updates.pageLayoutId !== undefined) {
|
||||
if (updatedEditableProperties.pageLayoutId !== undefined) {
|
||||
const { pageLayoutUniversalIdentifier } =
|
||||
resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'commandMenuItem',
|
||||
foreignKeyValues: {
|
||||
pageLayoutId: flatCommandMenuItemToUpdate.pageLayoutId,
|
||||
pageLayoutId: mergedRecord.pageLayoutId,
|
||||
},
|
||||
flatEntityMaps: { flatPageLayoutMaps },
|
||||
});
|
||||
@@ -78,5 +105,18 @@ export const fromUpdateCommandMenuItemInputToFlatCommandMenuItemToUpdateOrThrow
|
||||
pageLayoutUniversalIdentifier;
|
||||
}
|
||||
|
||||
if (isDefined(overrides)) {
|
||||
flatCommandMenuItemToUpdate.universalOverrides =
|
||||
fromCommandMenuItemOverridesToUniversalOverrides({
|
||||
overrides: overrides as CommandMenuItemOverrides,
|
||||
objectMetadataUniversalIdentifierById:
|
||||
flatObjectMetadataMaps.universalIdentifierById,
|
||||
pageLayoutUniversalIdentifierById:
|
||||
flatPageLayoutMaps.universalIdentifierById,
|
||||
});
|
||||
} else {
|
||||
flatCommandMenuItemToUpdate.universalOverrides = null;
|
||||
}
|
||||
|
||||
return flatCommandMenuItemToUpdate;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user