Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6abe8059c9 | ||
|
|
06d4d62e90 | ||
|
|
eb4665bc98 | ||
|
|
f19fcd0010 | ||
|
|
cb3e32df86 | ||
|
|
db5b4d9c6c | ||
|
|
660536d6bb | ||
|
|
e8f8189167 | ||
|
|
78473a606a | ||
|
|
b21fb4aa6f | ||
|
|
38664249cf | ||
|
|
69542898a1 | ||
|
|
09beddb63d | ||
|
|
2eac82c207 |
@@ -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
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
+28
-24
@@ -1,35 +1,39 @@
|
||||
import { downloadFile } from '@/activities/files/utils/downloadFile';
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
jest.mock('file-saver', () => ({
|
||||
saveAs: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockBlob = new Blob(['test content'], { type: 'application/pdf' });
|
||||
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
status: 200,
|
||||
blob: jest.fn(),
|
||||
blob: () => Promise.resolve(mockBlob),
|
||||
} as unknown as Response),
|
||||
);
|
||||
|
||||
window.URL.createObjectURL = jest.fn(() => 'mock-url');
|
||||
window.URL.revokeObjectURL = jest.fn();
|
||||
|
||||
// FIXME: jest is behaving weirdly here, it's not finding the element
|
||||
// Also the document's innerHTML is empty
|
||||
// `global.fetch` and `window.fetch` are also undefined
|
||||
describe.skip('downloadFile', () => {
|
||||
it('should download a file', () => {
|
||||
downloadFile('url/to/file.pdf', 'file.pdf');
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith('url/to/file.pdf');
|
||||
|
||||
const link = document.querySelector(
|
||||
'a[href="mock-url"][download="file.pdf"]',
|
||||
);
|
||||
|
||||
expect(link).not.toBeNull();
|
||||
// oxlint-disable-next-line @typescripttypescript/ban-ts-comment
|
||||
// @ts-ignore
|
||||
expect(link?.style?.display).toBe('none');
|
||||
|
||||
expect(link).toHaveBeenCalledTimes(1);
|
||||
|
||||
describe('downloadFile', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should download a file', async () => {
|
||||
await downloadFile('url/to/file.pdf', 'file.pdf');
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith('url/to/file.pdf');
|
||||
expect(saveAs).toHaveBeenCalledWith(mockBlob, 'file.pdf');
|
||||
});
|
||||
|
||||
it('should reject when fetch fails', async () => {
|
||||
(global.fetch as jest.Mock).mockResolvedValueOnce({
|
||||
status: 404,
|
||||
blob: () => Promise.resolve(mockBlob),
|
||||
});
|
||||
|
||||
await expect(downloadFile('url/to/file.pdf', 'file.pdf')).rejects.toBe(
|
||||
'Failed downloading file',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
export const downloadFile = (fullPath: string, fileName: string) => {
|
||||
fetch(fullPath)
|
||||
return fetch(fullPath)
|
||||
.then((resp) =>
|
||||
resp.status === 200
|
||||
? resp.blob()
|
||||
|
||||
@@ -24,6 +24,7 @@ import { mapDBMessagesToUIMessages } from '@/ai/utils/mapDBMessagesToUIMessages'
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
type GetChatThreadsQuery,
|
||||
GetChatThreadsDocument,
|
||||
@@ -181,7 +182,11 @@ export const useAgentChatData = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const isNewThread = currentAIChatThread === AGENT_CHAT_NEW_THREAD_DRAFT_KEY;
|
||||
const isNewThread = useMemo(
|
||||
() => currentAIChatThread === AGENT_CHAT_NEW_THREAD_DRAFT_KEY,
|
||||
[currentAIChatThread],
|
||||
);
|
||||
|
||||
const { loading: messagesLoading, data } = useGetChatMessagesQuery({
|
||||
variables: { threadId: currentAIChatThread! },
|
||||
skip: !isDefined(currentAIChatThread) || isNewThread,
|
||||
@@ -191,7 +196,7 @@ export const useAgentChatData = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const ensureThreadForDraft = () => {
|
||||
const ensureThreadForDraft = useCallback(() => {
|
||||
const current = store.get(currentAIChatThreadState.atom);
|
||||
if (current !== AGENT_CHAT_NEW_THREAD_DRAFT_KEY) {
|
||||
return;
|
||||
@@ -218,9 +223,16 @@ export const useAgentChatData = () => {
|
||||
threadIdPromise.finally(() => {
|
||||
setPendingCreateFromDraftPromise(null);
|
||||
});
|
||||
};
|
||||
}, [
|
||||
createChatThread,
|
||||
setPendingCreateFromDraftPromise,
|
||||
store,
|
||||
setIsCreatingChatThread,
|
||||
]);
|
||||
|
||||
const ensureThreadIdForSend = async (): Promise<string | null> => {
|
||||
const ensureThreadIdForSend = useCallback(async (): Promise<
|
||||
string | null
|
||||
> => {
|
||||
const current = store.get(currentAIChatThreadState.atom);
|
||||
if (current !== AGENT_CHAT_NEW_THREAD_DRAFT_KEY) {
|
||||
return current;
|
||||
@@ -247,16 +259,33 @@ export const useAgentChatData = () => {
|
||||
} finally {
|
||||
setIsCreatingChatThread(false);
|
||||
}
|
||||
};
|
||||
}, [createChatThread, store, setIsCreatingChatThread]);
|
||||
|
||||
const uiMessages = mapDBMessagesToUIMessages(data?.chatMessages || []);
|
||||
const isLoading = messagesLoading || threadsLoading;
|
||||
const threadsLoadingMemoized = useMemo(
|
||||
() => threadsLoading,
|
||||
[threadsLoading],
|
||||
);
|
||||
|
||||
const messagesLoadingMemoized = useMemo(
|
||||
() => messagesLoading,
|
||||
[messagesLoading],
|
||||
);
|
||||
|
||||
const uiMessages = useMemo(
|
||||
() => mapDBMessagesToUIMessages(data?.chatMessages || []),
|
||||
[data?.chatMessages],
|
||||
);
|
||||
|
||||
const isLoading = useMemo(
|
||||
() => messagesLoadingMemoized || threadsLoadingMemoized,
|
||||
[messagesLoadingMemoized, threadsLoadingMemoized],
|
||||
);
|
||||
|
||||
return {
|
||||
uiMessages,
|
||||
isLoading,
|
||||
threadsLoading,
|
||||
messagesLoading,
|
||||
threadsLoading: threadsLoadingMemoized,
|
||||
messagesLoading: messagesLoadingMemoized,
|
||||
ensureThreadForDraft,
|
||||
ensureThreadIdForSend,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AI_CHAT_SCROLL_WRAPPER_ID } from '@/ai/constants/AiChatScrollWrapperId'
|
||||
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
|
||||
import { scrollWrapperScrollBottomComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollBottomComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const SCROLL_BOTTOM_THRESHOLD_PX = 10;
|
||||
@@ -16,9 +17,12 @@ export const useAgentChatScrollToBottom = () => {
|
||||
AI_CHAT_SCROLL_WRAPPER_ID,
|
||||
);
|
||||
|
||||
const isNearBottom = scrollWrapperScrollBottom <= SCROLL_BOTTOM_THRESHOLD_PX;
|
||||
const isNearBottom = useMemo(
|
||||
() => scrollWrapperScrollBottom <= SCROLL_BOTTOM_THRESHOLD_PX,
|
||||
[scrollWrapperScrollBottom],
|
||||
);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
const scrollToBottom = useCallback(() => {
|
||||
const { scrollWrapperElement } = getScrollWrapperElement();
|
||||
if (!isDefined(scrollWrapperElement)) {
|
||||
return;
|
||||
@@ -27,7 +31,7 @@ export const useAgentChatScrollToBottom = () => {
|
||||
scrollWrapperElement.scrollTo({
|
||||
top: scrollWrapperElement.scrollHeight,
|
||||
});
|
||||
};
|
||||
}, [getScrollWrapperElement]);
|
||||
|
||||
return { scrollToBottom, isNearBottom };
|
||||
};
|
||||
|
||||
@@ -80,9 +80,26 @@ export const useProcessUIToolCallMessage = () => {
|
||||
|
||||
break;
|
||||
}
|
||||
case 'navigateToView':
|
||||
// TODO: implement
|
||||
case 'navigateToView': {
|
||||
const viewObjectNamePlural = objectMetadataItems.find(
|
||||
(item) =>
|
||||
item.nameSingular === navigateAppOutput.objectNameSingular,
|
||||
)?.namePlural;
|
||||
|
||||
if (!isDefined(viewObjectNamePlural)) {
|
||||
throw new Error(
|
||||
`Object with singular name ${navigateAppOutput.objectNameSingular} not found, cannot navigate to view from chat.`,
|
||||
);
|
||||
}
|
||||
|
||||
navigateApp(
|
||||
AppPath.RecordIndexPage,
|
||||
{ objectNamePlural: viewObjectNamePlural },
|
||||
{ viewId: navigateAppOutput.viewId },
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'wait': {
|
||||
await sleep(navigateAppOutput.durationMs);
|
||||
break;
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
|
||||
+77
@@ -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 />,
|
||||
};
|
||||
+1
@@ -10,6 +10,7 @@ export const COMMAND_MENU_ITEM_FRAGMENT = gql`
|
||||
name
|
||||
isHeadless
|
||||
}
|
||||
engineComponentKey
|
||||
label
|
||||
icon
|
||||
shortLabel
|
||||
|
||||
+121
-48
@@ -1,3 +1,4 @@
|
||||
import { ENGINE_COMPONENT_KEY_COMPONENT_MAP } from '@/command-menu-item/constants/EngineComponentKeyComponentMap';
|
||||
import { Command } from '@/command-menu-item/display/components/Command';
|
||||
import { HeadlessFrontComponentCommandMenuItem } from '@/command-menu-item/display/components/HeadlessFrontComponentCommandMenuItem';
|
||||
import { CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope';
|
||||
@@ -22,6 +23,7 @@ import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants
|
||||
import {
|
||||
CommandMenuItemAvailabilityType,
|
||||
type CommandMenuItemFieldsFragment,
|
||||
type EngineComponentKey,
|
||||
useFindManyCommandMenuItemsQuery,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
@@ -30,6 +32,10 @@ type CommandMenuItemWithFrontComponent = CommandMenuItemFieldsFragment & {
|
||||
conditionalAvailabilityExpression?: string | null;
|
||||
};
|
||||
|
||||
type CommandMenuItemWithSource = CommandMenuItemFieldsFragment & {
|
||||
conditionalAvailabilityExpression?: string | null;
|
||||
};
|
||||
|
||||
type BuildCommandMenuItemFromFrontComponentParams = {
|
||||
item: CommandMenuItemWithFrontComponent;
|
||||
type?: CommandMenuItemType;
|
||||
@@ -53,8 +59,6 @@ type BuildCommandMenuItemFromFrontComponentParams = {
|
||||
commandMenuContextApi: CommandMenuContextApi;
|
||||
};
|
||||
|
||||
// TODO: we should remove this backward compatibility logic in the future
|
||||
// once we have migrated all command menu items
|
||||
const buildCommandMenuItemFromFrontComponent = ({
|
||||
item,
|
||||
type = CommandMenuItemType.FrontComponent,
|
||||
@@ -115,6 +119,47 @@ const buildCommandMenuItemFromFrontComponent = ({
|
||||
};
|
||||
};
|
||||
|
||||
type BuildCommandMenuItemFromStandardKeyParams = {
|
||||
item: CommandMenuItemWithSource;
|
||||
engineComponentKey: EngineComponentKey;
|
||||
type?: CommandMenuItemType;
|
||||
scope: CommandMenuItemScope;
|
||||
isPinned: boolean;
|
||||
getIcon: ReturnType<typeof useIcons>['getIcon'];
|
||||
commandMenuContextApi: CommandMenuContextApi;
|
||||
};
|
||||
|
||||
const buildCommandItemFromEngineKey = ({
|
||||
item,
|
||||
engineComponentKey,
|
||||
type = CommandMenuItemType.Standard,
|
||||
scope,
|
||||
isPinned,
|
||||
getIcon,
|
||||
commandMenuContextApi,
|
||||
}: BuildCommandMenuItemFromStandardKeyParams) => {
|
||||
const Icon = getIcon(item.icon, COMMAND_MENU_DEFAULT_ICON);
|
||||
|
||||
const component = ENGINE_COMPONENT_KEY_COMPONENT_MAP[engineComponentKey];
|
||||
|
||||
return {
|
||||
type,
|
||||
key: `command-menu-item-engine-${item.id}`,
|
||||
scope,
|
||||
label: item.label,
|
||||
shortLabel: item.shortLabel ?? undefined,
|
||||
position: item.position,
|
||||
isPinned,
|
||||
Icon,
|
||||
shouldBeRegistered: () =>
|
||||
evaluateConditionalAvailabilityExpression(
|
||||
item.conditionalAvailabilityExpression,
|
||||
commandMenuContextApi,
|
||||
),
|
||||
component,
|
||||
};
|
||||
};
|
||||
|
||||
export const useCommandMenuItemFrontComponentCommands = (
|
||||
commandMenuContextApi: CommandMenuContextApi,
|
||||
) => {
|
||||
@@ -159,74 +204,102 @@ export const useCommandMenuItemFrontComponentCommands = (
|
||||
|
||||
const { data } = useFindManyCommandMenuItemsQuery();
|
||||
|
||||
const frontComponentItems =
|
||||
data?.commandMenuItems?.filter(
|
||||
(item): item is CommandMenuItemWithFrontComponent =>
|
||||
isDefined(item.frontComponentId),
|
||||
) ?? [];
|
||||
const allItems = data?.commandMenuItems ?? [];
|
||||
|
||||
const objectMatches = (item: CommandMenuItemWithFrontComponent) =>
|
||||
const objectMatches = (item: CommandMenuItemFieldsFragment) =>
|
||||
!isDefined(item.availabilityObjectMetadataId) ||
|
||||
item.availabilityObjectMetadataId ===
|
||||
contextStoreCurrentObjectMetadataItemId;
|
||||
|
||||
const frontComponentItemsWithObjectMatches =
|
||||
frontComponentItems.filter(objectMatches);
|
||||
const itemsWithObjectMatches = allItems.filter(objectMatches);
|
||||
|
||||
const globalItems = frontComponentItemsWithObjectMatches.filter(
|
||||
const buildCommandMenuItem = ({
|
||||
item,
|
||||
scope,
|
||||
isPinned,
|
||||
typeOverride,
|
||||
}: {
|
||||
item: CommandMenuItemFieldsFragment;
|
||||
scope: CommandMenuItemScope;
|
||||
isPinned: boolean;
|
||||
typeOverride?: CommandMenuItemType;
|
||||
}) => {
|
||||
if (isDefined(item.engineComponentKey)) {
|
||||
return buildCommandItemFromEngineKey({
|
||||
item,
|
||||
engineComponentKey: item.engineComponentKey,
|
||||
type: typeOverride,
|
||||
scope,
|
||||
isPinned,
|
||||
getIcon,
|
||||
commandMenuContextApi,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(item.frontComponentId)) {
|
||||
return buildCommandMenuItemFromFrontComponent({
|
||||
item: item as CommandMenuItemWithFrontComponent,
|
||||
type: typeOverride,
|
||||
scope,
|
||||
isPinned,
|
||||
getIcon,
|
||||
openFrontComponentInSidePanel,
|
||||
mountHeadlessFrontComponent,
|
||||
commandMenuContextApi,
|
||||
mountContext,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const globalItems = itemsWithObjectMatches.filter(
|
||||
(item) => item.availabilityType === CommandMenuItemAvailabilityType.GLOBAL,
|
||||
);
|
||||
|
||||
const recordScopedItems = frontComponentItemsWithObjectMatches.filter(
|
||||
const recordScopedItems = itemsWithObjectMatches.filter(
|
||||
(item) =>
|
||||
item.availabilityType ===
|
||||
CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
);
|
||||
|
||||
const fallbackItems = frontComponentItemsWithObjectMatches.filter(
|
||||
const fallbackItems = itemsWithObjectMatches.filter(
|
||||
(item) =>
|
||||
item.availabilityType === CommandMenuItemAvailabilityType.FALLBACK,
|
||||
);
|
||||
|
||||
const globalCommandMenuItems = globalItems.map((item) =>
|
||||
buildCommandMenuItemFromFrontComponent({
|
||||
item,
|
||||
scope: CommandMenuItemScope.Global,
|
||||
isPinned: !contextStoreIsPageInEditMode && item.isPinned,
|
||||
getIcon,
|
||||
openFrontComponentInSidePanel,
|
||||
mountHeadlessFrontComponent,
|
||||
commandMenuContextApi,
|
||||
}),
|
||||
);
|
||||
const globalCommandMenuItems = globalItems
|
||||
.map((item) =>
|
||||
buildCommandMenuItem({
|
||||
item,
|
||||
scope: CommandMenuItemScope.Global,
|
||||
isPinned: !contextStoreIsPageInEditMode && item.isPinned,
|
||||
}),
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
const recordScopedCommandMenuItems = hasRecordSelection
|
||||
? recordScopedItems.map((item) =>
|
||||
buildCommandMenuItemFromFrontComponent({
|
||||
item,
|
||||
scope: CommandMenuItemScope.RecordSelection,
|
||||
isPinned: !contextStoreIsPageInEditMode && item.isPinned,
|
||||
getIcon,
|
||||
openFrontComponentInSidePanel,
|
||||
mountHeadlessFrontComponent,
|
||||
commandMenuContextApi,
|
||||
mountContext,
|
||||
}),
|
||||
)
|
||||
? recordScopedItems
|
||||
.map((item) =>
|
||||
buildCommandMenuItem({
|
||||
item,
|
||||
scope: CommandMenuItemScope.RecordSelection,
|
||||
isPinned: !contextStoreIsPageInEditMode && item.isPinned,
|
||||
}),
|
||||
)
|
||||
.filter(isDefined)
|
||||
: [];
|
||||
|
||||
const fallbackCommandMenuItems = fallbackItems.map((item) =>
|
||||
buildCommandMenuItemFromFrontComponent({
|
||||
item,
|
||||
type: CommandMenuItemType.Fallback,
|
||||
scope: CommandMenuItemScope.Global,
|
||||
isPinned: false,
|
||||
getIcon,
|
||||
openFrontComponentInSidePanel,
|
||||
mountHeadlessFrontComponent,
|
||||
commandMenuContextApi,
|
||||
}),
|
||||
);
|
||||
const fallbackCommandMenuItems = fallbackItems
|
||||
.map((item) =>
|
||||
buildCommandMenuItem({
|
||||
item,
|
||||
scope: CommandMenuItemScope.Global,
|
||||
isPinned: false,
|
||||
typeOverride: CommandMenuItemType.Fallback,
|
||||
}),
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
return [
|
||||
...globalCommandMenuItems,
|
||||
|
||||
+6
-2
@@ -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(
|
||||
|
||||
+6
-2
@@ -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(
|
||||
|
||||
+6
-4
@@ -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],
|
||||
);
|
||||
|
||||
+6
-2
@@ -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);
|
||||
|
||||
+21
-13
@@ -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,
|
||||
|
||||
+16
@@ -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>
|
||||
|
||||
+19
-1
@@ -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>
|
||||
}
|
||||
|
||||
+1
-9
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+3
-2
@@ -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>
|
||||
)}
|
||||
|
||||
+4
-4
@@ -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)}
|
||||
|
||||
+6
-3
@@ -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 };
|
||||
|
||||
+2
-2
@@ -14,11 +14,11 @@ import { captureException } from '@sentry/react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { print, type ExecutionResult } from 'graphql';
|
||||
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import { type EventSubscription } from '~/generated-metadata/graphql';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
export const useTriggerEventStreamCreation = () => {
|
||||
const store = useStore();
|
||||
@@ -79,7 +79,7 @@ export const useTriggerEventStreamCreation = () => {
|
||||
onEventSubscription: EventSubscription;
|
||||
}>,
|
||||
) => {
|
||||
if (isDefined(value?.errors)) {
|
||||
if (isDefined(value?.errors) && Array.isArray(value.errors)) {
|
||||
captureException(
|
||||
new Error(`SSE subscription error: ${value.errors[0]?.message}`),
|
||||
);
|
||||
|
||||
@@ -9,7 +9,10 @@ import { Logo } from '@/auth/components/Logo';
|
||||
import { SubTitle } from '@/auth/components/SubTitle';
|
||||
import { Title } from '@/auth/components/Title';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useFetchAndLoadIndexViews } from '@/metadata-store/hooks/useFetchAndLoadIndexViews';
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
|
||||
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
@@ -19,6 +22,7 @@ import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser';
|
||||
import { ApolloError } from '@apollo/client';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useStore } from 'jotai';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
@@ -68,6 +72,9 @@ export const CreateWorkspace = () => {
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const setNextOnboardingStatus = useSetNextOnboardingStatus();
|
||||
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
|
||||
const { updateDraft, applyChanges } = useMetadataStore();
|
||||
const { fetchAndLoadIndexViews } = useFetchAndLoadIndexViews();
|
||||
const store = useStore();
|
||||
|
||||
const { loadCurrentUser } = useLoadCurrentUser();
|
||||
const [activateWorkspace] = useActivateWorkspaceMutation();
|
||||
@@ -117,12 +124,18 @@ export const CreateWorkspace = () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (isDefined(result.errors)) {
|
||||
throw result.errors ?? new Error(t`Unknown error`);
|
||||
}
|
||||
|
||||
await refreshObjectMetadataItems();
|
||||
|
||||
const loadedObjects = store.get(objectMetadataItemsState.atom);
|
||||
updateDraft('objectMetadataItems', loadedObjects);
|
||||
applyChanges();
|
||||
|
||||
await fetchAndLoadIndexViews();
|
||||
|
||||
await loadCurrentUser();
|
||||
setNextOnboardingStatus();
|
||||
} catch (error: any) {
|
||||
@@ -138,6 +151,10 @@ export const CreateWorkspace = () => {
|
||||
enqueueErrorSnackBar,
|
||||
loadCurrentUser,
|
||||
refreshObjectMetadataItems,
|
||||
updateDraft,
|
||||
applyChanges,
|
||||
store,
|
||||
fetchAndLoadIndexViews,
|
||||
setNextOnboardingStatus,
|
||||
t,
|
||||
],
|
||||
|
||||
@@ -131,16 +131,12 @@ export const InviteTeam = () => {
|
||||
),
|
||||
);
|
||||
|
||||
if (emails.length === 0) {
|
||||
setNextOnboardingStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await sendInvitation({ emails });
|
||||
|
||||
if (isDefined(result.errors)) {
|
||||
throw result.errors;
|
||||
}
|
||||
|
||||
if (emails.length > 0) {
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Invite link sent to email addresses`,
|
||||
|
||||
+6
-1
@@ -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;
|
||||
}
|
||||
|
||||
+8
@@ -65,11 +65,14 @@ export class UploadFilesOrchestratorStep {
|
||||
return;
|
||||
}
|
||||
|
||||
step.status = 'in_progress';
|
||||
|
||||
this.state.addEvent({
|
||||
message: `Uploading ${builtPath}`,
|
||||
status: 'info',
|
||||
});
|
||||
this.state.updateEntityStatus(sourcePath, 'uploading');
|
||||
this.notify();
|
||||
|
||||
const uploadPromise = step.output.fileUploader
|
||||
.uploadFile({ builtPath, fileFolder })
|
||||
@@ -95,6 +98,11 @@ export class UploadFilesOrchestratorStep {
|
||||
})
|
||||
.finally(() => {
|
||||
step.output.activeUploads.delete(uploadPromise);
|
||||
|
||||
if (step.output.activeUploads.size === 0) {
|
||||
step.status = 'done';
|
||||
this.notify();
|
||||
}
|
||||
});
|
||||
|
||||
step.output.activeUploads.add(uploadPromise);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { type OrchestratorStateEvent } from '@/cli/utilities/dev/orchestrator/de
|
||||
import { DevUiApplicationPanel } from '@/cli/utilities/dev/ui/components/dev-ui-application-panel';
|
||||
import { DevUiEntityLegend } from '@/cli/utilities/dev/ui/components/dev-ui-entity-section';
|
||||
import { DevUiEventItem } from '@/cli/utilities/dev/ui/components/dev-ui-event-log';
|
||||
import { AnimationProvider } from '@/cli/utilities/dev/ui/dev-ui-animation-context';
|
||||
import { InkProvider, useInk } from '@/cli/utilities/dev/ui/dev-ui-ink-context';
|
||||
import { type DevUiStateManager } from '@/cli/utilities/dev/ui/dev-ui-state-manager';
|
||||
import React, { useReducer, useEffect } from 'react';
|
||||
@@ -45,7 +46,9 @@ export const renderDevUI = async (
|
||||
|
||||
const { unmount } = render(
|
||||
<InkProvider value={{ Box, Text, Static }}>
|
||||
<DevUI uiStateManager={uiStateManager} />
|
||||
<AnimationProvider>
|
||||
<DevUI uiStateManager={uiStateManager} />
|
||||
</AnimationProvider>
|
||||
</InkProvider>,
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
SPINNER_FRAMES,
|
||||
UPLOAD_FRAMES,
|
||||
} from '@/cli/utilities/dev/ui/dev-ui-constants';
|
||||
|
||||
type AnimationFrames = {
|
||||
spinnerFrame: string;
|
||||
uploadFrame: string;
|
||||
};
|
||||
|
||||
const AnimationContext = createContext<AnimationFrames>({
|
||||
spinnerFrame: SPINNER_FRAMES[0],
|
||||
uploadFrame: UPLOAD_FRAMES[0],
|
||||
});
|
||||
|
||||
const TICK_INTERVAL_MS = 120;
|
||||
const UPLOAD_TICK_RATIO = Math.round(200 / TICK_INTERVAL_MS);
|
||||
|
||||
export const AnimationProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): React.ReactElement => {
|
||||
const [tick, setTick] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setTick((previous) => previous + 1);
|
||||
}, TICK_INTERVAL_MS);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
const spinnerFrame = SPINNER_FRAMES[tick % SPINNER_FRAMES.length];
|
||||
const uploadFrame =
|
||||
UPLOAD_FRAMES[Math.floor(tick / UPLOAD_TICK_RATIO) % UPLOAD_FRAMES.length];
|
||||
|
||||
return (
|
||||
<AnimationContext.Provider value={{ spinnerFrame, uploadFrame }}>
|
||||
{children}
|
||||
</AnimationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAnimationFrames = (): AnimationFrames => {
|
||||
return useContext(AnimationContext);
|
||||
};
|
||||
@@ -1,44 +1,12 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import {
|
||||
type DevUiStatus,
|
||||
DEV_UI_STATUS_CONFIG,
|
||||
SPINNER_FRAMES,
|
||||
UPLOAD_FRAMES,
|
||||
} from '@/cli/utilities/dev/ui/dev-ui-constants';
|
||||
|
||||
export const useAnimatedFrame = (
|
||||
frames: string[],
|
||||
interval = 80,
|
||||
enabled = true,
|
||||
): string => {
|
||||
const [frameIndex, setFrameIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setFrameIndex((currentIndex) => (currentIndex + 1) % frames.length);
|
||||
}, interval);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [frames, interval, enabled]);
|
||||
|
||||
return frames[frameIndex];
|
||||
};
|
||||
import { useAnimationFrames } from '@/cli/utilities/dev/ui/dev-ui-animation-context';
|
||||
|
||||
export const useStatusIcon = (uiStatus: DevUiStatus): string => {
|
||||
const config = DEV_UI_STATUS_CONFIG[uiStatus];
|
||||
const spinnerFrame = useAnimatedFrame(
|
||||
SPINNER_FRAMES,
|
||||
80,
|
||||
config.icon === 'spinner',
|
||||
);
|
||||
const uploadFrame = useAnimatedFrame(
|
||||
UPLOAD_FRAMES,
|
||||
200,
|
||||
config.icon === 'upload',
|
||||
);
|
||||
const { spinnerFrame, uploadFrame } = useAnimationFrames();
|
||||
|
||||
if (config.icon === 'spinner') return spinnerFrame;
|
||||
if (config.icon === 'upload') return uploadFrame;
|
||||
|
||||
@@ -5,6 +5,7 @@ export type DevUiStateListener = (state: OrchestratorState) => void;
|
||||
export class DevUiStateManager {
|
||||
private orchestratorState: OrchestratorState;
|
||||
private listeners = new Set<DevUiStateListener>();
|
||||
private pendingNotify = false;
|
||||
|
||||
constructor(orchestratorState: OrchestratorState) {
|
||||
this.orchestratorState = orchestratorState;
|
||||
@@ -22,8 +23,18 @@ export class DevUiStateManager {
|
||||
}
|
||||
|
||||
notify(): void {
|
||||
for (const listener of this.listeners) {
|
||||
listener(this.orchestratorState);
|
||||
if (this.pendingNotify) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pendingNotify = true;
|
||||
|
||||
queueMicrotask(() => {
|
||||
this.pendingNotify = false;
|
||||
|
||||
for (const listener of this.listeners) {
|
||||
listener(this.orchestratorState);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user