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('');
|
||||
}
|
||||
};
|
||||
|
||||
+9
-2
@@ -1,11 +1,12 @@
|
||||
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
||||
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useObjectNameSingularFromPlural = ({
|
||||
objectNamePlural,
|
||||
}: {
|
||||
objectNamePlural: string;
|
||||
}): { objectNameSingular: string | undefined } => {
|
||||
}) => {
|
||||
const objectMetadataItem = useAtomFamilySelectorValue(
|
||||
objectMetadataItemFamilySelector,
|
||||
{
|
||||
@@ -14,5 +15,11 @@ export const useObjectNameSingularFromPlural = ({
|
||||
},
|
||||
);
|
||||
|
||||
return { objectNameSingular: objectMetadataItem?.nameSingular };
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
throw new Error(
|
||||
`Object metadata item not found for ${objectNamePlural} object`,
|
||||
);
|
||||
}
|
||||
|
||||
return { objectNameSingular: objectMetadataItem.nameSingular };
|
||||
};
|
||||
|
||||
+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}
|
||||
|
||||
+1
-3
@@ -104,9 +104,7 @@ export const getAxisLayerLayout = ({
|
||||
-margins.left + axisConfig.leftAxisLegendOffsetPadding;
|
||||
|
||||
const valueRange = valueDomain.max - valueDomain.min;
|
||||
const zeroIsWithinDomain =
|
||||
valueDomain.min < 0 && valueDomain.max > 0 && valueRange !== 0;
|
||||
const shouldRenderZeroLine = hasNegativeValues && zeroIsWithinDomain;
|
||||
const shouldRenderZeroLine = hasNegativeValues && valueRange !== 0;
|
||||
const zeroPosition = shouldRenderZeroLine
|
||||
? isVertical
|
||||
? innerHeight - ((0 - valueDomain.min) / valueRange) * innerHeight
|
||||
|
||||
-4
@@ -36,10 +36,6 @@ export const renderGridLayer = ({
|
||||
for (const tickValue of valueTickValues) {
|
||||
const normalizedPosition = (tickValue - valueDomain.min) / range;
|
||||
|
||||
if (normalizedPosition <= 0 || normalizedPosition >= 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
if (isVertical) {
|
||||
const y = innerHeight * (1 - normalizedPosition);
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
export const TEXT_MARGIN_EXTRAS = {
|
||||
tickPaddingExtra: 4,
|
||||
bottomTickExtraNonRotated: 20,
|
||||
rightTickExtra: 16,
|
||||
} as const;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
export const TEXT_MARGIN_LIMITS = {
|
||||
min: { top: 10, right: 10, bottom: 30, left: 40 },
|
||||
max: { top: 30, right: 160, bottom: 140, left: 160 },
|
||||
max: { top: 30, right: 30, bottom: 140, left: 160 },
|
||||
} as const;
|
||||
|
||||
+1
-7
@@ -1,7 +1,7 @@
|
||||
import { BarChartLayers } from '@/page-layout/widgets/graph/graph-widget-bar-chart/components/BarChartLayers';
|
||||
import { useBarChartLayout } from '@/page-layout/widgets/graph/graph-widget-bar-chart/hooks/useBarChartLayout';
|
||||
import { useBarChartTheme } from '@/page-layout/widgets/graph/graph-widget-bar-chart/hooks/useBarChartTheme';
|
||||
import { useMemoizedBarPositions } from '@/page-layout/widgets/graph/graph-widget-bar-chart/hooks/useMemoizedBarPositions';
|
||||
import { useBarChartLayout } from '@/page-layout/widgets/graph/graph-widget-bar-chart/hooks/useBarChartLayout';
|
||||
import { type BarChartDatum } from '@/page-layout/widgets/graph/graph-widget-bar-chart/types/BarChartDatum';
|
||||
import { type BarChartEnrichedKey } from '@/page-layout/widgets/graph/graph-widget-bar-chart/types/BarChartEnrichedKey';
|
||||
import { type BarChartSlice } from '@/page-layout/widgets/graph/graph-widget-bar-chart/types/BarChartSlice';
|
||||
@@ -30,14 +30,12 @@ type BarChartProps = {
|
||||
layout: BarChartLayout;
|
||||
groupMode: 'grouped' | 'stacked';
|
||||
effectiveValueRange: { minimum: number; maximum: number };
|
||||
hasExplicitRangeBounds: boolean;
|
||||
formatOptions: GraphValueFormatOptions;
|
||||
axisConfig?: {
|
||||
xAxisLabel?: string;
|
||||
yAxisLabel?: string;
|
||||
showGrid?: boolean;
|
||||
};
|
||||
rightTickLabels?: string[];
|
||||
dataLabelsConfig?: {
|
||||
show: boolean;
|
||||
omitNullValues: boolean;
|
||||
@@ -67,10 +65,8 @@ export const BarChart = ({
|
||||
layout,
|
||||
groupMode,
|
||||
effectiveValueRange,
|
||||
hasExplicitRangeBounds,
|
||||
formatOptions,
|
||||
axisConfig,
|
||||
rightTickLabels,
|
||||
dataLabelsConfig,
|
||||
hoveredSliceIndexValue,
|
||||
onSliceHover,
|
||||
@@ -106,13 +102,11 @@ export const BarChart = ({
|
||||
chartWidth,
|
||||
data,
|
||||
effectiveValueRange,
|
||||
hasExplicitRangeBounds,
|
||||
formatOptions,
|
||||
groupMode,
|
||||
indexBy,
|
||||
keys,
|
||||
layout,
|
||||
rightTickLabels,
|
||||
});
|
||||
|
||||
const isVerticalLayout = layout === BarChartLayout.VERTICAL;
|
||||
|
||||
+9
-48
@@ -1,3 +1,4 @@
|
||||
import { isSidePanelAnimatingState } from '@/side-panel/states/isSidePanelAnimatingState';
|
||||
import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState';
|
||||
import { pageLayoutResizingWidgetIdComponentState } from '@/page-layout/states/pageLayoutResizingWidgetIdComponentState';
|
||||
import { GraphWidgetChartContainer } from '@/page-layout/widgets/graph/components/GraphWidgetChartContainer';
|
||||
@@ -16,17 +17,12 @@ import { calculateValueRangeFromBarChartKeys } from '@/page-layout/widgets/graph
|
||||
import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode';
|
||||
import { computeEffectiveValueRange } from '@/page-layout/widgets/graph/utils/computeEffectiveValueRange';
|
||||
import { createGraphColorRegistry } from '@/page-layout/widgets/graph/utils/createGraphColorRegistry';
|
||||
import {
|
||||
formatGraphValue,
|
||||
type GraphValueFormatOptions,
|
||||
} from '@/page-layout/widgets/graph/utils/graphFormatters';
|
||||
import { isSidePanelAnimatingState } from '@/side-panel/states/isSidePanelAnimatingState';
|
||||
import { type GraphValueFormatOptions } from '@/page-layout/widgets/graph/utils/graphFormatters';
|
||||
import { NodeDimensionEffect } from '@/ui/utilities/dimensions/components/NodeDimensionEffect';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { styled } from '@linaria/react';
|
||||
import { isNumber } from '@sniptt/guards';
|
||||
import { useContext, useMemo, useRef, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
@@ -124,16 +120,13 @@ export const GraphWidgetBarChart = ({
|
||||
|
||||
const allowDataTransitions = !isLayoutAnimating;
|
||||
|
||||
const formatOptions = useMemo<GraphValueFormatOptions>(
|
||||
() => ({
|
||||
customFormatter,
|
||||
decimals,
|
||||
displayType,
|
||||
prefix,
|
||||
suffix,
|
||||
}),
|
||||
[customFormatter, decimals, displayType, prefix, suffix],
|
||||
);
|
||||
const formatOptions: GraphValueFormatOptions = {
|
||||
customFormatter,
|
||||
decimals,
|
||||
displayType,
|
||||
prefix,
|
||||
suffix,
|
||||
};
|
||||
|
||||
const { enrichedKeysMap, enrichedKeys, legendItems, visibleKeys } =
|
||||
useBarChartData({ keys, series, colorRegistry, seriesLabels, colorMode });
|
||||
@@ -158,36 +151,6 @@ export const GraphWidgetBarChart = ({
|
||||
rangeMin,
|
||||
});
|
||||
|
||||
const hasExplicitRangeBounds = isDefined(rangeMin) || isDefined(rangeMax);
|
||||
|
||||
const rightTickLabels = useMemo(() => {
|
||||
if (
|
||||
!hasExplicitRangeBounds ||
|
||||
!showValues ||
|
||||
layout !== BarChartLayout.HORIZONTAL
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const labels: string[] = [];
|
||||
for (const dataItem of data) {
|
||||
for (const visibleKey of visibleKeys) {
|
||||
const value = dataItem[visibleKey];
|
||||
|
||||
if (isNumber(value)) {
|
||||
labels.push(formatGraphValue(value, formatOptions));
|
||||
}
|
||||
}
|
||||
}
|
||||
return labels;
|
||||
}, [
|
||||
data,
|
||||
visibleKeys,
|
||||
formatOptions,
|
||||
hasExplicitRangeBounds,
|
||||
showValues,
|
||||
layout,
|
||||
]);
|
||||
|
||||
const dataByIndexValue = useMemo(
|
||||
() => new Map(data.map((row) => [String(row[indexBy]), row])),
|
||||
[data, indexBy],
|
||||
@@ -259,10 +222,8 @@ export const GraphWidgetBarChart = ({
|
||||
maximum: effectiveMaximumValue,
|
||||
minimum: effectiveMinimumValue,
|
||||
}}
|
||||
hasExplicitRangeBounds={hasExplicitRangeBounds}
|
||||
enrichedKeysMap={enrichedKeysMap}
|
||||
formatOptions={formatOptions}
|
||||
rightTickLabels={rightTickLabels}
|
||||
groupMode={groupMode}
|
||||
hasNoData={hasNoData}
|
||||
hoveredSliceIndexValue={graphWidgetHoveredSliceIndex}
|
||||
|
||||
-6
@@ -21,13 +21,11 @@ type UseBarChartLayoutParams = {
|
||||
chartWidth: number;
|
||||
data: BarChartDatum[];
|
||||
effectiveValueRange: { minimum: number; maximum: number };
|
||||
hasExplicitRangeBounds: boolean;
|
||||
formatOptions: GraphValueFormatOptions;
|
||||
groupMode: 'grouped' | 'stacked';
|
||||
indexBy: string;
|
||||
keys: string[];
|
||||
layout: BarChartLayout;
|
||||
rightTickLabels?: string[];
|
||||
};
|
||||
|
||||
type UseBarChartLayoutResult = {
|
||||
@@ -54,13 +52,11 @@ export const useBarChartLayout = ({
|
||||
chartWidth,
|
||||
data,
|
||||
effectiveValueRange,
|
||||
hasExplicitRangeBounds,
|
||||
formatOptions,
|
||||
groupMode,
|
||||
indexBy,
|
||||
keys,
|
||||
layout,
|
||||
rightTickLabels,
|
||||
}: UseBarChartLayoutParams): UseBarChartLayoutResult => {
|
||||
const { tickFontSize, legendFontSize } = resolveAxisFontSizes(axisTheme);
|
||||
|
||||
@@ -77,11 +73,9 @@ export const useBarChartLayout = ({
|
||||
data,
|
||||
effectiveMaximumValue: effectiveValueRange.maximum,
|
||||
effectiveMinimumValue: effectiveValueRange.minimum,
|
||||
hasExplicitRangeBounds,
|
||||
formatOptions,
|
||||
indexBy,
|
||||
layout,
|
||||
rightTickLabels,
|
||||
xAxisLabel: axisConfig?.xAxisLabel,
|
||||
yAxisLabel: axisConfig?.yAxisLabel,
|
||||
});
|
||||
|
||||
-24
@@ -142,28 +142,4 @@ describe('computeBarPositions', () => {
|
||||
expect(bar2).toBeDefined();
|
||||
expect(bar1!.y).toBeGreaterThan(bar2!.y);
|
||||
});
|
||||
|
||||
it('clamps horizontal bars to value axis when value is above explicit max', () => {
|
||||
const chartWidth = 300;
|
||||
|
||||
const result = computeBarPositions({
|
||||
data: [{ category: 'A', value1: 15 }],
|
||||
indexBy: 'category',
|
||||
keys: ['value1'],
|
||||
enrichedKeysMap: defaultEnrichedKeysMap,
|
||||
chartWidth,
|
||||
chartHeight: 300,
|
||||
margins: defaultMargins,
|
||||
layout: BarChartLayout.HORIZONTAL,
|
||||
groupMode: 'grouped',
|
||||
valueDomain: { min: 0, max: 10 },
|
||||
innerPadding: 2,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].x).toBe(0);
|
||||
expect(result[0].width).toBe(
|
||||
chartWidth - defaultMargins.left - defaultMargins.right,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
-17
@@ -124,23 +124,6 @@ describe('getBarChartLayout', () => {
|
||||
TEXT_MARGIN_LIMITS.max.left,
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps explicit range bounds as value domain', () => {
|
||||
const result = getBarChartLayout({
|
||||
...baseParams,
|
||||
chartWidth: 280,
|
||||
layout: BarChartLayout.HORIZONTAL,
|
||||
effectiveMinimumValue: 0,
|
||||
effectiveMaximumValue: 10,
|
||||
hasExplicitRangeBounds: true,
|
||||
});
|
||||
|
||||
expect(result.valueDomain).toEqual({ min: 0, max: 10 });
|
||||
expect(result.valueTickValues[0]).toBe(0);
|
||||
expect(result.valueTickValues[result.valueTickValues.length - 1]).toBe(
|
||||
10,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
|
||||
-40
@@ -59,44 +59,4 @@ describe('getGroupedBarDimensions', () => {
|
||||
height: 19,
|
||||
});
|
||||
});
|
||||
|
||||
it('clamps vertical bar when value exceeds axis length', () => {
|
||||
const ctx = createContext({ isVertical: true });
|
||||
const layout = computeGroupedBarLayout(ctx, 2);
|
||||
|
||||
const dimensions = getGroupedBarDimensions({
|
||||
ctx,
|
||||
layout,
|
||||
categoryStart: 10,
|
||||
keyIndex: 1,
|
||||
value: 150,
|
||||
});
|
||||
|
||||
expect(dimensions).toEqual({
|
||||
x: 31,
|
||||
y: 0,
|
||||
width: 19,
|
||||
height: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it('clamps horizontal bar when value exceeds axis length', () => {
|
||||
const ctx = createContext({ isVertical: false });
|
||||
const layout = computeGroupedBarLayout(ctx, 2);
|
||||
|
||||
const dimensions = getGroupedBarDimensions({
|
||||
ctx,
|
||||
layout,
|
||||
categoryStart: 10,
|
||||
keyIndex: 0,
|
||||
value: 150,
|
||||
});
|
||||
|
||||
expect(dimensions).toEqual({
|
||||
x: 0,
|
||||
y: 10,
|
||||
width: 100,
|
||||
height: 19,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
-88
@@ -114,92 +114,4 @@ describe('getStackedBarDimensions', () => {
|
||||
newNegativeStackPixel: 30,
|
||||
});
|
||||
});
|
||||
|
||||
it('clamps vertical positive bar when stack exceeds axis length', () => {
|
||||
const ctx = createContext({ isVertical: true });
|
||||
|
||||
const dimensions = getStackedBarDimensions({
|
||||
ctx,
|
||||
layout,
|
||||
categoryStart: 20,
|
||||
value: 40,
|
||||
positiveStackPixel: 80,
|
||||
negativeStackPixel: 50,
|
||||
});
|
||||
|
||||
expect(dimensions).toEqual({
|
||||
x: 22,
|
||||
y: 0,
|
||||
width: 10,
|
||||
height: 20,
|
||||
newPositiveStackPixel: 120,
|
||||
newNegativeStackPixel: 50,
|
||||
});
|
||||
});
|
||||
|
||||
it('clamps vertical negative bar when stack goes below zero', () => {
|
||||
const ctx = createContext({ isVertical: true });
|
||||
|
||||
const dimensions = getStackedBarDimensions({
|
||||
ctx,
|
||||
layout,
|
||||
categoryStart: 20,
|
||||
value: -40,
|
||||
positiveStackPixel: 50,
|
||||
negativeStackPixel: 20,
|
||||
});
|
||||
|
||||
expect(dimensions).toEqual({
|
||||
x: 22,
|
||||
y: 80,
|
||||
width: 10,
|
||||
height: 20,
|
||||
newPositiveStackPixel: 50,
|
||||
newNegativeStackPixel: -20,
|
||||
});
|
||||
});
|
||||
|
||||
it('clamps horizontal positive bar when stack exceeds axis length', () => {
|
||||
const ctx = createContext({ isVertical: false });
|
||||
|
||||
const dimensions = getStackedBarDimensions({
|
||||
ctx,
|
||||
layout,
|
||||
categoryStart: 20,
|
||||
value: 40,
|
||||
positiveStackPixel: 80,
|
||||
negativeStackPixel: 50,
|
||||
});
|
||||
|
||||
expect(dimensions).toEqual({
|
||||
x: 80,
|
||||
y: 22,
|
||||
width: 20,
|
||||
height: 10,
|
||||
newPositiveStackPixel: 120,
|
||||
newNegativeStackPixel: 50,
|
||||
});
|
||||
});
|
||||
|
||||
it('clamps horizontal negative bar when stack goes below zero', () => {
|
||||
const ctx = createContext({ isVertical: false });
|
||||
|
||||
const dimensions = getStackedBarDimensions({
|
||||
ctx,
|
||||
layout,
|
||||
categoryStart: 20,
|
||||
value: -40,
|
||||
positiveStackPixel: 50,
|
||||
negativeStackPixel: 20,
|
||||
});
|
||||
|
||||
expect(dimensions).toEqual({
|
||||
x: 0,
|
||||
y: 22,
|
||||
width: 20,
|
||||
height: 10,
|
||||
newPositiveStackPixel: 50,
|
||||
newNegativeStackPixel: -20,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+3
-14
@@ -31,8 +31,6 @@ type GetBarChartLayoutParams = {
|
||||
formatOptions: GraphValueFormatOptions;
|
||||
effectiveMinimumValue: number;
|
||||
effectiveMaximumValue: number;
|
||||
hasExplicitRangeBounds?: boolean;
|
||||
rightTickLabels?: string[];
|
||||
};
|
||||
|
||||
type BarChartLayoutResult = {
|
||||
@@ -129,8 +127,6 @@ export const getBarChartLayout = ({
|
||||
formatOptions,
|
||||
effectiveMinimumValue,
|
||||
effectiveMaximumValue,
|
||||
hasExplicitRangeBounds = false,
|
||||
rightTickLabels = [],
|
||||
}: GetBarChartLayoutParams): BarChartLayoutResult => {
|
||||
const { tickFontSize, legendFontSize } = resolveAxisFontSizes(axisTheme);
|
||||
|
||||
@@ -156,7 +152,6 @@ export const getBarChartLayout = ({
|
||||
minimum: effectiveMinimumValue,
|
||||
maximum: effectiveMaximumValue,
|
||||
tickCount: currentTickConfiguration.numberOfValueTicks,
|
||||
preserveDomainBounds: hasExplicitRangeBounds,
|
||||
}),
|
||||
getTickRotation: (currentTickConfiguration) =>
|
||||
currentTickConfiguration.bottomAxisTickRotation,
|
||||
@@ -166,19 +161,13 @@ export const getBarChartLayout = ({
|
||||
tickFontSize,
|
||||
tickRotation: parameters.tickConfiguration.bottomAxisTickRotation,
|
||||
}),
|
||||
resolveMarginInputs: (currentTickConfiguration, tickResult) => {
|
||||
const marginInputs = resolveMarginInputs({
|
||||
resolveMarginInputs: (currentTickConfiguration, tickResult) =>
|
||||
resolveMarginInputs({
|
||||
tickConfiguration: currentTickConfiguration,
|
||||
tickResult,
|
||||
layout,
|
||||
formatOptions,
|
||||
});
|
||||
|
||||
return {
|
||||
...marginInputs,
|
||||
rightTickLabels,
|
||||
};
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const { tickValues: valueTickValues, domain: valueDomain } = valueTickResult;
|
||||
|
||||
+3
-7
@@ -52,13 +52,9 @@ export const getGroupedBarDimensions = ({
|
||||
const { isVertical, valueAxisLength, valueToPixel, zeroPixel } = ctx;
|
||||
const { barThickness, groupCenteringOffset, barStride } = layout;
|
||||
|
||||
const valuePixel = Math.max(
|
||||
0,
|
||||
Math.min(valueAxisLength, valueToPixel(value)),
|
||||
);
|
||||
const clampedZeroPixel = Math.max(0, Math.min(valueAxisLength, zeroPixel));
|
||||
const barStart = Math.min(clampedZeroPixel, valuePixel);
|
||||
const barLength = Math.abs(valuePixel - clampedZeroPixel);
|
||||
const valuePixel = valueToPixel(value);
|
||||
const barStart = Math.min(zeroPixel, valuePixel);
|
||||
const barLength = Math.abs(valuePixel - zeroPixel);
|
||||
|
||||
const categoryPosition =
|
||||
categoryStart + groupCenteringOffset + keyIndex * barStride;
|
||||
|
||||
+12
-31
@@ -74,64 +74,45 @@ export const getStackedBarDimensions = ({
|
||||
const valuePixelDelta =
|
||||
stackRange === 0 ? 0 : stackValueToPixel(Math.abs(value));
|
||||
|
||||
const clampToAxis = (pixel: number) =>
|
||||
Math.max(0, Math.min(valueAxisLength, pixel));
|
||||
|
||||
if (isVertical && isNegative) {
|
||||
const newStack = negativeStackPixel - valuePixelDelta;
|
||||
const clampedCurrent = clampToAxis(negativeStackPixel);
|
||||
const clampedNew = clampToAxis(newStack);
|
||||
|
||||
return {
|
||||
x: categoryPosition,
|
||||
y: valueAxisLength - clampedCurrent,
|
||||
y: valueAxisLength - negativeStackPixel,
|
||||
width: barThickness,
|
||||
height: clampedCurrent - clampedNew,
|
||||
height: valuePixelDelta,
|
||||
newPositiveStackPixel: positiveStackPixel,
|
||||
newNegativeStackPixel: newStack,
|
||||
newNegativeStackPixel: negativeStackPixel - valuePixelDelta,
|
||||
};
|
||||
}
|
||||
|
||||
if (isVertical) {
|
||||
const newStack = positiveStackPixel + valuePixelDelta;
|
||||
const clampedCurrent = clampToAxis(positiveStackPixel);
|
||||
const clampedNew = clampToAxis(newStack);
|
||||
|
||||
return {
|
||||
x: categoryPosition,
|
||||
y: valueAxisLength - clampedNew,
|
||||
y: valueAxisLength - (positiveStackPixel + valuePixelDelta),
|
||||
width: barThickness,
|
||||
height: clampedNew - clampedCurrent,
|
||||
newPositiveStackPixel: newStack,
|
||||
height: valuePixelDelta,
|
||||
newPositiveStackPixel: positiveStackPixel + valuePixelDelta,
|
||||
newNegativeStackPixel: negativeStackPixel,
|
||||
};
|
||||
}
|
||||
|
||||
if (isNegative) {
|
||||
const newStack = negativeStackPixel - valuePixelDelta;
|
||||
const clampedCurrent = clampToAxis(negativeStackPixel);
|
||||
const clampedNew = clampToAxis(newStack);
|
||||
|
||||
return {
|
||||
x: clampedNew,
|
||||
x: negativeStackPixel - valuePixelDelta,
|
||||
y: categoryPosition,
|
||||
width: clampedCurrent - clampedNew,
|
||||
width: valuePixelDelta,
|
||||
height: barThickness,
|
||||
newPositiveStackPixel: positiveStackPixel,
|
||||
newNegativeStackPixel: newStack,
|
||||
newNegativeStackPixel: negativeStackPixel - valuePixelDelta,
|
||||
};
|
||||
}
|
||||
|
||||
const newStack = positiveStackPixel + valuePixelDelta;
|
||||
const clampedCurrent = clampToAxis(positiveStackPixel);
|
||||
const clampedNew = clampToAxis(newStack);
|
||||
|
||||
return {
|
||||
x: clampedCurrent,
|
||||
x: positiveStackPixel,
|
||||
y: categoryPosition,
|
||||
width: clampedNew - clampedCurrent,
|
||||
width: valuePixelDelta,
|
||||
height: barThickness,
|
||||
newPositiveStackPixel: newStack,
|
||||
newPositiveStackPixel: positiveStackPixel + valuePixelDelta,
|
||||
newNegativeStackPixel: negativeStackPixel,
|
||||
};
|
||||
};
|
||||
|
||||
-26
@@ -46,30 +46,4 @@ describe('computeValueTickValues', () => {
|
||||
expect(result.domain.min).toBeLessThanOrEqual(-100);
|
||||
expect(result.domain.max).toBeGreaterThanOrEqual(-10);
|
||||
});
|
||||
|
||||
it('should preserve explicit domain bounds when requested', () => {
|
||||
const result = computeValueTickValues({
|
||||
minimum: 0,
|
||||
maximum: 10,
|
||||
tickCount: 2,
|
||||
preserveDomainBounds: true,
|
||||
});
|
||||
|
||||
expect(result.domain).toEqual({ min: 0, max: 10 });
|
||||
expect(result.tickValues[0]).toBe(0);
|
||||
expect(result.tickValues[result.tickValues.length - 1]).toBe(10);
|
||||
expect(result.tickValues.every((tick) => tick >= 0 && tick <= 10)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should still allow domain expansion when preserving bounds is disabled', () => {
|
||||
const result = computeValueTickValues({
|
||||
minimum: 0,
|
||||
maximum: 10,
|
||||
tickCount: 2,
|
||||
});
|
||||
|
||||
expect(result.domain.max).toBeGreaterThan(10);
|
||||
});
|
||||
});
|
||||
|
||||
-36
@@ -24,42 +24,6 @@ describe('getChartMarginsFromText', () => {
|
||||
expect(result.right).toBe(18);
|
||||
});
|
||||
|
||||
it('clamps right margin when rightTickLabels are very long', () => {
|
||||
const longLabel = 'X'.repeat(200);
|
||||
|
||||
const result = getChartMarginsFromText({
|
||||
tickFontSize: 12,
|
||||
legendFontSize: 12,
|
||||
bottomTickLabels: ['a'],
|
||||
leftTickLabels: ['b'],
|
||||
rightTickLabels: [longLabel],
|
||||
xAxisLabel: 'x',
|
||||
yAxisLabel: 'y',
|
||||
tickRotation: COMMON_CHART_CONSTANTS.NO_ROTATION_ANGLE,
|
||||
bottomLegendOffset: 0,
|
||||
});
|
||||
|
||||
expect(result.right).toBe(TEXT_MARGIN_LIMITS.max.right);
|
||||
});
|
||||
|
||||
it('increases right margin when rightTickLabels are provided', () => {
|
||||
const topRightBase = Math.ceil(12 * 1.5);
|
||||
|
||||
const result = getChartMarginsFromText({
|
||||
tickFontSize: 12,
|
||||
legendFontSize: 12,
|
||||
bottomTickLabels: ['a'],
|
||||
leftTickLabels: ['b'],
|
||||
rightTickLabels: ['$10,000'],
|
||||
xAxisLabel: 'x',
|
||||
yAxisLabel: 'y',
|
||||
tickRotation: COMMON_CHART_CONSTANTS.NO_ROTATION_ANGLE,
|
||||
bottomLegendOffset: 0,
|
||||
});
|
||||
|
||||
expect(result.right).toBeGreaterThan(topRightBase);
|
||||
});
|
||||
|
||||
it('uses bottom legend offset when it exceeds tick and label blocks', () => {
|
||||
const bottomLegendOffset = 100;
|
||||
const result = getChartMarginsFromText({
|
||||
|
||||
-3
@@ -4,7 +4,6 @@ import { getChartMarginsFromText } from '@/page-layout/widgets/graph/utils/getCh
|
||||
type ChartMarginInputs = {
|
||||
bottomTickLabels?: string[];
|
||||
leftTickLabels?: string[];
|
||||
rightTickLabels?: string[];
|
||||
};
|
||||
|
||||
type ComputeChartMarginsParams<TTickConfig, TValueTickResult> = {
|
||||
@@ -77,7 +76,6 @@ export const computeChartMargins = <TTickConfig, TValueTickResult>({
|
||||
legendFontSize,
|
||||
bottomTickLabels: provisionalMarginInputs.bottomTickLabels,
|
||||
leftTickLabels: provisionalMarginInputs.leftTickLabels,
|
||||
rightTickLabels: provisionalMarginInputs.rightTickLabels,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
tickRotation: getTickRotation(provisionalTickConfiguration),
|
||||
@@ -100,7 +98,6 @@ export const computeChartMargins = <TTickConfig, TValueTickResult>({
|
||||
legendFontSize,
|
||||
bottomTickLabels: marginInputs.bottomTickLabels,
|
||||
leftTickLabels: marginInputs.leftTickLabels,
|
||||
rightTickLabels: marginInputs.rightTickLabels,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
tickRotation: getTickRotation(tickConfiguration),
|
||||
|
||||
-38
@@ -25,12 +25,10 @@ export const computeValueTickValues = ({
|
||||
minimum,
|
||||
maximum,
|
||||
tickCount,
|
||||
preserveDomainBounds = false,
|
||||
}: {
|
||||
minimum: number;
|
||||
maximum: number;
|
||||
tickCount: number;
|
||||
preserveDomainBounds?: boolean;
|
||||
}): {
|
||||
tickValues: number[];
|
||||
domain: { min: number; max: number };
|
||||
@@ -54,42 +52,6 @@ export const computeValueTickValues = ({
|
||||
};
|
||||
}
|
||||
|
||||
if (preserveDomainBounds) {
|
||||
if (minimum > maximum) {
|
||||
return {
|
||||
tickValues: [minimum],
|
||||
domain: { min: minimum, max: minimum },
|
||||
};
|
||||
}
|
||||
|
||||
const tickValues: number[] = [Number(minimum.toFixed(12))];
|
||||
const firstInteriorTick =
|
||||
Math.ceil(minimum / niceStepInterval) * niceStepInterval;
|
||||
|
||||
for (
|
||||
let tickValue = firstInteriorTick;
|
||||
tickValue <= maximum + niceStepInterval / 2;
|
||||
tickValue += niceStepInterval
|
||||
) {
|
||||
const roundedTickValue = Number(tickValue.toFixed(12));
|
||||
|
||||
if (roundedTickValue > minimum && roundedTickValue < maximum) {
|
||||
tickValues.push(roundedTickValue);
|
||||
}
|
||||
}
|
||||
|
||||
const roundedMaximum = Number(maximum.toFixed(12));
|
||||
|
||||
if (tickValues[tickValues.length - 1] !== roundedMaximum) {
|
||||
tickValues.push(roundedMaximum);
|
||||
}
|
||||
|
||||
return {
|
||||
tickValues,
|
||||
domain: { min: minimum, max: maximum },
|
||||
};
|
||||
}
|
||||
|
||||
const niceMinimum = Math.floor(minimum / niceStepInterval) * niceStepInterval;
|
||||
const niceMaximum = Math.ceil(maximum / niceStepInterval) * niceStepInterval;
|
||||
const tickValues: number[] = [];
|
||||
|
||||
+1
-15
@@ -22,7 +22,6 @@ export const getChartMarginsFromText = ({
|
||||
legendFontSize,
|
||||
bottomTickLabels,
|
||||
leftTickLabels,
|
||||
rightTickLabels,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
tickRotation,
|
||||
@@ -32,7 +31,6 @@ export const getChartMarginsFromText = ({
|
||||
legendFontSize?: number;
|
||||
bottomTickLabels?: string[];
|
||||
leftTickLabels?: string[];
|
||||
rightTickLabels?: string[];
|
||||
xAxisLabel?: string;
|
||||
yAxisLabel?: string;
|
||||
tickRotation: number;
|
||||
@@ -42,7 +40,6 @@ export const getChartMarginsFromText = ({
|
||||
|
||||
const bottomMaxLabelLength = getMaxLabelLength(bottomTickLabels);
|
||||
const leftMaxLabelLength = getMaxLabelLength(leftTickLabels);
|
||||
const rightMaxLabelLength = getMaxLabelLength(rightTickLabels);
|
||||
|
||||
const tickPaddingExtra = xAxisLabel ? TEXT_MARGIN_EXTRAS.tickPaddingExtra : 0;
|
||||
const bottomTickHeight =
|
||||
@@ -95,17 +92,6 @@ export const getChartMarginsFromText = ({
|
||||
TEXT_MARGIN_LIMITS.max.left,
|
||||
);
|
||||
|
||||
const rightTickWidth =
|
||||
rightMaxLabelLength > 0
|
||||
? estimateLabelWidth(rightMaxLabelLength, tickFontSize)
|
||||
: 0;
|
||||
const rightTicksBlock =
|
||||
rightTickWidth > 0
|
||||
? rightTickWidth +
|
||||
COMMON_CHART_CONSTANTS.TICK_PADDING +
|
||||
TEXT_MARGIN_EXTRAS.rightTickExtra
|
||||
: 0;
|
||||
|
||||
const topRightBase = Math.ceil(tickFontSize * 1.5);
|
||||
|
||||
return {
|
||||
@@ -115,7 +101,7 @@ export const getChartMarginsFromText = ({
|
||||
TEXT_MARGIN_LIMITS.max.top,
|
||||
),
|
||||
right: clamp(
|
||||
Math.max(topRightBase, rightTicksBlock),
|
||||
topRightBase,
|
||||
TEXT_MARGIN_LIMITS.min.right,
|
||||
TEXT_MARGIN_LIMITS.max.right,
|
||||
),
|
||||
|
||||
+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;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user