Create view and navigation item on add-object command - to be tested (#18134)

<img width="580" height="141" alt="Capture d’écran 2026-02-20 à 15 52
11"
src="https://github.com/user-attachments/assets/40254d3a-f7f9-4f2a-a51f-c37dc1f5d167"
/>
This commit is contained in:
Thomas Trompette
2026-02-20 17:41:16 +01:00
committed by GitHub
parent 26fabdc3d0
commit 932adf8e4b
5 changed files with 120 additions and 9 deletions
+2
View File
@@ -71,6 +71,7 @@
"inquirer": "^10.0.0",
"jsonc-parser": "^3.2.0",
"lodash.camelcase": "^4.3.0",
"lodash.kebabcase": "^4.1.1",
"preact": "^10.28.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -89,6 +90,7 @@
"@types/fs-extra": "^11.0.0",
"@types/inquirer": "^9.0.0",
"@types/lodash.camelcase": "^4.3.7",
"@types/lodash.kebabcase": "^4.1.9",
"@types/node": "^24.0.0",
"@types/react": "18.2.66",
"@types/react-dom": "18.2.22",
@@ -17,10 +17,13 @@ import { SyncableEntity } from 'twenty-shared/application';
import { FieldMetadataType } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
import { getFieldBaseFile } from '@/cli/utilities/entity/entity-field-template';
import { v4 } from 'uuid';
const APP_FOLDER = 'src';
export class EntityAddCommand {
private lastObjectUniversalIdentifier: string | undefined;
async execute(entityType?: SyncableEntity, path?: string): Promise<void> {
try {
const entity = entityType ?? (await this.getEntity());
@@ -50,6 +53,10 @@ export class EntityAddCommand {
chalk.green(`✓ Created ${entityName}:`),
chalk.cyan(relative(CURRENT_EXECUTION_DIRECTORY, filePath)),
);
if (entity === SyncableEntity.Object) {
await this.promptAndCreateViewAndNavigationMenuItem(name, path);
}
} catch (error) {
console.error(
chalk.red(`Add new entity failed:`),
@@ -65,10 +72,14 @@ export class EntityAddCommand {
const entityData = await this.getObjectData();
const name = entityData.nameSingular;
const objectUniversalIdentifier = v4();
this.lastObjectUniversalIdentifier = objectUniversalIdentifier;
const file = getObjectBaseFile({
data: entityData,
name,
universalIdentifier: objectUniversalIdentifier,
});
return { name, file };
@@ -163,6 +174,96 @@ export class EntityAddCommand {
}
}
private async promptAndCreateViewAndNavigationMenuItem(
objectName: string,
customPath?: string,
): Promise<void> {
const { createViewAndNavItem } = await inquirer.prompt<{
createViewAndNavItem: boolean;
}>([
{
type: 'confirm',
name: 'createViewAndNavItem',
message:
'Also create a view and navigation menu item for this object? (recommended)',
default: true,
},
]);
if (!createViewAndNavItem || !this.lastObjectUniversalIdentifier) {
return;
}
const viewUniversalIdentifier = v4();
const viewFile = getViewBaseFile({
name: `all-${kebabcase(objectName)}`,
universalIdentifier: viewUniversalIdentifier,
objectUniversalIdentifier: this.lastObjectUniversalIdentifier,
});
const viewFolderPath = customPath
? join(CURRENT_EXECUTION_DIRECTORY, customPath)
: join(
CURRENT_EXECUTION_DIRECTORY,
APP_FOLDER,
this.getFolderName(SyncableEntity.View),
);
await fs.ensureDir(viewFolderPath);
const viewFileName = `all-${kebabcase(objectName)}.ts`;
const viewFilePath = join(viewFolderPath, viewFileName);
if (await fs.pathExists(viewFilePath)) {
const { overwrite } = await this.handleFileExist();
if (!overwrite) {
return;
}
}
await fs.writeFile(viewFilePath, viewFile);
console.log(
chalk.green(`✓ Created view:`),
chalk.cyan(relative(CURRENT_EXECUTION_DIRECTORY, viewFilePath)),
);
const navFile = getNavigationMenuItemBaseFile({
name: objectName,
viewUniversalIdentifier,
});
const navFolderPath = customPath
? join(CURRENT_EXECUTION_DIRECTORY, customPath)
: join(
CURRENT_EXECUTION_DIRECTORY,
APP_FOLDER,
this.getFolderName(SyncableEntity.NavigationMenuItem),
);
await fs.ensureDir(navFolderPath);
const navFileName = `${kebabcase(objectName)}.ts`;
const navFilePath = join(navFolderPath, navFileName);
if (await fs.pathExists(navFilePath)) {
const { overwrite } = await this.handleFileExist();
if (!overwrite) {
return;
}
}
await fs.writeFile(navFilePath, navFile);
console.log(
chalk.green(`✓ Created navigation menu item:`),
chalk.cyan(relative(CURRENT_EXECUTION_DIRECTORY, navFilePath)),
);
}
private async getEntity() {
const { entity } = await inquirer.prompt<{ entity: SyncableEntity }>([
{
@@ -4,12 +4,23 @@ import { v4 } from 'uuid';
export const getNavigationMenuItemBaseFile = ({
name,
universalIdentifier = v4(),
viewUniversalIdentifier,
}: {
name: string;
universalIdentifier?: string;
viewUniversalIdentifier?: string;
}) => {
const kebabCaseName = kebabCase(name);
const linkConfig = viewUniversalIdentifier
? ` viewUniversalIdentifier: '${viewUniversalIdentifier}',`
: ` // Link to a view:
// viewUniversalIdentifier: '...',
// Or link to an object:
// targetObjectUniversalIdentifier: '...',
// Or link to an external URL:
// link: 'https://example.com',`;
return `import { defineNavigationMenuItem } from 'twenty-sdk';
export default defineNavigationMenuItem({
@@ -17,12 +28,7 @@ export default defineNavigationMenuItem({
name: '${kebabCaseName}',
icon: 'IconList',
position: 0,
// Link to a view:
// viewUniversalIdentifier: '...',
// Or link to an object:
// targetObjectUniversalIdentifier: '...',
// Or link to an external URL:
// link: 'https://example.com',
${linkConfig}
});
`;
};
@@ -2,6 +2,7 @@ import { v4 } from 'uuid';
export const getObjectBaseFile = ({
data,
universalIdentifier = v4(),
}: {
data: {
nameSingular: string;
@@ -10,9 +11,8 @@ export const getObjectBaseFile = ({
labelPlural: string;
};
name: string;
universalIdentifier?: string;
}) => {
const universalIdentifier = v4();
return `import { defineObject } from 'twenty-sdk';
export default defineObject({
@@ -4,9 +4,11 @@ import { v4 } from 'uuid';
export const getViewBaseFile = ({
name,
universalIdentifier = v4(),
objectUniversalIdentifier = 'fill-later',
}: {
name: string;
universalIdentifier?: string;
objectUniversalIdentifier?: string;
}) => {
const kebabCaseName = kebabCase(name);
@@ -15,7 +17,7 @@ export const getViewBaseFile = ({
export default defineView({
universalIdentifier: '${universalIdentifier}',
name: '${kebabCaseName}',
objectUniversalIdentifier: 'fill-later',
objectUniversalIdentifier: '${objectUniversalIdentifier}',
icon: 'IconList',
position: 0,
// fields: [