Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e22709e28 |
@@ -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,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**: key–value 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 logické funkce
|
||||
├── logic-functions/
|
||||
│ ├── hello-world.ts # Example logic function
|
||||
│ └── post-install.ts # Post-install logic function
|
||||
│ └── hello-world.ts # Ukázková logická 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üssel–Wert-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 chiave–valore 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 chave–valor 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 cheie–valoare 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 anahtar–değ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
|
||||
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
export const METADATA_OPERATION_BROWSER_EVENT_NAME =
|
||||
'metadata-operation-browser-event';
|
||||
-53
@@ -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;
|
||||
};
|
||||
-9
@@ -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>;
|
||||
};
|
||||
-14
@@ -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,
|
||||
}),
|
||||
);
|
||||
};
|
||||
+3
-11
@@ -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) ||
|
||||
|
||||
-1
@@ -6,7 +6,6 @@ export const FIND_ONE_FRONT_COMPONENT = gql`
|
||||
id
|
||||
name
|
||||
applicationId
|
||||
builtComponentChecksum
|
||||
applicationTokenPair {
|
||||
applicationAccessToken {
|
||||
token
|
||||
|
||||
-37
@@ -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,
|
||||
});
|
||||
};
|
||||
-54
@@ -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}`;
|
||||
};
|
||||
+1
-11
@@ -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
-2
@@ -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';
|
||||
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+2
-2
@@ -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';
|
||||
|
||||
+3
-9
@@ -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}
|
||||
|
||||
-17
@@ -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 />
|
||||
|
||||
+10
-76
@@ -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
-18
@@ -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);
|
||||
|
||||
+2
-2
@@ -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';
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+6
-3
@@ -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 });
|
||||
|
||||
+3
-21
@@ -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)) {
|
||||
|
||||
+4
-4
@@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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 = () => {
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
|
||||
+2
-16
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
-54
@@ -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 };
|
||||
};
|
||||
+6
-6
@@ -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({
|
||||
|
||||
+3
-8
@@ -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 }) =>
|
||||
+6
-56
@@ -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: [],
|
||||
|
||||
+2
-10
@@ -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: [],
|
||||
|
||||
+7
-5
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-66
@@ -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
-1
@@ -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 = () => {
|
||||
|
||||
+2
-2
@@ -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
-3
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
-2
@@ -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]',
|
||||
|
||||
-2
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -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,
|
||||
),
|
||||
|
||||
+30
-5
@@ -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) };
|
||||
});
|
||||
},
|
||||
});
|
||||
-2
@@ -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> {
|
||||
|
||||
+24
-51
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-49
@@ -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
Reference in New Issue
Block a user