## Add standard command menu items
### Summary
This PR introduces standard command menu items, migrating hardcoded
command menu actions to the backend command menu item architecture
powered by front components. It adds a new `twenty-standard-application`
package that defines, builds, and registers front components as standard
command menu items, gated behind the `IS_COMMAND_MENU_ITEM_ENABLED`
feature flag.
### Description
- **New `twenty-standard-application` package**: Contains front
component definitions with an esbuild-based build pipeline that
generates minified `.mjs` bundles and a manifest with checksums.
- **Server-side registration**: New constants register all items with
metadata (labels, icons, positions, availability types, conditional
expressions). A `StandardFrontComponentUploadService` uploads built
components to file storage.
- **`FALLBACK` availability type**: New enum value for command menu
items that appear as fallback options (e.g., "Search Records" fallback).
- **`CommandMenuContextApi` refactor**
- **Conditional availability enhancements**: New array-based helper
functions for evaluating multi-record conditions.
- **Frontend wiring** (twenty-front):
`useCommandMenuItemFrontComponentCommands`
## Next steps
Only simple commands have been implemented for now:
- **Navigation (9)** -- `CommandLink`: go-to-companies,
go-to-dashboards, go-to-notes, go-to-opportunities, go-to-people,
go-to-runs, go-to-settings, go-to-tasks, go-to-workflows
- **Side panel (4)** -- `CommandOpenSidePanelPage`: ask-ai,
search-records, search-records-fallback, view-previous-ai-chats
We still have to implement front components for all the following
commands:
All have placeholder `execute` logic (`async () => {}`) with a `// TODO:
implement execute logic` comment:
**Record (22)**
- `add-to-favorites`, `remove-from-favorites`
- `create-new-record`, `create-new-view`
- `delete-single-record`, `delete-multiple-records`
- `destroy-single-record`, `destroy-multiple-records`
- `restore-single-record`, `restore-multiple-records`
- `export-from-record-index`, `export-from-record-show`,
`export-multiple-records`, `export-note-to-pdf`, `export-view`
- `hide-deleted-records`, `see-deleted-records`
- `import-records`, `merge-multiple-records`, `update-multiple-records`
- `navigate-to-next-record`, `navigate-to-previous-record`
**Page layout (3)** -- `cancel-record-page-layout`,
`edit-record-page-layout`, `save-record-page-layout`
**Dashboard (4)** -- `cancel-dashboard-layout`, `duplicate-dashboard`,
`edit-dashboard-layout`, `save-dashboard-layout`
**Workflow (10)** -- `activate-workflow`, `add-node-workflow`,
`deactivate-workflow`, `discard-draft-workflow`, `duplicate-workflow`,
`see-active-version-workflow`, `see-runs-workflow`,
`see-versions-workflow`, `test-workflow`, `tidy-up-workflow`
**Workflow version (4)** -- `see-runs-workflow-version`,
`see-versions-workflow-version`, `see-workflow-workflow-version`,
`use-as-draft-workflow-version`
**Workflow run (3)** -- `see-version-workflow-run`,
`see-workflow-workflow-run`, `stop-workflow-run`
137 lines
3.2 KiB
TypeScript
137 lines
3.2 KiB
TypeScript
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
|
|
import { CoreApiClient } from 'twenty-sdk/clients';
|
|
import { type TelemetryEvent } from 'src/logic-functions/types/telemetry-event.type';
|
|
|
|
export const main = async (
|
|
params: RoutePayload<TelemetryEvent>,
|
|
): Promise<{
|
|
success: boolean;
|
|
message: string;
|
|
error?: string;
|
|
}> => {
|
|
try {
|
|
const {
|
|
action,
|
|
workspaceId,
|
|
userWorkspaceId,
|
|
userId,
|
|
userEmail,
|
|
userFirstName,
|
|
userLastName,
|
|
locale,
|
|
serverUrl,
|
|
serverId,
|
|
} = params.body || {};
|
|
|
|
if (action !== 'user_signup') {
|
|
return {
|
|
success: true,
|
|
message: `Event type '${action}' ignored`,
|
|
};
|
|
}
|
|
|
|
if (!userEmail) {
|
|
return {
|
|
success: true,
|
|
message: 'No email found in telemetry event',
|
|
};
|
|
}
|
|
|
|
if (
|
|
userEmail.toLowerCase().includes('example') ||
|
|
userEmail.toLowerCase().includes('test')
|
|
) {
|
|
return {
|
|
success: true,
|
|
message: `Email '${userEmail}' ignored (contains test/example data)`,
|
|
};
|
|
}
|
|
|
|
const client = new CoreApiClient();
|
|
|
|
let existingSelfHostingUserId: string | undefined = undefined;
|
|
try {
|
|
const { selfHostingUser: existingSelfHostingUser } = await client.query({
|
|
selfHostingUser: {
|
|
__args: {
|
|
filter: {
|
|
email: { primaryEmail: { eq: userEmail } },
|
|
},
|
|
},
|
|
id: true,
|
|
},
|
|
});
|
|
|
|
existingSelfHostingUserId = existingSelfHostingUser?.id;
|
|
} catch {
|
|
//
|
|
}
|
|
|
|
if (existingSelfHostingUserId) {
|
|
await client.mutation({
|
|
updateSelfHostingUser: {
|
|
__args: {
|
|
id: existingSelfHostingUserId,
|
|
data: {
|
|
name: { firstName: userFirstName, lastName: userLastName },
|
|
email: { primaryEmail: userEmail, additionalEmails: null },
|
|
userWorkspaceId,
|
|
userId,
|
|
locale,
|
|
serverUrl,
|
|
serverId,
|
|
},
|
|
},
|
|
id: true,
|
|
},
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
message: `Self hosting user ${existingSelfHostingUserId} updated`,
|
|
};
|
|
}
|
|
|
|
const { createSelfHostingUser } = await client.mutation({
|
|
createSelfHostingUser: {
|
|
__args: {
|
|
data: {
|
|
name: { firstName: userFirstName, lastName: userLastName },
|
|
email: { primaryEmail: userEmail, additionalEmails: null },
|
|
workspaceId,
|
|
userWorkspaceId,
|
|
userId,
|
|
locale,
|
|
serverUrl,
|
|
serverId,
|
|
},
|
|
},
|
|
id: true,
|
|
},
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
message: `Self hosting user ${createSelfHostingUser?.id} created`,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
message: 'Failed to process telemetry event',
|
|
error: error instanceof Error ? error.message : String(error),
|
|
};
|
|
}
|
|
};
|
|
|
|
export default defineLogicFunction({
|
|
universalIdentifier: '10104201-622b-4a5e-9f27-8f2af19b2a3c',
|
|
name: 'telemetry-webhook',
|
|
timeoutSeconds: 10,
|
|
handler: main,
|
|
httpRouteTriggerSettings: {
|
|
path: '/webhook/telemetry',
|
|
httpMethod: 'POST',
|
|
isAuthRequired: false,
|
|
},
|
|
});
|