merge: resolve conflicts with latest main, add EngineComponentKey

- Resolve 4 merge conflicts properly:
  - generated-metadata/graphql.ts: keep typed-document-node format, add
    EngineComponentKey enum and engineComponentKey field to CommandMenuItem
  - useAgentChatData.ts: keep useEffect + useQuery, add useMemo from main
  - useCommandMenuItemFrontComponentCommands.tsx: keep FindManyCommandMenuItemsDocument, add EngineComponentKey
  - CreateWorkspace.tsx: keep result.error (v4 MutateResult API)
- Add TODO to useSnackBarOnQueryError about centralizing error handling

https://claude.ai/code/session_019WGZ6Rd7sEHuBg9sTrXRqJ
This commit is contained in:
Claude
2026-03-12 14:13:59 +00:00
280 changed files with 7636 additions and 2773 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"
@@ -25,6 +25,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,
@@ -184,7 +185,11 @@ export const useAgentChatData = () => {
}
}, [threadsData, store, setCurrentAIChatThread, setAgentChatInput, setCurrentAIChatThreadTitle, setAgentChatUsage]);
const isNewThread = currentAIChatThread === AGENT_CHAT_NEW_THREAD_DRAFT_KEY;
const isNewThread = useMemo(
() => currentAIChatThread === AGENT_CHAT_NEW_THREAD_DRAFT_KEY,
[currentAIChatThread],
);
const { loading: messagesLoading, data } = useQuery(GetChatMessagesDocument, {
variables: { threadId: currentAIChatThread! },
skip: !isDefined(currentAIChatThread) || isNewThread,
@@ -197,7 +202,7 @@ export const useAgentChatData = () => {
}
}, [data, store, scrollToBottom]);
const ensureThreadForDraft = () => {
const ensureThreadForDraft = useCallback(() => {
const current = store.get(currentAIChatThreadState.atom);
if (current !== AGENT_CHAT_NEW_THREAD_DRAFT_KEY) {
return;
@@ -224,9 +229,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;
@@ -253,16 +265,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;
@@ -4,6 +4,12 @@ import { useEffect } from 'react';
// Utility hook to show an error snackbar when a query returns an error.
// Replaces the removed onError callback from useQuery in Apollo v4.
//
// TODO: Consider moving generic error→snackbar handling into the centralized
// Apollo error link (apollo.factory.ts) which already handles auth errors,
// Sentry, retries, etc. This would eliminate the need for per-component
// useEffect calls. Keep this hook only for call sites that need a custom
// error message (e.g. useWebhookForm, SettingsAdminWorkerMetricsGraph).
export const useSnackBarOnQueryError = (
error: Error | CombinedGraphQLErrors | undefined,
message?: string,
@@ -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
@@ -142,8 +142,10 @@ export const DEFAULT_RECORD_COMMAND_MENU_ITEMS_CONFIG: Record<
selectedRecord,
hasAnySoftDeleteFilterOnView,
objectPermissions,
objectMetadataItem,
}) =>
(isDefined(selectedRecord) &&
(!objectMetadataItem?.isSystem &&
isDefined(selectedRecord) &&
!selectedRecord.isRemote &&
!hasAnySoftDeleteFilterOnView &&
objectPermissions.canSoftDeleteObjectRecords &&
@@ -170,8 +172,10 @@ export const DEFAULT_RECORD_COMMAND_MENU_ITEMS_CONFIG: Record<
isRemote,
hasAnySoftDeleteFilterOnView,
numberOfSelectedRecords,
objectMetadataItem,
}) =>
(objectPermissions.canSoftDeleteObjectRecords &&
(!objectMetadataItem?.isSystem &&
objectPermissions.canSoftDeleteObjectRecords &&
!isRemote &&
!hasAnySoftDeleteFilterOnView &&
isDefined(numberOfSelectedRecords) &&
@@ -196,8 +200,10 @@ export const DEFAULT_RECORD_COMMAND_MENU_ITEMS_CONFIG: Record<
isRemote,
isShowPage,
hasAnySoftDeleteFilterOnView,
objectMetadataItem,
}) =>
(!isRemote &&
(!objectMetadataItem?.isSystem &&
!isRemote &&
isDefined(selectedRecord?.deletedAt) &&
objectPermissions.canSoftDeleteObjectRecords &&
((isDefined(isShowPage) && isShowPage) ||
@@ -225,8 +231,10 @@ export const DEFAULT_RECORD_COMMAND_MENU_ITEMS_CONFIG: Record<
isRemote,
hasAnySoftDeleteFilterOnView,
numberOfSelectedRecords,
objectMetadataItem,
}) =>
(objectPermissions.canSoftDeleteObjectRecords &&
(!objectMetadataItem?.isSystem &&
objectPermissions.canSoftDeleteObjectRecords &&
!isRemote &&
isDefined(hasAnySoftDeleteFilterOnView) &&
hasAnySoftDeleteFilterOnView &&
@@ -246,8 +254,14 @@ export const DEFAULT_RECORD_COMMAND_MENU_ITEMS_CONFIG: Record<
Icon: IconTrashX,
accent: 'danger',
isPinned: true,
shouldBeRegistered: ({ selectedRecord, objectPermissions, isRemote }) =>
(objectPermissions.canDestroyObjectRecords &&
shouldBeRegistered: ({
selectedRecord,
objectPermissions,
isRemote,
objectMetadataItem,
}) =>
(!objectMetadataItem?.isSystem &&
objectPermissions.canDestroyObjectRecords &&
!isRemote &&
isDefined(selectedRecord?.deletedAt)) ??
false,
@@ -272,8 +286,10 @@ export const DEFAULT_RECORD_COMMAND_MENU_ITEMS_CONFIG: Record<
isRemote,
hasAnySoftDeleteFilterOnView,
numberOfSelectedRecords,
objectMetadataItem,
}) =>
(objectPermissions.canDestroyObjectRecords &&
(!objectMetadataItem?.isSystem &&
objectPermissions.canDestroyObjectRecords &&
!isRemote &&
isDefined(hasAnySoftDeleteFilterOnView) &&
hasAnySoftDeleteFilterOnView &&
@@ -297,7 +313,9 @@ export const DEFAULT_RECORD_COMMAND_MENU_ITEMS_CONFIG: Record<
selectedRecord,
isFavorite,
hasAnySoftDeleteFilterOnView,
objectMetadataItem,
}) =>
!objectMetadataItem?.isSystem &&
!selectedRecord?.isRemote &&
!isFavorite &&
!isDefined(selectedRecord?.deletedAt) &&
@@ -321,7 +339,9 @@ export const DEFAULT_RECORD_COMMAND_MENU_ITEMS_CONFIG: Record<
selectedRecord,
isFavorite,
hasAnySoftDeleteFilterOnView,
objectMetadataItem,
}) =>
!objectMetadataItem?.isSystem &&
isDefined(selectedRecord) &&
!selectedRecord?.isRemote &&
isDefined(isFavorite) &&
@@ -392,8 +412,10 @@ export const DEFAULT_RECORD_COMMAND_MENU_ITEMS_CONFIG: Record<
Icon: IconEdit,
accent: 'default',
isPinned: true,
shouldBeRegistered: ({ objectPermissions, isRemote }) =>
objectPermissions.canUpdateObjectRecords && !isRemote,
shouldBeRegistered: ({ objectPermissions, isRemote, objectMetadataItem }) =>
!objectMetadataItem?.isSystem &&
objectPermissions.canUpdateObjectRecords &&
!isRemote,
availableOn: [CommandMenuItemViewType.INDEX_PAGE_BULK_SELECTION],
component: <UpdateMultipleRecordsCommand />,
},
@@ -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';
@@ -23,6 +24,7 @@ import { useQuery } from '@apollo/client/react';
import {
CommandMenuItemAvailabilityType,
type CommandMenuItemFieldsFragment,
type EngineComponentKey,
FindManyCommandMenuItemsDocument,
} from '~/generated-metadata/graphql';
@@ -31,6 +33,10 @@ type CommandMenuItemWithFrontComponent = CommandMenuItemFieldsFragment & {
conditionalAvailabilityExpression?: string | null;
};
type CommandMenuItemWithSource = CommandMenuItemFieldsFragment & {
conditionalAvailabilityExpression?: string | null;
};
type BuildCommandMenuItemFromFrontComponentParams = {
item: CommandMenuItemWithFrontComponent;
type?: CommandMenuItemType;
@@ -54,8 +60,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,
@@ -116,6 +120,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,
) => {
@@ -160,74 +205,102 @@ export const useCommandMenuItemFrontComponentCommands = (
const { data } = useQuery(FindManyCommandMenuItemsDocument);
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,
@@ -1,3 +1,5 @@
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { PageLayoutLeftPanel } from '@/page-layout/components/PageLayoutLeftPanel';
import { PageLayoutTabList } from '@/page-layout/components/PageLayoutTabList';
import { PageLayoutTabListEffect } from '@/page-layout/components/PageLayoutTabListEffect';
@@ -19,6 +21,7 @@ import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingC
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
@@ -72,6 +75,17 @@ export const PageLayoutRendererContent = () => {
const isMobile = useIsMobile();
const metadataStore = useAtomFamilyStateValue(
metadataStoreState,
'objectMetadataItems',
);
const isSystemObject =
(metadataStore.current as ObjectMetadataItem[]).find(
(item) =>
item.nameSingular === targetRecordIdentifier?.targetObjectNameSingular,
)?.isSystem ?? false;
if (!isDefined(currentPageLayout)) {
return null;
}
@@ -100,8 +114,14 @@ export const PageLayoutRendererContent = () => {
isEditMode: isPageLayoutInEditMode,
});
const tabsForCurrentObject = isSystemObject
? tabsWithVisibleWidgets.filter(
(tab) => tab.title === 'Home' || tab.title === 'Timeline',
)
: tabsWithVisibleWidgets;
const { tabsToRenderInTabList, pinnedLeftTab } = getTabsByDisplayMode({
tabs: tabsWithVisibleWidgets,
tabs: tabsForCurrentObject,
pageLayoutType: currentPageLayout.type,
isMobile,
isInSidePanel,
@@ -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 { CombinedGraphQLErrors } from '@apollo/client/errors';
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';
@@ -69,6 +73,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] = useMutation(ActivateWorkspaceDocument);
@@ -124,6 +131,13 @@ export const CreateWorkspace = () => {
}
await refreshObjectMetadataItems();
const loadedObjects = store.get(objectMetadataItemsState.atom);
updateDraft('objectMetadataItems', loadedObjects);
applyChanges();
await fetchAndLoadIndexViews();
await loadCurrentUser();
setNextOnboardingStatus();
} catch (error: any) {
@@ -139,6 +153,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.error)) {
throw result.error;
}
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;

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