diff --git a/packages/twenty-sdk/package.json b/packages/twenty-sdk/package.json index d2eecfb278..baded9cc05 100644 --- a/packages/twenty-sdk/package.json +++ b/packages/twenty-sdk/package.json @@ -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", diff --git a/packages/twenty-sdk/src/cli/commands/entity/entity-add.ts b/packages/twenty-sdk/src/cli/commands/entity/entity-add.ts index 2b9bc8aab5..951a6b44fe 100644 --- a/packages/twenty-sdk/src/cli/commands/entity/entity-add.ts +++ b/packages/twenty-sdk/src/cli/commands/entity/entity-add.ts @@ -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 { 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 { + 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 }>([ { diff --git a/packages/twenty-sdk/src/cli/utilities/entity/entity-navigation-menu-item-template.ts b/packages/twenty-sdk/src/cli/utilities/entity/entity-navigation-menu-item-template.ts index 1ab1c9653b..077d82607f 100644 --- a/packages/twenty-sdk/src/cli/utilities/entity/entity-navigation-menu-item-template.ts +++ b/packages/twenty-sdk/src/cli/utilities/entity/entity-navigation-menu-item-template.ts @@ -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} }); `; }; diff --git a/packages/twenty-sdk/src/cli/utilities/entity/entity-object-template.ts b/packages/twenty-sdk/src/cli/utilities/entity/entity-object-template.ts index 73683db925..1d220c142e 100644 --- a/packages/twenty-sdk/src/cli/utilities/entity/entity-object-template.ts +++ b/packages/twenty-sdk/src/cli/utilities/entity/entity-object-template.ts @@ -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({ diff --git a/packages/twenty-sdk/src/cli/utilities/entity/entity-view-template.ts b/packages/twenty-sdk/src/cli/utilities/entity/entity-view-template.ts index 7667d74ef3..9f3ffbbb74 100644 --- a/packages/twenty-sdk/src/cli/utilities/entity/entity-view-template.ts +++ b/packages/twenty-sdk/src/cli/utilities/entity/entity-view-template.ts @@ -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: [