Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 6abe8059c9 Null filter value in not clause crashes GraphQL query parser with TypeError
https://sonarly.com/issue/13694?type=bug

The GraphQL filter parser crashes with `TypeError: Cannot convert undefined or null to object` when a `not` filter contains a field set to `null` (e.g., `{ not: { deletedAt: null } }`), because `Object.entries(null)` is called without a null guard.

Fix: Added a null guard in `GraphqlQueryFilterFieldParser.parse()` before the `Object.entries(filterValue)` call on line 72. When `filterValue` is `null` or `undefined` (e.g., from a filter like `{ not: { deletedAt: null } }`), the code now throws a `GraphqlQueryRunnerException` with `INVALID_QUERY_INPUT` code and a user-friendly message suggesting the correct syntax (`{ is: "NULL" }`).

This follows the existing patterns in the same file:
- Uses `isDefined()` from `twenty-shared/utils` (already imported)
- Uses `GraphqlQueryRunnerException` with `INVALID_QUERY_INPUT` (same pattern as the array operator check below)
- Uses `msg` from `@lingui/core/macro` for the user-friendly message (already imported)

The `INVALID_QUERY_INPUT` exception code is handled by `graphqlQueryRunnerExceptionHandler` and mapped to a `UserInputError`, which is returned as a proper GraphQL error response instead of crashing the server with an unhandled `TypeError`.

**This also resolves the monitoring noise**: the unhandled `TypeError` that was being captured by Sentry is now a properly handled `UserInputError` — a client-side input validation error that should not be reported to Sentry.
2026-03-12 13:43:12 +00:00
WeikoandGitHub 06d4d62e90 Move 1.19 backfill pagelayout and views to 1.20 (#18582) 2026-03-12 13:46:07 +01:00
WeikoandGitHub eb4665bc98 Create missing standard table and fields widget views (#18543) 2026-03-12 13:28:05 +01:00
f19fcd0010 i18n - translations (#18578)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-12 13:23:54 +01:00
Lucas BordeauandGitHub cb3e32df86 Fix AI demo workspace skill (#18575)
This PR fixes what allows to have a working demo workspace skill.

- Skill updated many times into something that works
- Fixed infinite loop in AI chat by memoizing ai-sdk output
- Finished navigateToView implementation
- Increased MAX_STEPS to 300 so the chat don't quit in the middle of a
long running skill
- Added CreateManyRelationFields
2026-03-12 13:19:01 +01:00
Hamza FaidiandGitHub db5b4d9c6c fix: replace unsafe JSON.parse casts with parseJson in filter dropdowns (#18513)
## Problem

Four filter dropdown components were calling `JSON.parse(filter.value)
as string[]` to parse stored filter state. This throws a `SyntaxError`
if the value is malformed (truncated URL, stale localStorage, migration
artifact), crashing the entire dropdown with no recovery.

## Solution

Replace with the existing `parseJson<string[]>` utility from
`twenty-shared`, which wraps `JSON.parse` in a try/catch and returns
`null` on failure. The `?? []` fallback gracefully degrades to an empty
selection instead of crashing.

All four files had an explicit `// TODO: replace by a safe parse`
marking this as a known issue.

## Testing


No new tests — `parseJson` is already tested in `twenty-shared`. No new
logic introduced.

## issue link 
#18514
2026-03-12 13:15:22 +01:00
Charles BochetandGitHub 660536d6bb Fix onboarding flow: workspace creation modal and invite team skip (#18577)
## Summary

- **Fix create-profile modal not showing after workspace creation**:
After activating a workspace, `CreateWorkspace.onSubmit` called
`refreshObjectMetadataItems()` which only updated the
`objectMetadataItemsState` atom but never marked the metadata store as
ready (`metadataStoreState` stayed at `'empty'`). Since `MetadataGater`
excludes `CreateWorkspace` but not `CreateProfile` from its loading
check, navigating to `/create/profile` triggered the skeleton loader
instead of the modal. The fix adds the full metadata pipeline after
refresh — `updateDraft('objectMetadataItems')` + `applyChanges()` for
objects, and `fetchAndLoadIndexViews()` for views — so
`isAppMetadataReady` is `true` before navigation.

- **Fix invite-team "Skip" not persisting to server**: Clicking "Skip"
on the invite-team page called `setNextOnboardingStatus()` which only
updated the local Jotai atom. The early return for empty emails bypassed
`sendInvitation`, so the server never cleared the
`ONBOARDING_INVITE_TEAM_PENDING` user var. On page refresh,
`GetCurrentUser` returned `INVITE_TEAM` and the user was stuck. The fix
removes the early return so `sendInvitation({ emails: [] })` always runs
— the server handles empty arrays fine and clears the pending flag.
2026-03-12 13:15:05 +01:00
e8f8189167 [COMMAND MENU ITEMS] Add engine component key (#18554)
## PR Description

In the process of migrating all the existing commands to the backend, we
stumbled across a couple of problems that made us reconsider the full
migration. This PR introduces a way for command menu items to bypass
front components and to directly reference a frontend component from
twenty front.

It:
- Introduces a `engineFrontComponentKey` field on `CommandMenuItem` as
an alternative to `frontComponentId` and `workflowVersionId`, allowing
command menu items to reference frontend components by key directly
rather than requiring a FrontComponent entity
- Updates the DB constraint to allow exactly one of `workflowVersionId`,
`frontComponentId`, or `engineFrontComponentKey`

### All standard command menu items from the frontend which use
`standardFrontComponentKey`

These are all commands that execute a GraphQL query or a mutation.
Two mains concerned have been raised that made us go with this
(temporary) architecture instead:
- If those commands are part of the standard application, they can only
alter objects from that application and not custom objects.
- We would need to implement a way to trigger optimistic rendering from
the front components, which might take some time to implement.

List:
- Create new record
- Delete (single record)
- Delete records (multiple)
- Restore record
- Restore records (multiple)
- Permanently destroy record
- Permanently destroy records (multiple)
- Add to favorites
- Remove from favorites
- Merge records
- Duplicate Dashboard
- Save Dashboard
- Save Page Layout
- Activate Workflow
- Deactivate Workflow
- Discard Draft (workflow)
- Test Workflow
- Tidy up workflow
- Duplicate Workflow
- Stop (workflow run)
- Use as draft (workflow version)

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-12 13:14:45 +01:00
martmullandGitHub 78473a606a Fix app dev flickering (#18562)
- fix ticker issue
- fix too many rendering
2026-03-12 11:58:44 +01:00
neo773andGitHub b21fb4aa6f Fix PDF Upload edge case (#18533)
we were using an older version of `file-type` which has limited support
for PDF as it's a complex spec
Updated to latest version which includes support for plugins and added
`@file-type/pdf` which has extensive spec compliant detection approach

fixes TWENTY-SERVER-FAN
2026-03-12 10:34:24 +00:00
38664249cf i18n - translations (#18576)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-12 11:24:07 +01:00
Baptiste DevessierandGitHub 69542898a1 Display a single Add a Section button (#18563)
- Display a single Add a Section button at the end of the list
- Move other buttons to the section's dropdown menu


https://github.com/user-attachments/assets/b51d8846-635a-477a-9205-bf3266cfcff4
2026-03-12 10:01:48 +00:00
09beddb63d i18n - docs translations (#18566)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-12 10:55:44 +01:00
Hamza FaidiandGitHub 2eac82c207 fix(front): stabilize downloadFile unit test and return promise chain (#18484)
# Description

## What this PR fixes
This PR fixes a flaky/skipped unit test for
[downloadFile](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html)
and aligns the test with the actual implementation.

## Changes made
Updated
[downloadFile](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html)
test to validate file-saver behavior instead of DOM anchor creation.
Mocked
[saveAs](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html)
from file-saver and asserted it is called with the fetched blob and
filename.
Added proper async assertions for:
successful file download
failed fetch path ([status !==
200](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html))
rejecting with Failed downloading file
Updated
[downloadFile](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html)
implementation to return the fetch promise chain so callers/tests can
await it reliably.

## Related Issue
Closes #18485
2026-03-12 09:00:50 +01:00
281 changed files with 7629 additions and 2793 deletions
@@ -297,46 +297,60 @@ yarn command:prod cron:workflow:automated-cron-trigger
**وضع بيئي فقط:** إذا كنت قد ضبطت `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`، فأضف هذه المتغيرات إلى ملف `.env` الخاص بك بدلاً من ذلك.
</Warning>
## الوظائف المنطقية
## الوظائف المنطقية ومفسر الشيفرة
تدعم Twenty الوظائف المنطقية لعمليات سير العمل والمنطق المخصص. يتم تكوين بيئة التنفيذ عبر متغير البيئة `SERVERLESS_TYPE`.
تدعم Twenty الوظائف المنطقية لعمليات سير العمل ومفسر الشيفرة لتحليل بيانات الذكاء الاصطناعي. كلاهما يقوم بتشغيل الشيفرة المقدمة من المستخدم ويتطلب تهيئة صريحة لأغراض الأمان.
### الإعدادات الافتراضية للأمان
**في بيئة الإنتاج (NODE_ENV=production):** يكون الإعداد الافتراضي لكل من الوظائف المنطقية ومفسر الشيفرة هو **معطل**. يجب تمكينهما صراحة باستخدام `LOGIC_FUNCTION_TYPE` و`CODE_INTERPRETER_TYPE` إذا كنت تحتاج إلى هذه الميزات.
**في بيئة التطوير (NODE_ENV=development):** يكون الإعداد الافتراضي لكليهما **LOCAL** لتسهيل التشغيل محلياً.
<Warning>
**ملاحظة أمنية:** يقوم برنامج التشغيل المحلي (`SERVERLESS_TYPE=LOCAL`) بتشغيل الشيفرة مباشرةً على المضيف ضمن عملية Node.js من دون عزل. يجب استخدامه فقط للشيفرة الموثوقة أثناء التطوير. بالنسبة لعمليات النشر الإنتاجية التي تتعامل مع شيفرة غير موثوق بها، نوصي بشدة باستخدام `SERVERLESS_TYPE=LAMBDA` أو `SERVERLESS_TYPE=DISABLED`.
**ملاحظة أمنية:** يقوم برنامج التشغيل المحلي (`LOGIC_FUNCTION_TYPE=LOCAL` أو `CODE_INTERPRETER_TYPE=LOCAL`) بتشغيل الشيفرة مباشرة على المضيف ضمن عملية Node.js من دون عزل. يجب استخدامه فقط للشيفرة الموثوقة أثناء التطوير. لعمليات النشر الإنتاجية التي تتعامل مع شيفرة غير موثوقة، استخدم `LOGIC_FUNCTION_TYPE=LAMBDA` أو `CODE_INTERPRETER_TYPE=E2B` (مع وضع الحماية)، أو اتركهما مُعطَّلَيْن.
</Warning>
### برامج التشغيل المتاحة
### الوظائف المنطقية - برامج التشغيل المتاحة
| برنامج التشغيل | متغير البيئة | حالة الاستخدام | مستوى الأمان |
| -------------- | -------------------------- | ------------------------------- | ----------------------------- |
| معطل | `SERVERLESS_TYPE=DISABLED` | تعطيل الوظائف المنطقية بالكامل | غير متاح |
| محلي | `SERVERLESS_TYPE=LOCAL` | بيئات التطوير والبيئات الموثوقة | منخفض (من دون عزل) |
| Lambda | `SERVERLESS_TYPE=LAMBDA` | الإنتاج مع شيفرة غير موثوق بها | مرتفع (عزل على مستوى الأجهزة) |
| برنامج التشغيل | متغير البيئة | حالة الاستخدام | مستوى الأمان |
| -------------- | ------------------------------ | ------------------------------- | ----------------------------- |
| معطل | `LOGIC_FUNCTION_TYPE=DISABLED` | تعطيل الوظائف المنطقية بالكامل | غير متاح |
| محلي | `LOGIC_FUNCTION_TYPE=LOCAL` | بيئات التطوير والبيئات الموثوقة | منخفض (من دون عزل) |
| Lambda | `LOGIC_FUNCTION_TYPE=LAMBDA` | الإنتاج مع شيفرة غير موثوق بها | مرتفع (عزل على مستوى الأجهزة) |
### التكوين الموصى به
### الوظائف المنطقية - الإعداد الموصى به
**للتطوير:**
```bash
SERVERLESS_TYPE=LOCAL # default
LOGIC_FUNCTION_TYPE=LOCAL # default when NODE_ENV=development
```
**للإنتاج (AWS):**
```bash
SERVERLESS_TYPE=LAMBDA
SERVERLESS_LAMBDA_REGION=us-east-1
SERVERLESS_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
LOGIC_FUNCTION_TYPE=LAMBDA
LOGIC_FUNCTION_LAMBDA_REGION=us-east-1
LOGIC_FUNCTION_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID=your-access-key
LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
```
**لتعطيل الوظائف المنطقية:**
```bash
SERVERLESS_TYPE=DISABLED
LOGIC_FUNCTION_TYPE=DISABLED # default when NODE_ENV=production
```
### مفسر الشيفرة - برامج التشغيل المتاحة
| برنامج التشغيل | متغير البيئة | حالة الاستخدام | مستوى الأمان |
| -------------- | -------------------------------- | ------------------------------------- | ------------------------ |
| معطل | `CODE_INTERPRETER_TYPE=DISABLED` | تعطيل تنفيذ الشيفرة بالذكاء الاصطناعي | غير متاح |
| محلي | `CODE_INTERPRETER_TYPE=LOCAL` | للتطوير فقط | منخفض (من دون عزل) |
| E2B | `CODE_INTERPRETER_TYPE=E_2_B` | الإنتاج مع تنفيذ ضمن صندوق رمل معزول | مرتفعة (صندوق رمل معزول) |
<Note>
عند استخدام `SERVERLESS_TYPE=DISABLED`، ستؤدي أي محاولة لتنفيذ وظيفة منطقية إلى إرجاع خطأ. يكون هذا مفيدًا إذا كنت ترغب في تشغيل Twenty من دون إمكانات الوظائف المنطقية.
عند استخدام `LOGIC_FUNCTION_TYPE=DISABLED` أو `CODE_INTERPRETER_TYPE=DISABLED`، سترجع أي محاولة للتنفيذ خطأً. يكون هذا مفيدًا إذا كنت ترغب في تشغيل Twenty من دون هذه الإمكانات.
</Note>
@@ -297,46 +297,60 @@ yarn command:prod cron:workflow:automated-cron-trigger
**Nur-Umgebungsmodus:** Wenn Sie `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false` setzen, fügen Sie diese Variablen stattdessen Ihrer `.env`-Datei hinzu.
</Warning>
## Logikfunktionen
## Logikfunktionen & Code-Interpreter
Twenty unterstützt Logikfunktionen für Workflows und benutzerdefinierte Logik. Die Ausführungsumgebung wird über die Umgebungsvariable `SERVERLESS_TYPE` konfiguriert.
Twenty unterstützt Logikfunktionen für Workflows und den Code-Interpreter für KI-Datenanalyse. Beide führen vom Benutzer bereitgestellten Code aus und erfordern aus Sicherheitsgründen eine explizite Konfiguration.
### Sicherheits-Standardeinstellungen
**In Produktion (NODE_ENV=production):** Sowohl Logikfunktionen als auch der Code-Interpreter sind standardmäßig **deaktiviert**. Sie müssen sie, wenn Sie diese Funktionen benötigen, explizit mit `LOGIC_FUNCTION_TYPE` und `CODE_INTERPRETER_TYPE` aktivieren.
**In der Entwicklung (NODE_ENV=development):** Beide sind der Einfachheit halber beim lokalen Betrieb standardmäßig **LOCAL**.
<Warning>
**Sicherheitshinweis:** Der lokale Treiber (`SERVERLESS_TYPE=LOCAL`) führt Code ohne Sandbox direkt auf dem Host in einem Node.js-Prozess aus. Er sollte nur für vertrauenswürdigen Code in der Entwicklung verwendet werden. Für Produktivbereitstellungen, die nicht vertrauenswürdigen Code verarbeiten, empfehlen wir nachdrücklich, `SERVERLESS_TYPE=LAMBDA` oder `SERVERLESS_TYPE=DISABLED` zu verwenden.
**Sicherheitshinweis:** Der lokale Treiber (`LOGIC_FUNCTION_TYPE=LOCAL` oder `CODE_INTERPRETER_TYPE=LOCAL`) führt Code ohne Sandbox direkt auf dem Host in einem Node.js-Prozess aus. Er sollte nur für vertrauenswürdigen Code in der Entwicklung verwendet werden. Für Produktionsbereitstellungen, die nicht vertrauenswürdigen Code verarbeiten, verwenden Sie `LOGIC_FUNCTION_TYPE=LAMBDA` oder `CODE_INTERPRETER_TYPE=E2B` (mit Sandbox-Isolierung), oder lassen Sie sie deaktiviert.
</Warning>
### Verfügbare Treiber
### Logikfunktionen - Verfügbare Treiber
| Treiber | Umgebungsvariable | Anwendungsfall | Sicherheitsstufe |
| ----------- | -------------------------- | -------------------------------------------------- | ---------------------------------- |
| Deaktiviert | `SERVERLESS_TYPE=DISABLED` | Logikfunktionen vollständig deaktivieren | N/A |
| Lokal | `SERVERLESS_TYPE=LOCAL` | Entwicklung und vertrauenswürdige Umgebungen | Niedrig (keine Sandbox) |
| Lambda | `SERVERLESS_TYPE=LAMBDA` | Produktivbetrieb mit nicht vertrauenswürdigem Code | Hoch (Isolation auf Hardwareebene) |
| Treiber | Umgebungsvariable | Anwendungsfall | Sicherheitsstufe |
| ----------- | ------------------------------ | -------------------------------------------------- | ---------------------------------- |
| Deaktiviert | `LOGIC_FUNCTION_TYPE=DISABLED` | Logikfunktionen vollständig deaktivieren | N/A |
| Lokal | `LOGIC_FUNCTION_TYPE=LOCAL` | Entwicklung und vertrauenswürdige Umgebungen | Niedrig (keine Sandbox) |
| Lambda | `LOGIC_FUNCTION_TYPE=LAMBDA` | Produktivbetrieb mit nicht vertrauenswürdigem Code | Hoch (Isolation auf Hardwareebene) |
### Empfohlene Konfiguration
### Logikfunktionen - Empfohlene Konfiguration
**Für die Entwicklung:**
```bash
SERVERLESS_TYPE=LOCAL # default
LOGIC_FUNCTION_TYPE=LOCAL # default when NODE_ENV=development
```
**Für den Produktivbetrieb (AWS):**
```bash
SERVERLESS_TYPE=LAMBDA
SERVERLESS_LAMBDA_REGION=us-east-1
SERVERLESS_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
LOGIC_FUNCTION_TYPE=LAMBDA
LOGIC_FUNCTION_LAMBDA_REGION=us-east-1
LOGIC_FUNCTION_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID=your-access-key
LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
```
**Zum Deaktivieren von Logikfunktionen:**
```bash
SERVERLESS_TYPE=DISABLED
LOGIC_FUNCTION_TYPE=DISABLED # default when NODE_ENV=production
```
### Code-Interpreter - Verfügbare Treiber
| Treiber | Umgebungsvariable | Anwendungsfall | Sicherheitsstufe |
| ----------- | -------------------------------- | ------------------------------------------ | ------------------------ |
| Deaktiviert | `CODE_INTERPRETER_TYPE=DISABLED` | KI-Codeausführung deaktivieren | N/A |
| Lokal | `CODE_INTERPRETER_TYPE=LOCAL` | Nur für die Entwicklung | Niedrig (keine Sandbox) |
| E2B | `CODE_INTERPRETER_TYPE=E_2_B` | Produktion mit Ausführung in einer Sandbox | Hoch (isolierte Sandbox) |
<Note>
Bei Verwendung von `SERVERLESS_TYPE=DISABLED` führt jeder Versuch, eine Logikfunktion auszuführen, zu einem Fehler. Dies ist nützlich, wenn Sie Twenty ohne Unterstützung für Logikfunktionen betreiben möchten.
Bei Verwendung von `LOGIC_FUNCTION_TYPE=DISABLED` oder `CODE_INTERPRETER_TYPE=DISABLED` führt jeder Ausführungsversuch zu einem Fehler. Dies ist nützlich, wenn Sie Twenty ohne diese Funktionen betreiben möchten.
</Note>
@@ -296,46 +296,60 @@ yarn command:prod cron:workflow:automated-cron-trigger
**Modalità solo ambiente:** Se imposti `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, aggiungi queste variabili al tuo file `.env` invece.
</Warning>
## Funzioni logiche
## Funzioni logiche & interprete del codice
Twenty supporta le funzioni logiche per i workflow e la logica personalizzata. L'ambiente di esecuzione è configurato tramite la variabile di ambiente `SERVERLESS_TYPE`.
Twenty supporta le funzioni logiche per i workflow e l'interprete del codice per l'analisi dei dati con IA. Entrambi eseguono codice fornito dall'utente e richiedono una configurazione esplicita per motivi di sicurezza.
### Impostazioni di sicurezza predefinite
**In produzione (NODE_ENV=production):** Sia le funzioni logiche sia l'interprete del codice hanno come impostazione predefinita **Disabilitato**. È necessario abilitarli esplicitamente con `LOGIC_FUNCTION_TYPE` e `CODE_INTERPRETER_TYPE` se queste funzionalità sono necessarie.
**In sviluppo (NODE_ENV=development):** Entrambi sono impostati su **LOCAL** per comodità quando vengono eseguiti in locale.
<Warning>
**Avviso di sicurezza:** Il driver locale (`SERVERLESS_TYPE=LOCAL`) esegue il codice direttamente sull'host in un processo Node.js senza sandboxing. Dovrebbe essere utilizzato solo per codice attendibile in fase di sviluppo. Per le distribuzioni in produzione che gestiscono codice non attendibile, consigliamo vivamente di usare `SERVERLESS_TYPE=LAMBDA` o `SERVERLESS_TYPE=DISABLED`.
**Avviso di sicurezza:** Il driver locale (`LOGIC_FUNCTION_TYPE=LOCAL` o `CODE_INTERPRETER_TYPE=LOCAL`) esegue il codice direttamente sull'host in un processo Node.js senza sandboxing. Dovrebbe essere utilizzato solo per codice attendibile in fase di sviluppo. Per le distribuzioni in produzione che gestiscono codice non affidabile, usa `LOGIC_FUNCTION_TYPE=LAMBDA` o `CODE_INTERPRETER_TYPE=E2B` (con sandboxing), oppure lasciali disabilitati.
</Warning>
### Driver disponibili
### Funzioni logiche - Driver disponibili
| Driver | Variabile di ambiente | Caso d'uso | Livello di sicurezza |
| ------------ | -------------------------- | -------------------------------------------- | ------------------------------------ |
| Disabilitato | `SERVERLESS_TYPE=DISABLED` | Disabilita completamente le funzioni logiche | N/A |
| Locale | `SERVERLESS_TYPE=LOCAL` | Ambienti di sviluppo e attendibili | Basso (nessuna sandbox) |
| Lambda | `SERVERLESS_TYPE=LAMBDA` | Produzione con codice non attendibile | Alto (isolamento a livello hardware) |
| Driver | Variabile di ambiente | Caso d'uso | Livello di sicurezza |
| ------------ | ------------------------------ | -------------------------------------------- | ------------------------------------ |
| Disabilitato | `LOGIC_FUNCTION_TYPE=DISABLED` | Disabilita completamente le funzioni logiche | N/A |
| Locale | `LOGIC_FUNCTION_TYPE=LOCAL` | Ambienti di sviluppo e attendibili | Basso (nessuna sandbox) |
| Lambda | `LOGIC_FUNCTION_TYPE=LAMBDA` | Produzione con codice non attendibile | Alto (isolamento a livello hardware) |
### Configurazione consigliata
### Funzioni logiche - Configurazione consigliata
**Per lo sviluppo:**
```bash
SERVERLESS_TYPE=LOCAL # default
LOGIC_FUNCTION_TYPE=LOCAL # default when NODE_ENV=development
```
**Per la produzione (AWS):**
```bash
SERVERLESS_TYPE=LAMBDA
SERVERLESS_LAMBDA_REGION=us-east-1
SERVERLESS_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
LOGIC_FUNCTION_TYPE=LAMBDA
LOGIC_FUNCTION_LAMBDA_REGION=us-east-1
LOGIC_FUNCTION_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID=your-access-key
LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
```
**Per disabilitare le funzioni logiche:**
```bash
SERVERLESS_TYPE=DISABLED
LOGIC_FUNCTION_TYPE=DISABLED # default when NODE_ENV=production
```
### Interprete del codice - Driver disponibili
| Driver | Variabile di ambiente | Caso d'uso | Livello di sicurezza |
| ------------ | -------------------------------- | ------------------------------------- | ----------------------- |
| Disabilitato | `CODE_INTERPRETER_TYPE=DISABLED` | Disabilita l'esecuzione del codice IA | N/A |
| Locale | `CODE_INTERPRETER_TYPE=LOCAL` | Solo per lo sviluppo | Basso (nessuna sandbox) |
| E2B | `CODE_INTERPRETER_TYPE=E_2_B` | Produzione con esecuzione in sandbox | Alta (sandbox isolata) |
<Note>
Quando si utilizza `SERVERLESS_TYPE=DISABLED`, qualsiasi tentativo di eseguire una funzione logica restituirà un errore. Ciò è utile se si desidera eseguire Twenty senza il supporto per le funzioni logiche.
Quando si utilizza `LOGIC_FUNCTION_TYPE=DISABLED` o `CODE_INTERPRETER_TYPE=DISABLED`, qualsiasi tentativo di esecuzione restituirà un errore. Ciò è utile se si desidera eseguire Twenty senza queste funzionalità.
</Note>
@@ -297,46 +297,60 @@ yarn command:prod cron:workflow:automated-cron-trigger
**Modo somente ambiente:** Se você definir `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, adicione estas variáveis ao seu arquivo `.env`.
</Warning>
## Funções lógicas
## Funções lógicas e interpretador de código
O Twenty oferece suporte a funções lógicas para fluxos de trabalho e lógica personalizada. O ambiente de execução é configurado por meio da variável de ambiente `SERVERLESS_TYPE`.
O Twenty oferece suporte a funções lógicas para fluxos de trabalho e ao interpretador de código para análise de dados com IA. Ambos executam código fornecido pelo usuário e exigem configuração explícita por motivos de segurança.
### Padrões de segurança
**Em produção (NODE_ENV=production):** Tanto as funções lógicas quanto o interpretador de código têm como padrão **Desativado**. Você deve habilitá-los explicitamente com `LOGIC_FUNCTION_TYPE` e `CODE_INTERPRETER_TYPE` se precisar desses recursos.
**Em desenvolvimento (NODE_ENV=development):** Ambos têm como padrão **LOCAL** por conveniência ao executar localmente.
<Warning>
**Aviso de segurança:** O driver local (`SERVERLESS_TYPE=LOCAL`) executa código diretamente no host em um processo Node.js sem sandbox. Deve ser usado apenas para código confiável em desenvolvimento. Para implantações de produção que lidam com código não confiável, recomendamos fortemente usar `SERVERLESS_TYPE=LAMBDA` ou `SERVERLESS_TYPE=DISABLED`.
**Aviso de segurança:** O driver local (`LOGIC_FUNCTION_TYPE=LOCAL` ou `CODE_INTERPRETER_TYPE=LOCAL`) executa código diretamente no host em um processo Node.js sem sandbox. Deve ser usado apenas para código confiável em desenvolvimento. Para implantações de produção que lidam com código não confiável, use `LOGIC_FUNCTION_TYPE=LAMBDA` ou `CODE_INTERPRETER_TYPE=E2B` (com sandbox), ou mantenha-os desativados.
</Warning>
### Drivers disponíveis
### Funções lógicas - Drivers disponíveis
| Driver | Variável de ambiente | Caso de uso | Nível de segurança |
| ---------- | -------------------------- | ------------------------------------------ | -------------------------------------- |
| Desativado | `SERVERLESS_TYPE=DISABLED` | Desativar completamente as funções lógicas | N/A |
| Local | `SERVERLESS_TYPE=LOCAL` | Desenvolvimento e ambientes confiáveis | Baixo (sem sandbox) |
| Lambda | `SERVERLESS_TYPE=LAMBDA` | Produção com código não confiável | Alto (isolamento em nível de hardware) |
| Driver | Variável de ambiente | Caso de uso | Nível de segurança |
| ---------- | ------------------------------ | ------------------------------------------ | -------------------------------------- |
| Desativado | `LOGIC_FUNCTION_TYPE=DISABLED` | Desativar completamente as funções lógicas | N/A |
| Local | `LOGIC_FUNCTION_TYPE=LOCAL` | Desenvolvimento e ambientes confiáveis | Baixo (sem sandbox) |
| Lambda | `LOGIC_FUNCTION_TYPE=LAMBDA` | Produção com código não confiável | Alto (isolamento em nível de hardware) |
### Configuração recomendada
### Funções lógicas - Configuração recomendada
**Para desenvolvimento:**
```bash
SERVERLESS_TYPE=LOCAL # default
LOGIC_FUNCTION_TYPE=LOCAL # default when NODE_ENV=development
```
**Para produção (AWS):**
```bash
SERVERLESS_TYPE=LAMBDA
SERVERLESS_LAMBDA_REGION=us-east-1
SERVERLESS_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
LOGIC_FUNCTION_TYPE=LAMBDA
LOGIC_FUNCTION_LAMBDA_REGION=us-east-1
LOGIC_FUNCTION_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID=your-access-key
LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
```
**Para desativar as funções lógicas:**
```bash
SERVERLESS_TYPE=DISABLED
LOGIC_FUNCTION_TYPE=DISABLED # default when NODE_ENV=production
```
### Interpretador de código - Drivers disponíveis
| Driver | Variável de ambiente | Caso de uso | Nível de segurança |
| ---------- | -------------------------------- | ------------------------------------ | ---------------------- |
| Desativado | `CODE_INTERPRETER_TYPE=DISABLED` | Desativar a execução de código de IA | N/A |
| Local | `CODE_INTERPRETER_TYPE=LOCAL` | Apenas para desenvolvimento | Baixo (sem sandbox) |
| E2B | `CODE_INTERPRETER_TYPE=E_2_B` | Produção com execução em sandbox | Alto (sandbox isolado) |
<Note>
Ao usar `SERVERLESS_TYPE=DISABLED`, qualquer tentativa de executar uma função lógica retornará um erro. Isso é útil se você quiser executar o Twenty sem recursos de funções lógicas.
Ao usar `LOGIC_FUNCTION_TYPE=DISABLED` ou `CODE_INTERPRETER_TYPE=DISABLED`, qualquer tentativa de execução retornará um erro. Isso é útil se você quiser executar o Twenty sem esses recursos.
</Note>
@@ -297,46 +297,60 @@ yarn command:prod cron:workflow:automated-cron-trigger
**Mod doar pentru mediu:** Dacă setați `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, adăugați aceste variabile în fișierul dvs. `.env` în schimb.
</Warning>
## Funcții logice
## Funcții logice și interpretor de cod
Twenty acceptă funcții logice pentru fluxuri de lucru și logică personalizată. Mediul de execuție este configurat prin variabila de mediu `SERVERLESS_TYPE`.
Twenty acceptă funcții logice pentru fluxuri de lucru și interpretorul de cod pentru analiza datelor cu AI. Ambele rulează cod furnizat de utilizator și necesită o configurare explicită din motive de securitate.
### Valori implicite de securitate
**În producție (NODE_ENV=production):** Atât funcțiile logice, cât și interpretorul de cod au implicit valoarea **Dezactivat**. Trebuie să le activezi explicit cu `LOGIC_FUNCTION_TYPE` și `CODE_INTERPRETER_TYPE` dacă ai nevoie de aceste funcționalități.
**În dezvoltare (NODE_ENV=development):** Ambele au implicit valoarea **LOCAL** pentru comoditate când rulezi local.
<Warning>
**Atenționare de securitate:** Driverul local (`SERVERLESS_TYPE=LOCAL`) rulează codul direct pe gazdă într-un proces Node.js, fără sandboxing. Ar trebui utilizat doar pentru cod de încredere, în dezvoltare. Pentru implementări de producție care gestionează cod neverificat, recomandăm cu tărie utilizarea `SERVERLESS_TYPE=LAMBDA` sau `SERVERLESS_TYPE=DISABLED`.
**Atenționare de securitate:** Driverul local (`LOGIC_FUNCTION_TYPE=LOCAL` sau `CODE_INTERPRETER_TYPE=LOCAL`) rulează codul direct pe gazdă într-un proces Node.js, fără sandboxing. Ar trebui utilizat doar pentru cod de încredere, în dezvoltare. Pentru implementări în producție care gestionează cod neverificat, folosiți `LOGIC_FUNCTION_TYPE=LAMBDA` sau `CODE_INTERPRETER_TYPE=E2B` (cu sandboxing) ori păstrați-le dezactivate.
</Warning>
### Drivere disponibile
### Funcții logice - Drivere disponibile
| Driver | Variabilă de mediu | Caz de utilizare | Nivel de securitate |
| ---------- | -------------------------- | ------------------------------------- | -------------------------------------- |
| Dezactivat | `SERVERLESS_TYPE=DISABLED` | Dezactivează complet funcțiile logice | N/A |
| Local | `SERVERLESS_TYPE=LOCAL` | Dezvoltare și medii de încredere | Scăzut (fără sandboxing) |
| Lambda | `SERVERLESS_TYPE=LAMBDA` | Producție cu cod neverificat | Ridicat (izolare la nivel de hardware) |
| Driver | Variabilă de mediu | Caz de utilizare | Nivel de securitate |
| ---------- | ------------------------------ | ------------------------------------- | -------------------------------------- |
| Dezactivat | `LOGIC_FUNCTION_TYPE=DISABLED` | Dezactivează complet funcțiile logice | N/A |
| Local | `LOGIC_FUNCTION_TYPE=LOCAL` | Dezvoltare și medii de încredere | Scăzut (fără sandboxing) |
| Lambda | `LOGIC_FUNCTION_TYPE=LAMBDA` | Producție cu cod neverificat | Ridicat (izolare la nivel de hardware) |
### Configurație recomandată
### Funcții logice - Configurare recomandată
**Pentru dezvoltare:**
```bash
SERVERLESS_TYPE=LOCAL # default
LOGIC_FUNCTION_TYPE=LOCAL # default when NODE_ENV=development
```
**Pentru producție (AWS):**
```bash
SERVERLESS_TYPE=LAMBDA
SERVERLESS_LAMBDA_REGION=us-east-1
SERVERLESS_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
LOGIC_FUNCTION_TYPE=LAMBDA
LOGIC_FUNCTION_LAMBDA_REGION=us-east-1
LOGIC_FUNCTION_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID=your-access-key
LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
```
**Pentru a dezactiva funcțiile logice:**
```bash
SERVERLESS_TYPE=DISABLED
LOGIC_FUNCTION_TYPE=DISABLED # default when NODE_ENV=production
```
### Interpretor de cod - Drivere disponibile
| Driver | Variabilă de mediu | Caz de utilizare | Nivel de securitate |
| ---------- | -------------------------------- | ---------------------------------------- | ------------------------ |
| Dezactivat | `CODE_INTERPRETER_TYPE=DISABLED` | Dezactivați execuția codului de către AI | N/A |
| Local | `CODE_INTERPRETER_TYPE=LOCAL` | Doar pentru dezvoltare | Scăzut (fără sandboxing) |
| E2B | `CODE_INTERPRETER_TYPE=E_2_B` | Producție cu execuție în sandbox | Ridicat (sandbox izolat) |
<Note>
Când se utilizează `SERVERLESS_TYPE=DISABLED`, orice încercare de a executa o funcție logică va returna o eroare. Acest lucru este util dacă doriți să rulați Twenty fără capabilități de funcții logice.
Când utilizați `LOGIC_FUNCTION_TYPE=DISABLED` sau `CODE_INTERPRETER_TYPE=DISABLED`, orice încercare de execuție va returna o eroare. Acest lucru este util dacă doriți să rulați Twenty fără aceste capabilități.
</Note>
@@ -296,46 +296,60 @@ yarn command:prod cron:workflow:automated-cron-trigger
**Режим только для среды:** если вы установили `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, добавьте эти переменные в свой `.env` файл вместо этого.
</Warning>
## Логические функции
## Логические функции и интерпретатор кода
Twenty поддерживает логические функции для рабочих процессов и пользовательской логики. Среда выполнения настраивается через переменную окружения `SERVERLESS_TYPE`.
Twenty поддерживает логические функции для рабочих процессов и интерпретатор кода для анализа данных ИИ. Оба запускают предоставленный пользователем код и требуют явной настройки в целях безопасности.
### Настройки безопасности по умолчанию
**В продакшене (NODE_ENV=production):** логические функции и интерпретатор кода по умолчанию — **Отключено**. Если вам нужны эти функции, вы должны явно включить их с помощью `LOGIC_FUNCTION_TYPE` и `CODE_INTERPRETER_TYPE`.
**В разработке (NODE_ENV=development):** оба по умолчанию — **LOCAL** для удобства при локальном запуске.
<Warning>
**Уведомление о безопасности:** локальный драйвер (`SERVERLESS_TYPE=LOCAL`) выполняет код напрямую на хосте в процессе Node.js без изоляции. Его следует использовать только для доверенного кода в разработке. Для промышленных развертываний, обрабатывающих недоверенный код, настоятельно рекомендуем использовать `SERVERLESS_TYPE=LAMBDA` или `SERVERLESS_TYPE=DISABLED`.
**Уведомление о безопасности:** локальный драйвер (`LOGIC_FUNCTION_TYPE=LOCAL` или `CODE_INTERPRETER_TYPE=LOCAL`) выполняет код напрямую на хосте в процессе Node.js без изоляции. Его следует использовать только для доверенного кода в разработке. Для рабочих развёртываний, обрабатывающих недоверенный код, используйте `LOGIC_FUNCTION_TYPE=LAMBDA` или `CODE_INTERPRETER_TYPE=E2B` (с песочницей) либо оставьте их отключёнными.
</Warning>
### Доступные драйверы
### Логические функции — доступные драйверы
| Драйвер | Переменная окружения | Сценарий использования | Уровень безопасности |
| --------- | -------------------------- | -------------------------------------- | ----------------------------------------- |
| Отключено | `SERVERLESS_TYPE=DISABLED` | Полностью отключить логические функции | Н/Д |
| Локальный | `SERVERLESS_TYPE=LOCAL` | Разработка и доверенные среды | Низкий (без изоляции) |
| Lambda | `SERVERLESS_TYPE=LAMBDA` | Продакшн с недоверенным кодом | Высокий (изоляция на уровне оборудования) |
| Драйвер | Переменная окружения | Сценарий использования | Уровень безопасности |
| --------- | ------------------------------ | -------------------------------------- | ----------------------------------------- |
| Отключено | `LOGIC_FUNCTION_TYPE=DISABLED` | Полностью отключить логические функции | Н/Д |
| Локальный | `LOGIC_FUNCTION_TYPE=LOCAL` | Разработка и доверенные среды | Низкий (без изоляции) |
| Lambda | `LOGIC_FUNCTION_TYPE=LAMBDA` | Продакшн с недоверенным кодом | Высокий (изоляция на уровне оборудования) |
### Рекомендуемая конфигурация
### Логические функции — рекомендуемая конфигурация
**Для разработки:**
```bash
SERVERLESS_TYPE=LOCAL # default
LOGIC_FUNCTION_TYPE=LOCAL # default when NODE_ENV=development
```
**Для продакшна (AWS):**
```bash
SERVERLESS_TYPE=LAMBDA
SERVERLESS_LAMBDA_REGION=us-east-1
SERVERLESS_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
LOGIC_FUNCTION_TYPE=LAMBDA
LOGIC_FUNCTION_LAMBDA_REGION=us-east-1
LOGIC_FUNCTION_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID=your-access-key
LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
```
**Чтобы отключить логические функции:**
```bash
SERVERLESS_TYPE=DISABLED
LOGIC_FUNCTION_TYPE=DISABLED # default when NODE_ENV=production
```
### Интерпретатор кода — доступные драйверы
| Драйвер | Переменная окружения | Сценарий использования | Уровень безопасности |
| --------- | -------------------------------- | ----------------------------------------------------- | --------------------------------- |
| Отключено | `CODE_INTERPRETER_TYPE=DISABLED` | Отключить выполнение кода ИИ | Н/Д |
| Локальный | `CODE_INTERPRETER_TYPE=LOCAL` | Только для разработки | Низкий (без изоляции) |
| E2B | `CODE_INTERPRETER_TYPE=E_2_B` | Рабочая среда с изолированным исполнением в песочнице | Высокий (изолированная песочница) |
<Note>
При использовании `SERVERLESS_TYPE=DISABLED` любая попытка выполнить логическую функцию вернет ошибку. Это полезно, если вы хотите запускать Twenty без возможностей логических функций.
При использовании `LOGIC_FUNCTION_TYPE=DISABLED` или `CODE_INTERPRETER_TYPE=DISABLED` любая попытка выполнения вернёт ошибку. Это полезно, если вы хотите запускать Twenty без этих возможностей.
</Note>
@@ -297,46 +297,60 @@ yarn command:prod cron:workflow:automated-cron-trigger
**Çevre-yalnızca modu:** `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false` ayarlarsanız, bu değişkenleri `.env` dosyanıza ekleyin.
</Warning>
## Mantıksal işlevler
## Mantıksal İşlevler ve Kod Yorumlayıcısı
Twenty, iş akışları ve özel mantık için mantıksal işlevleri destekler. Yürütme ortamı, `SERVERLESS_TYPE` ortam değişkeni aracılığıyla yapılandırılır.
Twenty, iş akışları için mantıksal işlevleri ve yapay zekâ veri analizi için kod yorumlayıcısını destekler. Her ikisi de kullanıcı tarafından sağlanan kodu çalıştırır ve güvenlik için açık bir yapılandırma gerektirir.
### Güvenlik Varsayılanları
**Üretimde (NODE_ENV=production):** Hem mantıksal işlevler hem de kod yorumlayıcı varsayılan olarak **Devre Dışı**dır. Bu özelliklere ihtiyacınız varsa, onları `LOGIC_FUNCTION_TYPE` ve `CODE_INTERPRETER_TYPE` ile açıkça etkinleştirmeniz gerekir.
**Geliştirmede (NODE_ENV=development):** Yerel olarak çalıştırırken kolaylık olması için her ikisinin varsayılanı **LOCAL**dır.
<Warning>
**Güvenlik Notu:** Yerel sürücü (`SERVERLESS_TYPE=LOCAL`), herhangi bir sandbox olmadan kodu ana makinede doğrudan bir Node.js sürecinde çalıştırır. Yalnızca geliştirirken güvenilen kod için kullanılmalıdır. Güvenilmeyen kodu işleyen üretim dağıtımları için `SERVERLESS_TYPE=LAMBDA` veya `SERVERLESS_TYPE=DISABLED` kullanmanızı şiddetle öneririz.
**Güvenlik Notu:** Yerel sürücü (`LOGIC_FUNCTION_TYPE=LOCAL` veya `CODE_INTERPRETER_TYPE=LOCAL`), herhangi bir sandbox olmadan kodu ana makinede doğrudan bir Node.js sürecinde çalıştırır. Yalnızca geliştirirken güvenilen kod için kullanılmalıdır. Üretim dağıtımlarında güvenilmeyen kodu işlerken, `LOGIC_FUNCTION_TYPE=LAMBDA` veya `CODE_INTERPRETER_TYPE=E2B` (korumalı alanla) kullanın ya da bunları devre dışı bırakın.
</Warning>
### Kullanılabilir Sürücüler
### Mantıksal İşlevler - Kullanılabilir Sürücüler
| Sürücü | Ortam Değişkeni | Kullanım alanı | Güvenlik Düzeyi |
| ---------- | -------------------------- | ---------------------------------------------- | ------------------------------------ |
| Devre dışı | `SERVERLESS_TYPE=DISABLED` | Mantıksal işlevleri tamamen devre dışı bırakın | Uygulanamaz |
| Yerel | `SERVERLESS_TYPE=LOCAL` | Geliştirme ve güvenilir ortamlar | Düşük (sandbox yok) |
| Lambda | `SERVERLESS_TYPE=LAMBDA` | Güvenilmeyen kodla üretim | Yüksek (donanım düzeyinde izolasyon) |
| Sürücü | Ortam Değişkeni | Kullanım alanı | Güvenlik Düzeyi |
| ---------- | ------------------------------ | ---------------------------------------------- | ------------------------------------ |
| Devre dışı | `LOGIC_FUNCTION_TYPE=DISABLED` | Mantıksal işlevleri tamamen devre dışı bırakın | Uygulanamaz |
| Yerel | `LOGIC_FUNCTION_TYPE=LOCAL` | Geliştirme ve güvenilir ortamlar | Düşük (sandbox yok) |
| Lambda | `LOGIC_FUNCTION_TYPE=LAMBDA` | Güvenilmeyen kodla üretim | Yüksek (donanım düzeyinde izolasyon) |
### Önerilen Yapılandırma
### Mantıksal İşlevler - Önerilen Yapılandırma
**Geliştirme için:**
```bash
SERVERLESS_TYPE=LOCAL # default
LOGIC_FUNCTION_TYPE=LOCAL # default when NODE_ENV=development
```
**Üretim için (AWS):**
```bash
SERVERLESS_TYPE=LAMBDA
SERVERLESS_LAMBDA_REGION=us-east-1
SERVERLESS_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
LOGIC_FUNCTION_TYPE=LAMBDA
LOGIC_FUNCTION_LAMBDA_REGION=us-east-1
LOGIC_FUNCTION_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID=your-access-key
LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
```
**Mantıksal işlevleri devre dışı bırakmak için:**
```bash
SERVERLESS_TYPE=DISABLED
LOGIC_FUNCTION_TYPE=DISABLED # default when NODE_ENV=production
```
### Kod Yorumlayıcısı - Kullanılabilir Sürücüler
| Sürücü | Ortam Değişkeni | Kullanım Senaryosu | Güvenlik Düzeyi |
| ---------- | -------------------------------- | --------------------------------------------- | --------------------------------- |
| Devre dışı | `CODE_INTERPRETER_TYPE=DISABLED` | Yapay zekâ kod yürütmesini devre dışı bırakın | Uygulanamaz |
| Yerel | `CODE_INTERPRETER_TYPE=LOCAL` | Yalnızca geliştirme için | Düşük (sandbox yok) |
| E2B | `CODE_INTERPRETER_TYPE=E_2_B` | Korumalı alanlı yürütmeyle üretim | Yüksek (yalıtılmış korumalı alan) |
<Note>
`SERVERLESS_TYPE=DISABLED` kullanıldığında, bir mantıksal işlevi yürütmeye yönelik herhangi bir girişim bir hata döndürür. Bu, Twenty'yi mantıksal işlev yetenekleri olmadan çalıştırmak istiyorsanız kullanışlıdır.
`LOGIC_FUNCTION_TYPE=DISABLED` veya `CODE_INTERPRETER_TYPE=DISABLED` kullanıldığında, herhangi bir yürütme girişimi bir hata döndürür. Bu, Twenty'yi bu yetenekler olmadan çalıştırmak istiyorsanız kullanışlıdır.
</Note>
@@ -297,46 +297,60 @@ yarn command:prod cron:workflow:automated-cron-trigger
**仅限环境模式:** 如果你设置 `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`,请将这些变量添加到 `.env` 文件中。
</Warning>
## 逻辑函数
## 逻辑函数与代码解释器
Twenty 支持用于工作流和自定义逻辑的逻辑函数。 执行环境通过 `SERVERLESS_TYPE` 环境变量进行配置
Twenty 支持用于工作流的逻辑函数,以及用于 AI 数据分析的代码解释器。 二者都会运行用户提供的代码,并要求进行显式配置以确保安全
### 安全默认设置
**在生产环境(NODE_ENV=production):** 逻辑函数和代码解释器的默认设置为**禁用**。 如需这些功能,必须通过 `LOGIC_FUNCTION_TYPE` 和 `CODE_INTERPRETER_TYPE` 显式启用它们。
**在开发环境(NODE_ENV=development):** 为方便在本地运行,二者默认均为**LOCAL**。
<Warning>
\*\*安全提示:\*\*本地驱动(`SERVERLESS_TYPE=LOCAL`)在没有沙箱的情况下,在主机上的 Node.js 进程中直接运行代码。 它应仅在开发环境中用于可信代码。 对于在生产环境中处理不受信任代码的部署,我们强烈建议使用 `SERVERLESS_TYPE=LAMBDA` 或 `SERVERLESS_TYPE=DISABLED`
**安全提示:** 本地驱动(`LOGIC_FUNCTION_TYPE=LOCAL` 或 `CODE_INTERPRETER_TYPE=LOCAL`在没有沙箱的情况下,在主机上的 Node.js 进程中直接运行代码。 它应仅在开发环境中用于可信代码。 对于在生产环境中处理不受信任代码的部署,请使用 `LOGIC_FUNCTION_TYPE=LAMBDA` 或 `CODE_INTERPRETER_TYPE=E2B`(使用沙盒),或将它们保持禁用
</Warning>
### 可用驱动
### 逻辑函数 - 可用驱动程序
| 驱动 | 环境变量 | 用例 | 安全级别 |
| ------ | -------------------------- | -------------- | -------- |
| 禁用 | `SERVERLESS_TYPE=DISABLED` | 完全禁用逻辑函数 | 不适用 |
| 本地 | `SERVERLESS_TYPE=LOCAL` | 开发和可信环境 | 低(无沙箱) |
| Lambda | `SERVERLESS_TYPE=LAMBDA` | 生产环境(处理不受信任代码) | 高(硬件级隔离) |
| 驱动 | 环境变量 | 用例 | 安全级别 |
| ------ | ------------------------------ | -------------- | -------- |
| 禁用 | `LOGIC_FUNCTION_TYPE=DISABLED` | 完全禁用逻辑函数 | 不适用 |
| 本地 | `LOGIC_FUNCTION_TYPE=LOCAL` | 开发和可信环境 | 低(无沙箱) |
| Lambda | `LOGIC_FUNCTION_TYPE=LAMBDA` | 生产环境(处理不受信任代码) | 高(硬件级隔离) |
### 推荐配置
### 逻辑函数 - 推荐配置
**用于开发:**
```bash
SERVERLESS_TYPE=LOCAL # default
LOGIC_FUNCTION_TYPE=LOCAL # default when NODE_ENV=development
```
**用于生产(AWS):**
```bash
SERVERLESS_TYPE=LAMBDA
SERVERLESS_LAMBDA_REGION=us-east-1
SERVERLESS_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
LOGIC_FUNCTION_TYPE=LAMBDA
LOGIC_FUNCTION_LAMBDA_REGION=us-east-1
LOGIC_FUNCTION_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID=your-access-key
LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
```
**要禁用逻辑函数:**
```bash
SERVERLESS_TYPE=DISABLED
LOGIC_FUNCTION_TYPE=DISABLED # default when NODE_ENV=production
```
### 代码解释器 - 可用驱动程序
| 驱动 | 环境变量 | 用例 | 安全级别 |
| --- | -------------------------------- | ----------- | ------- |
| 禁用 | `CODE_INTERPRETER_TYPE=DISABLED` | 禁用 AI 代码执行 | 不适用 |
| 本地 | `CODE_INTERPRETER_TYPE=LOCAL` | 仅限开发环境 | 低(无沙箱) |
| E2B | `CODE_INTERPRETER_TYPE=E_2_B` | 生产环境(沙盒化执行) | 高(隔离沙盒) |
<Note>
使用 `SERVERLESS_TYPE=DISABLED` 时,任何尝试执行逻辑函数的操作都会返回错误。 如果你想在不启用逻辑函数能力的情况下运行 Twenty,这将很有用。
使用 `LOGIC_FUNCTION_TYPE=DISABLED` 或 `CODE_INTERPRETER_TYPE=DISABLED` 时,任何执行尝试都会返回错误。 如果你想在不启用这些功能的情况下运行 Twenty,这将很有用。
</Note>
File diff suppressed because one or more lines are too long
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "Voeg \"{trimmedName}\" by opsies"
msgid "Add a {objectLabelSingular}"
msgstr "Voeg 'n {objectLabelSingular} by"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Voeg 'n nodus by"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "أضف \"{trimmedName}\" إلى الخيارات"
msgid "Add a {objectLabelSingular}"
msgstr "أضف {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "إضافة عقدة"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "Afegeix \"{trimmedName}\" a les opcions"
msgid "Add a {objectLabelSingular}"
msgstr "Afegeix un {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Afegeix un node"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "Přidat \"{trimmedName}\" do možností"
msgid "Add a {objectLabelSingular}"
msgstr "Přidat {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Přidat uzel"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "Tilføj \"{trimmedName}\" til muligheder"
msgid "Add a {objectLabelSingular}"
msgstr "Tilføj {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Tilføj en node"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "\"{trimmedName}\" zu den Optionen hinzufügen"
msgid "Add a {objectLabelSingular}"
msgstr "Ein {objectLabelSingular} hinzufügen"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Knoten hinzufügen"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "Προσθήκη \"{trimmedName}\" στις επιλογές"
msgid "Add a {objectLabelSingular}"
msgstr "Προσθήκη {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Προσθήκη κόμβου"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -801,6 +801,13 @@ msgstr "Add \"{trimmedName}\" to options"
msgid "Add a {objectLabelSingular}"
msgstr "Add a {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr "Add a Group"
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -817,12 +824,6 @@ msgstr "Add a node"
msgid "Add a record"
msgstr "Add a record"
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr "Add a Section"
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "Agregar \"{trimmedName}\" a las opciones"
msgid "Add a {objectLabelSingular}"
msgstr "Agregar un {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Añadir un nodo"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "Lisää \"{trimmedName}\" valintoihin"
msgid "Add a {objectLabelSingular}"
msgstr "Lisää {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Lisää solmu"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "Ajouter \"{trimmedName}\" aux options"
msgid "Add a {objectLabelSingular}"
msgstr "Ajouter un {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Ajouter un nœud"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "הוסף \"{trimmedName}\" לאפשרויות"
msgid "Add a {objectLabelSingular}"
msgstr "הוסף {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "הוסף צומת"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "\"{trimmedName}\" hozzáadása az opciókhoz"
msgid "Add a {objectLabelSingular}"
msgstr "Új {objectLabelSingular} hozzáadása"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Csomópont hozzáadása"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "Aggiungi \"{trimmedName}\" alle opzioni"
msgid "Add a {objectLabelSingular}"
msgstr "Aggiungi {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Aggiungi un nodo"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "「{trimmedName}」をオプションに追加"
msgid "Add a {objectLabelSingular}"
msgstr "{objectLabelSingular} を追加"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "ノードを追加する"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "\"{trimmedName}\"을 옵션에 추가하기"
msgid "Add a {objectLabelSingular}"
msgstr "{objectLabelSingular} 추가"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "노드 추가"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "Voeg \"{trimmedName}\" toe aan opties"
msgid "Add a {objectLabelSingular}"
msgstr "Voeg een {objectLabelSingular} toe"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Voeg een knooppunt toe"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "Legg til \"{trimmedName}\" i valg"
msgid "Add a {objectLabelSingular}"
msgstr "Legg til {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Legg til node"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "Dodaj \"{trimmedName}\" do opcji"
msgid "Add a {objectLabelSingular}"
msgstr "Dodaj {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Dodaj węzeł"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
@@ -801,6 +801,13 @@ msgstr ""
msgid "Add a {objectLabelSingular}"
msgstr ""
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -817,12 +824,6 @@ msgstr ""
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "Adicionar \"{trimmedName}\" às opções"
msgid "Add a {objectLabelSingular}"
msgstr "Adicionar um {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Adicionar nó"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "Adicionar \"{trimmedName}\" às opções"
msgid "Add a {objectLabelSingular}"
msgstr "Adicionar um {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Adicionar nó"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "Adaugă \"{trimmedName}\" la opțiuni"
msgid "Add a {objectLabelSingular}"
msgstr "Adaugă un {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Adaugă nod"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
Binary file not shown.
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "Додај \"{trimmedName}\" у опције"
msgid "Add a {objectLabelSingular}"
msgstr "Додај {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Додај чвор"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "Lägg till \"{trimmedName}\" i alternativ"
msgid "Add a {objectLabelSingular}"
msgstr "Lägg till {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Lägg till en nod"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "\"{trimmedName}\" seçeneğe ekle"
msgid "Add a {objectLabelSingular}"
msgstr "Bir {objectLabelSingular} ekle"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Bir düğüm ekle"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "Додати \"{trimmedName}\" до параметрів"
msgid "Add a {objectLabelSingular}"
msgstr "Додати {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Додати вузол"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "Thêm \"{trimmedName}\" vào các tùy chọn"
msgid "Add a {objectLabelSingular}"
msgstr "Thêm {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "Thêm nút"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "将 \"{trimmedName}\" 添加到选项中"
msgid "Add a {objectLabelSingular}"
msgstr "添加一个 {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "添加节点"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
+7 -6
View File
@@ -806,6 +806,13 @@ msgstr "將 \"{trimmedName}\" 添加到選項中"
msgid "Add a {objectLabelSingular}"
msgstr "新增一個 {objectLabelSingular}"
#. js-lingui-id: 7W8nJr
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
msgid "Add a Group"
msgstr ""
#. js-lingui-id: CZDwqQ
#: src/modules/ai/components/suggested-prompts/default-suggested-prompts.ts
msgid "Add a new company we're in touch with (e.g. name, website, industry). Details: "
@@ -822,12 +829,6 @@ msgstr "添加節點"
msgid "Add a record"
msgstr ""
#. js-lingui-id: r8W+9y
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor.tsx
#: src/modules/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor.tsx
msgid "Add a Section"
msgstr ""
#. js-lingui-id: eMc2xs
#: src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
msgid "Add a step"
@@ -1,35 +1,39 @@
import { downloadFile } from '@/activities/files/utils/downloadFile';
import { saveAs } from 'file-saver';
jest.mock('file-saver', () => ({
saveAs: jest.fn(),
}));
const mockBlob = new Blob(['test content'], { type: 'application/pdf' });
global.fetch = jest.fn(() =>
Promise.resolve({
status: 200,
blob: jest.fn(),
blob: () => Promise.resolve(mockBlob),
} as unknown as Response),
);
window.URL.createObjectURL = jest.fn(() => 'mock-url');
window.URL.revokeObjectURL = jest.fn();
// FIXME: jest is behaving weirdly here, it's not finding the element
// Also the document's innerHTML is empty
// `global.fetch` and `window.fetch` are also undefined
describe.skip('downloadFile', () => {
it('should download a file', () => {
downloadFile('url/to/file.pdf', 'file.pdf');
expect(fetch).toHaveBeenCalledWith('url/to/file.pdf');
const link = document.querySelector(
'a[href="mock-url"][download="file.pdf"]',
);
expect(link).not.toBeNull();
// oxlint-disable-next-line @typescripttypescript/ban-ts-comment
// @ts-ignore
expect(link?.style?.display).toBe('none');
expect(link).toHaveBeenCalledTimes(1);
describe('downloadFile', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('should download a file', async () => {
await downloadFile('url/to/file.pdf', 'file.pdf');
expect(fetch).toHaveBeenCalledWith('url/to/file.pdf');
expect(saveAs).toHaveBeenCalledWith(mockBlob, 'file.pdf');
});
it('should reject when fetch fails', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
status: 404,
blob: () => Promise.resolve(mockBlob),
});
await expect(downloadFile('url/to/file.pdf', 'file.pdf')).rejects.toBe(
'Failed downloading file',
);
});
});
@@ -1,7 +1,7 @@
import { saveAs } from 'file-saver';
export const downloadFile = (fullPath: string, fileName: string) => {
fetch(fullPath)
return fetch(fullPath)
.then((resp) =>
resp.status === 200
? resp.blob()
@@ -24,6 +24,7 @@ import { mapDBMessagesToUIMessages } from '@/ai/utils/mapDBMessagesToUIMessages'
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { useCallback, useMemo } from 'react';
import {
type GetChatThreadsQuery,
GetChatThreadsDocument,
@@ -181,7 +182,11 @@ export const useAgentChatData = () => {
},
});
const isNewThread = currentAIChatThread === AGENT_CHAT_NEW_THREAD_DRAFT_KEY;
const isNewThread = useMemo(
() => currentAIChatThread === AGENT_CHAT_NEW_THREAD_DRAFT_KEY,
[currentAIChatThread],
);
const { loading: messagesLoading, data } = useGetChatMessagesQuery({
variables: { threadId: currentAIChatThread! },
skip: !isDefined(currentAIChatThread) || isNewThread,
@@ -191,7 +196,7 @@ export const useAgentChatData = () => {
},
});
const ensureThreadForDraft = () => {
const ensureThreadForDraft = useCallback(() => {
const current = store.get(currentAIChatThreadState.atom);
if (current !== AGENT_CHAT_NEW_THREAD_DRAFT_KEY) {
return;
@@ -218,9 +223,16 @@ export const useAgentChatData = () => {
threadIdPromise.finally(() => {
setPendingCreateFromDraftPromise(null);
});
};
}, [
createChatThread,
setPendingCreateFromDraftPromise,
store,
setIsCreatingChatThread,
]);
const ensureThreadIdForSend = async (): Promise<string | null> => {
const ensureThreadIdForSend = useCallback(async (): Promise<
string | null
> => {
const current = store.get(currentAIChatThreadState.atom);
if (current !== AGENT_CHAT_NEW_THREAD_DRAFT_KEY) {
return current;
@@ -247,16 +259,33 @@ export const useAgentChatData = () => {
} finally {
setIsCreatingChatThread(false);
}
};
}, [createChatThread, store, setIsCreatingChatThread]);
const uiMessages = mapDBMessagesToUIMessages(data?.chatMessages || []);
const isLoading = messagesLoading || threadsLoading;
const threadsLoadingMemoized = useMemo(
() => threadsLoading,
[threadsLoading],
);
const messagesLoadingMemoized = useMemo(
() => messagesLoading,
[messagesLoading],
);
const uiMessages = useMemo(
() => mapDBMessagesToUIMessages(data?.chatMessages || []),
[data?.chatMessages],
);
const isLoading = useMemo(
() => messagesLoadingMemoized || threadsLoadingMemoized,
[messagesLoadingMemoized, threadsLoadingMemoized],
);
return {
uiMessages,
isLoading,
threadsLoading,
messagesLoading,
threadsLoading: threadsLoadingMemoized,
messagesLoading: messagesLoadingMemoized,
ensureThreadForDraft,
ensureThreadIdForSend,
};
@@ -2,6 +2,7 @@ import { AI_CHAT_SCROLL_WRAPPER_ID } from '@/ai/constants/AiChatScrollWrapperId'
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
import { scrollWrapperScrollBottomComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollBottomComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useCallback, useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
const SCROLL_BOTTOM_THRESHOLD_PX = 10;
@@ -16,9 +17,12 @@ export const useAgentChatScrollToBottom = () => {
AI_CHAT_SCROLL_WRAPPER_ID,
);
const isNearBottom = scrollWrapperScrollBottom <= SCROLL_BOTTOM_THRESHOLD_PX;
const isNearBottom = useMemo(
() => scrollWrapperScrollBottom <= SCROLL_BOTTOM_THRESHOLD_PX,
[scrollWrapperScrollBottom],
);
const scrollToBottom = () => {
const scrollToBottom = useCallback(() => {
const { scrollWrapperElement } = getScrollWrapperElement();
if (!isDefined(scrollWrapperElement)) {
return;
@@ -27,7 +31,7 @@ export const useAgentChatScrollToBottom = () => {
scrollWrapperElement.scrollTo({
top: scrollWrapperElement.scrollHeight,
});
};
}, [getScrollWrapperElement]);
return { scrollToBottom, isNearBottom };
};
@@ -80,9 +80,26 @@ export const useProcessUIToolCallMessage = () => {
break;
}
case 'navigateToView':
// TODO: implement
case 'navigateToView': {
const viewObjectNamePlural = objectMetadataItems.find(
(item) =>
item.nameSingular === navigateAppOutput.objectNameSingular,
)?.namePlural;
if (!isDefined(viewObjectNamePlural)) {
throw new Error(
`Object with singular name ${navigateAppOutput.objectNameSingular} not found, cannot navigate to view from chat.`,
);
}
navigateApp(
AppPath.RecordIndexPage,
{ objectNamePlural: viewObjectNamePlural },
{ viewId: navigateAppOutput.viewId },
);
break;
}
case 'wait': {
await sleep(navigateAppOutput.durationMs);
break;
@@ -1,6 +1,6 @@
import { OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME } from '@/browser-event/constants/ObjectRecordOperationBrowserEventName';
import { type ObjectRecordOperation } from '@/object-record/types/ObjectRecordOperation';
import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
import { type ObjectRecordOperation } from '@/object-record/types/ObjectRecordOperation';
import { useEffect } from 'react';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
@@ -0,0 +1,77 @@
import { DeleteMultipleRecordsCommand } from '@/command-menu-item/record/multiple-records/components/DeleteMultipleRecordsCommand';
import { DestroyMultipleRecordsCommand } from '@/command-menu-item/record/multiple-records/components/DestroyMultipleRecordsCommand';
import { MergeMultipleRecordsCommand } from '@/command-menu-item/record/multiple-records/components/MergeMultipleRecordsCommand';
import { RestoreMultipleRecordsCommand } from '@/command-menu-item/record/multiple-records/components/RestoreMultipleRecordsCommand';
import { CreateNewIndexRecordNoSelectionRecordCommand } from '@/command-menu-item/record/no-selection/components/CreateNewIndexRecordNoSelectionRecordCommand';
import { AddToFavoritesSingleRecordCommand } from '@/command-menu-item/record/single-record/components/AddToFavoritesSingleRecordCommand';
import { DeleteSingleRecordCommand } from '@/command-menu-item/record/single-record/components/DeleteSingleRecordCommand';
import { DestroySingleRecordCommand } from '@/command-menu-item/record/single-record/components/DestroySingleRecordCommand';
import { RemoveFromFavoritesSingleRecordCommand } from '@/command-menu-item/record/single-record/components/RemoveFromFavoritesSingleRecordCommand';
import { RestoreSingleRecordCommand } from '@/command-menu-item/record/single-record/components/RestoreSingleRecordCommand';
import { DuplicateDashboardSingleRecordCommand } from '@/command-menu-item/record/single-record/dashboard/components/DuplicateDashboardSingleRecordCommand';
import { SaveDashboardSingleRecordCommand } from '@/command-menu-item/record/single-record/dashboard/components/SaveDashboardSingleRecordCommand';
import { SaveRecordPageLayoutSingleRecordCommand } from '@/command-menu-item/record/single-record/record-page-layout/components/SaveRecordPageLayoutSingleRecordCommand';
import { ActivateWorkflowSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow/components/ActivateWorkflowSingleRecordCommand';
import { DeactivateWorkflowSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow/components/DeactivateWorkflowSingleRecordCommand';
import { DiscardDraftWorkflowSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow/components/DiscardDraftWorkflowSingleRecordCommand';
import { DuplicateWorkflowSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow/components/DuplicateWorkflowSingleRecordCommand';
import { TestWorkflowSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow/components/TestWorkflowSingleRecordCommand';
import { TidyUpWorkflowSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow/components/TidyUpWorkflowSingleRecordCommand';
import { StopWorkflowRunSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow-runs/components/StopWorkflowRunSingleRecordCommand';
import { UseAsDraftWorkflowVersionSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow-versions/components/UseAsDraftWorkflowVersionSingleRecordCommand';
import { EngineComponentKey } from '~/generated-metadata/graphql';
export const ENGINE_COMPONENT_KEY_COMPONENT_MAP: Record<
EngineComponentKey,
React.ReactNode
> = {
[EngineComponentKey.CREATE_NEW_RECORD]: (
<CreateNewIndexRecordNoSelectionRecordCommand />
),
[EngineComponentKey.DELETE_SINGLE_RECORD]: <DeleteSingleRecordCommand />,
[EngineComponentKey.DELETE_MULTIPLE_RECORDS]: (
<DeleteMultipleRecordsCommand />
),
[EngineComponentKey.RESTORE_SINGLE_RECORD]: <RestoreSingleRecordCommand />,
[EngineComponentKey.RESTORE_MULTIPLE_RECORDS]: (
<RestoreMultipleRecordsCommand />
),
[EngineComponentKey.DESTROY_SINGLE_RECORD]: <DestroySingleRecordCommand />,
[EngineComponentKey.DESTROY_MULTIPLE_RECORDS]: (
<DestroyMultipleRecordsCommand />
),
[EngineComponentKey.ADD_TO_FAVORITES]: <AddToFavoritesSingleRecordCommand />,
[EngineComponentKey.REMOVE_FROM_FAVORITES]: (
<RemoveFromFavoritesSingleRecordCommand />
),
[EngineComponentKey.MERGE_MULTIPLE_RECORDS]: <MergeMultipleRecordsCommand />,
[EngineComponentKey.DUPLICATE_DASHBOARD]: (
<DuplicateDashboardSingleRecordCommand />
),
[EngineComponentKey.DUPLICATE_WORKFLOW]: (
<DuplicateWorkflowSingleRecordCommand />
),
[EngineComponentKey.ACTIVATE_WORKFLOW]: (
<ActivateWorkflowSingleRecordCommand />
),
[EngineComponentKey.DEACTIVATE_WORKFLOW]: (
<DeactivateWorkflowSingleRecordCommand />
),
[EngineComponentKey.DISCARD_DRAFT_WORKFLOW]: (
<DiscardDraftWorkflowSingleRecordCommand />
),
[EngineComponentKey.TEST_WORKFLOW]: <TestWorkflowSingleRecordCommand />,
[EngineComponentKey.STOP_WORKFLOW_RUN]: (
<StopWorkflowRunSingleRecordCommand />
),
[EngineComponentKey.USE_AS_DRAFT_WORKFLOW_VERSION]: (
<UseAsDraftWorkflowVersionSingleRecordCommand />
),
[EngineComponentKey.SAVE_RECORD_PAGE_LAYOUT]: (
<SaveRecordPageLayoutSingleRecordCommand />
),
[EngineComponentKey.SAVE_DASHBOARD_LAYOUT]: (
<SaveDashboardSingleRecordCommand />
),
[EngineComponentKey.TIDY_UP_WORKFLOW]: <TidyUpWorkflowSingleRecordCommand />,
};
@@ -10,6 +10,7 @@ export const COMMAND_MENU_ITEM_FRAGMENT = gql`
name
isHeadless
}
engineComponentKey
label
icon
shortLabel
@@ -1,3 +1,4 @@
import { ENGINE_COMPONENT_KEY_COMPONENT_MAP } from '@/command-menu-item/constants/EngineComponentKeyComponentMap';
import { Command } from '@/command-menu-item/display/components/Command';
import { HeadlessFrontComponentCommandMenuItem } from '@/command-menu-item/display/components/HeadlessFrontComponentCommandMenuItem';
import { CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope';
@@ -22,6 +23,7 @@ import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants
import {
CommandMenuItemAvailabilityType,
type CommandMenuItemFieldsFragment,
type EngineComponentKey,
useFindManyCommandMenuItemsQuery,
} from '~/generated-metadata/graphql';
@@ -30,6 +32,10 @@ type CommandMenuItemWithFrontComponent = CommandMenuItemFieldsFragment & {
conditionalAvailabilityExpression?: string | null;
};
type CommandMenuItemWithSource = CommandMenuItemFieldsFragment & {
conditionalAvailabilityExpression?: string | null;
};
type BuildCommandMenuItemFromFrontComponentParams = {
item: CommandMenuItemWithFrontComponent;
type?: CommandMenuItemType;
@@ -53,8 +59,6 @@ type BuildCommandMenuItemFromFrontComponentParams = {
commandMenuContextApi: CommandMenuContextApi;
};
// TODO: we should remove this backward compatibility logic in the future
// once we have migrated all command menu items
const buildCommandMenuItemFromFrontComponent = ({
item,
type = CommandMenuItemType.FrontComponent,
@@ -115,6 +119,47 @@ const buildCommandMenuItemFromFrontComponent = ({
};
};
type BuildCommandMenuItemFromStandardKeyParams = {
item: CommandMenuItemWithSource;
engineComponentKey: EngineComponentKey;
type?: CommandMenuItemType;
scope: CommandMenuItemScope;
isPinned: boolean;
getIcon: ReturnType<typeof useIcons>['getIcon'];
commandMenuContextApi: CommandMenuContextApi;
};
const buildCommandItemFromEngineKey = ({
item,
engineComponentKey,
type = CommandMenuItemType.Standard,
scope,
isPinned,
getIcon,
commandMenuContextApi,
}: BuildCommandMenuItemFromStandardKeyParams) => {
const Icon = getIcon(item.icon, COMMAND_MENU_DEFAULT_ICON);
const component = ENGINE_COMPONENT_KEY_COMPONENT_MAP[engineComponentKey];
return {
type,
key: `command-menu-item-engine-${item.id}`,
scope,
label: item.label,
shortLabel: item.shortLabel ?? undefined,
position: item.position,
isPinned,
Icon,
shouldBeRegistered: () =>
evaluateConditionalAvailabilityExpression(
item.conditionalAvailabilityExpression,
commandMenuContextApi,
),
component,
};
};
export const useCommandMenuItemFrontComponentCommands = (
commandMenuContextApi: CommandMenuContextApi,
) => {
@@ -159,74 +204,102 @@ export const useCommandMenuItemFrontComponentCommands = (
const { data } = useFindManyCommandMenuItemsQuery();
const frontComponentItems =
data?.commandMenuItems?.filter(
(item): item is CommandMenuItemWithFrontComponent =>
isDefined(item.frontComponentId),
) ?? [];
const allItems = data?.commandMenuItems ?? [];
const objectMatches = (item: CommandMenuItemWithFrontComponent) =>
const objectMatches = (item: CommandMenuItemFieldsFragment) =>
!isDefined(item.availabilityObjectMetadataId) ||
item.availabilityObjectMetadataId ===
contextStoreCurrentObjectMetadataItemId;
const frontComponentItemsWithObjectMatches =
frontComponentItems.filter(objectMatches);
const itemsWithObjectMatches = allItems.filter(objectMatches);
const globalItems = frontComponentItemsWithObjectMatches.filter(
const buildCommandMenuItem = ({
item,
scope,
isPinned,
typeOverride,
}: {
item: CommandMenuItemFieldsFragment;
scope: CommandMenuItemScope;
isPinned: boolean;
typeOverride?: CommandMenuItemType;
}) => {
if (isDefined(item.engineComponentKey)) {
return buildCommandItemFromEngineKey({
item,
engineComponentKey: item.engineComponentKey,
type: typeOverride,
scope,
isPinned,
getIcon,
commandMenuContextApi,
});
}
if (isDefined(item.frontComponentId)) {
return buildCommandMenuItemFromFrontComponent({
item: item as CommandMenuItemWithFrontComponent,
type: typeOverride,
scope,
isPinned,
getIcon,
openFrontComponentInSidePanel,
mountHeadlessFrontComponent,
commandMenuContextApi,
mountContext,
});
}
return null;
};
const globalItems = itemsWithObjectMatches.filter(
(item) => item.availabilityType === CommandMenuItemAvailabilityType.GLOBAL,
);
const recordScopedItems = frontComponentItemsWithObjectMatches.filter(
const recordScopedItems = itemsWithObjectMatches.filter(
(item) =>
item.availabilityType ===
CommandMenuItemAvailabilityType.RECORD_SELECTION,
);
const fallbackItems = frontComponentItemsWithObjectMatches.filter(
const fallbackItems = itemsWithObjectMatches.filter(
(item) =>
item.availabilityType === CommandMenuItemAvailabilityType.FALLBACK,
);
const globalCommandMenuItems = globalItems.map((item) =>
buildCommandMenuItemFromFrontComponent({
item,
scope: CommandMenuItemScope.Global,
isPinned: !contextStoreIsPageInEditMode && item.isPinned,
getIcon,
openFrontComponentInSidePanel,
mountHeadlessFrontComponent,
commandMenuContextApi,
}),
);
const globalCommandMenuItems = globalItems
.map((item) =>
buildCommandMenuItem({
item,
scope: CommandMenuItemScope.Global,
isPinned: !contextStoreIsPageInEditMode && item.isPinned,
}),
)
.filter(isDefined);
const recordScopedCommandMenuItems = hasRecordSelection
? recordScopedItems.map((item) =>
buildCommandMenuItemFromFrontComponent({
item,
scope: CommandMenuItemScope.RecordSelection,
isPinned: !contextStoreIsPageInEditMode && item.isPinned,
getIcon,
openFrontComponentInSidePanel,
mountHeadlessFrontComponent,
commandMenuContextApi,
mountContext,
}),
)
? recordScopedItems
.map((item) =>
buildCommandMenuItem({
item,
scope: CommandMenuItemScope.RecordSelection,
isPinned: !contextStoreIsPageInEditMode && item.isPinned,
}),
)
.filter(isDefined)
: [];
const fallbackCommandMenuItems = fallbackItems.map((item) =>
buildCommandMenuItemFromFrontComponent({
item,
type: CommandMenuItemType.Fallback,
scope: CommandMenuItemScope.Global,
isPinned: false,
getIcon,
openFrontComponentInSidePanel,
mountHeadlessFrontComponent,
commandMenuContextApi,
}),
);
const fallbackCommandMenuItems = fallbackItems
.map((item) =>
buildCommandMenuItem({
item,
scope: CommandMenuItemScope.Global,
isPinned: false,
typeOverride: CommandMenuItemType.Fallback,
}),
)
.filter(isDefined);
return [
...globalCommandMenuItems,
@@ -15,8 +15,9 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { type ChangeEvent, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, parseJson } from 'twenty-shared/utils';
import { MenuItem, MenuItemMultiSelectAvatar } from 'twenty-ui/navigation';
import { z } from 'zod';
export const EMPTY_FILTER_VALUE = '[]';
export const MAX_ITEMS_TO_DISPLAY = 5;
@@ -44,7 +45,10 @@ export const ObjectFilterDropdownCountrySelect = () => {
const selectedCountryNames = isNonEmptyString(
objectFilterDropdownCurrentRecordFilter?.value,
)
? (JSON.parse(objectFilterDropdownCurrentRecordFilter.value) as string[]) // TODO: replace by a safe parse
? (z
.array(z.string())
.safeParse(parseJson(objectFilterDropdownCurrentRecordFilter.value))
.data ?? [])
: [];
const filteredSelectableItems = countriesAsSelectableItems.filter(
@@ -14,8 +14,9 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { type ChangeEvent, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, parseJson } from 'twenty-shared/utils';
import { MenuItem, MenuItemMultiSelectAvatar } from 'twenty-ui/navigation';
import { z } from 'zod';
export const EMPTY_FILTER_VALUE = '[]';
export const MAX_ITEMS_TO_DISPLAY = 3;
@@ -41,7 +42,10 @@ export const ObjectFilterDropdownCurrencySelect = () => {
const selectedCurrencies = isNonEmptyString(
objectFilterDropdownCurrentRecordFilter?.value,
)
? (JSON.parse(objectFilterDropdownCurrentRecordFilter.value) as string[]) // TODO: replace by a safe parse
? (z
.array(z.string())
.safeParse(parseJson(objectFilterDropdownCurrentRecordFilter.value))
.data ?? [])
: [];
const filteredSelectableItems = currenciesAsSelectableItems.filter(
@@ -22,8 +22,9 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { MAX_OPTIONS_TO_DISPLAY } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, parseJson } from 'twenty-shared/utils';
import { MenuItem, MenuItemMultiSelect } from 'twenty-ui/navigation';
import { z } from 'zod';
export const EMPTY_FILTER_VALUE = '';
@@ -58,9 +59,10 @@ export const ObjectFilterDropdownOptionSelect = ({
const selectedOptions = useMemo(
() =>
isNonEmptyString(objectFilterDropdownCurrentRecordFilter?.value)
? (JSON.parse(
objectFilterDropdownCurrentRecordFilter.value,
) as string[]) // TODO: replace by a safe parse
? (z
.array(z.string())
.safeParse(parseJson(objectFilterDropdownCurrentRecordFilter.value))
.data ?? [])
: [],
[objectFilterDropdownCurrentRecordFilter?.value],
);
@@ -11,7 +11,8 @@ import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, parseJson } from 'twenty-shared/utils';
import { z } from 'zod';
export const EMPTY_FILTER_VALUE = '[]';
export const MAX_ITEMS_TO_DISPLAY = 3;
@@ -39,7 +40,10 @@ export const ObjectFilterDropdownSourceSelect = ({
const selectedSources = isNonEmptyString(
objectFilterDropdownCurrentRecordFilter?.value,
)
? (JSON.parse(objectFilterDropdownCurrentRecordFilter.value) as string[]) // TODO: replace by a safe parse
? (z
.array(z.string())
.safeParse(parseJson(objectFilterDropdownCurrentRecordFilter.value))
.data ?? [])
: [];
const sourceTypes = getActorSourceMultiSelectOptions(selectedSources);
@@ -15,7 +15,7 @@ import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/h
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useStore } from 'jotai';
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { computeRecordGqlOperationFilter } from 'twenty-shared/utils';
export const RecordTableEmptyHasNewRecordEffect = () => {
@@ -79,20 +79,28 @@ export const RecordTableEmptyHasNewRecordEffect = () => {
operationSignature,
});
const handleObjectRecordOperation = (
objectRecordOperationEventDetail: ObjectRecordOperationBrowserEventDetail,
) => {
const objectRecordOperation = objectRecordOperationEventDetail.operation;
const handleObjectRecordOperation = useCallback(
(
objectRecordOperationEventDetail: ObjectRecordOperationBrowserEventDetail,
) => {
const objectRecordOperation = objectRecordOperationEventDetail.operation;
if (
objectRecordOperation.type.includes('update') ||
objectRecordOperation.type.includes('create')
) {
if (!isRecordTableInitialLoading && !recordTableHasRecords) {
store.set(recordTableWentFromEmptyToNotEmptyCallbackState, true);
if (
objectRecordOperation.type.includes('update') ||
objectRecordOperation.type.includes('create')
) {
if (!isRecordTableInitialLoading && !recordTableHasRecords) {
store.set(recordTableWentFromEmptyToNotEmptyCallbackState, true);
}
}
}
};
},
[
recordTableHasRecords,
isRecordTableInitialLoading,
store,
recordTableWentFromEmptyToNotEmptyCallbackState,
],
);
useListenToObjectRecordOperationBrowserEvent({
onObjectRecordOperationBrowserEvent: handleObjectRecordOperation,
@@ -25,6 +25,8 @@ import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { IconNewSection } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
const StyledGroupsDroppable = styled.div`
display: flex;
@@ -32,6 +34,10 @@ const StyledGroupsDroppable = styled.div`
width: 100%;
`;
const StyledAddGroupButtonContainer = styled.div`
border: 1px solid transparent;
`;
type FieldsConfigurationEditorProps = {
pageLayoutId: string;
widgetId: string;
@@ -250,6 +256,16 @@ export const FieldsConfigurationEditor = ({
</Draggable>
))}
{provided.placeholder}
<StyledAddGroupButtonContainer>
<MenuItem
LeftIcon={IconNewSection}
text={t`Add a Group`}
onClick={() => handleAddGroup({})}
withIconContainer
withIconContainerBackground={false}
/>
</StyledAddGroupButtonContainer>
</StyledGroupsDroppable>
)}
</Droppable>
@@ -1,5 +1,10 @@
import { useLingui } from '@lingui/react/macro';
import { IconDotsVertical, IconPencil, IconTrash } from 'twenty-ui/display';
import {
IconDotsVertical,
IconNewSection,
IconPencil,
IconTrash,
} from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
@@ -14,12 +19,14 @@ type FieldsConfigurationGroupDropdownProps = {
groupId: string;
onStartRename: () => void;
onDelete: () => void;
onAddGroup?: () => void;
};
export const FieldsConfigurationGroupDropdown = ({
groupId,
onStartRename,
onDelete,
onAddGroup,
}: FieldsConfigurationGroupDropdownProps) => {
const { t } = useLingui();
@@ -37,6 +44,11 @@ export const FieldsConfigurationGroupDropdown = ({
onDelete();
};
const handleAddGroup = () => {
closeDropdown(dropdownId);
onAddGroup?.();
};
return (
<Dropdown
dropdownId={dropdownId}
@@ -59,6 +71,12 @@ export const FieldsConfigurationGroupDropdown = ({
accent="danger"
text={t`Delete`}
/>
<MenuItem
LeftIcon={IconNewSection}
onClick={handleAddGroup}
accent="default"
text={t`Add a Group`}
/>
</DropdownMenuItemsContainer>
</DropdownContent>
}
@@ -15,8 +15,6 @@ import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
import { IconNewSection } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
import { FieldsConfigurationGroupDraggableHeader } from '@/page-layout/widgets/fields/components/FieldsConfigurationGroupDraggableHeader';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -172,6 +170,7 @@ export const FieldsConfigurationGroupEditor = ({
groupId={group.id}
onStartRename={handleStartRename}
onDelete={() => onDeleteGroup({ groupId: group.id })}
onAddGroup={onAddGroup}
/>
</StyledDropdownContainer>
</StyledGroupHeaderRow>
@@ -215,13 +214,6 @@ export const FieldsConfigurationGroupEditor = ({
</StyledFieldsDroppable>
)}
</Droppable>
<MenuItem
LeftIcon={IconNewSection}
withIconContainer
text={t`Add a Section`}
onClick={onAddGroup}
/>
</StyledGroupContainer>
);
};
@@ -82,9 +82,10 @@ export const FieldsConfigurationUngroupedEditor = ({
<MenuItem
LeftIcon={IconNewSection}
withIconContainer
text={t`Add a Section`}
text={t`Add a Group`}
onClick={onAddGroup}
withIconContainer
withIconContainerBackground={false}
/>
</StyledFieldsDroppable>
)}
@@ -42,15 +42,15 @@ const StyledPropertyBox = styled.div`
`;
const StyledInlineFieldsPropertyBox = styled.div<{
hasMoreSection: boolean;
hasMoreGroup: boolean;
}>`
align-self: stretch;
border-radius: ${themeCssVariables.border.radius.sm};
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing[2]};
padding-bottom: ${({ hasMoreSection }) =>
hasMoreSection ? themeCssVariables.spacing[3] : '0'};
padding-bottom: ${({ hasMoreGroup }) =>
hasMoreGroup ? themeCssVariables.spacing[3] : '0'};
padding-top: 0;
`;
@@ -137,7 +137,7 @@ export const FieldsWidget = ({ widget }: FieldsWidgetProps) => {
>
{displayMode === 'inline' ? (
<StyledInlineFieldsPropertyBox
hasMoreSection={shouldShowHiddenFields}
hasMoreGroup={shouldShowHiddenFields}
>
<FieldsWidgetFieldList
fields={groups.flatMap((group) => group.fields)}
@@ -1,13 +1,14 @@
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { groupObjectRecordSseEventsByObjectMetadataItemNameSingular } from '@/sse-db-event/utils/groupObjectRecordSseEventsByObjectMetadataItemNameSingular';
import { turnSseObjectRecordEventsToObjectRecordOperationBrowserEvents } from '@/sse-db-event/utils/turnSseObjectRecordEventToObjectRecordOperationBrowserEvent';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { type ObjectRecordEventWithQueryIds } from '~/generated-metadata/graphql';
export const useDispatchObjectRecordEventsFromSseToBrowserEvents = () => {
const { objectMetadataItems } = useObjectMetadataItems();
const store = useStore();
const dispatchObjectRecordEventsFromSseToBrowserEvents = useCallback(
(objectRecordEventsWithQueryIds: ObjectRecordEventWithQueryIds[]) => {
@@ -24,6 +25,8 @@ export const useDispatchObjectRecordEventsFromSseToBrowserEvents = () => {
objectRecordEventsByObjectMetadataItemNameSingular.keys(),
);
const objectMetadataItems = store.get(objectMetadataItemsState.atom);
for (const objectMetadataItemNameSingular of objectMetadataItemNamesSingular) {
const objectRecordEventsForThisObjectMetadataItem =
objectRecordEventsByObjectMetadataItemNameSingular.get(
@@ -49,7 +52,7 @@ export const useDispatchObjectRecordEventsFromSseToBrowserEvents = () => {
}
}
},
[objectMetadataItems],
[store],
);
return { dispatchObjectRecordEventsFromSseToBrowserEvents };
@@ -14,11 +14,11 @@ import { captureException } from '@sentry/react';
import { isNonEmptyString } from '@sniptt/guards';
import { print, type ExecutionResult } from 'graphql';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type EventSubscription } from '~/generated-metadata/graphql';
import { useStore } from 'jotai';
export const useTriggerEventStreamCreation = () => {
const store = useStore();
@@ -79,7 +79,7 @@ export const useTriggerEventStreamCreation = () => {
onEventSubscription: EventSubscription;
}>,
) => {
if (isDefined(value?.errors)) {
if (isDefined(value?.errors) && Array.isArray(value.errors)) {
captureException(
new Error(`SSE subscription error: ${value.errors[0]?.message}`),
);
@@ -9,7 +9,10 @@ import { Logo } from '@/auth/components/Logo';
import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useFetchAndLoadIndexViews } from '@/metadata-store/hooks/useFetchAndLoadIndexViews';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
@@ -19,6 +22,7 @@ import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser';
import { ApolloError } from '@apollo/client';
import { Trans, useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useStore } from 'jotai';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { isDefined } from 'twenty-shared/utils';
import { H2Title } from 'twenty-ui/display';
@@ -68,6 +72,9 @@ export const CreateWorkspace = () => {
const { enqueueErrorSnackBar } = useSnackBar();
const setNextOnboardingStatus = useSetNextOnboardingStatus();
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
const { updateDraft, applyChanges } = useMetadataStore();
const { fetchAndLoadIndexViews } = useFetchAndLoadIndexViews();
const store = useStore();
const { loadCurrentUser } = useLoadCurrentUser();
const [activateWorkspace] = useActivateWorkspaceMutation();
@@ -117,12 +124,18 @@ export const CreateWorkspace = () => {
},
},
});
if (isDefined(result.errors)) {
throw result.errors ?? new Error(t`Unknown error`);
}
await refreshObjectMetadataItems();
const loadedObjects = store.get(objectMetadataItemsState.atom);
updateDraft('objectMetadataItems', loadedObjects);
applyChanges();
await fetchAndLoadIndexViews();
await loadCurrentUser();
setNextOnboardingStatus();
} catch (error: any) {
@@ -138,6 +151,10 @@ export const CreateWorkspace = () => {
enqueueErrorSnackBar,
loadCurrentUser,
refreshObjectMetadataItems,
updateDraft,
applyChanges,
store,
fetchAndLoadIndexViews,
setNextOnboardingStatus,
t,
],
@@ -131,16 +131,12 @@ export const InviteTeam = () => {
),
);
if (emails.length === 0) {
setNextOnboardingStatus();
return;
}
const result = await sendInvitation({ emails });
if (isDefined(result.errors)) {
throw result.errors;
}
if (emails.length > 0) {
enqueueSuccessSnackBar({
message: t`Invite link sent to email addresses`,
@@ -55,9 +55,14 @@ export class CheckServerOrchestratorStep {
return false;
}
const wasReady = step.output.isReady;
step.output = { isReady: true, errorLogged: false };
step.status = 'done';
this.notify();
if (!wasReady) {
this.notify();
}
return true;
}
@@ -65,11 +65,14 @@ export class UploadFilesOrchestratorStep {
return;
}
step.status = 'in_progress';
this.state.addEvent({
message: `Uploading ${builtPath}`,
status: 'info',
});
this.state.updateEntityStatus(sourcePath, 'uploading');
this.notify();
const uploadPromise = step.output.fileUploader
.uploadFile({ builtPath, fileFolder })
@@ -95,6 +98,11 @@ export class UploadFilesOrchestratorStep {
})
.finally(() => {
step.output.activeUploads.delete(uploadPromise);
if (step.output.activeUploads.size === 0) {
step.status = 'done';
this.notify();
}
});
step.output.activeUploads.add(uploadPromise);
@@ -2,6 +2,7 @@ import { type OrchestratorStateEvent } from '@/cli/utilities/dev/orchestrator/de
import { DevUiApplicationPanel } from '@/cli/utilities/dev/ui/components/dev-ui-application-panel';
import { DevUiEntityLegend } from '@/cli/utilities/dev/ui/components/dev-ui-entity-section';
import { DevUiEventItem } from '@/cli/utilities/dev/ui/components/dev-ui-event-log';
import { AnimationProvider } from '@/cli/utilities/dev/ui/dev-ui-animation-context';
import { InkProvider, useInk } from '@/cli/utilities/dev/ui/dev-ui-ink-context';
import { type DevUiStateManager } from '@/cli/utilities/dev/ui/dev-ui-state-manager';
import React, { useReducer, useEffect } from 'react';
@@ -45,7 +46,9 @@ export const renderDevUI = async (
const { unmount } = render(
<InkProvider value={{ Box, Text, Static }}>
<DevUI uiStateManager={uiStateManager} />
<AnimationProvider>
<DevUI uiStateManager={uiStateManager} />
</AnimationProvider>
</InkProvider>,
);
@@ -0,0 +1,49 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import {
SPINNER_FRAMES,
UPLOAD_FRAMES,
} from '@/cli/utilities/dev/ui/dev-ui-constants';
type AnimationFrames = {
spinnerFrame: string;
uploadFrame: string;
};
const AnimationContext = createContext<AnimationFrames>({
spinnerFrame: SPINNER_FRAMES[0],
uploadFrame: UPLOAD_FRAMES[0],
});
const TICK_INTERVAL_MS = 120;
const UPLOAD_TICK_RATIO = Math.round(200 / TICK_INTERVAL_MS);
export const AnimationProvider = ({
children,
}: {
children: React.ReactNode;
}): React.ReactElement => {
const [tick, setTick] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setTick((previous) => previous + 1);
}, TICK_INTERVAL_MS);
return () => clearInterval(timer);
}, []);
const spinnerFrame = SPINNER_FRAMES[tick % SPINNER_FRAMES.length];
const uploadFrame =
UPLOAD_FRAMES[Math.floor(tick / UPLOAD_TICK_RATIO) % UPLOAD_FRAMES.length];
return (
<AnimationContext.Provider value={{ spinnerFrame, uploadFrame }}>
{children}
</AnimationContext.Provider>
);
};
export const useAnimationFrames = (): AnimationFrames => {
return useContext(AnimationContext);
};
@@ -1,44 +1,12 @@
import { useState, useEffect } from 'react';
import {
type DevUiStatus,
DEV_UI_STATUS_CONFIG,
SPINNER_FRAMES,
UPLOAD_FRAMES,
} from '@/cli/utilities/dev/ui/dev-ui-constants';
export const useAnimatedFrame = (
frames: string[],
interval = 80,
enabled = true,
): string => {
const [frameIndex, setFrameIndex] = useState(0);
useEffect(() => {
if (!enabled) return;
const timer = setInterval(() => {
setFrameIndex((currentIndex) => (currentIndex + 1) % frames.length);
}, interval);
return () => clearInterval(timer);
}, [frames, interval, enabled]);
return frames[frameIndex];
};
import { useAnimationFrames } from '@/cli/utilities/dev/ui/dev-ui-animation-context';
export const useStatusIcon = (uiStatus: DevUiStatus): string => {
const config = DEV_UI_STATUS_CONFIG[uiStatus];
const spinnerFrame = useAnimatedFrame(
SPINNER_FRAMES,
80,
config.icon === 'spinner',
);
const uploadFrame = useAnimatedFrame(
UPLOAD_FRAMES,
200,
config.icon === 'upload',
);
const { spinnerFrame, uploadFrame } = useAnimationFrames();
if (config.icon === 'spinner') return spinnerFrame;
if (config.icon === 'upload') return uploadFrame;
@@ -5,6 +5,7 @@ export type DevUiStateListener = (state: OrchestratorState) => void;
export class DevUiStateManager {
private orchestratorState: OrchestratorState;
private listeners = new Set<DevUiStateListener>();
private pendingNotify = false;
constructor(orchestratorState: OrchestratorState) {
this.orchestratorState = orchestratorState;
@@ -22,8 +23,18 @@ export class DevUiStateManager {
}
notify(): void {
for (const listener of this.listeners) {
listener(this.orchestratorState);
if (this.pendingNotify) {
return;
}
this.pendingNotify = true;
queueMicrotask(() => {
this.pendingNotify = false;
for (const listener of this.listeners) {
listener(this.orchestratorState);
}
});
}
}

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