From b53dfa0533ad9bcc9b8b593d907b6a543e27f51a Mon Sep 17 00:00:00 2001 From: martmull Date: Tue, 3 Feb 2026 18:16:54 +0100 Subject: [PATCH] Publish twenty packages (#17676) - removes code editor in settings - update readmes and docs --- packages/create-twenty-app/README.md | 12 +- .../src/utils/__tests__/app-template.spec.ts | 46 ++--- .../src/utils/app-template.ts | 47 +++-- .../hello-world/src/application.config.ts | 2 +- .../developers/extend/capabilities/apps.mdx | 190 +++++++++--------- .../SettingsLogicFunctionDetail.tsx | 74 +------ .../SettingsLogicFunctionDetail.stories.tsx | 2 +- packages/twenty-sdk/README.md | 2 +- .../marketplace/mocks/mock-manifest.json | 2 +- 9 files changed, 171 insertions(+), 206 deletions(-) diff --git a/packages/create-twenty-app/README.md b/packages/create-twenty-app/README.md index 83f09829d0..afd254dff7 100644 --- a/packages/create-twenty-app/README.md +++ b/packages/create-twenty-app/README.md @@ -61,15 +61,19 @@ yarn app:uninstall ``` ## What gets scaffolded -- A minimal app structure ready for Twenty +- A minimal app structure ready for Twenty with example files: + - `application-config.ts` - Application metadata configuration + - `roles/default-role.ts` - Default role for logic functions + - `logic-functions/hello-world.ts` - Example logic function with HTTP trigger + - `front-components/hello-world.tsx` - Example front component - TypeScript configuration - Prewired scripts that wrap the `twenty` CLI from twenty-sdk -- Example placeholders to help you add entities, actions, and sync logic ## Next steps -- Explore the generated project and add your first entity with `yarn entity:add` (functions, front components, objects, roles). -- Keep your types up‑to‑date using `yarn app:generate`. +- Use `yarn auth:login` to authenticate with your Twenty workspace. +- Explore the generated project and add your first entity with `yarn entity:add` (logic functions, front components, objects, roles). - Use `yarn app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time. +- Keep your types up‑to‑date using `yarn app:generate`. ## Publish your application diff --git a/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts b/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts index 636eab9d19..3c93f72b99 100644 --- a/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts +++ b/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts @@ -12,6 +12,9 @@ jest.mock('fs-extra', () => { }; }); +const APPLICATION_FILE_NAME = 'application-config.ts'; +const DEFAULT_ROLE_FILE_NAME = 'default-role.ts'; + describe('copyBaseApplicationProject', () => { let testAppDirectory: string; @@ -40,16 +43,16 @@ describe('copyBaseApplicationProject', () => { appDirectory: testAppDirectory, }); - // Verify src/app/ folder exists + // Verify src/ folder exists const srcAppPath = join(testAppDirectory, 'src'); expect(await fs.pathExists(srcAppPath)).toBe(true); - // Verify application.config.ts exists in src/app/ - const appConfigPath = join(srcAppPath, 'application.config.ts'); + // Verify application-config.ts exists in src/ + const appConfigPath = join(srcAppPath, APPLICATION_FILE_NAME); expect(await fs.pathExists(appConfigPath)).toBe(true); - // Verify default.role.ts exists in src/app/ - const roleConfigPath = join(srcAppPath, 'default.role.ts'); + // Verify default-role.ts exists in src/ + const roleConfigPath = join(srcAppPath, 'roles', DEFAULT_ROLE_FILE_NAME); expect(await fs.pathExists(roleConfigPath)).toBe(true); }); @@ -102,7 +105,7 @@ describe('copyBaseApplicationProject', () => { expect(yarnLockContent).toContain('yarn lockfile v1'); }); - it('should create application.config.ts with defineApplication and correct values', async () => { + it('should create application-config.ts with defineApplication and correct values', async () => { await copyBaseApplicationProject({ appName: 'my-test-app', appDisplayName: 'My Test App', @@ -110,11 +113,7 @@ describe('copyBaseApplicationProject', () => { appDirectory: testAppDirectory, }); - const appConfigPath = join( - testAppDirectory, - 'src', - 'application.config.ts', - ); + const appConfigPath = join(testAppDirectory, 'src', APPLICATION_FILE_NAME); const appConfigContent = await fs.readFile(appConfigPath, 'utf8'); // Verify it uses defineApplication @@ -125,7 +124,7 @@ describe('copyBaseApplicationProject', () => { // Verify it imports the role identifier expect(appConfigContent).toContain( - "import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/default.role'", + "import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'", ); // Verify display name and description @@ -143,7 +142,7 @@ describe('copyBaseApplicationProject', () => { ); }); - it('should create default.role.ts with defineRole and correct values', async () => { + it('should create default-role.ts with defineRole and correct values', async () => { await copyBaseApplicationProject({ appName: 'my-test-app', appDisplayName: 'My Test App', @@ -151,7 +150,12 @@ describe('copyBaseApplicationProject', () => { appDirectory: testAppDirectory, }); - const roleConfigPath = join(testAppDirectory, 'src', 'default.role.ts'); + const roleConfigPath = join( + testAppDirectory, + 'src', + 'roles', + DEFAULT_ROLE_FILE_NAME, + ); const roleConfigContent = await fs.readFile(roleConfigPath, 'utf8'); // Verify it uses defineRole @@ -206,11 +210,7 @@ describe('copyBaseApplicationProject', () => { appDirectory: testAppDirectory, }); - const appConfigPath = join( - testAppDirectory, - 'src', - 'application.config.ts', - ); + const appConfigPath = join(testAppDirectory, 'src', APPLICATION_FILE_NAME); const appConfigContent = await fs.readFile(appConfigPath, 'utf8'); expect(appConfigContent).toContain("description: ''"); @@ -239,11 +239,11 @@ describe('copyBaseApplicationProject', () => { // Read both app configs const firstAppConfig = await fs.readFile( - join(firstAppDir, 'src', 'application.config.ts'), + join(firstAppDir, 'src', APPLICATION_FILE_NAME), 'utf8', ); const secondAppConfig = await fs.readFile( - join(secondAppDir, 'src', 'application.config.ts'), + join(secondAppDir, 'src', APPLICATION_FILE_NAME), 'utf8', ); @@ -280,12 +280,12 @@ describe('copyBaseApplicationProject', () => { }); const firstRoleConfig = await fs.readFile( - join(firstAppDir, 'src', 'default.role.ts'), + join(firstAppDir, 'src', 'roles', DEFAULT_ROLE_FILE_NAME), 'utf8', ); const secondRoleConfig = await fs.readFile( - join(secondAppDir, 'src', 'default.role.ts'), + join(secondAppDir, 'src', 'roles', DEFAULT_ROLE_FILE_NAME), 'utf8', ); diff --git a/packages/create-twenty-app/src/utils/app-template.ts b/packages/create-twenty-app/src/utils/app-template.ts index 22da0a677f..c41fa8a522 100644 --- a/packages/create-twenty-app/src/utils/app-template.ts +++ b/packages/create-twenty-app/src/utils/app-template.ts @@ -33,20 +33,27 @@ export const copyBaseApplicationProject = async ({ await createDefaultRoleConfig({ displayName: appDisplayName, appDirectory: sourceFolderPath, + fileFolder: 'roles', + fileName: 'default-role.ts', }); await createDefaultFrontComponent({ appDirectory: sourceFolderPath, + fileFolder: 'front-components', + fileName: 'hello-world.tsx', }); await createDefaultFunction({ appDirectory: sourceFolderPath, + fileFolder: 'logic-functions', + fileName: 'hello-world.ts', }); await createApplicationConfig({ displayName: appDisplayName, description: appDescription, appDirectory: sourceFolderPath, + fileName: 'application-config.ts', }); }; @@ -108,9 +115,13 @@ yarn-error.log* const createDefaultRoleConfig = async ({ displayName, appDirectory, + fileFolder, + fileName, }: { displayName: string; appDirectory: string; + fileFolder?: string; + fileName: string; }) => { const universalIdentifier = v4(); @@ -130,13 +141,18 @@ export default defineRole({ }); `; - await fs.writeFile(join(appDirectory, 'default.role.ts'), content); + await fs.ensureDir(join(appDirectory, fileFolder ?? '')); + await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content); }; const createDefaultFrontComponent = async ({ appDirectory, + fileFolder, + fileName, }: { appDirectory: string; + fileFolder?: string; + fileName: string; }) => { const universalIdentifier = v4(); @@ -159,16 +175,18 @@ export default defineFrontComponent({ }); `; - await fs.writeFile( - join(appDirectory, 'hello-world.front-component.tsx'), - content, - ); + await fs.ensureDir(join(appDirectory, fileFolder ?? '')); + await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content); }; const createDefaultFunction = async ({ appDirectory, + fileFolder, + fileName, }: { appDirectory: string; + fileFolder?: string; + fileName: string; }) => { const universalIdentifier = v4(); const triggerUniversalIdentifier = v4(); @@ -198,23 +216,25 @@ export default defineLogicFunction({ }); `; - await fs.writeFile( - join(appDirectory, 'hello-world.logic-function.ts'), - content, - ); + await fs.ensureDir(join(appDirectory, fileFolder ?? '')); + await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content); }; const createApplicationConfig = async ({ displayName, description, appDirectory, + fileFolder, + fileName, }: { displayName: string; description?: string; appDirectory: string; + fileFolder?: string; + fileName: string; }) => { const content = `import { defineApplication } from 'twenty-sdk'; -import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/default.role'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'; export default defineApplication({ universalIdentifier: '${v4()}', @@ -224,7 +244,8 @@ export default defineApplication({ }); `; - await fs.writeFile(join(appDirectory, 'application.config.ts'), content); + await fs.ensureDir(join(appDirectory, fileFolder ?? '')); + await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content); }; const createPackageJson = async ({ @@ -266,8 +287,8 @@ const createPackageJson = async ({ devDependencies: { typescript: '^5.9.3', '@types/node': '^24.7.2', - '@types/react': '^19.0.0', - react: '^19.0.0', + '@types/react': '^18.2.0', + react: '^18.2.0', eslint: '^9.32.0', 'typescript-eslint': '^8.50.0', }, diff --git a/packages/twenty-apps/hello-world/src/application.config.ts b/packages/twenty-apps/hello-world/src/application.config.ts index eb3e9655af..8978cee22c 100644 --- a/packages/twenty-apps/hello-world/src/application.config.ts +++ b/packages/twenty-apps/hello-world/src/application.config.ts @@ -13,7 +13,7 @@ const config: ApplicationConfig = { isSecret: false, }, }, - functionRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061', + defaultRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061', }; export default config; diff --git a/packages/twenty-docs/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/developers/extend/capabilities/apps.mdx index 779e62b8f2..18e2b478c2 100644 --- a/packages/twenty-docs/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/developers/extend/capabilities/apps.mdx @@ -16,9 +16,6 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf - Build logic functions with custom triggers - Deploy the same app across multiple workspaces -**Coming soon:** -- Custom UI layouts and components - ## Prerequisites - Node.js 24+ and Yarn 4 @@ -93,78 +90,53 @@ my-twenty-app/ README.md public/ # Public assets folder (images, fonts, etc.) src/ - application.config.ts # Required - main application configuration - default-function.role.ts # Default role for serverless functions - hello-world.function.ts # Example serverless function - hello-world.front-component.tsx # Example front component - // your entities (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts) -``` - -### Convention-over-configuration - -Applications use a **convention-over-configuration** approach where entities are detected by their file suffix. This allows flexible organization within the `src/app/` folder: - -| File suffix | Entity type | -|-------------|-------------| -| `*.object.ts` | Custom object definitions | -| `*.function.ts` | Serverless function definitions | -| `*.front-component.tsx` | Front component definitions | -| `*.role.ts` | Role definitions | - -### Supported folder organizations - -You can organize your entities in any of these patterns: - -**Traditional (by type):** -```text -src/ -├── application.config.ts -├── objects/ -│ └── postCard.object.ts -├── functions/ -│ └── createPostCard.function.ts -├── components/ -│ └── card.front-component.tsx -└── roles/ - └── admin.role.ts -``` - -**Feature-based:** -```text -src/ -├── application.config.ts -└── post-card/ - ├── postCard.object.ts - ├── createPostCard.function.ts - ├── card.front-component.tsx - └── postCardAdmin.role.ts -``` - -**Flat:** -```text -src/ -├── application.config.ts -├── postCard.object.ts -├── createPostCard.function.ts -├── card.front-component.tsx -└── admin.role.ts + ├── application-config.ts # Required - main application configuration + ├── roles/ + │ └── default-role.ts # Default role for logic functions + ├── logic-functions/ + │ └── hello-world.ts # Example logic function + └── front-components/ + └── hello-world.tsx # Example front component ``` At a high level: -- **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus scripts like `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall`, and `auth:login` that delegate to the local `twenty` CLI. +- **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus scripts like `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall`, and authentication commands that delegate to the local `twenty` CLI. - **.gitignore**: Ignores common artifacts such as `node_modules`, `.yarn`, `generated/` (typed client), `dist/`, `build/`, coverage folders, log files, and `.env*` files. - **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Lock and configure the Yarn 4 toolchain used by the project. - **.nvmrc**: Pins the Node.js version expected by the project. - **eslint.config.mjs** and **tsconfig.json**: Provide linting and TypeScript configuration for your app's TypeScript sources. - **README.md**: A short README in the app root with basic instructions. - **public/**: A folder for storing public assets (images, fonts, static files) that will be served with your application. Files placed here are uploaded during sync and accessible at runtime. -- **src/**: The main place where you define your application-as-code: - - `application.config.ts`: Global configuration for your app (metadata and runtime wiring). See "Application config" below. - - `*.role.ts`: Role definitions used by your logic functions. See "Default function role" below. - - `*.object.ts`: Custom object definitions. - - `*.function.ts`: Logic function definitions. - - `*.front-component.tsx`: Front component definitions. +- **src/**: The main place where you define your application-as-code + +### Entity detection + +The SDK detects entities by parsing your TypeScript files for **`export default define({...})`** calls. Each entity type has a corresponding helper function exported from `twenty-sdk`: + +| Helper function | Entity type | +|-----------------|-------------| +| `defineObject()` | Custom object definitions | +| `defineLogicFunction()` | Logic function definitions | +| `defineFrontComponent()` | Front component definitions | +| `defineRole()` | Role definitions | +| `defineField()` | Field extensions for existing objects | + + +**File naming is flexible.** Entity detection is AST-based — the SDK scans your source files for the `export default define({...})` pattern. You can organize your files and folders however you like. Grouping by entity type (e.g., `logic-functions/`, `roles/`) is just a convention for code organization, not a requirement. + + +Example of a detected entity: +```typescript +// This file can be named anything and placed anywhere in src/ +import { defineObject, FieldType } from 'twenty-sdk'; + +export default defineObject({ + universalIdentifier: '...', + nameSingular: 'postCard', + // ... rest of config +}); +``` Later commands will add more files and folders: @@ -210,16 +182,18 @@ The twenty-sdk provides typed building blocks and helper functions you use insid ### Helper functions -The SDK provides four helper functions with built-in validation for defining your app entities: +The SDK provides helper functions for defining your app entities. As described in [Entity detection](#entity-detection), you must use `export default define({...})` for your entities to be detected: | Function | Purpose | |----------|---------| -| `defineApplication()` | Configure application metadata | +| `defineApplication()` | Configure application metadata (required, one per app) | | `defineObject()` | Define custom objects with fields | -| `defineFunction()` | Define logic functions with handlers | +| `defineLogicFunction()` | Define logic functions with handlers | +| `defineFrontComponent()` | Define front components for custom UI | | `defineRole()` | Configure role permissions and object access | +| `defineField()` | Extend existing objects with additional fields | -These functions validate your configuration at runtime and provide better IDE autocompletion and type safety. +These functions validate your configuration at build time and provide IDE autocompletion and type safety. ### Defining objects @@ -307,9 +281,9 @@ Key points: -### Application config (application.config.ts) +### Application config (application-config.ts) -Every app has a single `application.config.ts` file that describes: +Every app has a single `application-config.ts` file that describes: - **Who the app is**: identifiers, display name, and description. - **How its functions run**: which role they use for permissions. @@ -318,9 +292,9 @@ Every app has a single `application.config.ts` file that describes: Use `defineApplication()` to define your application configuration: ```typescript -// src/app/application.config.ts +// src/application-config.ts import { defineApplication } from 'twenty-sdk'; -import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'; export default defineApplication({ universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', @@ -335,18 +309,18 @@ export default defineApplication({ isSecret: false, }, }, - roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, + defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, }); ``` Notes: - `universalIdentifier` fields are deterministic IDs you own; generate them once and keep them stable across syncs. - `applicationVariables` become environment variables for your functions (for example, `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`). -- `roleUniversalIdentifier` must match the role you define in your `*.role.ts` file (see below). +- `defaultRoleUniversalIdentifier` must match the role file (see below). #### Roles and permissions -Applications can define roles that encapsulate permissions on your workspace's objects and actions. The field `roleUniversalIdentifier` in `application.config.ts` designates the default role used by your app's logic functions. +Applications can define roles that encapsulate permissions on your workspace's objects and actions. The field `defaultRoleUniversalIdentifier` in `application-config.ts` designates the default role used by your app's logic functions. - The runtime API key injected as `TWENTY_API_KEY` is derived from this default function role. - The typed client will be restricted to the permissions granted to that role. @@ -357,7 +331,7 @@ Applications can define roles that encapsulate permissions on your workspace's o When you scaffold a new app, the CLI also creates a default role file. Use `defineRole()` to define roles with built-in validation: ```typescript -// src/app/default-function.role.ts +// src/roles/default-role.ts import { defineRole, PermissionFlag } from 'twenty-sdk'; export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = @@ -396,10 +370,10 @@ export default defineRole({ }); ``` -The `universalIdentifier` of this role is then referenced in `application.config.ts` as `roleUniversalIdentifier`. In other words: +The `universalIdentifier` of this role is then referenced in `application-config.ts` as `defaultRoleUniversalIdentifier`. In other words: - **\*.role.ts** defines what the default function role can do. -- **application.config.ts** points to that role so your functions inherit its permissions. +- **application-config.ts** points to that role so your functions inherit its permissions. Notes: - Start from the scaffolded role, then progressively restrict it following least‑privilege. @@ -409,11 +383,11 @@ Notes: ### Logic function config and entrypoint -Each function file uses `defineFunction()` to export a configuration with a handler and optional triggers. Use the `*.function.ts` file suffix for automatic detection. +Each function file uses `defineLogicFunction()` to export a configuration with a handler and optional triggers. ```typescript -// src/app/createPostCard.function.ts -import { defineFunction } from 'twenty-sdk'; +// src/app/createPostCard.logic-function.ts +import { defineLogicFunction } from 'twenty-sdk'; import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk'; import Twenty, { type Person } from '~/generated'; @@ -433,7 +407,7 @@ const handler = async (params: RoutePayload) => { return result; }; -export default defineFunction({ +export default defineLogicFunction({ universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', name: 'create-new-post-card', timeoutSeconds: 2, @@ -502,7 +476,7 @@ const handler = async (event: RoutePayload) => { When a route trigger invokes your logic function, it receives a `RoutePayload` object that follows the AWS HTTP API v2 format. Import the type from `twenty-sdk`: ```typescript -import { defineFunction, type RoutePayload } from 'twenty-sdk'; +import { defineLogicFunction, type RoutePayload } from 'twenty-sdk'; const handler = async (event: RoutePayload) => { // Access request data @@ -532,7 +506,7 @@ The `RoutePayload` type has the following structure: By default, HTTP headers from incoming requests are **not** passed to your logic function for security reasons. To access specific headers, explicitly list them in the `forwardedRequestHeaders` array: ```typescript -export default defineFunction({ +export default defineLogicFunction({ universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', name: 'webhook-handler', handler, @@ -567,8 +541,44 @@ const handler = async (event: RoutePayload) => { You can create new functions in two ways: -- **Scaffolded**: Run `yarn entity:add` and choose the option to add a new function. This generates a starter file with a handler and config. -- **Manual**: Create a new `*.function.ts` file and use `defineFunction()`, following the same pattern. +- **Scaffolded**: Run `yarn entity:add` and choose the option to add a new logic function. This generates a starter file with a handler and config. +- **Manual**: Create a new `*.logic-function.ts` file and use `defineLogicFunction()`, following the same pattern. + +### Front components + +Front components let you build custom React components that render within Twenty's UI. Use `defineFrontComponent()` to define components with built-in validation: + +```typescript +// src/my-widget.front-component.tsx +import { defineFrontComponent } from 'twenty-sdk'; + +const MyWidget = () => { + return ( +
+

My Custom Widget

+

This is a custom front component for Twenty.

+
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + name: 'my-widget', + description: 'A custom widget component', + component: MyWidget, +}); +``` + +Key points: +- Front components are React components that render in isolated contexts within Twenty. +- Use the `*.front-component.tsx` file suffix for automatic detection. +- The `component` field references your React component. +- Components are built and synced automatically during `yarn app:dev`. + +You can create new front components in two ways: + +- **Scaffolded**: Run `yarn entity:add` and choose the option to add a new front component. +- **Manual**: Create a new `*.front-component.tsx` file and use `defineFrontComponent()`. ### Generated typed client @@ -592,13 +602,13 @@ When your function runs on Twenty, the platform injects credentials as environme Notes: - You do not need to pass URL or API key to the generated client. It reads `TWENTY_API_URL` and `TWENTY_API_KEY` from process.env at runtime. -- The API key's permissions are determined by the role referenced in your `application.config.ts` via `roleUniversalIdentifier`. This is the default role used by logic functions of your application. -- Applications can define roles to follow least‑privilege. Grant only the permissions your functions need, then point `roleUniversalIdentifier` to that role's universal identifier. +- The API key's permissions are determined by the role referenced in your `application-config.ts` via `defaultRoleUniversalIdentifier`. This is the default role used by logic functions of your application. +- Applications can define roles to follow least‑privilege. Grant only the permissions your functions need, then point `defaultRoleUniversalIdentifier` to that role's universal identifier. ### Hello World example -Explore a minimal, end-to-end example that demonstrates objects, functions, and multiple triggers [here](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world): +Explore a minimal, end-to-end example that demonstrates objects, logic functions, front components, and multiple triggers [here](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world): ## Manual setup (without the scaffolder) diff --git a/packages/twenty-front/src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx b/packages/twenty-front/src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx index c31d8696e8..07a647ed5b 100644 --- a/packages/twenty-front/src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx +++ b/packages/twenty-front/src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx @@ -1,11 +1,6 @@ -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useTestLogicFunction } from '@/logic-functions/hooks/useTestLogicFunction'; -import { computeNewSources } from '@/logic-functions/utils/computeNewSources'; -import { flattenSources } from '@/logic-functions/utils/flattenSources'; -import { getToolInputSchemaFromSourceCode } from '@/logic-functions/utils/getToolInputSchemaFromSourceCode'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsLogicFunctionLabelContainer } from '@/settings/logic-functions/components/SettingsLogicFunctionLabelContainer'; -import { SettingsLogicFunctionCodeEditorTab } from '@/settings/logic-functions/components/tabs/SettingsLogicFunctionCodeEditorTab'; import { SettingsLogicFunctionSettingsTab } from '@/settings/logic-functions/components/tabs/SettingsLogicFunctionSettingsTab'; import { SettingsLogicFunctionTestTab } from '@/settings/logic-functions/components/tabs/SettingsLogicFunctionTestTab'; import { SettingsLogicFunctionTriggersTab } from '@/settings/logic-functions/components/tabs/SettingsLogicFunctionTriggersTab'; @@ -20,15 +15,9 @@ import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTab import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { t } from '@lingui/core/macro'; import { useNavigate, useParams } from 'react-router-dom'; -import { useRecoilValue } from 'recoil'; import { SettingsPath } from 'twenty-shared/types'; import { getSettingsPath, isDefined } from 'twenty-shared/utils'; -import { - IconBolt, - IconCode, - IconSettings, - IconTestPipe, -} from 'twenty-ui/display'; +import { IconBolt, IconSettings, IconTestPipe } from 'twenty-ui/display'; import { useDebouncedCallback } from 'use-debounce'; import { useFindOneApplicationQuery } from '~/generated-metadata/graphql'; @@ -38,7 +27,6 @@ export const SettingsLogicFunctionDetail = () => { const { logicFunctionId = '', applicationId = '' } = useParams(); const navigate = useNavigate(); - const currentWorkspace = useRecoilValue(currentWorkspaceState); const { data, loading: applicationLoading } = useFindOneApplicationQuery({ variables: { id: applicationId }, @@ -47,15 +35,6 @@ export const SettingsLogicFunctionDetail = () => { const applicationName = data?.findOneApplication?.name; - // A logic function is "managed" if it belongs to an application - // other than the workspace's custom application - const workspaceCustomApplicationId = - currentWorkspace?.workspaceCustomApplication?.id; - const isManaged = - isDefined(applicationId) && - applicationId !== '' && - applicationId !== workspaceCustomApplicationId; - const instanceId = `${LOGIC_FUNCTION_DETAIL_ID}-${logicFunctionId}`; const activeTabId = useRecoilComponentValue( @@ -98,57 +77,17 @@ export const SettingsLogicFunctionDetail = () => { }; }; - const onCodeChange = async (filePath: string, value: string) => { - setFormValues((prevState: LogicFunctionFormValues) => { - return { - ...prevState, - code: computeNewSources({ - previousCode: prevState['code'], - filePath, - value, - }), - }; - }); - - // Parse and save schema if editing the handler file - let toolInputSchema: object | null | undefined; - - if (filePath === logicFunction?.sourceHandlerPath) { - toolInputSchema = await getToolInputSchemaFromSourceCode(value); - } - - await handleSave(toolInputSchema); - }; - const handleTestFunction = async () => { navigate('#test'); await testLogicFunction(); }; const tabs = [ - { id: 'editor', title: t`Editor`, Icon: IconCode }, + { id: 'settings', title: t`Settings`, Icon: IconSettings }, { id: 'triggers', title: t`Triggers`, Icon: IconBolt }, { id: 'test', title: t`Test`, Icon: IconTestPipe }, - { id: 'settings', title: t`Settings`, Icon: IconSettings }, ]; - const flattenedCode = flattenSources(formValues.code); - - const files = flattenedCode - .map((file) => ({ - path: file.path, - language: 'typescript', - content: file.content, - })) - .sort((a, b) => - a.path === logicFunction?.sourceHandlerPath - ? -1 - : b.path === logicFunction?.sourceHandlerPath - ? 1 - : 0, - ); - - const isEditorTab = activeTabId === 'editor'; const isTriggersTab = activeTabId === 'triggers'; const isTestTab = activeTabId === 'test'; const isSettingsTab = activeTabId === 'settings'; @@ -203,15 +142,6 @@ export const SettingsLogicFunctionDetail = () => { > - {isEditorTab && ( - - )} {isTriggersTab && logicFunction && ( )} diff --git a/packages/twenty-front/src/pages/settings/logic-functions/__stories__/SettingsLogicFunctionDetail.stories.tsx b/packages/twenty-front/src/pages/settings/logic-functions/__stories__/SettingsLogicFunctionDetail.stories.tsx index 4b7cfab80a..84a2fee014 100644 --- a/packages/twenty-front/src/pages/settings/logic-functions/__stories__/SettingsLogicFunctionDetail.stories.tsx +++ b/packages/twenty-front/src/pages/settings/logic-functions/__stories__/SettingsLogicFunctionDetail.stories.tsx @@ -64,7 +64,7 @@ export const Default: Story = { play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { const canvas = within(canvasElement); await sleep(100); - await canvas.findByText('Code your function', undefined, { + await canvas.findByText('Name and describe your function', undefined, { timeout: 3000, }); }, diff --git a/packages/twenty-sdk/README.md b/packages/twenty-sdk/README.md index 7ee1c554a4..69f854da9b 100644 --- a/packages/twenty-sdk/README.md +++ b/packages/twenty-sdk/README.md @@ -56,7 +56,7 @@ Commands: app:uninstall Uninstall application from Twenty entity:add Add a new entity to your application function:logs Watch application function logs - function:execute Execute a serverless function with a JSON payload + function:execute Execute a logic function with a JSON payload help [command] display help for command ``` diff --git a/packages/twenty-server/src/engine/core-modules/marketplace/mocks/mock-manifest.json b/packages/twenty-server/src/engine/core-modules/marketplace/mocks/mock-manifest.json index 46865d0291..d351624e7e 100644 --- a/packages/twenty-server/src/engine/core-modules/marketplace/mocks/mock-manifest.json +++ b/packages/twenty-server/src/engine/core-modules/marketplace/mocks/mock-manifest.json @@ -1,7 +1,7 @@ { "application": { "universalIdentifier": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", - "functionRoleUniversalIdentifier": "00000000-0000-0000-0000-000000000000", + "defaultRoleUniversalIdentifier": "00000000-0000-0000-0000-000000000000", "displayName": "Data Enrichment", "description": "Enrich your data easily. Choose your provider.", "icon": "IconSparkles",