Add example in create-twenty-app (#18043)

- add interactive mode to create-twenty-app
- by default create an example for each entities

<img width="1181" height="168" alt="image"
src="https://github.com/user-attachments/assets/a2490d8f-66a1-4cd5-bf41-57166cc20a1e"
/>
This commit is contained in:
martmull
2026-02-18 19:04:19 +01:00
committed by GitHub
parent 9733ff1b8e
commit 2cc3c75c7e
15 changed files with 934 additions and 38 deletions
@@ -1,10 +1,12 @@
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import { getFrontComponentBaseFile } from '@/cli/utilities/entity/entity-front-component-template';
import { getLogicFunctionBaseFile } from '@/cli/utilities/entity/entity-logic-function-template';
import { getNavigationMenuItemBaseFile } from '@/cli/utilities/entity/entity-navigation-menu-item-template';
import { convertToLabel } from '@/cli/utilities/entity/entity-label';
import { getObjectBaseFile } from '@/cli/utilities/entity/entity-object-template';
import { getPageLayoutBaseFile } from '@/cli/utilities/entity/entity-page-layout-template';
import { getRoleBaseFile } from '@/cli/utilities/entity/entity-role-template';
import { getViewBaseFile } from '@/cli/utilities/entity/entity-view-template';
import chalk from 'chalk';
import * as fs from 'fs-extra';
import inquirer from 'inquirer';
@@ -114,13 +116,34 @@ export class EntityAddCommand {
return { name, file };
}
case SyncableEntity.View: {
const entityData = await this.getViewData();
const name = entityData.name;
const file = getViewBaseFile({
name,
});
return { name, file };
}
case SyncableEntity.NavigationMenuItem: {
const name = await this.getEntityName(entity);
const file = getNavigationMenuItemBaseFile({
name,
});
return { name, file };
}
case SyncableEntity.PageLayout: {
const name = await this.getEntityName(entity);
const file = getPageLayoutBaseFile({
name,
});
return { name, file };
}
@@ -302,6 +325,39 @@ export class EntityAddCommand {
]);
}
private async getViewData() {
return inquirer.prompt<{
name: string;
objectUniversalIdentifier: string;
}>([
{
type: 'input',
name: 'name',
message: 'Enter a name for your view:',
default: '',
validate: (input: string) => {
if (!input || input.trim().length === 0) {
return 'Please enter a non empty string';
}
return true;
},
},
{
type: 'input',
name: 'objectUniversalIdentifier',
message:
'Enter the universalIdentifier of the object this view belongs to:',
default: 'fill-later',
validate: (input: string) => {
if (!input || input.trim().length === 0) {
return 'Please enter a non empty string';
}
return true;
},
},
]);
}
getFolderName(entity: SyncableEntity) {
return `${kebabcase(entity)}s`;
}
@@ -69,6 +69,8 @@ const ENTITY_TYPE_TO_SYNCABLE: Record<string, SyncableEntity | undefined> = {
logicFunctions: SyncableEntity.LogicFunction,
frontComponents: SyncableEntity.FrontComponent,
roles: SyncableEntity.Role,
views: SyncableEntity.View,
navigationMenuItems: SyncableEntity.NavigationMenuItem,
pageLayouts: SyncableEntity.PageLayout,
};
@@ -97,6 +97,8 @@ export const ENTITY_LABELS: Record<SyncableEntity, string> = {
[SyncableEntity.LogicFunction]: 'Logic functions',
[SyncableEntity.FrontComponent]: 'Front components',
[SyncableEntity.Role]: 'Roles',
[SyncableEntity.View]: 'Views',
[SyncableEntity.NavigationMenuItem]: 'Navigation menu items',
[SyncableEntity.PageLayout]: 'Page layouts',
};
@@ -0,0 +1,28 @@
import kebabCase from 'lodash.kebabcase';
import { v4 } from 'uuid';
export const getNavigationMenuItemBaseFile = ({
name,
universalIdentifier = v4(),
}: {
name: string;
universalIdentifier?: string;
}) => {
const kebabCaseName = kebabCase(name);
return `import { defineNavigationMenuItem } from 'twenty-sdk';
export default defineNavigationMenuItem({
universalIdentifier: '${universalIdentifier}',
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',
});
`;
};
@@ -0,0 +1,39 @@
import kebabCase from 'lodash.kebabcase';
import { v4 } from 'uuid';
export const getViewBaseFile = ({
name,
universalIdentifier = v4(),
}: {
name: string;
universalIdentifier?: string;
}) => {
const kebabCaseName = kebabCase(name);
return `import { defineView } from 'twenty-sdk';
export default defineView({
universalIdentifier: '${universalIdentifier}',
name: '${kebabCaseName}',
objectUniversalIdentifier: 'fill-later',
icon: 'IconList',
position: 0,
// fields: [
// {
// universalIdentifier: '...',
// fieldMetadataUniversalIdentifier: '...',
// position: 0,
// isVisible: true,
// },
// ],
// filters: [
// {
// universalIdentifier: '...',
// fieldMetadataUniversalIdentifier: '...',
// operand: 'Contains',
// value: '',
// },
// ],
});
`;
};