[FRONT COMPONENTS] Declare command menu items in front components (#18047)
## Description
- Adds support for declaring command menu items directly within
`defineFrontComponent` via an optional command config property
- Introduces a new `CommandMenuItemManifest` type in twenty-shared and
wires it through the manifest build pipeline
## Example Of usage
```tsx
import { defineFrontComponent } from "twenty-sdk";
const TestAction = () => {
return <div>Test Action</div>;
};
export default defineFrontComponent({
universalIdentifier: "6c289461-0007-4a62-a99f-69e5c11a4ce7",
name: "test-action",
description: "Test Action",
component: TestAction,
command: {
universalIdentifier: "c07df864-495f-46f3-9f5b-9d3ce2589e9b",
label: "Run My Action",
icon: "IconBolt",
isPinned: false,
},
});
```
## Video QA
https://github.com/user-attachments/assets/f910fc6a-44a9-45d1-87c5-f0ce64bb3878
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@ const DEFINE_FRONT_COMPONENT_IMPORT_PATTERN =
|
||||
/import\s*\{\s*defineFrontComponent\s*\}\s*from\s*['"][^'"]+['"];?\n?/g;
|
||||
|
||||
const DEFINE_FRONT_COMPONENT_EXPORT_PATTERN =
|
||||
/export\s+default\s+defineFrontComponent\s*\(\s*\{[^}]*component\s*:\s*(\w+)[^}]*\}\s*\)\s*;?/s;
|
||||
/export\s+default\s+defineFrontComponent\s*\(\s*\{[\s\S]*?component\s*:\s*(\w+)[\s\S]*?\}\s*\)\s*;?/s;
|
||||
|
||||
export const unwrapDefineFrontComponentToDirectExport = (
|
||||
sourceCode: string,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING,
|
||||
} from '@/cli/utilities/build/manifest/manifest-extract-config';
|
||||
import { extractManifestFromFile } from '@/cli/utilities/build/manifest/manifest-extract-config-from-file';
|
||||
import { injectDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/inject-default-fields-in-object-fields';
|
||||
import {
|
||||
type ApplicationConfig,
|
||||
type FrontComponentConfig,
|
||||
@@ -32,7 +33,6 @@ import {
|
||||
} from 'twenty-shared/application';
|
||||
import { getInputSchemaFromSourceCode } from 'twenty-shared/logic-function';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { injectDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/inject-default-fields-in-object-fields';
|
||||
|
||||
const loadSources = async (appPath: string): Promise<string[]> => {
|
||||
return await glob(['**/*.ts', '**/*.tsx'], {
|
||||
@@ -216,6 +216,7 @@ export const buildManifest = async (
|
||||
|
||||
frontComponents.push(config);
|
||||
frontComponentsFilePaths.push(relativePath);
|
||||
|
||||
break;
|
||||
}
|
||||
case ManifestEntityKey.Views: {
|
||||
|
||||
@@ -19,6 +19,16 @@ export const defineFrontComponent: DefineEntity<FrontComponentConfig> = (
|
||||
errors.push('Front component component must be a React component');
|
||||
}
|
||||
|
||||
if (config.command) {
|
||||
if (!config.command.universalIdentifier) {
|
||||
errors.push('Command must have a universalIdentifier');
|
||||
}
|
||||
|
||||
if (!config.command.label) {
|
||||
errors.push('Command must have a label');
|
||||
}
|
||||
}
|
||||
|
||||
return createValidationResult({
|
||||
config,
|
||||
errors,
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { type FrontComponentManifest } from 'twenty-shared/application';
|
||||
import {
|
||||
type FrontComponentCommandManifest,
|
||||
type FrontComponentManifest,
|
||||
} from 'twenty-shared/application';
|
||||
|
||||
export type FrontComponentType = React.ComponentType<any>;
|
||||
|
||||
export type FrontComponentCommandConfig = FrontComponentCommandManifest;
|
||||
|
||||
export type FrontComponentConfig = Omit<
|
||||
FrontComponentManifest,
|
||||
| 'sourceComponentPath'
|
||||
|
||||
@@ -24,6 +24,7 @@ export { OnDeleteAction } from './fields/on-delete-action';
|
||||
export { RelationType } from './fields/relation-type';
|
||||
export { validateFields } from './fields/validate-fields';
|
||||
export type {
|
||||
FrontComponentCommandConfig,
|
||||
FrontComponentConfig,
|
||||
FrontComponentType,
|
||||
} from './front-component-config';
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ export const APPLICATION_MANIFEST_METADATA_NAMES = [
|
||||
'pageLayout',
|
||||
'pageLayoutTab',
|
||||
'pageLayoutWidget',
|
||||
'commandMenuItem',
|
||||
] as const satisfies AllMetadataName[];
|
||||
|
||||
export type ApplicationManifestMetadataName =
|
||||
|
||||
+20
@@ -1,6 +1,7 @@
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
|
||||
import { type ApplicationManifestMetadataName } from 'src/engine/core-modules/application/constants/application-manifest-metadata-names.constant';
|
||||
import { fromCommandMenuItemManifestToUniversalFlatCommandMenuItem } from 'src/engine/core-modules/application/utils/from-command-menu-item-manifest-to-universal-flat-command-menu-item.util';
|
||||
import { fromFieldManifestToUniversalFlatFieldMetadata } from 'src/engine/core-modules/application/utils/from-field-manifest-to-universal-flat-field-metadata.util';
|
||||
import { fromFrontComponentManifestToUniversalFlatFrontComponent } from 'src/engine/core-modules/application/utils/from-front-component-manifest-to-universal-flat-front-component.util';
|
||||
import { fromLogicFunctionManifestToUniversalFlatLogicFunction } from 'src/engine/core-modules/application/utils/from-logic-function-manifest-to-universal-flat-logic-function.util';
|
||||
@@ -117,6 +118,25 @@ export const computeApplicationManifestAllUniversalFlatEntityMaps = ({
|
||||
universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps,
|
||||
},
|
||||
);
|
||||
|
||||
if (frontComponentManifest.command) {
|
||||
addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow(
|
||||
{
|
||||
metadataName: 'commandMenuItem',
|
||||
universalFlatEntity:
|
||||
fromCommandMenuItemManifestToUniversalFlatCommandMenuItem({
|
||||
commandMenuItemManifest: {
|
||||
...frontComponentManifest.command,
|
||||
frontComponentUniversalIdentifier:
|
||||
frontComponentManifest.universalIdentifier,
|
||||
},
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
}),
|
||||
universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const roleManifest of manifest.roles) {
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { type CommandMenuItemManifest } from 'twenty-shared/application';
|
||||
|
||||
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
|
||||
import { type UniversalFlatCommandMenuItem } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-command-menu-item.type';
|
||||
|
||||
const AVAILABILITY_TYPE_MAP: Record<
|
||||
NonNullable<CommandMenuItemManifest['availabilityType']>,
|
||||
CommandMenuItemAvailabilityType
|
||||
> = {
|
||||
GLOBAL: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
SINGLE_RECORD: CommandMenuItemAvailabilityType.SINGLE_RECORD,
|
||||
BULK_RECORDS: CommandMenuItemAvailabilityType.BULK_RECORDS,
|
||||
};
|
||||
|
||||
export const fromCommandMenuItemManifestToUniversalFlatCommandMenuItem = ({
|
||||
commandMenuItemManifest,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
}: {
|
||||
commandMenuItemManifest: CommandMenuItemManifest;
|
||||
applicationUniversalIdentifier: string;
|
||||
now: string;
|
||||
}): UniversalFlatCommandMenuItem => {
|
||||
return {
|
||||
universalIdentifier: commandMenuItemManifest.universalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
label: commandMenuItemManifest.label,
|
||||
icon: commandMenuItemManifest.icon ?? null,
|
||||
isPinned: commandMenuItemManifest.isPinned ?? false,
|
||||
availabilityType: commandMenuItemManifest.availabilityType
|
||||
? AVAILABILITY_TYPE_MAP[commandMenuItemManifest.availabilityType]
|
||||
: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
frontComponentUniversalIdentifier:
|
||||
commandMenuItemManifest.frontComponentUniversalIdentifier,
|
||||
availabilityObjectMetadataUniversalIdentifier:
|
||||
commandMenuItemManifest.availabilityObjectUniversalIdentifier ?? null,
|
||||
workflowVersionId: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
};
|
||||
@@ -1,3 +1,19 @@
|
||||
import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType';
|
||||
|
||||
export type CommandMenuItemManifest = SyncableEntityOptions & {
|
||||
label: string;
|
||||
icon?: string;
|
||||
isPinned?: boolean;
|
||||
availabilityType?: 'GLOBAL' | 'SINGLE_RECORD' | 'BULK_RECORDS';
|
||||
availabilityObjectUniversalIdentifier?: string;
|
||||
frontComponentUniversalIdentifier: string;
|
||||
};
|
||||
|
||||
export type FrontComponentCommandManifest = Omit<
|
||||
CommandMenuItemManifest,
|
||||
'frontComponentUniversalIdentifier'
|
||||
>;
|
||||
|
||||
export type FrontComponentManifest = {
|
||||
universalIdentifier: string;
|
||||
name?: string;
|
||||
@@ -6,4 +22,5 @@ export type FrontComponentManifest = {
|
||||
builtComponentPath: string;
|
||||
builtComponentChecksum: string;
|
||||
componentName: string;
|
||||
command?: FrontComponentCommandManifest;
|
||||
};
|
||||
|
||||
@@ -25,7 +25,11 @@ export type {
|
||||
RelationFieldManifest,
|
||||
FieldManifest,
|
||||
} from './fieldManifestType';
|
||||
export type { FrontComponentManifest } from './frontComponentManifestType';
|
||||
export type {
|
||||
CommandMenuItemManifest,
|
||||
FrontComponentCommandManifest,
|
||||
FrontComponentManifest,
|
||||
} from './frontComponentManifestType';
|
||||
export type {
|
||||
LogicFunctionManifest,
|
||||
CronTriggerSettings,
|
||||
|
||||
Reference in New Issue
Block a user