Compare commits

..
Author SHA1 Message Date
Sonarly Claude Code 6a35ea3b61 fix: handle ResourceConflictException in LambdaDriver.build() for concurrent executions
https://sonarly.com/issue/16137?type=bug

The `LambdaDriver.build()` method has a TOCTOU race condition: it checks if an AWS Lambda function exists (GET → 404), then creates it (POST → 409 conflict), without handling the case where another concurrent worker creates the same function between the check and create.

Fix: **What changed:** Added `ResourceConflictException` handling to all three `CreateFunctionCommand` call sites in `LambdaDriver`:

1. **`build()`** (line 738) — the main logic function executor Lambda creation, which is the exact call site in the Sentry stack trace
2. **`ensureYarnInstallLambdaExists()`** (line 358) — the yarn-install helper Lambda creation
3. **`ensureBuilderLambdaExists()`** (line 454) — the esbuild transpiler helper Lambda creation

All three methods had the same TOCTOU race condition: check if function exists → get 404 → try to create → 409 because another concurrent worker already created it.

**Pattern:** The fix uses the exact same error-handling pattern already established in the file for `ResourceNotFoundException` — catch the specific AWS SDK exception, re-throw anything else. When `ResourceConflictException` is caught, execution continues normally because the function now exists (created by the other concurrent worker), and `waitFunctionActive` (called after `build()`) will wait for it to be ready.

**Import:** Added `ResourceConflictException` to the existing `@aws-sdk/client-lambda` import block, alphabetically next to the already-imported `ResourceNotFoundException`.
2026-03-18 22:08:28 +00:00
471 changed files with 5286 additions and 17075 deletions
+2 -2
View File
@@ -10,8 +10,8 @@ permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
group: ${{ github.workflow }}-${{ github.event_name == 'merge_group' && github.event.merge_group.base_ref || github.ref }}
cancel-in-progress: ${{ github.event_name != 'merge_group' }}
jobs:
e2e-test:
@@ -157,9 +157,7 @@ plugins: [
Run `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';` in database container to get access to admin panel.
#### When running workflow, workflow run fails with "Logic function execution is disabled. Set LOGIC_FUNCTION_TYPE to LOCAL or LAMBDA to enable."
In production, logic functions are disabled by default. Set the `LOGIC_FUNCTION_TYPE` environment variable to `LOCAL` or `LAMBDA` to enable them. This can be configured via environment variables or through the admin panel database variables. See the [Logic Functions setup guide](/developers/self-host/capabilities/setup#logic-functions-available-drivers) for details.
### 1-click Docker compose
@@ -21,51 +21,19 @@ description: أنشئ وأدِر تخصيصات Twenty على هيئة كود.
## المتطلبات الأساسية
* Node.js 24+ وYarn 4
* Docker (لخادم تطوير Twenty المحلي)
* مساحة عمل Twenty ومفتاح واجهة برمجة التطبيقات (أنشئ واحدًا على https://app.twenty.com/settings/api-webhooks)
## البدء
أنشئ تطبيقًا جديدًا باستخدام المولّد الرسمي. يمكنه بدء مثيل محلي من Twenty تلقائيًا لك:
أنشئ تطبيقًا جديدًا باستخدام المُهيئ الرسمي، ثم قم بالمصادقة وابدأ التطوير:
```bash filename="Terminal"
# إنشاء تطبيق جديد — ستعرض واجهة سطر الأوامر خيار بدء خادم Twenty محلي
# إنشاء تطبيق جديد (يتضمن جميع الأمثلة افتراضيًا)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# ابدأ وضع التطوير: يُزامن التغييرات المحلية تلقائيًا مع مساحة العمل الخاصة بك
yarn twenty dev
```
### إدارة الخادم المحلي
يتضمن SDK أوامر لإدارة خادم تطوير Twenty محلي (صورة Docker متكاملة تتضمن PostgreSQL وRedis والخادم والعامل):
```bash filename="Terminal"
# ابدأ الخادم المحلي (يسحب الصورة إذا لزم الأمر)
yarn twenty server start
# تحقّق من حالة الخادم
yarn twenty server status
# بثّ سجلات الخادم
yarn twenty server logs
# أوقف الخادم
yarn twenty server stop
# أعد ضبط جميع البيانات وابدأ من جديد
yarn twenty server reset
```
يأتي الخادم المحلي مهيأً مسبقًا بمساحة عمل ومستخدم (`tim@apple.dev` / `tim@apple.dev`)، بحيث يمكنك البدء في التطوير فورًا دون أي إعداد يدوي.
### المصادقة
وصّل تطبيقك بالخادم المحلي باستخدام OAuth:
```bash filename="Terminal"
# المصادقة عبر OAuth (يفتح المتصفح)
yarn twenty remote add --local
yarn twenty app:dev
```
يدعم المُنشئ وضعين للتحكم في ملفات الأمثلة التي سيتم تضمينها:
@@ -166,10 +166,6 @@ plugins: [
قم بتشغيل `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';` في حاوية قاعدة البيانات للحصول على الوصول إلى لوحة الإدارة.
#### عند تشغيل سير العمل، يفشل تشغيل سير العمل مع الرسالة "تم تعطيل تنفيذ دوال المنطق. عيّن LOGIC_FUNCTION_TYPE إلى LOCAL أو LAMBDA لتمكين ذلك."
في بيئة الإنتاج، يتم تعطيل دوال المنطق افتراضيًا. قم بتعيين متغير البيئة `LOGIC_FUNCTION_TYPE` إلى `LOCAL` أو `LAMBDA` لتمكينها. يمكن تكوين ذلك عبر متغيرات البيئة أو من خلال متغيرات قاعدة البيانات في لوحة الإدارة. راجع [دليل إعداد دوال المنطق](/l/ar/developers/self-host/capabilities/setup#logic-functions-available-drivers) للحصول على التفاصيل.
### Docker compose بنقرة واحدة
#### غير قادر على تسجيل الدخول
@@ -21,51 +21,19 @@ Mit Apps können Sie Twenty-Anpassungen **als Code** erstellen und verwalten. An
## Voraussetzungen
* Node.js 24+ und Yarn 4
* Docker (für den lokalen Twenty-Dev-Server)
* Ein Twenty-Workspace und ein API-Schlüssel (unter https://app.twenty.com/settings/api-webhooks erstellen)
## Erste Schritte
Erstelle eine neue App mit dem offiziellen Scaffolder. Der Scaffolder kann für dich automatisch eine lokale Twenty-Instanz starten:
Erstellen Sie mit dem offiziellen Scaffolder eine neue App, authentifizieren Sie sich und beginnen Sie mit der Entwicklung:
```bash filename="Terminal"
# Eine neue App erstellen — die CLI bietet an, einen lokalen Twenty-Server zu starten
# Eine neue App erstellen (enthält standardmäßig alle Beispiele)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Dev-Modus starten: synchronisiert lokale Änderungen automatisch mit deinem Arbeitsbereich
yarn twenty dev
```
### Lokale Serververwaltung
Das SDK enthält Befehle zur Verwaltung eines lokalen Twenty-Dev-Servers (All-in-One-Docker-Image mit PostgreSQL, Redis, Server und Worker):
```bash filename="Terminal"
# Den lokalen Server starten (lädt das Image bei Bedarf herunter)
yarn twenty server start
# Serverstatus prüfen
yarn twenty server status
# Serverprotokolle streamen
yarn twenty server logs
# Server stoppen
yarn twenty server stop
# Alle Daten zurücksetzen und neu starten
yarn twenty server reset
```
Der lokale Server ist bereits mit einem Arbeitsbereich und einem Benutzer (`tim@apple.dev` / `tim@apple.dev`) vorbefüllt, sodass Sie ohne manuelle Einrichtung sofort mit der Entwicklung beginnen können.
### Authentifizierung
Verbinden Sie Ihre App mithilfe von OAuth mit dem lokalen Server:
```bash filename="Terminal"
# Authenticate via OAuth (opens browser)
yarn twenty remote add --local
yarn twenty app:dev
```
Das Scaffolding-Tool unterstützt zwei Modi, um zu steuern, welche Beispieldateien enthalten sind:
@@ -166,10 +166,6 @@ plugins: [
Führen Sie den folgenden Befehl im Datenbankcontainer aus, um Zugriff auf das Admin-Panel zu erhalten: `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';`
#### Beim Ausführen eines Workflows schlägt die Workflow-Ausführung fehl mit "Logic function execution is disabled. Set LOGIC_FUNCTION_TYPE to LOCAL or LAMBDA to enable."
In der Produktion sind Logikfunktionen standardmäßig deaktiviert. Setzen Sie die Umgebungsvariable `LOGIC_FUNCTION_TYPE` auf `LOCAL` oder `LAMBDA`, um sie zu aktivieren. Dies kann über Umgebungsvariablen oder über die Datenbankvariablen des Admin-Panels konfiguriert werden. Details finden Sie im [Leitfaden zur Einrichtung von Logikfunktionen](/l/de/developers/self-host/capabilities/setup#logic-functions-available-drivers).
### 1-Klick Docker Compose
#### Kann mich nicht einloggen
@@ -21,51 +21,19 @@ Le app ti consentono di creare e gestire le personalizzazioni di Twenty **come c
## Prerequisiti
* Node.js 24+ e Yarn 4
* Docker (per il server di sviluppo locale di Twenty)
* Uno spazio di lavoro Twenty e una chiave API (creane una su https://app.twenty.com/settings/api-webhooks)
## Per iniziare
Crea una nuova app utilizzando lo scaffolder ufficiale. Può avviare automaticamente un'istanza locale di Twenty per te:
Crea una nuova app utilizzando lo scaffolder ufficiale, quindi autenticati e inizia a sviluppare:
```bash filename="Terminal"
# Crea lo scaffold di una nuova app — la CLI offrirà di avviare un server locale di Twenty
# Crea lo scaffold di una nuova app (include tutti gli esempi per impostazione predefinita)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Avvia la modalità di sviluppo: sincronizza automaticamente le modifiche locali con il tuo workspace
yarn twenty dev
```
### Gestione del server locale
L'SDK include comandi per gestire un server di sviluppo locale di Twenty (immagine Docker all-in-one con PostgreSQL, Redis, server e worker):
```bash filename="Terminal"
# Avvia il server locale (scarica l'immagine se necessario)
yarn twenty server start
# Verifica lo stato del server
yarn twenty server status
# Segui i log del server
yarn twenty server logs
# Arresta il server
yarn twenty server stop
# Reimposta tutti i dati e riparti da zero
yarn twenty server reset
```
Il server locale è preconfigurato con uno spazio di lavoro e un utente (`tim@apple.dev` / `tim@apple.dev`), così puoi iniziare a sviluppare immediatamente senza alcuna configurazione manuale.
### Autenticazione
Collega la tua app al server locale tramite OAuth:
```bash filename="Terminal"
# Autenticati tramite OAuth (apre il browser)
yarn twenty remote add --local
yarn twenty app:dev
```
Lo strumento di scaffolding supporta due modalità per controllare quali file di esempio vengono inclusi:
@@ -167,10 +167,6 @@ plugins: [
Esegui `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';` nel container del database per accedere al pannello di amministrazione.
#### Durante l'esecuzione di un workflow, l'esecuzione del workflow non riesce con "L'esecuzione delle funzioni logiche è disabilitata. Imposta LOGIC_FUNCTION_TYPE su LOCAL o LAMBDA per abilitarle."
In produzione, le funzioni logiche sono disabilitate per impostazione predefinita. Imposta la variabile d'ambiente `LOGIC_FUNCTION_TYPE` su `LOCAL` o `LAMBDA` per abilitarle. Questo può essere configurato tramite variabili d'ambiente o tramite le variabili del database del pannello di amministrazione. Consulta la [guida alla configurazione delle funzioni logiche](/l/it/developers/self-host/capabilities/setup#logic-functions-available-drivers) per dettagli.
### Composizione Docker a un clic
#### Impossibile connettersi
@@ -21,51 +21,19 @@ Os aplicativos permitem criar e gerenciar personalizações do Twenty **como có
## Pré-requisitos
* Node.js 24+ e Yarn 4
* Docker (para o servidor de desenvolvimento local do Twenty)
* Um espaço de trabalho do Twenty e uma chave de API (crie uma em https://app.twenty.com/settings/api-webhooks)
## Primeiros passos
Crie um novo app usando o gerador oficial de estrutura. Ele pode iniciar automaticamente uma instância local do Twenty para você:
Crie um novo aplicativo usando o gerador oficial, depois autentique-se e comece a desenvolver:
```bash filename="Terminal"
# Criar a estrutura de um novo app — a CLI oferecerá iniciar um servidor local do Twenty
# Criar a estrutura de um novo app (inclui todos os exemplos por padrão)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Iniciar modo de desenvolvimento: sincroniza automaticamente as alterações locais com seu workspace
yarn twenty dev
```
### Gerenciamento do Servidor Local
O SDK inclui comandos para gerenciar um servidor de desenvolvimento local do Twenty (imagem Docker all-in-one com PostgreSQL, Redis, servidor e worker):
```bash filename="Terminal"
# Iniciar o servidor local (faz pull da imagem se necessário)
yarn twenty server start
# Verificar o status do servidor
yarn twenty server status
# Transmitir os logs do servidor
yarn twenty server logs
# Parar o servidor
yarn twenty server stop
# Redefinir todos os dados e começar do zero
yarn twenty server reset
```
O servidor local já vem pré-configurado com um espaço de trabalho e um usuário (`tim@apple.dev` / `tim@apple.dev`), para que você possa começar a desenvolver imediatamente, sem qualquer configuração manual.
### Autenticação
Conecte seu aplicativo ao servidor local usando OAuth:
```bash filename="Terminal"
# Authenticate via OAuth (opens browser)
yarn twenty remote add --local
yarn twenty app:dev
```
O gerador de estrutura oferece suporte a dois modos para controlar quais arquivos de exemplo são incluídos:
@@ -166,10 +166,6 @@ plugins: [
Execute `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'você@seudominio.com';` no contêiner de banco de dados para obter acesso ao painel administrativo.
#### Ao executar um fluxo de trabalho, a execução do fluxo de trabalho falha com "A execução da função de lógica está desativada. Defina LOGIC_FUNCTION_TYPE como LOCAL ou LAMBDA para ativar."
Em produção, as funções de lógica estão desativadas por padrão. Defina a variável de ambiente `LOGIC_FUNCTION_TYPE` como `LOCAL` ou `LAMBDA` para ativá-las. Isso pode ser configurado por meio de variáveis de ambiente ou pelas variáveis de banco de dados do painel de administração. Veja o [guia de configuração de Funções de Lógica](/l/pt/developers/self-host/capabilities/setup#logic-functions-available-drivers) para obter detalhes.
### Docker compose com um clique
#### Impossível efetuar login
@@ -21,51 +21,19 @@ Aplicațiile vă permit să construiți și să gestionați personalizările Twe
## Cerințe
* Node.js 24+ și Yarn 4
* Docker (pentru serverul local de dezvoltare Twenty)
* Un spațiu de lucru Twenty și o cheie API (creați una la https://app.twenty.com/settings/api-webhooks)
## Începeți
Creează o aplicație nouă folosind generatorul oficial. Poate porni automat o instanță Twenty locală pentru tine:
Creați o aplicație nouă folosind generatorul oficial, apoi autentificați-vă și începeți să dezvoltați:
```bash filename="Terminal"
# Creează scheletul unei aplicații noi — CLI-ul îți va oferi opțiunea de a porni un server Twenty local
# Creează scheletul unei aplicații noi (include toate exemplele în mod implicit)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Pornește modul de dezvoltare: sincronizează automat modificările locale cu spațiul tău de lucru
yarn twenty dev
```
### Gestionarea serverului local
SDK-ul include comenzi pentru a gestiona un server local de dezvoltare Twenty (imagine Docker all-in-one cu PostgreSQL, Redis, server și worker):
```bash filename="Terminal"
# Pornește serverul local (descarcă imaginea dacă este necesar)
yarn twenty server start
# Verifică starea serverului
yarn twenty server status
# Afișează în timp real jurnalele serverului
yarn twenty server logs
# Oprește serverul
yarn twenty server stop
# Resetează toate datele și pornește de la zero
yarn twenty server reset
```
Serverul local vine preconfigurat cu un spațiu de lucru și un utilizator (`tim@apple.dev` / `tim@apple.dev`), astfel încât să poți începe să dezvolți imediat, fără nicio configurare manuală.
### Autentificare
Conectează-ți aplicația la serverul local folosind OAuth:
```bash filename="Terminal"
# Autentifică-te prin OAuth (se deschide browserul)
yarn twenty remote add --local
yarn twenty app:dev
```
Generatorul de schelet acceptă două moduri pentru a controla ce fișiere de exemplu sunt incluse:
@@ -167,10 +167,6 @@ plugins: [
Rulați `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'tine@domeniultău.com';` în containerul de baze de date pentru a obține acces la panoul de administrare.
#### Când rulați un flux de lucru, rularea fluxului de lucru eșuează cu "Execuția funcțiilor logice este dezactivată. Setați LOGIC_FUNCTION_TYPE la LOCAL sau LAMBDA pentru a le activa."
În producție, funcțiile logice sunt dezactivate în mod implicit. Setați variabila de mediu `LOGIC_FUNCTION_TYPE` la `LOCAL` sau `LAMBDA` pentru a le activa. Acest lucru poate fi configurat prin variabile de mediu sau prin variabilele bazei de date din panoul de administrare. Consultați [Ghidul de configurare a funcțiilor logice](/l/ro/developers/self-host/capabilities/setup#logic-functions-available-drivers) pentru detalii.
### Docker compose cu un singur click
#### Nu se poate conecta
@@ -21,51 +21,19 @@ description: Создавайте и управляйте настройками
## Требования
* Node.js 24+ и Yarn 4
* Docker (для локального сервера разработки Twenty)
* Рабочее пространство Twenty и ключ API (создайте его на https://app.twenty.com/settings/api-webhooks)
## Начало работы
Создайте новое приложение с помощью официального генератора каркаса. Он может автоматически запустить локальный экземпляр Twenty:
Создайте новое приложение с помощью официального генератора, затем выполните аутентификацию и начните разработку:
```bash filename="Terminal"
# Создать каркас нового приложения — CLI предложит запустить локальный сервер Twenty
# Создать каркас нового приложения (по умолчанию включает все примеры)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Запустить режим разработки: автоматически синхронизирует локальные изменения с вашим рабочим пространством
yarn twenty dev
```
### Управление локальным сервером
SDK включает команды для управления локальным сервером разработки Twenty (универсальный образ Docker с PostgreSQL, Redis, сервером и воркером):
```bash filename="Terminal"
# Запустить локальный сервер (при необходимости будет загружен образ)
yarn twenty server start
# Проверить статус сервера
yarn twenty server status
# Просмотр логов сервера в реальном времени
yarn twenty server logs
# Остановить сервер
yarn twenty server stop
# Сбросить все данные и начать с нуля
yarn twenty server reset
```
Локальный сервер уже содержит рабочее пространство и пользователя (`tim@apple.dev` / `tim@apple.dev`), так что вы можете сразу начать разработку без какой-либо ручной настройки.
### Аутентификация
Подключите своё приложение к локальному серверу с помощью OAuth:
```bash filename="Terminal"
# Authenticate via OAuth (opens browser)
yarn twenty remote add --local
yarn twenty app:dev
```
Генератор каркаса поддерживает два режима для управления тем, какие файлы-примеры включаются:
@@ -167,10 +167,6 @@ plugins: [
Выполните `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';` в контейнере базы данных, чтобы получить доступ к административной панели.
#### При запуске рабочего процесса выполнение завершается ошибкой: "Выполнение логической функции отключено. Установите LOGIC_FUNCTION_TYPE в значение LOCAL или LAMBDA, чтобы их включить.
В производственной среде логические функции по умолчанию отключены. Установите переменную окружения `LOGIC_FUNCTION_TYPE` в `LOCAL` или `LAMBDA`, чтобы их включить. Это можно настроить через переменные окружения или через переменные базы данных в панели администратора. См. [руководство по настройке логических функций](/l/ru/developers/self-host/capabilities/setup#logic-functions-available-drivers) для получения подробной информации.
### Docker Compose в один клик
#### Не удается войти в систему
@@ -21,51 +21,19 @@ Uygulamalar, Twenty özelleştirmelerini **kod olarak** oluşturup yönetmenizi
## Ön Gereksinimler
* Node.js 24+ ve Yarn 4
* Docker (yerel Twenty geliştirme sunucusu için)
* Bir Twenty çalışma alanı ve bir API anahtarı (https://app.twenty.com/settings/api-webhooks adresinde oluşturun)
## Başlarken
Resmi iskelet oluşturucusunu kullanarak yeni bir uygulama oluşturun. Sizin için otomatik olarak yerel bir Twenty örneğini başlatabilir:
Resmi scaffolder aracını kullanarak yeni bir uygulama oluşturun, ardından kimlik doğrulaması yapıp geliştirmeye başlayın:
```bash filename="Terminal"
# Yeni bir uygulamanın iskeletini oluşturun — CLI yerel bir Twenty sunucusunu başlatmayı önerecektir
# Yeni bir uygulamanın iskeletini oluşturun (varsayılan olarak tüm örnekleri içerir)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Geliştirme modunu başlatın: yerel değişiklikleri çalışma alanınızla otomatik olarak senkronize eder
yarn twenty dev
```
### Yerel Sunucu Yönetimi
SDK, yerel bir Twenty geliştirme sunucusunu yönetmek için komutlar içerir (PostgreSQL, Redis, sunucu ve worker içeren hepsi bir arada Docker imajı):
```bash filename="Terminal"
# Yerel sunucuyu başlatın (gerekirse imajı indirir)
yarn twenty server start
# Sunucu durumunu kontrol edin
yarn twenty server status
# Sunucu günlüklerini akış olarak görüntüleyin
yarn twenty server logs
# Sunucuyu durdurun
yarn twenty server stop
# Tüm verileri sıfırlayın ve temiz bir başlangıç yapın
yarn twenty server reset
```
Yerel sunucu, bir çalışma alanı ve kullanıcıyla (`tim@apple.dev` / `tim@apple.dev`) önceden yapılandırılmış olarak gelir; böylece herhangi bir manuel kurulum gerektirmeden hemen geliştirmeye başlayabilirsiniz.
### Kimlik Doğrulama
Uygulamanızı OAuth kullanarak yerel sunucuya bağlayın:
```bash filename="Terminal"
# Authenticate via OAuth (opens browser)
yarn twenty remote add --local
# Geliştirme modunu başlatın: yerel değişiklikleri otomatik olarak çalışma alanınızla senkronize eder
yarn twenty app:dev
```
İskelet oluşturucu, hangi örnek dosyaların dahil edileceğini kontrol etmek için iki modu destekler:
@@ -166,10 +166,6 @@ plugins: [
Veritabanı konteynerinde `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';` komutunu çalıştırarak yönetim paneline erişim sağlayın.
#### Bir iş akışı çalıştırılırken, iş akışının çalıştırılması şu hatayla başarısız oluyor: "Mantık işlevi yürütme devre dışı. Etkinleştirmek için LOGIC_FUNCTION_TYPE değerini LOCAL veya LAMBDA olarak ayarlayın."
Üretimde, mantık işlevleri varsayılan olarak devre dışıdır. Bunları etkinleştirmek için `LOGIC_FUNCTION_TYPE` ortam değişkenini `LOCAL` veya `LAMBDA` olarak ayarlayın. Bu, ortam değişkenleri aracılığıyla veya yönetici panelindeki veritabanı değişkenleri üzerinden yapılandırılabilir. Ayrıntılar için [Mantık İşlevleri kurulum kılavuzu](/l/tr/developers/self-host/capabilities/setup#logic-functions-available-drivers) bölümüne bakın.
### 1-tıklama ile Docker Compose
#### Giriş Yapılamıyor
@@ -21,51 +21,19 @@ description: 以代码的形式构建并管理 Twenty 自定义项。
## 先决条件
* Node.js 24+ 和 Yarn 4
* Docker (用于本地 Twenty 开发服务器)
* 一个 Twenty 工作空间和一个 API 密钥(在 https://app.twenty.com/settings/api-webhooks 创建)
## 开始使用
使用官方脚手架创建一个新应用。 它可以为你自动启动一个本地 Twenty 实例
使用官方脚手架创建一个新应用,然后进行身份验证并开始开发
```bash filename="Terminal"
# Scaffold a new app — the CLI will offer to start a local Twenty server
# 创建一个新应用脚手架(默认包含所有示例)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty dev
```
### 本地服务器管理
该 SDK 包含用于管理本地 Twenty 开发服务器的命令(该服务器是一体化 Docker 镜像,内含 PostgreSQL、Redis、服务器和工作进程):
```bash filename="Terminal"
# Start the local server (pulls the image if needed)
yarn twenty server start
# Check server status
yarn twenty server status
# Stream server logs
yarn twenty server logs
# Stop the server
yarn twenty server stop
# Reset all data and start fresh
yarn twenty server reset
```
本地服务器预置了一个工作区和用户 (`tim@apple.dev` / `tim@apple.dev`),因此你可以无需任何手动设置即可立即开始开发。
### 身份验证
使用 OAuth 将你的应用连接到本地服务器:
```bash filename="Terminal"
# 通过 OAuth 进行身份验证 (将打开浏览器)
yarn twenty remote add --local
# 启动开发模式:会将本地更改自动同步到你的工作区
yarn twenty app:dev
```
脚手架工具支持两种模式,用于控制包含哪些示例文件:
@@ -166,10 +166,6 @@ plugins: [
在数据库容器中运行 `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';` 以访问管理面板。
#### 在运行工作流时,工作流运行失败,并提示 "Logic function execution is disabled." 将 LOGIC_FUNCTION_TYPE 设置为 LOCAL 或 LAMBDA 以启用。
在生产环境中,逻辑函数默认处于禁用状态。 将 `LOGIC_FUNCTION_TYPE` 环境变量设置为 `LOCAL` 或 `LAMBDA` 以启用它们。 这可以通过环境变量或管理面板的数据库变量进行配置。 详情请参见[逻辑函数设置指南](/l/zh/developers/self-host/capabilities/setup#logic-functions-available-drivers)。
### 一键使用 Docker Compose
#### 无法登录
@@ -761,55 +761,6 @@ export type BooleanFieldComparison = {
isNot?: InputMaybe<Scalars['Boolean']>;
};
export type CalendarChannel = {
__typename?: 'CalendarChannel';
connectedAccountId: Scalars['UUID'];
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy;
createdAt: Scalars['DateTime'];
handle: Scalars['String'];
id: Scalars['UUID'];
isContactAutoCreationEnabled: Scalars['Boolean'];
isSyncEnabled: Scalars['Boolean'];
syncStage: CalendarChannelSyncStage;
syncStageStartedAt?: Maybe<Scalars['DateTime']>;
syncStatus: CalendarChannelSyncStatus;
syncedAt?: Maybe<Scalars['DateTime']>;
throttleFailureCount: Scalars['Float'];
updatedAt: Scalars['DateTime'];
visibility: CalendarChannelVisibility;
};
export enum CalendarChannelContactAutoCreationPolicy {
AS_ORGANIZER = 'AS_ORGANIZER',
AS_PARTICIPANT = 'AS_PARTICIPANT',
AS_PARTICIPANT_AND_ORGANIZER = 'AS_PARTICIPANT_AND_ORGANIZER',
NONE = 'NONE'
}
export enum CalendarChannelSyncStage {
CALENDAR_EVENTS_IMPORT_ONGOING = 'CALENDAR_EVENTS_IMPORT_ONGOING',
CALENDAR_EVENTS_IMPORT_PENDING = 'CALENDAR_EVENTS_IMPORT_PENDING',
CALENDAR_EVENTS_IMPORT_SCHEDULED = 'CALENDAR_EVENTS_IMPORT_SCHEDULED',
CALENDAR_EVENT_LIST_FETCH_ONGOING = 'CALENDAR_EVENT_LIST_FETCH_ONGOING',
CALENDAR_EVENT_LIST_FETCH_PENDING = 'CALENDAR_EVENT_LIST_FETCH_PENDING',
CALENDAR_EVENT_LIST_FETCH_SCHEDULED = 'CALENDAR_EVENT_LIST_FETCH_SCHEDULED',
FAILED = 'FAILED',
PENDING_CONFIGURATION = 'PENDING_CONFIGURATION'
}
export enum CalendarChannelSyncStatus {
ACTIVE = 'ACTIVE',
FAILED_INSUFFICIENT_PERMISSIONS = 'FAILED_INSUFFICIENT_PERMISSIONS',
FAILED_UNKNOWN = 'FAILED_UNKNOWN',
NOT_SYNCED = 'NOT_SYNCED',
ONGOING = 'ONGOING'
}
export enum CalendarChannelVisibility {
METADATA = 'METADATA',
SHARE_EVERYTHING = 'SHARE_EVERYTHING'
}
export type CalendarConfiguration = {
__typename?: 'CalendarConfiguration';
configurationType: WidgetConfigurationType;
@@ -976,21 +927,6 @@ export type ConfigVariablesGroupData = {
variables: Array<ConfigVariable>;
};
export type ConnectedAccountDto = {
__typename?: 'ConnectedAccountDTO';
authFailedAt?: Maybe<Scalars['DateTime']>;
createdAt: Scalars['DateTime'];
handle: Scalars['String'];
handleAliases?: Maybe<Array<Scalars['String']>>;
id: Scalars['UUID'];
lastCredentialsRefreshedAt?: Maybe<Scalars['DateTime']>;
lastSignedInAt?: Maybe<Scalars['DateTime']>;
provider: Scalars['String'];
scopes?: Maybe<Array<Scalars['String']>>;
updatedAt: Scalars['DateTime'];
userWorkspaceId: Scalars['UUID'];
};
export type ConnectedImapSmtpCaldavAccount = {
__typename?: 'ConnectedImapSmtpCaldavAccount';
accountOwnerId: Scalars['UUID'];
@@ -1682,10 +1618,8 @@ export enum FeatureFlagKey {
IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED',
IS_ATTACHMENT_MIGRATED = 'IS_ATTACHMENT_MIGRATED',
IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED',
IS_CONNECTED_ACCOUNT_MIGRATED = 'IS_CONNECTED_ACCOUNT_MIGRATED',
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED = 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED',
IS_DIRECT_GRAPHQL_EXECUTION_ENABLED = 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED',
IS_DRAFT_EMAIL_ENABLED = 'IS_DRAFT_EMAIL_ENABLED',
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
@@ -2355,96 +2289,6 @@ export type MarketplaceAppRoleObjectPermission = {
objectUniversalIdentifier: Scalars['String'];
};
export type MessageChannel = {
__typename?: 'MessageChannel';
connectedAccountId: Scalars['UUID'];
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy;
createdAt: Scalars['DateTime'];
excludeGroupEmails: Scalars['Boolean'];
excludeNonProfessionalEmails: Scalars['Boolean'];
handle: Scalars['String'];
id: Scalars['UUID'];
isContactAutoCreationEnabled: Scalars['Boolean'];
isSyncEnabled: Scalars['Boolean'];
messageFolderImportPolicy: MessageFolderImportPolicy;
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction;
syncStage: MessageChannelSyncStage;
syncStageStartedAt?: Maybe<Scalars['DateTime']>;
syncStatus: MessageChannelSyncStatus;
syncedAt?: Maybe<Scalars['DateTime']>;
throttleFailureCount: Scalars['Float'];
throttleRetryAfter?: Maybe<Scalars['DateTime']>;
type: MessageChannelType;
updatedAt: Scalars['DateTime'];
visibility: MessageChannelVisibility;
};
export enum MessageChannelContactAutoCreationPolicy {
NONE = 'NONE',
SENT = 'SENT',
SENT_AND_RECEIVED = 'SENT_AND_RECEIVED'
}
export enum MessageChannelPendingGroupEmailsAction {
GROUP_EMAILS_DELETION = 'GROUP_EMAILS_DELETION',
GROUP_EMAILS_IMPORT = 'GROUP_EMAILS_IMPORT',
NONE = 'NONE'
}
export enum MessageChannelSyncStage {
FAILED = 'FAILED',
MESSAGES_IMPORT_ONGOING = 'MESSAGES_IMPORT_ONGOING',
MESSAGES_IMPORT_PENDING = 'MESSAGES_IMPORT_PENDING',
MESSAGES_IMPORT_SCHEDULED = 'MESSAGES_IMPORT_SCHEDULED',
MESSAGE_LIST_FETCH_ONGOING = 'MESSAGE_LIST_FETCH_ONGOING',
MESSAGE_LIST_FETCH_PENDING = 'MESSAGE_LIST_FETCH_PENDING',
MESSAGE_LIST_FETCH_SCHEDULED = 'MESSAGE_LIST_FETCH_SCHEDULED',
PENDING_CONFIGURATION = 'PENDING_CONFIGURATION'
}
export enum MessageChannelSyncStatus {
ACTIVE = 'ACTIVE',
FAILED_INSUFFICIENT_PERMISSIONS = 'FAILED_INSUFFICIENT_PERMISSIONS',
FAILED_UNKNOWN = 'FAILED_UNKNOWN',
NOT_SYNCED = 'NOT_SYNCED',
ONGOING = 'ONGOING'
}
export enum MessageChannelType {
EMAIL = 'EMAIL',
SMS = 'SMS'
}
export enum MessageChannelVisibility {
METADATA = 'METADATA',
SHARE_EVERYTHING = 'SHARE_EVERYTHING',
SUBJECT = 'SUBJECT'
}
export type MessageFolder = {
__typename?: 'MessageFolder';
createdAt: Scalars['DateTime'];
externalId?: Maybe<Scalars['String']>;
id: Scalars['UUID'];
isSentFolder: Scalars['Boolean'];
isSynced: Scalars['Boolean'];
messageChannelId: Scalars['UUID'];
name?: Maybe<Scalars['String']>;
parentFolderId?: Maybe<Scalars['UUID']>;
pendingSyncAction: MessageFolderPendingSyncAction;
updatedAt: Scalars['DateTime'];
};
export enum MessageFolderImportPolicy {
ALL_FOLDERS = 'ALL_FOLDERS',
SELECTED_FOLDERS = 'SELECTED_FOLDERS'
}
export enum MessageFolderPendingSyncAction {
FOLDER_DELETION = 'FOLDER_DELETION',
NONE = 'NONE'
}
export type MetadataEvent = {
__typename?: 'MetadataEvent';
metadataName: Scalars['String'];
@@ -2554,7 +2398,6 @@ export type Mutation = {
deleteApplicationRegistrationVariable: Scalars['Boolean'];
deleteApprovedAccessDomain: Scalars['Boolean'];
deleteCommandMenuItem: CommandMenuItem;
deleteConnectedAccount: ConnectedAccountDto;
deleteCurrentWorkspace: Workspace;
deleteDatabaseConfigVariable: Scalars['Boolean'];
deleteEmailingDomain: Scalars['Boolean'];
@@ -2644,14 +2487,10 @@ export type Mutation = {
updateApiKey?: Maybe<ApiKey>;
updateApplicationRegistration: ApplicationRegistration;
updateApplicationRegistrationVariable: ApplicationRegistrationVariable;
updateCalendarChannel: CalendarChannel;
updateCommandMenuItem: CommandMenuItem;
updateDatabaseConfigVariable: Scalars['Boolean'];
updateFrontComponent: FrontComponent;
updateLabPublicFeatureFlag: FeatureFlag;
updateMessageChannel: MessageChannel;
updateMessageFolder: MessageFolder;
updateMessageFolders: Array<MessageFolder>;
updateNavigationMenuItem: NavigationMenuItem;
updateOneAgent: Agent;
updateOneApplicationVariable: Scalars['Boolean'];
@@ -2955,11 +2794,6 @@ export type MutationDeleteCommandMenuItemArgs = {
};
export type MutationDeleteConnectedAccountArgs = {
id: Scalars['UUID'];
};
export type MutationDeleteDatabaseConfigVariableArgs = {
key: Scalars['String'];
};
@@ -3383,11 +3217,6 @@ export type MutationUpdateApplicationRegistrationVariableArgs = {
};
export type MutationUpdateCalendarChannelArgs = {
input: UpdateCalendarChannelInput;
};
export type MutationUpdateCommandMenuItemArgs = {
input: UpdateCommandMenuItemInput;
};
@@ -3409,21 +3238,6 @@ export type MutationUpdateLabPublicFeatureFlagArgs = {
};
export type MutationUpdateMessageChannelArgs = {
input: UpdateMessageChannelInput;
};
export type MutationUpdateMessageFolderArgs = {
input: UpdateMessageFolderInput;
};
export type MutationUpdateMessageFoldersArgs = {
input: UpdateMessageFoldersInput;
};
export type MutationUpdateNavigationMenuItemArgs = {
input: UpdateOneNavigationMenuItemInput;
};
@@ -3873,6 +3687,21 @@ export type ObjectStandardOverrides = {
translations?: Maybe<Scalars['JSON']>;
};
export type OnDbEvent = {
__typename?: 'OnDbEvent';
action: DatabaseEventAction;
eventDate: Scalars['DateTime'];
objectNameSingular: Scalars['String'];
record: Scalars['JSON'];
updatedFields?: Maybe<Array<Scalars['String']>>;
};
export type OnDbEventInput = {
action?: InputMaybe<DatabaseEventAction>;
objectNameSingular?: InputMaybe<Scalars['String']>;
recordId?: InputMaybe<Scalars['UUID']>;
};
/** Onboarding status */
export enum OnboardingStatus {
BOOK_ONBOARDING = 'BOOK_ONBOARDING',
@@ -4132,7 +3961,6 @@ export type Query = {
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid;
commandMenuItem?: Maybe<CommandMenuItem>;
commandMenuItems: Array<CommandMenuItem>;
connectedAccounts: Array<ConnectedAccountDto>;
currentUser: User;
currentWorkspace: Workspace;
enterpriseCheckoutSession?: Maybe<Scalars['String']>;
@@ -4208,10 +4036,6 @@ export type Query = {
lineChartData: LineChartData;
listPlans: Array<BillingPlan>;
minimalMetadata: MinimalMetadata;
myCalendarChannels: Array<CalendarChannel>;
myConnectedAccounts: Array<ConnectedAccountDto>;
myMessageChannels: Array<MessageChannel>;
myMessageFolders: Array<MessageFolder>;
navigationMenuItem?: Maybe<NavigationMenuItem>;
navigationMenuItems: Array<NavigationMenuItem>;
object: Object;
@@ -4548,21 +4372,6 @@ export type QueryLineChartDataArgs = {
};
export type QueryMyCalendarChannelsArgs = {
connectedAccountId?: InputMaybe<Scalars['UUID']>;
};
export type QueryMyMessageChannelsArgs = {
connectedAccountId?: InputMaybe<Scalars['UUID']>;
};
export type QueryMyMessageFoldersArgs = {
messageChannelId?: InputMaybe<Scalars['UUID']>;
};
export type QueryNavigationMenuItemArgs = {
id: Scalars['UUID'];
};
@@ -4929,6 +4738,7 @@ export type StandardOverrides = {
export type Subscription = {
__typename?: 'Subscription';
logicFunctionLogs: LogicFunctionLogs;
onDbEvent: OnDbEvent;
onEventSubscription?: Maybe<EventSubscription>;
};
@@ -4938,6 +4748,11 @@ export type SubscriptionLogicFunctionLogsArgs = {
};
export type SubscriptionOnDbEventArgs = {
input: OnDbEventInput;
};
export type SubscriptionOnEventSubscriptionArgs = {
eventStreamId: Scalars['String'];
};
@@ -5077,18 +4892,6 @@ export type UpdateApplicationRegistrationVariablePayload = {
value?: InputMaybe<Scalars['String']>;
};
export type UpdateCalendarChannelInput = {
id: Scalars['UUID'];
update: UpdateCalendarChannelInputUpdates;
};
export type UpdateCalendarChannelInputUpdates = {
contactAutoCreationPolicy?: InputMaybe<CalendarChannelContactAutoCreationPolicy>;
isContactAutoCreationEnabled?: InputMaybe<Scalars['Boolean']>;
isSyncEnabled?: InputMaybe<Scalars['Boolean']>;
visibility?: InputMaybe<CalendarChannelVisibility>;
};
export type UpdateCommandMenuItemInput = {
availabilityObjectMetadataId?: InputMaybe<Scalars['UUID']>;
availabilityType?: InputMaybe<CommandMenuItemAvailabilityType>;
@@ -5158,35 +4961,6 @@ export type UpdateLogicFunctionFromSourceInputUpdates = {
toolInputSchema?: InputMaybe<Scalars['JSON']>;
};
export type UpdateMessageChannelInput = {
id: Scalars['UUID'];
update: UpdateMessageChannelInputUpdates;
};
export type UpdateMessageChannelInputUpdates = {
contactAutoCreationPolicy?: InputMaybe<MessageChannelContactAutoCreationPolicy>;
excludeGroupEmails?: InputMaybe<Scalars['Boolean']>;
excludeNonProfessionalEmails?: InputMaybe<Scalars['Boolean']>;
isContactAutoCreationEnabled?: InputMaybe<Scalars['Boolean']>;
isSyncEnabled?: InputMaybe<Scalars['Boolean']>;
messageFolderImportPolicy?: InputMaybe<MessageFolderImportPolicy>;
visibility?: InputMaybe<MessageChannelVisibility>;
};
export type UpdateMessageFolderInput = {
id: Scalars['UUID'];
update: UpdateMessageFolderInputUpdates;
};
export type UpdateMessageFolderInputUpdates = {
isSynced?: InputMaybe<Scalars['Boolean']>;
};
export type UpdateMessageFoldersInput = {
ids: Array<Scalars['UUID']>;
update: UpdateMessageFolderInputUpdates;
};
export type UpdateNavigationMenuItemInput = {
color?: InputMaybe<Scalars['String']>;
folderId?: InputMaybe<Scalars['UUID']>;
@@ -6824,13 +6598,6 @@ export type PieChartDataQueryVariables = Exact<{
export type PieChartDataQuery = { __typename?: 'Query', pieChartData: { __typename?: 'PieChartData', showLegend: boolean, showDataLabels: boolean, showCenterMetric: boolean, hasTooManyGroups: boolean, formattedToRawLookup: any, data: Array<{ __typename?: 'PieChartDataItem', id: string, value: number }> } };
export type DeleteConnectedAccountMutationVariables = Exact<{
id: Scalars['UUID'];
}>;
export type DeleteConnectedAccountMutation = { __typename?: 'Mutation', deleteConnectedAccount: { __typename?: 'ConnectedAccountDTO', id: string } };
export type SaveImapSmtpCaldavAccountMutationVariables = Exact<{
accountOwnerId: Scalars['UUID'];
handle: Scalars['String'];
@@ -6848,34 +6615,6 @@ export type StartChannelSyncMutationVariables = Exact<{
export type StartChannelSyncMutation = { __typename?: 'Mutation', startChannelSync: { __typename?: 'ChannelSyncSuccess', success: boolean } };
export type UpdateCalendarChannelMutationVariables = Exact<{
input: UpdateCalendarChannelInput;
}>;
export type UpdateCalendarChannelMutation = { __typename?: 'Mutation', updateCalendarChannel: { __typename?: 'CalendarChannel', id: string, visibility: CalendarChannelVisibility, isContactAutoCreationEnabled: boolean, contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy } };
export type UpdateMessageChannelMutationVariables = Exact<{
input: UpdateMessageChannelInput;
}>;
export type UpdateMessageChannelMutation = { __typename?: 'Mutation', updateMessageChannel: { __typename?: 'MessageChannel', id: string, visibility: MessageChannelVisibility, contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy, excludeNonProfessionalEmails: boolean, excludeGroupEmails: boolean, messageFolderImportPolicy: MessageFolderImportPolicy } };
export type UpdateMessageFolderMutationVariables = Exact<{
input: UpdateMessageFolderInput;
}>;
export type UpdateMessageFolderMutation = { __typename?: 'Mutation', updateMessageFolder: { __typename?: 'MessageFolder', id: string, isSynced: boolean } };
export type UpdateMessageFoldersMutationVariables = Exact<{
input: UpdateMessageFoldersInput;
}>;
export type UpdateMessageFoldersMutation = { __typename?: 'Mutation', updateMessageFolders: Array<{ __typename?: 'MessageFolder', id: string, isSynced: boolean }> };
export type GetConnectedImapSmtpCaldavAccountQueryVariables = Exact<{
id: Scalars['UUID'];
}>;
@@ -6883,32 +6622,6 @@ export type GetConnectedImapSmtpCaldavAccountQueryVariables = Exact<{
export type GetConnectedImapSmtpCaldavAccountQuery = { __typename?: 'Query', getConnectedImapSmtpCaldavAccount: { __typename?: 'ConnectedImapSmtpCaldavAccount', id: string, handle: string, provider: string, accountOwnerId: string, connectionParameters?: { __typename?: 'ImapSmtpCaldavConnectionParameters', IMAP?: { __typename?: 'ConnectionParametersOutput', host: string, port: number, secure?: boolean | null, username?: string | null, password: string } | null, SMTP?: { __typename?: 'ConnectionParametersOutput', host: string, username?: string | null, port: number, secure?: boolean | null, password: string } | null, CALDAV?: { __typename?: 'ConnectionParametersOutput', host: string, username?: string | null, password: string } | null } | null } };
export type MyCalendarChannelsQueryVariables = Exact<{
connectedAccountId?: InputMaybe<Scalars['UUID']>;
}>;
export type MyCalendarChannelsQuery = { __typename?: 'Query', myCalendarChannels: Array<{ __typename?: 'CalendarChannel', id: string, handle: string, visibility: CalendarChannelVisibility, syncStatus: CalendarChannelSyncStatus, syncStage: CalendarChannelSyncStage, syncStageStartedAt?: string | null, isContactAutoCreationEnabled: boolean, contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy, isSyncEnabled: boolean, connectedAccountId: string, createdAt: string, updatedAt: string }> };
export type MyConnectedAccountsQueryVariables = Exact<{ [key: string]: never; }>;
export type MyConnectedAccountsQuery = { __typename?: 'Query', myConnectedAccounts: Array<{ __typename?: 'ConnectedAccountDTO', id: string, handle: string, provider: string, authFailedAt?: string | null, scopes?: Array<string> | null, handleAliases?: Array<string> | null, lastSignedInAt?: string | null, userWorkspaceId: string, createdAt: string, updatedAt: string }> };
export type MyMessageChannelsQueryVariables = Exact<{
connectedAccountId?: InputMaybe<Scalars['UUID']>;
}>;
export type MyMessageChannelsQuery = { __typename?: 'Query', myMessageChannels: Array<{ __typename?: 'MessageChannel', id: string, handle: string, visibility: MessageChannelVisibility, type: MessageChannelType, isContactAutoCreationEnabled: boolean, contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy, messageFolderImportPolicy: MessageFolderImportPolicy, excludeNonProfessionalEmails: boolean, excludeGroupEmails: boolean, isSyncEnabled: boolean, syncStatus: MessageChannelSyncStatus, syncStage: MessageChannelSyncStage, syncStageStartedAt?: string | null, connectedAccountId: string, createdAt: string, updatedAt: string }> };
export type MyMessageFoldersQueryVariables = Exact<{
messageChannelId?: InputMaybe<Scalars['UUID']>;
}>;
export type MyMessageFoldersQuery = { __typename?: 'Query', myMessageFolders: Array<{ __typename?: 'MessageFolder', id: string, name?: string | null, isSynced: boolean, isSentFolder: boolean, parentFolderId?: string | null, externalId?: string | null, messageChannelId: string, createdAt: string, updatedAt: string }> };
export type SetAdminAiModelEnabledMutationVariables = Exact<{
modelId: Scalars['String'];
enabled: Scalars['Boolean'];
@@ -8010,18 +7723,9 @@ export const FindAllRecordPageLayoutsDocument = {"kind":"Document","definitions"
export const BarChartDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BarChartData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"BarChartDataInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"barChartData"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"indexBy"}},{"kind":"Field","name":{"kind":"Name","value":"keys"}},{"kind":"Field","name":{"kind":"Name","value":"series"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"label"}}]}},{"kind":"Field","name":{"kind":"Name","value":"xAxisLabel"}},{"kind":"Field","name":{"kind":"Name","value":"yAxisLabel"}},{"kind":"Field","name":{"kind":"Name","value":"showLegend"}},{"kind":"Field","name":{"kind":"Name","value":"showDataLabels"}},{"kind":"Field","name":{"kind":"Name","value":"layout"}},{"kind":"Field","name":{"kind":"Name","value":"groupMode"}},{"kind":"Field","name":{"kind":"Name","value":"hasTooManyGroups"}},{"kind":"Field","name":{"kind":"Name","value":"formattedToRawLookup"}}]}}]}}]} as unknown as DocumentNode<BarChartDataQuery, BarChartDataQueryVariables>;
export const LineChartDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"LineChartData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"LineChartDataInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lineChartData"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"series"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"x"}},{"kind":"Field","name":{"kind":"Name","value":"y"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"xAxisLabel"}},{"kind":"Field","name":{"kind":"Name","value":"yAxisLabel"}},{"kind":"Field","name":{"kind":"Name","value":"showLegend"}},{"kind":"Field","name":{"kind":"Name","value":"showDataLabels"}},{"kind":"Field","name":{"kind":"Name","value":"hasTooManyGroups"}},{"kind":"Field","name":{"kind":"Name","value":"formattedToRawLookup"}}]}}]}}]} as unknown as DocumentNode<LineChartDataQuery, LineChartDataQueryVariables>;
export const PieChartDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PieChartData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PieChartDataInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pieChartData"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showLegend"}},{"kind":"Field","name":{"kind":"Name","value":"showDataLabels"}},{"kind":"Field","name":{"kind":"Name","value":"showCenterMetric"}},{"kind":"Field","name":{"kind":"Name","value":"hasTooManyGroups"}},{"kind":"Field","name":{"kind":"Name","value":"formattedToRawLookup"}}]}}]}}]} as unknown as DocumentNode<PieChartDataQuery, PieChartDataQueryVariables>;
export const DeleteConnectedAccountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteConnectedAccount"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteConnectedAccount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<DeleteConnectedAccountMutation, DeleteConnectedAccountMutationVariables>;
export const SaveImapSmtpCaldavAccountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SaveImapSmtpCaldavAccount"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"accountOwnerId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"handle"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectionParameters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EmailAccountConnectionParameters"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"saveImapSmtpCaldavAccount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"accountOwnerId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"accountOwnerId"}}},{"kind":"Argument","name":{"kind":"Name","value":"handle"},"value":{"kind":"Variable","name":{"kind":"Name","value":"handle"}}},{"kind":"Argument","name":{"kind":"Name","value":"connectionParameters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectionParameters"}}},{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccountId"}}]}}]}}]} as unknown as DocumentNode<SaveImapSmtpCaldavAccountMutation, SaveImapSmtpCaldavAccountMutationVariables>;
export const StartChannelSyncDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"StartChannelSync"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"startChannelSync"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"connectedAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}}]}}]}}]} as unknown as DocumentNode<StartChannelSyncMutation, StartChannelSyncMutationVariables>;
export const UpdateCalendarChannelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateCalendarChannel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateCalendarChannelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateCalendarChannel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"isContactAutoCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}}]}}]}}]} as unknown as DocumentNode<UpdateCalendarChannelMutation, UpdateCalendarChannelMutationVariables>;
export const UpdateMessageChannelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageChannel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageChannelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageChannel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"excludeNonProfessionalEmails"}},{"kind":"Field","name":{"kind":"Name","value":"excludeGroupEmails"}},{"kind":"Field","name":{"kind":"Name","value":"messageFolderImportPolicy"}}]}}]}}]} as unknown as DocumentNode<UpdateMessageChannelMutation, UpdateMessageChannelMutationVariables>;
export const UpdateMessageFolderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageFolder"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageFolderInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageFolder"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}}]}}]}}]} as unknown as DocumentNode<UpdateMessageFolderMutation, UpdateMessageFolderMutationVariables>;
export const UpdateMessageFoldersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageFolders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageFoldersInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageFolders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}}]}}]}}]} as unknown as DocumentNode<UpdateMessageFoldersMutation, UpdateMessageFoldersMutationVariables>;
export const GetConnectedImapSmtpCaldavAccountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetConnectedImapSmtpCaldavAccount"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getConnectedImapSmtpCaldavAccount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"accountOwnerId"}},{"kind":"Field","name":{"kind":"Name","value":"connectionParameters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"IMAP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"secure"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}}]}},{"kind":"Field","name":{"kind":"Name","value":"SMTP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"secure"}},{"kind":"Field","name":{"kind":"Name","value":"password"}}]}},{"kind":"Field","name":{"kind":"Name","value":"CALDAV"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetConnectedImapSmtpCaldavAccountQuery, GetConnectedImapSmtpCaldavAccountQueryVariables>;
export const MyCalendarChannelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyCalendarChannels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myCalendarChannels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"connectedAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"syncStage"}},{"kind":"Field","name":{"kind":"Name","value":"syncStageStartedAt"}},{"kind":"Field","name":{"kind":"Name","value":"isContactAutoCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"isSyncEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccountId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<MyCalendarChannelsQuery, MyCalendarChannelsQueryVariables>;
export const MyConnectedAccountsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyConnectedAccounts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myConnectedAccounts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"authFailedAt"}},{"kind":"Field","name":{"kind":"Name","value":"scopes"}},{"kind":"Field","name":{"kind":"Name","value":"handleAliases"}},{"kind":"Field","name":{"kind":"Name","value":"lastSignedInAt"}},{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<MyConnectedAccountsQuery, MyConnectedAccountsQueryVariables>;
export const MyMessageChannelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyMessageChannels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myMessageChannels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"connectedAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"isContactAutoCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"messageFolderImportPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"excludeNonProfessionalEmails"}},{"kind":"Field","name":{"kind":"Name","value":"excludeGroupEmails"}},{"kind":"Field","name":{"kind":"Name","value":"isSyncEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"syncStage"}},{"kind":"Field","name":{"kind":"Name","value":"syncStageStartedAt"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccountId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<MyMessageChannelsQuery, MyMessageChannelsQueryVariables>;
export const MyMessageFoldersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyMessageFolders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"messageChannelId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myMessageFolders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"messageChannelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"messageChannelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}},{"kind":"Field","name":{"kind":"Name","value":"isSentFolder"}},{"kind":"Field","name":{"kind":"Name","value":"parentFolderId"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"messageChannelId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<MyMessageFoldersQuery, MyMessageFoldersQueryVariables>;
export const SetAdminAiModelEnabledDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetAdminAiModelEnabled"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"enabled"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setAdminAiModelEnabled"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"modelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}},{"kind":"Argument","name":{"kind":"Name","value":"enabled"},"value":{"kind":"Variable","name":{"kind":"Name","value":"enabled"}}}]}]}}]} as unknown as DocumentNode<SetAdminAiModelEnabledMutation, SetAdminAiModelEnabledMutationVariables>;
export const GetAdminAiModelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAdminAiModels"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAdminAiModels"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"autoEnableNewModels"}},{"kind":"Field","name":{"kind":"Name","value":"models"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"modelId"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"modelFamily"}},{"kind":"Field","name":{"kind":"Name","value":"inferenceProvider"}},{"kind":"Field","name":{"kind":"Name","value":"isAvailable"}},{"kind":"Field","name":{"kind":"Name","value":"isAdminEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"deprecated"}},{"kind":"Field","name":{"kind":"Name","value":"isRecommended"}}]}}]}}]}}]} as unknown as DocumentNode<GetAdminAiModelsQuery, GetAdminAiModelsQueryVariables>;
export const FindAllApplicationRegistrationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindAllApplicationRegistrations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findAllApplicationRegistrations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ApplicationRegistrationFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ApplicationRegistrationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ApplicationRegistration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"author"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthClientId"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthRedirectUris"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthScopes"}},{"kind":"Field","name":{"kind":"Name","value":"sourceType"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"latestAvailableVersion"}},{"kind":"Field","name":{"kind":"Name","value":"websiteUrl"}},{"kind":"Field","name":{"kind":"Name","value":"termsUrl"}},{"kind":"Field","name":{"kind":"Name","value":"isListed"}},{"kind":"Field","name":{"kind":"Name","value":"isFeatured"}},{"kind":"Field","name":{"kind":"Name","value":"ownerWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<FindAllApplicationRegistrationsQuery, FindAllApplicationRegistrationsQueryVariables>;
@@ -72,13 +72,11 @@ export const AgentChatFilePreview = ({
);
const rightComponent = onRemove ? (
<div onClick={(e) => e.stopPropagation()}>
<AvatarOrIcon
Icon={IconX}
IconColor={theme.font.color.secondary}
onClick={onRemove}
/>
</div>
<AvatarOrIcon
Icon={IconX}
IconColor={theme.font.color.secondary}
onClick={onRemove}
/>
) : undefined;
const hasRightDivider = isDefined(onRemove);
@@ -63,6 +63,7 @@ import { i18n } from '@lingui/core';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
import { isDefined } from 'twenty-shared/utils';
import { cookieStorage } from '~/utils/cookie-storage';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { useStore } from 'jotai';
@@ -178,6 +179,7 @@ export const useAuth = () => {
const handleSetAuthTokens = useCallback(
(tokens: AuthTokenPair) => {
setTokenPair(tokens);
cookieStorage.setItem('tokenPair', JSON.stringify(tokens));
},
[setTokenPair],
);
@@ -1,3 +1,8 @@
import { useMemo } from 'react';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
@@ -5,28 +10,68 @@ import { isDefined } from 'twenty-shared/utils';
import { useNavigationMenuItemsData } from './useNavigationMenuItemsData';
export const useWorkspaceNavigationMenuItems = (): {
objectMetadataIdsInWorkspaceNav: Set<string>;
workspaceNavigationMenuItemsObjectMetadataItems: ObjectMetadataItem[];
} => {
const { workspaceNavigationMenuItems: rawWorkspaceNavigationMenuItems } =
useNavigationMenuItemsData();
const views = useAtomStateValue(viewsSelector);
const workspaceNavViewIds = new Set(
rawWorkspaceNavigationMenuItems
const workspaceFolderIds = useMemo(
() =>
new Set(
rawWorkspaceNavigationMenuItems
.filter(isNavigationMenuItemFolder)
.map((item) => item.id),
),
[rawWorkspaceNavigationMenuItems],
);
const workspaceNavigationMenuItemsIncludingFolderItems = useMemo(
() =>
rawWorkspaceNavigationMenuItems.filter(
(item) =>
!isDefined(item.folderId) ||
(isDefined(item.folderId) && workspaceFolderIds.has(item.folderId)),
),
[rawWorkspaceNavigationMenuItems, workspaceFolderIds],
);
const workspaceNavigationMenuItemViewIds = new Set(
workspaceNavigationMenuItemsIncludingFolderItems
.map((item) => item.viewId)
.filter((viewId) => isDefined(viewId)),
);
const objectMetadataIdsInWorkspaceNav = new Set([
...views
.filter((view) => workspaceNavViewIds.has(view.id))
.map((view) => view.objectMetadataId),
...rawWorkspaceNavigationMenuItems
const navigationMenuItemViewObjectMetadataIds = new Set(
views.reduce<string[]>((acc, view) => {
if (workspaceNavigationMenuItemViewIds.has(view.id)) {
acc.push(view.objectMetadataId);
}
return acc;
}, []),
);
const navigationMenuItemRecordObjectMetadataIds = new Set(
workspaceNavigationMenuItemsIncludingFolderItems
.map((item) => item.targetObjectMetadataId)
.filter((objectMetadataId) => isDefined(objectMetadataId)),
);
const allNavigationMenuItemObjectMetadataIds = new Set([
...navigationMenuItemViewObjectMetadataIds,
...navigationMenuItemRecordObjectMetadataIds,
]);
const { activeNonSystemObjectMetadataItems } =
useFilteredObjectMetadataItems();
const activeNonSystemObjectMetadataItemsInWorkspaceNavigationMenuItems: ObjectMetadataItem[] =
activeNonSystemObjectMetadataItems.filter((item: ObjectMetadataItem) =>
allNavigationMenuItemObjectMetadataIds.has(item.id),
);
return {
objectMetadataIdsInWorkspaceNav,
workspaceNavigationMenuItemsObjectMetadataItems:
activeNonSystemObjectMetadataItemsInWorkspaceNavigationMenuItems,
};
};
@@ -0,0 +1,14 @@
import { normalizeUrl } from '@/navigation-menu-item/display/link/utils/normalizeUrl';
describe('normalizeUrl', () => {
it('should leave url unchanged when it has protocol, otherwise prepend https', () => {
expect(normalizeUrl('https://example.com')).toBe('https://example.com');
expect(normalizeUrl('example.com')).toBe('https://example.com');
expect(normalizeUrl(' example.com ')).toBe('https://example.com');
});
it('should return empty string for empty or whitespace input', () => {
expect(normalizeUrl('')).toBe('');
expect(normalizeUrl(' ')).toBe('');
});
});
@@ -0,0 +1,11 @@
export const normalizeUrl = (url: string) => {
const trimmedUrl = url.trim();
if (trimmedUrl === '') {
return trimmedUrl;
}
return trimmedUrl.startsWith('http://') || trimmedUrl.startsWith('https://')
? trimmedUrl
: `https://${trimmedUrl}`;
};
@@ -3,8 +3,15 @@ import { useParams } from 'react-router-dom';
import { useWorkspaceNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useWorkspaceNavigationMenuItems';
import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useLingui } from '@lingui/react/macro';
const WORKFLOW_OBJECTS_IN_SIDEBAR = [
CoreObjectNameSingular.Workflow,
CoreObjectNameSingular.WorkflowRun,
CoreObjectNameSingular.WorkflowVersion,
];
export const NavigationDrawerOpenedSection = () => {
const { t } = useLingui();
@@ -12,7 +19,8 @@ export const NavigationDrawerOpenedSection = () => {
const filteredActiveNonSystemObjectMetadataItems =
activeObjectMetadataItems.filter((item) => !item.isRemote);
const { objectMetadataIdsInWorkspaceNav } = useWorkspaceNavigationMenuItems();
const { workspaceNavigationMenuItemsObjectMetadataItems } =
useWorkspaceNavigationMenuItems();
const {
objectNamePlural: currentObjectNamePlural,
@@ -33,12 +41,18 @@ export const NavigationDrawerOpenedSection = () => {
return;
}
const isObjectAlreadyInNavbar = objectMetadataIdsInWorkspaceNav.has(
objectMetadataItem.id,
const isWorkflowObjectInSidebar = WORKFLOW_OBJECTS_IN_SIDEBAR.includes(
objectMetadataItem.nameSingular as CoreObjectNameSingular,
);
const shouldDisplayObjectInOpenedSection =
!isWorkflowObjectInSidebar &&
!workspaceNavigationMenuItemsObjectMetadataItems
.map((item) => item.id)
.includes(objectMetadataItem.id);
return (
!isObjectAlreadyInNavbar && (
shouldDisplayObjectInOpenedSection && (
<NavigationDrawerSectionForObjectMetadataItems
sectionTitle={t`Opened`}
objectMetadataItems={[objectMetadataItem]}
@@ -1,35 +0,0 @@
import { useCallback } from 'react';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { isDefined } from 'twenty-shared/utils';
import { useStore } from 'jotai';
export const useOptimisticRemoveNavigationMenuItemsByViewId = () => {
const store = useStore();
const { replaceDraft, applyChanges } = useMetadataStore();
const removeNavigationMenuItemsByViewIds = useCallback(
(viewIds: string[]) => {
const viewIdsSet = new Set(viewIds);
const entry = store.get(
metadataStoreState.atomFamily('navigationMenuItems'),
);
const currentNavigationMenuItems =
entry.current as unknown as NavigationMenuItem[];
const updatedNavigationMenuItems = currentNavigationMenuItems.filter(
(item) => !isDefined(item.viewId) || !viewIdsSet.has(item.viewId),
);
replaceDraft('navigationMenuItems', updatedNavigationMenuItems);
applyChanges();
},
[store, replaceDraft, applyChanges],
);
return {
removeNavigationMenuItemsByViewIds,
};
};
@@ -1,11 +1,12 @@
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined, normalizeUrl } from 'twenty-shared/utils';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorLink';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
import { computeInsertIndexAndPosition } from '@/navigation-menu-item/common/utils/computeInsertIndexAndPosition';
import { normalizeUrl } from '@/navigation-menu-item/display/link/utils/normalizeUrl';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
export const useAddLinkToNavigationMenuDraft = () => {
@@ -1,7 +1,7 @@
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react';
import { ensureAbsoluteUrl } from 'twenty-shared/utils';
import { getAbsoluteUrl } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { extractDomainFromUrl } from '@/navigation-menu-item/display/link/utils/extractDomainFromUrl';
@@ -45,7 +45,7 @@ export const SidePanelEditLinkItemView = ({
const currentName = selectedItem.name ?? defaultLabel;
const currentDomain = selectedItem.link
? extractDomainFromUrl(ensureAbsoluteUrl(selectedItem.link))
? extractDomainFromUrl(getAbsoluteUrl(selectedItem.link))
: undefined;
const canAutoUpdateName =
currentName === defaultLabel ||
@@ -57,7 +57,7 @@ export const SidePanelEditLinkItemView = ({
if (!canAutoUpdateName) return;
const trimmed = value.trim();
if (!isNonEmptyString(trimmed)) return;
const domain = extractDomainFromUrl(ensureAbsoluteUrl(trimmed));
const domain = extractDomainFromUrl(getAbsoluteUrl(trimmed));
if (domain !== undefined) {
setLastAutoSetName(domain);
onUpdateLink(selectedItem.id, { name: domain });
@@ -67,7 +67,7 @@ export const SidePanelEditLinkItemView = ({
const handleUrlBlur = (event: React.FocusEvent<HTMLInputElement>) => {
const value = event.target.value.trim();
if (isNonEmptyString(value)) {
onUpdateLink(selectedItem.id, { link: ensureAbsoluteUrl(value) });
onUpdateLink(selectedItem.id, { link: getAbsoluteUrl(value) });
setUrlEditInput('');
}
};
@@ -1,11 +1,12 @@
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { isDefined } from 'twenty-shared/utils';
export const useObjectNameSingularFromPlural = ({
objectNamePlural,
}: {
objectNamePlural: string;
}): { objectNameSingular: string | undefined } => {
}) => {
const objectMetadataItem = useAtomFamilySelectorValue(
objectMetadataItemFamilySelector,
{
@@ -14,5 +15,11 @@ export const useObjectNameSingularFromPlural = ({
},
);
return { objectNameSingular: objectMetadataItem?.nameSingular };
if (!isDefined(objectMetadataItem)) {
throw new Error(
`Object metadata item not found for ${objectNamePlural} object`,
);
}
return { objectNameSingular: objectMetadataItem.nameSingular };
};
@@ -11,7 +11,7 @@ import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecor
import { t } from '@lingui/core/macro';
import { useContext } from 'react';
import { FieldMetadataSettingsOnClickAction } from 'twenty-shared/types';
import { ensureAbsoluteUrl, isDefined } from 'twenty-shared/utils';
import { getAbsoluteUrl, isDefined } from 'twenty-shared/utils';
import { IconArrowUpRight, IconCopy } from 'twenty-ui/display';
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
@@ -69,7 +69,7 @@ export const useGetSecondaryRecordTableCellButton = () => {
if (isFieldLinks(fieldDefinition)) {
const url = (fieldValue as FieldLinksValue).primaryLinkUrl ?? '';
openLinkOnClick = () => {
window.open(ensureAbsoluteUrl(url), '_blank');
window.open(getAbsoluteUrl(url), '_blank');
};
copyOnClick = () => {
copyToClipboard(url, t`Link copied to clipboard`);
@@ -7,7 +7,6 @@ import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFi
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -42,8 +41,8 @@ export const RecordTitleCellSingleTextDisplayMode = ({
const recordStore = useAtomFamilyStateValue(recordStoreFamilyState, recordId);
const fieldValue = recordStore?.[fieldDefinition.metadata.fieldName];
const isEmpty = !isDefined(fieldValue) || fieldValue.trim() === '';
const isEmpty =
recordStore?.[fieldDefinition.metadata.fieldName]?.trim() === '';
const { openRecordTitleCell } = useRecordTitleCell();
@@ -519,94 +519,6 @@ describe('buildRecordFromImportedStructuredRow', () => {
});
});
it('should lowercase relation email composite subfield', () => {
const importedStructuredRow: ImportedStructuredRow = {
'emailField (relationField)': 'John.Doe@Example.COM',
};
const spreadsheetImportFields = [
{
fieldMetadataItemId: '6',
isNestedField: false,
isRelationConnectField: true,
label: 'Relation Field / Email Field',
key: 'emailField (relationField)',
fieldMetadataType: FieldMetadataType.RELATION,
uniqueFieldMetadataItem: {
name: 'emailField',
type: FieldMetadataType.EMAILS,
},
compositeSubFieldKey: 'primaryEmail',
},
] as SpreadsheetImportField[];
const result = buildRecordFromImportedStructuredRow({
importedStructuredRow,
fieldMetadataItems: fields,
spreadsheetImportFields,
});
expect(result).toEqual({
relationField: {
connect: {
where: {
emailField: {
primaryEmail: 'john.doe@example.com',
},
},
},
},
createdBy: {
source: 'IMPORT',
context: {},
},
});
});
it('should normalize relation links composite subfield', () => {
const importedStructuredRow: ImportedStructuredRow = {
'domainNameField (relationField)': 'HTTPS://Example.COM/path/',
};
const spreadsheetImportFields = [
{
fieldMetadataItemId: '6',
isNestedField: false,
isRelationConnectField: true,
label: 'Relation Field / Domain Name Field',
key: 'domainNameField (relationField)',
fieldMetadataType: FieldMetadataType.RELATION,
uniqueFieldMetadataItem: {
name: 'linksField',
type: FieldMetadataType.LINKS,
},
compositeSubFieldKey: 'primaryLinkUrl',
},
] as SpreadsheetImportField[];
const result = buildRecordFromImportedStructuredRow({
importedStructuredRow,
fieldMetadataItems: fields,
spreadsheetImportFields,
});
expect(result).toEqual({
relationField: {
connect: {
where: {
linksField: {
primaryLinkUrl: 'https://example.com/path',
},
},
},
},
createdBy: {
source: 'IMPORT',
context: {},
},
});
});
it('should return empty record for empty imported row', () => {
const importedStructuredRow: ImportedStructuredRow = {};
@@ -12,7 +12,7 @@ import {
assertUnreachable,
isDefined,
isEmptyObject,
normalizeUrlOrigin,
lowercaseUrlOriginAndRemoveTrailingSlash,
} from 'twenty-shared/utils';
import { z } from 'zod';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
@@ -185,7 +185,7 @@ export const buildRecordFromImportedStructuredRow = ({
},
[FieldMetadataType.LINKS]: {
primaryLinkLabel: castToString,
primaryLinkUrl: normalizeUrlOrigin,
primaryLinkUrl: lowercaseUrlOriginAndRemoveTrailingSlash,
secondaryLinks: linkArrayJSONSchema.parse,
},
@@ -202,7 +202,7 @@ export const buildRecordFromImportedStructuredRow = ({
},
[FieldMetadataType.EMAILS]: {
primaryEmail: (value: unknown) => castToString(value).toLowerCase(),
primaryEmail: castToString,
additionalEmails: stringArrayJSONSchema.parse,
},
[FieldMetadataType.FULL_NAME]: {
@@ -13,7 +13,7 @@ import { FieldMetadataType } from 'twenty-shared/types';
import {
getUniqueConstraintsFields,
isDefined,
normalizeUrlOrigin,
lowercaseUrlOriginAndRemoveTrailingSlash,
} from 'twenty-shared/utils';
type Column = {
@@ -104,7 +104,9 @@ const getUniqueValues = (
.primaryLinkUrl,
)
) {
return normalizeUrlOrigin(row?.[columnName]?.toString().trim() || '');
return lowercaseUrlOriginAndRemoveTrailingSlash(
row?.[columnName]?.toString().trim() || '',
);
}
return row?.[columnName]?.toString().trim().toLowerCase();
@@ -172,7 +172,6 @@ export const PageLayoutTabsRenderer = () => {
<PageLayoutTabList
tabs={sortedTabs}
behaveAsLinks={!isInSidePanel && !isPageLayoutInEditMode}
isInSidePanel={isInSidePanel}
componentInstanceId={tabListInstanceId}
onAddTab={handleAddTab}
isReorderEnabled={canEnableTabEditing}
@@ -104,9 +104,7 @@ export const getAxisLayerLayout = ({
-margins.left + axisConfig.leftAxisLegendOffsetPadding;
const valueRange = valueDomain.max - valueDomain.min;
const zeroIsWithinDomain =
valueDomain.min < 0 && valueDomain.max > 0 && valueRange !== 0;
const shouldRenderZeroLine = hasNegativeValues && zeroIsWithinDomain;
const shouldRenderZeroLine = hasNegativeValues && valueRange !== 0;
const zeroPosition = shouldRenderZeroLine
? isVertical
? innerHeight - ((0 - valueDomain.min) / valueRange) * innerHeight
@@ -36,10 +36,6 @@ export const renderGridLayer = ({
for (const tickValue of valueTickValues) {
const normalizedPosition = (tickValue - valueDomain.min) / range;
if (normalizedPosition <= 0 || normalizedPosition >= 1) {
continue;
}
ctx.beginPath();
if (isVertical) {
const y = innerHeight * (1 - normalizedPosition);
@@ -1,5 +1,4 @@
export const TEXT_MARGIN_EXTRAS = {
tickPaddingExtra: 4,
bottomTickExtraNonRotated: 20,
rightTickExtra: 16,
} as const;
@@ -1,4 +1,4 @@
export const TEXT_MARGIN_LIMITS = {
min: { top: 10, right: 10, bottom: 30, left: 40 },
max: { top: 30, right: 160, bottom: 140, left: 160 },
max: { top: 30, right: 30, bottom: 140, left: 160 },
} as const;
@@ -1,7 +1,7 @@
import { BarChartLayers } from '@/page-layout/widgets/graph/graph-widget-bar-chart/components/BarChartLayers';
import { useBarChartLayout } from '@/page-layout/widgets/graph/graph-widget-bar-chart/hooks/useBarChartLayout';
import { useBarChartTheme } from '@/page-layout/widgets/graph/graph-widget-bar-chart/hooks/useBarChartTheme';
import { useMemoizedBarPositions } from '@/page-layout/widgets/graph/graph-widget-bar-chart/hooks/useMemoizedBarPositions';
import { useBarChartLayout } from '@/page-layout/widgets/graph/graph-widget-bar-chart/hooks/useBarChartLayout';
import { type BarChartDatum } from '@/page-layout/widgets/graph/graph-widget-bar-chart/types/BarChartDatum';
import { type BarChartEnrichedKey } from '@/page-layout/widgets/graph/graph-widget-bar-chart/types/BarChartEnrichedKey';
import { type BarChartSlice } from '@/page-layout/widgets/graph/graph-widget-bar-chart/types/BarChartSlice';
@@ -30,14 +30,12 @@ type BarChartProps = {
layout: BarChartLayout;
groupMode: 'grouped' | 'stacked';
effectiveValueRange: { minimum: number; maximum: number };
hasExplicitRangeBounds: boolean;
formatOptions: GraphValueFormatOptions;
axisConfig?: {
xAxisLabel?: string;
yAxisLabel?: string;
showGrid?: boolean;
};
rightTickLabels?: string[];
dataLabelsConfig?: {
show: boolean;
omitNullValues: boolean;
@@ -67,10 +65,8 @@ export const BarChart = ({
layout,
groupMode,
effectiveValueRange,
hasExplicitRangeBounds,
formatOptions,
axisConfig,
rightTickLabels,
dataLabelsConfig,
hoveredSliceIndexValue,
onSliceHover,
@@ -106,13 +102,11 @@ export const BarChart = ({
chartWidth,
data,
effectiveValueRange,
hasExplicitRangeBounds,
formatOptions,
groupMode,
indexBy,
keys,
layout,
rightTickLabels,
});
const isVerticalLayout = layout === BarChartLayout.VERTICAL;
@@ -1,3 +1,4 @@
import { isSidePanelAnimatingState } from '@/side-panel/states/isSidePanelAnimatingState';
import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState';
import { pageLayoutResizingWidgetIdComponentState } from '@/page-layout/states/pageLayoutResizingWidgetIdComponentState';
import { GraphWidgetChartContainer } from '@/page-layout/widgets/graph/components/GraphWidgetChartContainer';
@@ -16,17 +17,12 @@ import { calculateValueRangeFromBarChartKeys } from '@/page-layout/widgets/graph
import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode';
import { computeEffectiveValueRange } from '@/page-layout/widgets/graph/utils/computeEffectiveValueRange';
import { createGraphColorRegistry } from '@/page-layout/widgets/graph/utils/createGraphColorRegistry';
import {
formatGraphValue,
type GraphValueFormatOptions,
} from '@/page-layout/widgets/graph/utils/graphFormatters';
import { isSidePanelAnimatingState } from '@/side-panel/states/isSidePanelAnimatingState';
import { type GraphValueFormatOptions } from '@/page-layout/widgets/graph/utils/graphFormatters';
import { NodeDimensionEffect } from '@/ui/utilities/dimensions/components/NodeDimensionEffect';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { styled } from '@linaria/react';
import { isNumber } from '@sniptt/guards';
import { useContext, useMemo, useRef, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { ThemeContext } from 'twenty-ui/theme-constants';
@@ -124,16 +120,13 @@ export const GraphWidgetBarChart = ({
const allowDataTransitions = !isLayoutAnimating;
const formatOptions = useMemo<GraphValueFormatOptions>(
() => ({
customFormatter,
decimals,
displayType,
prefix,
suffix,
}),
[customFormatter, decimals, displayType, prefix, suffix],
);
const formatOptions: GraphValueFormatOptions = {
customFormatter,
decimals,
displayType,
prefix,
suffix,
};
const { enrichedKeysMap, enrichedKeys, legendItems, visibleKeys } =
useBarChartData({ keys, series, colorRegistry, seriesLabels, colorMode });
@@ -158,36 +151,6 @@ export const GraphWidgetBarChart = ({
rangeMin,
});
const hasExplicitRangeBounds = isDefined(rangeMin) || isDefined(rangeMax);
const rightTickLabels = useMemo(() => {
if (
!hasExplicitRangeBounds ||
!showValues ||
layout !== BarChartLayout.HORIZONTAL
) {
return undefined;
}
const labels: string[] = [];
for (const dataItem of data) {
for (const visibleKey of visibleKeys) {
const value = dataItem[visibleKey];
if (isNumber(value)) {
labels.push(formatGraphValue(value, formatOptions));
}
}
}
return labels;
}, [
data,
visibleKeys,
formatOptions,
hasExplicitRangeBounds,
showValues,
layout,
]);
const dataByIndexValue = useMemo(
() => new Map(data.map((row) => [String(row[indexBy]), row])),
[data, indexBy],
@@ -259,10 +222,8 @@ export const GraphWidgetBarChart = ({
maximum: effectiveMaximumValue,
minimum: effectiveMinimumValue,
}}
hasExplicitRangeBounds={hasExplicitRangeBounds}
enrichedKeysMap={enrichedKeysMap}
formatOptions={formatOptions}
rightTickLabels={rightTickLabels}
groupMode={groupMode}
hasNoData={hasNoData}
hoveredSliceIndexValue={graphWidgetHoveredSliceIndex}
@@ -21,13 +21,11 @@ type UseBarChartLayoutParams = {
chartWidth: number;
data: BarChartDatum[];
effectiveValueRange: { minimum: number; maximum: number };
hasExplicitRangeBounds: boolean;
formatOptions: GraphValueFormatOptions;
groupMode: 'grouped' | 'stacked';
indexBy: string;
keys: string[];
layout: BarChartLayout;
rightTickLabels?: string[];
};
type UseBarChartLayoutResult = {
@@ -54,13 +52,11 @@ export const useBarChartLayout = ({
chartWidth,
data,
effectiveValueRange,
hasExplicitRangeBounds,
formatOptions,
groupMode,
indexBy,
keys,
layout,
rightTickLabels,
}: UseBarChartLayoutParams): UseBarChartLayoutResult => {
const { tickFontSize, legendFontSize } = resolveAxisFontSizes(axisTheme);
@@ -77,11 +73,9 @@ export const useBarChartLayout = ({
data,
effectiveMaximumValue: effectiveValueRange.maximum,
effectiveMinimumValue: effectiveValueRange.minimum,
hasExplicitRangeBounds,
formatOptions,
indexBy,
layout,
rightTickLabels,
xAxisLabel: axisConfig?.xAxisLabel,
yAxisLabel: axisConfig?.yAxisLabel,
});
@@ -142,28 +142,4 @@ describe('computeBarPositions', () => {
expect(bar2).toBeDefined();
expect(bar1!.y).toBeGreaterThan(bar2!.y);
});
it('clamps horizontal bars to value axis when value is above explicit max', () => {
const chartWidth = 300;
const result = computeBarPositions({
data: [{ category: 'A', value1: 15 }],
indexBy: 'category',
keys: ['value1'],
enrichedKeysMap: defaultEnrichedKeysMap,
chartWidth,
chartHeight: 300,
margins: defaultMargins,
layout: BarChartLayout.HORIZONTAL,
groupMode: 'grouped',
valueDomain: { min: 0, max: 10 },
innerPadding: 2,
});
expect(result).toHaveLength(1);
expect(result[0].x).toBe(0);
expect(result[0].width).toBe(
chartWidth - defaultMargins.left - defaultMargins.right,
);
});
});
@@ -124,23 +124,6 @@ describe('getBarChartLayout', () => {
TEXT_MARGIN_LIMITS.max.left,
);
});
it('keeps explicit range bounds as value domain', () => {
const result = getBarChartLayout({
...baseParams,
chartWidth: 280,
layout: BarChartLayout.HORIZONTAL,
effectiveMinimumValue: 0,
effectiveMaximumValue: 10,
hasExplicitRangeBounds: true,
});
expect(result.valueDomain).toEqual({ min: 0, max: 10 });
expect(result.valueTickValues[0]).toBe(0);
expect(result.valueTickValues[result.valueTickValues.length - 1]).toBe(
10,
);
});
});
describe('edge cases', () => {
@@ -59,44 +59,4 @@ describe('getGroupedBarDimensions', () => {
height: 19,
});
});
it('clamps vertical bar when value exceeds axis length', () => {
const ctx = createContext({ isVertical: true });
const layout = computeGroupedBarLayout(ctx, 2);
const dimensions = getGroupedBarDimensions({
ctx,
layout,
categoryStart: 10,
keyIndex: 1,
value: 150,
});
expect(dimensions).toEqual({
x: 31,
y: 0,
width: 19,
height: 100,
});
});
it('clamps horizontal bar when value exceeds axis length', () => {
const ctx = createContext({ isVertical: false });
const layout = computeGroupedBarLayout(ctx, 2);
const dimensions = getGroupedBarDimensions({
ctx,
layout,
categoryStart: 10,
keyIndex: 0,
value: 150,
});
expect(dimensions).toEqual({
x: 0,
y: 10,
width: 100,
height: 19,
});
});
});
@@ -114,92 +114,4 @@ describe('getStackedBarDimensions', () => {
newNegativeStackPixel: 30,
});
});
it('clamps vertical positive bar when stack exceeds axis length', () => {
const ctx = createContext({ isVertical: true });
const dimensions = getStackedBarDimensions({
ctx,
layout,
categoryStart: 20,
value: 40,
positiveStackPixel: 80,
negativeStackPixel: 50,
});
expect(dimensions).toEqual({
x: 22,
y: 0,
width: 10,
height: 20,
newPositiveStackPixel: 120,
newNegativeStackPixel: 50,
});
});
it('clamps vertical negative bar when stack goes below zero', () => {
const ctx = createContext({ isVertical: true });
const dimensions = getStackedBarDimensions({
ctx,
layout,
categoryStart: 20,
value: -40,
positiveStackPixel: 50,
negativeStackPixel: 20,
});
expect(dimensions).toEqual({
x: 22,
y: 80,
width: 10,
height: 20,
newPositiveStackPixel: 50,
newNegativeStackPixel: -20,
});
});
it('clamps horizontal positive bar when stack exceeds axis length', () => {
const ctx = createContext({ isVertical: false });
const dimensions = getStackedBarDimensions({
ctx,
layout,
categoryStart: 20,
value: 40,
positiveStackPixel: 80,
negativeStackPixel: 50,
});
expect(dimensions).toEqual({
x: 80,
y: 22,
width: 20,
height: 10,
newPositiveStackPixel: 120,
newNegativeStackPixel: 50,
});
});
it('clamps horizontal negative bar when stack goes below zero', () => {
const ctx = createContext({ isVertical: false });
const dimensions = getStackedBarDimensions({
ctx,
layout,
categoryStart: 20,
value: -40,
positiveStackPixel: 50,
negativeStackPixel: 20,
});
expect(dimensions).toEqual({
x: 0,
y: 22,
width: 20,
height: 10,
newPositiveStackPixel: 50,
newNegativeStackPixel: -20,
});
});
});
@@ -31,8 +31,6 @@ type GetBarChartLayoutParams = {
formatOptions: GraphValueFormatOptions;
effectiveMinimumValue: number;
effectiveMaximumValue: number;
hasExplicitRangeBounds?: boolean;
rightTickLabels?: string[];
};
type BarChartLayoutResult = {
@@ -129,8 +127,6 @@ export const getBarChartLayout = ({
formatOptions,
effectiveMinimumValue,
effectiveMaximumValue,
hasExplicitRangeBounds = false,
rightTickLabels = [],
}: GetBarChartLayoutParams): BarChartLayoutResult => {
const { tickFontSize, legendFontSize } = resolveAxisFontSizes(axisTheme);
@@ -156,7 +152,6 @@ export const getBarChartLayout = ({
minimum: effectiveMinimumValue,
maximum: effectiveMaximumValue,
tickCount: currentTickConfiguration.numberOfValueTicks,
preserveDomainBounds: hasExplicitRangeBounds,
}),
getTickRotation: (currentTickConfiguration) =>
currentTickConfiguration.bottomAxisTickRotation,
@@ -166,19 +161,13 @@ export const getBarChartLayout = ({
tickFontSize,
tickRotation: parameters.tickConfiguration.bottomAxisTickRotation,
}),
resolveMarginInputs: (currentTickConfiguration, tickResult) => {
const marginInputs = resolveMarginInputs({
resolveMarginInputs: (currentTickConfiguration, tickResult) =>
resolveMarginInputs({
tickConfiguration: currentTickConfiguration,
tickResult,
layout,
formatOptions,
});
return {
...marginInputs,
rightTickLabels,
};
},
}),
});
const { tickValues: valueTickValues, domain: valueDomain } = valueTickResult;
@@ -52,13 +52,9 @@ export const getGroupedBarDimensions = ({
const { isVertical, valueAxisLength, valueToPixel, zeroPixel } = ctx;
const { barThickness, groupCenteringOffset, barStride } = layout;
const valuePixel = Math.max(
0,
Math.min(valueAxisLength, valueToPixel(value)),
);
const clampedZeroPixel = Math.max(0, Math.min(valueAxisLength, zeroPixel));
const barStart = Math.min(clampedZeroPixel, valuePixel);
const barLength = Math.abs(valuePixel - clampedZeroPixel);
const valuePixel = valueToPixel(value);
const barStart = Math.min(zeroPixel, valuePixel);
const barLength = Math.abs(valuePixel - zeroPixel);
const categoryPosition =
categoryStart + groupCenteringOffset + keyIndex * barStride;
@@ -74,64 +74,45 @@ export const getStackedBarDimensions = ({
const valuePixelDelta =
stackRange === 0 ? 0 : stackValueToPixel(Math.abs(value));
const clampToAxis = (pixel: number) =>
Math.max(0, Math.min(valueAxisLength, pixel));
if (isVertical && isNegative) {
const newStack = negativeStackPixel - valuePixelDelta;
const clampedCurrent = clampToAxis(negativeStackPixel);
const clampedNew = clampToAxis(newStack);
return {
x: categoryPosition,
y: valueAxisLength - clampedCurrent,
y: valueAxisLength - negativeStackPixel,
width: barThickness,
height: clampedCurrent - clampedNew,
height: valuePixelDelta,
newPositiveStackPixel: positiveStackPixel,
newNegativeStackPixel: newStack,
newNegativeStackPixel: negativeStackPixel - valuePixelDelta,
};
}
if (isVertical) {
const newStack = positiveStackPixel + valuePixelDelta;
const clampedCurrent = clampToAxis(positiveStackPixel);
const clampedNew = clampToAxis(newStack);
return {
x: categoryPosition,
y: valueAxisLength - clampedNew,
y: valueAxisLength - (positiveStackPixel + valuePixelDelta),
width: barThickness,
height: clampedNew - clampedCurrent,
newPositiveStackPixel: newStack,
height: valuePixelDelta,
newPositiveStackPixel: positiveStackPixel + valuePixelDelta,
newNegativeStackPixel: negativeStackPixel,
};
}
if (isNegative) {
const newStack = negativeStackPixel - valuePixelDelta;
const clampedCurrent = clampToAxis(negativeStackPixel);
const clampedNew = clampToAxis(newStack);
return {
x: clampedNew,
x: negativeStackPixel - valuePixelDelta,
y: categoryPosition,
width: clampedCurrent - clampedNew,
width: valuePixelDelta,
height: barThickness,
newPositiveStackPixel: positiveStackPixel,
newNegativeStackPixel: newStack,
newNegativeStackPixel: negativeStackPixel - valuePixelDelta,
};
}
const newStack = positiveStackPixel + valuePixelDelta;
const clampedCurrent = clampToAxis(positiveStackPixel);
const clampedNew = clampToAxis(newStack);
return {
x: clampedCurrent,
x: positiveStackPixel,
y: categoryPosition,
width: clampedNew - clampedCurrent,
width: valuePixelDelta,
height: barThickness,
newPositiveStackPixel: newStack,
newPositiveStackPixel: positiveStackPixel + valuePixelDelta,
newNegativeStackPixel: negativeStackPixel,
};
};
@@ -46,30 +46,4 @@ describe('computeValueTickValues', () => {
expect(result.domain.min).toBeLessThanOrEqual(-100);
expect(result.domain.max).toBeGreaterThanOrEqual(-10);
});
it('should preserve explicit domain bounds when requested', () => {
const result = computeValueTickValues({
minimum: 0,
maximum: 10,
tickCount: 2,
preserveDomainBounds: true,
});
expect(result.domain).toEqual({ min: 0, max: 10 });
expect(result.tickValues[0]).toBe(0);
expect(result.tickValues[result.tickValues.length - 1]).toBe(10);
expect(result.tickValues.every((tick) => tick >= 0 && tick <= 10)).toBe(
true,
);
});
it('should still allow domain expansion when preserving bounds is disabled', () => {
const result = computeValueTickValues({
minimum: 0,
maximum: 10,
tickCount: 2,
});
expect(result.domain.max).toBeGreaterThan(10);
});
});
@@ -24,42 +24,6 @@ describe('getChartMarginsFromText', () => {
expect(result.right).toBe(18);
});
it('clamps right margin when rightTickLabels are very long', () => {
const longLabel = 'X'.repeat(200);
const result = getChartMarginsFromText({
tickFontSize: 12,
legendFontSize: 12,
bottomTickLabels: ['a'],
leftTickLabels: ['b'],
rightTickLabels: [longLabel],
xAxisLabel: 'x',
yAxisLabel: 'y',
tickRotation: COMMON_CHART_CONSTANTS.NO_ROTATION_ANGLE,
bottomLegendOffset: 0,
});
expect(result.right).toBe(TEXT_MARGIN_LIMITS.max.right);
});
it('increases right margin when rightTickLabels are provided', () => {
const topRightBase = Math.ceil(12 * 1.5);
const result = getChartMarginsFromText({
tickFontSize: 12,
legendFontSize: 12,
bottomTickLabels: ['a'],
leftTickLabels: ['b'],
rightTickLabels: ['$10,000'],
xAxisLabel: 'x',
yAxisLabel: 'y',
tickRotation: COMMON_CHART_CONSTANTS.NO_ROTATION_ANGLE,
bottomLegendOffset: 0,
});
expect(result.right).toBeGreaterThan(topRightBase);
});
it('uses bottom legend offset when it exceeds tick and label blocks', () => {
const bottomLegendOffset = 100;
const result = getChartMarginsFromText({
@@ -4,7 +4,6 @@ import { getChartMarginsFromText } from '@/page-layout/widgets/graph/utils/getCh
type ChartMarginInputs = {
bottomTickLabels?: string[];
leftTickLabels?: string[];
rightTickLabels?: string[];
};
type ComputeChartMarginsParams<TTickConfig, TValueTickResult> = {
@@ -77,7 +76,6 @@ export const computeChartMargins = <TTickConfig, TValueTickResult>({
legendFontSize,
bottomTickLabels: provisionalMarginInputs.bottomTickLabels,
leftTickLabels: provisionalMarginInputs.leftTickLabels,
rightTickLabels: provisionalMarginInputs.rightTickLabels,
xAxisLabel,
yAxisLabel,
tickRotation: getTickRotation(provisionalTickConfiguration),
@@ -100,7 +98,6 @@ export const computeChartMargins = <TTickConfig, TValueTickResult>({
legendFontSize,
bottomTickLabels: marginInputs.bottomTickLabels,
leftTickLabels: marginInputs.leftTickLabels,
rightTickLabels: marginInputs.rightTickLabels,
xAxisLabel,
yAxisLabel,
tickRotation: getTickRotation(tickConfiguration),
@@ -25,12 +25,10 @@ export const computeValueTickValues = ({
minimum,
maximum,
tickCount,
preserveDomainBounds = false,
}: {
minimum: number;
maximum: number;
tickCount: number;
preserveDomainBounds?: boolean;
}): {
tickValues: number[];
domain: { min: number; max: number };
@@ -54,42 +52,6 @@ export const computeValueTickValues = ({
};
}
if (preserveDomainBounds) {
if (minimum > maximum) {
return {
tickValues: [minimum],
domain: { min: minimum, max: minimum },
};
}
const tickValues: number[] = [Number(minimum.toFixed(12))];
const firstInteriorTick =
Math.ceil(minimum / niceStepInterval) * niceStepInterval;
for (
let tickValue = firstInteriorTick;
tickValue <= maximum + niceStepInterval / 2;
tickValue += niceStepInterval
) {
const roundedTickValue = Number(tickValue.toFixed(12));
if (roundedTickValue > minimum && roundedTickValue < maximum) {
tickValues.push(roundedTickValue);
}
}
const roundedMaximum = Number(maximum.toFixed(12));
if (tickValues[tickValues.length - 1] !== roundedMaximum) {
tickValues.push(roundedMaximum);
}
return {
tickValues,
domain: { min: minimum, max: maximum },
};
}
const niceMinimum = Math.floor(minimum / niceStepInterval) * niceStepInterval;
const niceMaximum = Math.ceil(maximum / niceStepInterval) * niceStepInterval;
const tickValues: number[] = [];
@@ -22,7 +22,6 @@ export const getChartMarginsFromText = ({
legendFontSize,
bottomTickLabels,
leftTickLabels,
rightTickLabels,
xAxisLabel,
yAxisLabel,
tickRotation,
@@ -32,7 +31,6 @@ export const getChartMarginsFromText = ({
legendFontSize?: number;
bottomTickLabels?: string[];
leftTickLabels?: string[];
rightTickLabels?: string[];
xAxisLabel?: string;
yAxisLabel?: string;
tickRotation: number;
@@ -42,7 +40,6 @@ export const getChartMarginsFromText = ({
const bottomMaxLabelLength = getMaxLabelLength(bottomTickLabels);
const leftMaxLabelLength = getMaxLabelLength(leftTickLabels);
const rightMaxLabelLength = getMaxLabelLength(rightTickLabels);
const tickPaddingExtra = xAxisLabel ? TEXT_MARGIN_EXTRAS.tickPaddingExtra : 0;
const bottomTickHeight =
@@ -95,17 +92,6 @@ export const getChartMarginsFromText = ({
TEXT_MARGIN_LIMITS.max.left,
);
const rightTickWidth =
rightMaxLabelLength > 0
? estimateLabelWidth(rightMaxLabelLength, tickFontSize)
: 0;
const rightTicksBlock =
rightTickWidth > 0
? rightTickWidth +
COMMON_CHART_CONSTANTS.TICK_PADDING +
TEXT_MARGIN_EXTRAS.rightTickExtra
: 0;
const topRightBase = Math.ceil(tickFontSize * 1.5);
return {
@@ -115,7 +101,7 @@ export const getChartMarginsFromText = ({
TEXT_MARGIN_LIMITS.max.top,
),
right: clamp(
Math.max(topRightBase, rightTicksBlock),
topRightBase,
TEXT_MARGIN_LIMITS.min.right,
TEXT_MARGIN_LIMITS.max.right,
),
@@ -1,9 +1,6 @@
import { type CalendarChannel } from '@/accounts/types/CalendarChannel';
import { CoreObjectNameSingular, FeatureFlagKey } from 'twenty-shared/types';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { UPDATE_CALENDAR_CHANNEL } from '@/settings/accounts/graphql/mutations/updateCalendarChannel';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useMutation } from '@apollo/client/react';
import { SettingsAccountsEventVisibilitySettingsCard } from '@/settings/accounts/components/SettingsAccountsCalendarVisibilitySettingsCard';
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
import { styled } from '@linaria/react';
@@ -30,33 +27,26 @@ type SettingsAccountsCalendarChannelDetailsProps = {
export const SettingsAccountsCalendarChannelDetails = ({
calendarChannel,
}: SettingsAccountsCalendarChannelDetailsProps) => {
const featureFlagsMap = useFeatureFlagsMap();
const isMigrated =
featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false;
const { updateOneRecord } = useUpdateOneRecord();
const [updateMetadataChannel] = useMutation(UPDATE_CALENDAR_CHANNEL);
const updateChannel = (update: Record<string, unknown>) => {
if (isMigrated) {
updateMetadataChannel({
variables: { input: { id: calendarChannel.id, update } },
});
} else {
updateOneRecord({
objectNameSingular: CoreObjectNameSingular.CalendarChannel,
idToUpdate: calendarChannel.id,
updateOneRecordInput: update,
});
}
};
const handleVisibilityChange = (value: CalendarChannelVisibility) => {
updateChannel({ visibility: value });
updateOneRecord({
objectNameSingular: CoreObjectNameSingular.CalendarChannel,
idToUpdate: calendarChannel.id,
updateOneRecordInput: {
visibility: value,
},
});
};
const handleContactAutoCreationToggle = (value: boolean) => {
updateChannel({ isContactAutoCreationEnabled: value });
updateOneRecord({
objectNameSingular: CoreObjectNameSingular.CalendarChannel,
idToUpdate: calendarChannel.id,
updateOneRecordInput: {
isContactAutoCreationEnabled: value,
},
});
};
return (
@@ -1,9 +1,17 @@
import { styled } from '@linaria/react';
import {
type CalendarChannel,
CalendarChannelSyncStage,
} from '@/accounts/types/CalendarChannel';
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { SettingsAccountsCalendarChannelDetails } from '@/settings/accounts/components/SettingsAccountsCalendarChannelDetails';
import { SettingsNewAccountSection } from '@/settings/accounts/components/SettingsNewAccountSection';
import { SETTINGS_ACCOUNT_CALENDAR_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountCalendarChannelsTabListComponentId';
import { useMyCalendarChannels } from '@/settings/accounts/hooks/useMyCalendarChannels';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
@@ -19,8 +27,33 @@ export const SettingsAccountsCalendarChannelsContainer = () => {
activeTabIdComponentState,
SETTINGS_ACCOUNT_CALENDAR_CHANNELS_TAB_LIST_COMPONENT_ID,
);
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
const { channels: calendarChannels } = useMyCalendarChannels();
const { records: accounts } = useFindManyRecords<ConnectedAccount>({
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
filter: {
accountOwnerId: {
eq: currentWorkspaceMember?.id,
},
},
});
const { records: calendarChannels } = useFindManyRecords<
CalendarChannel & {
connectedAccount: ConnectedAccount;
}
>({
objectNameSingular: CoreObjectNameSingular.CalendarChannel,
filter: {
connectedAccountId: {
in: accounts.map((account) => account.id),
},
syncStage: {
neq: CalendarChannelSyncStage.PENDING_CONFIGURATION,
},
},
skip: !accounts.length,
});
const tabs = [
...calendarChannels.map((calendarChannel) => ({
@@ -5,11 +5,8 @@ import {
type MessageChannelContactAutoCreationPolicy,
type MessageFolderImportPolicy,
} from '@/accounts/types/MessageChannel';
import { CoreObjectNameSingular, FeatureFlagKey } from 'twenty-shared/types';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { UPDATE_MESSAGE_CHANNEL } from '@/settings/accounts/graphql/mutations/updateMessageChannel';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useMutation } from '@apollo/client/react';
import { SettingsAccountsMessageAutoCreationCard } from '@/settings/accounts/components/SettingsAccountsMessageAutoCreationCard';
import { SettingsAccountsMessageFolderCard } from '@/settings/accounts/components/SettingsAccountsMessageFolderCard';
import { SettingsAccountsMessageVisibilityCard } from '@/settings/accounts/components/SettingsAccountsMessageVisibilityCard';
@@ -43,49 +40,58 @@ const StyledDetailsContainer = styled.div`
export const SettingsAccountsMessageChannelDetails = ({
messageChannel,
}: SettingsAccountsMessageChannelDetailsProps) => {
const featureFlagsMap = useFeatureFlagsMap();
const isMigrated =
featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false;
const { updateOneRecord } = useUpdateOneRecord();
const [updateMetadataChannel] = useMutation(UPDATE_MESSAGE_CHANNEL);
const updateChannel = (update: Record<string, unknown>) => {
if (isMigrated) {
updateMetadataChannel({
variables: { input: { id: messageChannel.id, update } },
});
} else {
updateOneRecord({
objectNameSingular: CoreObjectNameSingular.MessageChannel,
idToUpdate: messageChannel.id,
updateOneRecordInput: update,
});
}
};
const handleVisibilityChange = (value: MessageChannelVisibility) => {
updateChannel({ visibility: value });
updateOneRecord({
objectNameSingular: CoreObjectNameSingular.MessageChannel,
idToUpdate: messageChannel.id,
updateOneRecordInput: {
visibility: value,
},
});
};
const handleContactAutoCreationChange = (
value: MessageChannelContactAutoCreationPolicy,
) => {
updateChannel({ contactAutoCreationPolicy: value });
updateOneRecord({
objectNameSingular: CoreObjectNameSingular.MessageChannel,
idToUpdate: messageChannel.id,
updateOneRecordInput: {
contactAutoCreationPolicy: value,
},
});
};
const handleIsGroupEmailExcludedToggle = (value: boolean) => {
updateChannel({ excludeGroupEmails: value });
updateOneRecord({
objectNameSingular: CoreObjectNameSingular.MessageChannel,
idToUpdate: messageChannel.id,
updateOneRecordInput: {
excludeGroupEmails: value,
},
});
};
const handleIsNonProfessionalEmailExcludedToggle = (value: boolean) => {
updateChannel({ excludeNonProfessionalEmails: value });
updateOneRecord({
objectNameSingular: CoreObjectNameSingular.MessageChannel,
idToUpdate: messageChannel.id,
updateOneRecordInput: {
excludeNonProfessionalEmails: value,
},
});
};
const handleMessageFolderImportPolicyChange = (
value: MessageFolderImportPolicy,
) => {
updateChannel({ messageFolderImportPolicy: value });
updateOneRecord({
objectNameSingular: CoreObjectNameSingular.MessageChannel,
idToUpdate: messageChannel.id,
updateOneRecordInput: { messageFolderImportPolicy: value },
});
};
return (
@@ -1,15 +1,23 @@
import { styled } from '@linaria/react';
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import {
type MessageChannel,
MessageChannelSyncStage,
} from '@/accounts/types/MessageChannel';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { SettingsAccountsMessageChannelDetails } from '@/settings/accounts/components/SettingsAccountsMessageChannelDetails';
import { SettingsAccountsSelectedMessageChannelEffect } from '@/settings/accounts/components/SettingsAccountsSelectedMessageChannelEffect';
import { SettingsNewAccountSection } from '@/settings/accounts/components/SettingsNewAccountSection';
import { SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountMessageChannelsTabListComponentId';
import { useMyMessageChannels } from '@/settings/accounts/hooks/useMyMessageChannels';
import { settingsAccountsSelectedMessageChannelState } from '@/settings/accounts/states/settingsAccountsSelectedMessageChannelState';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import React, { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -23,11 +31,48 @@ export const SettingsAccountsMessageChannelsContainer = () => {
activeTabIdComponentState,
SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID,
);
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
const setSettingsAccountsSelectedMessageChannel = useSetAtomState(
settingsAccountsSelectedMessageChannelState,
);
const { channels: messageChannels } = useMyMessageChannels();
const { records: accounts } = useFindManyRecords<ConnectedAccount>({
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
filter: {
accountOwnerId: {
eq: currentWorkspaceMember?.id,
},
},
});
const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({
objectNameSingular: CoreObjectNameSingular.MessageChannel,
depth: 1,
});
const { records: messageChannels } = useFindManyRecords<
MessageChannel & {
connectedAccount: ConnectedAccount;
}
>({
objectNameSingular: CoreObjectNameSingular.MessageChannel,
filter: {
connectedAccountId: {
in: accounts.map((account) => account.id),
},
isSyncEnabled: {
eq: true,
},
syncStage: {
neq: MessageChannelSyncStage.PENDING_CONFIGURATION,
},
},
recordGqlFields,
onCompleted: (data) => {
setSettingsAccountsSelectedMessageChannel(data[0]);
},
skip: !accounts.length,
});
const tabs = messageChannels.map((messageChannel) => ({
id: messageChannel.id,
@@ -52,9 +97,6 @@ export const SettingsAccountsMessageChannelsContainer = () => {
return (
<>
<SettingsAccountsSelectedMessageChannelEffect
messageChannels={messageChannels}
/>
{tabs.length > 1 && (
<StyledMessageContainer>
<TabList
@@ -1,16 +1,12 @@
import { useApolloClient, useMutation } from '@apollo/client/react';
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import { CalendarChannelSyncStage } from '@/accounts/types/CalendarChannel';
import { MessageChannelSyncStage } from '@/accounts/types/MessageChannel';
import {
CoreObjectNameSingular,
ConnectedAccountProvider,
FeatureFlagKey,
SettingsPath,
} from 'twenty-shared/types';
import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
import { useTriggerProviderReconnect } from '@/settings/accounts/hooks/useTriggerProviderReconnect';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
@@ -31,8 +27,6 @@ import {
import { LightIconButton } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { DELETE_CONNECTED_ACCOUNT } from '../graphql/mutations/deleteConnectedAccount';
type SettingsAccountsRowDropdownMenuProps = {
account: ConnectedAccount;
@@ -51,17 +45,9 @@ export const SettingsAccountsRowDropdownMenu = ({
const navigate = useNavigateSettings();
const { closeDropdown } = useCloseDropdown();
const featureFlagsMap = useFeatureFlagsMap();
const isMigrated =
featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false;
const apolloClient = useApolloClient();
const { destroyOneRecord } = useDestroyOneRecord({
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
});
const [deleteConnectedAccountMutation] = useMutation(
DELETE_CONNECTED_ACCOUNT,
);
const { triggerProviderReconnect } = useTriggerProviderReconnect();
const hasPendingConfiguration =
@@ -75,14 +61,7 @@ export const SettingsAccountsRowDropdownMenu = ({
);
const deleteAccount = async () => {
if (isMigrated) {
await deleteConnectedAccountMutation({
variables: { id: account.id },
});
await apolloClient.refetchQueries({ include: 'active' });
} else {
await destroyOneRecord(account.id);
}
await destroyOneRecord(account.id);
};
return (
@@ -1,40 +0,0 @@
import { type MessageChannel } from '@/accounts/types/MessageChannel';
import { SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountMessageChannelsTabListComponentId';
import { settingsAccountsSelectedMessageChannelState } from '@/settings/accounts/states/settingsAccountsSelectedMessageChannelState';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { useEffect } from 'react';
type SettingsAccountsSelectedMessageChannelEffectProps = {
messageChannels: MessageChannel[];
};
export const SettingsAccountsSelectedMessageChannelEffect = ({
messageChannels,
}: SettingsAccountsSelectedMessageChannelEffectProps) => {
const activeTabId = useAtomComponentStateValue(
activeTabIdComponentState,
SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID,
);
const setSettingsAccountsSelectedMessageChannel = useSetAtomState(
settingsAccountsSelectedMessageChannelState,
);
useEffect(() => {
if (messageChannels.length === 0) {
return;
}
const currentSelectionStillExists = activeTabId
? messageChannels.some((channel) => channel.id === activeTabId)
: false;
if (!currentSelectionStillExists) {
setSettingsAccountsSelectedMessageChannel(messageChannels[0]);
}
}, [messageChannels, activeTabId, setSettingsAccountsSelectedMessageChannel]);
return null;
};
@@ -1,10 +1,13 @@
import { type MessageChannel } from '@/accounts/types/MessageChannel';
import { type MessageFolder } from '@/accounts/types/MessageFolder';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { SettingsMessageFoldersEmptyStateCard } from '@/settings/accounts/components/message-folders/SettingsMessageFoldersEmptyStateCard';
import { SettingsMessageFoldersSkeletonLoader } from '@/settings/accounts/components/message-folders/SettingsMessageFoldersSkeletonLoader';
import { SettingsMessageFoldersTreeItem } from '@/settings/accounts/components/message-folders/SettingsMessageFoldersTreeItem';
import { computeFolderIdsForSyncToggle } from '@/settings/accounts/components/message-folders/utils/computeFolderIdsForSyncToggle';
import { computeMessageFolderTree } from '@/settings/accounts/components/message-folders/utils/computeMessageFolderTree';
import { useMyMessageFolders } from '@/settings/accounts/hooks/useMyMessageFolders';
import { useUpdateMessageFoldersSyncStatus } from '@/settings/accounts/hooks/useUpdateMessageFoldersSyncStatus';
import { settingsAccountsSelectedMessageChannelState } from '@/settings/accounts/states/settingsAccountsSelectedMessageChannelState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
@@ -72,9 +75,19 @@ export const SettingsAccountsMessageFoldersCard = () => {
const { updateMessageFoldersSyncStatus } =
useUpdateMessageFoldersSyncStatus();
const { messageFolders, loading } = useMyMessageFolders(
settingsAccountsSelectedMessageChannel?.id,
);
const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({
objectNameSingular: CoreObjectNameSingular.MessageChannel,
depth: 1,
shouldOnlyLoadRelationIdentifiers: false,
});
const { record: messageChannel, loading } = useFindOneRecord<MessageChannel>({
objectNameSingular: CoreObjectNameSingular.MessageChannel,
objectRecordId: settingsAccountsSelectedMessageChannel?.id,
recordGqlFields,
});
const { messageFolders = [] } = messageChannel ?? {};
const filteredMessageFolders = useMemo(() => {
return messageFolders.filter((folder) =>
@@ -146,25 +146,4 @@ describe('computeMessageFolderTree', () => {
expect(result[0].children[0].folder.name).toBe('Clients');
expect(result[0].children[1].folder.name).toBe('Projects');
});
it('should resolve parent-child when parentFolderId references parent id instead of externalId', () => {
const parent = createFolder(
'20202020-aaaa-bbbb-cccc-000000000001',
'custom folder',
null,
'Label_5900090362003645629',
);
const child = createFolder(
'20202020-aaaa-bbbb-cccc-000000000002',
'child folder',
'20202020-aaaa-bbbb-cccc-000000000001',
'Label_7713410187110265162',
);
const result = computeMessageFolderTree([parent, child]);
expect(result).toHaveLength(1);
expect(result[0].folder.name).toBe('custom folder');
expect(result[0].children).toHaveLength(1);
expect(result[0].children[0].folder.name).toBe('child folder');
});
});
@@ -16,11 +16,9 @@ export const computeFolderIdsForSyncToggle = ({
const collectChildren = (id: string): string[] => {
const folder = folderById.get(id);
const children = folder
const children = folder?.externalId
? allFolders.filter(
(childFolder) =>
childFolder.parentFolderId === folder.externalId ||
childFolder.parentFolderId === folder.id,
(childFolder) => childFolder.parentFolderId === folder.externalId,
)
: [];
@@ -40,9 +38,7 @@ export const computeFolderIdsForSyncToggle = ({
break;
}
const parent =
folderByExternalId.get(current.parentFolderId) ??
folderById.get(current.parentFolderId);
const parent = folderByExternalId.get(current.parentFolderId);
if (!parent) {
break;
@@ -67,9 +63,7 @@ export const computeFolderIdsForSyncToggle = ({
for (const parent of collectParents(folderId)) {
const children = allFolders.filter(
(folder) =>
folder.parentFolderId === parent.externalId ||
folder.parentFolderId === parent.id,
(folder) => folder.parentFolderId === parent.externalId,
);
const hasOtherSyncedChild = children.some(
(child) => child.isSynced && !idsToUnsync.has(child.id),
@@ -11,22 +11,17 @@ export const computeMessageFolderTree = (
folders: MessageFolder[],
): MessageFolderTreeNode[] => {
const folderByExternalIdMap = new Map<string, MessageFolder>();
const folderByIdMap = new Map<string, MessageFolder>();
const childrenMap = new Map<string, MessageFolder[]>();
folders.forEach((folder) => {
if (isDefined(folder.externalId)) {
folderByExternalIdMap.set(folder.externalId, folder);
}
folderByIdMap.set(folder.id, folder);
});
folders.forEach((folder) => {
if (isDefined(folder.parentFolderId)) {
const parent =
folderByExternalIdMap.get(folder.parentFolderId) ??
folderByIdMap.get(folder.parentFolderId);
const parent = folderByExternalIdMap.get(folder.parentFolderId);
if (isDefined(parent)) {
const siblings = childrenMap.get(parent.id) || [];
siblings.push(folder);
@@ -51,10 +46,7 @@ export const computeMessageFolderTree = (
const rootFolders = folders.filter((folder) => {
if (!folder.parentFolderId) return true;
return (
!folderByExternalIdMap.has(folder.parentFolderId) &&
!folderByIdMap.has(folder.parentFolderId)
);
return !folderByExternalIdMap.has(folder.parentFolderId);
});
rootFolders.sort((a, b) => a.name.localeCompare(b.name));
@@ -1,9 +0,0 @@
import { gql } from '@apollo/client';
export const DELETE_CONNECTED_ACCOUNT = gql`
mutation DeleteConnectedAccount($id: UUID!) {
deleteConnectedAccount(id: $id) {
id
}
}
`;
@@ -1,12 +0,0 @@
import { gql } from '@apollo/client';
export const UPDATE_CALENDAR_CHANNEL = gql`
mutation UpdateCalendarChannel($input: UpdateCalendarChannelInput!) {
updateCalendarChannel(input: $input) {
id
visibility
isContactAutoCreationEnabled
contactAutoCreationPolicy
}
}
`;
@@ -1,14 +0,0 @@
import { gql } from '@apollo/client';
export const UPDATE_MESSAGE_CHANNEL = gql`
mutation UpdateMessageChannel($input: UpdateMessageChannelInput!) {
updateMessageChannel(input: $input) {
id
visibility
contactAutoCreationPolicy
excludeNonProfessionalEmails
excludeGroupEmails
messageFolderImportPolicy
}
}
`;
@@ -1,10 +0,0 @@
import { gql } from '@apollo/client';
export const UPDATE_MESSAGE_FOLDER = gql`
mutation UpdateMessageFolder($input: UpdateMessageFolderInput!) {
updateMessageFolder(input: $input) {
id
isSynced
}
}
`;
@@ -1,10 +0,0 @@
import { gql } from '@apollo/client';
export const UPDATE_MESSAGE_FOLDERS = gql`
mutation UpdateMessageFolders($input: UpdateMessageFoldersInput!) {
updateMessageFolders(input: $input) {
id
isSynced
}
}
`;
@@ -1,20 +0,0 @@
import { gql } from '@apollo/client';
export const GET_MY_CALENDAR_CHANNELS = gql`
query MyCalendarChannels($connectedAccountId: UUID) {
myCalendarChannels(connectedAccountId: $connectedAccountId) {
id
handle
visibility
syncStatus
syncStage
syncStageStartedAt
isContactAutoCreationEnabled
contactAutoCreationPolicy
isSyncEnabled
connectedAccountId
createdAt
updatedAt
}
}
`;
@@ -1,18 +0,0 @@
import { gql } from '@apollo/client';
export const GET_MY_CONNECTED_ACCOUNTS = gql`
query MyConnectedAccounts {
myConnectedAccounts {
id
handle
provider
authFailedAt
scopes
handleAliases
lastSignedInAt
userWorkspaceId
createdAt
updatedAt
}
}
`;
@@ -1,24 +0,0 @@
import { gql } from '@apollo/client';
export const GET_MY_MESSAGE_CHANNELS = gql`
query MyMessageChannels($connectedAccountId: UUID) {
myMessageChannels(connectedAccountId: $connectedAccountId) {
id
handle
visibility
type
isContactAutoCreationEnabled
contactAutoCreationPolicy
messageFolderImportPolicy
excludeNonProfessionalEmails
excludeGroupEmails
isSyncEnabled
syncStatus
syncStage
syncStageStartedAt
connectedAccountId
createdAt
updatedAt
}
}
`;
@@ -1,17 +0,0 @@
import { gql } from '@apollo/client';
export const GET_MY_MESSAGE_FOLDERS = gql`
query MyMessageFolders($messageChannelId: UUID) {
myMessageFolders(messageChannelId: $messageChannelId) {
id
name
isSynced
isSentFolder
parentFolderId
externalId
messageChannelId
createdAt
updatedAt
}
}
`;
@@ -1,111 +0,0 @@
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import {
type CalendarChannel,
CalendarChannelSyncStage,
} from '@/accounts/types/CalendarChannel';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { GET_MY_CALENDAR_CHANNELS } from '@/settings/accounts/graphql/queries/getMyCalendarChannels';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useApolloClient, useQuery } from '@apollo/client/react';
import { useMemo } from 'react';
import { CoreObjectNameSingular, FeatureFlagKey } from 'twenty-shared/types';
type MetadataCalendarChannel = {
id: string;
handle: string;
visibility: string;
syncStatus: string;
syncStage: string;
syncStageStartedAt: string | null;
isContactAutoCreationEnabled: boolean;
contactAutoCreationPolicy: string;
isSyncEnabled: boolean;
connectedAccountId: string;
createdAt: string;
updatedAt: string;
};
export const useMyCalendarChannels = () => {
const featureFlagsMap = useFeatureFlagsMap();
const isMigrated =
featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false;
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
const apolloClient = useApolloClient();
const { records: workspaceAccounts } = useFindManyRecords<ConnectedAccount>({
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
filter: {
accountOwnerId: {
eq: currentWorkspaceMember?.id,
},
},
skip: isMigrated,
});
const { records: workspaceChannels, loading: workspaceLoading } =
useFindManyRecords<
CalendarChannel & { connectedAccount: ConnectedAccount }
>({
objectNameSingular: CoreObjectNameSingular.CalendarChannel,
filter: {
connectedAccountId: {
in: workspaceAccounts.map((account) => account.id),
},
syncStage: {
neq: CalendarChannelSyncStage.PENDING_CONFIGURATION,
},
},
skip: isMigrated || !workspaceAccounts.length,
});
const { data: metadataData, loading: metadataLoading } = useQuery<{
myCalendarChannels: MetadataCalendarChannel[];
}>(GET_MY_CALENDAR_CHANNELS, {
client: apolloClient,
skip: !isMigrated,
});
const channels = useMemo(() => {
if (!isMigrated) {
return workspaceChannels;
}
if (!metadataData?.myCalendarChannels) {
return [];
}
return metadataData.myCalendarChannels
.filter(
(channel: MetadataCalendarChannel) =>
channel.syncStage !== 'PENDING_CONFIGURATION',
)
.map(
(channel: MetadataCalendarChannel) =>
({
id: channel.id,
handle: channel.handle,
visibility: channel.visibility,
isContactAutoCreationEnabled: channel.isContactAutoCreationEnabled,
contactAutoCreationPolicy: channel.contactAutoCreationPolicy,
isSyncEnabled: channel.isSyncEnabled,
syncStatus: channel.syncStatus,
syncStage: channel.syncStage,
syncCursor: '',
syncStageStartedAt: channel.syncStageStartedAt
? new Date(channel.syncStageStartedAt)
: null,
throttleFailureCount: 0,
connectedAccountId: channel.connectedAccountId,
__typename: 'CalendarChannel',
}) as CalendarChannel,
);
}, [isMigrated, workspaceChannels, metadataData]);
return {
channels,
loading: isMigrated ? metadataLoading : workspaceLoading,
};
};
@@ -1,116 +0,0 @@
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
import { GET_MY_CONNECTED_ACCOUNTS } from '@/settings/accounts/graphql/queries/getMyConnectedAccounts';
import { useMyCalendarChannels } from '@/settings/accounts/hooks/useMyCalendarChannels';
import { useMyMessageChannels } from '@/settings/accounts/hooks/useMyMessageChannels';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useApolloClient, useQuery } from '@apollo/client/react';
import { useMemo } from 'react';
import { CoreObjectNameSingular, FeatureFlagKey } from 'twenty-shared/types';
type MetadataConnectedAccount = {
id: string;
handle: string;
provider: string;
authFailedAt: string | null;
scopes: string[] | null;
handleAliases: string[] | null;
lastSignedInAt: string | null;
userWorkspaceId: string;
createdAt: string;
updatedAt: string;
};
export const useMyConnectedAccounts = () => {
const featureFlagsMap = useFeatureFlagsMap();
const isMigrated =
featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false;
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
const apolloClient = useApolloClient();
const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
depth: 1,
shouldOnlyLoadRelationIdentifiers: false,
});
const { records: workspaceAccounts, loading: workspaceLoading } =
useFindManyRecords<ConnectedAccount>({
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
filter: {
accountOwnerId: {
eq: currentWorkspaceMember?.id,
},
},
recordGqlFields,
skip: isMigrated,
});
const { data: metadataData, loading: metadataLoading } = useQuery<{
myConnectedAccounts: MetadataConnectedAccount[];
}>(GET_MY_CONNECTED_ACCOUNTS, {
client: apolloClient,
skip: !isMigrated,
});
const { channels: messageChannels, loading: messageChannelsLoading } =
useMyMessageChannels();
const { channels: calendarChannels, loading: calendarChannelsLoading } =
useMyCalendarChannels();
const accounts = useMemo<ConnectedAccount[]>(() => {
if (!isMigrated) {
return workspaceAccounts;
}
if (!metadataData?.myConnectedAccounts) {
return [];
}
return metadataData.myConnectedAccounts.map(
(account: MetadataConnectedAccount) =>
({
id: account.id,
handle: account.handle,
provider: account.provider,
accessToken: '',
refreshToken: '',
accountOwnerId: account.userWorkspaceId,
lastSyncHistoryId: '',
authFailedAt: account.authFailedAt
? new Date(account.authFailedAt)
: null,
messageChannels: messageChannels.filter(
(channel) =>
(channel as unknown as { connectedAccountId: string })
.connectedAccountId === account.id,
),
calendarChannels: calendarChannels.filter(
(channel) =>
(channel as unknown as { connectedAccountId: string })
.connectedAccountId === account.id,
),
scopes: account.scopes,
__typename: 'ConnectedAccount',
}) as ConnectedAccount,
);
}, [
isMigrated,
workspaceAccounts,
metadataData,
messageChannels,
calendarChannels,
]);
return {
accounts,
loading: isMigrated
? metadataLoading || messageChannelsLoading || calendarChannelsLoading
: workspaceLoading,
};
};
@@ -1,127 +0,0 @@
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import {
type MessageChannel,
MessageChannelSyncStage,
} from '@/accounts/types/MessageChannel';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
import { GET_MY_MESSAGE_CHANNELS } from '@/settings/accounts/graphql/queries/getMyMessageChannels';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useApolloClient, useQuery } from '@apollo/client/react';
import { useMemo } from 'react';
import { CoreObjectNameSingular, FeatureFlagKey } from 'twenty-shared/types';
type MetadataMessageChannel = {
id: string;
handle: string;
visibility: string;
type: string;
isContactAutoCreationEnabled: boolean;
contactAutoCreationPolicy: string;
messageFolderImportPolicy: string;
excludeNonProfessionalEmails: boolean;
excludeGroupEmails: boolean;
isSyncEnabled: boolean;
syncStatus: string;
syncStage: string;
syncStageStartedAt: string | null;
connectedAccountId: string;
createdAt: string;
updatedAt: string;
};
export const useMyMessageChannels = () => {
const featureFlagsMap = useFeatureFlagsMap();
const isMigrated =
featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false;
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
const apolloClient = useApolloClient();
const { records: workspaceAccounts } = useFindManyRecords<ConnectedAccount>({
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
filter: {
accountOwnerId: {
eq: currentWorkspaceMember?.id,
},
},
skip: isMigrated,
});
const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({
objectNameSingular: CoreObjectNameSingular.MessageChannel,
depth: 1,
});
const { records: workspaceChannels, loading: workspaceLoading } =
useFindManyRecords<MessageChannel & { connectedAccount: ConnectedAccount }>(
{
objectNameSingular: CoreObjectNameSingular.MessageChannel,
filter: {
connectedAccountId: {
in: workspaceAccounts.map((account) => account.id),
},
isSyncEnabled: { eq: true },
syncStage: {
neq: MessageChannelSyncStage.PENDING_CONFIGURATION,
},
},
recordGqlFields,
skip: isMigrated || !workspaceAccounts.length,
},
);
const { data: metadataData, loading: metadataLoading } = useQuery<{
myMessageChannels: MetadataMessageChannel[];
}>(GET_MY_MESSAGE_CHANNELS, {
client: apolloClient,
skip: !isMigrated,
});
const channels = useMemo(() => {
if (!isMigrated) {
return workspaceChannels;
}
if (!metadataData?.myMessageChannels) {
return [];
}
return metadataData.myMessageChannels
.filter(
(channel: MetadataMessageChannel) =>
channel.isSyncEnabled &&
channel.syncStage !== 'PENDING_CONFIGURATION',
)
.map(
(channel: MetadataMessageChannel) =>
({
id: channel.id,
handle: channel.handle,
visibility: channel.visibility,
contactAutoCreationPolicy: channel.contactAutoCreationPolicy,
excludeNonProfessionalEmails: channel.excludeNonProfessionalEmails,
excludeGroupEmails: channel.excludeGroupEmails,
isSyncEnabled: channel.isSyncEnabled,
messageFolders: [],
messageFolderImportPolicy: channel.messageFolderImportPolicy,
syncStatus: channel.syncStatus,
syncStage: channel.syncStage,
syncCursor: '',
syncStageStartedAt: channel.syncStageStartedAt
? new Date(channel.syncStageStartedAt)
: null,
throttleFailureCount: 0,
connectedAccountId: channel.connectedAccountId,
__typename: 'MessageChannel',
}) as MessageChannel,
);
}, [isMigrated, workspaceChannels, metadataData]);
return {
channels,
loading: isMigrated ? metadataLoading : workspaceLoading,
};
};
@@ -1,80 +0,0 @@
import { type MessageChannel } from '@/accounts/types/MessageChannel';
import { type MessageFolder } from '@/accounts/types/MessageFolder';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
import { GET_MY_MESSAGE_FOLDERS } from '@/settings/accounts/graphql/queries/getMyMessageFolders';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useApolloClient, useQuery } from '@apollo/client/react';
import { useMemo } from 'react';
import { CoreObjectNameSingular, FeatureFlagKey } from 'twenty-shared/types';
type MetadataMessageFolder = {
id: string;
name: string | null;
isSynced: boolean;
isSentFolder: boolean;
parentFolderId: string | null;
externalId: string | null;
messageChannelId: string;
createdAt: string;
updatedAt: string;
};
export const useMyMessageFolders = (messageChannelId?: string) => {
const featureFlagsMap = useFeatureFlagsMap();
const isMigrated =
featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false;
const apolloClient = useApolloClient();
const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({
objectNameSingular: CoreObjectNameSingular.MessageChannel,
depth: 1,
shouldOnlyLoadRelationIdentifiers: false,
});
const { record: messageChannel, loading: workspaceLoading } =
useFindOneRecord<MessageChannel>({
objectNameSingular: CoreObjectNameSingular.MessageChannel,
objectRecordId: messageChannelId,
recordGqlFields,
skip: isMigrated || !messageChannelId,
});
const { data: metadataData, loading: metadataLoading } = useQuery<{
myMessageFolders: MetadataMessageFolder[];
}>(GET_MY_MESSAGE_FOLDERS, {
client: apolloClient,
variables: messageChannelId ? { messageChannelId } : undefined,
skip: !isMigrated,
});
const messageFolders = useMemo<MessageFolder[]>(() => {
if (!isMigrated) {
return messageChannel?.messageFolders ?? [];
}
if (!metadataData?.myMessageFolders) {
return [];
}
return metadataData.myMessageFolders.map(
(folder: MetadataMessageFolder) => ({
id: folder.id,
name: folder.name ?? '',
syncCursor: '',
isSynced: folder.isSynced,
isSentFolder: folder.isSentFolder,
parentFolderId: folder.parentFolderId,
messageChannelId: folder.messageChannelId,
externalId: folder.externalId,
__typename: 'MessageFolder' as const,
}),
);
}, [isMigrated, messageChannel, metadataData]);
return {
messageFolders,
loading: isMigrated ? metadataLoading : workspaceLoading,
};
};
@@ -1,10 +1,7 @@
import { useCallback } from 'react';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useUpdateManyRecords } from '@/object-record/hooks/useUpdateManyRecords';
import { UPDATE_MESSAGE_FOLDERS } from '@/settings/accounts/graphql/mutations/updateMessageFolders';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useApolloClient } from '@apollo/client/react';
import { CoreObjectNameSingular, FeatureFlagKey } from 'twenty-shared/types';
type UpdateMessageFoldersSyncStatusArgs = {
messageFolderIds: string[];
@@ -12,12 +9,6 @@ type UpdateMessageFoldersSyncStatusArgs = {
};
export const useUpdateMessageFoldersSyncStatus = () => {
const featureFlagsMap = useFeatureFlagsMap();
const isMigrated =
featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false;
const apolloClient = useApolloClient();
const { updateManyRecords } = useUpdateManyRecords({
objectNameSingular: CoreObjectNameSingular.MessageFolder,
recordGqlFields: {
@@ -31,37 +22,12 @@ export const useUpdateMessageFoldersSyncStatus = () => {
messageFolderIds,
isSynced,
}: UpdateMessageFoldersSyncStatusArgs) => {
if (isMigrated) {
if (messageFolderIds.length === 0) {
return;
}
await apolloClient.mutate({
mutation: UPDATE_MESSAGE_FOLDERS,
variables: {
input: {
ids: messageFolderIds,
update: { isSynced },
},
},
optimisticResponse: {
updateMessageFolders: messageFolderIds.map((id) => ({
__typename: 'MessageFolder',
id,
isSynced,
})),
},
});
return;
}
await updateManyRecords({
recordIdsToUpdate: messageFolderIds,
updateOneRecordInput: { isSynced },
});
},
[isMigrated, apolloClient, updateManyRecords],
[updateManyRecords],
);
return { updateMessageFoldersSyncStatus };
@@ -40,7 +40,8 @@ export const SettingsNavigationDrawerItem = ({
to={href}
Icon={item.Icon}
active={isActive}
modifier={item.modifier}
soon={item.soon}
isNew={item.isNew}
onClick={item.onClick}
/>
</AdvancedSettingsWrapper>
@@ -55,7 +56,8 @@ export const SettingsNavigationDrawerItem = ({
to={href || undefined}
Icon={item.Icon}
active={isActive}
modifier={item.modifier}
soon={item.soon}
isNew={item.isNew}
onClick={item.onClick}
/>
);
@@ -7,10 +7,7 @@ import { billingState } from '@/client-config/states/billingState';
import { supportChatState } from '@/client-config/states/supportChatState';
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
import { getDocumentationUrl } from '@/support/utils/getDocumentationUrl';
import {
type NavigationDrawerItemIndentationLevel,
type NavigationDrawerItemModifier,
} from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { type NavigationDrawerItemIndentationLevel } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { t } from '@lingui/core/macro';
@@ -60,7 +57,8 @@ export type SettingsNavigationItem = {
isHidden?: boolean;
subItems?: SettingsNavigationItem[];
isAdvanced?: boolean;
modifier?: NavigationDrawerItemModifier;
soon?: boolean;
isNew?: boolean;
};
const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
@@ -181,7 +179,7 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
isHidden:
!isApplicationEnabled ||
!permissionMap[PermissionFlagType.WORKSPACE],
modifier: 'new',
isNew: true,
},
{
label: t`AI`,
@@ -189,7 +187,7 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
Icon: IconSparkles,
isHidden:
!isAIEnabled || !permissionMap[PermissionFlagType.WORKSPACE],
modifier: 'new',
isNew: true,
},
{
label: t`Security`,
@@ -45,7 +45,7 @@ export const SignInAppNavigationDrawerMock = ({
label={t`Search`}
Icon={IconSearch}
onClick={() => {}}
modifier={{ keyboard: [getOsControlSymbol(), 'K'] }}
keyboard={[getOsControlSymbol(), 'K']}
/>
<NavigationDrawerItem
label={t`Settings`}
@@ -1,5 +1,4 @@
import { InputErrorHelper } from '@/ui/input/components/InputErrorHelper';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { css } from '@linaria/core';
@@ -460,7 +459,7 @@ const TextInputWithAutoGrowWrapper = forwardRef<
{props.autoGrow ? (
<StyledAutogrowWrapper
sizeVariant={props.sizeVariant}
node={isNonEmptyString(props.value) ? props.value : props.placeholder}
node={props.value ?? props.placeholder}
>
<TextInputComponent
// oxlint-disable-next-line react/jsx-props-no-spreading
@@ -32,8 +32,8 @@ const StyledAnimatedContainer = styled.div<{
isExpanded: boolean;
isResizing: boolean;
}>`
height: 100%;
max-height: 100%;
height: 100vh;
max-height: 100vh;
overflow: hidden;
position: relative;
transition: ${({ isResizing }) =>
@@ -37,11 +37,6 @@ const DEFAULT_INDENTATION_LEVEL = 1;
export type NavigationDrawerItemIndentationLevel = 1 | 2;
export type NavigationDrawerItemModifier =
| 'soon'
| 'new'
| { keyboard: string[] };
export type NavigationDrawerItemProps = {
className?: string;
label: string;
@@ -53,12 +48,17 @@ export type NavigationDrawerItemProps = {
Icon?: IconComponent | ((props: TablerIconsProps) => JSX.Element);
iconColor?: string | null;
active?: boolean;
modifier?: NavigationDrawerItemModifier;
danger?: boolean;
soon?: boolean;
isNew?: boolean;
count?: number;
keyboard?: string[];
rightOptions?: ReactNode;
alwaysShowRightOptions?: boolean;
isDragging?: boolean;
isRightOptionsDropdownOpen?: boolean;
triggerEvent?: TriggerEventType;
mouseUpNavigation?: boolean;
preventCollapseOnMobile?: boolean;
isSelectedInEditMode?: boolean;
variant?: 'default' | 'tertiary';
@@ -67,13 +67,14 @@ export type NavigationDrawerItemProps = {
type StyledItemProps = Pick<
NavigationDrawerItemProps,
| 'active'
| 'danger'
| 'indentationLevel'
| 'soon'
| 'to'
| 'isDragging'
| 'isSelectedInEditMode'
| 'variant'
> & {
isSoon: boolean;
isNavigationDrawerExpanded: boolean;
hasRightOptions: boolean;
href?: string;
@@ -91,11 +92,14 @@ const StyledItem = styled.button<StyledItemProps>`
: '1px solid transparent'};
border-radius: ${themeCssVariables.border.radius.sm};
box-sizing: border-box;
color: ${({ active, isSoon, variant }) => {
color: ${({ active, danger, soon, variant }) => {
if (active === true) {
return themeCssVariables.font.color.primary;
}
if (isSoon) {
if (danger === true) {
return themeCssVariables.color.red;
}
if (soon === true) {
return themeCssVariables.font.color.light;
}
if (variant === 'tertiary') {
@@ -103,8 +107,8 @@ const StyledItem = styled.button<StyledItemProps>`
}
return themeCssVariables.font.color.secondary;
}};
cursor: ${({ isSoon, isDragging }) =>
isDragging ? 'grabbing' : isSoon ? 'default' : 'pointer'};
cursor: ${({ soon, isDragging }) =>
isDragging ? 'grabbing' : soon ? 'default' : 'pointer'};
display: flex;
font-family: ${themeCssVariables.font.family};
font-size: ${themeCssVariables.font.size.md};
@@ -118,7 +122,7 @@ const StyledItem = styled.button<StyledItemProps>`
? themeCssVariables.spacing['0.5']
: themeCssVariables.spacing[1]};
padding-top: ${themeCssVariables.spacing[1]};
pointer-events: ${({ isSoon }) => (isSoon ? 'none' : 'auto')};
pointer-events: ${({ soon }) => (soon ? 'none' : 'auto')};
text-decoration: none;
user-select: none;
width: ${({ isNavigationDrawerExpanded, hasRightOptions }) =>
@@ -128,7 +132,10 @@ const StyledItem = styled.button<StyledItemProps>`
&:hover {
background: ${themeCssVariables.background.transparent.light};
color: ${themeCssVariables.font.color.primary};
color: ${({ danger }) =>
danger
? themeCssVariables.color.red
: themeCssVariables.font.color.primary};
}
&:hover .keyboard-shortcuts {
@@ -165,6 +172,20 @@ const StyledItemSecondaryLabel = styled.span`
font-weight: ${themeCssVariables.font.weight.regular};
`;
const StyledItemCount = styled.span`
align-items: center;
background-color: ${themeCssVariables.color.blue};
border-radius: ${themeCssVariables.border.radius.rounded};
color: ${themeCssVariables.grayScale.gray1};
display: flex;
font-size: ${themeCssVariables.font.size.xs};
font-weight: ${themeCssVariables.font.weight.semiBold};
height: 16px;
justify-content: center;
margin-left: auto;
width: 16px;
`;
const StyledKeyBoardShortcut = styled.span`
align-items: center;
background: ${themeCssVariables.background.transparent.lighter};
@@ -256,13 +277,18 @@ export const NavigationDrawerItem = ({
to,
onClick,
active,
modifier,
danger,
soon,
isNew,
count,
keyboard,
subItemState,
rightOptions,
alwaysShowRightOptions = false,
isDragging,
isRightOptionsDropdownOpen,
triggerEvent,
mouseUpNavigation = false,
preventCollapseOnMobile = false,
isSelectedInEditMode = false,
variant = 'default',
@@ -275,15 +301,10 @@ export const NavigationDrawerItem = ({
const { navigationItemId } = useNavigationDrawerTooltip(label, to);
const isSoon = modifier === 'soon';
const isNew = modifier === 'new';
const keyboardKeys =
isDefined(modifier) && typeof modifier === 'object'
? modifier.keyboard
: undefined;
const showBreadcrumb = indentationLevel === 2;
const showStyledSpacer = isDefined(modifier) || isDefined(rightOptions);
const showStyledSpacer = Boolean(
soon || isNew || count || keyboard || rightOptions,
);
const handleMobileNavigation = () => {
if (isMobile && !preventCollapseOnMobile) {
@@ -293,7 +314,6 @@ export const NavigationDrawerItem = ({
const isExternalLink =
isDefined(to) && (to.startsWith('http://') || to.startsWith('https://'));
const isInternalLink = isDefined(to) && !isExternalLink;
const handleExternalLinkClick = () => {
handleMobileNavigation();
@@ -312,37 +332,40 @@ export const NavigationDrawerItem = ({
triggerEvent,
});
const elementType = isExternalLink
? 'a'
: isInternalLink
? Link
: isDefined(rightOptions)
? 'div'
: undefined;
return (
<StyledNavigationDrawerItemContainer>
<StyledItem
id={navigationItemId}
className={`navigation-drawer-item ${className || ''}`}
onClick={handleMouseDownNavigationClickClick}
onMouseDown={handleMouseDown}
onClick={
mouseUpNavigation ? onClick : handleMouseDownNavigationClickClick
}
onMouseDown={mouseUpNavigation ? undefined : handleMouseDown}
active={active}
aria-selected={active}
isSoon={isSoon}
danger={danger}
soon={soon}
variant={variant}
as={
to
? isExternalLink
? 'a'
: Link
: isDefined(rightOptions)
? 'div'
: undefined
}
role={to ? undefined : isDefined(rightOptions) ? 'button' : undefined}
to={isExternalLink ? undefined : to}
href={isExternalLink ? to : undefined}
target={isExternalLink ? '_blank' : undefined}
rel={isExternalLink ? 'noopener noreferrer' : undefined}
draggable={to && !isExternalLink ? false : undefined}
indentationLevel={indentationLevel}
isNavigationDrawerExpanded={isNavigationDrawerExpanded}
isDragging={isDragging}
hasRightOptions={isDefined(rightOptions)}
isSelectedInEditMode={isSelectedInEditMode}
as={elementType}
role={!to && isDefined(rightOptions) ? 'button' : undefined}
to={isInternalLink ? to : undefined}
href={isExternalLink ? to : undefined}
target={isExternalLink ? '_blank' : undefined}
rel={isExternalLink ? 'noopener noreferrer' : undefined}
draggable={isInternalLink ? false : undefined}
>
<StyledItemElementsContainer>
{showBreadcrumb && (
@@ -396,7 +419,7 @@ export const NavigationDrawerItem = ({
{showStyledSpacer && <StyledSpacer />}
{isSoon && (
{soon && (
<NavigationDrawerAnimatedCollapseWrapper>
<Pill label={t`Soon`} />
</NavigationDrawerAnimatedCollapseWrapper>
@@ -408,25 +431,23 @@ export const NavigationDrawerItem = ({
</NavigationDrawerAnimatedCollapseWrapper>
)}
{isDefined(keyboardKeys) && (
{!!count && (
<NavigationDrawerAnimatedCollapseWrapper>
<StyledItemCount>{count}</StyledItemCount>
</NavigationDrawerAnimatedCollapseWrapper>
)}
{keyboard && (
<NavigationDrawerAnimatedCollapseWrapper>
<StyledKeyBoardShortcut className="keyboard-shortcuts">
<Label>{keyboardKeys}</Label>
<Label>{keyboard}</Label>
</StyledKeyBoardShortcut>
</NavigationDrawerAnimatedCollapseWrapper>
)}
{isDefined(rightOptions) && (
<NavigationDrawerAnimatedCollapseWrapper>
{/* When StyledItem renders as a Link, we need both handlers to
prevent navigation when interacting with rightOptions:
- onMouseDown: stops useMouseDownNavigation from calling navigate()
- onClickCapture: prevents the native <a> follow since the child's
stopPropagation blocks Link's own preventDefault */}
<StyledRightOptionsContainer
onMouseDown={(e) => e.stopPropagation()}
onClickCapture={(e) => e.preventDefault()}
>
<StyledRightOptionsContainer>
<StyledRightOptionsVisbility
data-visible={
isMobile ||
@@ -14,7 +14,10 @@ export const NavigationDrawerSubItem = ({
to,
onClick,
active,
modifier,
danger,
soon,
count,
keyboard,
subItemState,
rightOptions,
isDragging,
@@ -34,7 +37,10 @@ export const NavigationDrawerSubItem = ({
to={to}
onClick={onClick}
active={active}
modifier={modifier}
danger={danger}
soon={soon}
count={count}
keyboard={keyboard}
rightOptions={rightOptions}
isDragging={isDragging}
isSelectedInEditMode={isSelectedInEditMode}
@@ -86,19 +86,24 @@ export const Default: Story = {
label="Notifications"
to="/inbox"
Icon={IconBell}
modifier="soon"
soon={true}
/>
<NavigationDrawerItem
label="Search"
Icon={IconSearch}
modifier={{ keyboard: [`${getOsControlSymbol()}`, 'K'] }}
keyboard={[`${getOsControlSymbol()}`, 'K']}
/>
<NavigationDrawerItem
label="Settings"
to="/settings/profile"
Icon={IconSettings}
/>
<NavigationDrawerItem label="Tasks" to="/tasks" Icon={IconCheckbox} />
<NavigationDrawerItem
label="Tasks"
to="/tasks"
Icon={IconCheckbox}
count={2}
/>
</NavigationDrawerSection>
<NavigationDrawerSection>
@@ -105,14 +105,23 @@ export const NewPill: Story = {
args={{
label: 'New Feature',
Icon: IconSearch,
modifier: 'new',
isNew: true,
}}
/>
<Story
args={{
label: 'Feature with Keyboard Shortcut',
label: 'New Feature with Count',
Icon: IconSearch,
modifier: { keyboard: [getOsControlSymbol(), 'N'] },
isNew: true,
count: 5,
}}
/>
<Story
args={{
label: 'New Feature with Keyboard Shortcut',
Icon: IconSearch,
isNew: true,
keyboard: [getOsControlSymbol(), 'N'],
}}
/>
</StyledContainer>
@@ -186,6 +195,12 @@ export const Catalog: CatalogStory<Story, typeof NavigationDrawerItem> = {
pseudo: { hover: ['.hover'] },
catalog: {
dimensions: [
{
name: 'danger',
values: [true, false],
props: (danger: boolean) => ({ danger }),
labels: (danger: boolean) => (danger ? 'Danger' : 'No Danger'),
},
{
name: 'active',
values: [true, false],
@@ -201,19 +216,23 @@ export const Catalog: CatalogStory<Story, typeof NavigationDrawerItem> = {
},
{
name: 'adornments',
values: ['Without Modifier', 'Soon', 'New', 'Keyboard Keys'],
values: [
'Without Adornments',
'Soon Pill',
'New Pill',
'Count',
'Keyboard Keys',
],
props: (adornmentName: string) =>
adornmentName === 'Soon'
? { modifier: 'soon' }
: adornmentName === 'New'
? { modifier: 'new' }
: adornmentName === 'Keyboard Keys'
? {
modifier: {
keyboard: [getOsControlSymbol(), 'K'],
},
}
: {},
adornmentName === 'Soon Pill'
? { soon: true }
: adornmentName === 'New Pill'
? { isNew: true }
: adornmentName === 'Count'
? { count: 3 }
: adornmentName === 'Keyboard Keys'
? { keyboard: [getOsControlSymbol(), 'K'] }
: {},
},
],
},
@@ -2,7 +2,6 @@ import { useCallback } from 'react';
import { useStore } from 'jotai';
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
import { useOptimisticRemoveNavigationMenuItemsByViewId } from '@/navigation-menu-item/edit/hooks/useOptimisticRemoveNavigationMenuItemsByViewId';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
@@ -46,8 +45,6 @@ export const useDestroyViewFromCurrentState = (viewBarInstanceId?: string) => {
const { changeView } = useChangeView();
const { performViewAPIDestroy } = usePerformViewAPIPersist();
const { removeNavigationMenuItemsByViewIds } =
useOptimisticRemoveNavigationMenuItemsByViewId();
const store = useStore();
@@ -75,13 +72,11 @@ export const useDestroyViewFromCurrentState = (viewBarInstanceId?: string) => {
}
await performViewAPIDestroy({ id: viewPickerReferenceViewId });
removeNavigationMenuItemsByViewIds([viewPickerReferenceViewId]);
}, [
currentView,
closeAndResetViewPicker,
changeView,
performViewAPIDestroy,
removeNavigationMenuItemsByViewIds,
store,
viewPickerIsDirtyCallbackState,
viewPickerIsPersistingCallbackState,
@@ -11,7 +11,6 @@ import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersState';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
@@ -75,9 +74,6 @@ export const CreateProfile = () => {
currentWorkspaceMemberState,
);
const setCurrentUser = useSetAtomState(currentUserState);
const setCurrentWorkspaceMembers = useSetAtomState(
currentWorkspaceMembersState,
);
const { updateOneRecord } = useUpdateOneRecord();
// Form
@@ -132,20 +128,6 @@ export const CreateProfile = () => {
return current;
});
setCurrentWorkspaceMembers((members) =>
members.map((member) =>
member.id === currentWorkspaceMember?.id
? {
...member,
name: {
firstName: data.firstName,
lastName: data.lastName,
},
}
: member,
),
);
setCurrentUser((current) => {
if (isDefined(current)) {
return {
@@ -169,7 +151,6 @@ export const CreateProfile = () => {
setNextOnboardingStatus,
enqueueErrorSnackBar,
setCurrentWorkspaceMember,
setCurrentWorkspaceMembers,
setCurrentUser,
updateOneRecord,
],
@@ -1,20 +1,40 @@
import { SettingsPath } from 'twenty-shared/types';
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CoreObjectNameSingular, SettingsPath } from 'twenty-shared/types';
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { SettingsAccountLoader } from '@/settings/accounts/components/SettingsAccountLoader';
import { SettingsAccountsBlocklistSection } from '@/settings/accounts/components/SettingsAccountsBlocklistSection';
import { SettingsAccountsConnectedAccountsListCard } from '@/settings/accounts/components/SettingsAccountsConnectedAccountsListCard';
import { SettingsAccountsSettingsSection } from '@/settings/accounts/components/SettingsAccountsSettingsSection';
import { useMyConnectedAccounts } from '@/settings/accounts/hooks/useMyConnectedAccounts';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { useLingui } from '@lingui/react/macro';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { getSettingsPath } from 'twenty-shared/utils';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
export const SettingsAccounts = () => {
const { t } = useLingui();
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
const { accounts: allAccounts, loading } = useMyConnectedAccounts();
const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
depth: 1,
shouldOnlyLoadRelationIdentifiers: false,
});
const { records: allAccounts, loading } =
useFindManyRecords<ConnectedAccount>({
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
filter: {
accountOwnerId: {
eq: currentWorkspaceMember?.id,
},
},
recordGqlFields,
});
return (
<SubMenuTopBarContainer
@@ -5,22 +5,14 @@ import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomStat
import { type CalendarChannel } from '@/accounts/types/CalendarChannel';
import { type MessageChannel } from '@/accounts/types/MessageChannel';
import {
CoreObjectNameSingular,
FeatureFlagKey,
SettingsPath,
} from 'twenty-shared/types';
import { CoreObjectNameSingular, SettingsPath } from 'twenty-shared/types';
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { GET_MY_CALENDAR_CHANNELS } from '@/settings/accounts/graphql/queries/getMyCalendarChannels';
import { GET_MY_MESSAGE_CHANNELS } from '@/settings/accounts/graphql/queries/getMyMessageChannels';
import { settingsAccountsSelectedMessageChannelState } from '@/settings/accounts/states/settingsAccountsSelectedMessageChannelState';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useMutation, useQuery } from '@apollo/client/react';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { useMutation } from '@apollo/client/react';
import { StartChannelSyncDocument } from '~/generated-metadata/graphql';
import { SettingsAccountsConfigurationSelectedMessageChannelEffect } from '~/pages/settings/accounts/SettingsAccountsConfigurationSelectedMessageChannelEffect';
import { SettingsAccountsConfigurationStepCalendar } from '~/pages/settings/accounts/SettingsAccountsConfigurationStepCalendar';
import { SettingsAccountsConfigurationStepEmail } from '~/pages/settings/accounts/SettingsAccountsConfigurationStepEmail';
@@ -48,66 +40,38 @@ export const SettingsAccountsConfiguration = () => {
SettingsAccountsConfigurationStep.Email,
);
const featureFlagsMap = useFeatureFlagsMap();
const isMigrated =
featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false;
const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({
objectNameSingular: CoreObjectNameSingular.MessageChannel,
depth: 1,
shouldOnlyLoadRelationIdentifiers: false,
});
const { records: workspaceMessageChannels } =
useFindManyRecords<MessageChannel>({
objectNameSingular: CoreObjectNameSingular.MessageChannel,
filter: {
connectedAccountId: {
eq: connectedAccountId,
},
const { records: messageChannels } = useFindManyRecords<MessageChannel>({
objectNameSingular: CoreObjectNameSingular.MessageChannel,
filter: {
connectedAccountId: {
eq: connectedAccountId,
},
recordGqlFields,
onCompleted: (data) => {
if (isDefined(data[0])) {
setSettingsAccountsSelectedMessageChannel(data[0]);
}
},
skip: !connectedAccountId || isMigrated,
});
const { records: workspaceCalendarChannels } =
useFindManyRecords<CalendarChannel>({
objectNameSingular: CoreObjectNameSingular.CalendarChannel,
filter: {
connectedAccountId: {
eq: connectedAccountId,
},
},
skip: !connectedAccountId || isMigrated,
});
const { data: metadataMessageChannelData } = useQuery<{
myMessageChannels: MessageChannel[];
}>(GET_MY_MESSAGE_CHANNELS, {
variables: { connectedAccountId },
skip: !isMigrated || !connectedAccountId,
},
recordGqlFields,
onCompleted: (data) => {
if (isDefined(data[0])) {
setSettingsAccountsSelectedMessageChannel(data[0]);
}
},
skip: !connectedAccountId,
});
const { data: metadataCalendarChannelData } = useQuery<{
myCalendarChannels: CalendarChannel[];
}>(GET_MY_CALENDAR_CHANNELS, {
variables: { connectedAccountId },
skip: !isMigrated || !connectedAccountId,
const { records: calendarChannels } = useFindManyRecords<CalendarChannel>({
objectNameSingular: CoreObjectNameSingular.CalendarChannel,
filter: {
connectedAccountId: {
eq: connectedAccountId,
},
},
skip: !connectedAccountId,
});
const messageChannels = isMigrated
? (metadataMessageChannelData?.myMessageChannels ?? [])
: workspaceMessageChannels;
const calendarChannels = isMigrated
? (metadataCalendarChannelData?.myCalendarChannels ?? [])
: workspaceCalendarChannels;
const messageChannel = messageChannels[0];
const calendarChannel = calendarChannels[0];
@@ -142,20 +106,13 @@ export const SettingsAccountsConfiguration = () => {
if (showEmailStep) {
return (
<>
{isMigrated && (
<SettingsAccountsConfigurationSelectedMessageChannelEffect
messageChannel={messageChannel as unknown as MessageChannel}
/>
)}
<SettingsAccountsConfigurationStepEmail
messageChannel={messageChannel}
hasNextStep={isDefined(calendarChannel)}
isSubmitting={isSubmitting}
onNext={handleNext}
onAddAccount={handleAddAccount}
/>
</>
<SettingsAccountsConfigurationStepEmail
messageChannel={messageChannel}
hasNextStep={isDefined(calendarChannel)}
isSubmitting={isSubmitting}
onNext={handleNext}
onAddAccount={handleAddAccount}
/>
);
}
@@ -1,25 +0,0 @@
import { type MessageChannel } from '@/accounts/types/MessageChannel';
import { settingsAccountsSelectedMessageChannelState } from '@/settings/accounts/states/settingsAccountsSelectedMessageChannelState';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { useEffect } from 'react';
import { isDefined } from 'twenty-shared/utils';
type SettingsAccountsConfigurationSelectedMessageChannelEffectProps = {
messageChannel: MessageChannel | undefined;
};
export const SettingsAccountsConfigurationSelectedMessageChannelEffect = ({
messageChannel,
}: SettingsAccountsConfigurationSelectedMessageChannelEffectProps) => {
const setSettingsAccountsSelectedMessageChannel = useSetAtomState(
settingsAccountsSelectedMessageChannelState,
);
useEffect(() => {
if (isDefined(messageChannel)) {
setSettingsAccountsSelectedMessageChannel(messageChannel);
}
}, [messageChannel, setSettingsAccountsSelectedMessageChannel]);
return null;
};
@@ -23,12 +23,13 @@ const main = async () => {
);
const serverUrl = process.env.TWENTY_API_URL ?? 'http://localhost:3000';
const token = process.env.TWENTY_API_KEY;
const clientWrapperTemplateSource = await readFile(TEMPLATE_PATH, 'utf-8');
const clientService = new ClientService({
clientWrapperTemplateSource,
serverUrl,
skipAuth: true,
token,
});
await clientService.generateMetadataClient({ outputPath });
@@ -12,14 +12,8 @@ export class ApiClient {
disableInterceptors?: boolean;
serverUrl?: string;
token?: string;
skipAuth?: boolean;
}) {
const {
disableInterceptors = false,
serverUrl,
token,
skipAuth = false,
} = options || {};
const { disableInterceptors = false, serverUrl, token } = options || {};
this.configService = new ConfigService();
this.tokenOverride = token;
this.serverUrlOverride = serverUrl;
@@ -30,7 +24,7 @@ export class ApiClient {
config.baseURL = this.serverUrlOverride ?? twentyConfig.apiUrl;
if (!config.headers.Authorization && !skipAuth) {
if (!config.headers.Authorization) {
const authToken = await this.resolveAuthToken();
if (authToken) {
@@ -10,7 +10,6 @@ type ApiServiceOptions = {
disableInterceptors?: boolean;
serverUrl?: string;
token?: string;
skipAuth?: boolean;
};
export class ApiService {
@@ -79,14 +79,12 @@ export class ClientService {
clientWrapperTemplateSource?: string;
serverUrl?: string;
token?: string;
skipAuth?: boolean;
}) {
this.clientWrapperTemplateSource =
options?.clientWrapperTemplateSource ?? twentyClientTemplateSource;
this.apiService = new ApiService({
disableInterceptors: true,
serverUrl: options?.serverUrl,
skipAuth: true,
token: options?.token,
});
}
@@ -1236,6 +1236,107 @@ enum PageLayoutType {
DASHBOARD
}
type FileWithSignedUrl {
id: UUID!
path: String!
size: Float!
createdAt: DateTime!
url: String!
}
type RecordIdentifier {
id: UUID!
labelIdentifier: String!
imageIdentifier: String
}
type NavigationMenuItem {
id: UUID!
userWorkspaceId: UUID
targetRecordId: UUID
targetObjectMetadataId: UUID
viewId: UUID
type: NavigationMenuItemType!
name: String
link: String
icon: String
color: String
folderId: UUID
position: Float!
applicationId: UUID
createdAt: DateTime!
updatedAt: DateTime!
targetRecordIdentifier: RecordIdentifier
}
enum NavigationMenuItemType {
VIEW
FOLDER
LINK
OBJECT
RECORD
}
type ObjectRecordEventProperties {
updatedFields: [String!]
before: JSON
after: JSON
diff: JSON
}
type MetadataEvent {
type: MetadataEventAction!
metadataName: String!
recordId: String!
properties: ObjectRecordEventProperties!
updatedCollectionHash: String
}
"""Metadata Event Action"""
enum MetadataEventAction {
CREATED
UPDATED
DELETED
}
type ObjectRecordEvent {
action: DatabaseEventAction!
objectNameSingular: String!
recordId: String!
userId: String
workspaceMemberId: String
properties: ObjectRecordEventProperties!
}
"""Database Event Action"""
enum DatabaseEventAction {
CREATED
UPDATED
DELETED
DESTROYED
RESTORED
UPSERTED
}
type ObjectRecordEventWithQueryIds {
queryIds: [String!]!
objectRecordEvent: ObjectRecordEvent!
}
type EventSubscription {
eventStreamId: String!
objectRecordEventsWithQueryIds: [ObjectRecordEventWithQueryIds!]!
metadataEvents: [MetadataEvent!]!
}
type OnDbEvent {
action: DatabaseEventAction!
objectNameSingular: String!
eventDate: DateTime!
record: JSON!
updatedFields: [String!]
}
type Analytics {
"""Boolean that confirms query was dispatched"""
success: Boolean!
@@ -1422,14 +1523,6 @@ type ApprovedAccessDomain {
createdAt: DateTime!
}
type FileWithSignedUrl {
id: UUID!
path: String!
size: Float!
createdAt: DateTime!
url: String!
}
type WorkspaceInvitation {
id: UUID!
email: String!
@@ -1548,9 +1641,7 @@ enum FeatureFlagKey {
IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED
IS_DRAFT_EMAIL_ENABLED
IS_RICH_TEXT_V1_MIGRATED
IS_DIRECT_GRAPHQL_EXECUTION_ENABLED
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED
IS_CONNECTED_ACCOUNT_MIGRATED
}
type SSOIdentityProvider {
@@ -1867,91 +1958,6 @@ type WorkspaceInviteHashValid {
isValid: Boolean!
}
type RecordIdentifier {
id: UUID!
labelIdentifier: String!
imageIdentifier: String
}
type NavigationMenuItem {
id: UUID!
userWorkspaceId: UUID
targetRecordId: UUID
targetObjectMetadataId: UUID
viewId: UUID
type: NavigationMenuItemType!
name: String
link: String
icon: String
color: String
folderId: UUID
position: Float!
applicationId: UUID
createdAt: DateTime!
updatedAt: DateTime!
targetRecordIdentifier: RecordIdentifier
}
enum NavigationMenuItemType {
VIEW
FOLDER
LINK
OBJECT
RECORD
}
type ObjectRecordEventProperties {
updatedFields: [String!]
before: JSON
after: JSON
diff: JSON
}
type MetadataEvent {
type: MetadataEventAction!
metadataName: String!
recordId: String!
properties: ObjectRecordEventProperties!
updatedCollectionHash: String
}
"""Metadata Event Action"""
enum MetadataEventAction {
CREATED
UPDATED
DELETED
}
type ObjectRecordEvent {
action: DatabaseEventAction!
objectNameSingular: String!
recordId: String!
userId: String
workspaceMemberId: String
properties: ObjectRecordEventProperties!
}
"""Database Event Action"""
enum DatabaseEventAction {
CREATED
UPDATED
DELETED
DESTROYED
RESTORED
UPSERTED
}
type ObjectRecordEventWithQueryIds {
queryIds: [String!]!
objectRecordEvent: ObjectRecordEvent!
}
type EventSubscription {
eventStreamId: String!
objectRecordEventsWithQueryIds: [ObjectRecordEventWithQueryIds!]!
metadataEvents: [MetadataEvent!]!
}
type LogicFunctionExecutionResult {
"""Execution result in JSON format"""
data: JSON
@@ -2206,156 +2212,6 @@ type AgentTurn {
createdAt: DateTime!
}
type CalendarChannel {
id: UUID!
handle: String!
syncStatus: CalendarChannelSyncStatus!
syncStage: CalendarChannelSyncStage!
visibility: CalendarChannelVisibility!
isContactAutoCreationEnabled: Boolean!
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy!
isSyncEnabled: Boolean!
syncedAt: DateTime
syncStageStartedAt: DateTime
throttleFailureCount: Float!
connectedAccountId: UUID!
createdAt: DateTime!
updatedAt: DateTime!
}
enum CalendarChannelSyncStatus {
NOT_SYNCED
ONGOING
ACTIVE
FAILED_INSUFFICIENT_PERMISSIONS
FAILED_UNKNOWN
}
enum CalendarChannelSyncStage {
PENDING_CONFIGURATION
CALENDAR_EVENT_LIST_FETCH_PENDING
CALENDAR_EVENT_LIST_FETCH_SCHEDULED
CALENDAR_EVENT_LIST_FETCH_ONGOING
CALENDAR_EVENTS_IMPORT_PENDING
CALENDAR_EVENTS_IMPORT_SCHEDULED
CALENDAR_EVENTS_IMPORT_ONGOING
FAILED
}
enum CalendarChannelVisibility {
METADATA
SHARE_EVERYTHING
}
enum CalendarChannelContactAutoCreationPolicy {
AS_PARTICIPANT_AND_ORGANIZER
AS_PARTICIPANT
AS_ORGANIZER
NONE
}
type ConnectedAccountDTO {
id: UUID!
handle: String!
provider: String!
lastCredentialsRefreshedAt: DateTime
authFailedAt: DateTime
handleAliases: [String!]
scopes: [String!]
lastSignedInAt: DateTime
userWorkspaceId: UUID!
createdAt: DateTime!
updatedAt: DateTime!
}
type MessageChannel {
id: UUID!
visibility: MessageChannelVisibility!
handle: String!
type: MessageChannelType!
isContactAutoCreationEnabled: Boolean!
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy!
messageFolderImportPolicy: MessageFolderImportPolicy!
excludeNonProfessionalEmails: Boolean!
excludeGroupEmails: Boolean!
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction!
isSyncEnabled: Boolean!
syncedAt: DateTime
syncStatus: MessageChannelSyncStatus!
syncStage: MessageChannelSyncStage!
syncStageStartedAt: DateTime
throttleFailureCount: Float!
throttleRetryAfter: DateTime
connectedAccountId: UUID!
createdAt: DateTime!
updatedAt: DateTime!
}
enum MessageChannelVisibility {
METADATA
SUBJECT
SHARE_EVERYTHING
}
enum MessageChannelType {
EMAIL
SMS
}
enum MessageChannelContactAutoCreationPolicy {
SENT_AND_RECEIVED
SENT
NONE
}
enum MessageFolderImportPolicy {
ALL_FOLDERS
SELECTED_FOLDERS
}
enum MessageChannelPendingGroupEmailsAction {
GROUP_EMAILS_DELETION
GROUP_EMAILS_IMPORT
NONE
}
enum MessageChannelSyncStatus {
NOT_SYNCED
ONGOING
ACTIVE
FAILED_INSUFFICIENT_PERMISSIONS
FAILED_UNKNOWN
}
enum MessageChannelSyncStage {
PENDING_CONFIGURATION
MESSAGE_LIST_FETCH_PENDING
MESSAGE_LIST_FETCH_SCHEDULED
MESSAGE_LIST_FETCH_ONGOING
MESSAGES_IMPORT_PENDING
MESSAGES_IMPORT_SCHEDULED
MESSAGES_IMPORT_ONGOING
FAILED
}
type MessageFolder {
id: UUID!
name: String
isSentFolder: Boolean!
isSynced: Boolean!
parentFolderId: UUID
externalId: String
pendingSyncAction: MessageFolderPendingSyncAction!
messageChannelId: UUID!
createdAt: DateTime!
updatedAt: DateTime!
}
enum MessageFolderPendingSyncAction {
FOLDER_DELETION
NONE
}
type CollectionHash {
collectionName: AllMetadataName!
hash: String!
@@ -3162,11 +3018,6 @@ type Query {
findWorkspaceFromInviteHash(inviteHash: String!): Workspace!
validatePasswordResetToken(passwordResetToken: String!): ValidatePasswordResetToken!
getSSOIdentityProviders: [FindAvailableSSOIDP!]!
myMessageFolders(messageChannelId: UUID): [MessageFolder!]!
myMessageChannels(connectedAccountId: UUID): [MessageChannel!]!
myConnectedAccounts: [ConnectedAccountDTO!]!
connectedAccounts: [ConnectedAccountDTO!]!
myCalendarChannels(connectedAccountId: UUID): [CalendarChannel!]!
webhooks: [Webhook!]!
webhook(id: UUID!): Webhook
minimalMetadata: MinimalMetadata!
@@ -3464,11 +3315,6 @@ type Mutation {
createSAMLIdentityProvider(input: SetupSAMLSsoInput!): SetupSso!
deleteSSOIdentityProvider(input: DeleteSsoInput!): DeleteSso!
editSSOIdentityProvider(input: EditSsoInput!): EditSso!
updateMessageFolder(input: UpdateMessageFolderInput!): MessageFolder!
updateMessageFolders(input: UpdateMessageFoldersInput!): [MessageFolder!]!
updateMessageChannel(input: UpdateMessageChannelInput!): MessageChannel!
deleteConnectedAccount(id: UUID!): ConnectedAccountDTO!
updateCalendarChannel(input: UpdateCalendarChannelInput!): CalendarChannel!
createWebhook(input: CreateWebhookInput!): Webhook!
updateWebhook(input: UpdateWebhookInput!): Webhook!
deleteWebhook(id: UUID!): Webhook!
@@ -4365,47 +4211,6 @@ input EditSsoInput {
status: SSOIdentityProviderStatus!
}
input UpdateMessageFolderInput {
id: UUID!
update: UpdateMessageFolderInputUpdates!
}
input UpdateMessageFolderInputUpdates {
isSynced: Boolean
}
input UpdateMessageFoldersInput {
ids: [UUID!]!
update: UpdateMessageFolderInputUpdates!
}
input UpdateMessageChannelInput {
id: UUID!
update: UpdateMessageChannelInputUpdates!
}
input UpdateMessageChannelInputUpdates {
visibility: MessageChannelVisibility
isContactAutoCreationEnabled: Boolean
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy
messageFolderImportPolicy: MessageFolderImportPolicy
isSyncEnabled: Boolean
excludeNonProfessionalEmails: Boolean
excludeGroupEmails: Boolean
}
input UpdateCalendarChannelInput {
id: UUID!
update: UpdateCalendarChannelInputUpdates!
}
input UpdateCalendarChannelInputUpdates {
visibility: CalendarChannelVisibility
isContactAutoCreationEnabled: Boolean
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy
isSyncEnabled: Boolean
}
input CreateWebhookInput {
id: UUID
targetUrl: String!
@@ -4511,10 +4316,17 @@ enum FileFolder {
}
type Subscription {
onDbEvent(input: OnDbEventInput!): OnDbEvent!
onEventSubscription(eventStreamId: String!): EventSubscription
logicFunctionLogs(input: LogicFunctionLogsInput!): LogicFunctionLogs!
}
input OnDbEventInput {
action: DatabaseEventAction
objectNameSingular: String
recordId: UUID
}
input LogicFunctionLogsInput {
applicationId: UUID
applicationUniversalIdentifier: UUID
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-1
View File
@@ -23,6 +23,5 @@ declare module 'express-serve-static-core' {
userWorkspaceId?: string;
authProvider?: AuthProviderEnum | null;
impersonationContext?: RawAuthContext['impersonationContext'];
skipWorkspaceSchemaCreation?: boolean;
}
}

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