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:
@@ -324,6 +324,115 @@ type FrontComponent {
|
||||
applicationTokenPair: ApplicationTokenPair
|
||||
}
|
||||
|
||||
type CommandMenuItem {
|
||||
id: UUID!
|
||||
workflowVersionId: UUID
|
||||
frontComponentId: UUID
|
||||
frontComponent: FrontComponent
|
||||
engineComponentKey: EngineComponentKey!
|
||||
label: String!
|
||||
icon: String
|
||||
shortLabel: String
|
||||
position: Float!
|
||||
isPinned: Boolean!
|
||||
availabilityType: CommandMenuItemAvailabilityType!
|
||||
payload: CommandMenuItemPayload
|
||||
hotKeys: [String!]
|
||||
conditionalAvailabilityExpression: String
|
||||
availabilityObjectMetadataId: UUID
|
||||
pageLayoutId: UUID
|
||||
universalIdentifier: UUID
|
||||
applicationId: UUID
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
enum EngineComponentKey {
|
||||
NAVIGATE_TO_NEXT_RECORD
|
||||
NAVIGATE_TO_PREVIOUS_RECORD
|
||||
CREATE_NEW_RECORD
|
||||
DELETE_RECORDS
|
||||
RESTORE_RECORDS
|
||||
DESTROY_RECORDS
|
||||
ADD_TO_FAVORITES
|
||||
REMOVE_FROM_FAVORITES
|
||||
EXPORT_NOTE_TO_PDF
|
||||
EXPORT_RECORDS
|
||||
UPDATE_MULTIPLE_RECORDS
|
||||
MERGE_MULTIPLE_RECORDS
|
||||
IMPORT_RECORDS
|
||||
EXPORT_VIEW
|
||||
SEE_DELETED_RECORDS
|
||||
CREATE_NEW_VIEW
|
||||
HIDE_DELETED_RECORDS
|
||||
EDIT_RECORD_PAGE_LAYOUT
|
||||
EDIT_DASHBOARD_LAYOUT
|
||||
SAVE_DASHBOARD_LAYOUT
|
||||
CANCEL_DASHBOARD_LAYOUT
|
||||
DUPLICATE_DASHBOARD
|
||||
ACTIVATE_WORKFLOW
|
||||
DEACTIVATE_WORKFLOW
|
||||
DISCARD_DRAFT_WORKFLOW
|
||||
TEST_WORKFLOW
|
||||
SEE_ACTIVE_VERSION_WORKFLOW
|
||||
SEE_RUNS_WORKFLOW
|
||||
SEE_VERSIONS_WORKFLOW
|
||||
ADD_NODE_WORKFLOW
|
||||
TIDY_UP_WORKFLOW
|
||||
DUPLICATE_WORKFLOW
|
||||
SEE_VERSION_WORKFLOW_RUN
|
||||
SEE_WORKFLOW_WORKFLOW_RUN
|
||||
STOP_WORKFLOW_RUN
|
||||
SEE_RUNS_WORKFLOW_VERSION
|
||||
SEE_WORKFLOW_WORKFLOW_VERSION
|
||||
USE_AS_DRAFT_WORKFLOW_VERSION
|
||||
SEE_VERSIONS_WORKFLOW_VERSION
|
||||
SEARCH_RECORDS
|
||||
SEARCH_RECORDS_FALLBACK
|
||||
ASK_AI
|
||||
VIEW_PREVIOUS_AI_CHATS
|
||||
NAVIGATION
|
||||
TRIGGER_WORKFLOW_VERSION
|
||||
FRONT_COMPONENT_RENDERER
|
||||
REPLY_TO_EMAIL_THREAD
|
||||
COMPOSE_EMAIL
|
||||
GO_TO_PEOPLE
|
||||
GO_TO_COMPANIES
|
||||
GO_TO_DASHBOARDS
|
||||
GO_TO_OPPORTUNITIES
|
||||
GO_TO_SETTINGS
|
||||
GO_TO_TASKS
|
||||
GO_TO_NOTES
|
||||
GO_TO_WORKFLOWS
|
||||
GO_TO_RUNS
|
||||
DELETE_SINGLE_RECORD
|
||||
DELETE_MULTIPLE_RECORDS
|
||||
RESTORE_SINGLE_RECORD
|
||||
RESTORE_MULTIPLE_RECORDS
|
||||
DESTROY_SINGLE_RECORD
|
||||
DESTROY_MULTIPLE_RECORDS
|
||||
EXPORT_FROM_RECORD_INDEX
|
||||
EXPORT_FROM_RECORD_SHOW
|
||||
EXPORT_MULTIPLE_RECORDS
|
||||
}
|
||||
|
||||
enum CommandMenuItemAvailabilityType {
|
||||
GLOBAL
|
||||
GLOBAL_OBJECT_CONTEXT
|
||||
RECORD_SELECTION
|
||||
FALLBACK
|
||||
}
|
||||
|
||||
union CommandMenuItemPayload = PathCommandMenuItemPayload | ObjectMetadataCommandMenuItemPayload
|
||||
|
||||
type PathCommandMenuItemPayload {
|
||||
path: String!
|
||||
}
|
||||
|
||||
type ObjectMetadataCommandMenuItemPayload {
|
||||
objectMetadataItemId: UUID!
|
||||
}
|
||||
|
||||
type LogicFunction {
|
||||
id: UUID!
|
||||
name: String!
|
||||
@@ -595,6 +704,7 @@ type Application {
|
||||
defaultLogicFunctionRole: Role
|
||||
agents: [Agent!]!
|
||||
frontComponents: [FrontComponent!]!
|
||||
commandMenuItems: [CommandMenuItem!]!
|
||||
logicFunctions: [LogicFunction!]!
|
||||
objects: [Object!]!
|
||||
applicationVariables: [ApplicationVariable!]!
|
||||
@@ -2324,114 +2434,6 @@ type PostgresCredentials {
|
||||
workspaceId: UUID!
|
||||
}
|
||||
|
||||
type CommandMenuItem {
|
||||
id: UUID!
|
||||
workflowVersionId: UUID
|
||||
frontComponentId: UUID
|
||||
frontComponent: FrontComponent
|
||||
engineComponentKey: EngineComponentKey!
|
||||
label: String!
|
||||
icon: String
|
||||
shortLabel: String
|
||||
position: Float!
|
||||
isPinned: Boolean!
|
||||
availabilityType: CommandMenuItemAvailabilityType!
|
||||
payload: CommandMenuItemPayload
|
||||
hotKeys: [String!]
|
||||
conditionalAvailabilityExpression: String
|
||||
availabilityObjectMetadataId: UUID
|
||||
pageLayoutId: UUID
|
||||
applicationId: UUID
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
enum EngineComponentKey {
|
||||
NAVIGATE_TO_NEXT_RECORD
|
||||
NAVIGATE_TO_PREVIOUS_RECORD
|
||||
CREATE_NEW_RECORD
|
||||
DELETE_RECORDS
|
||||
RESTORE_RECORDS
|
||||
DESTROY_RECORDS
|
||||
ADD_TO_FAVORITES
|
||||
REMOVE_FROM_FAVORITES
|
||||
EXPORT_NOTE_TO_PDF
|
||||
EXPORT_RECORDS
|
||||
UPDATE_MULTIPLE_RECORDS
|
||||
MERGE_MULTIPLE_RECORDS
|
||||
IMPORT_RECORDS
|
||||
EXPORT_VIEW
|
||||
SEE_DELETED_RECORDS
|
||||
CREATE_NEW_VIEW
|
||||
HIDE_DELETED_RECORDS
|
||||
EDIT_RECORD_PAGE_LAYOUT
|
||||
EDIT_DASHBOARD_LAYOUT
|
||||
SAVE_DASHBOARD_LAYOUT
|
||||
CANCEL_DASHBOARD_LAYOUT
|
||||
DUPLICATE_DASHBOARD
|
||||
ACTIVATE_WORKFLOW
|
||||
DEACTIVATE_WORKFLOW
|
||||
DISCARD_DRAFT_WORKFLOW
|
||||
TEST_WORKFLOW
|
||||
SEE_ACTIVE_VERSION_WORKFLOW
|
||||
SEE_RUNS_WORKFLOW
|
||||
SEE_VERSIONS_WORKFLOW
|
||||
ADD_NODE_WORKFLOW
|
||||
TIDY_UP_WORKFLOW
|
||||
DUPLICATE_WORKFLOW
|
||||
SEE_VERSION_WORKFLOW_RUN
|
||||
SEE_WORKFLOW_WORKFLOW_RUN
|
||||
STOP_WORKFLOW_RUN
|
||||
SEE_RUNS_WORKFLOW_VERSION
|
||||
SEE_WORKFLOW_WORKFLOW_VERSION
|
||||
USE_AS_DRAFT_WORKFLOW_VERSION
|
||||
SEE_VERSIONS_WORKFLOW_VERSION
|
||||
SEARCH_RECORDS
|
||||
SEARCH_RECORDS_FALLBACK
|
||||
ASK_AI
|
||||
VIEW_PREVIOUS_AI_CHATS
|
||||
NAVIGATION
|
||||
TRIGGER_WORKFLOW_VERSION
|
||||
FRONT_COMPONENT_RENDERER
|
||||
REPLY_TO_EMAIL_THREAD
|
||||
COMPOSE_EMAIL
|
||||
GO_TO_PEOPLE
|
||||
GO_TO_COMPANIES
|
||||
GO_TO_DASHBOARDS
|
||||
GO_TO_OPPORTUNITIES
|
||||
GO_TO_SETTINGS
|
||||
GO_TO_TASKS
|
||||
GO_TO_NOTES
|
||||
GO_TO_WORKFLOWS
|
||||
GO_TO_RUNS
|
||||
DELETE_SINGLE_RECORD
|
||||
DELETE_MULTIPLE_RECORDS
|
||||
RESTORE_SINGLE_RECORD
|
||||
RESTORE_MULTIPLE_RECORDS
|
||||
DESTROY_SINGLE_RECORD
|
||||
DESTROY_MULTIPLE_RECORDS
|
||||
EXPORT_FROM_RECORD_INDEX
|
||||
EXPORT_FROM_RECORD_SHOW
|
||||
EXPORT_MULTIPLE_RECORDS
|
||||
}
|
||||
|
||||
enum CommandMenuItemAvailabilityType {
|
||||
GLOBAL
|
||||
GLOBAL_OBJECT_CONTEXT
|
||||
RECORD_SELECTION
|
||||
FALLBACK
|
||||
}
|
||||
|
||||
union CommandMenuItemPayload = PathCommandMenuItemPayload | ObjectMetadataCommandMenuItemPayload
|
||||
|
||||
type PathCommandMenuItemPayload {
|
||||
path: String!
|
||||
}
|
||||
|
||||
type ObjectMetadataCommandMenuItemPayload {
|
||||
objectMetadataItemId: UUID!
|
||||
}
|
||||
|
||||
type ToolIndexEntry {
|
||||
name: String!
|
||||
description: String!
|
||||
|
||||
@@ -278,6 +278,46 @@ export interface FrontComponent {
|
||||
__typename: 'FrontComponent'
|
||||
}
|
||||
|
||||
export interface CommandMenuItem {
|
||||
id: Scalars['UUID']
|
||||
workflowVersionId?: Scalars['UUID']
|
||||
frontComponentId?: Scalars['UUID']
|
||||
frontComponent?: FrontComponent
|
||||
engineComponentKey: EngineComponentKey
|
||||
label: Scalars['String']
|
||||
icon?: Scalars['String']
|
||||
shortLabel?: Scalars['String']
|
||||
position: Scalars['Float']
|
||||
isPinned: Scalars['Boolean']
|
||||
availabilityType: CommandMenuItemAvailabilityType
|
||||
payload?: CommandMenuItemPayload
|
||||
hotKeys?: Scalars['String'][]
|
||||
conditionalAvailabilityExpression?: Scalars['String']
|
||||
availabilityObjectMetadataId?: Scalars['UUID']
|
||||
pageLayoutId?: Scalars['UUID']
|
||||
universalIdentifier?: Scalars['UUID']
|
||||
applicationId?: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
__typename: 'CommandMenuItem'
|
||||
}
|
||||
|
||||
export type EngineComponentKey = 'NAVIGATE_TO_NEXT_RECORD' | 'NAVIGATE_TO_PREVIOUS_RECORD' | 'CREATE_NEW_RECORD' | 'DELETE_RECORDS' | 'RESTORE_RECORDS' | 'DESTROY_RECORDS' | 'ADD_TO_FAVORITES' | 'REMOVE_FROM_FAVORITES' | 'EXPORT_NOTE_TO_PDF' | 'EXPORT_RECORDS' | 'UPDATE_MULTIPLE_RECORDS' | 'MERGE_MULTIPLE_RECORDS' | 'IMPORT_RECORDS' | 'EXPORT_VIEW' | 'SEE_DELETED_RECORDS' | 'CREATE_NEW_VIEW' | 'HIDE_DELETED_RECORDS' | 'EDIT_RECORD_PAGE_LAYOUT' | 'EDIT_DASHBOARD_LAYOUT' | 'SAVE_DASHBOARD_LAYOUT' | 'CANCEL_DASHBOARD_LAYOUT' | 'DUPLICATE_DASHBOARD' | 'ACTIVATE_WORKFLOW' | 'DEACTIVATE_WORKFLOW' | 'DISCARD_DRAFT_WORKFLOW' | 'TEST_WORKFLOW' | 'SEE_ACTIVE_VERSION_WORKFLOW' | 'SEE_RUNS_WORKFLOW' | 'SEE_VERSIONS_WORKFLOW' | 'ADD_NODE_WORKFLOW' | 'TIDY_UP_WORKFLOW' | 'DUPLICATE_WORKFLOW' | 'SEE_VERSION_WORKFLOW_RUN' | 'SEE_WORKFLOW_WORKFLOW_RUN' | 'STOP_WORKFLOW_RUN' | 'SEE_RUNS_WORKFLOW_VERSION' | 'SEE_WORKFLOW_WORKFLOW_VERSION' | 'USE_AS_DRAFT_WORKFLOW_VERSION' | 'SEE_VERSIONS_WORKFLOW_VERSION' | 'SEARCH_RECORDS' | 'SEARCH_RECORDS_FALLBACK' | 'ASK_AI' | 'VIEW_PREVIOUS_AI_CHATS' | 'NAVIGATION' | 'TRIGGER_WORKFLOW_VERSION' | 'FRONT_COMPONENT_RENDERER' | 'REPLY_TO_EMAIL_THREAD' | 'COMPOSE_EMAIL' | 'GO_TO_PEOPLE' | 'GO_TO_COMPANIES' | 'GO_TO_DASHBOARDS' | 'GO_TO_OPPORTUNITIES' | 'GO_TO_SETTINGS' | 'GO_TO_TASKS' | 'GO_TO_NOTES' | 'GO_TO_WORKFLOWS' | 'GO_TO_RUNS' | 'DELETE_SINGLE_RECORD' | 'DELETE_MULTIPLE_RECORDS' | 'RESTORE_SINGLE_RECORD' | 'RESTORE_MULTIPLE_RECORDS' | 'DESTROY_SINGLE_RECORD' | 'DESTROY_MULTIPLE_RECORDS' | 'EXPORT_FROM_RECORD_INDEX' | 'EXPORT_FROM_RECORD_SHOW' | 'EXPORT_MULTIPLE_RECORDS'
|
||||
|
||||
export type CommandMenuItemAvailabilityType = 'GLOBAL' | 'GLOBAL_OBJECT_CONTEXT' | 'RECORD_SELECTION' | 'FALLBACK'
|
||||
|
||||
export type CommandMenuItemPayload = (PathCommandMenuItemPayload | ObjectMetadataCommandMenuItemPayload) & { __isUnion?: true }
|
||||
|
||||
export interface PathCommandMenuItemPayload {
|
||||
path: Scalars['String']
|
||||
__typename: 'PathCommandMenuItemPayload'
|
||||
}
|
||||
|
||||
export interface ObjectMetadataCommandMenuItemPayload {
|
||||
objectMetadataItemId: Scalars['UUID']
|
||||
__typename: 'ObjectMetadataCommandMenuItemPayload'
|
||||
}
|
||||
|
||||
export interface LogicFunction {
|
||||
id: Scalars['UUID']
|
||||
name: Scalars['String']
|
||||
@@ -429,6 +469,7 @@ export interface Application {
|
||||
defaultLogicFunctionRole?: Role
|
||||
agents: Agent[]
|
||||
frontComponents: FrontComponent[]
|
||||
commandMenuItems: CommandMenuItem[]
|
||||
logicFunctions: LogicFunction[]
|
||||
objects: Object[]
|
||||
applicationVariables: ApplicationVariable[]
|
||||
@@ -2070,45 +2111,6 @@ export interface PostgresCredentials {
|
||||
__typename: 'PostgresCredentials'
|
||||
}
|
||||
|
||||
export interface CommandMenuItem {
|
||||
id: Scalars['UUID']
|
||||
workflowVersionId?: Scalars['UUID']
|
||||
frontComponentId?: Scalars['UUID']
|
||||
frontComponent?: FrontComponent
|
||||
engineComponentKey: EngineComponentKey
|
||||
label: Scalars['String']
|
||||
icon?: Scalars['String']
|
||||
shortLabel?: Scalars['String']
|
||||
position: Scalars['Float']
|
||||
isPinned: Scalars['Boolean']
|
||||
availabilityType: CommandMenuItemAvailabilityType
|
||||
payload?: CommandMenuItemPayload
|
||||
hotKeys?: Scalars['String'][]
|
||||
conditionalAvailabilityExpression?: Scalars['String']
|
||||
availabilityObjectMetadataId?: Scalars['UUID']
|
||||
pageLayoutId?: Scalars['UUID']
|
||||
applicationId?: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
__typename: 'CommandMenuItem'
|
||||
}
|
||||
|
||||
export type EngineComponentKey = 'NAVIGATE_TO_NEXT_RECORD' | 'NAVIGATE_TO_PREVIOUS_RECORD' | 'CREATE_NEW_RECORD' | 'DELETE_RECORDS' | 'RESTORE_RECORDS' | 'DESTROY_RECORDS' | 'ADD_TO_FAVORITES' | 'REMOVE_FROM_FAVORITES' | 'EXPORT_NOTE_TO_PDF' | 'EXPORT_RECORDS' | 'UPDATE_MULTIPLE_RECORDS' | 'MERGE_MULTIPLE_RECORDS' | 'IMPORT_RECORDS' | 'EXPORT_VIEW' | 'SEE_DELETED_RECORDS' | 'CREATE_NEW_VIEW' | 'HIDE_DELETED_RECORDS' | 'EDIT_RECORD_PAGE_LAYOUT' | 'EDIT_DASHBOARD_LAYOUT' | 'SAVE_DASHBOARD_LAYOUT' | 'CANCEL_DASHBOARD_LAYOUT' | 'DUPLICATE_DASHBOARD' | 'ACTIVATE_WORKFLOW' | 'DEACTIVATE_WORKFLOW' | 'DISCARD_DRAFT_WORKFLOW' | 'TEST_WORKFLOW' | 'SEE_ACTIVE_VERSION_WORKFLOW' | 'SEE_RUNS_WORKFLOW' | 'SEE_VERSIONS_WORKFLOW' | 'ADD_NODE_WORKFLOW' | 'TIDY_UP_WORKFLOW' | 'DUPLICATE_WORKFLOW' | 'SEE_VERSION_WORKFLOW_RUN' | 'SEE_WORKFLOW_WORKFLOW_RUN' | 'STOP_WORKFLOW_RUN' | 'SEE_RUNS_WORKFLOW_VERSION' | 'SEE_WORKFLOW_WORKFLOW_VERSION' | 'USE_AS_DRAFT_WORKFLOW_VERSION' | 'SEE_VERSIONS_WORKFLOW_VERSION' | 'SEARCH_RECORDS' | 'SEARCH_RECORDS_FALLBACK' | 'ASK_AI' | 'VIEW_PREVIOUS_AI_CHATS' | 'NAVIGATION' | 'TRIGGER_WORKFLOW_VERSION' | 'FRONT_COMPONENT_RENDERER' | 'REPLY_TO_EMAIL_THREAD' | 'COMPOSE_EMAIL' | 'GO_TO_PEOPLE' | 'GO_TO_COMPANIES' | 'GO_TO_DASHBOARDS' | 'GO_TO_OPPORTUNITIES' | 'GO_TO_SETTINGS' | 'GO_TO_TASKS' | 'GO_TO_NOTES' | 'GO_TO_WORKFLOWS' | 'GO_TO_RUNS' | 'DELETE_SINGLE_RECORD' | 'DELETE_MULTIPLE_RECORDS' | 'RESTORE_SINGLE_RECORD' | 'RESTORE_MULTIPLE_RECORDS' | 'DESTROY_SINGLE_RECORD' | 'DESTROY_MULTIPLE_RECORDS' | 'EXPORT_FROM_RECORD_INDEX' | 'EXPORT_FROM_RECORD_SHOW' | 'EXPORT_MULTIPLE_RECORDS'
|
||||
|
||||
export type CommandMenuItemAvailabilityType = 'GLOBAL' | 'GLOBAL_OBJECT_CONTEXT' | 'RECORD_SELECTION' | 'FALLBACK'
|
||||
|
||||
export type CommandMenuItemPayload = (PathCommandMenuItemPayload | ObjectMetadataCommandMenuItemPayload) & { __isUnion?: true }
|
||||
|
||||
export interface PathCommandMenuItemPayload {
|
||||
path: Scalars['String']
|
||||
__typename: 'PathCommandMenuItemPayload'
|
||||
}
|
||||
|
||||
export interface ObjectMetadataCommandMenuItemPayload {
|
||||
objectMetadataItemId: Scalars['UUID']
|
||||
__typename: 'ObjectMetadataCommandMenuItemPayload'
|
||||
}
|
||||
|
||||
export interface ToolIndexEntry {
|
||||
name: Scalars['String']
|
||||
description: Scalars['String']
|
||||
@@ -3145,6 +3147,49 @@ export interface FrontComponentGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface CommandMenuItemGenqlSelection{
|
||||
id?: boolean | number
|
||||
workflowVersionId?: boolean | number
|
||||
frontComponentId?: boolean | number
|
||||
frontComponent?: FrontComponentGenqlSelection
|
||||
engineComponentKey?: boolean | number
|
||||
label?: boolean | number
|
||||
icon?: boolean | number
|
||||
shortLabel?: boolean | number
|
||||
position?: boolean | number
|
||||
isPinned?: boolean | number
|
||||
availabilityType?: boolean | number
|
||||
payload?: CommandMenuItemPayloadGenqlSelection
|
||||
hotKeys?: boolean | number
|
||||
conditionalAvailabilityExpression?: boolean | number
|
||||
availabilityObjectMetadataId?: boolean | number
|
||||
pageLayoutId?: boolean | number
|
||||
universalIdentifier?: boolean | number
|
||||
applicationId?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface CommandMenuItemPayloadGenqlSelection{
|
||||
on_PathCommandMenuItemPayload?:PathCommandMenuItemPayloadGenqlSelection,
|
||||
on_ObjectMetadataCommandMenuItemPayload?:ObjectMetadataCommandMenuItemPayloadGenqlSelection,
|
||||
__typename?: boolean | number
|
||||
}
|
||||
|
||||
export interface PathCommandMenuItemPayloadGenqlSelection{
|
||||
path?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ObjectMetadataCommandMenuItemPayloadGenqlSelection{
|
||||
objectMetadataItemId?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface LogicFunctionGenqlSelection{
|
||||
id?: boolean | number
|
||||
name?: boolean | number
|
||||
@@ -3333,6 +3378,7 @@ export interface ApplicationGenqlSelection{
|
||||
defaultLogicFunctionRole?: RoleGenqlSelection
|
||||
agents?: AgentGenqlSelection
|
||||
frontComponents?: FrontComponentGenqlSelection
|
||||
commandMenuItems?: CommandMenuItemGenqlSelection
|
||||
logicFunctions?: LogicFunctionGenqlSelection
|
||||
objects?: ObjectGenqlSelection
|
||||
applicationVariables?: ApplicationVariableGenqlSelection
|
||||
@@ -5068,48 +5114,6 @@ export interface PostgresCredentialsGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface CommandMenuItemGenqlSelection{
|
||||
id?: boolean | number
|
||||
workflowVersionId?: boolean | number
|
||||
frontComponentId?: boolean | number
|
||||
frontComponent?: FrontComponentGenqlSelection
|
||||
engineComponentKey?: boolean | number
|
||||
label?: boolean | number
|
||||
icon?: boolean | number
|
||||
shortLabel?: boolean | number
|
||||
position?: boolean | number
|
||||
isPinned?: boolean | number
|
||||
availabilityType?: boolean | number
|
||||
payload?: CommandMenuItemPayloadGenqlSelection
|
||||
hotKeys?: boolean | number
|
||||
conditionalAvailabilityExpression?: boolean | number
|
||||
availabilityObjectMetadataId?: boolean | number
|
||||
pageLayoutId?: boolean | number
|
||||
applicationId?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface CommandMenuItemPayloadGenqlSelection{
|
||||
on_PathCommandMenuItemPayload?:PathCommandMenuItemPayloadGenqlSelection,
|
||||
on_ObjectMetadataCommandMenuItemPayload?:ObjectMetadataCommandMenuItemPayloadGenqlSelection,
|
||||
__typename?: boolean | number
|
||||
}
|
||||
|
||||
export interface PathCommandMenuItemPayloadGenqlSelection{
|
||||
path?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ObjectMetadataCommandMenuItemPayloadGenqlSelection{
|
||||
objectMetadataItemId?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ToolIndexEntryGenqlSelection{
|
||||
name?: boolean | number
|
||||
description?: boolean | number
|
||||
@@ -6441,6 +6445,38 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const CommandMenuItem_possibleTypes: string[] = ['CommandMenuItem']
|
||||
export const isCommandMenuItem = (obj?: { __typename?: any } | null): obj is CommandMenuItem => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isCommandMenuItem"')
|
||||
return CommandMenuItem_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const CommandMenuItemPayload_possibleTypes: string[] = ['PathCommandMenuItemPayload','ObjectMetadataCommandMenuItemPayload']
|
||||
export const isCommandMenuItemPayload = (obj?: { __typename?: any } | null): obj is CommandMenuItemPayload => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isCommandMenuItemPayload"')
|
||||
return CommandMenuItemPayload_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const PathCommandMenuItemPayload_possibleTypes: string[] = ['PathCommandMenuItemPayload']
|
||||
export const isPathCommandMenuItemPayload = (obj?: { __typename?: any } | null): obj is PathCommandMenuItemPayload => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isPathCommandMenuItemPayload"')
|
||||
return PathCommandMenuItemPayload_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ObjectMetadataCommandMenuItemPayload_possibleTypes: string[] = ['ObjectMetadataCommandMenuItemPayload']
|
||||
export const isObjectMetadataCommandMenuItemPayload = (obj?: { __typename?: any } | null): obj is ObjectMetadataCommandMenuItemPayload => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isObjectMetadataCommandMenuItemPayload"')
|
||||
return ObjectMetadataCommandMenuItemPayload_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const LogicFunction_possibleTypes: string[] = ['LogicFunction']
|
||||
export const isLogicFunction = (obj?: { __typename?: any } | null): obj is LogicFunction => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isLogicFunction"')
|
||||
@@ -7873,38 +7909,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const CommandMenuItem_possibleTypes: string[] = ['CommandMenuItem']
|
||||
export const isCommandMenuItem = (obj?: { __typename?: any } | null): obj is CommandMenuItem => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isCommandMenuItem"')
|
||||
return CommandMenuItem_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const CommandMenuItemPayload_possibleTypes: string[] = ['PathCommandMenuItemPayload','ObjectMetadataCommandMenuItemPayload']
|
||||
export const isCommandMenuItemPayload = (obj?: { __typename?: any } | null): obj is CommandMenuItemPayload => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isCommandMenuItemPayload"')
|
||||
return CommandMenuItemPayload_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const PathCommandMenuItemPayload_possibleTypes: string[] = ['PathCommandMenuItemPayload']
|
||||
export const isPathCommandMenuItemPayload = (obj?: { __typename?: any } | null): obj is PathCommandMenuItemPayload => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isPathCommandMenuItemPayload"')
|
||||
return PathCommandMenuItemPayload_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ObjectMetadataCommandMenuItemPayload_possibleTypes: string[] = ['ObjectMetadataCommandMenuItemPayload']
|
||||
export const isObjectMetadataCommandMenuItemPayload = (obj?: { __typename?: any } | null): obj is ObjectMetadataCommandMenuItemPayload => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isObjectMetadataCommandMenuItemPayload"')
|
||||
return ObjectMetadataCommandMenuItemPayload_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ToolIndexEntry_possibleTypes: string[] = ['ToolIndexEntry']
|
||||
export const isToolIndexEntry = (obj?: { __typename?: any } | null): obj is ToolIndexEntry => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isToolIndexEntry"')
|
||||
@@ -8304,6 +8308,82 @@ export const enumWorkspaceMemberNumberFormatEnum = {
|
||||
APOSTROPHE_AND_DOT: 'APOSTROPHE_AND_DOT' as const
|
||||
}
|
||||
|
||||
export const enumEngineComponentKey = {
|
||||
NAVIGATE_TO_NEXT_RECORD: 'NAVIGATE_TO_NEXT_RECORD' as const,
|
||||
NAVIGATE_TO_PREVIOUS_RECORD: 'NAVIGATE_TO_PREVIOUS_RECORD' as const,
|
||||
CREATE_NEW_RECORD: 'CREATE_NEW_RECORD' as const,
|
||||
DELETE_RECORDS: 'DELETE_RECORDS' as const,
|
||||
RESTORE_RECORDS: 'RESTORE_RECORDS' as const,
|
||||
DESTROY_RECORDS: 'DESTROY_RECORDS' as const,
|
||||
ADD_TO_FAVORITES: 'ADD_TO_FAVORITES' as const,
|
||||
REMOVE_FROM_FAVORITES: 'REMOVE_FROM_FAVORITES' as const,
|
||||
EXPORT_NOTE_TO_PDF: 'EXPORT_NOTE_TO_PDF' as const,
|
||||
EXPORT_RECORDS: 'EXPORT_RECORDS' as const,
|
||||
UPDATE_MULTIPLE_RECORDS: 'UPDATE_MULTIPLE_RECORDS' as const,
|
||||
MERGE_MULTIPLE_RECORDS: 'MERGE_MULTIPLE_RECORDS' as const,
|
||||
IMPORT_RECORDS: 'IMPORT_RECORDS' as const,
|
||||
EXPORT_VIEW: 'EXPORT_VIEW' as const,
|
||||
SEE_DELETED_RECORDS: 'SEE_DELETED_RECORDS' as const,
|
||||
CREATE_NEW_VIEW: 'CREATE_NEW_VIEW' as const,
|
||||
HIDE_DELETED_RECORDS: 'HIDE_DELETED_RECORDS' as const,
|
||||
EDIT_RECORD_PAGE_LAYOUT: 'EDIT_RECORD_PAGE_LAYOUT' as const,
|
||||
EDIT_DASHBOARD_LAYOUT: 'EDIT_DASHBOARD_LAYOUT' as const,
|
||||
SAVE_DASHBOARD_LAYOUT: 'SAVE_DASHBOARD_LAYOUT' as const,
|
||||
CANCEL_DASHBOARD_LAYOUT: 'CANCEL_DASHBOARD_LAYOUT' as const,
|
||||
DUPLICATE_DASHBOARD: 'DUPLICATE_DASHBOARD' as const,
|
||||
ACTIVATE_WORKFLOW: 'ACTIVATE_WORKFLOW' as const,
|
||||
DEACTIVATE_WORKFLOW: 'DEACTIVATE_WORKFLOW' as const,
|
||||
DISCARD_DRAFT_WORKFLOW: 'DISCARD_DRAFT_WORKFLOW' as const,
|
||||
TEST_WORKFLOW: 'TEST_WORKFLOW' as const,
|
||||
SEE_ACTIVE_VERSION_WORKFLOW: 'SEE_ACTIVE_VERSION_WORKFLOW' as const,
|
||||
SEE_RUNS_WORKFLOW: 'SEE_RUNS_WORKFLOW' as const,
|
||||
SEE_VERSIONS_WORKFLOW: 'SEE_VERSIONS_WORKFLOW' as const,
|
||||
ADD_NODE_WORKFLOW: 'ADD_NODE_WORKFLOW' as const,
|
||||
TIDY_UP_WORKFLOW: 'TIDY_UP_WORKFLOW' as const,
|
||||
DUPLICATE_WORKFLOW: 'DUPLICATE_WORKFLOW' as const,
|
||||
SEE_VERSION_WORKFLOW_RUN: 'SEE_VERSION_WORKFLOW_RUN' as const,
|
||||
SEE_WORKFLOW_WORKFLOW_RUN: 'SEE_WORKFLOW_WORKFLOW_RUN' as const,
|
||||
STOP_WORKFLOW_RUN: 'STOP_WORKFLOW_RUN' as const,
|
||||
SEE_RUNS_WORKFLOW_VERSION: 'SEE_RUNS_WORKFLOW_VERSION' as const,
|
||||
SEE_WORKFLOW_WORKFLOW_VERSION: 'SEE_WORKFLOW_WORKFLOW_VERSION' as const,
|
||||
USE_AS_DRAFT_WORKFLOW_VERSION: 'USE_AS_DRAFT_WORKFLOW_VERSION' as const,
|
||||
SEE_VERSIONS_WORKFLOW_VERSION: 'SEE_VERSIONS_WORKFLOW_VERSION' as const,
|
||||
SEARCH_RECORDS: 'SEARCH_RECORDS' as const,
|
||||
SEARCH_RECORDS_FALLBACK: 'SEARCH_RECORDS_FALLBACK' as const,
|
||||
ASK_AI: 'ASK_AI' as const,
|
||||
VIEW_PREVIOUS_AI_CHATS: 'VIEW_PREVIOUS_AI_CHATS' as const,
|
||||
NAVIGATION: 'NAVIGATION' as const,
|
||||
TRIGGER_WORKFLOW_VERSION: 'TRIGGER_WORKFLOW_VERSION' as const,
|
||||
FRONT_COMPONENT_RENDERER: 'FRONT_COMPONENT_RENDERER' as const,
|
||||
REPLY_TO_EMAIL_THREAD: 'REPLY_TO_EMAIL_THREAD' as const,
|
||||
COMPOSE_EMAIL: 'COMPOSE_EMAIL' as const,
|
||||
GO_TO_PEOPLE: 'GO_TO_PEOPLE' as const,
|
||||
GO_TO_COMPANIES: 'GO_TO_COMPANIES' as const,
|
||||
GO_TO_DASHBOARDS: 'GO_TO_DASHBOARDS' as const,
|
||||
GO_TO_OPPORTUNITIES: 'GO_TO_OPPORTUNITIES' as const,
|
||||
GO_TO_SETTINGS: 'GO_TO_SETTINGS' as const,
|
||||
GO_TO_TASKS: 'GO_TO_TASKS' as const,
|
||||
GO_TO_NOTES: 'GO_TO_NOTES' as const,
|
||||
GO_TO_WORKFLOWS: 'GO_TO_WORKFLOWS' as const,
|
||||
GO_TO_RUNS: 'GO_TO_RUNS' as const,
|
||||
DELETE_SINGLE_RECORD: 'DELETE_SINGLE_RECORD' as const,
|
||||
DELETE_MULTIPLE_RECORDS: 'DELETE_MULTIPLE_RECORDS' as const,
|
||||
RESTORE_SINGLE_RECORD: 'RESTORE_SINGLE_RECORD' as const,
|
||||
RESTORE_MULTIPLE_RECORDS: 'RESTORE_MULTIPLE_RECORDS' as const,
|
||||
DESTROY_SINGLE_RECORD: 'DESTROY_SINGLE_RECORD' as const,
|
||||
DESTROY_MULTIPLE_RECORDS: 'DESTROY_MULTIPLE_RECORDS' as const,
|
||||
EXPORT_FROM_RECORD_INDEX: 'EXPORT_FROM_RECORD_INDEX' as const,
|
||||
EXPORT_FROM_RECORD_SHOW: 'EXPORT_FROM_RECORD_SHOW' as const,
|
||||
EXPORT_MULTIPLE_RECORDS: 'EXPORT_MULTIPLE_RECORDS' as const
|
||||
}
|
||||
|
||||
export const enumCommandMenuItemAvailabilityType = {
|
||||
GLOBAL: 'GLOBAL' as const,
|
||||
GLOBAL_OBJECT_CONTEXT: 'GLOBAL_OBJECT_CONTEXT' as const,
|
||||
RECORD_SELECTION: 'RECORD_SELECTION' as const,
|
||||
FALLBACK: 'FALLBACK' as const
|
||||
}
|
||||
|
||||
export const enumFieldMetadataType = {
|
||||
ACTOR: 'ACTOR' as const,
|
||||
ADDRESS: 'ADDRESS' as const,
|
||||
@@ -8663,82 +8743,6 @@ export const enumEmailingDomainStatus = {
|
||||
TEMPORARY_FAILURE: 'TEMPORARY_FAILURE' as const
|
||||
}
|
||||
|
||||
export const enumEngineComponentKey = {
|
||||
NAVIGATE_TO_NEXT_RECORD: 'NAVIGATE_TO_NEXT_RECORD' as const,
|
||||
NAVIGATE_TO_PREVIOUS_RECORD: 'NAVIGATE_TO_PREVIOUS_RECORD' as const,
|
||||
CREATE_NEW_RECORD: 'CREATE_NEW_RECORD' as const,
|
||||
DELETE_RECORDS: 'DELETE_RECORDS' as const,
|
||||
RESTORE_RECORDS: 'RESTORE_RECORDS' as const,
|
||||
DESTROY_RECORDS: 'DESTROY_RECORDS' as const,
|
||||
ADD_TO_FAVORITES: 'ADD_TO_FAVORITES' as const,
|
||||
REMOVE_FROM_FAVORITES: 'REMOVE_FROM_FAVORITES' as const,
|
||||
EXPORT_NOTE_TO_PDF: 'EXPORT_NOTE_TO_PDF' as const,
|
||||
EXPORT_RECORDS: 'EXPORT_RECORDS' as const,
|
||||
UPDATE_MULTIPLE_RECORDS: 'UPDATE_MULTIPLE_RECORDS' as const,
|
||||
MERGE_MULTIPLE_RECORDS: 'MERGE_MULTIPLE_RECORDS' as const,
|
||||
IMPORT_RECORDS: 'IMPORT_RECORDS' as const,
|
||||
EXPORT_VIEW: 'EXPORT_VIEW' as const,
|
||||
SEE_DELETED_RECORDS: 'SEE_DELETED_RECORDS' as const,
|
||||
CREATE_NEW_VIEW: 'CREATE_NEW_VIEW' as const,
|
||||
HIDE_DELETED_RECORDS: 'HIDE_DELETED_RECORDS' as const,
|
||||
EDIT_RECORD_PAGE_LAYOUT: 'EDIT_RECORD_PAGE_LAYOUT' as const,
|
||||
EDIT_DASHBOARD_LAYOUT: 'EDIT_DASHBOARD_LAYOUT' as const,
|
||||
SAVE_DASHBOARD_LAYOUT: 'SAVE_DASHBOARD_LAYOUT' as const,
|
||||
CANCEL_DASHBOARD_LAYOUT: 'CANCEL_DASHBOARD_LAYOUT' as const,
|
||||
DUPLICATE_DASHBOARD: 'DUPLICATE_DASHBOARD' as const,
|
||||
ACTIVATE_WORKFLOW: 'ACTIVATE_WORKFLOW' as const,
|
||||
DEACTIVATE_WORKFLOW: 'DEACTIVATE_WORKFLOW' as const,
|
||||
DISCARD_DRAFT_WORKFLOW: 'DISCARD_DRAFT_WORKFLOW' as const,
|
||||
TEST_WORKFLOW: 'TEST_WORKFLOW' as const,
|
||||
SEE_ACTIVE_VERSION_WORKFLOW: 'SEE_ACTIVE_VERSION_WORKFLOW' as const,
|
||||
SEE_RUNS_WORKFLOW: 'SEE_RUNS_WORKFLOW' as const,
|
||||
SEE_VERSIONS_WORKFLOW: 'SEE_VERSIONS_WORKFLOW' as const,
|
||||
ADD_NODE_WORKFLOW: 'ADD_NODE_WORKFLOW' as const,
|
||||
TIDY_UP_WORKFLOW: 'TIDY_UP_WORKFLOW' as const,
|
||||
DUPLICATE_WORKFLOW: 'DUPLICATE_WORKFLOW' as const,
|
||||
SEE_VERSION_WORKFLOW_RUN: 'SEE_VERSION_WORKFLOW_RUN' as const,
|
||||
SEE_WORKFLOW_WORKFLOW_RUN: 'SEE_WORKFLOW_WORKFLOW_RUN' as const,
|
||||
STOP_WORKFLOW_RUN: 'STOP_WORKFLOW_RUN' as const,
|
||||
SEE_RUNS_WORKFLOW_VERSION: 'SEE_RUNS_WORKFLOW_VERSION' as const,
|
||||
SEE_WORKFLOW_WORKFLOW_VERSION: 'SEE_WORKFLOW_WORKFLOW_VERSION' as const,
|
||||
USE_AS_DRAFT_WORKFLOW_VERSION: 'USE_AS_DRAFT_WORKFLOW_VERSION' as const,
|
||||
SEE_VERSIONS_WORKFLOW_VERSION: 'SEE_VERSIONS_WORKFLOW_VERSION' as const,
|
||||
SEARCH_RECORDS: 'SEARCH_RECORDS' as const,
|
||||
SEARCH_RECORDS_FALLBACK: 'SEARCH_RECORDS_FALLBACK' as const,
|
||||
ASK_AI: 'ASK_AI' as const,
|
||||
VIEW_PREVIOUS_AI_CHATS: 'VIEW_PREVIOUS_AI_CHATS' as const,
|
||||
NAVIGATION: 'NAVIGATION' as const,
|
||||
TRIGGER_WORKFLOW_VERSION: 'TRIGGER_WORKFLOW_VERSION' as const,
|
||||
FRONT_COMPONENT_RENDERER: 'FRONT_COMPONENT_RENDERER' as const,
|
||||
REPLY_TO_EMAIL_THREAD: 'REPLY_TO_EMAIL_THREAD' as const,
|
||||
COMPOSE_EMAIL: 'COMPOSE_EMAIL' as const,
|
||||
GO_TO_PEOPLE: 'GO_TO_PEOPLE' as const,
|
||||
GO_TO_COMPANIES: 'GO_TO_COMPANIES' as const,
|
||||
GO_TO_DASHBOARDS: 'GO_TO_DASHBOARDS' as const,
|
||||
GO_TO_OPPORTUNITIES: 'GO_TO_OPPORTUNITIES' as const,
|
||||
GO_TO_SETTINGS: 'GO_TO_SETTINGS' as const,
|
||||
GO_TO_TASKS: 'GO_TO_TASKS' as const,
|
||||
GO_TO_NOTES: 'GO_TO_NOTES' as const,
|
||||
GO_TO_WORKFLOWS: 'GO_TO_WORKFLOWS' as const,
|
||||
GO_TO_RUNS: 'GO_TO_RUNS' as const,
|
||||
DELETE_SINGLE_RECORD: 'DELETE_SINGLE_RECORD' as const,
|
||||
DELETE_MULTIPLE_RECORDS: 'DELETE_MULTIPLE_RECORDS' as const,
|
||||
RESTORE_SINGLE_RECORD: 'RESTORE_SINGLE_RECORD' as const,
|
||||
RESTORE_MULTIPLE_RECORDS: 'RESTORE_MULTIPLE_RECORDS' as const,
|
||||
DESTROY_SINGLE_RECORD: 'DESTROY_SINGLE_RECORD' as const,
|
||||
DESTROY_MULTIPLE_RECORDS: 'DESTROY_MULTIPLE_RECORDS' as const,
|
||||
EXPORT_FROM_RECORD_INDEX: 'EXPORT_FROM_RECORD_INDEX' as const,
|
||||
EXPORT_FROM_RECORD_SHOW: 'EXPORT_FROM_RECORD_SHOW' as const,
|
||||
EXPORT_MULTIPLE_RECORDS: 'EXPORT_MULTIPLE_RECORDS' as const
|
||||
}
|
||||
|
||||
export const enumCommandMenuItemAvailabilityType = {
|
||||
GLOBAL: 'GLOBAL' as const,
|
||||
GLOBAL_OBJECT_CONTEXT: 'GLOBAL_OBJECT_CONTEXT' as const,
|
||||
RECORD_SELECTION: 'RECORD_SELECTION' as const,
|
||||
FALLBACK: 'FALLBACK' as const
|
||||
}
|
||||
|
||||
export const enumCalendarChannelSyncStatus = {
|
||||
NOT_SYNCED: 'NOT_SYNCED' as const,
|
||||
ONGOING: 'ONGOING' as const,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@ Front components can render in two locations within Twenty:
|
||||
|
||||
## Basic example
|
||||
|
||||
The quickest way to see a front component in action is to register it as a **command**. Adding a `command` field with `isPinned: true` makes it appear as a quick-action button in the top-right corner of the page — no page layout needed:
|
||||
The quickest way to see a front component in action is to register it with a **command menu item**. Use `defineCommandMenuItem` in a separate file to make the component appear as a quick-action button in the top-right corner of the page:
|
||||
|
||||
```tsx src/front-components/hello-world.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
@@ -34,14 +34,20 @@ export default defineFrontComponent({
|
||||
name: 'hello-world',
|
||||
description: 'A simple front component',
|
||||
component: HelloWorld,
|
||||
command: {
|
||||
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
|
||||
shortLabel: 'Hello',
|
||||
label: 'Hello World',
|
||||
icon: 'IconBolt',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/command-menu-items/hello-world.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
|
||||
shortLabel: 'Hello',
|
||||
label: 'Hello World',
|
||||
icon: 'IconBolt',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
});
|
||||
```
|
||||
|
||||
@@ -62,7 +68,6 @@ Click it to render the component inline.
|
||||
| `name` | No | Display name |
|
||||
| `description` | No | Description of what the component does |
|
||||
| `isHeadless` | No | Set to `true` if the component has no visible UI (see below) |
|
||||
| `command` | No | Register the component as a command (see [command options](#command-options) below) |
|
||||
|
||||
## Placing a front component on a page
|
||||
|
||||
@@ -141,11 +146,17 @@ export default defineFrontComponent({
|
||||
description: 'Creates a task from the command menu',
|
||||
component: RunAction,
|
||||
isHeadless: true,
|
||||
command: {
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/command-menu-items/run-action.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
});
|
||||
```
|
||||
|
||||
@@ -177,11 +188,6 @@ export default defineFrontComponent({
|
||||
description: 'Deletes a draft with confirmation',
|
||||
component: DeleteDraft,
|
||||
isHeadless: true,
|
||||
command: {
|
||||
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
|
||||
label: 'Delete draft',
|
||||
icon: 'IconTrash',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -342,14 +348,29 @@ export default defineFrontComponent({
|
||||
});
|
||||
```
|
||||
|
||||
## Command options
|
||||
## defineCommandMenuItem
|
||||
|
||||
Adding a `command` field to `defineFrontComponent` registers the component in the command menu (Cmd+K). If `isPinned` is `true`, it also appears as a quick-action button in the top-right corner of the page.
|
||||
Use `defineCommandMenuItem` to register a front component in the command menu (Cmd+K). If `isPinned` is `true`, it also appears as a quick-action button in the top-right corner of the page.
|
||||
|
||||
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
label: 'Open Dashboard',
|
||||
shortLabel: 'Dashboard',
|
||||
icon: 'IconLayoutDashboard',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
});
|
||||
```
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `universalIdentifier` | Yes | Stable unique ID for the command |
|
||||
| `label` | Yes | Full label shown in the command menu (Cmd+K) |
|
||||
| `frontComponentUniversalIdentifier` | Yes | The `universalIdentifier` of the front component this command opens |
|
||||
| `shortLabel` | No | Shorter label displayed on the pinned quick-action button |
|
||||
| `icon` | No | Icon name displayed next to the label (e.g. `'IconBolt'`, `'IconSend'`) |
|
||||
| `isPinned` | No | When `true`, shows the command as a quick-action button in the top-right corner of the page |
|
||||
@@ -361,30 +382,23 @@ Adding a `command` field to `defineFrontComponent` registers the component in th
|
||||
|
||||
The `conditionalAvailabilityExpression` field lets you control when a command is visible based on the current page context. Import typed variables and operators from `twenty-sdk` to build expressions:
|
||||
|
||||
```tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
```ts src/command-menu-items/bulk-update.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
import {
|
||||
pageType,
|
||||
numberOfSelectedRecords,
|
||||
objectPermissions,
|
||||
everyEquals,
|
||||
isDefined,
|
||||
} from 'twenty-sdk/front-component';
|
||||
|
||||
export default defineFrontComponent({
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: '...',
|
||||
name: 'bulk-action',
|
||||
component: BulkAction,
|
||||
command: {
|
||||
universalIdentifier: '...',
|
||||
label: 'Bulk Update',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
conditionalAvailabilityExpression: everyEquals(
|
||||
objectPermissions,
|
||||
'canUpdateObjectRecords',
|
||||
true,
|
||||
),
|
||||
},
|
||||
label: 'Bulk Update',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
frontComponentUniversalIdentifier: '...',
|
||||
conditionalAvailabilityExpression: everyEquals(
|
||||
objectPermissions,
|
||||
'canUpdateObjectRecords',
|
||||
true,
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -188,6 +188,14 @@ const SettingsApplicationFrontComponentDetail = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsApplicationCommandMenuItemDetail = lazy(() =>
|
||||
import(
|
||||
'~/pages/settings/applications/SettingsApplicationCommandMenuItemDetail'
|
||||
).then((module) => ({
|
||||
default: module.SettingsApplicationCommandMenuItemDetail,
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsLayoutViewDetail = lazy(() =>
|
||||
import('~/pages/settings/layout/SettingsLayoutViewDetail').then((module) => ({
|
||||
default: module.SettingsLayoutViewDetail,
|
||||
@@ -802,6 +810,10 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
|
||||
path={SettingsPath.ApplicationFrontComponentDetail}
|
||||
element={<SettingsApplicationFrontComponentDetail />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.ApplicationCommandMenuItemDetail}
|
||||
element={<SettingsApplicationCommandMenuItemDetail />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.ApplicationViewDetail}
|
||||
element={<SettingsLayoutViewDetail />}
|
||||
|
||||
+14
@@ -48,6 +48,20 @@ export const APPLICATION_FRAGMENT = gql`
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
commandMenuItems {
|
||||
id
|
||||
label
|
||||
shortLabel
|
||||
icon
|
||||
isPinned
|
||||
availabilityType
|
||||
conditionalAvailabilityExpression
|
||||
frontComponentId
|
||||
universalIdentifier
|
||||
applicationId
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
objects {
|
||||
...ObjectMetadataFields
|
||||
}
|
||||
|
||||
+1
@@ -5,6 +5,7 @@ export const CUSTOM_WORKSPACE_APPLICATION_MOCK = {
|
||||
agents: [],
|
||||
applicationVariables: [],
|
||||
frontComponents: [],
|
||||
commandMenuItems: [],
|
||||
availablePackages: {},
|
||||
canBeUninstalled: false,
|
||||
description: 'workpace custom application',
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import { type ApplicationContentRow } from '~/pages/settings/applications/compon
|
||||
|
||||
type InstalledApplicationForObjectAndFields = Omit<
|
||||
Application,
|
||||
'objects' | 'universalIdentifier' | 'frontComponents'
|
||||
'objects' | 'universalIdentifier' | 'frontComponents' | 'commandMenuItems'
|
||||
> & {
|
||||
objects: { id: string }[];
|
||||
};
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import { FindOneApplicationDocument } from '~/generated-metadata/graphql';
|
||||
import { SettingsApplicationCommandMenuItemSettingsTab } from '~/pages/settings/applications/tabs/SettingsApplicationCommandMenuItemSettingsTab';
|
||||
|
||||
export const SettingsApplicationCommandMenuItemDetail = () => {
|
||||
const { applicationId = '', commandMenuItemId = '' } = useParams<{
|
||||
applicationId: string;
|
||||
commandMenuItemId: string;
|
||||
}>();
|
||||
|
||||
const { data, loading } = useQuery(FindOneApplicationDocument, {
|
||||
variables: { id: applicationId },
|
||||
skip: !applicationId,
|
||||
});
|
||||
|
||||
const application = data?.findOneApplication;
|
||||
const commandMenuItem = application?.commandMenuItems?.find(
|
||||
(item) => item.id === commandMenuItemId,
|
||||
);
|
||||
|
||||
const frontComponent = commandMenuItem?.frontComponentId
|
||||
? application?.frontComponents?.find(
|
||||
(fc) => fc.id === commandMenuItem.frontComponentId,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const applicationContentHref = getSettingsPath(
|
||||
SettingsPath.ApplicationDetail,
|
||||
{ applicationId },
|
||||
undefined,
|
||||
'content',
|
||||
);
|
||||
const breadcrumbLinks = [
|
||||
{
|
||||
children: t`Workspace`,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: t`Applications`,
|
||||
href: getSettingsPath(SettingsPath.Applications),
|
||||
},
|
||||
{ children: application?.name ?? '', href: applicationContentHref },
|
||||
{ children: t`Command menu items`, href: applicationContentHref },
|
||||
{ children: commandMenuItem?.label ?? '' },
|
||||
];
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title={commandMenuItem?.label ?? t`Command menu item`}
|
||||
links={breadcrumbLinks}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
{loading || !isDefined(commandMenuItem) ? (
|
||||
<SettingsSectionSkeletonLoader />
|
||||
) : (
|
||||
<SettingsApplicationCommandMenuItemSettingsTab
|
||||
label={commandMenuItem.label}
|
||||
shortLabel={commandMenuItem.shortLabel}
|
||||
icon={commandMenuItem.icon}
|
||||
isPinned={commandMenuItem.isPinned}
|
||||
availabilityType={commandMenuItem.availabilityType}
|
||||
conditionalAvailabilityExpression={
|
||||
commandMenuItem.conditionalAvailabilityExpression
|
||||
}
|
||||
frontComponentName={frontComponent?.name}
|
||||
universalIdentifier={commandMenuItem.universalIdentifier}
|
||||
createdAt={commandMenuItem.createdAt}
|
||||
updatedAt={commandMenuItem.updatedAt}
|
||||
/>
|
||||
)}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
+8
-5
@@ -138,10 +138,9 @@ export const SettingsAvailableApplicationDetails = () => {
|
||||
icon: IconGraph,
|
||||
count: (manifest?.frontComponents ?? []).filter(
|
||||
(fc) =>
|
||||
!isDefined(fc.command) &&
|
||||
fc.universalIdentifier !==
|
||||
manifest?.application
|
||||
.settingsCustomTabFrontComponentUniversalIdentifier,
|
||||
!(manifest?.commandMenuItems ?? [])
|
||||
.map((cm) => cm.frontComponentUniversalIdentifier)
|
||||
.includes(fc.universalIdentifier),
|
||||
).length,
|
||||
one: t`widget`,
|
||||
many: t`widgets`,
|
||||
@@ -149,7 +148,11 @@ export const SettingsAvailableApplicationDetails = () => {
|
||||
{
|
||||
icon: IconCommand,
|
||||
count: (manifest?.frontComponents ?? []).filter(
|
||||
(fc) => isDefined(fc.command) && !fc.isHeadless,
|
||||
(fc) =>
|
||||
!fc.isHeadless &&
|
||||
(manifest?.commandMenuItems ?? [])
|
||||
.map((cm) => cm.frontComponentUniversalIdentifier)
|
||||
.includes(fc.universalIdentifier),
|
||||
).length,
|
||||
one: t`command`,
|
||||
many: t`commands`,
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { TableSection } from '@/ui/layout/table/components/TableSection';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type ReactNode } from 'react';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type SettingsApplicationCommandMenuItemSettingsTabProps = {
|
||||
label: string;
|
||||
shortLabel?: string | null;
|
||||
icon?: string | null;
|
||||
isPinned: boolean;
|
||||
availabilityType: string;
|
||||
conditionalAvailabilityExpression?: string | null;
|
||||
frontComponentName?: string | null;
|
||||
universalIdentifier?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
const StyledMonoText = styled.span`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-family: ${themeCssVariables.code.font.family}, monospace;
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const formatDateTime = (isoString: string): string => {
|
||||
const date = new Date(isoString);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return isoString;
|
||||
}
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
const GRID_TEMPLATE = '220px 1fr';
|
||||
|
||||
export const SettingsApplicationCommandMenuItemSettingsTab = ({
|
||||
label,
|
||||
shortLabel,
|
||||
icon,
|
||||
isPinned,
|
||||
availabilityType,
|
||||
conditionalAvailabilityExpression,
|
||||
frontComponentName,
|
||||
universalIdentifier,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
}: SettingsApplicationCommandMenuItemSettingsTabProps) => {
|
||||
const detailRows: { key: string; label: string; value: ReactNode }[] = [
|
||||
{
|
||||
key: 'label',
|
||||
label: t`Label`,
|
||||
value: label,
|
||||
},
|
||||
{
|
||||
key: 'shortLabel',
|
||||
label: t`Short label`,
|
||||
value: shortLabel ?? t`Not set`,
|
||||
},
|
||||
{
|
||||
key: 'icon',
|
||||
label: t`Icon`,
|
||||
value: icon ? <StyledMonoText>{icon}</StyledMonoText> : t`Not set`,
|
||||
},
|
||||
{
|
||||
key: 'isPinned',
|
||||
label: t`Pinned`,
|
||||
value: isPinned ? t`Yes` : t`No`,
|
||||
},
|
||||
{
|
||||
key: 'availabilityType',
|
||||
label: t`Availability`,
|
||||
value: <StyledMonoText>{availabilityType}</StyledMonoText>,
|
||||
},
|
||||
{
|
||||
key: 'conditionalAvailabilityExpression',
|
||||
label: t`Conditional availability`,
|
||||
value: conditionalAvailabilityExpression ? (
|
||||
<StyledMonoText>{conditionalAvailabilityExpression}</StyledMonoText>
|
||||
) : (
|
||||
t`Not set`
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'frontComponent',
|
||||
label: t`Front component`,
|
||||
value: frontComponentName ? (
|
||||
<StyledMonoText>{frontComponentName}</StyledMonoText>
|
||||
) : (
|
||||
t`Not set`
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'universalIdentifier',
|
||||
label: t`Universal identifier`,
|
||||
value: (
|
||||
<StyledMonoText>{universalIdentifier ?? t`Not set`}</StyledMonoText>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'createdAt',
|
||||
label: t`Created`,
|
||||
value: formatDateTime(createdAt),
|
||||
},
|
||||
{
|
||||
key: 'updatedAt',
|
||||
label: t`Updated`,
|
||||
value: formatDateTime(updatedAt),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Details`}
|
||||
description={t`Configuration of this command menu item`}
|
||||
/>
|
||||
<Table>
|
||||
<TableRow gridTemplateColumns={GRID_TEMPLATE}>
|
||||
<TableHeader>{t`Property`}</TableHeader>
|
||||
<TableHeader>{t`Value`}</TableHeader>
|
||||
</TableRow>
|
||||
<TableSection title={t`Command menu item`}>
|
||||
{detailRows.map((row) => (
|
||||
<TableRow key={row.key} gridTemplateColumns={GRID_TEMPLATE}>
|
||||
<TableCell color={themeCssVariables.font.color.secondary}>
|
||||
{row.label}
|
||||
</TableCell>
|
||||
<TableCell minWidth="0" overflow="hidden">
|
||||
{row.value}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableSection>
|
||||
</Table>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+33
-2
@@ -20,7 +20,7 @@ import { normalizeSearchText } from '~/utils/normalizeSearchText';
|
||||
|
||||
type InstalledApplicationForContentTab = Omit<
|
||||
Application,
|
||||
'objects' | 'frontComponents'
|
||||
'objects' | 'frontComponents' | 'commandMenuItems'
|
||||
> & {
|
||||
objects: { id: string }[];
|
||||
frontComponents?: {
|
||||
@@ -28,6 +28,11 @@ type InstalledApplicationForContentTab = Omit<
|
||||
name: string;
|
||||
description?: string | null;
|
||||
}[];
|
||||
commandMenuItems?: {
|
||||
id: string;
|
||||
label: string;
|
||||
shortLabel?: string | null;
|
||||
}[];
|
||||
};
|
||||
|
||||
type SettingsApplicationDetailContentTabProps = {
|
||||
@@ -124,6 +129,24 @@ export const SettingsApplicationDetailContentTab = ({
|
||||
secondary: fc.description ?? undefined,
|
||||
}));
|
||||
|
||||
const commandMenuItemRows: ApplicationContentRow[] = isDefined(
|
||||
installedApplication,
|
||||
)
|
||||
? (installedApplication.commandMenuItems ?? []).map((item) => ({
|
||||
key: item.id,
|
||||
name: item.label,
|
||||
secondary: item.shortLabel ?? undefined,
|
||||
link: getSettingsPath(SettingsPath.ApplicationCommandMenuItemDetail, {
|
||||
applicationId,
|
||||
commandMenuItemId: item.id,
|
||||
}),
|
||||
}))
|
||||
: (manifestContent?.commandMenuItems ?? []).map((item) => ({
|
||||
key: item.universalIdentifier,
|
||||
name: item.label,
|
||||
secondary: item.shortLabel ?? undefined,
|
||||
}));
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const normalizedSearch = normalizeSearchText(searchTerm);
|
||||
|
||||
@@ -134,6 +157,7 @@ export const SettingsApplicationDetailContentTab = ({
|
||||
views: filterRows(viewRows, normalizedSearch),
|
||||
navigation: filterRows(navigationMenuItemRows, normalizedSearch),
|
||||
frontComponents: filterRows(frontComponentRows, normalizedSearch),
|
||||
commandMenuItems: filterRows(commandMenuItemRows, normalizedSearch),
|
||||
logicFunctions: filterRows(logicFunctionRows, normalizedSearch),
|
||||
agents: filterRows(agentRows, normalizedSearch),
|
||||
skills: filterRows(skillRows, normalizedSearch),
|
||||
@@ -146,7 +170,8 @@ export const SettingsApplicationDetailContentTab = ({
|
||||
filtered.pageLayouts.length > 0 ||
|
||||
filtered.views.length > 0 ||
|
||||
filtered.navigation.length > 0 ||
|
||||
filtered.frontComponents.length > 0;
|
||||
filtered.frontComponents.length > 0 ||
|
||||
filtered.commandMenuItems.length > 0;
|
||||
const hasLogic =
|
||||
filtered.logicFunctions.length > 0 ||
|
||||
filtered.agents.length > 0 ||
|
||||
@@ -222,6 +247,12 @@ export const SettingsApplicationDetailContentTab = ({
|
||||
applicationId={applicationId}
|
||||
fallbackApplicationData={fallbackApplicationData}
|
||||
/>
|
||||
<SettingsApplicationContentSubtable
|
||||
title={t`Command menu items`}
|
||||
rows={filtered.commandMenuItems}
|
||||
applicationId={applicationId}
|
||||
fallbackApplicationData={fallbackApplicationData}
|
||||
/>
|
||||
</Table>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
+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',
|
||||
});
|
||||
`;
|
||||
};
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { defineCommandMenuItem } from '@/sdk/define';
|
||||
|
||||
const baseValidConfig = {
|
||||
universalIdentifier: '11111111-1111-4111-8111-111111111111',
|
||||
label: 'Open dashboard',
|
||||
frontComponentUniversalIdentifier: '22222222-2222-4222-8222-222222222222',
|
||||
};
|
||||
|
||||
describe('defineCommandMenuItem', () => {
|
||||
it('returns success for a valid config', () => {
|
||||
const result = defineCommandMenuItem(baseValidConfig);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('reports a missing universalIdentifier', () => {
|
||||
const result = defineCommandMenuItem({
|
||||
...baseValidConfig,
|
||||
universalIdentifier: '',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain(
|
||||
'CommandMenuItem must have a universalIdentifier',
|
||||
);
|
||||
});
|
||||
|
||||
it('reports a missing label', () => {
|
||||
const result = defineCommandMenuItem({
|
||||
...baseValidConfig,
|
||||
label: '',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain('CommandMenuItem must have a label');
|
||||
});
|
||||
|
||||
it('reports a missing frontComponentUniversalIdentifier', () => {
|
||||
const result = defineCommandMenuItem({
|
||||
...baseValidConfig,
|
||||
frontComponentUniversalIdentifier: '',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(
|
||||
result.errors.some((error) =>
|
||||
error.includes('frontComponentUniversalIdentifier'),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('passes through optional fields', () => {
|
||||
const result = defineCommandMenuItem({
|
||||
...baseValidConfig,
|
||||
icon: 'IconRocket',
|
||||
shortLabel: 'Open',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.config.icon).toBe('IconRocket');
|
||||
expect(result.config.isPinned).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { type CommandMenuItemManifest } from 'twenty-shared/application';
|
||||
|
||||
export type CommandMenuItemConfig = Omit<
|
||||
CommandMenuItemManifest,
|
||||
'conditionalAvailabilityExpression'
|
||||
> & {
|
||||
conditionalAvailabilityExpression?: boolean | string;
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { type CommandMenuItemConfig } from '@/sdk/define/command-menu-items/command-menu-item-config';
|
||||
import { type DefineEntity } from '@/sdk/define/common/types/define-entity.type';
|
||||
import { createValidationResult } from '@/sdk/define/common/utils/create-validation-result';
|
||||
|
||||
export const defineCommandMenuItem: DefineEntity<CommandMenuItemConfig> = (
|
||||
config,
|
||||
) => {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!config.universalIdentifier) {
|
||||
errors.push('CommandMenuItem must have a universalIdentifier');
|
||||
}
|
||||
|
||||
if (!config.label) {
|
||||
errors.push('CommandMenuItem must have a label');
|
||||
}
|
||||
|
||||
if (!config.frontComponentUniversalIdentifier) {
|
||||
errors.push(
|
||||
'CommandMenuItem must have a frontComponentUniversalIdentifier (the universalIdentifier of the front component this command opens)',
|
||||
);
|
||||
}
|
||||
|
||||
return createValidationResult({ config, errors });
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type ApplicationConfig } from '@/sdk/define/application/application-config';
|
||||
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 LogicFunctionConfig } from '@/sdk/define/logic-functions/logic-function-config';
|
||||
import { type ObjectConfig } from '@/sdk/define/objects/object-config';
|
||||
@@ -37,7 +38,8 @@ export type DefinableEntity =
|
||||
| ViewConfig
|
||||
| NavigationMenuItemManifest
|
||||
| PageLayoutConfig
|
||||
| PageLayoutTabConfig;
|
||||
| PageLayoutTabConfig
|
||||
| CommandMenuItemConfig;
|
||||
|
||||
export type DefineEntity<T extends DefinableEntity = DefinableEntity> = (
|
||||
config: T,
|
||||
|
||||
@@ -19,16 +19,6 @@ 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,17 +1,7 @@
|
||||
import {
|
||||
type FrontComponentCommandManifest,
|
||||
type FrontComponentManifest,
|
||||
} from 'twenty-shared/application';
|
||||
import { type FrontComponentManifest } from 'twenty-shared/application';
|
||||
|
||||
export type FrontComponentType = React.ComponentType<any>;
|
||||
|
||||
export type FrontComponentCommandConfig = Omit<
|
||||
FrontComponentCommandManifest,
|
||||
'conditionalAvailabilityExpression'
|
||||
> & {
|
||||
conditionalAvailabilityExpression?: boolean | string;
|
||||
};
|
||||
|
||||
export type FrontComponentConfig = Omit<
|
||||
FrontComponentManifest,
|
||||
| 'sourceComponentPath'
|
||||
@@ -19,8 +9,6 @@ export type FrontComponentConfig = Omit<
|
||||
| 'builtComponentChecksum'
|
||||
| 'componentName'
|
||||
| 'usesSdkClient'
|
||||
| 'command'
|
||||
> & {
|
||||
component: FrontComponentType;
|
||||
command?: FrontComponentCommandConfig;
|
||||
};
|
||||
|
||||
@@ -27,9 +27,12 @@ export { OnDeleteAction } from '@/sdk/define/fields/on-delete-action';
|
||||
export { RelationType } from '@/sdk/define/fields/relation-type';
|
||||
export { validateFields } from '@/sdk/define/fields/validate-fields';
|
||||
|
||||
export { defineCommandMenuItem } from '@/sdk/define/command-menu-items/define-command-menu-item';
|
||||
export type { CommandMenuItemConfig } from '@/sdk/define/command-menu-items/command-menu-item-config';
|
||||
export type { CommandMenuItemManifest } from 'twenty-shared/application';
|
||||
|
||||
export { defineFrontComponent } from '@/sdk/define/front-component/define-front-component';
|
||||
export type {
|
||||
FrontComponentCommandConfig,
|
||||
FrontComponentConfig,
|
||||
FrontComponentType,
|
||||
} from '@/sdk/define/front-component/front-component-config';
|
||||
|
||||
+1
@@ -79,6 +79,7 @@ export class ApplicationManifestMigrationService {
|
||||
navigationMenuItems: [],
|
||||
pageLayouts: [],
|
||||
pageLayoutTabs: [],
|
||||
commandMenuItems: [],
|
||||
};
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
+19
-17
@@ -158,23 +158,6 @@ export const computeApplicationManifestAllUniversalFlatEntityMaps = ({
|
||||
universalFlatEntityMapsToMutate:
|
||||
allUniversalFlatEntityMaps.flatFrontComponentMaps,
|
||||
});
|
||||
|
||||
if (frontComponentManifest.command) {
|
||||
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
|
||||
universalFlatEntity:
|
||||
fromCommandMenuItemManifestToUniversalFlatCommandMenuItem({
|
||||
commandMenuItemManifest: {
|
||||
...frontComponentManifest.command,
|
||||
frontComponentUniversalIdentifier:
|
||||
frontComponentManifest.universalIdentifier,
|
||||
},
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
}),
|
||||
universalFlatEntityMapsToMutate:
|
||||
allUniversalFlatEntityMaps.flatCommandMenuItemMaps,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const connectionProviderManifest of manifest.connectionProviders ?? []) {
|
||||
@@ -448,5 +431,24 @@ export const computeApplicationManifestAllUniversalFlatEntityMaps = ({
|
||||
}
|
||||
}
|
||||
|
||||
for (const commandMenuItemManifest of manifest.commandMenuItems ?? []) {
|
||||
if (!isDefined(commandMenuItemManifest.frontComponentUniversalIdentifier)) {
|
||||
throw new Error(
|
||||
`Top-level commandMenuItem "${commandMenuItemManifest.universalIdentifier}" is missing required frontComponentUniversalIdentifier`,
|
||||
);
|
||||
}
|
||||
|
||||
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
|
||||
universalFlatEntity:
|
||||
fromCommandMenuItemManifestToUniversalFlatCommandMenuItem({
|
||||
commandMenuItemManifest,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
}),
|
||||
universalFlatEntityMapsToMutate:
|
||||
allUniversalFlatEntityMaps.flatCommandMenuItemMaps,
|
||||
});
|
||||
}
|
||||
|
||||
return allUniversalFlatEntityMaps;
|
||||
};
|
||||
|
||||
+1
@@ -26,6 +26,7 @@ const buildMinimalManifest = (
|
||||
navigationMenuItems: [],
|
||||
pageLayouts: [],
|
||||
pageLayoutTabs: [],
|
||||
commandMenuItems: [],
|
||||
});
|
||||
|
||||
describe('resolveManifestAssetUrls', () => {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/appli
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
|
||||
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
|
||||
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
@@ -142,6 +143,15 @@ export class ApplicationEntity extends WorkspaceRelatedEntity {
|
||||
)
|
||||
frontComponents: Relation<FrontComponentEntity[]>;
|
||||
|
||||
@OneToMany(
|
||||
() => CommandMenuItemEntity,
|
||||
(commandMenuItem) => commandMenuItem.application,
|
||||
{
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
)
|
||||
commandMenuItems: Relation<CommandMenuItemEntity[]>;
|
||||
|
||||
@OneToMany(
|
||||
() => ApplicationVariableEntity,
|
||||
(applicationVariable) => applicationVariable.application,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
|
||||
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
|
||||
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
|
||||
@@ -23,6 +24,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
LogicFunctionEntity,
|
||||
AgentEntity,
|
||||
FrontComponentEntity,
|
||||
CommandMenuItemEntity,
|
||||
ObjectMetadataEntity,
|
||||
ApplicationVariableEntity,
|
||||
]),
|
||||
|
||||
@@ -17,6 +17,7 @@ import { type FlatApplication } from 'src/engine/core-modules/application/types/
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
|
||||
import { ALL_FLAT_ENTITY_MAPS_PROPERTIES } from 'src/engine/metadata-modules/flat-entity/constant/all-flat-entity-maps-properties.constant';
|
||||
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
|
||||
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
|
||||
@@ -40,6 +41,8 @@ export class ApplicationService {
|
||||
private readonly agentRepository: Repository<AgentEntity>,
|
||||
@InjectRepository(FrontComponentEntity)
|
||||
private readonly frontComponentRepository: Repository<FrontComponentEntity>,
|
||||
@InjectRepository(CommandMenuItemEntity)
|
||||
private readonly commandMenuItemRepository: Repository<CommandMenuItemEntity>,
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
@InjectRepository(ApplicationVariableEntity)
|
||||
@@ -187,6 +190,7 @@ export class ApplicationService {
|
||||
logicFunctions,
|
||||
agents,
|
||||
frontComponents,
|
||||
commandMenuItems,
|
||||
objects,
|
||||
applicationVariables,
|
||||
] = await Promise.all([
|
||||
@@ -199,6 +203,9 @@ export class ApplicationService {
|
||||
this.frontComponentRepository.find({
|
||||
where: { applicationId: application.id, workspaceId },
|
||||
}),
|
||||
this.commandMenuItemRepository.find({
|
||||
where: { applicationId: application.id, workspaceId },
|
||||
}),
|
||||
this.objectMetadataRepository.find({
|
||||
where: { applicationId: application.id, workspaceId },
|
||||
}),
|
||||
@@ -210,6 +217,7 @@ export class ApplicationService {
|
||||
application.logicFunctions = logicFunctions;
|
||||
application.agents = agents;
|
||||
application.frontComponents = frontComponents;
|
||||
application.commandMenuItems = commandMenuItems;
|
||||
application.objects = objects;
|
||||
application.applicationVariables = applicationVariables;
|
||||
|
||||
|
||||
+1
@@ -4,6 +4,7 @@ export const APPLICATION_ENTITY_RELATION_PROPERTIES = [
|
||||
'workspace',
|
||||
'agents',
|
||||
'frontComponents',
|
||||
'commandMenuItems',
|
||||
'logicFunctions',
|
||||
'objects',
|
||||
'applicationVariables',
|
||||
|
||||
@@ -13,6 +13,7 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/
|
||||
import { ApplicationRegistrationSummaryDTO } from 'src/engine/core-modules/application/application-registration/dtos/application-registration-summary.dto';
|
||||
import { ApplicationVariableEntityDTO } from 'src/engine/core-modules/application/application-variable/dtos/application-variable.dto';
|
||||
import { AgentDTO } from 'src/engine/metadata-modules/ai/ai-agent/dtos/agent.dto';
|
||||
import { CommandMenuItemDTO } from 'src/engine/metadata-modules/command-menu-item/dtos/command-menu-item.dto';
|
||||
import { FrontComponentDTO } from 'src/engine/metadata-modules/front-component/dtos/front-component.dto';
|
||||
import { LogicFunctionDTO } from 'src/engine/metadata-modules/logic-function/dtos/logic-function.dto';
|
||||
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
|
||||
@@ -100,6 +101,9 @@ export class ApplicationDTO {
|
||||
@Field(() => [FrontComponentDTO])
|
||||
frontComponents?: FrontComponentDTO[];
|
||||
|
||||
@Field(() => [CommandMenuItemDTO])
|
||||
commandMenuItems?: CommandMenuItemDTO[];
|
||||
|
||||
@Field(() => [LogicFunctionDTO])
|
||||
logicFunctions?: LogicFunctionDTO[];
|
||||
|
||||
|
||||
+3
@@ -99,6 +99,9 @@ export class CommandMenuItemDTO {
|
||||
@HideField()
|
||||
workspaceId: string;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
universalIdentifier?: string;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
applicationId?: string;
|
||||
|
||||
|
||||
+1
@@ -36,5 +36,6 @@ export const buildBaseManifest = ({
|
||||
navigationMenuItems: [],
|
||||
pageLayouts: [],
|
||||
pageLayoutTabs: [],
|
||||
commandMenuItems: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
@@ -11,4 +11,5 @@ export enum SyncableEntity {
|
||||
NavigationMenuItem = 'navigationMenuItem',
|
||||
PageLayout = 'pageLayout',
|
||||
PageLayoutTab = 'pageLayoutTab',
|
||||
CommandMenuItem = 'commandMenuItem',
|
||||
}
|
||||
|
||||
@@ -15,11 +15,6 @@ export type CommandMenuItemManifest = SyncableEntityOptions & {
|
||||
conditionalAvailabilityExpression?: string;
|
||||
};
|
||||
|
||||
export type FrontComponentCommandManifest = Omit<
|
||||
CommandMenuItemManifest,
|
||||
'frontComponentUniversalIdentifier'
|
||||
>;
|
||||
|
||||
export type FrontComponentManifest = {
|
||||
universalIdentifier: string;
|
||||
name?: string;
|
||||
@@ -30,5 +25,4 @@ export type FrontComponentManifest = {
|
||||
componentName: string;
|
||||
isHeadless?: boolean;
|
||||
usesSdkClient?: boolean;
|
||||
command?: FrontComponentCommandManifest;
|
||||
};
|
||||
|
||||
@@ -31,7 +31,6 @@ export type {
|
||||
} from './fieldManifestType';
|
||||
export type {
|
||||
CommandMenuItemManifest,
|
||||
FrontComponentCommandManifest,
|
||||
FrontComponentManifest,
|
||||
} from './frontComponentManifestType';
|
||||
export type {
|
||||
|
||||
@@ -3,7 +3,10 @@ import { type ApplicationManifest } from './applicationType';
|
||||
import { type AssetManifest } from './assetManifestType';
|
||||
import { type ConnectionProviderManifest } from './connectionProviderManifestType';
|
||||
import { type FieldManifest } from './fieldManifestType';
|
||||
import { type FrontComponentManifest } from './frontComponentManifestType';
|
||||
import {
|
||||
type CommandMenuItemManifest,
|
||||
type FrontComponentManifest,
|
||||
} from './frontComponentManifestType';
|
||||
import { type LogicFunctionManifest } from './logicFunctionManifestType';
|
||||
import { type NavigationMenuItemManifest } from './navigationMenuItemManifestType';
|
||||
import { type ObjectManifest } from './objectManifestType';
|
||||
@@ -30,4 +33,5 @@ export type Manifest = {
|
||||
navigationMenuItems: NavigationMenuItemManifest[];
|
||||
pageLayouts: PageLayoutManifest[];
|
||||
pageLayoutTabs: PageLayoutTabManifest[];
|
||||
commandMenuItems: CommandMenuItemManifest[];
|
||||
};
|
||||
|
||||
@@ -44,6 +44,7 @@ export enum SettingsPath {
|
||||
ApplicationDetail = 'applications/:applicationId',
|
||||
ApplicationLogicFunctionDetail = 'applications/:applicationId/logicFunctions/:logicFunctionId',
|
||||
ApplicationFrontComponentDetail = 'applications/:applicationId/frontComponents/:frontComponentId',
|
||||
ApplicationCommandMenuItemDetail = 'applications/:applicationId/commandMenuItems/:commandMenuItemId',
|
||||
ApplicationViewDetail = 'applications/:applicationId/views/:viewUniversalIdentifier',
|
||||
ApplicationPageLayoutDetail = 'applications/:applicationId/pageLayouts/:pageLayoutUniversalIdentifier',
|
||||
AvailableApplicationDetail = 'applications/available/:availableApplicationId',
|
||||
|
||||
Reference in New Issue
Block a user