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:
@@ -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 }>([
|
||||
{
|
||||
|
||||
+12
-6
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user