Publish twenty packages (#17676)

- removes code editor in settings
- update readmes and docs
This commit is contained in:
martmull
2026-02-03 18:16:54 +01:00
committed by GitHub
parent ccd40e7633
commit b53dfa0533
9 changed files with 171 additions and 206 deletions
+8 -4
View File
@@ -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 uptodate 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 uptodate using `yarn app:generate`.
## Publish your application
@@ -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',
);
@@ -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',
},
@@ -13,7 +13,7 @@ const config: ApplicationConfig = {
isSecret: false,
},
},
functionRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
defaultRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
};
export default config;
@@ -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<Entity>({...})`** 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 |
<Note>
**File naming is flexible.** Entity detection is AST-based — the SDK scans your source files for the `export default define<Entity>({...})` 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.
</Note>
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<Entity>({...})` 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:
</Note>
### 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 leastprivilege.
@@ -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 (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>My Custom Widget</h1>
<p>This is a custom front component for Twenty.</p>
</div>
);
};
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 leastprivilege. 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 leastprivilege. 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)
@@ -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 = () => {
>
<SettingsPageContainer>
<TabList tabs={tabs} componentInstanceId={instanceId} />
{isEditorTab && (
<SettingsLogicFunctionCodeEditorTab
files={files}
handleExecute={handleTestFunction}
onChange={onCodeChange}
isTesting={isTesting}
isManaged={isManaged}
/>
)}
{isTriggersTab && logicFunction && (
<SettingsLogicFunctionTriggersTab logicFunction={logicFunction} />
)}
@@ -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,
});
},
+1 -1
View File
@@ -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
```
@@ -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",