feat(sdk): add defineCommandMenuItem (#20256)
## Summary - Add `defineCommandMenuItem` and `definePageLayoutWidget` as standalone SDK defines, mirroring the existing `definePageLayoutTab` pattern. Both entities can still be declared nested inside their parent (`defineFrontComponent.command` / `definePageLayout.tabs[].widgets[]`). - Add `CommandMenuItem` and `PageLayoutWidget` to the `SyncableEntity` enum and the dev-mode UI labels. - Wire the SDK manifest-build to extract the two new defines into top-level `commandMenuItems` / `pageLayoutWidgets` arrays on the manifest, and the server aggregator to consume them through the existing flat-entity converters. - On the server, expose `Application.commandMenuItems` (relation + DTO + service hydration in `findOneApplication`). - On the front, list command menu items in the application content tab and add a dedicated detail page with a settings tab, mirroring how `frontComponents` are surfaced. - Add `twenty add` templates and Vitest unit tests for both new defines. - Document the standalone-vs-nested pattern in `packages/twenty-sdk/README.md`. ### Why Until now, command menu items could only be declared as the nested `command:` field on `defineFrontComponent` — there was no way to register a command menu item from a separate file or from another package. The `SyncableEntity` enum had 12 values, while the server already synced 18 (including `commandMenuItem` and `pageLayoutWidget`). The same gap existed for `pageLayoutWidget`, which had no top-level define despite being synced server-side. This PR closes both gaps and aligns the SDK surface with what the server actually accepts. The standalone defines coexist with the nested form — pick one per entity, never both with the same `universalIdentifier` (the manifest aggregator will throw on duplicates). The README now documents this. ## Test plan - [x] `npx nx typecheck twenty-sdk` / `twenty-server` / `twenty-front` - [x] `npx nx lint:diff-with-main twenty-front` / `twenty-server` - [x] `npx nx lint twenty-sdk` / `twenty-shared` - [x] New unit tests: `define-command-menu-item.spec.ts`, `define-page-layout-widget.spec.ts` - [x] Existing manifest extract config tests still pass - [ ] Codegen `npx nx run twenty-front:graphql:generate --configuration=metadata` should be re-run after merge — the generated `graphql.ts` was patched manually to include `commandMenuItems` on `Application` and the `FindOneApplication` document. - [ ] Smoke test: scaffold an app with `twenty add` for both new entity types, run `twenty dev`, confirm the dev UI shows them in the sync list and the settings page surfaces command menu items in the content tab. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
+1
@@ -7,6 +7,7 @@ import {
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
export const EXPECTED_MANIFEST: Manifest = {
|
||||
commandMenuItems: [],
|
||||
application: {
|
||||
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000001',
|
||||
displayName: 'Root App',
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ import {
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
export const EXPECTED_MANIFEST: Manifest = {
|
||||
commandMenuItems: [],
|
||||
pageLayouts: [],
|
||||
pageLayoutTabs: [
|
||||
{
|
||||
|
||||
@@ -33,6 +33,7 @@ export const normalizeManifestForComparison = <T extends Manifest>(
|
||||
navigationMenuItems: sortById(manifest.navigationMenuItems),
|
||||
pageLayouts: sortById(manifest.pageLayouts),
|
||||
pageLayoutTabs: sortById(manifest.pageLayoutTabs ?? []),
|
||||
commandMenuItems: sortById(manifest.commandMenuItems ?? []),
|
||||
logicFunctions: sortById(
|
||||
manifest.logicFunctions?.map((fn) => ({
|
||||
...fn,
|
||||
|
||||
@@ -15,6 +15,7 @@ import { getFrontComponentBaseFile } from '@/cli/utilities/entity/entity-front-c
|
||||
import { getLogicFunctionBaseFile } from '@/cli/utilities/entity/entity-logic-function-template';
|
||||
import { getNavigationMenuItemBaseFile } from '@/cli/utilities/entity/entity-navigation-menu-item-template';
|
||||
import { getObjectBaseFile } from '@/cli/utilities/entity/entity-object-template';
|
||||
import { getCommandMenuItemBaseFile } from '@/cli/utilities/entity/entity-command-menu-item-template';
|
||||
import { getPageLayoutBaseFile } from '@/cli/utilities/entity/entity-page-layout-template';
|
||||
import { getPageLayoutTabBaseFile } from '@/cli/utilities/entity/entity-page-layout-tab-template';
|
||||
import { getRecordPageLayoutBaseFile } from '@/cli/utilities/entity/entity-record-page-layout-template';
|
||||
@@ -217,6 +218,15 @@ export class EntityAddCommand {
|
||||
return { name, file };
|
||||
}
|
||||
|
||||
case SyncableEntity.CommandMenuItem: {
|
||||
const name = await this.getEntityName(entity);
|
||||
|
||||
const file = getCommandMenuItemBaseFile({
|
||||
name,
|
||||
});
|
||||
return { name, file };
|
||||
}
|
||||
|
||||
default:
|
||||
assertUnreachable(entity);
|
||||
}
|
||||
|
||||
+6
-11
@@ -1,14 +1,9 @@
|
||||
import { defineFrontComponent } from '@/sdk/define';
|
||||
import { defineCommandMenuItem } from '@/sdk/define';
|
||||
import { numberOfSelectedRecords } from '@/sdk/front-component';
|
||||
|
||||
const MyComponent = () => null;
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'comparison-operator',
|
||||
component: MyComponent,
|
||||
command: {
|
||||
universalIdentifier: 'comparison-operator-cmd',
|
||||
label: 'Comparison Operator',
|
||||
conditionalAvailabilityExpression: numberOfSelectedRecords > 0,
|
||||
},
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'comparison-operator-cmd',
|
||||
label: 'Comparison Operator',
|
||||
frontComponentUniversalIdentifier: 'comparison-operator',
|
||||
conditionalAvailabilityExpression: numberOfSelectedRecords > 0,
|
||||
});
|
||||
|
||||
+9
-14
@@ -1,4 +1,4 @@
|
||||
import { defineFrontComponent } from '@/sdk/define';
|
||||
import { defineCommandMenuItem } from '@/sdk/define';
|
||||
import {
|
||||
none,
|
||||
numberOfSelectedRecords,
|
||||
@@ -6,17 +6,12 @@ import {
|
||||
selectedRecords,
|
||||
} from '@/sdk/front-component';
|
||||
|
||||
const MyComponent = () => null;
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'complex-soft-delete',
|
||||
component: MyComponent,
|
||||
command: {
|
||||
universalIdentifier: 'complex-soft-delete-cmd',
|
||||
label: 'Complex Soft Delete',
|
||||
conditionalAvailabilityExpression:
|
||||
objectPermissions.canSoftDeleteObjectRecords &&
|
||||
none(selectedRecords, 'isRemote') &&
|
||||
numberOfSelectedRecords > 0,
|
||||
},
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'complex-soft-delete-cmd',
|
||||
label: 'Complex Soft Delete',
|
||||
frontComponentUniversalIdentifier: 'complex-soft-delete',
|
||||
conditionalAvailabilityExpression:
|
||||
objectPermissions.canSoftDeleteObjectRecords &&
|
||||
none(selectedRecords, 'isRemote') &&
|
||||
numberOfSelectedRecords > 0,
|
||||
});
|
||||
|
||||
+6
-14
@@ -1,17 +1,9 @@
|
||||
import { defineFrontComponent } from '@/sdk/define';
|
||||
import { defineCommandMenuItem } from '@/sdk/define';
|
||||
import { someDefined, selectedRecords } from '@/sdk/front-component';
|
||||
|
||||
const MyComponent = () => null;
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'custom-function',
|
||||
component: MyComponent,
|
||||
command: {
|
||||
universalIdentifier: 'custom-function-cmd',
|
||||
label: 'Custom Function',
|
||||
conditionalAvailabilityExpression: someDefined(
|
||||
selectedRecords,
|
||||
'deletedAt',
|
||||
),
|
||||
},
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'custom-function-cmd',
|
||||
label: 'Custom Function',
|
||||
frontComponentUniversalIdentifier: 'custom-function',
|
||||
conditionalAvailabilityExpression: someDefined(selectedRecords, 'deletedAt'),
|
||||
});
|
||||
|
||||
+8
-13
@@ -1,16 +1,11 @@
|
||||
import { defineFrontComponent } from '@/sdk/define';
|
||||
import { defineCommandMenuItem } from '@/sdk/define';
|
||||
import { featureFlags, objectPermissions } from '@/sdk/front-component';
|
||||
|
||||
const MyComponent = () => null;
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'feature-flag-gated',
|
||||
component: MyComponent,
|
||||
command: {
|
||||
universalIdentifier: 'feature-flag-gated-cmd',
|
||||
label: 'Feature Flag Gated',
|
||||
conditionalAvailabilityExpression:
|
||||
featureFlags.IS_JUNCTION_RELATIONS_ENABLED &&
|
||||
objectPermissions.canReadObjectRecords,
|
||||
},
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'feature-flag-gated-cmd',
|
||||
label: 'Feature Flag Gated',
|
||||
frontComponentUniversalIdentifier: 'feature-flag-gated',
|
||||
conditionalAvailabilityExpression:
|
||||
featureFlags.IS_JUNCTION_RELATIONS_ENABLED &&
|
||||
objectPermissions.canReadObjectRecords,
|
||||
});
|
||||
|
||||
+8
-13
@@ -1,20 +1,15 @@
|
||||
import { defineFrontComponent } from '@/sdk/define';
|
||||
import { defineCommandMenuItem } from '@/sdk/define';
|
||||
import {
|
||||
favoriteRecordIds,
|
||||
objectMetadataItem,
|
||||
pageType,
|
||||
} from '@/sdk/front-component';
|
||||
|
||||
const MyComponent = () => null;
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'parenthesized-expression',
|
||||
component: MyComponent,
|
||||
command: {
|
||||
universalIdentifier: 'parenthesized-expression-cmd',
|
||||
label: 'Parenthesized Expression',
|
||||
conditionalAvailabilityExpression:
|
||||
(pageType === 'RECORD_PAGE' || favoriteRecordIds.length > 0) &&
|
||||
!objectMetadataItem.isRemote,
|
||||
},
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'parenthesized-expression-cmd',
|
||||
label: 'Parenthesized Expression',
|
||||
frontComponentUniversalIdentifier: 'parenthesized-expression',
|
||||
conditionalAvailabilityExpression:
|
||||
(pageType === 'RECORD_PAGE' || favoriteRecordIds.length > 0) &&
|
||||
!objectMetadataItem.isRemote,
|
||||
});
|
||||
|
||||
+7
-12
@@ -1,15 +1,10 @@
|
||||
import { defineFrontComponent } from '@/sdk/define';
|
||||
import { defineCommandMenuItem } from '@/sdk/define';
|
||||
import { isInSidePanel, objectPermissions } from '@/sdk/front-component';
|
||||
|
||||
const MyComponent = () => null;
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'permissions-check',
|
||||
component: MyComponent,
|
||||
command: {
|
||||
universalIdentifier: 'permissions-check-cmd',
|
||||
label: 'Permissions Check',
|
||||
conditionalAvailabilityExpression:
|
||||
objectPermissions.canUpdateObjectRecords && !isInSidePanel,
|
||||
},
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'permissions-check-cmd',
|
||||
label: 'Permissions Check',
|
||||
frontComponentUniversalIdentifier: 'permissions-check',
|
||||
conditionalAvailabilityExpression:
|
||||
objectPermissions.canUpdateObjectRecords && !isInSidePanel,
|
||||
});
|
||||
|
||||
+6
-11
@@ -1,14 +1,9 @@
|
||||
import { defineFrontComponent } from '@/sdk/define';
|
||||
import { defineCommandMenuItem } from '@/sdk/define';
|
||||
import { pageType } from '@/sdk/front-component';
|
||||
|
||||
const MyComponent = () => null;
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'simple-boolean',
|
||||
component: MyComponent,
|
||||
command: {
|
||||
universalIdentifier: 'simple-boolean-cmd',
|
||||
label: 'Simple Boolean',
|
||||
conditionalAvailabilityExpression: pageType === 'RECORD_PAGE',
|
||||
},
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'simple-boolean-cmd',
|
||||
label: 'Simple Boolean',
|
||||
frontComponentUniversalIdentifier: 'simple-boolean',
|
||||
conditionalAvailabilityExpression: pageType === 'RECORD_PAGE',
|
||||
});
|
||||
|
||||
+8
-13
@@ -1,16 +1,11 @@
|
||||
import { defineFrontComponent } from '@/sdk/define';
|
||||
import { defineCommandMenuItem } from '@/sdk/define';
|
||||
import { everyEquals, pageType, selectedRecords } from '@/sdk/front-component';
|
||||
|
||||
const MyComponent = () => null;
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'string-comparison',
|
||||
component: MyComponent,
|
||||
command: {
|
||||
universalIdentifier: 'string-comparison-cmd',
|
||||
label: 'String Comparison',
|
||||
conditionalAvailabilityExpression:
|
||||
pageType === 'RECORD_PAGE' &&
|
||||
everyEquals(selectedRecords, 'company.name', 'apple'),
|
||||
},
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'string-comparison-cmd',
|
||||
label: 'String Comparison',
|
||||
frontComponentUniversalIdentifier: 'string-comparison',
|
||||
conditionalAvailabilityExpression:
|
||||
pageType === 'RECORD_PAGE' &&
|
||||
everyEquals(selectedRecords, 'company.name', 'apple'),
|
||||
});
|
||||
|
||||
+7
-12
@@ -1,15 +1,10 @@
|
||||
import { defineFrontComponent } from '@/sdk/define';
|
||||
import { defineCommandMenuItem } from '@/sdk/define';
|
||||
import { pageType, targetObjectWritePermissions } from '@/sdk/front-component';
|
||||
|
||||
const MyComponent = () => null;
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'target-permissions',
|
||||
component: MyComponent,
|
||||
command: {
|
||||
universalIdentifier: 'target-permissions-cmd',
|
||||
label: 'Target Permissions',
|
||||
conditionalAvailabilityExpression:
|
||||
pageType === 'RECORD_PAGE' && targetObjectWritePermissions.person,
|
||||
},
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'target-permissions-cmd',
|
||||
label: 'Target Permissions',
|
||||
frontComponentUniversalIdentifier: 'target-permissions',
|
||||
conditionalAvailabilityExpression:
|
||||
pageType === 'RECORD_PAGE' && targetObjectWritePermissions.person,
|
||||
});
|
||||
|
||||
+1
@@ -33,6 +33,7 @@ exports[`stub-twenty-sdk-define plugin > matches the recorded export partition 1
|
||||
"createValidationResult",
|
||||
"defineAgent",
|
||||
"defineApplication",
|
||||
"defineCommandMenuItem",
|
||||
"defineConnectionProvider",
|
||||
"defineField",
|
||||
"defineFrontComponent",
|
||||
|
||||
+2
-1
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
type ApplicationManifest,
|
||||
type Manifest,
|
||||
type FieldManifest,
|
||||
type Manifest,
|
||||
} from 'twenty-shared/application';
|
||||
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
|
||||
import { manifestValidate } from '@/cli/utilities/build/manifest/manifest-validate';
|
||||
@@ -25,6 +25,7 @@ const validField: FieldManifest = {
|
||||
};
|
||||
|
||||
const validManifest: Manifest = {
|
||||
commandMenuItems: [],
|
||||
application: validApplication,
|
||||
objects: [],
|
||||
frontComponents: [],
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { extractManifestFromFile } from '@/cli/utilities/build/manifest/manifest-extract-config-from-file';
|
||||
import { getDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-fields-in-object-fields';
|
||||
import { type ApplicationConfig, type LogicFunctionConfig } from '@/sdk/define';
|
||||
import { type CommandMenuItemConfig } from '@/sdk/define/command-menu-items/command-menu-item-config';
|
||||
import { type FrontComponentConfig } from '@/sdk/define/front-component/front-component-config';
|
||||
import { type ObjectConfig } from '@/sdk/define/objects/object-config';
|
||||
import { type PageLayoutConfig } from '@/sdk/define/page-layouts/page-layout-config';
|
||||
@@ -21,9 +22,9 @@ import {
|
||||
type ApplicationManifest,
|
||||
type AssetManifest,
|
||||
ASSETS_DIR,
|
||||
type CommandMenuItemManifest,
|
||||
type ConnectionProviderManifest,
|
||||
type FieldManifest,
|
||||
type FrontComponentCommandManifest,
|
||||
type FrontComponentManifest,
|
||||
type LogicFunctionManifest,
|
||||
type Manifest,
|
||||
@@ -89,6 +90,7 @@ export const buildManifest = async (
|
||||
const navigationMenuItems: NavigationMenuItemManifest[] = [];
|
||||
const pageLayouts: PageLayoutManifest[] = [];
|
||||
const pageLayoutTabs: PageLayoutTabManifest[] = [];
|
||||
const commandMenuItems: CommandMenuItemManifest[] = [];
|
||||
const postInstallLogicFunctions: PostInstallLogicFunctionApplicationManifest[] =
|
||||
[];
|
||||
const preInstallLogicFunctions: PreInstallLogicFunctionApplicationManifest[] =
|
||||
@@ -107,6 +109,7 @@ export const buildManifest = async (
|
||||
const navigationMenuItemsFilePaths: string[] = [];
|
||||
const pageLayoutsFilePaths: string[] = [];
|
||||
const pageLayoutTabsFilePaths: string[] = [];
|
||||
const commandMenuItemsFilePaths: string[] = [];
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
const fileContent = await readFile(filePath, 'utf-8');
|
||||
@@ -321,7 +324,7 @@ export const buildManifest = async (
|
||||
|
||||
errors.push(...extract.errors);
|
||||
|
||||
const { component, command, ...rest } = extract.config;
|
||||
const { component, ...rest } = extract.config;
|
||||
|
||||
const relativeFilePath = relative(appPath, filePath);
|
||||
|
||||
@@ -332,8 +335,6 @@ export const buildManifest = async (
|
||||
builtComponentPath: relativeFilePath.replace(/\.tsx?$/, '.mjs'),
|
||||
builtComponentChecksum: '',
|
||||
isHeadless: rest.isHeadless ?? false,
|
||||
// transformed by conditionalAvailabilityTransformPlugin
|
||||
command: command as FrontComponentCommandManifest,
|
||||
};
|
||||
|
||||
frontComponents.push(config);
|
||||
@@ -397,6 +398,19 @@ export const buildManifest = async (
|
||||
pageLayoutTabsFilePaths.push(relativePath);
|
||||
break;
|
||||
}
|
||||
case ManifestEntityKey.CommandMenuItems: {
|
||||
const extract = await extractManifestFromFile<CommandMenuItemConfig>({
|
||||
appPath,
|
||||
filePath,
|
||||
});
|
||||
|
||||
commandMenuItems.push(
|
||||
extract.config as unknown as CommandMenuItemManifest,
|
||||
);
|
||||
errors.push(...extract.errors);
|
||||
commandMenuItemsFilePaths.push(relativePath);
|
||||
break;
|
||||
}
|
||||
case ManifestEntityKey.PublicAssets: {
|
||||
// Public assets are handled below
|
||||
break;
|
||||
@@ -475,6 +489,7 @@ export const buildManifest = async (
|
||||
navigationMenuItems: navigationMenuItems.sort(byId),
|
||||
pageLayouts: pageLayouts.sort(byId),
|
||||
pageLayoutTabs: pageLayoutTabs.sort(byId),
|
||||
commandMenuItems: commandMenuItems.sort(byId),
|
||||
};
|
||||
|
||||
const entityFilePaths: EntityFilePaths = {
|
||||
@@ -492,6 +507,7 @@ export const buildManifest = async (
|
||||
navigationMenuItems: navigationMenuItemsFilePaths,
|
||||
pageLayouts: pageLayoutsFilePaths,
|
||||
pageLayoutTabs: pageLayoutTabsFilePaths,
|
||||
commandMenuItems: commandMenuItemsFilePaths,
|
||||
};
|
||||
|
||||
return { manifest, filePaths: entityFilePaths, errors };
|
||||
|
||||
@@ -16,6 +16,7 @@ export enum TargetFunction {
|
||||
DefineNavigationMenuItem = 'defineNavigationMenuItem',
|
||||
DefinePageLayout = 'definePageLayout',
|
||||
DefinePageLayoutTab = 'definePageLayoutTab',
|
||||
DefineCommandMenuItem = 'defineCommandMenuItem',
|
||||
}
|
||||
|
||||
export enum ManifestEntityKey {
|
||||
@@ -33,6 +34,7 @@ export enum ManifestEntityKey {
|
||||
NavigationMenuItems = 'navigationMenuItems',
|
||||
PageLayouts = 'pageLayouts',
|
||||
PageLayoutTabs = 'pageLayoutTabs',
|
||||
CommandMenuItems = 'commandMenuItems',
|
||||
}
|
||||
|
||||
export type EntityFilePaths = Record<ManifestEntityKey, string[]>;
|
||||
@@ -60,6 +62,7 @@ export const TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING: Record<
|
||||
ManifestEntityKey.NavigationMenuItems,
|
||||
[TargetFunction.DefinePageLayout]: ManifestEntityKey.PageLayouts,
|
||||
[TargetFunction.DefinePageLayoutTab]: ManifestEntityKey.PageLayoutTabs,
|
||||
[TargetFunction.DefineCommandMenuItem]: ManifestEntityKey.CommandMenuItems,
|
||||
};
|
||||
|
||||
const computeIsTargetFunctionCall = (node: ts.Node): string | undefined => {
|
||||
|
||||
@@ -76,6 +76,7 @@ const ENTITY_TYPE_TO_SYNCABLE: Record<string, SyncableEntity | undefined> = {
|
||||
navigationMenuItems: SyncableEntity.NavigationMenuItem,
|
||||
pageLayouts: SyncableEntity.PageLayout,
|
||||
pageLayoutTabs: SyncableEntity.PageLayoutTab,
|
||||
commandMenuItems: SyncableEntity.CommandMenuItem,
|
||||
};
|
||||
|
||||
const MAX_EVENT_COUNT = 200;
|
||||
|
||||
@@ -97,6 +97,7 @@ export const ENTITY_LABELS: Record<SyncableEntity, string> = {
|
||||
[SyncableEntity.Field]: 'Fields',
|
||||
[SyncableEntity.LogicFunction]: 'Logic functions',
|
||||
[SyncableEntity.FrontComponent]: 'Front components',
|
||||
[SyncableEntity.CommandMenuItem]: 'Command menu items',
|
||||
[SyncableEntity.Role]: 'Roles',
|
||||
[SyncableEntity.Skill]: 'Skills',
|
||||
[SyncableEntity.View]: 'Views',
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export const getCommandMenuItemBaseFile = ({ name }: { name: string }) => {
|
||||
return `import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: '${uuidv4()}',
|
||||
frontComponentUniversalIdentifier:
|
||||
'replace-with-existing-front-component-uuid',
|
||||
label: '${name}',
|
||||
availabilityType: 'GLOBAL',
|
||||
});
|
||||
`;
|
||||
};
|
||||
Reference in New Issue
Block a user