2de022afcf
## 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`
97 lines
2.8 KiB
TypeScript
97 lines
2.8 KiB
TypeScript
import crypto from 'crypto';
|
|
import esbuild from 'esbuild';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { glob } from 'tinyglobby';
|
|
import { getBaseFrontComponentBuildOptions } from 'twenty-sdk/build';
|
|
import { kebabToCamelCase } from 'twenty-shared/utils';
|
|
|
|
const FRONT_COMPONENTS_DIR = path.resolve(__dirname, '../src/front-components');
|
|
|
|
const BUILT_OUTPUT_DIR = path.resolve(__dirname, '../src/build');
|
|
|
|
const MANIFEST_OUTPUT_PATH = path.resolve(
|
|
__dirname,
|
|
'../src/standard-front-component-build-manifest.ts',
|
|
);
|
|
|
|
const buildStandardFrontComponents = async () => {
|
|
const tsxFiles = (
|
|
await glob(['**/*.front-component.tsx'], {
|
|
cwd: FRONT_COMPONENTS_DIR,
|
|
absolute: true,
|
|
onlyFiles: true,
|
|
})
|
|
).sort();
|
|
|
|
if (tsxFiles.length === 0) {
|
|
throw new Error(
|
|
`No .front-component.tsx files found in ${FRONT_COMPONENTS_DIR}`,
|
|
);
|
|
}
|
|
|
|
fs.mkdirSync(BUILT_OUTPUT_DIR, { recursive: true });
|
|
|
|
await esbuild.build({
|
|
...getBaseFrontComponentBuildOptions(),
|
|
entryPoints: tsxFiles,
|
|
outdir: BUILT_OUTPUT_DIR,
|
|
outbase: FRONT_COMPONENTS_DIR,
|
|
minify: true,
|
|
});
|
|
|
|
const manifestEntries: Record<
|
|
string,
|
|
{ builtComponentPath: string; builtComponentChecksum: string }
|
|
> = {};
|
|
|
|
for (const tsxFile of tsxFiles) {
|
|
const relativeTsx = path.relative(FRONT_COMPONENTS_DIR, tsxFile);
|
|
const relativeMjs = relativeTsx.replace(
|
|
'.front-component.tsx',
|
|
'.front-component.mjs',
|
|
);
|
|
const mjsFilePath = path.join(BUILT_OUTPUT_DIR, relativeMjs);
|
|
|
|
if (!fs.existsSync(mjsFilePath)) {
|
|
throw new Error(`Expected built file not found: ${mjsFilePath}`);
|
|
}
|
|
|
|
const content = fs.readFileSync(mjsFilePath);
|
|
const checksum = crypto.createHash('md5').update(content).digest('hex');
|
|
|
|
const stem = path.basename(tsxFile, '.front-component.tsx');
|
|
const camelKey = kebabToCamelCase(stem);
|
|
|
|
manifestEntries[camelKey] = {
|
|
builtComponentPath: relativeMjs,
|
|
builtComponentChecksum: checksum,
|
|
};
|
|
}
|
|
|
|
const manifestContent = `/*
|
|
* _____ _
|
|
*|_ _|_ _____ _ __ | |_ _ _
|
|
* | | \\ \\ /\\ / / _ \\ '_ \\| __| | | | Auto-generated file
|
|
* | | \\ V V / __/ | | | |_| |_| | Any edits to this will be overridden
|
|
* |_| \\_/\\_/ \\___|_| |_|\\__|\\__, |
|
|
* |___/
|
|
*/
|
|
|
|
export const STANDARD_FRONT_COMPONENT_BUILD_MANIFEST = ${JSON.stringify(manifestEntries, null, 2)} as const;
|
|
`;
|
|
|
|
fs.writeFileSync(MANIFEST_OUTPUT_PATH, manifestContent);
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
`Built ${tsxFiles.length} standard front components, manifest written to ${MANIFEST_OUTPUT_PATH}`,
|
|
);
|
|
};
|
|
|
|
buildStandardFrontComponents().catch((error) => {
|
|
// eslint-disable-next-line no-console
|
|
console.error('Failed to build standard front components:', error);
|
|
process.exit(1);
|
|
});
|