Compare commits

..
Author SHA1 Message Date
bosiraphael 6e22709e28 wip 2026-02-18 14:42:10 +01:00
265 changed files with 1538 additions and 5218 deletions
-4
View File
@@ -54,9 +54,6 @@ 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
```
@@ -66,7 +63,6 @@ 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",
"version": "0.6.0-alpha",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
@@ -49,12 +49,6 @@ 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,
@@ -202,6 +196,7 @@ 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',
@@ -220,38 +215,6 @@ 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,
@@ -267,14 +230,12 @@ 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,9 +53,6 @@ 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
@@ -72,7 +69,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, a default function role, and a post-install function
- Generates a default application config and a default function role
A freshly scaffolded app looks like this:
@@ -94,8 +91,7 @@ my-twenty-app/
├── roles/
│ └── default-role.ts # Default role for logic functions
├── logic-functions/
── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
── hello-world.ts # Example logic function
└── front-components/
└── hello-world.tsx # Example front component
```
@@ -293,7 +289,6 @@ 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:
@@ -301,7 +296,6 @@ 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',
@@ -317,7 +311,6 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -325,7 +318,6 @@ 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
@@ -458,54 +450,6 @@ 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,22 +45,19 @@ 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
```
@@ -73,7 +70,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
* يُولّد ضبطًا افتراضيًا للتطبيق ودورًا افتراضيًا للوظيفة
يبدو التطبيق المُنشأ حديثًا بالقالب كما يلي:
@@ -89,16 +86,15 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # Public assets folder (images, fonts, etc.)
public/ # مجلد الأصول العامة (صور، خطوط، إلخ)
src/
├── application-config.ts # Required - main application configuration
├── application-config.ts # مطلوب - التكوين الرئيسي للتطبيق
├── roles/
│ └── default-role.ts # Default role for logic functions
│ └── default-role.ts # الدور الافتراضي لوظائف المنطق
├── logic-functions/
── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
── hello-world.ts # مثال لوظيفة منطقية
└── front-components/
└── hello-world.tsx # Example front component
└── hello-world.tsx # مثال لمكوّن الواجهة الأمامية
```
بشكل عام:
@@ -294,7 +290,6 @@ export default defineObject({
* **هوية التطبيق**: المعرفات، اسم العرض، والوصف.
* **كيفية تشغيل وظائفه**: الدور الذي تستخدمه للأذونات.
* **متغيرات (اختياري)**: أزواج مفتاح-قيمة تُعرض لوظائفك كمتغيرات بيئة.
* **(Optional) post-install function**: a logic function that runs after the app is installed.
Use `defineApplication()` to define your application configuration:
@@ -302,7 +297,6 @@ 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',
@@ -318,7 +312,6 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -327,7 +320,6 @@ 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).
#### الأدوار والصلاحيات
@@ -466,55 +458,6 @@ 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,22 +45,19 @@ yarn twenty app:dev
Odtud můžete:
```bash filename="Terminal"
# Add a new entity to your application (guided)
# Přidejte do vaší aplikace novou entitu (s průvodcem)
yarn twenty entity:add
# Watch your application's function logs
# Sledujte logy funkcí vaší aplikace
yarn twenty function:logs
# Execute a function by name
# Spusťte funkci podle názvu
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
# Odinstalujte aplikaci z aktuálního pracovního prostoru
yarn twenty app:uninstall
# Display commands' help
# Zobrazte nápovědu k příkazům
yarn twenty help
```
@@ -73,7 +70,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
* Generates a default application config, a default function role, and a post-install function
* Vygeneruje výchozí konfiguraci aplikace a výchozí roli funkcí
Čerstvě vytvořená aplikace vypadá takto:
@@ -89,16 +86,15 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # Public assets folder (images, fonts, etc.)
public/ # Složka s veřejnými prostředky (obrázky, písma apod.)
src/
├── application-config.ts # Required - main application configuration
├── application-config.ts # Povinné hlavní konfigurace aplikace
├── roles/
│ └── default-role.ts # Default role for logic functions
│ └── default-role.ts # Výchozí role pro logic funkce
├── logic-functions/
── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
── hello-world.ts # Ukázková logic funkce
└── front-components/
└── hello-world.tsx # Example front component
└── hello-world.tsx # Ukázková front-endová komponenta
```
V kostce:
@@ -294,7 +290,6 @@ 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:
@@ -302,7 +297,6 @@ 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',
@@ -318,7 +312,6 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -327,7 +320,6 @@ 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í
@@ -466,55 +458,6 @@ 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,22 +45,19 @@ yarn twenty app:dev
Von hier aus können Sie:
```bash filename="Terminal"
# Add a new entity to your application (guided)
# Eine neue Entität zu Ihrer Anwendung hinzufügen (geführt)
yarn twenty entity:add
# Watch your application's function logs
# Die Funktionsprotokolle Ihrer Anwendung überwachen
yarn twenty function:logs
# Execute a function by name
# Eine Funktion anhand ihres Namens ausführen
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
# Die Anwendung aus dem aktuellen Arbeitsbereich deinstallieren
yarn twenty app:uninstall
# Display commands' help
# Hilfe zu Befehlen anzeigen
yarn twenty help
```
@@ -73,7 +70,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
* Generates a default application config, a default function role, and a post-install function
* Generiert eine Standard-Anwendungskonfiguration und eine Standard-Funktionsrolle
Eine frisch erzeugte App sieht so aus:
@@ -95,8 +92,7 @@ my-twenty-app/
├── roles/
│ └── default-role.ts # Default role for logic functions
├── logic-functions/
── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
── hello-world.ts # Example logic function
└── front-components/
└── hello-world.tsx # Example front component
```
@@ -294,7 +290,6 @@ 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:
@@ -302,7 +297,6 @@ 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',
@@ -318,7 +312,6 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -327,7 +320,6 @@ 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
@@ -466,55 +458,6 @@ 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,22 +45,19 @@ yarn twenty app:dev
Da qui puoi:
```bash filename="Terminal"
# Add a new entity to your application (guided)
# Aggiungi una nuova entità alla tua applicazione (guidata)
yarn twenty entity:add
# Watch your application's function logs
# Monitora i log delle funzioni della tua applicazione
yarn twenty function:logs
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Esegui una funzione per nome
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
# Disinstalla l'applicazione dallo spazio di lavoro corrente
yarn twenty app:uninstall
# Display commands' help
# Mostra l'aiuto dei comandi
yarn twenty help
```
@@ -73,7 +70,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`
* Generates a default application config, a default function role, and a post-install function
* Genera una configurazione applicativa predefinita e un ruolo funzione predefinito
Un'app appena generata dallo scaffolder si presenta così:
@@ -89,16 +86,15 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # Public assets folder (images, fonts, etc.)
public/ # Cartella delle risorse pubbliche (immagini, font, ecc.)
src/
├── application-config.ts # Required - main application configuration
├── application-config.ts # Obbligatorio - configurazione principale dell'applicazione
├── roles/
│ └── default-role.ts # Default role for logic functions
│ └── default-role.ts # Ruolo predefinito per le funzioni logiche
├── logic-functions/
── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
── hello-world.ts # Funzione logica di esempio
└── front-components/
└── hello-world.tsx # Example front component
└── hello-world.tsx # Componente front-end di esempio
```
A livello generale:
@@ -294,7 +290,6 @@ 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:
@@ -302,7 +297,6 @@ 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',
@@ -318,7 +312,6 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -327,7 +320,6 @@ 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
@@ -466,55 +458,6 @@ 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,22 +45,19 @@ yarn twenty app:dev
A partir daqui você pode:
```bash filename="Terminal"
# Add a new entity to your application (guided)
# Adicionar uma nova entidade à sua aplicação (assistido)
yarn twenty entity:add
# Watch your application's function logs
# Acompanhar os logs das funções da sua aplicação
yarn twenty function:logs
# Execute a function by name
# Executar uma função pelo nome
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
# Desinstalar a aplicação do espaço de trabalho atual
yarn twenty app:uninstall
# Display commands' help
# Exibir a ajuda dos comandos
yarn twenty help
```
@@ -73,7 +70,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`
* Generates a default application config, a default function role, and a post-install function
* Gera uma configuração de aplicativo padrão e um papel padrão para as funções
Um aplicativo recém-criado pelo scaffold fica assim:
@@ -89,16 +86,15 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # Public assets folder (images, fonts, etc.)
public/ # Pasta de recursos públicos (imagens, fontes, etc.)
src/
├── application-config.ts # Required - main application configuration
├── application-config.ts # Obrigatório - configuração principal da aplicação
├── roles/
│ └── default-role.ts # Default role for logic functions
│ └── default-role.ts # Papel padrão para funções de lógica
├── logic-functions/
── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
── hello-world.ts # Exemplo de função de lógica
└── front-components/
└── hello-world.tsx # Example front component
└── hello-world.tsx # Exemplo de componente de front-end
```
Em alto nível:
@@ -294,7 +290,6 @@ 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:
@@ -302,7 +297,6 @@ 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',
@@ -318,7 +312,6 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -327,7 +320,6 @@ 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
@@ -466,55 +458,6 @@ 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,9 +54,6 @@ 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
@@ -73,7 +70,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, un rol implicit pentru funcții și o funcție post-instalare
* Generează o configurație implicită a aplicației și un rol implicit pentru funcții
O aplicație nou generată arată astfel:
@@ -89,16 +86,15 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # Director pentru resurse publice (imagini, fonturi etc.)
public/ # Public assets folder (images, fonts, etc.)
src/
├── application-config.ts # Obligatoriu - configurația principală a aplicației
├── application-config.ts # Required - main application configuration
├── roles/
│ └── default-role.ts # Rol implicit pentru funcțiile logice
│ └── default-role.ts # Default role for logic functions
├── logic-functions/
── hello-world.ts # Exemplu de funcție logică
│ └── post-install.ts # Funcție logică post-instalare
── hello-world.ts # Example logic function
└── front-components/
└── hello-world.tsx # Exemplu de componentă de interfață
└── hello-world.tsx # Example front component
```
Pe scurt:
@@ -294,7 +290,6 @@ 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:
@@ -302,7 +297,6 @@ 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',
@@ -318,7 +312,6 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -327,7 +320,6 @@ 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
@@ -466,55 +458,6 @@ 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,22 +45,19 @@ 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"}'
# Выполнить функцию по имени
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
```
@@ -73,7 +70,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
* Генерирует конфигурацию приложения по умолчанию и роль функции по умолчанию
Свежесгенерированное приложение выглядит так:
@@ -89,16 +86,15 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # Public assets folder (images, fonts, etc.)
public/ # Папка общедоступных ресурсов (изображения, шрифты и т. п.)
src/
├── application-config.ts # Required - main application configuration
├── application-config.ts # Обязательный — основная конфигурация приложения
├── roles/
│ └── default-role.ts # Default role for logic functions
│ └── default-role.ts # Роль по умолчанию для логических функций
├── logic-functions/
── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
── hello-world.ts # Пример логической функции
└── front-components/
└── hello-world.tsx # Example front component
└── hello-world.tsx # Пример фронтенд-компонента
```
В общих чертах:
@@ -294,7 +290,6 @@ export default defineObject({
* **Что это за приложение**: идентификаторы, отображаемое имя и описание.
* **Как запускаются его функции**: какую роль они используют для прав доступа.
* **(Необязательно) переменные**: пары ключ-значение, предоставляемые вашим функциям как переменные окружения.
* **(Optional) post-install function**: a logic function that runs after the app is installed.
Используйте `defineApplication()` для определения конфигурации вашего приложения:
@@ -302,7 +297,6 @@ 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',
@@ -318,7 +312,6 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -327,7 +320,6 @@ 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).
#### Роли и разрешения
@@ -466,55 +458,6 @@ 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,9 +54,6 @@ 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
@@ -73,7 +70,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
* Generates a default application config, a default function role, and a post-install function
* Varsayılan bir uygulama yapılandırması ve varsayılan bir fonksiyon rolü üretir
Yeni şablondan oluşturulan bir uygulama şöyle görünür:
@@ -89,16 +86,15 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # Public assets folder (images, fonts, etc.)
public/ # Genel varlıklar klasörü (görseller, yazı tipleri vb.)
src/
├── application-config.ts # Required - main application configuration
├── application-config.ts # Gerekli - ana uygulama yapılandırması
├── roles/
│ └── default-role.ts # Default role for logic functions
│ └── default-role.ts # Mantık işlevleri için varsayılan rol
├── logic-functions/
── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
── hello-world.ts # Örnek mantık işlevi
└── front-components/
└── hello-world.tsx # Example front component
└── hello-world.tsx # Örnek ön uç bileşeni
```
Genel hatlarıyla:
@@ -294,7 +290,6 @@ 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:
@@ -302,7 +297,6 @@ 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',
@@ -318,7 +312,6 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -327,7 +320,6 @@ 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
@@ -466,55 +458,6 @@ 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,22 +45,19 @@ 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
```
@@ -73,7 +70,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
* 生成默认的应用配置和默认的函数角色
一个新生成的脚手架应用如下所示:
@@ -89,16 +86,15 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # Public assets folder (images, fonts, etc.)
public/ # 公共资源文件夹(图像、字体等)
src/
├── application-config.ts # Required - main application configuration
├── application-config.ts # 必需 - 主应用程序配置
├── roles/
│ └── default-role.ts # Default role for logic functions
│ └── default-role.ts # 用于逻辑函数的默认角色
├── logic-functions/
── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
── hello-world.ts # 示例逻辑函数
└── front-components/
└── hello-world.tsx # Example front component
└── hello-world.tsx # 示例前端组件
```
总体来说:
@@ -294,7 +290,6 @@ export default defineObject({
* **应用的身份**:标识符、显示名称和描述。
* **函数如何运行**:它们用于权限的角色。
* **(可选)变量**:以环境变量形式提供给函数的键值对。
* **(Optional) post-install function**: a logic function that runs after the app is installed.
使用 `defineApplication()` 定义你的应用配置:
@@ -302,7 +297,6 @@ 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',
@@ -318,7 +312,6 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -327,7 +320,6 @@ 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).
#### 角色和权限
@@ -466,55 +458,6 @@ 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,8 +1448,13 @@ export enum EventLogTable {
export type EventSubscription = {
__typename?: 'EventSubscription';
eventStreamId: Scalars['String'];
metadataEventsWithQueryIds: Array<MetadataEventWithQueryIds>;
objectRecordEventsWithQueryIds: Array<ObjectRecordEventWithQueryIds>;
eventWithQueryIdsList: Array<EventWithQueryIds>;
};
export type EventWithQueryIds = {
__typename?: 'EventWithQueryIds';
event: ObjectRecordEvent;
queryIds: Array<Scalars['String']>;
};
export type ExecuteOneLogicFunctionInput = {
@@ -1480,7 +1485,6 @@ 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',
@@ -2134,27 +2138,6 @@ 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',
@@ -3396,12 +3379,6 @@ 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',
@@ -5804,7 +5781,7 @@ export type FindOneFrontComponentQueryVariables = Exact<{
}>;
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 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 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 };
@@ -10592,7 +10569,6 @@ export const FindOneFrontComponentDocument = gql`
id
name
applicationId
builtComponentChecksum
applicationTokenPair {
applicationAccessToken {
token
@@ -1,2 +0,0 @@
export const METADATA_OPERATION_BROWSER_EVENT_NAME =
'metadata-operation-browser-event';
@@ -1,53 +0,0 @@
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,14 +0,0 @@
export type MetadataOperation<T extends Record<string, unknown>> =
| {
type: 'create';
createdRecord: T;
}
| {
type: 'update';
updatedRecord: T;
updatedFields?: string[];
}
| {
type: 'delete';
deletedRecordId: string;
};
@@ -1,9 +0,0 @@
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>;
};
@@ -1,14 +0,0 @@
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,6 +1,5 @@
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';
@@ -22,6 +21,8 @@ export const FrontComponentRenderer = ({
const { executionContext, frontComponentHostCommunicationApi } =
useFrontComponentExecutionContext();
const componentUrl = `${REST_API_BASE_URL}/front-components/${frontComponentId}`;
const handleError = useCallback(
(error?: Error) => {
if (!isDefined(error)) {
@@ -42,15 +43,6 @@ export const FrontComponentRenderer = ({
onError: handleError,
});
useOnFrontComponentUpdated({
frontComponentId,
});
const componentUrl = getFrontComponentUrl({
frontComponentId,
checksum: data?.frontComponent?.builtComponentChecksum,
});
if (
loading ||
!isDefined(data?.frontComponent) ||
@@ -6,7 +6,6 @@ export const FIND_ONE_FRONT_COMPONENT = gql`
id
name
applicationId
builtComponentChecksum
applicationTokenPair {
applicationAccessToken {
token
@@ -1,37 +0,0 @@
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,
});
};
@@ -1,54 +0,0 @@
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 };
};
@@ -1,14 +0,0 @@
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,7 +19,6 @@ 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';
@@ -63,10 +62,6 @@ 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);
@@ -183,12 +178,7 @@ export const AdvancedFilterCommandMenuValueFormInput = ({
}
const field = {
type:
isWholeDayFilterEnabled === true &&
recordFilter.type === FieldMetadataType.DATE_TIME &&
recordFilter.operand === RecordFilterOperand.IS
? FieldMetadataType.DATE
: (recordFilter.type as FieldMetadataType),
type: 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 '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { renderHook } from '@testing-library/react';
jest.mock('@/object-metadata/hooks/useObjectMetadataItem');
jest.mock('@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent');
jest.mock('@/object-record/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 '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/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 '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/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 '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/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 '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/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 '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/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 '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/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 '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/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 '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/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 '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/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 '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { getUpdatedFieldsFromRecordInput } from '@/object-record/utils/getUpdatedFieldsFromRecordInput';
const DEFAULT_DELAY_BETWEEN_MUTATIONS_MS = 50;
@@ -1,6 +1,6 @@
import { OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME } from '@/browser-event/constants/ObjectRecordOperationBrowserEventName';
import { OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME } from '@/object-record/constants/ObjectRecordOperationBrowserEventName';
import { type ObjectRecordOperation } from '@/object-record/types/ObjectRecordOperation';
import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
import { type ObjectRecordOperationBrowserEventDetail } from '@/object-record/types/ObjectRecordOperationBrowserEventDetail';
import { useEffect } from 'react';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
@@ -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 '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/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 '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/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 '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/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 '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { getUpdatedFieldsFromRecordInput } from '@/object-record/utils/getUpdatedFieldsFromRecordInput';
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
@@ -38,12 +38,7 @@ export const ObjectFilterDropdownDateInput = () => {
useApplyObjectFilterDropdownFilterValue();
const handleAbsoluteDateChange = (newPlainDate: string | null) => {
if (!isDefined(newPlainDate)) {
applyObjectFilterDropdownFilterValue('', '');
return;
}
const newFilterValue = newPlainDate;
const newFilterValue = newPlainDate ?? '';
// TODO: remove this and use getDisplayValue instead
const formattedDate = formatDateString({
@@ -96,7 +91,6 @@ export const ObjectFilterDropdownDateInput = () => {
? handleRelativeDateChange(null)
: handleAbsoluteDateChange(null);
};
const resolvedValue = objectFilterDropdownCurrentRecordFilter
? resolveDateFilter(objectFilterDropdownCurrentRecordFilter)
: null;
@@ -106,7 +100,7 @@ export const ObjectFilterDropdownDateInput = () => {
? resolvedValue
: undefined;
const safePlainDateValue: string | undefined =
const plainDateValue =
resolvedValue && typeof resolvedValue === 'string'
? resolvedValue
: undefined;
@@ -116,7 +110,7 @@ export const ObjectFilterDropdownDateInput = () => {
instanceId={`object-filter-dropdown-date-input`}
relativeDate={relativeDate}
isRelative={isRelativeOperand}
plainDateString={safePlainDateValue ?? null}
plainDateString={plainDateValue ?? null}
onChange={handleAbsoluteDateChange}
onRelativeDateChange={handleRelativeDateChange}
onClear={handleClear}
@@ -5,7 +5,6 @@ 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';
@@ -29,10 +28,6 @@ export const ObjectFilterDropdownFilterInput = ({
filterDropdownId,
recordFilterId,
}: ObjectFilterDropdownFilterInputProps) => {
const featureFlags = useFeatureFlagsMap();
const isWholeDayFilterEnabled =
featureFlags.IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED ?? false;
const fieldMetadataItemUsedInDropdown = useRecoilComponentValue(
fieldMetadataItemUsedInDropdownComponentSelector,
);
@@ -81,18 +76,6 @@ export const ObjectFilterDropdownFilterInput = ({
</>
);
} else if (filterType === 'DATE_TIME') {
if (
isWholeDayFilterEnabled &&
selectedOperandInDropdown === ViewFilterOperand.IS
) {
return (
<>
<ObjectFilterDropdownInnerSelectOperandDropdown />
<DropdownMenuSeparator />
<ObjectFilterDropdownDateInput />
</>
);
}
return (
<>
<ObjectFilterDropdownInnerSelectOperandDropdown />
@@ -1,6 +1,3 @@
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';
@@ -10,12 +7,14 @@ 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 { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { isNonEmptyString } from '@sniptt/guards';
import { Temporal } from 'temporal-polyfill';
import { DEFAULT_RELATIVE_DATE_FILTER_VALUE } from 'twenty-shared/constants';
import {
isDefined,
relativeDateFilterStringifiedSchema,
@@ -48,10 +47,6 @@ export const useApplyObjectFilterDropdownOperand = () => {
const { getRelativeDateFilterWithUserTimezone } =
useGetRelativeDateFilterWithUserTimezone();
const featureFlags = useFeatureFlagsMap();
const isWholeDayFilterEnabled =
featureFlags.IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED ?? false;
const applyObjectFilterDropdownOperand = (
newOperand: RecordFilterOperand,
) => {
@@ -111,24 +106,7 @@ export const useApplyObjectFilterDropdownOperand = () => {
recordFilterToUpsert.value,
);
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) {
if (filterValueIsEmpty || isStillRelativeFilterValue.success) {
const zonedDateToUse = Temporal.Now.zonedDateTimeISO(userTimezone);
if (recordFilterToUpsert.type === 'DATE') {
@@ -138,18 +116,11 @@ export const useApplyObjectFilterDropdownOperand = () => {
recordFilterToUpsert.value = initialNowDateFilterValue;
} else {
if (
newOperand === RecordFilterOperand.IS &&
isWholeDayFilterEnabled
) {
recordFilterToUpsert.value = zonedDateToUse
.toPlainDate()
.toString();
} else {
recordFilterToUpsert.value = zonedDateToUse
.toInstant()
.toString();
}
const initialNowDateTimeFilterValue = zonedDateToUse
.toInstant()
.toString();
recordFilterToUpsert.value = initialNowDateTimeFilterValue;
}
}
}
@@ -166,40 +137,3 @@ 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,12 +1,9 @@
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 { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { Temporal } from 'temporal-polyfill';
import { type FilterableAndTSVectorFieldType } from 'twenty-shared/types';
const activeDatePickerOperands = [
@@ -19,9 +16,6 @@ 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,
@@ -50,17 +44,6 @@ 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 '@/browser-event/hooks/useListenToObjectRecordOperationBrowserEvent';
import { useListenToObjectRecordOperationBrowserEvent } from '@/object-record/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 '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
import { type ObjectRecordOperationBrowserEventDetail } from '@/object-record/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 { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useListenToObjectRecordEventsForQuery } from '@/sse-db-event/hooks/useListenToObjectRecordEventsForQuery';
export const RecordBoardSSESubscribeEffect = () => {
const { recordBoardId } = useContext(RecordBoardContext);
@@ -13,7 +13,7 @@ export const RecordBoardSSESubscribeEffect = () => {
const queryId = `record-board-${recordBoardId}`;
useListenToEventsForQuery({
useListenToObjectRecordEventsForQuery({
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 { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useListenToObjectRecordEventsForQuery } from '@/sse-db-event/hooks/useListenToObjectRecordEventsForQuery';
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}`;
useListenToEventsForQuery({
useListenToObjectRecordEventsForQuery({
queryId,
operationSignature: {
objectNameSingular: objectMetadataItem.nameSingular,
@@ -26,11 +26,14 @@ export const useRecordCalendarMonthDaysRange = (
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const dateLocale = useRecoilValue(dateLocaleState);
if (!currentWorkspaceMember) {
throw new Error('Current workspace member not found');
}
const weekStartsOnDayIndex = (
!currentWorkspaceMember ||
currentWorkspaceMember.calendarStartDay === CalendarStartDay.SYSTEM
currentWorkspaceMember?.calendarStartDay === CalendarStartDay.SYSTEM
? CalendarStartDay[detectCalendarStartDay()]
: (currentWorkspaceMember.calendarStartDay ?? 0)
: (currentWorkspaceMember?.calendarStartDay ?? 0)
) as 0 | 1 | 2 | 3 | 4 | 5 | 6;
const firstDayOfMonth = selectedDate.with({ day: 1 });
@@ -1,6 +1,3 @@
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';
@@ -10,7 +7,8 @@ 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,
@@ -89,23 +87,7 @@ export const useGetRecordFilterDisplayValue = () => {
}
} else if (recordFilter.type === 'DATE_TIME') {
switch (recordFilter.operand) {
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:
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-17',
value: '2024-09-17T20:46:58.922Z',
fieldMetadataId: companyMockDateFieldMetadataId?.id,
displayValue: '2024-09-17',
displayValue: '2024-09-17T20:46:58.922Z',
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: {
gte: '2024-09-16T22:00:00Z',
lt: '2024-09-17T20:47:00Z',
},
},
{
createdAt: {
lt: '2024-09-17T22:00:00Z',
gte: '2024-09-17T20:46:00Z',
},
},
],
@@ -1,4 +1,4 @@
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useListenToObjectRecordEventsForQuery } from '@/sse-db-event/hooks/useListenToObjectRecordEventsForQuery';
type RecordShowPageSSESubscribeEffectProps = {
objectNameSingular: string;
@@ -11,7 +11,7 @@ export const RecordShowPageSSESubscribeEffect = ({
}: RecordShowPageSSESubscribeEffectProps) => {
const queryId = `record-show-${objectNameSingular}-${recordId}`;
useListenToEventsForQuery({
useListenToObjectRecordEventsForQuery({
queryId,
operationSignature: {
objectNameSingular,
@@ -1,10 +1,10 @@
import { useListenToObjectRecordOperationBrowserEvent } from '@/browser-event/hooks/useListenToObjectRecordOperationBrowserEvent';
import { useListenToObjectRecordOperationBrowserEvent } from '@/object-record/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 '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
import { type ObjectRecordOperationBrowserEventDetail } from '@/object-record/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 { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useListenToObjectRecordEventsForQuery } from '@/sse-db-event/hooks/useListenToObjectRecordEventsForQuery';
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}`;
useListenToEventsForQuery({
useListenToObjectRecordEventsForQuery({
queryId,
operationSignature: {
objectNameSingular: objectMetadataItem.nameSingular,
@@ -1,5 +1,5 @@
import { OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME } from '@/browser-event/constants/ObjectRecordOperationBrowserEventName';
import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
import { OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME } from '@/object-record/constants/ObjectRecordOperationBrowserEventName';
import { type ObjectRecordOperationBrowserEventDetail } from '@/object-record/types/ObjectRecordOperationBrowserEventDetail';
export const dispatchObjectRecordOperationBrowserEvent = (
detail: ObjectRecordOperationBrowserEventDetail,
@@ -37,11 +37,11 @@ export const SSEEventStreamEffect = () => {
const { triggerEventStreamDestroy } = useTriggerEventStreamDestroy();
useEffect(() => {
const isSseClientAvailable =
const isSseClientAvailabble =
!isCreatingSseEventStream && !isDestroyingEventStream;
const willCreateEventStream =
isSseClientAvailable &&
isSseClientAvailabble &&
isLoggedIn &&
isSseDbEventsEnabled &&
isDefined(currentUser) &&
@@ -51,7 +51,7 @@ export const SSEEventStreamEffect = () => {
isNonEmptyArray(objectMetadataItems);
const willDestroyEventStream =
isSseClientAvailable &&
isSseClientAvailabble &&
isNonEmptyString(sseEventStreamId) &&
shouldDestroyEventStream;
@@ -4,8 +4,8 @@ export const ON_EVENT_SUBSCRIPTION = gql`
subscription OnEventSubscription($eventStreamId: String!) {
onEventSubscription(eventStreamId: $eventStreamId) {
eventStreamId
objectRecordEventsWithQueryIds {
objectRecordEvent {
eventWithQueryIdsList {
event {
action
objectNameSingular
recordId
@@ -20,20 +20,6 @@ export const ON_EVENT_SUBSCRIPTION = gql`
}
queryIds
}
metadataEventsWithQueryIds {
metadataEvent {
type
metadataName
recordId
properties {
updatedFields
before
after
diff
}
}
queryIds
}
}
}
`;
@@ -1,54 +0,0 @@
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 '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/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 ObjectRecordEventWithQueryIds } from '~/generated-metadata/graphql';
import { type EventWithQueryIds } from '~/generated-metadata/graphql';
export const useDispatchObjectRecordEventsFromSseToBrowserEvents = () => {
const { objectMetadataItems } = useObjectMetadataItems();
const dispatchObjectRecordEventsFromSseToBrowserEvents = useCallback(
(objectRecordEventsWithQueryIds: ObjectRecordEventWithQueryIds[]) => {
const objectRecordEvents = objectRecordEventsWithQueryIds.map(
(item) => item.objectRecordEvent,
);
(eventsWithQueryIds: EventWithQueryIds[]) => {
const objectRecordEvents = eventsWithQueryIds.map((eventWithQueryIds) => {
return eventWithQueryIds.event;
});
const objectRecordEventsByObjectMetadataItemNameSingular =
groupObjectRecordSseEventsByObjectMetadataItemNameSingular({
@@ -2,19 +2,14 @@ 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 MetadataGqlOperationSignature,
type RecordGqlOperationSignature,
} from 'twenty-shared/types';
import { type RecordGqlOperationSignature } from 'twenty-shared/types';
export const useListenToEventsForQuery = ({
export const useListenToObjectRecordEventsForQuery = ({
queryId,
operationSignature,
}: {
queryId: string;
operationSignature:
| RecordGqlOperationSignature
| MetadataGqlOperationSignature;
operationSignature: RecordGqlOperationSignature;
}) => {
const changeQueryIdListenState = useRecoilCallback(
({ set, snapshot }) =>
@@ -1,5 +1,4 @@
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';
@@ -24,9 +23,6 @@ export const useTriggerEventStreamCreation = () => {
isCreatingSseEventStreamState,
);
const { dispatchMetadataEventsFromSseToBrowserEvents } =
useDispatchMetadataEventsFromSseToBrowserEvents();
const { dispatchObjectRecordEventsFromSseToBrowserEvents } =
useDispatchObjectRecordEventsFromSseToBrowserEvents();
@@ -77,55 +73,9 @@ 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 }) => {
@@ -159,12 +109,12 @@ export const useTriggerEventStreamCreation = () => {
const objectRecordEventsWithQueryIds =
result?.data?.onEventSubscription
?.objectRecordEventsWithQueryIds ?? [];
?.eventWithQueryIdsList ?? [];
const objectRecordEvents =
objectRecordEventsWithQueryIds.map(
(objectRecordEventWithQueryIds) => {
return objectRecordEventWithQueryIds.objectRecordEvent;
(eventWithQueryIds) => {
return eventWithQueryIds.event;
},
);
@@ -194,9 +144,9 @@ export const useTriggerEventStreamCreation = () => {
setIsCreatingSseEventStream(false);
},
[
dispatchMetadataEventsFromSseToBrowserEvents,
dispatchObjectRecordEventsFromSseToBrowserEvents,
setIsCreatingSseEventStream,
triggerOptimisticEffectFromSseEvents,
],
);
@@ -1,16 +1,8 @@
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
| MetadataGqlOperationSignature;
}[]
{ queryId: string; operationSignature: RecordGqlOperationSignature }[]
>({
key: 'activeQueryListenersState',
defaultValue: [],
@@ -1,16 +1,8 @@
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
| MetadataGqlOperationSignature;
}[]
{ queryId: string; operationSignature: RecordGqlOperationSignature }[]
>({
key: 'requiredQueryListenersState',
defaultValue: [],
@@ -1,20 +1,22 @@
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 ObjectRecordEventWithQueryIds } from '~/generated-metadata/graphql';
import { type EventWithQueryIds } from '~/generated-metadata/graphql';
export const dispatchObjectRecordEventsWithQueryIds = (
objectRecordEventsWithQueryIds: ObjectRecordEventWithQueryIds[],
objectRecordEventsWithQueryIds: EventWithQueryIds[],
) => {
const objectRecordEventsByQueryId: ObjectRecordEventsByQueryId = {};
for (const item of objectRecordEventsWithQueryIds) {
for (const queryId of item.queryIds) {
for (const objectRecordEventWithQueryIds of objectRecordEventsWithQueryIds) {
for (const queryId of objectRecordEventWithQueryIds.queryIds) {
if (!isDefined(objectRecordEventsByQueryId[queryId])) {
objectRecordEventsByQueryId[queryId] = [];
}
objectRecordEventsByQueryId[queryId].push(item.objectRecordEvent);
objectRecordEventsByQueryId[queryId].push(
objectRecordEventWithQueryIds.event,
);
}
}
@@ -1,66 +0,0 @@
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 '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
import { type ObjectRecordOperationBrowserEventDetail } from '@/object-record/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 '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
import { useStopWorkflowRunMutation } from '~/generated/graphql';
export const useStopWorkflowRun = () => {
@@ -1,5 +1,5 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useListenToObjectRecordEventsForQuery } from '@/sse-db-event/hooks/useListenToObjectRecordEventsForQuery';
export const WorkflowRunSSESubscribeEffect = ({
workflowRunId,
@@ -8,7 +8,7 @@ export const WorkflowRunSSESubscribeEffect = ({
}) => {
const queryId = `workflow-run-${workflowRunId}`;
useListenToEventsForQuery({
useListenToObjectRecordEventsForQuery({
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 '@/browser-event/hooks/useListenToObjectRecordOperationBrowserEvent';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useListenToObjectRecordOperationBrowserEvent } from '@/object-record/hooks/useListenToObjectRecordOperationBrowserEvent';
import { useListenToObjectRecordEventsForQuery } from '@/sse-db-event/hooks/useListenToObjectRecordEventsForQuery';
import { shouldWorkflowRefetchRequestFamilyState } from '@/workflow/states/shouldWorkflowRefetchRequestFamilyState';
export const WorkflowSSESubscribeEffect = ({
@@ -23,7 +23,7 @@ export const WorkflowSSESubscribeEffect = ({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
});
useListenToEventsForQuery({
useListenToObjectRecordEventsForQuery({
queryId,
operationSignature: {
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
+2 -6
View File
@@ -148,9 +148,8 @@ Application development commands.
- `twenty function:execute [appPath]` — Execute a logic function with a JSON payload.
- Options:
- `--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).
- `-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).
- `-p, --payload <payload>`: JSON payload to send to the function (default: `{}`).
Examples:
@@ -188,9 +187,6 @@ 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
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "twenty-sdk",
"version": "0.6.0",
"version": "0.6.0-alpha",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/sdk/index.d.ts",
@@ -63,6 +63,7 @@
"commander": "^12.0.0",
"dotenv": "^16.4.0",
"esbuild": "^0.25.0",
"esbuild-svelte": "^0.9.4",
"fast-glob": "^3.3.0",
"fs-extra": "^11.2.0",
"graphql": "^16.8.1",
@@ -95,6 +96,7 @@
"@vitest/browser-playwright": "^4.0.18",
"playwright": "^1.56.1",
"storybook": "^10.1.11",
"svelte": "^4.2.19",
"ts-morph": "^25.0.0",
"tsx": "^4.7.0",
"twenty-shared": "workspace:*",
@@ -54,9 +54,21 @@ const twentySharedAliases = Object.fromEntries(
]),
);
const svelteRoot = path.join(rootNodeModules, 'svelte');
const storyAlias = {
react: path.join(rootNodeModules, 'react'),
'react-dom': path.join(rootNodeModules, 'react-dom'),
vue: path.join(rootNodeModules, 'vue'),
svelte: svelteRoot,
'svelte/internal': path.join(
svelteRoot,
'src/runtime/internal/index.js',
),
'svelte/internal/disclose-version': path.join(
svelteRoot,
'src/runtime/internal/disclose-version/index.js',
),
'@/sdk': sdkIndividualIndex,
'twenty-sdk/ui': twentyUiIndividualIndex,
...twentySharedAliases,
@@ -74,6 +86,8 @@ const STORY_COMPONENTS = [
'mui-example.front-component',
'twenty-ui-example.front-component',
'sdk-context-example.front-component',
'vue-example.front-component',
'svelte-example.front-component',
];
const resolveEntryPoints = (): Record<string, string> => {
@@ -28,7 +28,6 @@ export const EXPECTED_MANIFEST: Manifest = {
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
yarnLockChecksum: 'd41d8cd98f00b204e9800998ecf8427e',
packageJsonChecksum: '2851d0e2c3621a57e1fd103a245b6fde',
apiClientChecksum: null,
},
frontComponents: [
{
@@ -412,7 +411,6 @@ export const EXPECTED_MANIFEST: Manifest = {
],
views: [],
navigationMenuItems: [],
pageLayouts: [],
logicFunctions: [
{
builtHandlerChecksum: '[checksum]',
@@ -11,7 +11,6 @@ export const EXPECTED_MANIFEST: Manifest = {
defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002',
packageJsonChecksum: '93ae1e2eb3db18351d06f43550700dcc',
yarnLockChecksum: 'd41d8cd98f00b204e9800998ecf8427e',
apiClientChecksum: null,
},
publicAssets: [],
fields: [],
@@ -142,7 +141,6 @@ export const EXPECTED_MANIFEST: Manifest = {
],
views: [],
navigationMenuItems: [],
pageLayouts: [],
roles: [
{
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000040',
@@ -129,7 +129,6 @@ 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',
@@ -148,20 +147,15 @@ export const registerCommands = (program: Command): void => {
async (
appPath?: string,
options?: {
postInstall?: boolean;
payload?: string;
functionUniversalIdentifier?: string;
functionName?: string;
},
) => {
if (
!options?.postInstall &&
!options?.functionUniversalIdentifier &&
!options?.functionName
) {
if (!options?.functionUniversalIdentifier && !options?.functionName) {
console.error(
chalk.red(
'Error: Either --postInstall or --functionName (-n) or --functionUniversalIdentifier (-u) is required.',
'Error: Either --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 { readManifestFromFile } from '@/cli/utilities/build/manifest/manifest-reader';
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
export class AppUninstallCommand {
private apiService = new ApiService();
@@ -25,7 +25,7 @@ export class AppUninstallCommand {
process.exit(1);
}
const manifest = await readManifestFromFile(appPath);
const { manifest } = await buildManifest(appPath);
if (!manifest) {
return { success: false, error: 'Build failed' };
@@ -3,7 +3,6 @@ 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';
@@ -114,16 +113,6 @@ 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,20 +3,18 @@ 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 { readManifestFromFile } from '@/cli/utilities/build/manifest/manifest-reader';
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
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;
@@ -32,7 +30,7 @@ export class LogicFunctionExecuteCommand {
process.exit(1);
}
const manifest = await readManifestFromFile(appPath);
const { manifest } = await buildManifest(appPath);
if (!manifest) {
console.error(chalk.red('Failed to build manifest.'));
@@ -56,12 +54,6 @@ export class LogicFunctionExecuteCommand {
);
const targetFunction = appFunctions.find((fn) => {
if (postInstall) {
return (
fn.universalIdentifier ===
manifest.application.postInstallLogicFunctionUniversalIdentifier
);
}
if (functionUniversalIdentifier) {
return fn.universalIdentifier === functionUniversalIdentifier;
}
@@ -72,9 +64,7 @@ export class LogicFunctionExecuteCommand {
});
if (!targetFunction) {
const identifier = postInstall
? 'post install'
: functionUniversalIdentifier || functionName;
const identifier = 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 { readManifestFromFile } from '@/cli/utilities/build/manifest/manifest-reader';
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
export class LogicFunctionLogsCommand {
private apiService = new ApiService();
@@ -16,7 +16,7 @@ export class LogicFunctionLogsCommand {
functionName?: string;
}): Promise<void> {
try {
const manifest = await readManifestFromFile(appPath);
const { manifest } = await buildManifest(appPath);
if (!manifest) {
process.exit(1);
@@ -8,7 +8,6 @@ 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';
@@ -215,10 +214,7 @@ export const createLogicFunctionsWatcher = (
externalModules: LOGIC_FUNCTION_EXTERNAL_MODULES,
fileFolder: FileFolder.BuiltLogicFunction,
platform: 'node',
extraPlugins: [
createTypecheckPlugin(options.appPath),
createSdkGeneratedResolverPlugin(options.appPath),
],
extraPlugins: [createSdkGeneratedResolverPlugin(options.appPath)],
banner: NODE_ESM_CJS_BANNER,
},
});
@@ -233,7 +229,6 @@ 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' || event === 'unlinkDir') {
if (event === 'addDir') {
return;
}
@@ -1,4 +1,5 @@
import type * as esbuild from 'esbuild';
import sveltePlugin from 'esbuild-svelte';
import { createJsxRuntimeRemoteWrapperPlugin } from '../jsx-runtime-remote-wrapper-plugin';
import { jsxTransformToRemoteDomWorkerFormatPlugin } from '../jsx-transform-to-remote-dom-worker-format-plugin';
@@ -12,6 +13,7 @@ type GetFrontComponentBuildPluginsOptions = {
export const getFrontComponentBuildPlugins = (
options?: GetFrontComponentBuildPluginsOptions,
): esbuild.Plugin[] => [
sveltePlugin(),
createJsxRuntimeRemoteWrapperPlugin(
options?.usePreact ? { usePreact: true } : undefined,
),
@@ -1,9 +1,34 @@
import { type FrontComponentFramework } from 'twenty-shared/application';
const DEFINE_FRONT_COMPONENT_IMPORT_PATTERN =
/import\s*\{\s*defineFrontComponent\s*\}\s*from\s*['"][^'"]+['"];?\n?/g;
const DEFINE_FRONT_COMPONENT_EXPORT_PATTERN =
/export\s+default\s+defineFrontComponent\s*\(\s*\{[^}]*component\s*:\s*(\w+)[^}]*\}\s*\)\s*;?/s;
const FRAMEWORK_PATTERN = /framework\s*:\s*['"](\w+)['"]/;
const extractFramework = (sourceCode: string): FrontComponentFramework => {
const match = sourceCode.match(FRAMEWORK_PATTERN);
return (match?.[1] as FrontComponentFramework) ?? 'react';
};
const REACT_MOUNT_IMPORTS =
`import { createRoot as __createRoot } from 'react-dom/client';\n` +
`import { jsx as __frontComponentJsx } from 'react/jsx-runtime';\n`;
const generateRenderExport = (
framework: FrontComponentFramework,
componentName: string,
): string => {
if (framework === 'react') {
return `export default function __renderFrontComponent(__container) { __createRoot(__container).render(__frontComponentJsx(${componentName}, {})); }`;
}
return `export default function __renderFrontComponent(__container) { ${componentName}(__container); }`;
};
export const unwrapDefineFrontComponentToDirectExport = (
sourceCode: string,
): string => {
@@ -18,6 +43,7 @@ export const unwrapDefineFrontComponentToDirectExport = (
if (defineFrontComponentMatch) {
const wrappedComponentName = defineFrontComponentMatch[1];
const framework = extractFramework(sourceCode);
const exportedComponentDeclarationPattern = new RegExp(
`export\\s+(const|function)\\s+${wrappedComponentName}\\b`,
@@ -28,14 +54,13 @@ export const unwrapDefineFrontComponentToDirectExport = (
`$1 ${wrappedComponentName}`,
);
transformedSource =
`import { createRoot as __createRoot } from 'react-dom/client';\n` +
`import { jsx as __frontComponentJsx } from 'react/jsx-runtime';\n` +
transformedSource;
if (framework === 'react') {
transformedSource = REACT_MOUNT_IMPORTS + transformedSource;
}
transformedSource = transformedSource.replace(
DEFINE_FRONT_COMPONENT_EXPORT_PATTERN,
`export default function __renderFrontComponent(__container) { __createRoot(__container).render(__frontComponentJsx(${wrappedComponentName}, {})); }`,
generateRenderExport(framework, wrappedComponentName),
);
}
@@ -1,103 +0,0 @@
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);
}
}
}
@@ -1,84 +0,0 @@
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,7 +13,6 @@ 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 = {
@@ -35,7 +34,6 @@ const validManifest: Manifest = {
publicAssets: [],
views: [],
navigationMenuItems: [],
pageLayouts: [],
};
describe('manifestValidate', () => {
@@ -11,7 +11,6 @@ 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';
@@ -26,7 +25,6 @@ import {
type Manifest,
type NavigationMenuItemManifest,
type ObjectManifest,
type PageLayoutManifest,
type RoleManifest,
type ViewManifest,
} from 'twenty-shared/application';
@@ -69,7 +67,6 @@ export const buildManifest = async (
const publicAssets: AssetManifest[] = [];
const views: ViewManifest[] = [];
const navigationMenuItems: NavigationMenuItemManifest[] = [];
const pageLayouts: PageLayoutManifest[] = [];
const applicationFilePaths: string[] = [];
const objectsFilePaths: string[] = [];
@@ -80,7 +77,6 @@ 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');
@@ -105,7 +101,6 @@ export const buildManifest = async (
...extract.config,
yarnLockChecksum: null,
packageJsonChecksum: null,
apiClientChecksum: null,
};
errors.push(...extract.errors);
applicationFilePaths.push(relativePath);
@@ -208,6 +203,7 @@ export const buildManifest = async (
const config: FrontComponentManifest = {
...rest,
framework: rest.framework ?? 'react',
componentName: component.name,
sourceComponentPath: relativeFilePath,
builtComponentPath: relativeFilePath.replace(/\.tsx?$/, '.mjs'),
@@ -244,21 +240,6 @@ 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;
@@ -299,7 +280,6 @@ export const buildManifest = async (
publicAssets,
views,
navigationMenuItems,
pageLayouts,
};
const entityFilePaths: EntityFilePaths = {
@@ -312,7 +292,6 @@ export const buildManifest = async (
publicAssets: publicAssetsFilePaths,
views: viewsFilePaths,
navigationMenuItems: navigationMenuItemsFilePaths,
pageLayouts: pageLayoutsFilePaths,
};
return { manifest, filePaths: entityFilePaths, errors };
@@ -9,7 +9,6 @@ export enum TargetFunction {
DefineFrontComponent = 'defineFrontComponent',
DefineView = 'defineView',
DefineNavigationMenuItem = 'defineNavigationMenuItem',
DefinePageLayout = 'definePageLayout',
}
export enum ManifestEntityKey {
@@ -22,7 +21,6 @@ export enum ManifestEntityKey {
PublicAssets = 'publicAssets',
Views = 'views',
NavigationMenuItems = 'navigationMenuItems',
PageLayouts = 'pageLayouts',
}
export type EntityFilePaths = Record<ManifestEntityKey, string[]>;
@@ -40,7 +38,6 @@ 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 => {
@@ -1,21 +0,0 @@
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,4 +1,3 @@
import crypto from 'crypto';
import { relative } from 'path';
import { type Manifest, OUTPUT_DIR } from 'twenty-shared/application';
import { FileFolder } from 'twenty-shared/types';
@@ -89,30 +88,5 @@ 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' || event === 'unlinkDir') {
if (event === 'addDir') {
return;
}
@@ -22,7 +22,6 @@ export class ClientService {
authToken?: string;
}): Promise<void> {
const outputPath = this.resolveGeneratedPath(appPath);
const tempPath = `${outputPath}.tmp`;
const getSchemaResponse = await this.apiService.getSchema({ authToken });
@@ -34,12 +33,12 @@ export class ClientService {
const { data: schema } = getSchemaResponse;
await fs.ensureDir(tempPath);
await fs.emptyDir(tempPath);
await fs.ensureDir(outputPath);
await fs.emptyDir(outputPath);
await generate({
schema,
output: tempPath,
output: outputPath,
scalarTypes: {
DateTime: 'string',
JSON: 'Record<string, unknown>',
@@ -47,10 +46,7 @@ export class ClientService {
},
});
await this.injectTwentyClient(tempPath);
await fs.remove(outputPath);
await fs.move(tempPath, outputPath);
await this.injectTwentyClient(outputPath);
}
private resolveGeneratedPath(appPath: string): string {
@@ -74,16 +70,22 @@ const defaultOptions: ClientOptions = {
export default class Twenty {
private client: Client;
private apiUrl: string;
private authorizationToken: string;
constructor(options?: ClientOptions) {
this.client = createClient({
const merged: ClientOptions = {
...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 }) {
@@ -93,6 +95,41 @@ 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,7 +69,6 @@ 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,10 +7,7 @@ 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,
type FileBuiltEvent,
} from '@/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step';
import { StartWatchersOrchestratorStep } 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';
@@ -72,7 +69,7 @@ export class DevModeOrchestrator {
this.startWatchersStep = new StartWatchersOrchestratorStep({
...stepDeps,
scheduleSync: this.scheduleSync.bind(this),
onFileBuilt: this.handleFileBuilt.bind(this),
uploadFilesStep: this.uploadFilesStep,
});
}
@@ -93,16 +90,6 @@ 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);
@@ -163,9 +150,11 @@ export class DevModeOrchestrator {
}
}
const objectsOrFieldsChanged = this.state.hasObjectsOrFieldsChanged(
buildResult.manifest!,
);
if (this.state.hasObjectsOrFieldsChanged(buildResult.manifest!)) {
await this.generateApiClientStep.execute({
appPath: this.state.appPath,
});
}
await this.uploadFilesStep.waitForUploads();
@@ -174,16 +163,6 @@ 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,23 +4,15 @@ 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;
};
@@ -29,25 +21,24 @@ export class StartWatchersOrchestratorStep {
private state: OrchestratorState;
private scheduleSync: () => void;
private notify: () => void;
private onFileBuilt: (event: FileBuiltEvent) => void;
private uploadFilesStep: UploadFilesOrchestratorStep;
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;
onFileBuilt: (event: FileBuiltEvent) => void;
uploadFilesStep: UploadFilesOrchestratorStep;
}) {
this.state = options.state;
this.scheduleSync = options.scheduleSync;
this.notify = options.notify;
this.onFileBuilt = options.onFileBuilt;
this.uploadFilesStep = options.uploadFilesStep;
}
async start(): Promise<void> {
@@ -83,8 +74,6 @@ export class StartWatchersOrchestratorStep {
}
async close(): Promise<void> {
this.tscWatcher?.close();
await Promise.all([
this.manifestWatcher?.close(),
this.logicFunctionsWatcher?.close(),
@@ -128,20 +117,32 @@ export class StartWatchersOrchestratorStep {
this.notify();
}
private handleFileBuilt(event: FileBuiltEvent): void {
private handleFileBuilt({
fileFolder,
builtPath,
sourcePath,
checksum,
}: {
fileFolder: FileFolder;
builtPath: string;
sourcePath: string;
checksum: string;
}): void {
this.state.addEvent({
message: `Successfully built ${event.builtPath}`,
message: `Successfully built ${builtPath}`,
status: 'success',
});
this.state.steps.uploadFiles.output.builtFileInfos.set(event.builtPath, {
checksum: event.checksum,
builtPath: event.builtPath,
sourcePath: event.sourcePath,
fileFolder: event.fileFolder,
this.state.steps.uploadFiles.output.builtFileInfos.set(builtPath, {
checksum,
builtPath,
sourcePath,
fileFolder,
});
this.onFileBuilt(event);
if (this.state.steps.uploadFiles.output.fileUploader) {
this.uploadFilesStep.uploadFile(builtPath, sourcePath, fileFolder);
}
this.notify();
this.scheduleSync();
@@ -152,7 +153,6 @@ export class StartWatchersOrchestratorStep {
frontComponents: string[],
): Promise<void> {
await Promise.all([
this.startTscWatcher(),
this.startLogicFunctionsWatcher(logicFunctions),
this.startFrontComponentsWatcher(frontComponents),
this.startAssetWatcher(),
@@ -207,31 +207,4 @@ 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,13 +3,7 @@ import {
type OrchestratorStateBuiltFileInfo,
} from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
import { FileUploader } from '@/cli/utilities/file/file-uploader';
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'];
import { type FileFolder } from 'twenty-shared/types';
export type UploadFilesOrchestratorStepOutput = {
fileUploader: FileUploader | null;
@@ -109,48 +103,6 @@ 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,7 +97,6 @@ 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,6 +46,7 @@ export default defineLogicFunction({
// databaseEventTriggerSettings: {
// eventName: 'objectName.created',
// },
],
});
`;
};

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