Compare commits

...
Author SHA1 Message Date
Sonarly Claude CodeandClaude Opus 4.6 ba5ebfb791 Fix query read timeout in CronTriggerCronJob by eliminating N+1 queries
The cron job was issuing a separate database query per active workspace
to find logic functions with cron triggers. With many active workspaces,
this caused cumulative query time to exceed the read timeout. Replace
the per-workspace loop with a single query using TypeORM's In() operator
to fetch all matching logic functions at once.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-02-18 17:12:32 +00:00
Charles BochetandGitHub c3781e87cc Sync page Layout (#18034)
## Sync page layouts, tabs, and widgets

Adds the ability for SDK applications to synchronize `pageLayout`,
`pageLayoutTab`, and `pageLayoutWidget` entities, following the same
pattern established in #18003 for views and navigation menu items.

### Changes

**`twenty-shared`**
- New `PageLayoutManifest`, `PageLayoutTabManifest`, and
`PageLayoutWidgetManifest` types with a hierarchical structure (page
layout → tabs → widgets)
- Added `pageLayouts: PageLayoutManifest[]` to the `Manifest` type

**`twenty-sdk`**
- New `definePageLayout()` SDK function with validation for
universalIdentifier, name, and nested tabs/widgets
- Wired into the manifest extraction and build pipeline
(`DefinePageLayout` target function, `PageLayouts` entity key)
- Exported from the SDK entry point

**`twenty-server`**
- Added `pageLayout`, `pageLayoutTab`, `pageLayoutWidget` to
`APPLICATION_MANIFEST_METADATA_NAMES`
- New conversion utilities: manifest → universal flat entity for all
three entity types
- Updated `computeApplicationManifestAllUniversalFlatEntity
2026-02-18 17:55:04 +01:00
e9b5cb830c i18n - docs translations (#18040)
Created by Github action

Co-authored-by: github-actions <[email protected]>
2026-02-18 17:54:40 +01:00
EtienneandGitHub e40c758aa6 Nav Menu Item Migration command fix (#18041) 2026-02-18 17:45:21 +01:00
martmullandGitHub 53c314d0fa 2094 extensibility define postinstall orand preinstall function to run in application (#18037)
- add a new optional key `postInstallLogicFunctionUniversalIdentifier`
in applicationConfig
- seed postInstall function in create-twenty-app
- update execute:function options
- update doc
2026-02-18 15:38:22 +00:00
618df704e6 [Chore] : Generate migration for DATE_TIME to DATE for DATE_TIME + IS operand filters (#17564)
migration command in response to the fix :
https://github.com/twentyhq/twenty/pull/17529 for the issue
https://github.com/twentyhq/core-team-issues/issues/2027

---------

Co-authored-by: Arun kumar <[email protected]>
Co-authored-by: Lucas Bordeau <[email protected]>
2026-02-18 15:15:38 +00:00
EtienneandGitHub 058489b5cc Fixes - Workspace logo migration (#18035)
- Update migration command to handle case where workspace logo is
originated from workspace email and point to twenty-icons.com
- Update same logic for new workspaces
- Add feature-flag for all newly created workspaces
2026-02-18 16:35:48 +01:00
3bd431e95d Use proper PostgreSQL identifier/literal escaping in workspace DDL (#18024)
## Summary

- Replace the character-stripping approach (`removeSqlDDLInjection`)
with standard PostgreSQL `escapeIdentifier` and `escapeLiteral`
functions across all workspace schema manager services
- Add missing identifier escaping to `createForeignKey` (was the only
method in the FK manager without it)
- Add allowlist validation for index WHERE clauses and FK action types
- Harden tsvector expression builder with proper identifier quoting

## Context

The workspace schema managers build DDL dynamically from metadata (table
names, column names, enum values, etc.). The previous approach stripped
all non-alphanumeric characters — safe but lossy (silently corrupts
values with legitimate special characters). The new approach uses
PostgreSQL's standard escaping:

- **Identifiers**: double internal `"` and wrap → `"my""table"` (same
algorithm as `pg` driver's `escapeIdentifier`)
- **Literals**: double internal `'` and wrap → `'it''s a value'` (same
algorithm as `pg` driver's `escapeLiteral`)

`removeSqlDDLInjection` is kept only for name generation (e.g.,
`computePostgresEnumName`) where stripping to `[a-zA-Z0-9_]` is the
correct behavior.

## Files changed

| File | What |
|------|------|
| `remove-sql-injection.util.ts` | Added `escapeIdentifier` +
`escapeLiteral` |
| `validate-index-where-clause.util.ts` | New — allowlist for partial
index WHERE clauses |
| 5 schema manager services | Replaced strip+manual-quote with
`escapeIdentifier`/`escapeLiteral` |
| `build-sql-column-definition.util.ts` | `escapeIdentifier` for column
names, validated `generatedType` |
| `sanitize-default-value.util.ts` | `escapeLiteral` instead of
stripping |
| `serialize-default-value.util.ts` | `escapeLiteral` for values,
`escapeIdentifier` for enum casts |
| `get-ts-vector-column-expression.util.ts` | `escapeIdentifier` for
field names in expressions |
| `sanitize-default-value.util.spec.ts` | Updated tests for escape
behavior |

## Test plan

- [x] All 64 existing tests pass across 6 test suites
- [x] `lint:diff-with-main` passes
- [x] TypeScript typecheck — no new errors
- [ ] Verify workspace sync-metadata still works end-to-end
- [ ] Verify custom object/field creation works
- [ ] Verify enum field option changes work


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <[email protected]>
2026-02-18 14:24:10 +00:00
f4a61f26c0 Replace generic "Unknown error" messages with descriptive error details (#18019)
## Summary

- Replace all generic `"Unknown error"` fallback messages across the
server codebase with messages that include the actual error details
- The most impactful change is in `guard-redirect.service.ts`, which
handles OAuth redirect errors — non-`AuthException` errors (e.g.,
passport state verification failures) now show `"Authentication error:
<actual message>"` instead of the opaque `"Unknown error"`
- Gmail/Google error handler services now include the error message in
the thrown exception instead of discarding it
- Other catch blocks (workflow delay resume, migration runner rollback,
code interpreter, marketplace) now use `String(error)` for non-Error
objects instead of a static fallback

Fixes the class of issues reported in
https://github.com/twentyhq/twenty/issues/17812, where a user saw
"Unknown error" during Google OAuth and had no way to diagnose the root
cause (which turned out to be a session cookie / SSL configuration
issue).

## Test plan

- [ ] Verify OAuth error flows (e.g., Google Auth with misconfigured
callback URL) now display the actual error message on the `/verify` page
instead of "Unknown error"
- [ ] Verify Gmail sync error handling still correctly classifies and
re-throws errors with descriptive messages
- [ ] Verify workflow delay resume failures include the error details in
the workflow run status


Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <[email protected]>
2026-02-18 14:12:47 +00:00
8e6b267ff3 Allow DATE_TIME IS operand to filter on a whole day (#17529)
Fixes:  https://github.com/twentyhq/core-team-issues/issues/2027

We've replaced the DATE_TIME picker with DATE picker, and changed the
logic to filter for complete day period.



https://github.com/user-attachments/assets/ba7e1078-bab3-4c62-a803-d6a851f14b7d

---------

Co-authored-by: Arun kumar <[email protected]>
Co-authored-by: Lucas Bordeau <[email protected]>
2026-02-18 13:44:05 +00:00
neo773andGitHub 88146c2170 Fix Gmail thread awareness for custom labels (#18031)
This fixes two edge cases for Gmail

- When policy was set to `SELECTED_FOLDERS` excluding root INBOX, it
missed label changes, so messages with newly applied labels weren't
imported until a full resync.

- Gmail thread replies by default by default do no inherit parent
message's label properties so thread context was also lost because only
individually labeled messages were returned, dropping earlier parts of
the conversation.

Fixed by subscribing to `labelAdded`/`labelRemoved` history events and
fetching full thread context when at least one message in a thread
carries a synced label. `ALL_FOLDERS` path is untouched.
2026-02-18 14:10:38 +01:00
EtienneandGitHub 3706da9bcb v1.18 - Fix command (#18032)
Same as here https://github.com/twentyhq/twenty/pull/18016
2026-02-18 13:52:13 +01:00
9bac8f15d4 i18n - translations (#18029)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-18 13:26:54 +01:00
Charles BochetandGitHub 7332379d26 Improve API Client usage and add Typescript check (#18023)
## Summary


https://github.com/user-attachments/assets/1e75cc9d-d9d2-4ef2-99f9-34450f5d8de7



Add background incremental type checking (`tsc --watch`) to the SDK dev
mode, so type regressions are caught when the generated API client
changes — without requiring a full rebuild of source files.

Previously, removing a field from the data model would regenerate the
API client, but existing front components/logic functions referencing
the removed field wouldn't surface type errors (since their source
didn't change, esbuild wouldn't rebuild them).

## What changed

- **Background `tsc --watch`**: a long-lived TypeScript watcher runs
alongside esbuild watchers, incrementally re-checking all files when the
generated client changes. Only logs on state transitions (errors appear
/ errors clear) to stay quiet.
- **Atomic client generation**: API client is now generated into a temp
directory and swapped in atomically, avoiding a race condition where
`tsc --watch` could see an empty `generated/` directory
mid-regeneration.
- **Step decoupling**: orchestrator steps no longer receive
`uploadFilesStep` directly. Instead, they use callbacks (`onFileBuilt`,
`onApiClientGenerated`), and each step manages its own `builtFileInfos`
state.
- **`apiClientChecksum` omitted from `ApplicationConfig`**: it's a
build-time computed value, same as `packageJsonChecksum`.
<img width="327" height="177" alt="image"
src="https://github.com/user-attachments/assets/02bd25bb-fa41-42b0-8d96-01c51bd4580c"
/>

<img width="529" height="452" alt="image"
src="https://github.com/user-attachments/assets/61f6e968-365b-4a5b-8f2b-a8419d6b1bd3"
/>
2026-02-18 13:26:30 +01:00
Raphaël BosiandGitHub 2455c859b4 Add SSE for metadata and plug front components (#17998)
Create the necessary tooling to listen to metadata events and plug it to
the front components. Now we have a hot reload like experience when we
edit a component in an app.

## Backend

- Split `EventWithQueryIds` into `ObjectRecordEventWithQueryIds` and
`MetadataEventWithQueryIds`
- Publish metadata event batches to active SSE streams in
`MetadataEventsToDbListener`

## Frontend

- Create a metadata event dispatching pipeline: SSE metadata events are
grouped by metadata name, transformed into
`MetadataOperationBrowserEventDetail` objects, and dispatched as browser
`CustomEvents`
- Add `useListenToMetadataOperationBrowserEvent` hook for consuming
metadata operation events filtered by metadata name and operation type
- Rename `useListenToObjectRecordEventsForQuery` to
`useListenToEventsForQuery`, now accepting both
`RecordGqlOperationSignature` and `MetadataGqlOperationSignature`
- Implement `useOnFrontComponentUpdated` which subscribes to front
component metadata events and updates the Apollo cache when the
component is modified
- Add `builtComponentChecksum` to the front component query and appends
it to the component URL for browser cache invalidation
2026-02-18 11:26:20 +00:00
martmullandGitHub e3753bf822 App feedbacks (#18028)
as title
2026-02-18 11:04:54 +00:00
252 changed files with 5231 additions and 1142 deletions
+4
View File
@@ -54,6 +54,9 @@ yarn twenty function:logs
# Execute a function with a JSON payload
yarn twenty function:execute -n my-function -p '{"key": "value"}'
# Execute the post-install function
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn twenty app:uninstall
```
@@ -63,6 +66,7 @@ yarn twenty app:uninstall
- `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
- `logic-functions/post-install.ts` - Post-install logic function (runs after app installation)
- `front-components/hello-world.tsx` - Example front component
- TypeScript configuration
- A prewired `twenty` script that delegates to the `twenty` CLI from twenty-sdk
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "0.6.0-alpha",
"version": "0.6.0",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
@@ -49,6 +49,12 @@ export const copyBaseApplicationProject = async ({
fileName: 'hello-world.ts',
});
await createDefaultPostInstallFunction({
appDirectory: sourceFolderPath,
fileFolder: 'logic-functions',
fileName: 'post-install.ts',
});
await createApplicationConfig({
displayName: appDisplayName,
description: appDescription,
@@ -196,7 +202,6 @@ const handler = async (): Promise<{ message: string }> => {
return { message: 'Hello, World!' };
};
// Logic function handler - rename and implement your logic
export default defineLogicFunction({
universalIdentifier: '${universalIdentifier}',
name: 'hello-world-logic-function',
@@ -215,6 +220,38 @@ export default defineLogicFunction({
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createDefaultPostInstallFunction = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '${universalIdentifier}';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createApplicationConfig = async ({
displayName,
description,
@@ -230,12 +267,14 @@ const createApplicationConfig = async ({
}) => {
const content = `import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '${v4()}',
displayName: '${displayName}',
description: '${description ?? ''}',
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
`;
@@ -53,6 +53,9 @@ yarn twenty function:logs
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Execute the post-install function
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn twenty app:uninstall
@@ -69,7 +72,7 @@ When you run `npx create-twenty-app@latest my-twenty-app`, the scaffolder:
- Copies a minimal base application into `my-twenty-app/`
- Adds a local `twenty-sdk` dependency and Yarn 4 configuration
- Creates config files and scripts wired to the `twenty` CLI
- Generates a default application config and a default function role
- Generates a default application config, a default function role, and a post-install function
A freshly scaffolded app looks like this:
@@ -91,7 +94,8 @@ my-twenty-app/
├── roles/
│ └── default-role.ts # Default role for logic functions
├── logic-functions/
── hello-world.ts # Example logic function
── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
└── front-components/
└── hello-world.tsx # Example front component
```
@@ -289,6 +293,7 @@ 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.
- **(Optional) variables**: keyvalue pairs exposed to your functions as environment variables.
- **(Optional) post-install function**: a logic function that runs after the app is installed.
Use `defineApplication()` to define your application configuration:
@@ -296,6 +301,7 @@ Use `defineApplication()` to define your application configuration:
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -311,6 +317,7 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -318,6 +325,7 @@ 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`).
- `defaultRoleUniversalIdentifier` must match the role file (see below).
- `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#post-install-functions).
#### Roles and permissions
@@ -450,6 +458,54 @@ Notes:
- The `triggers` array is optional. Functions without triggers can be used as utility functions called by other functions.
- You can mix multiple trigger types in a single function.
### Post-install functions
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
The function is wired into your app by referencing its universal identifier in `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
You can also manually execute the post-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
Key points:
- Post-install functions are standard logic functions — they use `defineLogicFunction()` like any other function.
- The `postInstallLogicFunctionUniversalIdentifier` field in `defineApplication()` is optional. If omitted, no function runs after installation.
- The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
- Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
### Route trigger payload
<Warning>
@@ -45,19 +45,22 @@ yarn twenty app:dev
من هنا يمكنك:
```bash filename="Terminal"
# أضف كيانًا جديدًا إلى تطبيقك (موجّه)
# Add a new entity to your application (guided)
yarn twenty entity:add
# راقب سجلات وظائف تطبيقك
# Watch your application's function logs
yarn twenty function:logs
# نفّذ وظيفة بالاسم
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# أزل تثبيت التطبيق من مساحة العمل الحالية
# Execute the post-install function
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn twenty app:uninstall
# اعرض مساعدة الأوامر
# Display commands' help
yarn twenty help
```
@@ -70,7 +73,7 @@ yarn twenty help
* ينسخ تطبيقًا أساسيًا مصغّرًا إلى `my-twenty-app/`
* يضيف اعتمادًا محليًا `twenty-sdk` وتهيئة Yarn 4
* ينشئ ملفات ضبط ونصوصًا مرتبطة بـ `twenty` CLI
* يُولّد ضبطًا افتراضيًا للتطبيق ودورًا افتراضيًا للوظيفة
* Generates a default application config, a default function role, and a post-install function
يبدو التطبيق المُنشأ حديثًا بالقالب كما يلي:
@@ -86,15 +89,16 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # مجلد الأصول العامة (صور، خطوط، إلخ)
public/ # Public assets folder (images, fonts, etc.)
src/
├── application-config.ts # مطلوب - التكوين الرئيسي للتطبيق
├── application-config.ts # Required - main application configuration
├── roles/
│ └── default-role.ts # الدور الافتراضي لوظائف المنطق
│ └── default-role.ts # Default role for logic functions
├── logic-functions/
── hello-world.ts # مثال لوظيفة منطقية
── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
└── front-components/
└── hello-world.tsx # مثال لمكوّن الواجهة الأمامية
└── hello-world.tsx # Example front component
```
بشكل عام:
@@ -290,6 +294,7 @@ export default defineObject({
* **هوية التطبيق**: المعرفات، اسم العرض، والوصف.
* **كيفية تشغيل وظائفه**: الدور الذي تستخدمه للأذونات.
* **متغيرات (اختياري)**: أزواج مفتاح-قيمة تُعرض لوظائفك كمتغيرات بيئة.
* **(Optional) post-install function**: a logic function that runs after the app is installed.
Use `defineApplication()` to define your application configuration:
@@ -297,6 +302,7 @@ Use `defineApplication()` to define your application configuration:
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -312,6 +318,7 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -320,6 +327,7 @@ export default defineApplication({
* حقول `universalIdentifier` هي معرّفات حتمية تخصك؛ أنشئها مرة واحدة واحتفظ بها ثابتة عبر عمليات المزامنة.
* `applicationVariables` تصبح متغيرات بيئة لوظائفك (على سبيل المثال، `DEFAULT_RECIPIENT_NAME` متاح كـ `process.env.DEFAULT_RECIPIENT_NAME`).
* `defaultRoleUniversalIdentifier` يجب أن يطابق ملف الدور (انظر أدناه).
* `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#post-install-functions).
#### الأدوار والصلاحيات
@@ -458,6 +466,55 @@ export default defineLogicFunction({
* المصفوفة `triggers` اختيارية. يمكن استخدام الوظائف بدون مشغلات كوظائف مساعدة تُستدعى بواسطة وظائف أخرى.
* يمكنك مزج أنواع متعددة من المشغلات في وظيفة واحدة.
### Post-install functions
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
The function is wired into your app by referencing its universal identifier in `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
You can also manually execute the post-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
النقاط الرئيسية:
* Post-install functions are standard logic functions — they use `defineLogicFunction()` like any other function.
* The `postInstallLogicFunctionUniversalIdentifier` field in `defineApplication()` is optional. If omitted, no function runs after installation.
* The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
* Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
### حمولة مشغل المسار
<Warning>
@@ -45,19 +45,22 @@ yarn twenty app:dev
Odtud můžete:
```bash filename="Terminal"
# Přidejte do vaší aplikace novou entitu (s průvodcem)
# Add a new entity to your application (guided)
yarn twenty entity:add
# Sledujte logy funkcí vaší aplikace
# Watch your application's function logs
yarn twenty function:logs
# Spusťte funkci podle názvu
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Odinstalujte aplikaci z aktuálního pracovního prostoru
# Execute the post-install function
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn twenty app:uninstall
# Zobrazte nápovědu k příkazům
# Display commands' help
yarn twenty help
```
@@ -70,7 +73,7 @@ Když spustíte `npx create-twenty-app@latest my-twenty-app`, scaffolder:
* Zkopíruje minimální základní aplikaci do `my-twenty-app/`
* Přidá lokální závislost `twenty-sdk` a konfiguraci pro Yarn 4
* Vytvoří konfigurační soubory a skripty napojené na `twenty` CLI
* Vygeneruje výchozí konfiguraci aplikace a výchozí roli funkcí
* Generates a default application config, a default function role, and a post-install function
Čerstvě vytvořená aplikace vypadá takto:
@@ -86,15 +89,16 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # Složka s veřejnými prostředky (obrázky, písma apod.)
public/ # Public assets folder (images, fonts, etc.)
src/
├── application-config.ts # Povinné hlavní konfigurace aplikace
├── application-config.ts # Required - main application configuration
├── roles/
│ └── default-role.ts # Výchozí role pro logic funkce
│ └── default-role.ts # Default role for logic functions
├── logic-functions/
── hello-world.ts # Ukázková logic funkce
── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
└── front-components/
└── hello-world.tsx # Ukázková front-endová komponenta
└── hello-world.tsx # Example front component
```
V kostce:
@@ -290,6 +294,7 @@ Každá aplikace má jeden soubor `application-config.ts`, který popisuje:
* **Identitu aplikace**: identifikátory, zobrazovaný název a popis.
* **Jak běží její funkce**: kterou roli používají pro oprávnění.
* **(Volitelné) proměnné**: dvojice klíč–hodnota zpřístupněné vašim funkcím jako proměnné prostředí.
* **(Optional) post-install function**: a logic function that runs after the app is installed.
Use `defineApplication()` to define your application configuration:
@@ -297,6 +302,7 @@ Use `defineApplication()` to define your application configuration:
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -312,6 +318,7 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -320,6 +327,7 @@ Poznámky:
* Pole `universalIdentifier` jsou deterministická ID, která vlastníte; vygenerujte je jednou a udržujte je stabilní napříč synchronizacemi.
* `applicationVariables` se stanou proměnnými prostředí pro vaše funkce (například `DEFAULT_RECIPIENT_NAME` je dostupné jako `process.env.DEFAULT_RECIPIENT_NAME`).
* `defaultRoleUniversalIdentifier` se musí shodovat se souborem role (viz níže).
* `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#post-install-functions).
#### Role a oprávnění
@@ -458,6 +466,55 @@ Poznámky:
* Pole `triggers` je volitelné. Funkce bez spouštěčů lze použít jako pomocné funkce volané jinými funkcemi.
* V jedné funkci můžete kombinovat více typů spouštěčů.
### Post-install functions
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
The function is wired into your app by referencing its universal identifier in `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
You can also manually execute the post-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
Hlavní body:
* Post-install functions are standard logic functions — they use `defineLogicFunction()` like any other function.
* The `postInstallLogicFunctionUniversalIdentifier` field in `defineApplication()` is optional. If omitted, no function runs after installation.
* The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
* Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
### Payload spouštěče trasy
<Warning>
@@ -45,19 +45,22 @@ yarn twenty app:dev
Von hier aus können Sie:
```bash filename="Terminal"
# Eine neue Entität zu Ihrer Anwendung hinzufügen (geführt)
# Add a new entity to your application (guided)
yarn twenty entity:add
# Die Funktionsprotokolle Ihrer Anwendung überwachen
# Watch your application's function logs
yarn twenty function:logs
# Eine Funktion anhand ihres Namens ausführen
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Die Anwendung aus dem aktuellen Arbeitsbereich deinstallieren
# Execute the post-install function
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn twenty app:uninstall
# Hilfe zu Befehlen anzeigen
# Display commands' help
yarn twenty help
```
@@ -70,7 +73,7 @@ Wenn Sie `npx create-twenty-app@latest my-twenty-app` ausführen, erledigt der S
* Kopiert eine minimale Basisanwendung nach `my-twenty-app/`
* Fügt eine lokale `twenty-sdk`-Abhängigkeit und die Yarn-4-Konfiguration hinzu
* Erstellt Konfigurationsdateien und Skripte, die an die `twenty`-CLI angebunden sind
* Generiert eine Standard-Anwendungskonfiguration und eine Standard-Funktionsrolle
* Generates a default application config, a default function role, and a post-install function
Eine frisch erzeugte App sieht so aus:
@@ -92,7 +95,8 @@ my-twenty-app/
├── roles/
│ └── default-role.ts # Default role for logic functions
├── logic-functions/
── hello-world.ts # Example logic function
── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
└── front-components/
└── hello-world.tsx # Example front component
```
@@ -290,6 +294,7 @@ Jede App hat eine einzelne Datei `application-config.ts`, die Folgendes beschrei
* **Was die App ist**: Bezeichner, Anzeigename und Beschreibung.
* **Wie ihre Funktionen ausgeführt werden**: welche Rolle sie für Berechtigungen verwenden.
* **(Optional) Variablen**: SchlüsselWert-Paare, die Ihren Funktionen als Umgebungsvariablen zur Verfügung gestellt werden.
* **(Optional) post-install function**: a logic function that runs after the app is installed.
Verwenden Sie `defineApplication()`, um Ihre Anwendungskonfiguration zu definieren:
@@ -297,6 +302,7 @@ Verwenden Sie `defineApplication()`, um Ihre Anwendungskonfiguration zu definier
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -312,6 +318,7 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -320,6 +327,7 @@ Notizen:
* `universalIdentifier`-Felder sind deterministische IDs, die Sie besitzen; generieren Sie sie einmal und halten Sie sie über Synchronisierungen hinweg stabil.
* `applicationVariables` werden zu Umgebungsvariablen für Ihre Funktionen (zum Beispiel ist `DEFAULT_RECIPIENT_NAME` als `process.env.DEFAULT_RECIPIENT_NAME` verfügbar).
* `defaultRoleUniversalIdentifier` muss mit der Rollendatei übereinstimmen (siehe unten).
* `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#post-install-functions).
#### Rollen und Berechtigungen
@@ -458,6 +466,55 @@ Notizen:
* Das Array `triggers` ist optional. Funktionen ohne Trigger können als von anderen Funktionen aufgerufene Utility-Funktionen verwendet werden.
* Sie können mehrere Trigger-Typen in einer Funktion kombinieren.
### Post-install functions
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
The function is wired into your app by referencing its universal identifier in `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
You can also manually execute the post-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
Hauptpunkte:
* Post-install functions are standard logic functions — they use `defineLogicFunction()` like any other function.
* The `postInstallLogicFunctionUniversalIdentifier` field in `defineApplication()` is optional. If omitted, no function runs after installation.
* The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
* Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
### Routen-Trigger-Payload
<Warning>
@@ -45,19 +45,22 @@ yarn twenty app:dev
Da qui puoi:
```bash filename="Terminal"
# Aggiungi una nuova entità alla tua applicazione (guidata)
# Add a new entity to your application (guided)
yarn twenty entity:add
# Monitora i log delle funzioni della tua applicazione
# Watch your application's function logs
yarn twenty function:logs
# Esegui una funzione per nome
yarn twenty function:execute -n my-function -p '{\"name\": \"test\"}'
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Disinstalla l'applicazione dallo spazio di lavoro corrente
# Execute the post-install function
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn twenty app:uninstall
# Mostra l'aiuto dei comandi
# Display commands' help
yarn twenty help
```
@@ -70,7 +73,7 @@ Quando esegui `npx create-twenty-app@latest my-twenty-app`, lo scaffolder:
* Copia un'applicazione base minimale in `my-twenty-app/`
* Aggiunge una dipendenza locale `twenty-sdk` e la configurazione di Yarn 4
* Crea file di configurazione e script collegati alla CLI `twenty`
* Genera una configurazione applicativa predefinita e un ruolo funzione predefinito
* Generates a default application config, a default function role, and a post-install function
Un'app appena generata dallo scaffolder si presenta così:
@@ -86,15 +89,16 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # Cartella delle risorse pubbliche (immagini, font, ecc.)
public/ # Public assets folder (images, fonts, etc.)
src/
├── application-config.ts # Obbligatorio - configurazione principale dell'applicazione
├── application-config.ts # Required - main application configuration
├── roles/
│ └── default-role.ts # Ruolo predefinito per le funzioni logiche
│ └── default-role.ts # Default role for logic functions
├── logic-functions/
── hello-world.ts # Funzione logica di esempio
── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
└── front-components/
└── hello-world.tsx # Componente front-end di esempio
└── hello-world.tsx # Example front component
```
A livello generale:
@@ -290,6 +294,7 @@ Ogni app ha un singolo file `application-config.ts` che descrive:
* **Identità dell'app**: identificatori, nome visualizzato e descrizione.
* **Come vengono eseguite le sue funzioni**: quale ruolo usano per i permessi.
* **Variabili (opzionali)**: coppie chiavevalore esposte alle funzioni come variabili d'ambiente.
* **(Optional) post-install function**: a logic function that runs after the app is installed.
Usa `defineApplication()` per definire la configurazione della tua applicazione:
@@ -297,6 +302,7 @@ Usa `defineApplication()` per definire la configurazione della tua applicazione:
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -312,6 +318,7 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -320,6 +327,7 @@ Note:
* I campi `universalIdentifier` sono ID deterministici sotto il tuo controllo; generali una volta e mantienili stabili tra le sincronizzazioni.
* `applicationVariables` diventano variabili d'ambiente per le tue funzioni (ad esempio, `DEFAULT_RECIPIENT_NAME` è disponibile come `process.env.DEFAULT_RECIPIENT_NAME`).
* `defaultRoleUniversalIdentifier` deve corrispondere al file del ruolo (vedi sotto).
* `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#post-install-functions).
#### Ruoli e permessi
@@ -458,6 +466,55 @@ Note:
* L'array `triggers` è facoltativo. Le funzioni senza trigger possono essere utilizzate come funzioni di utilità richiamate da altre funzioni.
* Puoi combinare più tipi di trigger in un'unica funzione.
### Post-install functions
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
The function is wired into your app by referencing its universal identifier in `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
You can also manually execute the post-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
Punti chiave:
* Post-install functions are standard logic functions — they use `defineLogicFunction()` like any other function.
* The `postInstallLogicFunctionUniversalIdentifier` field in `defineApplication()` is optional. If omitted, no function runs after installation.
* The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
* Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
### Payload del trigger di route
<Warning>
@@ -45,19 +45,22 @@ yarn twenty app:dev
A partir daqui você pode:
```bash filename="Terminal"
# Adicionar uma nova entidade à sua aplicação (assistido)
# Add a new entity to your application (guided)
yarn twenty entity:add
# Acompanhar os logs das funções da sua aplicação
# Watch your application's function logs
yarn twenty function:logs
# Executar uma função pelo nome
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Desinstalar a aplicação do espaço de trabalho atual
# Execute the post-install function
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn twenty app:uninstall
# Exibir a ajuda dos comandos
# Display commands' help
yarn twenty help
```
@@ -70,7 +73,7 @@ Ao executar `npx create-twenty-app@latest my-twenty-app`, o gerador:
* Copia um aplicativo base mínimo para `my-twenty-app/`
* Adiciona uma dependência local `twenty-sdk` e a configuração do Yarn 4
* Cria arquivos de configuração e scripts conectados à CLI `twenty`
* Gera uma configuração de aplicativo padrão e um papel padrão para as funções
* Generates a default application config, a default function role, and a post-install function
Um aplicativo recém-criado pelo scaffold fica assim:
@@ -86,15 +89,16 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # Pasta de recursos públicos (imagens, fontes, etc.)
public/ # Public assets folder (images, fonts, etc.)
src/
├── application-config.ts # Obrigatório - configuração principal da aplicação
├── application-config.ts # Required - main application configuration
├── roles/
│ └── default-role.ts # Papel padrão para funções de lógica
│ └── default-role.ts # Default role for logic functions
├── logic-functions/
── hello-world.ts # Exemplo de função de lógica
── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
└── front-components/
└── hello-world.tsx # Exemplo de componente de front-end
└── hello-world.tsx # Example front component
```
Em alto nível:
@@ -290,6 +294,7 @@ Todo aplicativo tem um único arquivo `application-config.ts` que descreve:
* **O que é o aplicativo**: identificadores, nome de exibição e descrição.
* **Como suas funções são executadas**: qual papel usam para permissões.
* **Variáveis (opcional)**: pares chavevalor expostos às suas funções como variáveis de ambiente.
* **(Optional) post-install function**: a logic function that runs after the app is installed.
Use `defineApplication()` to define your application configuration:
@@ -297,6 +302,7 @@ Use `defineApplication()` to define your application configuration:
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -312,6 +318,7 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -320,6 +327,7 @@ Notas:
* `universalIdentifier` são IDs determinísticos que você controla; gere-os uma vez e mantenha-os estáveis entre sincronizações.
* `applicationVariables` tornam-se variáveis de ambiente para suas funções (por exemplo, `DEFAULT_RECIPIENT_NAME` fica disponível como `process.env.DEFAULT_RECIPIENT_NAME`).
* `defaultRoleUniversalIdentifier` deve corresponder ao arquivo do papel (veja abaixo).
* `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#post-install-functions).
#### Papéis e permissões
@@ -458,6 +466,55 @@ Notas:
* O array `triggers` é opcional. Funções sem gatilhos podem ser usadas como funções utilitárias chamadas por outras funções.
* Você pode misturar vários tipos de gatilho em uma única função.
### Post-install functions
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
The function is wired into your app by referencing its universal identifier in `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
You can also manually execute the post-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
Pontos-chave:
* Post-install functions are standard logic functions — they use `defineLogicFunction()` like any other function.
* The `postInstallLogicFunctionUniversalIdentifier` field in `defineApplication()` is optional. If omitted, no function runs after installation.
* The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
* Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
### Payload de gatilho de rota
<Warning>
@@ -54,6 +54,9 @@ yarn twenty function:logs
# Execută o funcție după nume
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Execută funcția post-instalare
yarn twenty function:execute --postInstall
# Dezinstalează aplicația din spațiul de lucru curent
yarn twenty app:uninstall
@@ -70,7 +73,7 @@ Când rulați `npx create-twenty-app@latest my-twenty-app`, generatorul:
* Copiază o aplicație de bază minimală în `my-twenty-app/`
* Adaugă o dependență locală `twenty-sdk` și configurația Yarn 4
* Creează fișiere de configurare și scripturi conectate la CLI-ul `twenty`
* Generează o configurație implicită a aplicației și un rol implicit pentru funcții
* Generează o configurație implicită a aplicației, un rol implicit pentru funcții și o funcție post-instalare
O aplicație nou generată arată astfel:
@@ -86,15 +89,16 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # Public assets folder (images, fonts, etc.)
public/ # Director pentru resurse publice (imagini, fonturi etc.)
src/
├── application-config.ts # Required - main application configuration
├── application-config.ts # Obligatoriu - configurația principală a aplicației
├── roles/
│ └── default-role.ts # Default role for logic functions
│ └── default-role.ts # Rol implicit pentru funcțiile logice
├── logic-functions/
── hello-world.ts # Example logic function
── hello-world.ts # Exemplu de funcție logică
│ └── post-install.ts # Funcție logică post-instalare
└── front-components/
└── hello-world.tsx # Example front component
└── hello-world.tsx # Exemplu de componentă de interfață
```
Pe scurt:
@@ -290,6 +294,7 @@ Fiecare aplicație are un singur fișier `application-config.ts` care descrie:
* **Cine este aplicația**: identificatori, nume de afișare și descriere.
* **Cum rulează funcțiile**: ce rol folosesc pentru permisiuni.
* **(Opțional) variabile**: perechi cheievaloare expuse funcțiilor ca variabile de mediu.
* **(Opțional) funcție post-instalare**: o funcție logică care rulează după instalarea aplicației.
Folosiți `defineApplication()` pentru a defini configurația aplicației:
@@ -297,6 +302,7 @@ Folosiți `defineApplication()` pentru a defini configurația aplicației:
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -312,6 +318,7 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -320,6 +327,7 @@ Notițe:
* Câmpurile `universalIdentifier` sunt ID-uri deterministe pe care le dețineți; generați-le o singură dată și păstrați-le stabile între sincronizări.
* `applicationVariables` devin variabile de mediu pentru funcțiile dvs. (de exemplu, `DEFAULT_RECIPIENT_NAME` este disponibil ca `process.env.DEFAULT_RECIPIENT_NAME`).
* `defaultRoleUniversalIdentifier` trebuie să corespundă fișierului de rol (vedeți mai jos).
* `postInstallLogicFunctionUniversalIdentifier` (opțional) indică o funcție logică care rulează automat după instalarea aplicației. Vezi [Funcții post-instalare](#post-install-functions).
#### Roluri și permisiuni
@@ -458,6 +466,55 @@ Notițe:
* Matricea `triggers` este opțională. Funcțiile fără declanșatoare pot fi folosite ca funcții utilitare apelate de alte funcții.
* Puteți combina mai multe tipuri de declanșatoare într-o singură funcție.
### Funcții post-instalare
O funcție post-instalare este o funcție logică care rulează automat după instalarea aplicației într-un spațiu de lucru. Aceasta este utilă pentru sarcini de configurare unice, cum ar fi popularea cu date implicite, crearea înregistrărilor inițiale sau configurarea setărilor spațiului de lucru.
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
The function is wired into your app by referencing its universal identifier in `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
You can also manually execute the post-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
Puncte cheie:
* Post-install functions are standard logic functions — they use `defineLogicFunction()` like any other function.
* The `postInstallLogicFunctionUniversalIdentifier` field in `defineApplication()` is optional. If omitted, no function runs after installation.
* The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
* Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
### Payload-ul declanșatorului de rută
<Warning>
@@ -45,19 +45,22 @@ yarn twenty app:dev
Отсюда вы можете:
```bash filename="Terminal"
# Добавить новую сущность в ваше приложение (с мастером)
# Add a new entity to your application (guided)
yarn twenty entity:add
# Просматривать логи функций вашего приложения
# Watch your application's function logs
yarn twenty function:logs
# Выполнить функцию по имени
yarn twenty function:execute -n my-function -p '{\"name\": \"test\"}'
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Удалить приложение из текущего рабочего пространства
# Execute the post-install function
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn twenty app:uninstall
# Показать справку по командам
# Display commands' help
yarn twenty help
```
@@ -70,7 +73,7 @@ yarn twenty help
* Копирует минимальное базовое приложение в `my-twenty-app/`
* Добавляет локальную зависимость `twenty-sdk` и конфигурацию Yarn 4
* Создаёт файлы конфигурации и скрипты, подключённые к CLI `twenty`
* Генерирует конфигурацию приложения по умолчанию и роль функции по умолчанию
* Generates a default application config, a default function role, and a post-install function
Свежесгенерированное приложение выглядит так:
@@ -86,15 +89,16 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # Папка общедоступных ресурсов (изображения, шрифты и т. п.)
public/ # Public assets folder (images, fonts, etc.)
src/
├── application-config.ts # Обязательный — основная конфигурация приложения
├── application-config.ts # Required - main application configuration
├── roles/
│ └── default-role.ts # Роль по умолчанию для логических функций
│ └── default-role.ts # Default role for logic functions
├── logic-functions/
── hello-world.ts # Пример логической функции
── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
└── front-components/
└── hello-world.tsx # Пример фронтенд-компонента
└── hello-world.tsx # Example front component
```
В общих чертах:
@@ -290,6 +294,7 @@ export default defineObject({
* **Что это за приложение**: идентификаторы, отображаемое имя и описание.
* **Как запускаются его функции**: какую роль они используют для прав доступа.
* **(Необязательно) переменные**: пары ключ-значение, предоставляемые вашим функциям как переменные окружения.
* **(Optional) post-install function**: a logic function that runs after the app is installed.
Используйте `defineApplication()` для определения конфигурации вашего приложения:
@@ -297,6 +302,7 @@ export default defineObject({
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -312,6 +318,7 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -320,6 +327,7 @@ export default defineApplication({
* `universalIdentifier` — это детерминированные идентификаторы, которыми вы управляете; сгенерируйте их один раз и сохраняйте стабильными между синхронизациями.
* `applicationVariables` становятся переменными окружения для ваших функций (например, `DEFAULT_RECIPIENT_NAME` доступна как `process.env.DEFAULT_RECIPIENT_NAME`).
* `defaultRoleUniversalIdentifier` должен соответствовать файлу роли (см. ниже).
* `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#post-install-functions).
#### Роли и разрешения
@@ -458,6 +466,55 @@ export default defineLogicFunction({
* Массив `triggers` необязателен. Функции без триггеров можно использовать как вспомогательные, вызываемые другими функциями.
* Вы можете сочетать несколько типов триггеров в одной функции.
### Post-install functions
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
The function is wired into your app by referencing its universal identifier in `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
You can also manually execute the post-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
Основные моменты:
* Post-install functions are standard logic functions — they use `defineLogicFunction()` like any other function.
* The `postInstallLogicFunctionUniversalIdentifier` field in `defineApplication()` is optional. If omitted, no function runs after installation.
* The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
* Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
### Полезная нагрузка триггера маршрута
<Warning>
@@ -54,6 +54,9 @@ yarn twenty function:logs
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Execute the post-install function
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn twenty app:uninstall
@@ -70,7 +73,7 @@ Ayrıca bkz.: [create-twenty-app](https://www.npmjs.com/package/create-twenty-ap
* Minimal bir temel uygulamayı `my-twenty-app/` içine kopyalar
* Yerel bir `twenty-sdk` bağımlılığı ve Yarn 4 yapılandırması ekler
* `twenty` CLI ile bağlantılı yapılandırma dosyaları ve betikler oluşturur
* Varsayılan bir uygulama yapılandırması ve varsayılan bir fonksiyon rolü üretir
* Generates a default application config, a default function role, and a post-install function
Yeni şablondan oluşturulan bir uygulama şöyle görünür:
@@ -86,15 +89,16 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # Genel varlıklar klasörü (görseller, yazı tipleri vb.)
public/ # Public assets folder (images, fonts, etc.)
src/
├── application-config.ts # Gerekli - ana uygulama yapılandırması
├── application-config.ts # Required - main application configuration
├── roles/
│ └── default-role.ts # Mantık işlevleri için varsayılan rol
│ └── default-role.ts # Default role for logic functions
├── logic-functions/
── hello-world.ts # Örnek mantık işlevi
── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
└── front-components/
└── hello-world.tsx # Örnek ön uç bileşeni
└── hello-world.tsx # Example front component
```
Genel hatlarıyla:
@@ -290,6 +294,7 @@ Her uygulamanın aşağıdakileri açıklayan tek bir `application-config.ts` do
* **Uygulamanın kim olduğu**: tanımlayıcılar, görünen ad ve açıklama.
* **Fonksiyonlarının nasıl çalıştığı**: izinler için hangi rolü kullandıkları.
* **(İsteğe bağlı) değişkenler**: fonksiyonlarınıza ortam değişkenleri olarak sunulan anahtardeğer çiftleri.
* **(Optional) post-install function**: a logic function that runs after the app is installed.
Uygulama yapılandırmanızı tanımlamak için `defineApplication()` kullanın:
@@ -297,6 +302,7 @@ Uygulama yapılandırmanızı tanımlamak için `defineApplication()` kullanın:
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -312,6 +318,7 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -320,6 +327,7 @@ Notlar:
* `universalIdentifier` alanları size ait belirleyici kimliklerdir; bunları bir kez oluşturun ve eşitlemeler boyunca kararlı tutun.
* `applicationVariables`, fonksiyonlarınız için ortam değişkenlerine dönüşür (örneğin, `DEFAULT_RECIPIENT_NAME` değeri `process.env.DEFAULT_RECIPIENT_NAME` olarak kullanılabilir).
* `defaultRoleUniversalIdentifier`, rol dosyasıyla eşleşmelidir (aşağıya bakın).
* `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#post-install-functions).
#### Roller ve izinler
@@ -458,6 +466,55 @@ Notlar:
* `triggers` dizisi isteğe bağlıdır. Tetikleyicisi olmayan fonksiyonlar, diğer fonksiyonlar tarafından çağrılan yardımcı fonksiyonlar olarak kullanılabilir.
* Tek bir fonksiyonda birden çok tetikleyici türünü birleştirebilirsiniz.
### Post-install functions
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
The function is wired into your app by referencing its universal identifier in `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
You can also manually execute the post-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
Önemli noktalar:
* Post-install functions are standard logic functions — they use `defineLogicFunction()` like any other function.
* The `postInstallLogicFunctionUniversalIdentifier` field in `defineApplication()` is optional. If omitted, no function runs after installation.
* The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
* Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
### Rota tetikleyicisi yükü
<Warning>
@@ -45,19 +45,22 @@ yarn twenty app:dev
从这里您可以:
```bash filename="Terminal"
# 向你的应用添加一个新实体(引导式)
# Add a new entity to your application (guided)
yarn twenty entity:add
# 监听你的应用函数日志
# Watch your application's function logs
yarn twenty function:logs
# 按名称执行一个函数
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# 从当前工作区卸载该应用
# Execute the post-install function
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn twenty app:uninstall
# 显示命令帮助
# Display commands' help
yarn twenty help
```
@@ -70,7 +73,7 @@ yarn twenty help
* 将一个最小的基础应用复制到 `my-twenty-app/` 中
* 添加本地 `twenty-sdk` 依赖和 Yarn 4 配置
* 创建与 `twenty` CLI 关联的配置文件和脚本
* 生成默认的应用配置和默认的函数角色
* Generates a default application config, a default function role, and a post-install function
一个新生成的脚手架应用如下所示:
@@ -86,15 +89,16 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # 公共资源文件夹(图像、字体等)
public/ # Public assets folder (images, fonts, etc.)
src/
├── application-config.ts # 必需 - 主应用程序配置
├── application-config.ts # Required - main application configuration
├── roles/
│ └── default-role.ts # 用于逻辑函数的默认角色
│ └── default-role.ts # Default role for logic functions
├── logic-functions/
── hello-world.ts # 示例逻辑函数
── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
└── front-components/
└── hello-world.tsx # 示例前端组件
└── hello-world.tsx # Example front component
```
总体来说:
@@ -290,6 +294,7 @@ export default defineObject({
* **应用的身份**:标识符、显示名称和描述。
* **函数如何运行**:它们用于权限的角色。
* **(可选)变量**:以环境变量形式提供给函数的键值对。
* **(Optional) post-install function**: a logic function that runs after the app is installed.
使用 `defineApplication()` 定义你的应用配置:
@@ -297,6 +302,7 @@ export default defineObject({
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -312,6 +318,7 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -320,6 +327,7 @@ export default defineApplication({
* `universalIdentifier` 字段是你拥有的确定性 ID;生成一次并在多次同步中保持稳定。
* `applicationVariables` 会变成函数可用的环境变量(例如,`DEFAULT_RECIPIENT_NAME` 可作为 `process.env.DEFAULT_RECIPIENT_NAME` 使用)。
* `defaultRoleUniversalIdentifier` 必须与角色文件一致(见下文)。
* `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#post-install-functions).
#### 角色和权限
@@ -458,6 +466,55 @@ export default defineLogicFunction({
* `triggers` 数组是可选的。 没有触发器的函数可作为实用函数,被其他函数调用。
* 你可以在单个函数中混用多种触发器类型。
### Post-install functions
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
The function is wired into your app by referencing its universal identifier in `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
You can also manually execute the post-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
关键点:
* Post-install functions are standard logic functions — they use `defineLogicFunction()` like any other function.
* The `postInstallLogicFunctionUniversalIdentifier` field in `defineApplication()` is optional. If omitted, no function runs after installation.
* The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
* Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
### 路由触发器负载
<Warning>
@@ -1448,13 +1448,8 @@ export enum EventLogTable {
export type EventSubscription = {
__typename?: 'EventSubscription';
eventStreamId: Scalars['String'];
eventWithQueryIdsList: Array<EventWithQueryIds>;
};
export type EventWithQueryIds = {
__typename?: 'EventWithQueryIds';
event: ObjectRecordEvent;
queryIds: Array<Scalars['String']>;
metadataEventsWithQueryIds: Array<MetadataEventWithQueryIds>;
objectRecordEventsWithQueryIds: Array<ObjectRecordEventWithQueryIds>;
};
export type ExecuteOneLogicFunctionInput = {
@@ -1485,6 +1480,7 @@ export enum FeatureFlagKey {
IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED',
IS_CORE_PICTURE_MIGRATED = 'IS_CORE_PICTURE_MIGRATED',
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED = 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED',
IS_DRAFT_EMAIL_ENABLED = 'IS_DRAFT_EMAIL_ENABLED',
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
IS_FILES_FIELD_MIGRATED = 'IS_FILES_FIELD_MIGRATED',
@@ -2138,6 +2134,27 @@ export type MarketplaceAppRoleObjectPermission = {
objectUniversalIdentifier: Scalars['String'];
};
export type MetadataEvent = {
__typename?: 'MetadataEvent';
metadataName: Scalars['String'];
properties: ObjectRecordEventProperties;
recordId: Scalars['String'];
type: MetadataEventAction;
};
/** Metadata Event Action */
export enum MetadataEventAction {
CREATED = 'CREATED',
DELETED = 'DELETED',
UPDATED = 'UPDATED'
}
export type MetadataEventWithQueryIds = {
__typename?: 'MetadataEventWithQueryIds';
metadataEvent: MetadataEvent;
queryIds: Array<Scalars['String']>;
};
export enum ModelProvider {
ANTHROPIC = 'ANTHROPIC',
GROQ = 'GROQ',
@@ -3379,6 +3396,12 @@ export type ObjectRecordEventProperties = {
updatedFields?: Maybe<Array<Scalars['String']>>;
};
export type ObjectRecordEventWithQueryIds = {
__typename?: 'ObjectRecordEventWithQueryIds';
objectRecordEvent: ObjectRecordEvent;
queryIds: Array<Scalars['String']>;
};
/** Date granularity options (e.g. DAY, MONTH, QUARTER, YEAR, WEEK, DAY_OF_THE_WEEK, MONTH_OF_THE_YEAR, QUARTER_OF_THE_YEAR) */
export enum ObjectRecordGroupByDateGranularity {
DAY = 'DAY',
@@ -5781,7 +5804,7 @@ export type FindOneFrontComponentQueryVariables = Exact<{
}>;
export type FindOneFrontComponentQuery = { __typename?: 'Query', frontComponent?: { __typename?: 'FrontComponent', id: string, name: string, applicationId: string, applicationTokenPair?: { __typename?: 'ApplicationTokenPair', applicationAccessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, applicationRefreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } | null } | null };
export type FindOneFrontComponentQuery = { __typename?: 'Query', frontComponent?: { __typename?: 'FrontComponent', id: string, name: string, applicationId: string, builtComponentChecksum: string, applicationTokenPair?: { __typename?: 'ApplicationTokenPair', applicationAccessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, applicationRefreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } | null } | null };
export type LogicFunctionFieldsFragment = { __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, sourceHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string };
@@ -10569,6 +10592,7 @@ export const FindOneFrontComponentDocument = gql`
id
name
applicationId
builtComponentChecksum
applicationTokenPair {
applicationAccessToken {
token
@@ -0,0 +1,2 @@
export const METADATA_OPERATION_BROWSER_EVENT_NAME =
'metadata-operation-browser-event';
@@ -0,0 +1,53 @@
import { METADATA_OPERATION_BROWSER_EVENT_NAME } from '@/browser-event/constants/MetadataOperationBrowserEventName';
import { type MetadataOperation } from '@/browser-event/types/MetadataOperation';
import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail';
import { useEffect } from 'react';
import { type AllMetadataName } from 'twenty-shared/metadata';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
export const useListenToMetadataOperationBrowserEvent = <
T extends Record<string, unknown>,
>({
onMetadataOperationBrowserEvent,
metadataName,
operationTypes,
}: {
onMetadataOperationBrowserEvent: (
detail: MetadataOperationBrowserEventDetail<T>,
) => void;
metadataName?: AllMetadataName;
operationTypes?: MetadataOperation<T>['type'][];
}) => {
useEffect(() => {
const handleMetadataOperationEvent = (
event: CustomEvent<MetadataOperationBrowserEventDetail<T>>,
) => {
const detail = event.detail;
if (isDefined(metadataName) && detail.metadataName !== metadataName) {
return;
}
if (
isNonEmptyArray(operationTypes) &&
!operationTypes.includes(detail.operation.type)
) {
return;
}
onMetadataOperationBrowserEvent(detail);
};
window.addEventListener(
METADATA_OPERATION_BROWSER_EVENT_NAME,
handleMetadataOperationEvent as EventListener,
);
return () => {
window.removeEventListener(
METADATA_OPERATION_BROWSER_EVENT_NAME,
handleMetadataOperationEvent as EventListener,
);
};
}, [metadataName, onMetadataOperationBrowserEvent, operationTypes]);
};
@@ -1,6 +1,6 @@
import { OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME } from '@/object-record/constants/ObjectRecordOperationBrowserEventName';
import { OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME } from '@/browser-event/constants/ObjectRecordOperationBrowserEventName';
import { type ObjectRecordOperation } from '@/object-record/types/ObjectRecordOperation';
import { type ObjectRecordOperationBrowserEventDetail } from '@/object-record/types/ObjectRecordOperationBrowserEventDetail';
import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
import { useEffect } from 'react';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
@@ -0,0 +1,14 @@
export type MetadataOperation<T extends Record<string, unknown>> =
| {
type: 'create';
createdRecord: T;
}
| {
type: 'update';
updatedRecord: T;
updatedFields?: string[];
}
| {
type: 'delete';
deletedRecordId: string;
};
@@ -0,0 +1,9 @@
import { type MetadataOperation } from '@/browser-event/types/MetadataOperation';
import { type AllMetadataName } from 'twenty-shared/metadata';
export type MetadataOperationBrowserEventDetail<
T extends Record<string, unknown>,
> = {
metadataName: AllMetadataName;
operation: MetadataOperation<T>;
};
@@ -0,0 +1,14 @@
import { METADATA_OPERATION_BROWSER_EVENT_NAME } from '@/browser-event/constants/MetadataOperationBrowserEventName';
import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail';
export const dispatchMetadataOperationBrowserEvent = <
T extends Record<string, unknown>,
>(
detail: MetadataOperationBrowserEventDetail<T>,
) => {
window.dispatchEvent(
new CustomEvent(METADATA_OPERATION_BROWSER_EVENT_NAME, {
detail,
}),
);
};
@@ -1,5 +1,5 @@
import { OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME } from '@/object-record/constants/ObjectRecordOperationBrowserEventName';
import { type ObjectRecordOperationBrowserEventDetail } from '@/object-record/types/ObjectRecordOperationBrowserEventDetail';
import { OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME } from '@/browser-event/constants/ObjectRecordOperationBrowserEventName';
import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
export const dispatchObjectRecordOperationBrowserEvent = (
detail: ObjectRecordOperationBrowserEventDetail,
@@ -1,5 +1,6 @@
import { REST_API_BASE_URL } from '@/apollo/constant/rest-api-base-url';
import { useFrontComponentExecutionContext } from '@/front-components/hooks/useFrontComponentExecutionContext';
import { useOnFrontComponentUpdated } from '@/front-components/hooks/useOnFrontComponentUpdated';
import { getFrontComponentUrl } from '@/front-components/utils/getFrontComponentUrl';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useTheme } from '@emotion/react';
import { t } from '@lingui/core/macro';
@@ -21,8 +22,6 @@ export const FrontComponentRenderer = ({
const { executionContext, frontComponentHostCommunicationApi } =
useFrontComponentExecutionContext();
const componentUrl = `${REST_API_BASE_URL}/front-components/${frontComponentId}`;
const handleError = useCallback(
(error?: Error) => {
if (!isDefined(error)) {
@@ -43,6 +42,15 @@ export const FrontComponentRenderer = ({
onError: handleError,
});
useOnFrontComponentUpdated({
frontComponentId,
});
const componentUrl = getFrontComponentUrl({
frontComponentId,
checksum: data?.frontComponent?.builtComponentChecksum,
});
if (
loading ||
!isDefined(data?.frontComponent) ||
@@ -6,6 +6,7 @@ export const FIND_ONE_FRONT_COMPONENT = gql`
id
name
applicationId
builtComponentChecksum
applicationTokenPair {
applicationAccessToken {
token
@@ -0,0 +1,37 @@
import { useUpdateFrontComponentApolloCache } from '@/front-components/hooks/useUpdateFrontComponentApolloCache';
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import {
AllMetadataName,
type FrontComponent,
} from '~/generated-metadata/graphql';
type UseOnFrontComponentUpdatedArgs = {
frontComponentId: string;
};
export const useOnFrontComponentUpdated = ({
frontComponentId,
}: UseOnFrontComponentUpdatedArgs) => {
const queryId = `front-component-updated-${frontComponentId}`;
useListenToEventsForQuery({
queryId,
operationSignature: {
metadataName: AllMetadataName.frontComponent,
variables: {
filter: { id: { eq: frontComponentId } },
},
},
});
const { updateFrontComponentApolloCache } =
useUpdateFrontComponentApolloCache({
frontComponentId,
});
useListenToMetadataOperationBrowserEvent<FrontComponent>({
metadataName: AllMetadataName.frontComponent,
onMetadataOperationBrowserEvent: updateFrontComponentApolloCache,
});
};
@@ -0,0 +1,54 @@
import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail';
import { useApolloClient } from '@apollo/client';
import { isDefined } from 'twenty-shared/utils';
import {
FindOneFrontComponentDocument,
type FindOneFrontComponentQuery,
type FrontComponent,
} from '~/generated-metadata/graphql';
type UseUpdateFrontComponentApolloCacheArgs = {
frontComponentId: string;
};
export const useUpdateFrontComponentApolloCache = ({
frontComponentId,
}: UseUpdateFrontComponentApolloCacheArgs) => {
const apolloClient = useApolloClient();
const updateFrontComponentApolloCache = (
detail: MetadataOperationBrowserEventDetail<FrontComponent>,
) => {
if (detail.operation.type !== 'update') {
return;
}
const { updatedRecord } = detail.operation;
if (!isDefined(updatedRecord) || updatedRecord.id !== frontComponentId) {
return;
}
apolloClient.cache.updateQuery<FindOneFrontComponentQuery>(
{
query: FindOneFrontComponentDocument,
variables: { id: frontComponentId },
},
(existingData) => {
if (!isDefined(existingData?.frontComponent)) {
return existingData;
}
return {
...existingData,
frontComponent: {
...existingData.frontComponent,
...updatedRecord,
},
};
},
);
};
return { updateFrontComponentApolloCache };
};
@@ -0,0 +1,14 @@
import { REST_API_BASE_URL } from '@/apollo/constant/rest-api-base-url';
import { isDefined } from 'twenty-shared/utils';
export const getFrontComponentUrl = ({
frontComponentId,
checksum,
}: {
frontComponentId: string;
checksum?: string;
}): string => {
return isDefined(checksum)
? `${REST_API_BASE_URL}/front-components/${frontComponentId}?checksum=${checksum}`
: `${REST_API_BASE_URL}/front-components/${frontComponentId}`;
};
@@ -19,6 +19,7 @@ import {
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { stringifyRelativeDateFilter } from '@/views/view-filter-value/utils/stringifyRelativeDateFilter';
import { WORKFLOW_TIMEZONE } from '@/workflow/constants/WorkflowTimeZone';
import { isObject, isString } from '@sniptt/guards';
@@ -62,6 +63,10 @@ export const AdvancedFilterCommandMenuValueFormInput = ({
const { applyObjectFilterDropdownFilterValue } =
useApplyObjectFilterDropdownFilterValue();
const featureFlags = useFeatureFlagsMap();
const isWholeDayFilterEnabled =
featureFlags.IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED ?? false;
const handleChange = (newValue: JsonValue) => {
if (isString(newValue)) {
applyObjectFilterDropdownFilterValue(newValue);
@@ -178,7 +183,12 @@ export const AdvancedFilterCommandMenuValueFormInput = ({
}
const field = {
type: recordFilter.type as FieldMetadataType,
type:
isWholeDayFilterEnabled === true &&
recordFilter.type === FieldMetadataType.DATE_TIME &&
recordFilter.operand === RecordFilterOperand.IS
? FieldMetadataType.DATE
: (recordFilter.type as FieldMetadataType),
label: '',
metadata: fieldDefinition?.metadata as FieldMetadata,
};
@@ -2,11 +2,11 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { useIncrementalFetchAndMutateRecords } from '@/object-record/hooks/useIncrementalFetchAndMutateRecords';
import { useIncrementalUpdateManyRecords } from '@/object-record/hooks/useIncrementalUpdateManyRecords';
import { useUpdateManyRecords } from '@/object-record/hooks/useUpdateManyRecords';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { renderHook } from '@testing-library/react';
jest.mock('@/object-metadata/hooks/useObjectMetadataItem');
jest.mock('@/object-record/utils/dispatchObjectRecordOperationBrowserEvent');
jest.mock('@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent');
jest.mock('@/object-record/hooks/useUpdateManyRecords', () => ({
useUpdateManyRecords: jest.fn(),
}));
@@ -7,7 +7,7 @@ import {
} from '@/object-record/hooks/useCreateManyRecords';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { t } from '@lingui/core/macro';
@@ -19,7 +19,7 @@ import { type FieldActorForInputValue } from '@/object-record/record-field/ui/ty
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { getCreateManyRecordsMutationResponseField } from '@/object-record/utils/getCreateManyRecordsMutationResponseField';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
import { useRecoilValue } from 'recoil';
@@ -26,7 +26,7 @@ import {
import { type BaseObjectRecord } from '@/object-record/types/BaseObjectRecord';
import { computeOptimisticCreateRecordBaseRecordInput } from '@/object-record/utils/computeOptimisticCreateRecordBaseRecordInput';
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { getCreateOneRecordMutationResponseField } from '@/object-record/utils/getCreateOneRecordMutationResponseField';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
import { useRecoilValue } from 'recoil';
@@ -15,7 +15,7 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilValue } from 'recoil';
@@ -14,7 +14,7 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField';
import { isNull } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
@@ -11,7 +11,7 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField';
import { useRemoveNavigationMenuItemByTargetRecordId } from '@/navigation-menu-item/hooks/useRemoveNavigationMenuItemByTargetRecordId';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
@@ -9,7 +9,7 @@ import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordF
import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { getDestroyOneRecordMutationResponseField } from '@/object-record/utils/getDestroyOneRecordMutationResponseField';
import { capitalize, isDefined } from 'twenty-shared/utils';
@@ -16,7 +16,7 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
@@ -12,7 +12,7 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { sleep } from '~/utils/sleep';
@@ -5,7 +5,7 @@ import { useIncrementalFetchAndMutateRecords } from '@/object-record/hooks/useIn
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { useUpdateManyRecords } from '@/object-record/hooks/useUpdateManyRecords';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { getUpdatedFieldsFromRecordInput } from '@/object-record/utils/getUpdatedFieldsFromRecordInput';
const DEFAULT_DELAY_BETWEEN_MUTATIONS_MS = 50;
@@ -8,7 +8,7 @@ import { useFindOneRecordQuery } from '@/object-record/hooks/useFindOneRecordQue
import { useMergeManyRecordsMutation } from '@/object-record/hooks/useMergeManyRecordsMutation';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { getMergeManyRecordsMutationResponseField } from '@/object-record/utils/getMergeManyRecordsMutationResponseField';
import { getOperationName } from '@apollo/client/utilities';
import { type RecordGqlOperationGqlRecordFields } from 'twenty-shared/types';
@@ -12,7 +12,7 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
import { useRestoreManyRecordsMutation } from '@/object-record/hooks/useRestoreManyRecordsMutation';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { getRestoreManyRecordsMutationResponseField } from '@/object-record/utils/getRestoreManyRecordsMutationResponseField';
import { useRecoilValue } from 'recoil';
import { capitalize, isDefined } from 'twenty-shared/utils';
@@ -16,7 +16,7 @@ import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggr
import { useUpdateManyRecordsMutation } from '@/object-record/hooks/useUpdateManyRecordsMutation';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { getUpdatedFieldsFromRecordInput } from '@/object-record/utils/getUpdatedFieldsFromRecordInput';
import { getUpdateManyRecordsMutationResponseField } from '@/object-record/utils/getUpdateManyRecordsMutationResponseField';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
@@ -16,7 +16,7 @@ import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggr
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { getUpdatedFieldsFromRecordInput } from '@/object-record/utils/getUpdatedFieldsFromRecordInput';
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
@@ -38,7 +38,12 @@ export const ObjectFilterDropdownDateInput = () => {
useApplyObjectFilterDropdownFilterValue();
const handleAbsoluteDateChange = (newPlainDate: string | null) => {
const newFilterValue = newPlainDate ?? '';
if (!isDefined(newPlainDate)) {
applyObjectFilterDropdownFilterValue('', '');
return;
}
const newFilterValue = newPlainDate;
// TODO: remove this and use getDisplayValue instead
const formattedDate = formatDateString({
@@ -91,6 +96,7 @@ export const ObjectFilterDropdownDateInput = () => {
? handleRelativeDateChange(null)
: handleAbsoluteDateChange(null);
};
const resolvedValue = objectFilterDropdownCurrentRecordFilter
? resolveDateFilter(objectFilterDropdownCurrentRecordFilter)
: null;
@@ -100,7 +106,7 @@ export const ObjectFilterDropdownDateInput = () => {
? resolvedValue
: undefined;
const plainDateValue =
const safePlainDateValue: string | undefined =
resolvedValue && typeof resolvedValue === 'string'
? resolvedValue
: undefined;
@@ -110,7 +116,7 @@ export const ObjectFilterDropdownDateInput = () => {
instanceId={`object-filter-dropdown-date-input`}
relativeDate={relativeDate}
isRelative={isRelativeOperand}
plainDateString={plainDateValue ?? null}
plainDateString={safePlainDateValue ?? null}
onChange={handleAbsoluteDateChange}
onRelativeDateChange={handleRelativeDateChange}
onClear={handleClear}
@@ -5,6 +5,7 @@ import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-d
import { ObjectFilterDropdownRecordSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect';
import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { ViewFilterOperand } from 'twenty-shared/types';
@@ -28,6 +29,10 @@ export const ObjectFilterDropdownFilterInput = ({
filterDropdownId,
recordFilterId,
}: ObjectFilterDropdownFilterInputProps) => {
const featureFlags = useFeatureFlagsMap();
const isWholeDayFilterEnabled =
featureFlags.IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED ?? false;
const fieldMetadataItemUsedInDropdown = useRecoilComponentValue(
fieldMetadataItemUsedInDropdownComponentSelector,
);
@@ -76,6 +81,18 @@ export const ObjectFilterDropdownFilterInput = ({
</>
);
} else if (filterType === 'DATE_TIME') {
if (
isWholeDayFilterEnabled &&
selectedOperandInDropdown === ViewFilterOperand.IS
) {
return (
<>
<ObjectFilterDropdownInnerSelectOperandDropdown />
<DropdownMenuSeparator />
<ObjectFilterDropdownDateInput />
</>
);
}
return (
<>
<ObjectFilterDropdownInnerSelectOperandDropdown />
@@ -1,3 +1,6 @@
import { isNonEmptyString } from '@sniptt/guards';
import { Temporal } from 'temporal-polyfill';
import { useUpsertObjectFilterDropdownCurrentFilter } from '@/object-record/object-filter-dropdown/hooks/useUpsertObjectFilterDropdownCurrentFilter';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
@@ -7,14 +10,12 @@ import { useGetRelativeDateFilterWithUserTimezone } from '@/object-record/record
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { stringifyRelativeDateFilter } from '@/views/view-filter-value/utils/stringifyRelativeDateFilter';
import { isNonEmptyString } from '@sniptt/guards';
import { Temporal } from 'temporal-polyfill';
import { DEFAULT_RELATIVE_DATE_FILTER_VALUE } from 'twenty-shared/constants';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { DEFAULT_RELATIVE_DATE_FILTER_VALUE } from 'twenty-shared/constants';
import {
isDefined,
relativeDateFilterStringifiedSchema,
@@ -47,6 +48,10 @@ export const useApplyObjectFilterDropdownOperand = () => {
const { getRelativeDateFilterWithUserTimezone } =
useGetRelativeDateFilterWithUserTimezone();
const featureFlags = useFeatureFlagsMap();
const isWholeDayFilterEnabled =
featureFlags.IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED ?? false;
const applyObjectFilterDropdownOperand = (
newOperand: RecordFilterOperand,
) => {
@@ -106,7 +111,24 @@ export const useApplyObjectFilterDropdownOperand = () => {
recordFilterToUpsert.value,
);
if (filterValueIsEmpty || isStillRelativeFilterValue.success) {
const previousOperand =
objectFilterDropdownCurrentRecordFilter?.operand;
const isDateTimeOperandFormatChange =
recordFilterToUpsert.type === 'DATE_TIME' &&
!filterValueIsEmpty &&
!isStillRelativeFilterValue.success &&
(previousOperand === RecordFilterOperand.IS ||
newOperand === RecordFilterOperand.IS);
if (isDateTimeOperandFormatChange) {
recordFilterToUpsert.value = convertDateTimeFilterValue(
recordFilterToUpsert.value,
newOperand,
userTimezone,
isWholeDayFilterEnabled,
);
} else if (filterValueIsEmpty || isStillRelativeFilterValue.success) {
const zonedDateToUse = Temporal.Now.zonedDateTimeISO(userTimezone);
if (recordFilterToUpsert.type === 'DATE') {
@@ -116,11 +138,18 @@ export const useApplyObjectFilterDropdownOperand = () => {
recordFilterToUpsert.value = initialNowDateFilterValue;
} else {
const initialNowDateTimeFilterValue = zonedDateToUse
.toInstant()
.toString();
recordFilterToUpsert.value = initialNowDateTimeFilterValue;
if (
newOperand === RecordFilterOperand.IS &&
isWholeDayFilterEnabled
) {
recordFilterToUpsert.value = zonedDateToUse
.toPlainDate()
.toString();
} else {
recordFilterToUpsert.value = zonedDateToUse
.toInstant()
.toString();
}
}
}
}
@@ -137,3 +166,40 @@ export const useApplyObjectFilterDropdownOperand = () => {
applyObjectFilterDropdownOperand,
};
};
const convertDateTimeFilterValue = (
currentValue: string,
targetOperand: RecordFilterOperand,
userTimezone: string,
isWholeDayFilterEnabled = false,
): string => {
const zonedDateToUse = Temporal.Now.zonedDateTimeISO(userTimezone);
if (targetOperand === RecordFilterOperand.IS) {
try {
const existingZoned = currentValue.includes('T')
? Temporal.Instant.from(currentValue).toZonedDateTimeISO(userTimezone)
: Temporal.PlainDate.from(currentValue).toZonedDateTime(userTimezone);
if (isWholeDayFilterEnabled) {
return existingZoned.toPlainDate().toString();
} else {
return existingZoned.toInstant().toString();
}
} catch {
return zonedDateToUse.toPlainDate().toString();
}
} else {
try {
const existingPlainDate = Temporal.PlainDate.from(currentValue);
const currentTime = zonedDateToUse.toPlainTime();
const zonedFromPlain = existingPlainDate.toZonedDateTime({
timeZone: userTimezone,
plainTime: currentTime,
});
return zonedFromPlain.toInstant().toString();
} catch {
return zonedDateToUse.toInstant().toString();
}
}
};
@@ -1,9 +1,12 @@
import { Temporal } from 'temporal-polyfill';
import { useGetDateFilterDisplayValue } from '@/object-record/object-filter-dropdown/hooks/useGetDateFilterDisplayValue';
import { useGetDateTimeFilterDisplayValue } from '@/object-record/object-filter-dropdown/hooks/useGetDateTimeFilterDisplayValue';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { Temporal } from 'temporal-polyfill';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { type FilterableAndTSVectorFieldType } from 'twenty-shared/types';
const activeDatePickerOperands = [
@@ -16,6 +19,9 @@ export const useGetInitialFilterValue = () => {
const { userTimezone } = useUserTimezone();
const { getDateFilterDisplayValue } = useGetDateFilterDisplayValue();
const { getDateTimeFilterDisplayValue } = useGetDateTimeFilterDisplayValue();
const featureFlags = useFeatureFlagsMap();
const isWholeDayFilterEnabled =
featureFlags.IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED ?? false;
const getInitialFilterValue = (
newType: FilterableAndTSVectorFieldType,
@@ -44,6 +50,17 @@ export const useGetInitialFilterValue = () => {
alreadyExistingZonedDateTime ??
Temporal.Now.zonedDateTimeISO(userTimezone);
if (
isWholeDayFilterEnabled === true &&
newOperand === RecordFilterOperand.IS
) {
const value = referenceDate.toPlainDate().toString();
const { displayValue } = getDateFilterDisplayValue(referenceDate);
return { value, displayValue };
}
const value = referenceDate.toInstant().toString();
const { displayValue } = getDateTimeFilterDisplayValue(referenceDate);
@@ -1,4 +1,4 @@
import { useListenToObjectRecordOperationBrowserEvent } from '@/object-record/hooks/useListenToObjectRecordOperationBrowserEvent';
import { useListenToObjectRecordOperationBrowserEvent } from '@/browser-event/hooks/useListenToObjectRecordOperationBrowserEvent';
import { useGetShouldInitializeRecordBoardForUpdateInputs } from '@/object-record/record-board/hooks/useGetShouldInitializeRecordBoardForUpdateInputs';
import { useRemoveRecordsFromBoard } from '@/object-record/record-board/hooks/useRemoveRecordsFromBoard';
import { useTriggerRecordBoardInitialQuery } from '@/object-record/record-board/hooks/useTriggerRecordBoardInitialQuery';
@@ -7,7 +7,7 @@ import { useRecordIndexContextOrThrow } from '@/object-record/record-index/conte
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { type ObjectRecordOperationBrowserEventDetail } from '@/object-record/types/ObjectRecordOperationBrowserEventDetail';
import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { useRecoilCallback } from 'recoil';
@@ -3,7 +3,7 @@ import { useContext } from 'react';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useRecordIndexGroupCommonQueryVariables } from '@/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables';
import { useListenToObjectRecordEventsForQuery } from '@/sse-db-event/hooks/useListenToObjectRecordEventsForQuery';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
export const RecordBoardSSESubscribeEffect = () => {
const { recordBoardId } = useContext(RecordBoardContext);
@@ -13,7 +13,7 @@ export const RecordBoardSSESubscribeEffect = () => {
const queryId = `record-board-${recordBoardId}`;
useListenToObjectRecordEventsForQuery({
useListenToEventsForQuery({
queryId,
operationSignature: {
objectNameSingular: objectMetadataItem.nameSingular,
@@ -3,7 +3,7 @@ import { useRecordCalendarContextOrThrow } from '@/object-record/record-calendar
import { useRecordCalendarQueryDateRangeFilter } from '@/object-record/record-calendar/month/hooks/useRecordCalendarQueryDateRangeFilter';
import { RecordCalendarComponentInstanceContext } from '@/object-record/record-calendar/states/contexts/RecordCalendarComponentInstanceContext';
import { recordCalendarSelectedDateComponentState } from '@/object-record/record-calendar/states/recordCalendarSelectedDateComponentState';
import { useListenToObjectRecordEventsForQuery } from '@/sse-db-event/hooks/useListenToObjectRecordEventsForQuery';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { type RecordGqlOperationOrderBy } from 'twenty-shared/types';
@@ -32,7 +32,7 @@ export const RecordCalendarSSESubscribeEffect = () => {
const queryId = `record-calendar-${recordCalendarId}`;
useListenToObjectRecordEventsForQuery({
useListenToEventsForQuery({
queryId,
operationSignature: {
objectNameSingular: objectMetadataItem.nameSingular,
@@ -1,3 +1,6 @@
import { isNonEmptyString } from '@sniptt/guards';
import { Temporal } from 'temporal-polyfill';
import { useGetFieldMetadataItemByIdOrThrow } from '@/object-metadata/hooks/useGetFieldMetadataItemById';
import { useGetDateFilterDisplayValue } from '@/object-record/object-filter-dropdown/hooks/useGetDateFilterDisplayValue';
import { useGetDateTimeFilterDisplayValue } from '@/object-record/object-filter-dropdown/hooks/useGetDateTimeFilterDisplayValue';
@@ -7,8 +10,7 @@ import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordF
import { isRecordFilterConsideredEmpty } from '@/object-record/record-filter/utils/isRecordFilterConsideredEmpty';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { getTimezoneAbbreviationForZonedDateTime } from '@/ui/input/components/internal/date/utils/getTimeZoneAbbreviationForZonedDateTime';
import { isNonEmptyString } from '@sniptt/guards';
import { Temporal } from 'temporal-polyfill';
import { type Nullable } from 'twenty-shared/types';
import {
isDefined,
@@ -87,7 +89,23 @@ export const useGetRecordFilterDisplayValue = () => {
}
} else if (recordFilter.type === 'DATE_TIME') {
switch (recordFilter.operand) {
case RecordFilterOperand.IS:
case RecordFilterOperand.IS: {
if (!isNonEmptyString(recordFilter.value)) {
return '';
}
const zonedDateTime = recordFilter.value.includes('T')
? Temporal.Instant.from(recordFilter.value).toZonedDateTimeISO(
userTimezone,
)
: Temporal.PlainDate.from(recordFilter.value).toZonedDateTime(
userTimezone,
);
const { displayValue } = getDateFilterDisplayValue(zonedDateTime);
return `${displayValue}`;
}
case RecordFilterOperand.IS_AFTER:
case RecordFilterOperand.IS_BEFORE: {
if (!isNonEmptyString(recordFilter.value)) {
@@ -1024,9 +1024,9 @@ describe('should work as expected for the different field types', () => {
const dateFilterIs: RecordFilter = {
id: 'company-date-filter-is',
value: '2024-09-17T20:46:58.922Z',
value: '2024-09-17',
fieldMetadataId: companyMockDateFieldMetadataId?.id,
displayValue: '2024-09-17T20:46:58.922Z',
displayValue: '2024-09-17',
operand: ViewFilterOperand.IS,
label: 'Created At',
type: FieldMetadataType.DATE_TIME,
@@ -1081,12 +1081,12 @@ describe('should work as expected for the different field types', () => {
and: [
{
createdAt: {
lt: '2024-09-17T20:47:00Z',
gte: '2024-09-16T22:00:00Z',
},
},
{
createdAt: {
gte: '2024-09-17T20:46:00Z',
lt: '2024-09-17T22:00:00Z',
},
},
],
@@ -1,4 +1,4 @@
import { useListenToObjectRecordEventsForQuery } from '@/sse-db-event/hooks/useListenToObjectRecordEventsForQuery';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
type RecordShowPageSSESubscribeEffectProps = {
objectNameSingular: string;
@@ -11,7 +11,7 @@ export const RecordShowPageSSESubscribeEffect = ({
}: RecordShowPageSSESubscribeEffectProps) => {
const queryId = `record-show-${objectNameSingular}-${recordId}`;
useListenToObjectRecordEventsForQuery({
useListenToEventsForQuery({
queryId,
operationSignature: {
objectNameSingular,
@@ -1,10 +1,10 @@
import { useListenToObjectRecordOperationBrowserEvent } from '@/object-record/hooks/useListenToObjectRecordOperationBrowserEvent';
import { useListenToObjectRecordOperationBrowserEvent } from '@/browser-event/hooks/useListenToObjectRecordOperationBrowserEvent';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { SSE_TABLE_DEBOUNCE_TIME_IN_MS_TO_AVOID_SSE_OWN_EVENTS_RACE_CONDITION } from '@/object-record/record-table/virtualization/constants/SseTableDebounceTimeInMsToAvoidSseOwnEventsRaceCondition';
import { useGetShouldResetTableVirtualizationForUpdateInputs } from '@/object-record/record-table/virtualization/hooks/useGetShouldResetTableVirtualizationForUpdateInputs';
import { useResetVirtualizationBecauseDataChanged } from '@/object-record/record-table/virtualization/hooks/useResetVirtualizationBecauseDataChanged';
import { type ObjectRecordOperationBrowserEventDetail } from '@/object-record/types/ObjectRecordOperationBrowserEventDetail';
import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
import { useDebouncedCallback } from 'use-debounce';
export const RecordTableVirtualizedDataChangedEffect = () => {
@@ -4,7 +4,7 @@ import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
import { useListenToObjectRecordEventsForQuery } from '@/sse-db-event/hooks/useListenToObjectRecordEventsForQuery';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { computeRecordGqlOperationFilter } from 'twenty-shared/utils';
@@ -26,7 +26,7 @@ export const RecordTableVirtualizedSSESubscribeEffect = () => {
const queryId = `record-table-virtualized-${objectMetadataItem.nameSingular}`;
useListenToObjectRecordEventsForQuery({
useListenToEventsForQuery({
queryId,
operationSignature: {
objectNameSingular: objectMetadataItem.nameSingular,
@@ -37,11 +37,11 @@ export const SSEEventStreamEffect = () => {
const { triggerEventStreamDestroy } = useTriggerEventStreamDestroy();
useEffect(() => {
const isSseClientAvailabble =
const isSseClientAvailable =
!isCreatingSseEventStream && !isDestroyingEventStream;
const willCreateEventStream =
isSseClientAvailabble &&
isSseClientAvailable &&
isLoggedIn &&
isSseDbEventsEnabled &&
isDefined(currentUser) &&
@@ -51,7 +51,7 @@ export const SSEEventStreamEffect = () => {
isNonEmptyArray(objectMetadataItems);
const willDestroyEventStream =
isSseClientAvailabble &&
isSseClientAvailable &&
isNonEmptyString(sseEventStreamId) &&
shouldDestroyEventStream;
@@ -4,8 +4,8 @@ export const ON_EVENT_SUBSCRIPTION = gql`
subscription OnEventSubscription($eventStreamId: String!) {
onEventSubscription(eventStreamId: $eventStreamId) {
eventStreamId
eventWithQueryIdsList {
event {
objectRecordEventsWithQueryIds {
objectRecordEvent {
action
objectNameSingular
recordId
@@ -20,6 +20,20 @@ export const ON_EVENT_SUBSCRIPTION = gql`
}
queryIds
}
metadataEventsWithQueryIds {
metadataEvent {
type
metadataName
recordId
properties {
updatedFields
before
after
diff
}
}
queryIds
}
}
}
`;
@@ -0,0 +1,54 @@
import { dispatchMetadataOperationBrowserEvent } from '@/browser-event/utils/dispatchMetadataOperationBrowserEvent';
import { turnSseMetadataEventsToMetadataOperationBrowserEvents } from '@/sse-db-event/utils/turnSseMetadataEventsToMetadataOperationBrowserEvents';
import { useCallback } from 'react';
import {
type AllMetadataName,
type MetadataEvent,
type MetadataEventWithQueryIds,
} from '~/generated-metadata/graphql';
const groupSseMetadataEventsByMetadataName = (
sseMetadataEvents: MetadataEvent[],
): Map<AllMetadataName, MetadataEvent[]> => {
const eventsByMetadataName = new Map<AllMetadataName, MetadataEvent[]>();
for (const event of sseMetadataEvents) {
const metadataName = event.metadataName as AllMetadataName;
const existing = eventsByMetadataName.get(metadataName) ?? [];
eventsByMetadataName.set(metadataName, [...existing, event]);
}
return eventsByMetadataName;
};
export const useDispatchMetadataEventsFromSseToBrowserEvents = <
T extends Record<string, unknown>,
>() => {
const dispatchMetadataEventsFromSseToBrowserEvents = useCallback(
(metadataEventsWithQueryIds: MetadataEventWithQueryIds[]) => {
const sseMetadataEvents = metadataEventsWithQueryIds.map(
(item) => item.metadataEvent,
);
const eventsByMetadataName =
groupSseMetadataEventsByMetadataName(sseMetadataEvents);
for (const [metadataName, events] of eventsByMetadataName) {
const metadataOperationBrowserEvents =
turnSseMetadataEventsToMetadataOperationBrowserEvents<T>({
metadataName,
sseMetadataEvents: events,
});
for (const browserEvent of metadataOperationBrowserEvents) {
dispatchMetadataOperationBrowserEvent(browserEvent);
}
}
},
[],
);
return { dispatchMetadataEventsFromSseToBrowserEvents };
};
@@ -1,19 +1,19 @@
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { groupObjectRecordSseEventsByObjectMetadataItemNameSingular } from '@/sse-db-event/utils/groupObjectRecordSseEventsByObjectMetadataItemNameSingular';
import { turnSseObjectRecordEventsToObjectRecordOperationBrowserEvents } from '@/sse-db-event/utils/turnSseObjectRecordEventToObjectRecordOperationBrowserEvent';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { type EventWithQueryIds } from '~/generated-metadata/graphql';
import { type ObjectRecordEventWithQueryIds } from '~/generated-metadata/graphql';
export const useDispatchObjectRecordEventsFromSseToBrowserEvents = () => {
const { objectMetadataItems } = useObjectMetadataItems();
const dispatchObjectRecordEventsFromSseToBrowserEvents = useCallback(
(eventsWithQueryIds: EventWithQueryIds[]) => {
const objectRecordEvents = eventsWithQueryIds.map((eventWithQueryIds) => {
return eventWithQueryIds.event;
});
(objectRecordEventsWithQueryIds: ObjectRecordEventWithQueryIds[]) => {
const objectRecordEvents = objectRecordEventsWithQueryIds.map(
(item) => item.objectRecordEvent,
);
const objectRecordEventsByObjectMetadataItemNameSingular =
groupObjectRecordSseEventsByObjectMetadataItemNameSingular({
@@ -2,14 +2,19 @@ import { requiredQueryListenersState } from '@/sse-db-event/states/requiredQuery
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { useEffect } from 'react';
import { useRecoilCallback } from 'recoil';
import { type RecordGqlOperationSignature } from 'twenty-shared/types';
import {
type MetadataGqlOperationSignature,
type RecordGqlOperationSignature,
} from 'twenty-shared/types';
export const useListenToObjectRecordEventsForQuery = ({
export const useListenToEventsForQuery = ({
queryId,
operationSignature,
}: {
queryId: string;
operationSignature: RecordGqlOperationSignature;
operationSignature:
| RecordGqlOperationSignature
| MetadataGqlOperationSignature;
}) => {
const changeQueryIdListenState = useRecoilCallback(
({ set, snapshot }) =>
@@ -1,4 +1,5 @@
import { ON_EVENT_SUBSCRIPTION } from '@/sse-db-event/graphql/subscriptions/OnEventSubscription';
import { useDispatchMetadataEventsFromSseToBrowserEvents } from '@/sse-db-event/hooks/useDispatchMetadataEventsFromSseToBrowserEvents';
import { useDispatchObjectRecordEventsFromSseToBrowserEvents } from '@/sse-db-event/hooks/useDispatchObjectRecordEventsFromSseToBrowserEvents';
import { useTriggerOptimisticEffectFromSseEvents } from '@/sse-db-event/hooks/useTriggerOptimisticEffectFromSseEvents';
import { disposeFunctionForEventStreamState } from '@/sse-db-event/states/disposeFunctionByEventStreamMapState';
@@ -23,6 +24,9 @@ export const useTriggerEventStreamCreation = () => {
isCreatingSseEventStreamState,
);
const { dispatchMetadataEventsFromSseToBrowserEvents } =
useDispatchMetadataEventsFromSseToBrowserEvents();
const { dispatchObjectRecordEventsFromSseToBrowserEvents } =
useDispatchObjectRecordEventsFromSseToBrowserEvents();
@@ -73,9 +77,55 @@ export const useTriggerEventStreamCreation = () => {
},
},
{
next: (
value: ExecutionResult<{
onEventSubscription: EventSubscription;
}>,
) => {
if (isDefined(value?.errors)) {
captureException(
new Error(
`SSE subscription error: ${value.errors[0]?.message}`,
),
);
set(shouldDestroyEventStreamState, true);
return;
}
if (!hasReceivedFirstEvent) {
hasReceivedFirstEvent = true;
set(sseEventStreamReadyState, true);
}
const eventSubscription = value?.data?.onEventSubscription;
const objectRecordEventsWithQueryIds =
eventSubscription?.objectRecordEventsWithQueryIds ?? [];
const metadataEventsWithQueryIds =
eventSubscription?.metadataEventsWithQueryIds ?? [];
const objectRecordEvents = objectRecordEventsWithQueryIds.map(
(item) => item.objectRecordEvent,
);
triggerOptimisticEffectFromSseEvents({
objectRecordEvents,
});
dispatchObjectRecordEventsFromSseToBrowserEvents(
objectRecordEventsWithQueryIds,
);
dispatchMetadataEventsFromSseToBrowserEvents(
metadataEventsWithQueryIds,
);
},
error: (error) => {
captureException(error);
},
complete: () => {},
error: () => {},
next: () => {},
},
{
message: ({ data, event }) => {
@@ -109,12 +159,12 @@ export const useTriggerEventStreamCreation = () => {
const objectRecordEventsWithQueryIds =
result?.data?.onEventSubscription
?.eventWithQueryIdsList ?? [];
?.objectRecordEventsWithQueryIds ?? [];
const objectRecordEvents =
objectRecordEventsWithQueryIds.map(
(eventWithQueryIds) => {
return eventWithQueryIds.event;
(objectRecordEventWithQueryIds) => {
return objectRecordEventWithQueryIds.objectRecordEvent;
},
);
@@ -144,9 +194,9 @@ export const useTriggerEventStreamCreation = () => {
setIsCreatingSseEventStream(false);
},
[
dispatchMetadataEventsFromSseToBrowserEvents,
dispatchObjectRecordEventsFromSseToBrowserEvents,
setIsCreatingSseEventStream,
triggerOptimisticEffectFromSseEvents,
],
);
@@ -1,8 +1,16 @@
import { type RecordGqlOperationSignature } from 'twenty-shared/types';
import { createState } from '@/ui/utilities/state/utils/createState';
import {
type MetadataGqlOperationSignature,
type RecordGqlOperationSignature,
} from 'twenty-shared/types';
export const activeQueryListenersState = createState<
{ queryId: string; operationSignature: RecordGqlOperationSignature }[]
{
queryId: string;
operationSignature:
| RecordGqlOperationSignature
| MetadataGqlOperationSignature;
}[]
>({
key: 'activeQueryListenersState',
defaultValue: [],
@@ -1,8 +1,16 @@
import { type RecordGqlOperationSignature } from 'twenty-shared/types';
import { createState } from '@/ui/utilities/state/utils/createState';
import {
type MetadataGqlOperationSignature,
type RecordGqlOperationSignature,
} from 'twenty-shared/types';
export const requiredQueryListenersState = createState<
{ queryId: string; operationSignature: RecordGqlOperationSignature }[]
{
queryId: string;
operationSignature:
| RecordGqlOperationSignature
| MetadataGqlOperationSignature;
}[]
>({
key: 'requiredQueryListenersState',
defaultValue: [],
@@ -1,22 +1,20 @@
import { type ObjectRecordEventsByQueryId } from '@/sse-db-event/types/ObjectRecordEventsByQueryId';
import { getObjectRecordEventsForQueryEventName } from '@/sse-db-event/utils/getObjectRecordEventsForQueryEventName';
import { isDefined } from 'twenty-shared/utils';
import { type EventWithQueryIds } from '~/generated-metadata/graphql';
import { type ObjectRecordEventWithQueryIds } from '~/generated-metadata/graphql';
export const dispatchObjectRecordEventsWithQueryIds = (
objectRecordEventsWithQueryIds: EventWithQueryIds[],
objectRecordEventsWithQueryIds: ObjectRecordEventWithQueryIds[],
) => {
const objectRecordEventsByQueryId: ObjectRecordEventsByQueryId = {};
for (const objectRecordEventWithQueryIds of objectRecordEventsWithQueryIds) {
for (const queryId of objectRecordEventWithQueryIds.queryIds) {
for (const item of objectRecordEventsWithQueryIds) {
for (const queryId of item.queryIds) {
if (!isDefined(objectRecordEventsByQueryId[queryId])) {
objectRecordEventsByQueryId[queryId] = [];
}
objectRecordEventsByQueryId[queryId].push(
objectRecordEventWithQueryIds.event,
);
objectRecordEventsByQueryId[queryId].push(item.objectRecordEvent);
}
}
@@ -0,0 +1,66 @@
import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail';
import { isDefined } from 'twenty-shared/utils';
import {
MetadataEventAction,
type AllMetadataName,
type MetadataEvent,
} from '~/generated-metadata/graphql';
export const turnSseMetadataEventsToMetadataOperationBrowserEvents = <
T extends Record<string, unknown>,
>({
metadataName,
sseMetadataEvents,
}: {
metadataName: AllMetadataName;
sseMetadataEvents: MetadataEvent[];
}): MetadataOperationBrowserEventDetail<T>[] => {
return sseMetadataEvents
.map((event): MetadataOperationBrowserEventDetail<T> | null => {
switch (event.type) {
case MetadataEventAction.CREATED: {
const createdRecord = event.properties.after;
if (!isDefined(createdRecord)) {
return null;
}
return {
metadataName,
operation: {
type: 'create',
createdRecord,
},
};
}
case MetadataEventAction.UPDATED: {
const updatedRecord = event.properties.after;
if (!isDefined(updatedRecord)) {
return null;
}
return {
metadataName,
operation: {
type: 'update',
updatedRecord,
updatedFields: event.properties.updatedFields ?? undefined,
},
};
}
case MetadataEventAction.DELETED: {
return {
metadataName,
operation: {
type: 'delete',
deletedRecordId: event.recordId,
},
};
}
default:
return null;
}
})
.filter(isDefined);
};
@@ -1,5 +1,5 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type ObjectRecordOperationBrowserEventDetail } from '@/object-record/types/ObjectRecordOperationBrowserEventDetail';
import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
import { getObjectRecordOperationUpdateInputs } from '@/sse-db-event/utils/getObjectRecordOperationUpdateInputs';
import { groupObjectRecordSseEventsByEventType } from '@/sse-db-event/utils/groupObjectRecordSseEventsByEventType';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
@@ -1,7 +1,7 @@
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { useStopWorkflowRunMutation } from '~/generated/graphql';
export const useStopWorkflowRun = () => {
@@ -1,5 +1,5 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useListenToObjectRecordEventsForQuery } from '@/sse-db-event/hooks/useListenToObjectRecordEventsForQuery';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
export const WorkflowRunSSESubscribeEffect = ({
workflowRunId,
@@ -8,7 +8,7 @@ export const WorkflowRunSSESubscribeEffect = ({
}) => {
const queryId = `workflow-run-${workflowRunId}`;
useListenToObjectRecordEventsForQuery({
useListenToEventsForQuery({
queryId,
operationSignature: {
objectNameSingular: CoreObjectNameSingular.WorkflowRun,
@@ -3,8 +3,8 @@ import { useSetRecoilState } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useListenToObjectRecordOperationBrowserEvent } from '@/object-record/hooks/useListenToObjectRecordOperationBrowserEvent';
import { useListenToObjectRecordEventsForQuery } from '@/sse-db-event/hooks/useListenToObjectRecordEventsForQuery';
import { useListenToObjectRecordOperationBrowserEvent } from '@/browser-event/hooks/useListenToObjectRecordOperationBrowserEvent';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { shouldWorkflowRefetchRequestFamilyState } from '@/workflow/states/shouldWorkflowRefetchRequestFamilyState';
export const WorkflowSSESubscribeEffect = ({
@@ -23,7 +23,7 @@ export const WorkflowSSESubscribeEffect = ({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
useListenToObjectRecordEventsForQuery({
useListenToEventsForQuery({
queryId,
operationSignature: {
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
+6 -2
View File
@@ -148,8 +148,9 @@ Application development commands.
- `twenty function:execute [appPath]` — Execute a logic function with a JSON payload.
- Options:
- `-n, --functionName <name>`: Name of the function to execute (required if `-u` not provided).
- `-u, --functionUniversalIdentifier <id>`: Universal ID of the function to execute (required if `-n` not provided).
- `--postInstall`: Execute the post-install logic function defined in the application config (required if `-n` and `-u` not provided).
- `-n, --functionName <name>`: Name of the function to execute (required if `--postInstall` and `-u` not provided).
- `-u, --functionUniversalIdentifier <id>`: Universal ID of the function to execute (required if `--postInstall` and `-n` not provided).
- `-p, --payload <payload>`: JSON payload to send to the function (default: `{}`).
Examples:
@@ -187,6 +188,9 @@ twenty function:execute -n my-function -p '{"name": "test"}'
# Execute a function by universal identifier
twenty function:execute -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf -p '{"key": "value"}'
# Execute the post-install function
twenty function:execute --postInstall
```
## Configuration
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "twenty-sdk",
"version": "0.6.0-alpha",
"version": "0.6.0",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/sdk/index.d.ts",
@@ -28,6 +28,7 @@ export const EXPECTED_MANIFEST: Manifest = {
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
yarnLockChecksum: 'd41d8cd98f00b204e9800998ecf8427e',
packageJsonChecksum: '2851d0e2c3621a57e1fd103a245b6fde',
apiClientChecksum: null,
},
frontComponents: [
{
@@ -411,6 +412,7 @@ export const EXPECTED_MANIFEST: Manifest = {
],
views: [],
navigationMenuItems: [],
pageLayouts: [],
logicFunctions: [
{
builtHandlerChecksum: '[checksum]',
@@ -11,6 +11,7 @@ export const EXPECTED_MANIFEST: Manifest = {
defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002',
packageJsonChecksum: '93ae1e2eb3db18351d06f43550700dcc',
yarnLockChecksum: 'd41d8cd98f00b204e9800998ecf8427e',
apiClientChecksum: null,
},
publicAssets: [],
fields: [],
@@ -141,6 +142,7 @@ export const EXPECTED_MANIFEST: Manifest = {
],
views: [],
navigationMenuItems: [],
pageLayouts: [],
roles: [
{
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000040',
@@ -129,6 +129,7 @@ export const registerCommands = (program: Command): void => {
program
.command('function:execute [appPath]')
.option('--postInstall', 'Execute post-install logic function if defined')
.option(
'-p, --payload <payload>',
'JSON payload to send to the function',
@@ -147,15 +148,20 @@ export const registerCommands = (program: Command): void => {
async (
appPath?: string,
options?: {
postInstall?: boolean;
payload?: string;
functionUniversalIdentifier?: string;
functionName?: string;
},
) => {
if (!options?.functionUniversalIdentifier && !options?.functionName) {
if (
!options?.postInstall &&
!options?.functionUniversalIdentifier &&
!options?.functionName
) {
console.error(
chalk.red(
'Error: Either --functionName (-n) or --functionUniversalIdentifier (-u) is required.',
'Error: Either --postInstall or --functionName (-n) or --functionUniversalIdentifier (-u) is required.',
),
);
process.exit(1);
@@ -3,7 +3,7 @@ import { type ApiResponse } from '@/cli/utilities/api/api-response-type';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import chalk from 'chalk';
import inquirer from 'inquirer';
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
import { readManifestFromFile } from '@/cli/utilities/build/manifest/manifest-reader';
export class AppUninstallCommand {
private apiService = new ApiService();
@@ -25,7 +25,7 @@ export class AppUninstallCommand {
process.exit(1);
}
const { manifest } = await buildManifest(appPath);
const manifest = await readManifestFromFile(appPath);
if (!manifest) {
return { success: false, error: 'Build failed' };
@@ -3,6 +3,7 @@ import { getFrontComponentBaseFile } from '@/cli/utilities/entity/entity-front-c
import { getLogicFunctionBaseFile } from '@/cli/utilities/entity/entity-logic-function-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 chalk from 'chalk';
import * as fs from 'fs-extra';
@@ -113,6 +114,16 @@ export class EntityAddCommand {
return { name, file };
}
case SyncableEntity.PageLayout: {
const name = await this.getEntityName(entity);
const file = getPageLayoutBaseFile({
name,
});
return { name, file };
}
default:
assertUnreachable(entity);
}
@@ -3,18 +3,20 @@ import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-exec
import chalk from 'chalk';
import { type Manifest } from 'twenty-shared/application';
import { isDefined } from 'twenty-shared/utils';
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
import { readManifestFromFile } from '@/cli/utilities/build/manifest/manifest-reader';
export class LogicFunctionExecuteCommand {
private apiService = new ApiService();
async execute({
appPath = CURRENT_EXECUTION_DIRECTORY,
postInstall = false,
functionUniversalIdentifier,
functionName,
payload = '{}',
}: {
appPath?: string;
postInstall?: boolean;
functionUniversalIdentifier?: string;
functionName?: string;
payload?: string;
@@ -30,7 +32,7 @@ export class LogicFunctionExecuteCommand {
process.exit(1);
}
const { manifest } = await buildManifest(appPath);
const manifest = await readManifestFromFile(appPath);
if (!manifest) {
console.error(chalk.red('Failed to build manifest.'));
@@ -54,6 +56,12 @@ export class LogicFunctionExecuteCommand {
);
const targetFunction = appFunctions.find((fn) => {
if (postInstall) {
return (
fn.universalIdentifier ===
manifest.application.postInstallLogicFunctionUniversalIdentifier
);
}
if (functionUniversalIdentifier) {
return fn.universalIdentifier === functionUniversalIdentifier;
}
@@ -64,7 +72,9 @@ export class LogicFunctionExecuteCommand {
});
if (!targetFunction) {
const identifier = functionUniversalIdentifier || functionName;
const identifier = postInstall
? 'post install'
: functionUniversalIdentifier || functionName;
console.error(
chalk.red(`Function "${identifier}" not found in application.`),
);
@@ -1,7 +1,7 @@
import { ApiService } from '@/cli/utilities/api/api-service';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import chalk from 'chalk';
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
import { readManifestFromFile } from '@/cli/utilities/build/manifest/manifest-reader';
export class LogicFunctionLogsCommand {
private apiService = new ApiService();
@@ -16,7 +16,7 @@ export class LogicFunctionLogsCommand {
functionName?: string;
}): Promise<void> {
try {
const { manifest } = await buildManifest(appPath);
const manifest = await readManifestFromFile(appPath);
if (!manifest) {
process.exit(1);
@@ -8,6 +8,7 @@ import {
type RestartableWatcher,
type RestartableWatcherOptions,
} from '@/cli/utilities/build/common/restartable-watcher-interface';
import { createTypecheckPlugin } from '@/cli/utilities/build/common/typecheck-plugin';
import * as esbuild from 'esbuild';
import path from 'path';
import { OUTPUT_DIR, NODE_ESM_CJS_BANNER } from 'twenty-shared/application';
@@ -214,7 +215,10 @@ export const createLogicFunctionsWatcher = (
externalModules: LOGIC_FUNCTION_EXTERNAL_MODULES,
fileFolder: FileFolder.BuiltLogicFunction,
platform: 'node',
extraPlugins: [createSdkGeneratedResolverPlugin(options.appPath)],
extraPlugins: [
createTypecheckPlugin(options.appPath),
createSdkGeneratedResolverPlugin(options.appPath),
],
banner: NODE_ESM_CJS_BANNER,
},
});
@@ -229,6 +233,7 @@ export const createFrontComponentsWatcher = (
fileFolder: FileFolder.BuiltFrontComponent,
jsx: 'automatic',
extraPlugins: [
createTypecheckPlugin(options.appPath),
createSdkGeneratedResolverPlugin(options.appPath),
...getFrontComponentBuildPlugins(),
],
@@ -51,7 +51,7 @@ export class FileUploadWatcher {
});
this.watcher.on('all', (event, filePath) => {
if (event === 'addDir') {
if (event === 'addDir' || event === 'unlinkDir') {
return;
}
@@ -0,0 +1,103 @@
import { spawn, type ChildProcess } from 'node:child_process';
import * as fs from 'fs-extra';
import path from 'node:path';
import {
parseTscOutputLine,
type TypecheckError,
} from '@/cli/utilities/build/common/typecheck-plugin';
export type TscWatcherOptions = {
appPath: string;
onErrors: (errors: TypecheckError[]) => void;
};
export class TscWatcher {
private appPath: string;
private onErrors: (errors: TypecheckError[]) => void;
private process: ChildProcess | null = null;
private pendingErrors: TypecheckError[] = [];
private buffer = '';
private hasErrors = false;
constructor(options: TscWatcherOptions) {
this.appPath = options.appPath;
this.onErrors = options.onErrors;
}
async start(): Promise<void> {
const tscPath = path.join(this.appPath, 'node_modules', '.bin', 'tsc');
if (!(await fs.pathExists(tscPath))) {
return;
}
const tsconfigPath = path.join(this.appPath, 'tsconfig.json');
this.process = spawn(
tscPath,
['--watch', '--noEmit', '--pretty', 'false', '-p', tsconfigPath],
{ cwd: this.appPath, stdio: ['ignore', 'pipe', 'pipe'] },
);
this.process.on('error', () => {
this.process = null;
});
this.process.stdout?.on('data', (chunk: Buffer) => {
this.handleOutput(chunk.toString());
});
this.process.stderr?.on('data', (chunk: Buffer) => {
this.handleOutput(chunk.toString());
});
}
close(): void {
this.process?.kill();
this.process = null;
}
private handleOutput(data: string): void {
this.buffer += data;
const lines = this.buffer.split('\n');
this.buffer = lines.pop() ?? '';
for (const line of lines) {
this.processLine(line);
}
}
private processLine(line: string): void {
if (
line.includes('Starting compilation in watch mode...') ||
line.includes('Starting incremental compilation...')
) {
this.pendingErrors = [];
return;
}
if (line.includes('Watching for file changes.')) {
const hadErrors = this.hasErrors;
this.hasErrors = this.pendingErrors.length > 0;
if (this.hasErrors || hadErrors) {
this.onErrors(this.pendingErrors);
}
this.pendingErrors = [];
return;
}
const error = parseTscOutputLine(line);
if (error) {
this.pendingErrors.push(error);
}
}
}
@@ -0,0 +1,84 @@
import { execFile } from 'node:child_process';
import type * as esbuild from 'esbuild';
import path from 'node:path';
export type TypecheckError = {
text: string;
file: string;
line: number;
column: number;
};
const TSC_ERROR_REGEX = /^(.+)\((\d+),(\d+)\): error TS\d+: (.+)$/;
export const parseTscOutputLine = (line: string): TypecheckError | null => {
const match = line.match(TSC_ERROR_REGEX);
if (!match) {
return null;
}
const [, filePath, lineStr, columnStr, text] = match;
return {
text,
file: filePath,
line: Number(lineStr),
column: Number(columnStr) - 1,
};
};
const parseTscOutput = (output: string): TypecheckError[] => {
const errors: TypecheckError[] = [];
for (const line of output.split('\n')) {
const error = parseTscOutputLine(line);
if (error) {
errors.push(error);
}
}
return errors;
};
export const runTypecheck = (appPath: string): Promise<TypecheckError[]> => {
const tsconfigPath = path.join(appPath, 'tsconfig.json');
const tscPath = path.join(appPath, 'node_modules', '.bin', 'tsc');
return new Promise((resolve) => {
execFile(
tscPath,
['--noEmit', '--pretty', 'false', '-p', tsconfigPath],
{ cwd: appPath },
(_error, stdout, stderr) => {
resolve(parseTscOutput(stderr || stdout));
},
);
});
};
const toEsbuildErrors = (errors: TypecheckError[]): esbuild.PartialMessage[] =>
errors.map((error) => ({
text: error.text,
location: {
file: error.file,
line: error.line,
column: error.column,
lineText: '',
length: 0,
namespace: '',
suggestion: '',
},
}));
export const createTypecheckPlugin = (appPath: string): esbuild.Plugin => ({
name: 'typecheck',
setup: (build) => {
build.onStart(async () => {
const errors = await runTypecheck(appPath);
return { errors: toEsbuildErrors(errors) };
});
},
});
@@ -13,6 +13,7 @@ const validApplication: ApplicationManifest = {
defaultRoleUniversalIdentifier: '68bb56f3-8300-4cb5-8cc3-8da9ee66f1b2',
packageJsonChecksum: '98592af7-4be9-4655-b5c4-9bef307a996c',
yarnLockChecksum: '580ee05f-15fe-4146-bac2-6c382483c94e',
apiClientChecksum: null,
};
const validField: FieldManifest = {
@@ -34,6 +35,7 @@ const validManifest: Manifest = {
publicAssets: [],
views: [],
navigationMenuItems: [],
pageLayouts: [],
};
describe('manifestValidate', () => {
@@ -11,6 +11,7 @@ import {
type LogicFunctionConfig,
} from '@/sdk';
import { type ObjectConfig } from '@/sdk/objects/object-config';
import { type PageLayoutConfig } from '@/sdk/page-layouts/page-layout-config';
import { type ViewConfig } from '@/sdk/views/view-config';
import { glob } from 'fast-glob';
import { readFile } from 'fs-extra';
@@ -25,6 +26,7 @@ import {
type Manifest,
type NavigationMenuItemManifest,
type ObjectManifest,
type PageLayoutManifest,
type RoleManifest,
type ViewManifest,
} from 'twenty-shared/application';
@@ -67,6 +69,7 @@ export const buildManifest = async (
const publicAssets: AssetManifest[] = [];
const views: ViewManifest[] = [];
const navigationMenuItems: NavigationMenuItemManifest[] = [];
const pageLayouts: PageLayoutManifest[] = [];
const applicationFilePaths: string[] = [];
const objectsFilePaths: string[] = [];
@@ -77,6 +80,7 @@ export const buildManifest = async (
const publicAssetsFilePaths: string[] = [];
const viewsFilePaths: string[] = [];
const navigationMenuItemsFilePaths: string[] = [];
const pageLayoutsFilePaths: string[] = [];
for (const filePath of filePaths) {
const fileContent = await readFile(filePath, 'utf-8');
@@ -101,6 +105,7 @@ export const buildManifest = async (
...extract.config,
yarnLockChecksum: null,
packageJsonChecksum: null,
apiClientChecksum: null,
};
errors.push(...extract.errors);
applicationFilePaths.push(relativePath);
@@ -239,6 +244,21 @@ export const buildManifest = async (
navigationMenuItemsFilePaths.push(relativePath);
break;
}
case ManifestEntityKey.PageLayouts: {
const extract = await extractManifestFromFile<PageLayoutConfig>({
appPath,
filePath,
});
const pageLayoutManifest: PageLayoutManifest = {
...extract.config,
};
pageLayouts.push(pageLayoutManifest);
errors.push(...extract.errors);
pageLayoutsFilePaths.push(relativePath);
break;
}
case ManifestEntityKey.PublicAssets: {
// Public assets are handled below
break;
@@ -279,6 +299,7 @@ export const buildManifest = async (
publicAssets,
views,
navigationMenuItems,
pageLayouts,
};
const entityFilePaths: EntityFilePaths = {
@@ -291,6 +312,7 @@ export const buildManifest = async (
publicAssets: publicAssetsFilePaths,
views: viewsFilePaths,
navigationMenuItems: navigationMenuItemsFilePaths,
pageLayouts: pageLayoutsFilePaths,
};
return { manifest, filePaths: entityFilePaths, errors };
@@ -9,6 +9,7 @@ export enum TargetFunction {
DefineFrontComponent = 'defineFrontComponent',
DefineView = 'defineView',
DefineNavigationMenuItem = 'defineNavigationMenuItem',
DefinePageLayout = 'definePageLayout',
}
export enum ManifestEntityKey {
@@ -21,6 +22,7 @@ export enum ManifestEntityKey {
PublicAssets = 'publicAssets',
Views = 'views',
NavigationMenuItems = 'navigationMenuItems',
PageLayouts = 'pageLayouts',
}
export type EntityFilePaths = Record<ManifestEntityKey, string[]>;
@@ -38,6 +40,7 @@ export const TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING: Record<
[TargetFunction.DefineView]: ManifestEntityKey.Views,
[TargetFunction.DefineNavigationMenuItem]:
ManifestEntityKey.NavigationMenuItems,
[TargetFunction.DefinePageLayout]: ManifestEntityKey.PageLayouts,
};
const computeIsTargetFunctionCall = (node: ts.Node): string | undefined => {
@@ -0,0 +1,21 @@
import * as fs from 'fs-extra';
import path from 'path';
import { type Manifest, OUTPUT_DIR } from 'twenty-shared/application';
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
export const readManifestFromFile = async (
appPath: string,
): Promise<Manifest | null> => {
const outputDir = path.join(appPath, OUTPUT_DIR);
await fs.ensureDir(outputDir);
const manifestPath = path.join(outputDir, 'manifest.json');
if (!(await fs.pathExists(manifestPath))) {
const { manifest } = await buildManifest(appPath);
return manifest;
}
return await fs.readJson(manifestPath);
};
@@ -1,3 +1,4 @@
import crypto from 'crypto';
import { relative } from 'path';
import { type Manifest, OUTPUT_DIR } from 'twenty-shared/application';
import { FileFolder } from 'twenty-shared/types';
@@ -88,5 +89,30 @@ export const manifestUpdateChecksums = ({
}
}
}
const apiClientChecksums: string[] = [];
for (const [builtPath, { fileFolder }] of builtFileInfos.entries()) {
const rootBuiltPath = relative(OUTPUT_DIR, builtPath);
if (
fileFolder === FileFolder.Dependencies &&
rootBuiltPath.startsWith('api-client/')
) {
const entry = builtFileInfos.get(builtPath);
if (entry) {
apiClientChecksums.push(entry.checksum);
}
}
}
if (apiClientChecksums.length > 0) {
result.application.apiClientChecksum = crypto
.createHash('md5')
.update(apiClientChecksums.sort().join(''))
.digest('hex');
}
return result;
};
@@ -50,7 +50,7 @@ export class ManifestWatcher {
});
this.watcher.on('all', (event, filePath) => {
if (event === 'addDir') {
if (event === 'addDir' || event === 'unlinkDir') {
return;
}
@@ -22,6 +22,7 @@ export class ClientService {
authToken?: string;
}): Promise<void> {
const outputPath = this.resolveGeneratedPath(appPath);
const tempPath = `${outputPath}.tmp`;
const getSchemaResponse = await this.apiService.getSchema({ authToken });
@@ -33,12 +34,12 @@ export class ClientService {
const { data: schema } = getSchemaResponse;
await fs.ensureDir(outputPath);
await fs.emptyDir(outputPath);
await fs.ensureDir(tempPath);
await fs.emptyDir(tempPath);
await generate({
schema,
output: outputPath,
output: tempPath,
scalarTypes: {
DateTime: 'string',
JSON: 'Record<string, unknown>',
@@ -46,7 +47,10 @@ export class ClientService {
},
});
await this.injectTwentyClient(outputPath);
await this.injectTwentyClient(tempPath);
await fs.remove(outputPath);
await fs.move(tempPath, outputPath);
}
private resolveGeneratedPath(appPath: string): string {
@@ -70,22 +74,16 @@ const defaultOptions: ClientOptions = {
export default class Twenty {
private client: Client;
private apiUrl: string;
private authorizationToken: string;
constructor(options?: ClientOptions) {
const merged: ClientOptions = {
this.client = createClient({
...defaultOptions,
...options,
headers: {
...defaultOptions.headers,
...(options?.headers ?? {}),
},
};
this.client = createClient(merged);
this.apiUrl = merged.url;
this.authorizationToken = merged.headers.Authorization;
});
}
query<R extends QueryGenqlSelection>(request: R & { __name?: string }) {
@@ -95,41 +93,6 @@ export default class Twenty {
mutation<R extends MutationGenqlSelection>(request: R & { __name?: string }) {
return this.client.mutation(request);
}
async uploadFile(
fileBuffer: Buffer,
filename: string,
contentType: string = 'application/octet-stream',
fileFolder: string = 'Attachment',
): Promise<{ path: string; token: string }> {
const form = new FormData();
form.append('operations', JSON.stringify({
query: \`mutation UploadFile($file: Upload!, $fileFolder: FileFolder) {
uploadFile(file: $file, fileFolder: $fileFolder) { path token }
}\`,
variables: { file: null, fileFolder },
}));
form.append('map', JSON.stringify({ '0': ['variables.file'] }));
form.append('0', new Blob([fileBuffer], { type: contentType }), filename);
const response = await fetch(\`\${this.apiUrl}/graphql\`, {
method: 'POST',
headers: {
Authorization: this.authorizationToken,
},
body: form,
});
const result = await response.json();
if (result.errors) {
throw new GenqlError(result.errors, result.data);
}
return result.data.uploadFile;
}
}
`;
@@ -69,6 +69,7 @@ const ENTITY_TYPE_TO_SYNCABLE: Record<string, SyncableEntity | undefined> = {
logicFunctions: SyncableEntity.LogicFunction,
frontComponents: SyncableEntity.FrontComponent,
roles: SyncableEntity.Role,
pageLayouts: SyncableEntity.PageLayout,
};
const MAX_EVENT_COUNT = 200;
@@ -7,7 +7,10 @@ import { CheckServerOrchestratorStep } from '@/cli/utilities/dev/orchestrator/st
import { EnsureValidTokensOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/ensure-valid-tokens-orchestrator-step';
import { GenerateApiClientOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/generate-api-client-orchestrator-step';
import { ResolveApplicationOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/resolve-application-orchestrator-step';
import { StartWatchersOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step';
import {
StartWatchersOrchestratorStep,
type FileBuiltEvent,
} from '@/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step';
import { SyncApplicationOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/sync-application-orchestrator-step';
import { UploadFilesOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step';
import * as fs from 'fs-extra';
@@ -69,7 +72,7 @@ export class DevModeOrchestrator {
this.startWatchersStep = new StartWatchersOrchestratorStep({
...stepDeps,
scheduleSync: this.scheduleSync.bind(this),
uploadFilesStep: this.uploadFilesStep,
onFileBuilt: this.handleFileBuilt.bind(this),
});
}
@@ -90,6 +93,16 @@ export class DevModeOrchestrator {
return this.state;
}
private handleFileBuilt(event: FileBuiltEvent): void {
if (this.state.steps.uploadFiles.output.fileUploader) {
this.uploadFilesStep.uploadFile(
event.builtPath,
event.sourcePath,
event.fileFolder,
);
}
}
private scheduleSync(): void {
if (this.syncTimer) {
clearTimeout(this.syncTimer);
@@ -150,11 +163,9 @@ export class DevModeOrchestrator {
}
}
if (this.state.hasObjectsOrFieldsChanged(buildResult.manifest!)) {
await this.generateApiClientStep.execute({
appPath: this.state.appPath,
});
}
const objectsOrFieldsChanged = this.state.hasObjectsOrFieldsChanged(
buildResult.manifest!,
);
await this.uploadFilesStep.waitForUploads();
@@ -163,6 +174,16 @@ export class DevModeOrchestrator {
builtFileInfos: this.state.steps.uploadFiles.output.builtFileInfos,
appPath: this.state.appPath,
});
if (objectsOrFieldsChanged) {
await this.generateApiClientStep.execute({
appPath: this.state.appPath,
});
await this.uploadFilesStep.copyAndUploadApiClientFiles(
this.state.appPath,
);
}
}
private async initializePipeline(manifest: Manifest): Promise<boolean> {
@@ -4,15 +4,23 @@ import {
type EsbuildWatcher,
} from '@/cli/utilities/build/common/esbuild-watcher';
import { FileUploadWatcher } from '@/cli/utilities/build/common/file-upload-watcher';
import { TscWatcher } from '@/cli/utilities/build/common/tsc-watcher';
import { type TypecheckError } from '@/cli/utilities/build/common/typecheck-plugin';
import { type ManifestBuildResult } from '@/cli/utilities/build/manifest/manifest-update-checksums';
import { ManifestWatcher } from '@/cli/utilities/build/manifest/manifest-watcher';
import { type OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
import { type UploadFilesOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step';
import type { Location } from 'esbuild';
import { type EventName } from 'chokidar/handler.js';
import { ASSETS_DIR } from 'twenty-shared/application';
import { FileFolder } from 'twenty-shared/types';
export type FileBuiltEvent = {
fileFolder: FileFolder;
builtPath: string;
sourcePath: string;
checksum: string;
};
export type StartWatchersOrchestratorStepOutput = {
watchersStarted: boolean;
};
@@ -21,24 +29,25 @@ export class StartWatchersOrchestratorStep {
private state: OrchestratorState;
private scheduleSync: () => void;
private notify: () => void;
private uploadFilesStep: UploadFilesOrchestratorStep;
private onFileBuilt: (event: FileBuiltEvent) => void;
private manifestWatcher: ManifestWatcher | null = null;
private logicFunctionsWatcher: EsbuildWatcher | null = null;
private frontComponentsWatcher: EsbuildWatcher | null = null;
private assetWatcher: FileUploadWatcher | null = null;
private dependencyWatcher: FileUploadWatcher | null = null;
private tscWatcher: TscWatcher | null = null;
constructor(options: {
state: OrchestratorState;
scheduleSync: () => void;
notify: () => void;
uploadFilesStep: UploadFilesOrchestratorStep;
onFileBuilt: (event: FileBuiltEvent) => void;
}) {
this.state = options.state;
this.scheduleSync = options.scheduleSync;
this.notify = options.notify;
this.uploadFilesStep = options.uploadFilesStep;
this.onFileBuilt = options.onFileBuilt;
}
async start(): Promise<void> {
@@ -74,6 +83,8 @@ export class StartWatchersOrchestratorStep {
}
async close(): Promise<void> {
this.tscWatcher?.close();
await Promise.all([
this.manifestWatcher?.close(),
this.logicFunctionsWatcher?.close(),
@@ -117,32 +128,20 @@ export class StartWatchersOrchestratorStep {
this.notify();
}
private handleFileBuilt({
fileFolder,
builtPath,
sourcePath,
checksum,
}: {
fileFolder: FileFolder;
builtPath: string;
sourcePath: string;
checksum: string;
}): void {
private handleFileBuilt(event: FileBuiltEvent): void {
this.state.addEvent({
message: `Successfully built ${builtPath}`,
message: `Successfully built ${event.builtPath}`,
status: 'success',
});
this.state.steps.uploadFiles.output.builtFileInfos.set(builtPath, {
checksum,
builtPath,
sourcePath,
fileFolder,
this.state.steps.uploadFiles.output.builtFileInfos.set(event.builtPath, {
checksum: event.checksum,
builtPath: event.builtPath,
sourcePath: event.sourcePath,
fileFolder: event.fileFolder,
});
if (this.state.steps.uploadFiles.output.fileUploader) {
this.uploadFilesStep.uploadFile(builtPath, sourcePath, fileFolder);
}
this.onFileBuilt(event);
this.notify();
this.scheduleSync();
@@ -153,6 +152,7 @@ export class StartWatchersOrchestratorStep {
frontComponents: string[],
): Promise<void> {
await Promise.all([
this.startTscWatcher(),
this.startLogicFunctionsWatcher(logicFunctions),
this.startFrontComponentsWatcher(frontComponents),
this.startAssetWatcher(),
@@ -207,4 +207,31 @@ export class StartWatchersOrchestratorStep {
this.dependencyWatcher.start();
}
private async startTscWatcher(): Promise<void> {
this.tscWatcher = new TscWatcher({
appPath: this.state.appPath,
onErrors: this.handleTypecheckErrors.bind(this),
});
await this.tscWatcher.start();
}
private handleTypecheckErrors(errors: TypecheckError[]): void {
if (errors.length === 0) {
this.state.addEvent({
message: 'Typecheck passed',
status: 'success',
});
} else {
this.state.applyStepEvents(
errors.map((error) => ({
message: `Type error in ${error.file}(${error.line},${error.column}): ${error.text}`,
status: 'error' as const,
})),
);
}
this.notify();
}
}
@@ -3,7 +3,13 @@ import {
type OrchestratorStateBuiltFileInfo,
} from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
import { FileUploader } from '@/cli/utilities/file/file-uploader';
import { type FileFolder } from 'twenty-shared/types';
import crypto from 'crypto';
import * as fs from 'fs-extra';
import { join } from 'path';
import { OUTPUT_DIR } from 'twenty-shared/application';
import { FileFolder } from 'twenty-shared/types';
const API_CLIENT_FILES = ['types.ts', 'schema.ts'];
export type UploadFilesOrchestratorStepOutput = {
fileUploader: FileUploader | null;
@@ -103,6 +109,48 @@ export class UploadFilesOrchestratorStep {
this.notify();
}
async copyAndUploadApiClientFiles(appPath: string): Promise<void> {
const generatedDir = join(
appPath,
'node_modules',
'twenty-sdk',
'generated',
);
if (!(await fs.pathExists(generatedDir))) {
return;
}
const outputDir = join(appPath, OUTPUT_DIR, 'api-client');
await fs.ensureDir(outputDir);
for (const fileName of API_CLIENT_FILES) {
const absoluteSourcePath = join(generatedDir, fileName);
if (!(await fs.pathExists(absoluteSourcePath))) {
continue;
}
await fs.copy(absoluteSourcePath, join(outputDir, fileName));
const content = await fs.readFile(absoluteSourcePath);
const checksum = crypto.createHash('md5').update(content).digest('hex');
const builtPath = join(OUTPUT_DIR, 'api-client', fileName);
const sourcePath = join('api-client', fileName);
this.state.steps.uploadFiles.output.builtFileInfos.set(builtPath, {
checksum,
builtPath,
sourcePath,
fileFolder: FileFolder.Dependencies,
});
this.uploadFile(builtPath, sourcePath, FileFolder.Dependencies);
}
}
private uploadPendingFiles(): void {
for (const [
builtPath,
@@ -97,6 +97,7 @@ export const ENTITY_LABELS: Record<SyncableEntity, string> = {
[SyncableEntity.LogicFunction]: 'Logic functions',
[SyncableEntity.FrontComponent]: 'Front components',
[SyncableEntity.Role]: 'Roles',
[SyncableEntity.PageLayout]: 'Page layouts',
};
export const ENTITY_ORDER = Object.keys(ENTITY_LABELS) as SyncableEntity[];
@@ -46,7 +46,6 @@ export default defineLogicFunction({
// databaseEventTriggerSettings: {
// eventName: 'objectName.created',
// },
],
});
`;
};
@@ -0,0 +1,19 @@
import { v4 as uuidv4 } from 'uuid';
export const getPageLayoutBaseFile = ({ name }: { name: string }) => {
return `import { definePageLayout } from 'twenty-sdk';
export default definePageLayout({
universalIdentifier: '${uuidv4()}',
name: '${name}',
tabs: [
{
universalIdentifier: '${uuidv4()}',
title: 'Overview',
position: 0,
widgets: [],
},
],
});
`;
};
@@ -2,5 +2,5 @@ import { type ApplicationManifest } from 'twenty-shared/application';
export type ApplicationConfig = Omit<
ApplicationManifest,
'packageJsonChecksum' | 'yarnLockChecksum'
'packageJsonChecksum' | 'yarnLockChecksum' | 'apiClientChecksum'
>;
@@ -2,6 +2,7 @@ import { type ApplicationConfig } from '@/sdk/application/application-config';
import { type FrontComponentConfig } from '@/sdk/front-component-config';
import { type LogicFunctionConfig } from '@/sdk/logic-functions/logic-function-config';
import { type ObjectConfig } from '@/sdk/objects/object-config';
import { type PageLayoutConfig } from '@/sdk/page-layouts/page-layout-config';
import { type ViewConfig } from '@/sdk/views/view-config';
import {
type FieldManifest,
@@ -23,7 +24,8 @@ export type DefinableEntity =
| LogicFunctionConfig
| RoleManifest
| ViewConfig
| NavigationMenuItemManifest;
| NavigationMenuItemManifest
| PageLayoutConfig;
export type DefineEntity<T extends DefinableEntity = DefinableEntity> = (
config: T,
+2
View File
@@ -47,6 +47,8 @@ export type {
export type { RoutePayload } from './logic-functions/triggers/route-payload-type';
export { defineNavigationMenuItem } from './navigation-menu-items/define-navigation-menu-item';
export { defineObject } from './objects/define-object';
export { definePageLayout } from './page-layouts/define-page-layout';
export type { PageLayoutConfig } from './page-layouts/page-layout-config';
export { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from './objects/standard-object-ids';
export { defineRole } from './roles/define-role';
export { PermissionFlag } from './roles/permission-flag-type';

Some files were not shown because too many files have changed in this diff Show More