Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a35ea3b61 |
@@ -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],
|
||||
);
|
||||
|
||||
+54
-9
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
+14
@@ -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('');
|
||||
});
|
||||
});
|
||||
+11
@@ -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}`;
|
||||
};
|
||||
+18
-4
@@ -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]}
|
||||
|
||||
-35
@@ -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,
|
||||
};
|
||||
};
|
||||
+2
-1
@@ -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 = () => {
|
||||
|
||||
+4
-4
@@ -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('');
|
||||
}
|
||||
};
|
||||
|
||||
+2
-2
@@ -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`);
|
||||
|
||||
+2
-3
@@ -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();
|
||||
|
||||
|
||||
-88
@@ -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 = {};
|
||||
|
||||
|
||||
+3
-3
@@ -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]: {
|
||||
|
||||
+4
-2
@@ -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}
|
||||
|
||||
+15
-25
@@ -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 (
|
||||
|
||||
+35
-2
@@ -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) => ({
|
||||
|
||||
+34
-28
@@ -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 (
|
||||
|
||||
+49
-7
@@ -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
-22
@@ -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 (
|
||||
|
||||
-40
@@ -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;
|
||||
};
|
||||
+17
-4
@@ -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) =>
|
||||
|
||||
-21
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
+4
-10
@@ -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),
|
||||
|
||||
+2
-10
@@ -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));
|
||||
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const DELETE_CONNECTED_ACCOUNT = gql`
|
||||
mutation DeleteConnectedAccount($id: UUID!) {
|
||||
deleteConnectedAccount(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
-12
@@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
-14
@@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPDATE_MESSAGE_FOLDER = gql`
|
||||
mutation UpdateMessageFolder($input: UpdateMessageFolderInput!) {
|
||||
updateMessageFolder(input: $input) {
|
||||
id
|
||||
isSynced
|
||||
}
|
||||
}
|
||||
`;
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPDATE_MESSAGE_FOLDERS = gql`
|
||||
mutation UpdateMessageFolders($input: UpdateMessageFoldersInput!) {
|
||||
updateMessageFolders(input: $input) {
|
||||
id
|
||||
isSynced
|
||||
}
|
||||
}
|
||||
`;
|
||||
-20
@@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
-18
@@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
-24
@@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
-17
@@ -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,
|
||||
};
|
||||
};
|
||||
+2
-36
@@ -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 };
|
||||
|
||||
+4
-2
@@ -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`,
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -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 }) =>
|
||||
|
||||
+74
-53
@@ -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 ||
|
||||
|
||||
+8
-2
@@ -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}
|
||||
|
||||
+8
-3
@@ -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>
|
||||
|
||||
+34
-15
@@ -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'] }
|
||||
: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
-5
@@ -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
|
||||
|
||||
+30
-73
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
-25
@@ -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
@@ -23,6 +23,5 @@ declare module 'express-serve-static-core' {
|
||||
userWorkspaceId?: string;
|
||||
authProvider?: AuthProviderEnum | null;
|
||||
impersonationContext?: RawAuthContext['impersonationContext'];
|
||||
skipWorkspaceSchemaCreation?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
+11
-40
@@ -55,56 +55,27 @@ const FIRST_NON_TS_VECTOR_UNIVERSAL_IDENTIFIER =
|
||||
|
||||
const DUPLICATE_KEY_MESSAGE = 'duplicate key value violates unique constraint';
|
||||
|
||||
const getNestedErrorMessages = (
|
||||
error: WorkspaceMigrationRunnerException,
|
||||
): string | undefined => {
|
||||
const nestedErrors = error.errors;
|
||||
|
||||
if (!isDefined(nestedErrors)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const details = [
|
||||
nestedErrors.metadata && `metadata: ${nestedErrors.metadata.message}`,
|
||||
nestedErrors.workspaceSchema &&
|
||||
`workspaceSchema: ${nestedErrors.workspaceSchema.message}`,
|
||||
nestedErrors.actionTranspilation &&
|
||||
`actionTranspilation: ${nestedErrors.actionTranspilation.message}`,
|
||||
]
|
||||
.filter(isDefined)
|
||||
.join('; ');
|
||||
|
||||
return details || undefined;
|
||||
};
|
||||
|
||||
const isUniqueViolationError = (error: Error): boolean => {
|
||||
if (error.message.includes(DUPLICATE_KEY_MESSAGE)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (error instanceof WorkspaceMigrationRunnerException) {
|
||||
const nestedMessages = getNestedErrorMessages(error);
|
||||
const nestedErrors = [
|
||||
error.errors?.metadata,
|
||||
error.errors?.workspaceSchema,
|
||||
error.errors?.actionTranspilation,
|
||||
];
|
||||
|
||||
return nestedMessages?.includes(DUPLICATE_KEY_MESSAGE) === true;
|
||||
return nestedErrors.some(
|
||||
(nestedError) =>
|
||||
nestedError?.message?.includes(DUPLICATE_KEY_MESSAGE) === true,
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const enrichErrorMessage = (error: Error): Error => {
|
||||
if (!(error instanceof WorkspaceMigrationRunnerException)) {
|
||||
return error;
|
||||
}
|
||||
|
||||
const details = getNestedErrorMessages(error);
|
||||
|
||||
if (isDefined(details)) {
|
||||
error.message = `${error.message} (${details})`;
|
||||
}
|
||||
|
||||
return error;
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-19:add-missing-system-fields-to-standard-objects',
|
||||
description:
|
||||
@@ -152,7 +123,7 @@ export class AddMissingSystemFieldsToStandardObjectsCommand extends ActiveOrSusp
|
||||
});
|
||||
} catch (error) {
|
||||
if (!isUniqueViolationError(error)) {
|
||||
throw enrichErrorMessage(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.warn(
|
||||
@@ -211,7 +182,7 @@ export class AddMissingSystemFieldsToStandardObjectsCommand extends ActiveOrSusp
|
||||
});
|
||||
} catch (error) {
|
||||
if (!isUniqueViolationError(error)) {
|
||||
throw enrichErrorMessage(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.warn(
|
||||
|
||||
-99
@@ -1,99 +0,0 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-20:backfill-select-field-option-ids',
|
||||
description:
|
||||
'Backfill missing ids on SELECT and MULTI_SELECT field metadata options',
|
||||
})
|
||||
export class BackfillSelectFieldOptionIdsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const dryRun = options?.dryRun ?? false;
|
||||
|
||||
const selectFields: { id: string; options: Record<string, unknown>[] }[] =
|
||||
await this.coreDataSource.query(
|
||||
`SELECT "id", "options"
|
||||
FROM core."fieldMetadata"
|
||||
WHERE "workspaceId" = $1
|
||||
AND "type" IN ('SELECT', 'MULTI_SELECT')
|
||||
AND "options" IS NOT NULL`,
|
||||
[workspaceId],
|
||||
);
|
||||
|
||||
const fieldsToUpdate = selectFields.filter((field) =>
|
||||
field.options.some((option) => !isDefined(option.id)),
|
||||
);
|
||||
|
||||
if (fieldsToUpdate.length === 0) {
|
||||
this.logger.log(
|
||||
`No SELECT/MULTI_SELECT options missing ids in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${dryRun ? '[DRY RUN] ' : ''}Found ${fieldsToUpdate.length} field(s) with options missing ids in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
for (const field of fieldsToUpdate) {
|
||||
const patchedOptions = field.options.map((option) => ({
|
||||
...option,
|
||||
id: isDefined(option.id) ? option.id : v4(),
|
||||
}));
|
||||
|
||||
await queryRunner.query(
|
||||
`UPDATE core."fieldMetadata"
|
||||
SET "options" = $1::jsonb
|
||||
WHERE "id" = $2`,
|
||||
[JSON.stringify(patchedOptions), field.id],
|
||||
);
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
this.logger.log(
|
||||
`Backfilled option ids for ${fieldsToUpdate.length} field(s) in workspace ${workspaceId}`,
|
||||
);
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
-91
@@ -1,91 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-20:delete-orphan-navigation-menu-items',
|
||||
description: 'Delete navigation menu items pointing to deleted views',
|
||||
})
|
||||
export class DeleteOrphanNavigationMenuItemsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
protected readonly logger = new Logger(
|
||||
DeleteOrphanNavigationMenuItemsCommand.name,
|
||||
);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
@InjectRepository(NavigationMenuItemEntity)
|
||||
private readonly navigationMenuItemRepository: Repository<NavigationMenuItemEntity>,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const { flatViewMaps, flatNavigationMenuItemMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatViewMaps',
|
||||
'flatNavigationMenuItemMaps',
|
||||
]);
|
||||
|
||||
const activeViewIds = new Set(
|
||||
Object.values(flatViewMaps.byUniversalIdentifier)
|
||||
.filter((view): view is NonNullable<typeof view> => isDefined(view))
|
||||
.filter((view) => view.deletedAt === null)
|
||||
.map((view) => view.id),
|
||||
);
|
||||
|
||||
const orphanViewNavigationMenuItemIds = Object.values(
|
||||
flatNavigationMenuItemMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(
|
||||
(item): item is NonNullable<typeof item> =>
|
||||
isDefined(item) &&
|
||||
item.type === NavigationMenuItemType.VIEW &&
|
||||
isDefined(item.viewId) &&
|
||||
!activeViewIds.has(item.viewId),
|
||||
)
|
||||
.map((item) => item.id);
|
||||
|
||||
if (orphanViewNavigationMenuItemIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would delete ${orphanViewNavigationMenuItemIds.length} orphan navigation menu item(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.navigationMenuItemRepository.delete({
|
||||
workspaceId,
|
||||
id: In(orphanViewNavigationMenuItemIds),
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Deleted ${orphanViewNavigationMenuItemIds.length} orphan navigation menu item(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
await this.workspaceCacheService.flush(workspaceId, [
|
||||
'flatNavigationMenuItemMaps',
|
||||
]);
|
||||
}
|
||||
}
|
||||
-400
@@ -1,400 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { Command } from 'nest-commander';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
|
||||
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-20:migrate-messaging-infrastructure-to-metadata',
|
||||
description:
|
||||
'Backfill connectedAccount, messageChannel, calendarChannel, and messageFolder to core metadata schema',
|
||||
})
|
||||
export class MigrateMessagingInfrastructureToMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
@InjectRepository(ConnectedAccountEntity)
|
||||
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
|
||||
@InjectRepository(MessageChannelEntity)
|
||||
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
|
||||
@InjectRepository(CalendarChannelEntity)
|
||||
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
|
||||
@InjectRepository(MessageFolderEntity)
|
||||
private readonly messageFolderRepository: Repository<MessageFolderEntity>,
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
const isAlreadyMigrated = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (isAlreadyMigrated) {
|
||||
this.logger.log(
|
||||
`IS_CONNECTED_ACCOUNT_MIGRATED already enabled for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const connectedAccountWorkspaceRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
const messageChannelWorkspaceRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
|
||||
const calendarChannelWorkspaceRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
const messageFolderWorkspaceRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<MessageFolderWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageFolder',
|
||||
);
|
||||
|
||||
const connectedAccounts = await connectedAccountWorkspaceRepository.find();
|
||||
const messageChannels = await messageChannelWorkspaceRepository.find();
|
||||
const calendarChannels = await calendarChannelWorkspaceRepository.find();
|
||||
const messageFolders = await messageFolderWorkspaceRepository.find();
|
||||
|
||||
const workspaceMemberIdToUserWorkspaceIdMap =
|
||||
await this.buildWorkspaceMemberIdToUserWorkspaceIdMap(workspaceId);
|
||||
|
||||
const connectedAccountsWithMissingHandle = connectedAccounts.filter(
|
||||
(account) => !account.handle,
|
||||
);
|
||||
const connectedAccountsWithUnresolvedOwner = connectedAccounts.filter(
|
||||
(account) =>
|
||||
!workspaceMemberIdToUserWorkspaceIdMap.has(account.accountOwnerId),
|
||||
);
|
||||
const messageChannelsWithMissingHandle = messageChannels.filter(
|
||||
(channel) => !channel.handle,
|
||||
);
|
||||
const calendarChannelsWithMissingHandle = calendarChannels.filter(
|
||||
(channel) => !channel.handle,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Workspace ${workspaceId}: ` +
|
||||
`${connectedAccounts.length} connected accounts, ` +
|
||||
`${messageChannels.length} message channels, ` +
|
||||
`${calendarChannels.length} calendar channels, ` +
|
||||
`${messageFolders.length} message folders`,
|
||||
);
|
||||
|
||||
if (connectedAccountsWithMissingHandle.length > 0) {
|
||||
this.logger.warn(
|
||||
`[DRY RUN] ${connectedAccountsWithMissingHandle.length} connected accounts have empty handle`,
|
||||
);
|
||||
}
|
||||
|
||||
if (connectedAccountsWithUnresolvedOwner.length > 0) {
|
||||
this.logger.warn(
|
||||
`[DRY RUN] ${connectedAccountsWithUnresolvedOwner.length} connected accounts have unresolvable accountOwnerId (no matching userWorkspace)`,
|
||||
);
|
||||
}
|
||||
|
||||
if (messageChannelsWithMissingHandle.length > 0) {
|
||||
this.logger.warn(
|
||||
`[DRY RUN] ${messageChannelsWithMissingHandle.length} message channels have empty handle`,
|
||||
);
|
||||
}
|
||||
|
||||
if (calendarChannelsWithMissingHandle.length > 0) {
|
||||
this.logger.warn(
|
||||
`[DRY RUN] ${calendarChannelsWithMissingHandle.length} calendar channels have empty handle`,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let migratedConnectedAccountIds = new Set(
|
||||
connectedAccounts.map((account) => account.id),
|
||||
);
|
||||
let migratedMessageChannelIds = new Set(
|
||||
messageChannels.map((channel) => channel.id),
|
||||
);
|
||||
|
||||
if (connectedAccounts.length > 0) {
|
||||
const coreConnectedAccounts = connectedAccounts
|
||||
.filter((workspaceEntity) => {
|
||||
const userWorkspaceId = workspaceMemberIdToUserWorkspaceIdMap.get(
|
||||
workspaceEntity.accountOwnerId,
|
||||
);
|
||||
|
||||
if (!userWorkspaceId) {
|
||||
this.logger.warn(
|
||||
`Skipping connected account ${workspaceEntity.id}: no userWorkspace found for workspaceMember ${workspaceEntity.accountOwnerId}`,
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.map((workspaceEntity) => {
|
||||
const handleAliases = isNonEmptyString(workspaceEntity.handleAliases)
|
||||
? workspaceEntity.handleAliases
|
||||
.split(',')
|
||||
.map((alias) => alias.trim())
|
||||
: null;
|
||||
|
||||
return {
|
||||
id: workspaceEntity.id,
|
||||
handle: workspaceEntity.handle ?? '',
|
||||
provider: workspaceEntity.provider,
|
||||
accessToken: workspaceEntity.accessToken,
|
||||
refreshToken: workspaceEntity.refreshToken,
|
||||
lastCredentialsRefreshedAt:
|
||||
workspaceEntity.lastCredentialsRefreshedAt,
|
||||
authFailedAt: workspaceEntity.authFailedAt,
|
||||
handleAliases,
|
||||
scopes: workspaceEntity.scopes,
|
||||
connectionParameters:
|
||||
workspaceEntity.connectionParameters as Record<
|
||||
string,
|
||||
unknown
|
||||
> | null,
|
||||
userWorkspaceId: workspaceMemberIdToUserWorkspaceIdMap.get(
|
||||
workspaceEntity.accountOwnerId,
|
||||
)!,
|
||||
workspaceId,
|
||||
createdAt: workspaceEntity.createdAt,
|
||||
updatedAt: workspaceEntity.updatedAt,
|
||||
};
|
||||
});
|
||||
|
||||
if (coreConnectedAccounts.length > 0) {
|
||||
await this.connectedAccountRepository.save(
|
||||
coreConnectedAccounts as unknown as ConnectedAccountEntity[],
|
||||
);
|
||||
this.logger.log(
|
||||
`Migrated ${coreConnectedAccounts.length} connected accounts for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
migratedConnectedAccountIds = new Set(
|
||||
coreConnectedAccounts.map((account) => account.id),
|
||||
);
|
||||
}
|
||||
|
||||
if (messageChannels.length > 0) {
|
||||
const coreMessageChannels = messageChannels
|
||||
.filter((workspaceEntity) =>
|
||||
migratedConnectedAccountIds.has(workspaceEntity.connectedAccountId),
|
||||
)
|
||||
.map((workspaceEntity) => ({
|
||||
id: workspaceEntity.id,
|
||||
visibility: workspaceEntity.visibility,
|
||||
handle: workspaceEntity.handle ?? '',
|
||||
type: workspaceEntity.type,
|
||||
isContactAutoCreationEnabled:
|
||||
workspaceEntity.isContactAutoCreationEnabled,
|
||||
contactAutoCreationPolicy: workspaceEntity.contactAutoCreationPolicy,
|
||||
messageFolderImportPolicy: workspaceEntity.messageFolderImportPolicy,
|
||||
excludeNonProfessionalEmails:
|
||||
workspaceEntity.excludeNonProfessionalEmails,
|
||||
excludeGroupEmails: workspaceEntity.excludeGroupEmails,
|
||||
pendingGroupEmailsAction: workspaceEntity.pendingGroupEmailsAction,
|
||||
isSyncEnabled: workspaceEntity.isSyncEnabled,
|
||||
syncCursor: workspaceEntity.syncCursor,
|
||||
syncedAt: workspaceEntity.syncedAt
|
||||
? new Date(workspaceEntity.syncedAt)
|
||||
: null,
|
||||
syncStatus: workspaceEntity.syncStatus ?? 'NOT_SYNCED',
|
||||
syncStage: workspaceEntity.syncStage,
|
||||
syncStageStartedAt: workspaceEntity.syncStageStartedAt
|
||||
? new Date(workspaceEntity.syncStageStartedAt)
|
||||
: null,
|
||||
throttleFailureCount: workspaceEntity.throttleFailureCount,
|
||||
throttleRetryAfter: workspaceEntity.throttleRetryAfter
|
||||
? new Date(workspaceEntity.throttleRetryAfter)
|
||||
: null,
|
||||
connectedAccountId: workspaceEntity.connectedAccountId,
|
||||
workspaceId,
|
||||
createdAt: workspaceEntity.createdAt,
|
||||
updatedAt: workspaceEntity.updatedAt,
|
||||
}));
|
||||
|
||||
if (coreMessageChannels.length > 0) {
|
||||
await this.messageChannelRepository.save(
|
||||
coreMessageChannels as unknown as MessageChannelEntity[],
|
||||
);
|
||||
this.logger.log(
|
||||
`Migrated ${coreMessageChannels.length} message channels for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
migratedMessageChannelIds = new Set(
|
||||
coreMessageChannels.map((channel) => channel.id),
|
||||
);
|
||||
}
|
||||
|
||||
if (calendarChannels.length > 0) {
|
||||
const coreCalendarChannels = calendarChannels
|
||||
.filter((workspaceEntity) =>
|
||||
migratedConnectedAccountIds.has(workspaceEntity.connectedAccountId),
|
||||
)
|
||||
.map((workspaceEntity) => ({
|
||||
id: workspaceEntity.id,
|
||||
handle: workspaceEntity.handle ?? '',
|
||||
syncStatus: workspaceEntity.syncStatus ?? 'NOT_SYNCED',
|
||||
syncStage: workspaceEntity.syncStage,
|
||||
visibility: workspaceEntity.visibility,
|
||||
isContactAutoCreationEnabled:
|
||||
workspaceEntity.isContactAutoCreationEnabled,
|
||||
contactAutoCreationPolicy: workspaceEntity.contactAutoCreationPolicy,
|
||||
isSyncEnabled: workspaceEntity.isSyncEnabled,
|
||||
syncCursor: workspaceEntity.syncCursor,
|
||||
syncedAt: workspaceEntity.syncedAt
|
||||
? new Date(workspaceEntity.syncedAt)
|
||||
: null,
|
||||
syncStageStartedAt: workspaceEntity.syncStageStartedAt
|
||||
? new Date(workspaceEntity.syncStageStartedAt)
|
||||
: null,
|
||||
throttleFailureCount: workspaceEntity.throttleFailureCount,
|
||||
connectedAccountId: workspaceEntity.connectedAccountId,
|
||||
workspaceId,
|
||||
createdAt: workspaceEntity.createdAt,
|
||||
updatedAt: workspaceEntity.updatedAt,
|
||||
}));
|
||||
|
||||
await this.calendarChannelRepository.save(
|
||||
coreCalendarChannels as unknown as CalendarChannelEntity[],
|
||||
);
|
||||
this.logger.log(
|
||||
`Migrated ${coreCalendarChannels.length} calendar channels for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (messageFolders.length > 0) {
|
||||
const externalIdToFolderIdMap = new Map(
|
||||
messageFolders.map((folder) => [folder.externalId, folder.id]),
|
||||
);
|
||||
|
||||
const coreMessageFolders = messageFolders
|
||||
.filter((workspaceEntity) =>
|
||||
migratedMessageChannelIds.has(workspaceEntity.messageChannelId),
|
||||
)
|
||||
.map((workspaceEntity) => {
|
||||
let resolvedParentFolderId: string | null = null;
|
||||
|
||||
if (isNonEmptyString(workspaceEntity.parentFolderId)) {
|
||||
resolvedParentFolderId =
|
||||
externalIdToFolderIdMap.get(workspaceEntity.parentFolderId) ??
|
||||
null;
|
||||
|
||||
if (!resolvedParentFolderId) {
|
||||
this.logger.warn(
|
||||
`Message folder ${workspaceEntity.id}: could not resolve parentFolderId externalId "${workspaceEntity.parentFolderId}" to a UUID`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: workspaceEntity.id,
|
||||
name: workspaceEntity.name,
|
||||
syncCursor: workspaceEntity.syncCursor,
|
||||
isSentFolder: workspaceEntity.isSentFolder,
|
||||
isSynced: workspaceEntity.isSynced,
|
||||
parentFolderId: resolvedParentFolderId,
|
||||
externalId: workspaceEntity.externalId,
|
||||
pendingSyncAction: workspaceEntity.pendingSyncAction,
|
||||
messageChannelId: workspaceEntity.messageChannelId,
|
||||
workspaceId,
|
||||
createdAt: workspaceEntity.createdAt,
|
||||
updatedAt: workspaceEntity.updatedAt,
|
||||
};
|
||||
});
|
||||
|
||||
await this.messageFolderRepository.save(
|
||||
coreMessageFolders as unknown as MessageFolderEntity[],
|
||||
);
|
||||
this.logger.log(
|
||||
`Migrated ${coreMessageFolders.length} message folders for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.featureFlagService.enableFeatureFlags(
|
||||
[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Enabled IS_CONNECTED_ACCOUNT_MIGRATED for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async buildWorkspaceMemberIdToUserWorkspaceIdMap(
|
||||
workspaceId: string,
|
||||
): Promise<Map<string, string>> {
|
||||
const workspaceMemberRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
);
|
||||
|
||||
const workspaceMembers = await workspaceMemberRepository.find();
|
||||
const userWorkspaces = await this.userWorkspaceRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'userId'],
|
||||
});
|
||||
|
||||
const userWorkspaceIdByUserId = new Map(
|
||||
userWorkspaces.map((userWorkspace) => [
|
||||
userWorkspace.userId,
|
||||
userWorkspace.id,
|
||||
]),
|
||||
);
|
||||
|
||||
return new Map(
|
||||
workspaceMembers
|
||||
.filter((member) => userWorkspaceIdByUserId.has(member.userId))
|
||||
.map((member) => [
|
||||
member.id,
|
||||
userWorkspaceIdByUserId.get(member.userId)!,
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-24
@@ -4,26 +4,17 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { BackfillCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command';
|
||||
import { BackfillNavigationMenuItemTypeCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-navigation-menu-item-type.command';
|
||||
import { BackfillPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts.command';
|
||||
import { BackfillSelectFieldOptionIdsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-select-field-option-ids.command';
|
||||
import { DeleteOrphanNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-delete-orphan-navigation-menu-items.command';
|
||||
import { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-object-permission-metadata.command';
|
||||
import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command';
|
||||
import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MigrateMessagingInfrastructureToMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command';
|
||||
import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command';
|
||||
import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command';
|
||||
import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
|
||||
import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
@@ -32,15 +23,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
WorkspaceEntity,
|
||||
ConnectedAccountEntity,
|
||||
MessageChannelEntity,
|
||||
CalendarChannelEntity,
|
||||
MessageFolderEntity,
|
||||
UserWorkspaceEntity,
|
||||
NavigationMenuItemEntity,
|
||||
]),
|
||||
TypeOrmModule.forFeature([WorkspaceEntity]),
|
||||
DataSourceModule,
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
@@ -59,11 +42,8 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
BackfillCommandMenuItemsCommand,
|
||||
BackfillNavigationMenuItemTypeCommand,
|
||||
BackfillPageLayoutsCommand,
|
||||
BackfillSelectFieldOptionIdsCommand,
|
||||
DeleteOrphanNavigationMenuItemsCommand,
|
||||
SeedCliApplicationRegistrationCommand,
|
||||
MigrateRichTextToTextCommand,
|
||||
MigrateMessagingInfrastructureToMetadataCommand,
|
||||
],
|
||||
exports: [
|
||||
IdentifyPermissionFlagMetadataCommand,
|
||||
@@ -73,11 +53,8 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
BackfillCommandMenuItemsCommand,
|
||||
BackfillNavigationMenuItemTypeCommand,
|
||||
BackfillPageLayoutsCommand,
|
||||
BackfillSelectFieldOptionIdsCommand,
|
||||
DeleteOrphanNavigationMenuItemsCommand,
|
||||
SeedCliApplicationRegistrationCommand,
|
||||
MigrateRichTextToTextCommand,
|
||||
MigrateMessagingInfrastructureToMetadataCommand,
|
||||
],
|
||||
})
|
||||
export class V1_20_UpgradeVersionCommandModule {}
|
||||
|
||||
-9
@@ -35,14 +35,11 @@ import { FixInvalidStandardUniversalIdentifiersCommand } from 'src/database/comm
|
||||
import { SeedServerIdCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-seed-server-id.command';
|
||||
import { BackfillCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command';
|
||||
import { BackfillNavigationMenuItemTypeCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-navigation-menu-item-type.command';
|
||||
import { DeleteOrphanNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-delete-orphan-navigation-menu-items.command';
|
||||
import { BackfillPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts.command';
|
||||
import { BackfillSelectFieldOptionIdsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-select-field-option-ids.command';
|
||||
import { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-object-permission-metadata.command';
|
||||
import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command';
|
||||
import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MigrateMessagingInfrastructureToMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command';
|
||||
import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command';
|
||||
import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
@@ -101,12 +98,9 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
protected readonly makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
protected readonly backfillNavigationMenuItemTypeCommand: BackfillNavigationMenuItemTypeCommand,
|
||||
protected readonly backfillCommandMenuItemsCommand: BackfillCommandMenuItemsCommand,
|
||||
protected readonly deleteOrphanNavigationMenuItemsCommand: DeleteOrphanNavigationMenuItemsCommand,
|
||||
protected readonly backfillPageLayoutsCommand: BackfillPageLayoutsCommand,
|
||||
protected readonly seedCliApplicationRegistrationCommand: SeedCliApplicationRegistrationCommand,
|
||||
protected readonly migrateRichTextToTextCommand: MigrateRichTextToTextCommand,
|
||||
protected readonly migrateMessagingInfrastructureToMetadataCommand: MigrateMessagingInfrastructureToMetadataCommand,
|
||||
protected readonly backfillSelectFieldOptionIdsCommand: BackfillSelectFieldOptionIdsCommand,
|
||||
) {
|
||||
super(
|
||||
workspaceRepository,
|
||||
@@ -162,12 +156,9 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
.makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
this.backfillNavigationMenuItemTypeCommand,
|
||||
this.migrateRichTextToTextCommand,
|
||||
this.deleteOrphanNavigationMenuItemsCommand,
|
||||
this.backfillCommandMenuItemsCommand,
|
||||
this.backfillPageLayoutsCommand,
|
||||
this.seedCliApplicationRegistrationCommand,
|
||||
this.migrateMessagingInfrastructureToMetadataCommand,
|
||||
this.backfillSelectFieldOptionIdsCommand,
|
||||
];
|
||||
|
||||
this.allCommands = {
|
||||
|
||||
-139
@@ -1,139 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddMessagingInfrastructureMetadataEntities1773945207801
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddMessagingInfrastructureMetadataEntities1773945207801';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "core"."connectedAccount" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "handle" character varying NOT NULL, "provider" character varying NOT NULL, "accessToken" character varying, "refreshToken" character varying, "lastCredentialsRefreshedAt" TIMESTAMP WITH TIME ZONE, "authFailedAt" TIMESTAMP WITH TIME ZONE, "handleAliases" character varying array, "scopes" character varying array, "connectionParameters" jsonb, "lastSignedInAt" TIMESTAMP WITH TIME ZONE, "oidcTokenClaims" jsonb, "userWorkspaceId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_8e7a0a0bbc2e06ac2acf89b7f3a" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."messageChannel_visibility_enum" AS ENUM('METADATA', 'SUBJECT', 'SHARE_EVERYTHING')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."messageChannel_type_enum" AS ENUM('EMAIL', 'SMS')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."messageChannel_contactautocreationpolicy_enum" AS ENUM('SENT_AND_RECEIVED', 'SENT', 'NONE')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."messageChannel_messagefolderimportpolicy_enum" AS ENUM('ALL_FOLDERS', 'SELECTED_FOLDERS')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."messageChannel_pendinggroupemailsaction_enum" AS ENUM('GROUP_EMAILS_DELETION', 'GROUP_EMAILS_IMPORT', 'NONE')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."messageChannel_syncstatus_enum" AS ENUM('NOT_SYNCED', 'ONGOING', 'ACTIVE', 'FAILED_INSUFFICIENT_PERMISSIONS', 'FAILED_UNKNOWN')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."messageChannel_syncstage_enum" AS ENUM('PENDING_CONFIGURATION', 'MESSAGE_LIST_FETCH_PENDING', 'MESSAGE_LIST_FETCH_SCHEDULED', 'MESSAGE_LIST_FETCH_ONGOING', 'MESSAGES_IMPORT_PENDING', 'MESSAGES_IMPORT_SCHEDULED', 'MESSAGES_IMPORT_ONGOING', 'FAILED')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "core"."messageChannel" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "visibility" "core"."messageChannel_visibility_enum" NOT NULL, "handle" character varying NOT NULL, "type" "core"."messageChannel_type_enum" NOT NULL, "isContactAutoCreationEnabled" boolean NOT NULL DEFAULT true, "contactAutoCreationPolicy" "core"."messageChannel_contactautocreationpolicy_enum" NOT NULL DEFAULT 'SENT', "messageFolderImportPolicy" "core"."messageChannel_messagefolderimportpolicy_enum" NOT NULL DEFAULT 'ALL_FOLDERS', "excludeNonProfessionalEmails" boolean NOT NULL DEFAULT true, "excludeGroupEmails" boolean NOT NULL DEFAULT true, "pendingGroupEmailsAction" "core"."messageChannel_pendinggroupemailsaction_enum" NOT NULL, "isSyncEnabled" boolean NOT NULL DEFAULT true, "syncCursor" character varying, "syncedAt" TIMESTAMP WITH TIME ZONE, "syncStatus" "core"."messageChannel_syncstatus_enum" NOT NULL DEFAULT 'NOT_SYNCED', "syncStage" "core"."messageChannel_syncstage_enum" NOT NULL, "syncStageStartedAt" TIMESTAMP WITH TIME ZONE, "throttleFailureCount" integer NOT NULL DEFAULT '0', "throttleRetryAfter" TIMESTAMP WITH TIME ZONE, "connectedAccountId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_438b9412475f39712ed065f77af" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."messageFolder_pendingsyncaction_enum" AS ENUM('FOLDER_DELETION', 'NONE')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "core"."messageFolder" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying, "syncCursor" character varying, "isSentFolder" boolean NOT NULL, "isSynced" boolean NOT NULL, "parentFolderId" uuid, "externalId" character varying, "pendingSyncAction" "core"."messageFolder_pendingsyncaction_enum" NOT NULL DEFAULT 'NONE', "messageChannelId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_85cb5a339d9f7f1106dde9e4db8" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."calendarChannel_syncstatus_enum" AS ENUM('NOT_SYNCED', 'ONGOING', 'ACTIVE', 'FAILED_INSUFFICIENT_PERMISSIONS', 'FAILED_UNKNOWN')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."calendarChannel_syncstage_enum" AS ENUM('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')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."calendarChannel_visibility_enum" AS ENUM('METADATA', 'SHARE_EVERYTHING')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."calendarChannel_contactautocreationpolicy_enum" AS ENUM('AS_PARTICIPANT_AND_ORGANIZER', 'AS_PARTICIPANT', 'AS_ORGANIZER', 'NONE')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "core"."calendarChannel" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "handle" character varying NOT NULL, "syncStatus" "core"."calendarChannel_syncstatus_enum" NOT NULL DEFAULT 'NOT_SYNCED', "syncStage" "core"."calendarChannel_syncstage_enum" NOT NULL, "visibility" "core"."calendarChannel_visibility_enum" NOT NULL, "isContactAutoCreationEnabled" boolean NOT NULL DEFAULT true, "contactAutoCreationPolicy" "core"."calendarChannel_contactautocreationpolicy_enum" NOT NULL DEFAULT 'AS_PARTICIPANT_AND_ORGANIZER', "isSyncEnabled" boolean NOT NULL DEFAULT true, "syncCursor" character varying, "syncedAt" TIMESTAMP WITH TIME ZONE, "syncStageStartedAt" TIMESTAMP WITH TIME ZONE, "throttleFailureCount" integer NOT NULL DEFAULT '0', "connectedAccountId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_796d701c0c35518517d0f3e0e0b" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."connectedAccount" ADD CONSTRAINT "FK_1c7af038a011e99c27044793c6a" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."messageChannel" ADD CONSTRAINT "FK_22d9a21a23fdd99295dc0efc177" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."messageChannel" ADD CONSTRAINT "FK_2e966cbb240771c67630d52895c" FOREIGN KEY ("connectedAccountId") REFERENCES "core"."connectedAccount"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."messageFolder" ADD CONSTRAINT "FK_e7fb85af997d06d8f7cc7512801" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."messageFolder" ADD CONSTRAINT "FK_4237a2fe8a6583354f807c2f8fe" FOREIGN KEY ("messageChannelId") REFERENCES "core"."messageChannel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."calendarChannel" ADD CONSTRAINT "FK_bb5ebadf91b73c8050fb0a092fa" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."calendarChannel" ADD CONSTRAINT "FK_c7bc368c97a18a072413d67cf45" FOREIGN KEY ("connectedAccountId") REFERENCES "core"."connectedAccount"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."calendarChannel" DROP CONSTRAINT "FK_c7bc368c97a18a072413d67cf45"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."calendarChannel" DROP CONSTRAINT "FK_bb5ebadf91b73c8050fb0a092fa"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."messageFolder" DROP CONSTRAINT "FK_4237a2fe8a6583354f807c2f8fe"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."messageFolder" DROP CONSTRAINT "FK_e7fb85af997d06d8f7cc7512801"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."messageChannel" DROP CONSTRAINT "FK_2e966cbb240771c67630d52895c"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."messageChannel" DROP CONSTRAINT "FK_22d9a21a23fdd99295dc0efc177"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."connectedAccount" DROP CONSTRAINT "FK_1c7af038a011e99c27044793c6a"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "core"."calendarChannel"`);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."calendarChannel_contactautocreationpolicy_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."calendarChannel_visibility_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."calendarChannel_syncstage_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."calendarChannel_syncstatus_enum"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "core"."messageFolder"`);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."messageFolder_pendingsyncaction_enum"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "core"."messageChannel"`);
|
||||
await queryRunner.query(`DROP TYPE "core"."messageChannel_syncstage_enum"`);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."messageChannel_syncstatus_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."messageChannel_pendinggroupemailsaction_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."messageChannel_messagefolderimportpolicy_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."messageChannel_contactautocreationpolicy_enum"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TYPE "core"."messageChannel_type_enum"`);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."messageChannel_visibility_enum"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "core"."connectedAccount"`);
|
||||
}
|
||||
}
|
||||
+1
-123
@@ -1,6 +1,6 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { DataArgProcessorService } from 'src/engine/api/common/common-args-processors/data-arg-processor/data-arg-processor.service';
|
||||
import { type SystemWorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
|
||||
@@ -116,118 +116,6 @@ describe('DataArgProcessorService', () => {
|
||||
expect(dataArgProcessorService).toBeDefined();
|
||||
});
|
||||
|
||||
it('should normalize relation connect where composite values', async () => {
|
||||
const flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata> = {
|
||||
byUniversalIdentifier: {
|
||||
'company-universal-id': {
|
||||
id: 'company-id',
|
||||
name: 'company',
|
||||
type: FieldMetadataType.RELATION,
|
||||
isNullable: true,
|
||||
objectMetadataId: 'object-id',
|
||||
universalIdentifier: 'company-universal-id',
|
||||
relationTargetObjectMetadataId: 'target-company-object-id',
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
joinColumnName: 'companyId',
|
||||
},
|
||||
} as FlatFieldMetadata,
|
||||
'emails-universal-id': {
|
||||
id: 'emails-id',
|
||||
name: 'emails',
|
||||
type: FieldMetadataType.EMAILS,
|
||||
isNullable: true,
|
||||
objectMetadataId: 'target-company-object-id',
|
||||
universalIdentifier: 'emails-universal-id',
|
||||
} as FlatFieldMetadata,
|
||||
'domainName-universal-id': {
|
||||
id: 'domainName-id',
|
||||
name: 'domainName',
|
||||
type: FieldMetadataType.LINKS,
|
||||
isNullable: true,
|
||||
objectMetadataId: 'target-company-object-id',
|
||||
universalIdentifier: 'domainName-universal-id',
|
||||
} as FlatFieldMetadata,
|
||||
},
|
||||
universalIdentifierById: {
|
||||
'company-id': 'company-universal-id',
|
||||
'emails-id': 'emails-universal-id',
|
||||
'domainName-id': 'domainName-universal-id',
|
||||
},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
};
|
||||
|
||||
const flatObjectMetadata = {
|
||||
id: 'object-id',
|
||||
nameSingular: 'testObject',
|
||||
namePlural: 'testObjects',
|
||||
isCustom: false,
|
||||
fieldIds: ['company-id'],
|
||||
universalIdentifier: 'test-object-universal-id',
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
imageIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
} as FlatObjectMetadata;
|
||||
|
||||
const flatObjectMetadataMaps = {
|
||||
byUniversalIdentifier: {
|
||||
'target-company-universal-id': {
|
||||
id: 'target-company-object-id',
|
||||
nameSingular: 'company',
|
||||
namePlural: 'companies',
|
||||
isCustom: false,
|
||||
fieldIds: ['emails-id', 'domainName-id'],
|
||||
universalIdentifier: 'target-company-universal-id',
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
imageIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
} as FlatObjectMetadata,
|
||||
},
|
||||
universalIdentifierById: {
|
||||
'target-company-object-id': 'target-company-universal-id',
|
||||
},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
};
|
||||
|
||||
const result = await dataArgProcessorService.process({
|
||||
partialRecordInputs: [
|
||||
{
|
||||
company: {
|
||||
connect: {
|
||||
where: {
|
||||
emails: {
|
||||
primaryEmail: 'User@Example.COM',
|
||||
},
|
||||
domainName: {
|
||||
primaryLinkUrl: 'HTTPS://Example.COM/path/',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
authContext: createMockAuthContext(),
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
company: {
|
||||
connect: {
|
||||
where: {
|
||||
emails: {
|
||||
primaryEmail: 'user@example.com',
|
||||
},
|
||||
domainName: {
|
||||
primaryLinkUrl: 'https://example.com/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('failing inputs validation', () => {
|
||||
const fieldMetadataTypesToTest = Object.keys(
|
||||
failingInputsByFieldMetadataType,
|
||||
@@ -258,11 +146,6 @@ describe('DataArgProcessorService', () => {
|
||||
authContext: createMockAuthContext(),
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps: {
|
||||
byUniversalIdentifier: {},
|
||||
universalIdentifierById: {},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
},
|
||||
}),
|
||||
).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
@@ -300,11 +183,6 @@ describe('DataArgProcessorService', () => {
|
||||
authContext: createMockAuthContext(),
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps: {
|
||||
byUniversalIdentifier: {},
|
||||
universalIdentifierById: {},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
|
||||
+4
-114
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isNull, isObject, isUndefined } from '@sniptt/guards';
|
||||
import { isNull, isUndefined } from '@sniptt/guards';
|
||||
import {
|
||||
FieldMetadataSettingsMapping,
|
||||
FieldMetadataType,
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { transformActorField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-actor-field.util';
|
||||
import { isRelationNestedOperation } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-relation-nested-operation.util';
|
||||
import { transformAddressField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-address-field.util';
|
||||
import { transformArrayField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-array-field.util';
|
||||
import { transformCurrencyField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-currency-field.util';
|
||||
@@ -22,7 +23,6 @@ import { transformFullNameField } from 'src/engine/api/common/common-args-proces
|
||||
import { transformNumericField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-numeric-field.util';
|
||||
import { transformRawJsonField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-raw-json-field.util';
|
||||
import { transformTextField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-text-field.util';
|
||||
import { isRelationNestedOperation } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-relation-nested-operation.util';
|
||||
import { validateActorFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-actor-field-or-throw.util';
|
||||
import { validateAddressFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-address-field-or-throw.util';
|
||||
import { validateArrayFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-array-field-or-throw.util';
|
||||
@@ -56,12 +56,12 @@ import { transformLinksValue } from 'src/engine/core-modules/record-transformer/
|
||||
import { transformPhonesValue } from 'src/engine/core-modules/record-transformer/utils/transform-phones-value.util';
|
||||
import { transformRichTextValue } from 'src/engine/core-modules/record-transformer/utils/transform-rich-text.util';
|
||||
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
|
||||
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
|
||||
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
|
||||
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
|
||||
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
|
||||
import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
|
||||
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
@Injectable()
|
||||
@@ -73,14 +73,12 @@ export class DataArgProcessorService {
|
||||
authContext,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
shouldBackfillPositionIfUndefined = true,
|
||||
}: {
|
||||
partialRecordInputs: Partial<ObjectRecord>[] | undefined;
|
||||
authContext: WorkspaceAuthContext;
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
shouldBackfillPositionIfUndefined?: boolean;
|
||||
}): Promise<Partial<ObjectRecord>[]> {
|
||||
if (!isDefined(partialRecordInputs)) {
|
||||
@@ -160,8 +158,6 @@ export class DataArgProcessorService {
|
||||
fieldMetadata,
|
||||
key,
|
||||
value,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
);
|
||||
}
|
||||
processedRecords.push(processedRecord);
|
||||
@@ -174,8 +170,6 @@ export class DataArgProcessorService {
|
||||
fieldMetadata: FlatFieldMetadata,
|
||||
key: string,
|
||||
value: unknown,
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
|
||||
): Promise<unknown> {
|
||||
switch (fieldMetadata.type) {
|
||||
case FieldMetadataType.POSITION:
|
||||
@@ -266,29 +260,6 @@ export class DataArgProcessorService {
|
||||
);
|
||||
}
|
||||
|
||||
const connectOperation = value as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>;
|
||||
const connectWhere = connectOperation.connect?.where;
|
||||
|
||||
if (isObject(connectWhere)) {
|
||||
const processedWhere = await this.processConnectWhere(
|
||||
connectWhere as Record<string, unknown>,
|
||||
fieldMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
);
|
||||
|
||||
return {
|
||||
...connectOperation,
|
||||
connect: {
|
||||
...connectOperation.connect,
|
||||
where: processedWhere,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
case FieldMetadataType.PHONES: {
|
||||
@@ -354,85 +325,4 @@ export class DataArgProcessorService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async processConnectWhere(
|
||||
connectWhere: Record<string, unknown>,
|
||||
relationFieldMetadata: FlatFieldMetadata,
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
if (!isDefined(relationFieldMetadata.relationTargetObjectMetadataId)) {
|
||||
throw new CommonQueryRunnerException(
|
||||
`Relation target object metadata id not found for field ${relationFieldMetadata.name}`,
|
||||
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
|
||||
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
|
||||
);
|
||||
}
|
||||
|
||||
const targetObjectMetadata =
|
||||
findFlatEntityByIdInFlatEntityMaps<FlatObjectMetadata>({
|
||||
flatEntityId: relationFieldMetadata.relationTargetObjectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(targetObjectMetadata)) {
|
||||
throw new CommonQueryRunnerException(
|
||||
`Relation target object metadata not found for field ${relationFieldMetadata.name}`,
|
||||
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
|
||||
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
|
||||
);
|
||||
}
|
||||
|
||||
const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata(
|
||||
flatFieldMetadataMaps,
|
||||
targetObjectMetadata,
|
||||
);
|
||||
|
||||
const processedWhere: Record<string, unknown> = {};
|
||||
|
||||
for (const [whereKey, whereValue] of Object.entries(connectWhere)) {
|
||||
const fieldId = fieldIdByName[whereKey];
|
||||
|
||||
if (!isDefined(fieldId)) {
|
||||
processedWhere[whereKey] = whereValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
const whereFieldMetadata =
|
||||
findFlatEntityByIdInFlatEntityMaps<FlatFieldMetadata>({
|
||||
flatEntityId: fieldId,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(whereFieldMetadata)) {
|
||||
processedWhere[whereKey] = whereValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const processedValue = await this.processField(
|
||||
whereFieldMetadata,
|
||||
whereKey,
|
||||
whereValue,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
);
|
||||
|
||||
// Only keep original keys — processField may add null subfields that alter WHERE semantics
|
||||
if (isObject(whereValue) && isObject(processedValue)) {
|
||||
const originalKeys = new Set(Object.keys(whereValue));
|
||||
|
||||
processedWhere[whereKey] = Object.fromEntries(
|
||||
Object.entries(processedValue).filter(([k]) => originalKeys.has(k)),
|
||||
);
|
||||
} else {
|
||||
processedWhere[whereKey] = processedValue;
|
||||
}
|
||||
} catch {
|
||||
processedWhere[whereKey] = whereValue;
|
||||
}
|
||||
}
|
||||
|
||||
return processedWhere;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-7
@@ -145,12 +145,8 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
args: CommonInput<CreateManyQueryArgs>,
|
||||
queryRunnerContext: CommonBaseQueryRunnerContext,
|
||||
): Promise<CommonInput<CreateManyQueryArgs>> {
|
||||
const {
|
||||
authContext,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
} = queryRunnerContext;
|
||||
const { authContext, flatObjectMetadata, flatFieldMetadataMaps } =
|
||||
queryRunnerContext;
|
||||
|
||||
return {
|
||||
...args,
|
||||
@@ -159,7 +155,6 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
authContext,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
+3
-8
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type ObjectRecord } from 'twenty-shared/types';
|
||||
|
||||
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
|
||||
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
|
||||
import { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service';
|
||||
import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type';
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
CreateOneQueryArgs,
|
||||
} from 'src/engine/api/common/types/common-query-args.type';
|
||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
|
||||
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
@@ -52,19 +52,14 @@ export class CommonCreateOneQueryRunnerService extends CommonBaseQueryRunnerServ
|
||||
args: CommonInput<CreateOneQueryArgs>,
|
||||
queryRunnerContext: CommonBaseQueryRunnerContext,
|
||||
): Promise<CommonInput<CreateOneQueryArgs>> {
|
||||
const {
|
||||
authContext,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
} = queryRunnerContext;
|
||||
const { authContext, flatObjectMetadata, flatFieldMetadataMaps } =
|
||||
queryRunnerContext;
|
||||
|
||||
const coercedData = await this.dataArgProcessor.process({
|
||||
partialRecordInputs: [args.data],
|
||||
authContext,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
+2
-7
@@ -176,12 +176,8 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne
|
||||
args: CommonInput<FindDuplicatesQueryArgs>,
|
||||
queryRunnerContext: CommonBaseQueryRunnerContext,
|
||||
): Promise<CommonInput<FindDuplicatesQueryArgs>> {
|
||||
const {
|
||||
authContext,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
} = queryRunnerContext;
|
||||
const { authContext, flatObjectMetadata, flatFieldMetadataMaps } =
|
||||
queryRunnerContext;
|
||||
|
||||
const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata(
|
||||
flatFieldMetadataMaps,
|
||||
@@ -206,7 +202,6 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne
|
||||
authContext,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
shouldBackfillPositionIfUndefined: false,
|
||||
}),
|
||||
};
|
||||
|
||||
+2
-7
@@ -100,12 +100,8 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
args: CommonInput<UpdateManyQueryArgs>,
|
||||
queryRunnerContext: CommonBaseQueryRunnerContext,
|
||||
): Promise<CommonInput<UpdateManyQueryArgs>> {
|
||||
const {
|
||||
authContext,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
} = queryRunnerContext;
|
||||
const { authContext, flatObjectMetadata, flatFieldMetadataMaps } =
|
||||
queryRunnerContext;
|
||||
|
||||
return {
|
||||
...args,
|
||||
@@ -120,7 +116,6 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
authContext,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
shouldBackfillPositionIfUndefined: false,
|
||||
})
|
||||
)[0],
|
||||
|
||||
+2
-7
@@ -65,12 +65,8 @@ export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerServ
|
||||
args: CommonInput<UpdateOneQueryArgs>,
|
||||
queryRunnerContext: CommonBaseQueryRunnerContext,
|
||||
): Promise<CommonInput<UpdateOneQueryArgs>> {
|
||||
const {
|
||||
authContext,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
} = queryRunnerContext;
|
||||
const { authContext, flatObjectMetadata, flatFieldMetadataMaps } =
|
||||
queryRunnerContext;
|
||||
|
||||
return {
|
||||
...args,
|
||||
@@ -80,7 +76,6 @@ export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerServ
|
||||
authContext,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
shouldBackfillPositionIfUndefined: false,
|
||||
})
|
||||
)[0],
|
||||
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { CoreCommonApiModule } from 'src/engine/api/common/core-common-api.module';
|
||||
import { DirectExecutionService } from 'src/engine/api/graphql/direct-execution/direct-execution.service';
|
||||
import { WorkspaceResolverNameMapCacheService } from 'src/engine/api/graphql/direct-execution/services/workspace-resolver-name-map-cache.service';
|
||||
import { WorkspaceResolverBuilderModule } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
CoreCommonApiModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceResolverBuilderModule,
|
||||
],
|
||||
providers: [DirectExecutionService, WorkspaceResolverNameMapCacheService],
|
||||
exports: [DirectExecutionService],
|
||||
})
|
||||
export class DirectExecutionModule {}
|
||||
-357
@@ -1,357 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { type Request } from 'express';
|
||||
import {
|
||||
GraphQLError,
|
||||
type DocumentNode,
|
||||
type FieldNode,
|
||||
type GraphQLFormattedError,
|
||||
type GraphQLResolveInfo,
|
||||
} from 'graphql';
|
||||
import { SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import graphqlFields from 'graphql-fields';
|
||||
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
|
||||
import {
|
||||
GraphqlDirectExecutionException,
|
||||
GraphqlDirectExecutionExceptionCode,
|
||||
} from 'src/engine/api/graphql/direct-execution/errors/graphql-direct-execution.exception';
|
||||
import { assertCreateManyArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-create-many-args.util';
|
||||
import { assertCreateOneArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-create-one-args.util';
|
||||
import { assertDeleteManyArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-delete-many-args.util';
|
||||
import { assertDeleteOneArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-delete-one-args.util';
|
||||
import { assertDestroyManyArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-destroy-many-args.util';
|
||||
import { assertDestroyOneArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-destroy-one-args.util';
|
||||
import { assertFindDuplicatesArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-find-duplicates-args.util';
|
||||
import { assertFindManyArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-find-many-args.util';
|
||||
import { assertFindOneArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-find-one-args.util';
|
||||
import { assertGroupByArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-group-by-args.util';
|
||||
import { assertMergeManyArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-merge-many-args.util';
|
||||
import { assertRestoreManyArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-restore-many-args.util';
|
||||
import { assertRestoreOneArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-restore-one-args.util';
|
||||
import { assertUpdateManyArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-update-many-args.util';
|
||||
import { assertUpdateOneArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-update-one-args.util';
|
||||
import { type ResolverNameMapEntry } from 'src/engine/api/graphql/direct-execution/utils/build-resolver-name-map.util';
|
||||
import { buildWorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/direct-execution/utils/build-workspace-schema-builder-context.util';
|
||||
import { extractArgumentsFromAst } from 'src/engine/api/graphql/direct-execution/utils/extract-arguments-from-ast.util';
|
||||
import { graphQLBackfillNullsFromSelectedFields } from 'src/engine/api/graphql/direct-execution/utils/graphql-backfill-nulls-from-selected-fields.util';
|
||||
import { graphQLBuildFragmentMap } from 'src/engine/api/graphql/direct-execution/utils/graphql-build-fragment-map.util';
|
||||
import { graphQLBuildPartialResolveInfo } from 'src/engine/api/graphql/direct-execution/utils/graphql-build-partial-resolve-info.util';
|
||||
import { graphQLExtractTopLevelFields } from 'src/engine/api/graphql/direct-execution/utils/graphql-extract-top-level-fields.util';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
import { CreateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory';
|
||||
import { CreateOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory';
|
||||
import { DeleteManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory';
|
||||
import { DeleteOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory';
|
||||
import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory';
|
||||
import { DestroyOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory';
|
||||
import { FindDuplicatesResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory';
|
||||
import { FindManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory';
|
||||
import { FindOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory';
|
||||
import { GroupByResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/group-by-resolver.factory';
|
||||
import { MergeManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory';
|
||||
import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory';
|
||||
import { RestoreOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory';
|
||||
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
|
||||
import { UpdateOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory';
|
||||
import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
||||
type DirectExecutionResult = {
|
||||
data: Record<string, unknown> | null;
|
||||
errors?: GraphQLFormattedError[];
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class DirectExecutionService {
|
||||
private readonly factoryMap: Map<
|
||||
string,
|
||||
WorkspaceResolverBuilderFactoryInterface
|
||||
>;
|
||||
|
||||
private readonly argsAssertionMap: Map<string, (args: unknown) => void>;
|
||||
|
||||
constructor(
|
||||
private readonly workspaceFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly i18nService: I18nService,
|
||||
private readonly findManyResolverFactory: FindManyResolverFactory,
|
||||
private readonly findOneResolverFactory: FindOneResolverFactory,
|
||||
private readonly findDuplicatesResolverFactory: FindDuplicatesResolverFactory,
|
||||
private readonly groupByResolverFactory: GroupByResolverFactory,
|
||||
private readonly createOneResolverFactory: CreateOneResolverFactory,
|
||||
private readonly createManyResolverFactory: CreateManyResolverFactory,
|
||||
private readonly updateOneResolverFactory: UpdateOneResolverFactory,
|
||||
private readonly updateManyResolverFactory: UpdateManyResolverFactory,
|
||||
private readonly deleteOneResolverFactory: DeleteOneResolverFactory,
|
||||
private readonly deleteManyResolverFactory: DeleteManyResolverFactory,
|
||||
private readonly destroyOneResolverFactory: DestroyOneResolverFactory,
|
||||
private readonly destroyManyResolverFactory: DestroyManyResolverFactory,
|
||||
private readonly restoreOneResolverFactory: RestoreOneResolverFactory,
|
||||
private readonly restoreManyResolverFactory: RestoreManyResolverFactory,
|
||||
private readonly mergeManyResolverFactory: MergeManyResolverFactory,
|
||||
) {
|
||||
this.factoryMap = new Map<string, WorkspaceResolverBuilderFactoryInterface>(
|
||||
[
|
||||
[RESOLVER_METHOD_NAMES.FIND_MANY, this.findManyResolverFactory],
|
||||
[RESOLVER_METHOD_NAMES.FIND_ONE, this.findOneResolverFactory],
|
||||
[
|
||||
RESOLVER_METHOD_NAMES.FIND_DUPLICATES,
|
||||
this.findDuplicatesResolverFactory,
|
||||
],
|
||||
[RESOLVER_METHOD_NAMES.GROUP_BY, this.groupByResolverFactory],
|
||||
[RESOLVER_METHOD_NAMES.CREATE_ONE, this.createOneResolverFactory],
|
||||
[RESOLVER_METHOD_NAMES.CREATE_MANY, this.createManyResolverFactory],
|
||||
[RESOLVER_METHOD_NAMES.UPDATE_ONE, this.updateOneResolverFactory],
|
||||
[RESOLVER_METHOD_NAMES.UPDATE_MANY, this.updateManyResolverFactory],
|
||||
[RESOLVER_METHOD_NAMES.DELETE_ONE, this.deleteOneResolverFactory],
|
||||
[RESOLVER_METHOD_NAMES.DELETE_MANY, this.deleteManyResolverFactory],
|
||||
[RESOLVER_METHOD_NAMES.DESTROY_ONE, this.destroyOneResolverFactory],
|
||||
[RESOLVER_METHOD_NAMES.DESTROY_MANY, this.destroyManyResolverFactory],
|
||||
[RESOLVER_METHOD_NAMES.RESTORE_ONE, this.restoreOneResolverFactory],
|
||||
[RESOLVER_METHOD_NAMES.RESTORE_MANY, this.restoreManyResolverFactory],
|
||||
[RESOLVER_METHOD_NAMES.MERGE_MANY, this.mergeManyResolverFactory],
|
||||
],
|
||||
);
|
||||
|
||||
this.argsAssertionMap = new Map<string, (args: unknown) => void>([
|
||||
[RESOLVER_METHOD_NAMES.FIND_MANY, assertFindManyArgs],
|
||||
[RESOLVER_METHOD_NAMES.FIND_ONE, assertFindOneArgs],
|
||||
[RESOLVER_METHOD_NAMES.FIND_DUPLICATES, assertFindDuplicatesArgs],
|
||||
[RESOLVER_METHOD_NAMES.GROUP_BY, assertGroupByArgs],
|
||||
[RESOLVER_METHOD_NAMES.CREATE_ONE, assertCreateOneArgs],
|
||||
[RESOLVER_METHOD_NAMES.CREATE_MANY, assertCreateManyArgs],
|
||||
[RESOLVER_METHOD_NAMES.UPDATE_ONE, assertUpdateOneArgs],
|
||||
[RESOLVER_METHOD_NAMES.UPDATE_MANY, assertUpdateManyArgs],
|
||||
[RESOLVER_METHOD_NAMES.DELETE_ONE, assertDeleteOneArgs],
|
||||
[RESOLVER_METHOD_NAMES.DELETE_MANY, assertDeleteManyArgs],
|
||||
[RESOLVER_METHOD_NAMES.DESTROY_ONE, assertDestroyOneArgs],
|
||||
[RESOLVER_METHOD_NAMES.DESTROY_MANY, assertDestroyManyArgs],
|
||||
[RESOLVER_METHOD_NAMES.RESTORE_ONE, assertRestoreOneArgs],
|
||||
[RESOLVER_METHOD_NAMES.RESTORE_MANY, assertRestoreManyArgs],
|
||||
[RESOLVER_METHOD_NAMES.MERGE_MANY, assertMergeManyArgs],
|
||||
]);
|
||||
}
|
||||
|
||||
async getGeneratedWorkspaceResolverNames(
|
||||
workspaceId: string,
|
||||
): Promise<Set<string> | null> {
|
||||
const { graphQLResolverNameMap } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'graphQLResolverNameMap',
|
||||
]);
|
||||
|
||||
return new Set(Object.keys(graphQLResolverNameMap));
|
||||
}
|
||||
|
||||
async execute(
|
||||
req: Request,
|
||||
document: DocumentNode,
|
||||
): Promise<DirectExecutionResult | null> {
|
||||
try {
|
||||
const workspaceId = req.workspace?.id;
|
||||
|
||||
if (!isDefined(workspaceId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const topLevelFields = graphQLExtractTopLevelFields(
|
||||
document,
|
||||
req.body.operationName,
|
||||
);
|
||||
|
||||
this.checkRootResolverLimitsOrThrow(topLevelFields);
|
||||
|
||||
const fragmentMap = graphQLBuildFragmentMap(document);
|
||||
const variables = req.body.variables ?? {};
|
||||
const data: Record<string, unknown> = {};
|
||||
|
||||
const { graphQLResolverNameMap } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'graphQLResolverNameMap',
|
||||
]);
|
||||
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
objectIdByNameSingular,
|
||||
} = await this.loadWorkspaceMetadata(workspaceId);
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
topLevelFields.map(async (field) => {
|
||||
const entry = graphQLResolverNameMap[field.name.value];
|
||||
const responseKey = field.alias?.value ?? field.name.value;
|
||||
|
||||
const args = extractArgumentsFromAst(field.arguments, variables);
|
||||
|
||||
const graphqlPartialResolveInfo = graphQLBuildPartialResolveInfo(
|
||||
field,
|
||||
fragmentMap,
|
||||
);
|
||||
|
||||
const workspaceSchemaBuilderContext =
|
||||
buildWorkspaceSchemaBuilderContext(
|
||||
entry,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
objectIdByNameSingular,
|
||||
);
|
||||
|
||||
const result = await this.executeField({
|
||||
entry,
|
||||
args,
|
||||
graphqlPartialResolveInfo,
|
||||
workspaceSchemaBuilderContext,
|
||||
});
|
||||
|
||||
graphQLBackfillNullsFromSelectedFields(
|
||||
result,
|
||||
graphqlFields(graphqlPartialResolveInfo as GraphQLResolveInfo),
|
||||
);
|
||||
|
||||
return { responseKey, result };
|
||||
}),
|
||||
);
|
||||
|
||||
const errors: GraphQLFormattedError[] = [];
|
||||
|
||||
for (const settled of results) {
|
||||
if (settled.status === 'fulfilled') {
|
||||
data[settled.value.responseKey] = settled.value.result;
|
||||
} else {
|
||||
errors.push(this.formatError(settled.reason, req));
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return { data, errors };
|
||||
}
|
||||
|
||||
return { data };
|
||||
} catch (error) {
|
||||
return { data: null, errors: [this.formatError(error, req)] };
|
||||
}
|
||||
}
|
||||
|
||||
private async executeField({
|
||||
entry,
|
||||
args,
|
||||
graphqlPartialResolveInfo,
|
||||
workspaceSchemaBuilderContext,
|
||||
}: {
|
||||
entry: ResolverNameMapEntry;
|
||||
args: Record<string, unknown>;
|
||||
graphqlPartialResolveInfo: Pick<
|
||||
GraphQLResolveInfo,
|
||||
'fieldNodes' | 'fragments'
|
||||
>;
|
||||
workspaceSchemaBuilderContext: WorkspaceSchemaBuilderContext;
|
||||
}): Promise<unknown> {
|
||||
const factory = this.factoryMap.get(entry.method);
|
||||
const assertFunction = this.argsAssertionMap.get(entry.method);
|
||||
|
||||
if (!isDefined(factory) || !isDefined(assertFunction)) {
|
||||
throw new GraphqlDirectExecutionException(
|
||||
`Unknown method: ${entry.method}`,
|
||||
GraphqlDirectExecutionExceptionCode.UNKNOWN_METHOD,
|
||||
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
|
||||
);
|
||||
}
|
||||
|
||||
assertFunction(args);
|
||||
|
||||
const resolver = factory.create(workspaceSchemaBuilderContext);
|
||||
|
||||
return resolver(
|
||||
null,
|
||||
args,
|
||||
null,
|
||||
graphqlPartialResolveInfo as GraphQLResolveInfo,
|
||||
);
|
||||
}
|
||||
|
||||
private formatError(error: any, req: Request): GraphQLFormattedError {
|
||||
try {
|
||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||
} catch (graphqlError) {
|
||||
if (graphqlError instanceof GraphQLError) {
|
||||
const json = graphqlError.toJSON();
|
||||
|
||||
if (json.extensions?.userFriendlyMessage) {
|
||||
const userLocale = req.locale ?? SOURCE_LOCALE;
|
||||
const i18n = this.i18nService.getI18nInstance(userLocale);
|
||||
|
||||
json.extensions.userFriendlyMessage = i18n._(
|
||||
json.extensions.userFriendlyMessage as MessageDescriptor,
|
||||
);
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: isDefined(error.message)
|
||||
? error.message
|
||||
: 'Internal server error',
|
||||
extensions: { code: 'INTERNAL_SERVER_ERROR' },
|
||||
};
|
||||
}
|
||||
|
||||
private checkRootResolverLimitsOrThrow(topLevelFields: FieldNode[]): void {
|
||||
const maxRootResolvers = this.twentyConfigService.get(
|
||||
'GRAPHQL_MAX_ROOT_RESOLVERS',
|
||||
);
|
||||
|
||||
if (
|
||||
isDefined(maxRootResolvers) &&
|
||||
topLevelFields.length > maxRootResolvers
|
||||
) {
|
||||
throw new UserInputError(
|
||||
`Query too complex - Too many root resolvers requested: ${topLevelFields.length} - Maximum allowed root resolvers: ${maxRootResolvers}`,
|
||||
);
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const field of topLevelFields) {
|
||||
const name = field.name.value;
|
||||
|
||||
if (seen.has(name)) {
|
||||
throw new UserInputError(`Duplicate root resolver: "${name}"`);
|
||||
}
|
||||
|
||||
seen.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadWorkspaceMetadata(workspaceId: string) {
|
||||
const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
|
||||
await this.workspaceFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const { idByNameSingular } = buildObjectIdByNameMaps(
|
||||
flatObjectMetadataMaps,
|
||||
);
|
||||
|
||||
return {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
objectIdByNameSingular: idByNameSingular,
|
||||
};
|
||||
}
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export enum GraphqlDirectExecutionExceptionCode {
|
||||
INVALID_QUERY_INPUT = 'INVALID_QUERY_INPUT',
|
||||
UNKNOWN_METHOD = 'UNKNOWN_METHOD',
|
||||
}
|
||||
|
||||
export class GraphqlDirectExecutionException extends CustomException<GraphqlDirectExecutionExceptionCode> {
|
||||
constructor(
|
||||
message: string,
|
||||
code: GraphqlDirectExecutionExceptionCode,
|
||||
{ userFriendlyMessage }: { userFriendlyMessage: MessageDescriptor },
|
||||
) {
|
||||
super(message, code, {
|
||||
userFriendlyMessage,
|
||||
});
|
||||
}
|
||||
}
|
||||
-95
@@ -1,95 +0,0 @@
|
||||
import { type Request } from 'express';
|
||||
import { DocumentNode, parse } from 'graphql';
|
||||
import { type Plugin } from 'graphql-yoga';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { type DirectExecutionService } from 'src/engine/api/graphql/direct-execution/direct-execution.service';
|
||||
import { computeSkipWorkspaceSchemaCreation } from 'src/engine/api/graphql/direct-execution/utils/compute-skip-workspace-schema-creation.util';
|
||||
import { findOperationDefinition } from 'src/engine/api/graphql/direct-execution/utils/find-operation-definition.util';
|
||||
import { hasOnlyGeneratedWorkspaceResolvers } from 'src/engine/api/graphql/direct-execution/utils/has-only-generated-workspace-resolvers.util';
|
||||
import { isSubscriptionOperation } from 'src/engine/api/graphql/direct-execution/utils/is-subscription-operation.util';
|
||||
import { type FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
|
||||
export type DirectExecutionPluginConfig = {
|
||||
directExecutionService: DirectExecutionService;
|
||||
featureFlagService: FeatureFlagService;
|
||||
};
|
||||
|
||||
export function useDirectExecution(
|
||||
config: DirectExecutionPluginConfig,
|
||||
): Plugin {
|
||||
return {
|
||||
onRequest: async ({ endResponse, serverContext }) => {
|
||||
const req = (serverContext as unknown as { req: Request }).req;
|
||||
|
||||
if (!req.workspace?.id || !req.body?.query) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isEnabled = await config.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_DIRECT_GRAPHQL_EXECUTION_ENABLED,
|
||||
req.workspace.id,
|
||||
);
|
||||
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const generatedWorkspaceResolverNames =
|
||||
await config.directExecutionService.getGeneratedWorkspaceResolverNames(
|
||||
req.workspace.id,
|
||||
);
|
||||
|
||||
if (!generatedWorkspaceResolverNames) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryString = req.body.query as string;
|
||||
const operationName = req.body.operationName as string | undefined;
|
||||
|
||||
let document: DocumentNode;
|
||||
try {
|
||||
document = parse(queryString);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!findOperationDefinition(document, operationName) ||
|
||||
isSubscriptionOperation(document, operationName)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
computeSkipWorkspaceSchemaCreation(
|
||||
queryString,
|
||||
document,
|
||||
operationName,
|
||||
generatedWorkspaceResolverNames,
|
||||
)
|
||||
) {
|
||||
req.skipWorkspaceSchemaCreation = true;
|
||||
}
|
||||
|
||||
if (
|
||||
!hasOnlyGeneratedWorkspaceResolvers(
|
||||
document,
|
||||
operationName,
|
||||
generatedWorkspaceResolverNames,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await config.directExecutionService.execute(req, document);
|
||||
|
||||
if (isNull(result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return endResponse(Response.json(result));
|
||||
},
|
||||
};
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
|
||||
|
||||
import {
|
||||
type ResolverNameMapEntry,
|
||||
buildResolverNameMap,
|
||||
} from 'src/engine/api/graphql/direct-execution/utils/build-resolver-name-map.util';
|
||||
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
||||
@Injectable()
|
||||
@WorkspaceCache('graphQLResolverNameMap')
|
||||
export class WorkspaceResolverNameMapCacheService extends WorkspaceCacheProvider<
|
||||
Record<string, ResolverNameMapEntry>
|
||||
> {
|
||||
constructor(private readonly workspaceCacheService: WorkspaceCacheService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async computeForCache(
|
||||
workspaceId: string,
|
||||
): Promise<Record<string, ResolverNameMapEntry>> {
|
||||
const { flatObjectMetadataMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
]);
|
||||
|
||||
return buildResolverNameMap(flatObjectMetadataMaps);
|
||||
}
|
||||
}
|
||||
-178
@@ -1,178 +0,0 @@
|
||||
import { parse } from 'graphql';
|
||||
|
||||
import { computeSkipWorkspaceSchemaCreation } from 'src/engine/api/graphql/direct-execution/utils/compute-skip-workspace-schema-creation.util';
|
||||
|
||||
const GENERATED_RESOLVERS = new Set([
|
||||
'findManyCompanies',
|
||||
'findOneCompany',
|
||||
'createOneCompany',
|
||||
'findManyPeople',
|
||||
'findOnePerson',
|
||||
]);
|
||||
|
||||
describe('computeSkipWorkspaceSchemaCreation', () => {
|
||||
it('should return true when all fields are core resolvers', () => {
|
||||
const query = `
|
||||
query {
|
||||
currentWorkspace { id }
|
||||
}
|
||||
`;
|
||||
|
||||
expect(
|
||||
computeSkipWorkspaceSchemaCreation(
|
||||
query,
|
||||
parse(query),
|
||||
undefined,
|
||||
GENERATED_RESOLVERS,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for multiple core resolver fields', () => {
|
||||
const query = `
|
||||
query {
|
||||
currentWorkspace { id }
|
||||
currentUser { id }
|
||||
}
|
||||
`;
|
||||
|
||||
expect(
|
||||
computeSkipWorkspaceSchemaCreation(
|
||||
query,
|
||||
parse(query),
|
||||
undefined,
|
||||
GENERATED_RESOLVERS,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when all fields are generated workspace resolvers', () => {
|
||||
const query = `
|
||||
query {
|
||||
findManyCompanies { id name }
|
||||
}
|
||||
`;
|
||||
|
||||
expect(
|
||||
computeSkipWorkspaceSchemaCreation(
|
||||
query,
|
||||
parse(query),
|
||||
undefined,
|
||||
GENERATED_RESOLVERS,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for mixed queries', () => {
|
||||
const query = `
|
||||
query {
|
||||
findManyCompanies { id }
|
||||
currentWorkspace { id }
|
||||
}
|
||||
`;
|
||||
|
||||
expect(
|
||||
computeSkipWorkspaceSchemaCreation(
|
||||
query,
|
||||
parse(query),
|
||||
undefined,
|
||||
GENERATED_RESOLVERS,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for __schema introspection', () => {
|
||||
const query = `
|
||||
query {
|
||||
__schema { types { name } }
|
||||
}
|
||||
`;
|
||||
|
||||
expect(
|
||||
computeSkipWorkspaceSchemaCreation(
|
||||
query,
|
||||
parse(query),
|
||||
undefined,
|
||||
GENERATED_RESOLVERS,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for __type introspection', () => {
|
||||
const query = `
|
||||
query {
|
||||
__type(name: "Company") { name fields { name } }
|
||||
}
|
||||
`;
|
||||
|
||||
expect(
|
||||
computeSkipWorkspaceSchemaCreation(
|
||||
query,
|
||||
parse(query),
|
||||
undefined,
|
||||
GENERATED_RESOLVERS,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should not treat __typename as introspection', () => {
|
||||
const query = `
|
||||
query {
|
||||
currentWorkspace { id __typename }
|
||||
}
|
||||
`;
|
||||
|
||||
expect(
|
||||
computeSkipWorkspaceSchemaCreation(
|
||||
query,
|
||||
parse(query),
|
||||
undefined,
|
||||
GENERATED_RESOLVERS,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when no operation matches (no fields to check)', () => {
|
||||
const query = `
|
||||
query GetCompanies { findManyCompanies { id } }
|
||||
`;
|
||||
|
||||
expect(
|
||||
computeSkipWorkspaceSchemaCreation(
|
||||
query,
|
||||
parse(query),
|
||||
'NonExistent',
|
||||
GENERATED_RESOLVERS,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should respect operationName', () => {
|
||||
const query = `
|
||||
query CoreQuery {
|
||||
currentWorkspace { id }
|
||||
}
|
||||
query WorkspaceQuery {
|
||||
findManyCompanies { id }
|
||||
}
|
||||
`;
|
||||
|
||||
expect(
|
||||
computeSkipWorkspaceSchemaCreation(
|
||||
query,
|
||||
parse(query),
|
||||
'CoreQuery',
|
||||
GENERATED_RESOLVERS,
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
computeSkipWorkspaceSchemaCreation(
|
||||
query,
|
||||
parse(query),
|
||||
'WorkspaceQuery',
|
||||
GENERATED_RESOLVERS,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user