Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b13c39e02 | ||
|
|
38a49650a3 | ||
|
|
35c4ec0d44 | ||
|
|
791462bab0 | ||
|
|
78aad6733f | ||
|
|
4cb64c6aa5 | ||
|
|
6b3ef404b0 | ||
|
|
6aaafb76b6 |
@@ -27,7 +27,7 @@ description: أنشئ وأدِر تخصيصات Twenty على هيئة كود.
|
||||
أنشئ تطبيقًا جديدًا باستخدام المُهيئ الرسمي، ثم قم بالمصادقة وابدأ التطوير:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# إنشاء تطبيق جديد
|
||||
# إنشاء تطبيق جديد (يتضمن جميع الأمثلة افتراضيًا)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
@@ -42,6 +42,19 @@ yarn twenty auth:login
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
يدعم المُنشئ ثلاثة أوضاع للتحكم في ملفات الأمثلة التي سيتم تضمينها:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# الافتراضي (شامل): جميع الأمثلة (كائن، حقل، دالة منطقية، مكوّن الواجهة الأمامية، عرض، عنصر قائمة التنقل)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# الأدنى: الملفات الأساسية فقط (application-config.ts و default-role.ts)
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
|
||||
# التفاعلي: اختر الأمثلة التي تريد تضمينها
|
||||
npx create-twenty-app@latest my-app --interactive
|
||||
```
|
||||
|
||||
من هنا يمكنك:
|
||||
|
||||
```bash filename="Terminal"
|
||||
@@ -73,9 +86,9 @@ yarn twenty help
|
||||
* ينسخ تطبيقًا أساسيًا مصغّرًا إلى `my-twenty-app/`
|
||||
* يضيف اعتمادًا محليًا `twenty-sdk` وتهيئة Yarn 4
|
||||
* ينشئ ملفات ضبط ونصوصًا مرتبطة بـ `twenty` CLI
|
||||
* Generates a default application config, a default function role, and a post-install function
|
||||
* يُنشئ الملفات الأساسية (تهيئة التطبيق، دور الدالة الافتراضي، دالة ما بعد التثبيت) بالإضافة إلى ملفات الأمثلة بحسب وضع الإنشاء
|
||||
|
||||
يبدو التطبيق المُنشأ حديثًا بالقالب كما يلي:
|
||||
يبدو التطبيق المُنشأ حديثًا باستخدام الوضع الافتراضي `--exhaustive` كما يلي:
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
@@ -89,18 +102,28 @@ my-twenty-app/
|
||||
eslint.config.mjs
|
||||
tsconfig.json
|
||||
README.md
|
||||
public/ # Public assets folder (images, fonts, etc.)
|
||||
public/ # مجلد الأصول العامة (صور، خطوط، إلخ)
|
||||
src/
|
||||
├── application-config.ts # Required - main application configuration
|
||||
├── application-config.ts # مطلوب - إعدادات التطبيق الرئيسية
|
||||
├── roles/
|
||||
│ └── default-role.ts # Default role for logic functions
|
||||
│ └── default-role.ts # الدور الافتراضي للدوال المنطقية
|
||||
├── objects/
|
||||
│ └── example-object.ts # تعريف كائن مخصص — مثال
|
||||
├── fields/
|
||||
│ └── example-field.ts # تعريف حقل مستقل — مثال
|
||||
├── logic-functions/
|
||||
│ ├── hello-world.ts # Example logic function
|
||||
│ └── post-install.ts # Post-install logic function
|
||||
└── front-components/
|
||||
└── hello-world.tsx # Example front component
|
||||
│ ├── hello-world.ts # دالة منطقية — مثال
|
||||
│ └── post-install.ts # دالة منطقية لما بعد التثبيت
|
||||
├── front-components/
|
||||
│ └── hello-world.tsx # مكوّن واجهة أمامية — مثال
|
||||
├── views/
|
||||
│ └── example-view.ts # تعريف عرض محفوظ — مثال
|
||||
└── navigation-menu-items/
|
||||
└── example-navigation-menu-item.ts # رابط تنقّل في الشريط الجانبي — مثال
|
||||
```
|
||||
|
||||
مع `--minimal`، سيتم إنشاء الملفات الأساسية فقط (`application-config.ts` و`roles/default-role.ts` و`logic-functions/post-install.ts`). مع `--interactive`، تختار ملفات الأمثلة التي تريد تضمينها.
|
||||
|
||||
بشكل عام:
|
||||
|
||||
* **package.json**: يصرّح باسم التطبيق والإصدار والمحرّكات (Node 24+، Yarn 4)، ويضيف `twenty-sdk` بالإضافة إلى نص برمجي `twenty` يفوِّض إلى `twenty` CLI المحلي. شغِّل `yarn twenty help` لعرض جميع الأوامر المتاحة.
|
||||
@@ -116,13 +139,15 @@ my-twenty-app/
|
||||
|
||||
يكتشف SDK الكيانات عبر تحليل ملفات TypeScript الخاصة بك بحثًا عن استدعاءات **`export default define<Entity>({...})`**. يحتوي كل نوع كيان على دالة مساعدة مقابلة يتم تصديرها من `twenty-sdk`:
|
||||
|
||||
| دالة مساعدة | نوع الكيان |
|
||||
| ------------------------ | --------------------------------- |
|
||||
| `defineObject()` | تعريفات كائنات مخصصة |
|
||||
| `defineLogicFunction()` | تعريفات الوظائف المنطقية |
|
||||
| `defineFrontComponent()` | Front component definitions |
|
||||
| `defineRole()` | تعريفات الأدوار |
|
||||
| `defineField()` | امتدادات الحقول للكائنات الموجودة |
|
||||
| دالة مساعدة | نوع الكيان |
|
||||
| ---------------------------- | --------------------------------- |
|
||||
| `defineObject()` | تعريفات كائنات مخصصة |
|
||||
| `defineLogicFunction()` | تعريفات الوظائف المنطقية |
|
||||
| `defineFrontComponent()` | Front component definitions |
|
||||
| `defineRole()` | تعريفات الأدوار |
|
||||
| `defineField()` | امتدادات الحقول للكائنات الموجودة |
|
||||
| `defineView()` | تعريفات العروض المحفوظة |
|
||||
| `defineNavigationMenuItem()` | تعريفات عناصر قائمة التنقل |
|
||||
|
||||
<Note>
|
||||
**تسمية الملفات مرنة.** يعتمد اكتشاف الكيانات على بنية الشجرة المجردة (AST) — إذ يقوم SDK بفحص ملفات المصدر لديك بحثًا عن النمط `export default define<Entity>({...})`. يمكنك تنظيم ملفاتك ومجلداتك كيفما تشاء. التجميع حسب نوع الكيان (مثلًا، `logic-functions/` و`roles/`) هو مجرد عرف لتنظيم الشيفرة، وليس مطلبًا إلزاميًا.
|
||||
@@ -187,14 +212,16 @@ yarn twenty auth:status
|
||||
|
||||
يوفّر SDK دوالًا مساعدة لتعريف كيانات تطبيقك. كما هو موضح في [اكتشاف الكيانات](#entity-detection)، يجب استخدام `export default define<Entity>({...})` كي يتم اكتشاف كياناتك:
|
||||
|
||||
| دالة | الغرض |
|
||||
| ------------------------ | ---------------------------------------------------- |
|
||||
| `defineApplication()` | تهيئة بيانات التعريف للتطبيق (مطلوب، واحد لكل تطبيق) |
|
||||
| `defineObject()` | تعريف كائنات مخصصة مع حقول |
|
||||
| `defineLogicFunction()` | تعريف وظائف منطقية مع معالجات |
|
||||
| `defineFrontComponent()` | عرِّف مكوّنات أمامية لواجهة مستخدم مخصّصة |
|
||||
| `defineRole()` | تهيئة صلاحيات الدور والوصول إلى الكائنات |
|
||||
| `defineField()` | وسّع الكائنات الموجودة بحقول إضافية |
|
||||
| دالة | الغرض |
|
||||
| ---------------------------- | ---------------------------------------------------- |
|
||||
| `defineApplication()` | تهيئة بيانات التعريف للتطبيق (مطلوب، واحد لكل تطبيق) |
|
||||
| `defineObject()` | تعريف كائنات مخصصة مع حقول |
|
||||
| `defineLogicFunction()` | تعريف وظائف منطقية مع معالجات |
|
||||
| `defineFrontComponent()` | عرِّف مكوّنات أمامية لواجهة مستخدم مخصّصة |
|
||||
| `defineRole()` | تهيئة صلاحيات الدور والوصول إلى الكائنات |
|
||||
| `defineField()` | وسّع الكائنات الموجودة بحقول إضافية |
|
||||
| `defineView()` | تعريف العروض المحفوظة للكائنات |
|
||||
| `defineNavigationMenuItem()` | تعريف روابط التنقل في الشريط الجانبي |
|
||||
|
||||
تتحقق هذه الدوال من تكوينك وقت البناء وتوفّر إكمالًا تلقائيًا في بيئة التطوير وأمان الأنواع.
|
||||
|
||||
@@ -470,7 +497,7 @@ export default defineLogicFunction({
|
||||
|
||||
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
|
||||
|
||||
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
|
||||
عند إنشاء هيكل تطبيق جديد باستخدام `create-twenty-app`، يتم إنشاء دالة ما بعد التثبيت لك في `src/logic-functions/post-install.ts`:
|
||||
|
||||
```typescript
|
||||
// src/logic-functions/post-install.ts
|
||||
@@ -491,7 +518,7 @@ export default defineLogicFunction({
|
||||
});
|
||||
```
|
||||
|
||||
The function is wired into your app by referencing its universal identifier in `application-config.ts`:
|
||||
يتم ربط الدالة بتطبيقك من خلال الإشارة إلى المعرِّف العالمي الخاص بها في `application-config.ts`:
|
||||
|
||||
```typescript
|
||||
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
|
||||
@@ -502,7 +529,7 @@ export default defineApplication({
|
||||
});
|
||||
```
|
||||
|
||||
You can also manually execute the post-install function at any time using the CLI:
|
||||
يمكنك أيضًا تنفيذ دالة ما بعد التثبيت يدويًا في أي وقت باستخدام CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty function:execute --postInstall
|
||||
@@ -510,10 +537,10 @@ yarn twenty function:execute --postInstall
|
||||
|
||||
النقاط الرئيسية:
|
||||
|
||||
* Post-install functions are standard logic functions — they use `defineLogicFunction()` like any other function.
|
||||
* The `postInstallLogicFunctionUniversalIdentifier` field in `defineApplication()` is optional. If omitted, no function runs after installation.
|
||||
* The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
|
||||
* Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
|
||||
* دوال ما بعد التثبيت هي دوال منطقية قياسية — فهي تستخدم `defineLogicFunction()` مثل أي دالة أخرى.
|
||||
* حقل `postInstallLogicFunctionUniversalIdentifier` في `defineApplication()` اختياري. إذا تم تجاهله، لن يتم تشغيل أي دالة بعد التثبيت.
|
||||
* تم تعيين مهلة افتراضية إلى 300 ثانية (5 دقائق) للسماح بمهام الإعداد الأطول مثل تهيئة البيانات.
|
||||
* لا تحتاج دوال ما بعد التثبيت إلى مُشغِّلات — حيث يستدعيها النظام الأساسي أثناء التثبيت أو يدويًا عبر `function:execute --postInstall`.
|
||||
|
||||
### حمولة مشغل المسار
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ Aplikace vám umožňují vytvářet a spravovat přizpůsobení Twenty **jako k
|
||||
Vytvořte novou aplikaci pomocí oficiálního scaffolderu, poté se ověřte a začněte vyvíjet:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Vygenerujte kostru nové aplikace
|
||||
# Vygenerujte kostru nové aplikace (ve výchozím nastavení zahrnuje všechny příklady)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
@@ -42,6 +42,19 @@ yarn twenty auth:login
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
Nástroj pro generování kostry podporuje tři režimy pro řízení toho, které ukázkové soubory jsou zahrnuty:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Výchozí (úplný): všechny příklady (objekt, pole, logická funkce, front-endová komponenta, zobrazení, položka navigační nabídky)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimální: pouze základní soubory (application-config.ts a default-role.ts)
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
|
||||
# Interaktivní: vyberte, které příklady zahrnout
|
||||
npx create-twenty-app@latest my-app --interactive
|
||||
```
|
||||
|
||||
Odtud můžete:
|
||||
|
||||
```bash filename="Terminal"
|
||||
@@ -73,9 +86,9 @@ Když spustíte `npx create-twenty-app@latest my-twenty-app`, scaffolder:
|
||||
* Zkopíruje minimální základní aplikaci do `my-twenty-app/`
|
||||
* Přidá lokální závislost `twenty-sdk` a konfiguraci pro Yarn 4
|
||||
* Vytvoří konfigurační soubory a skripty napojené na `twenty` CLI
|
||||
* Vygeneruje výchozí konfiguraci aplikace, výchozí roli funkcí a postinstalační funkci.
|
||||
* Vygeneruje základní soubory (konfigurace aplikace, výchozí role funkcí, postinstalační funkce) a k nim ukázkové soubory podle zvoleného režimu generování kostry
|
||||
|
||||
Čerstvě vytvořená aplikace vypadá takto:
|
||||
Čerstvě vygenerovaná aplikace s výchozím režimem `--exhaustive` vypadá takto:
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
@@ -94,13 +107,23 @@ my-twenty-app/
|
||||
├── application-config.ts # Povinné – hlavní konfigurace aplikace
|
||||
├── roles/
|
||||
│ └── default-role.ts # Výchozí role pro logické funkce
|
||||
├── objects/
|
||||
│ └── example-object.ts # Ukázková definice vlastního objektu
|
||||
├── fields/
|
||||
│ └── example-field.ts # Ukázková samostatná definice pole
|
||||
├── logic-functions/
|
||||
│ ├── hello-world.ts # Ukázková logická funkce
|
||||
│ └── post-install.ts # Postinstalační logická funkce
|
||||
└── front-components/
|
||||
└── hello-world.tsx # Ukázková front-endová komponenta
|
||||
├── front-components/
|
||||
│ └── hello-world.tsx # Ukázková front-endová komponenta
|
||||
├── views/
|
||||
│ └── example-view.ts # Ukázková definice uloženého zobrazení
|
||||
└── navigation-menu-items/
|
||||
└── example-navigation-menu-item.ts # Ukázkový odkaz postranní navigace
|
||||
```
|
||||
|
||||
S volbou `--minimal` se vytvoří pouze základní soubory (`application-config.ts`, `roles/default-role.ts` a `logic-functions/post-install.ts`). S volbou `--interactive` si vyberete, které ukázkové soubory chcete zahrnout.
|
||||
|
||||
V kostce:
|
||||
|
||||
* **package.json**: Deklaruje název aplikace, verzi, engines (Node 24+, Yarn 4) a přidává `twenty-sdk` plus skript `twenty`, který deleguje na lokální `twenty` CLI. Spusťte `yarn twenty help` pro výpis všech dostupných příkazů.
|
||||
@@ -116,13 +139,15 @@ V kostce:
|
||||
|
||||
SDK detekuje entity analýzou vašich souborů TypeScript a hledá volání **`export default define<Entity>({...})`**. Každý typ entity má odpovídající pomocnou funkci exportovanou z `twenty-sdk`:
|
||||
|
||||
| Pomocná funkce | Typ entity |
|
||||
| ------------------------ | ------------------------------------- |
|
||||
| `defineObject()` | Definice vlastních objektů |
|
||||
| `defineLogicFunction()` | Definice logických funkcí |
|
||||
| `defineFrontComponent()` | Definice frontendových komponent |
|
||||
| `defineRole()` | Definice rolí |
|
||||
| `defineField()` | Rozšíření polí u existujících objektů |
|
||||
| Pomocná funkce | Typ entity |
|
||||
| ---------------------------- | ------------------------------------- |
|
||||
| `defineObject()` | Definice vlastních objektů |
|
||||
| `defineLogicFunction()` | Definice logických funkcí |
|
||||
| `defineFrontComponent()` | Definice frontendových komponent |
|
||||
| `defineRole()` | Definice rolí |
|
||||
| `defineField()` | Rozšíření polí u existujících objektů |
|
||||
| `defineView()` | Definice uložených zobrazení |
|
||||
| `defineNavigationMenuItem()` | Definice položek navigační nabídky |
|
||||
|
||||
<Note>
|
||||
**Pojmenování souborů je flexibilní.** Detekce entit je založená na AST — SDK prochází vaše zdrojové soubory a hledá vzor `export default define<Entity>({...})`. Soubory a složky můžete organizovat, jak chcete. Seskupování podle typu entity (např. `logic-functions/`, `roles/`) je pouze konvence pro organizaci kódu, nikoli požadavek.
|
||||
@@ -187,14 +212,16 @@ twenty-sdk poskytuje typované stavební bloky a pomocné funkce, které použí
|
||||
|
||||
SDK poskytuje pomocné funkce pro definování entit vaší aplikace. Jak je popsáno v [Detekce entit](#entity-detection), musíte použít `export default define<Entity>({...})`, aby byly vaše entity detekovány:
|
||||
|
||||
| Funkce | Účel |
|
||||
| ------------------------ | ----------------------------------------------------------------- |
|
||||
| `defineApplication()` | Nakonfigurujte metadata aplikace (povinné, jedno na aplikaci) |
|
||||
| `defineObject()` | Definice vlastních objektů s poli |
|
||||
| `defineLogicFunction()` | Definice logických funkcí s obslužnými funkcemi |
|
||||
| `defineFrontComponent()` | Definujte frontendové komponenty pro vlastní uživatelské rozhraní |
|
||||
| `defineRole()` | Konfigurace oprávnění rolí a přístupu k objektům |
|
||||
| `defineField()` | Rozšiřte existující objekty o další pole |
|
||||
| Funkce | Účel |
|
||||
| ---------------------------- | ----------------------------------------------------------------- |
|
||||
| `defineApplication()` | Nakonfigurujte metadata aplikace (povinné, jedno na aplikaci) |
|
||||
| `defineObject()` | Definice vlastních objektů s poli |
|
||||
| `defineLogicFunction()` | Definice logických funkcí s obslužnými funkcemi |
|
||||
| `defineFrontComponent()` | Definujte frontendové komponenty pro vlastní uživatelské rozhraní |
|
||||
| `defineRole()` | Konfigurace oprávnění rolí a přístupu k objektům |
|
||||
| `defineField()` | Rozšiřte existující objekty o další pole |
|
||||
| `defineView()` | Definujte uložená zobrazení pro objekty |
|
||||
| `defineNavigationMenuItem()` | Definujte odkazy postranní navigace |
|
||||
|
||||
Tyto funkce validují vaši konfiguraci v době sestavení a poskytují automatické doplňování v IDE a typovou bezpečnost.
|
||||
|
||||
@@ -470,7 +497,7 @@ Poznámky:
|
||||
|
||||
Postinstalační funkce je logická funkce, která se automaticky spouští po instalaci vaší aplikace do pracovního prostoru. To je užitečné pro jednorázové úlohy nastavení, jako je naplnění výchozími daty, vytvoření počátečních záznamů nebo konfigurace nastavení pracovního prostoru.
|
||||
|
||||
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
|
||||
Když vygenerujete kostru nové aplikace pomocí `create-twenty-app`, vytvoří se pro vás postinstalační funkce v `src/logic-functions/post-install.ts`:
|
||||
|
||||
```typescript
|
||||
// src/logic-functions/post-install.ts
|
||||
@@ -491,7 +518,7 @@ export default defineLogicFunction({
|
||||
});
|
||||
```
|
||||
|
||||
The function is wired into your app by referencing its universal identifier in `application-config.ts`:
|
||||
Funkce je připojena do vaší aplikace odkazem na její univerzální identifikátor v `application-config.ts`:
|
||||
|
||||
```typescript
|
||||
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
|
||||
@@ -502,7 +529,7 @@ export default defineApplication({
|
||||
});
|
||||
```
|
||||
|
||||
You can also manually execute the post-install function at any time using the CLI:
|
||||
Postinstalační funkci můžete také kdykoli spustit ručně pomocí CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty function:execute --postInstall
|
||||
@@ -510,10 +537,10 @@ yarn twenty function:execute --postInstall
|
||||
|
||||
Hlavní body:
|
||||
|
||||
* Post-install functions are standard logic functions — they use `defineLogicFunction()` like any other function.
|
||||
* The `postInstallLogicFunctionUniversalIdentifier` field in `defineApplication()` is optional. If omitted, no function runs after installation.
|
||||
* The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
|
||||
* Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
|
||||
* Postinstalační funkce jsou standardní logické funkce — používají `defineLogicFunction()` stejně jako jakákoli jiná funkce.
|
||||
* Pole `postInstallLogicFunctionUniversalIdentifier` v `defineApplication()` je volitelné. Pokud je vynecháno, po instalaci se nespustí žádná funkce.
|
||||
* Výchozí časový limit je nastaven na 300 sekund (5 minut), aby umožnil delší úlohy nastavení, jako je naplnění daty.
|
||||
* Postinstalační funkce nepotřebují spouštěče — jsou spouštěny platformou během instalace nebo ručně pomocí `function:execute --postInstall`.
|
||||
|
||||
### Payload spouštěče trasy
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ Mit Apps können Sie Twenty-Anpassungen **als Code** erstellen und verwalten. An
|
||||
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
|
||||
# Eine neue App erstellen (enthält standardmäßig alle Beispiele)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
@@ -42,6 +42,19 @@ yarn twenty auth:login
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
Das Scaffolding-Tool unterstützt drei Modi, um zu steuern, welche Beispieldateien enthalten sind:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Standard (umfassend): alle Beispiele (Objekt, Feld, Logikfunktion, Frontend-Komponente, View, Navigationsmenüeintrag)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: nur Kerndateien (application-config.ts und default-role.ts)
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
|
||||
# Interaktiv: wähle aus, welche Beispiele enthalten sein sollen
|
||||
npx create-twenty-app@latest my-app --interactive
|
||||
```
|
||||
|
||||
Von hier aus können Sie:
|
||||
|
||||
```bash filename="Terminal"
|
||||
@@ -73,9 +86,9 @@ Wenn Sie `npx create-twenty-app@latest my-twenty-app` ausführen, erledigt der S
|
||||
* Kopiert eine minimale Basisanwendung nach `my-twenty-app/`
|
||||
* Fügt eine lokale `twenty-sdk`-Abhängigkeit und die Yarn-4-Konfiguration hinzu
|
||||
* Erstellt Konfigurationsdateien und Skripte, die an die `twenty`-CLI angebunden sind
|
||||
* Generiert eine Standard-Anwendungskonfiguration, eine Standard-Funktionsrolle und eine Post-Installationsfunktion
|
||||
* Erzeugt Kerndateien (Anwendungskonfiguration, Standardrolle für Logikfunktionen, Post-Installationsfunktion) sowie Beispieldateien entsprechend dem Scaffolding-Modus
|
||||
|
||||
Eine frisch erzeugte App sieht so aus:
|
||||
Eine frisch erstellte App mit dem Standardmodus `--exhaustive` sieht so aus:
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
@@ -91,16 +104,26 @@ my-twenty-app/
|
||||
README.md
|
||||
public/ # Ordner für öffentliche Assets (Bilder, Schriftarten usw.)
|
||||
src/
|
||||
├── application-config.ts # Erforderlich – Hauptkonfiguration der Anwendung
|
||||
├── roles/
|
||||
│ └── default-role.ts # Standardrolle für Logikfunktionen
|
||||
├── logic-functions/
|
||||
│ ├── hello-world.ts # Beispiel für eine Logikfunktion
|
||||
│ └── post-install.ts # Post-Installations-Logikfunktion
|
||||
└── front-components/
|
||||
└── hello-world.tsx # Beispiel für eine Frontend-Komponente
|
||||
application-config.ts # Erforderlich Hauptkonfiguration der Anwendung
|
||||
roles/
|
||||
default-role.ts # Standardrolle für Logikfunktionen
|
||||
objects/
|
||||
example-object.ts # Beispiel für eine benutzerdefinierte Objektdefinition
|
||||
fields/
|
||||
example-field.ts # Beispiel für eine eigenständige Felddefinition
|
||||
logic-functions/
|
||||
hello-world.ts # Beispiel für eine Logikfunktion
|
||||
post-install.ts # Post-Installations-Logikfunktion
|
||||
front-components/
|
||||
hello-world.tsx # Beispiel für eine Frontend-Komponente
|
||||
views/
|
||||
example-view.ts # Beispiel für eine gespeicherte View-Definition
|
||||
navigation-menu-items/
|
||||
example-navigation-menu-item.ts # Beispiel für einen Navigationslink in der Seitenleiste
|
||||
```
|
||||
|
||||
Mit `--minimal` werden nur die Kerndateien erstellt (`application-config.ts`, `roles/default-role.ts` und `logic-functions/post-install.ts`). Mit `--interactive` wählst du aus, welche Beispieldateien enthalten sein sollen.
|
||||
|
||||
Auf hoher Ebene:
|
||||
|
||||
* **package.json**: Deklariert den App-Namen, die Version und die Engines (Node 24+, Yarn 4) und fügt `twenty-sdk` sowie ein `twenty`-Skript hinzu, das an die lokale `twenty`-CLI delegiert. Führe `yarn twenty help` aus, um alle verfügbaren Befehle aufzulisten.
|
||||
@@ -116,13 +139,15 @@ Auf hoher Ebene:
|
||||
|
||||
Das SDK erkennt Entitäten, indem es Ihre TypeScript-Dateien nach Aufrufen von **`export default define<Entity>({...})`** parst. Für jeden Entitätstyp gibt es eine entsprechende Hilfsfunktion, die aus `twenty-sdk` exportiert wird:
|
||||
|
||||
| Hilfsfunktion | Entitätstyp |
|
||||
| ------------------------ | ---------------------------------------- |
|
||||
| `defineObject()` | Benutzerdefinierte Objektdefinitionen |
|
||||
| `defineLogicFunction()` | Definitionen von Logikfunktionen |
|
||||
| `defineFrontComponent()` | Definitionen von Frontend-Komponenten |
|
||||
| `defineRole()` | Rollendefinitionen |
|
||||
| `defineField()` | Felderweiterungen für bestehende Objekte |
|
||||
| Hilfsfunktion | Entitätstyp |
|
||||
| ---------------------------- | ----------------------------------------- |
|
||||
| `defineObject()` | Benutzerdefinierte Objektdefinitionen |
|
||||
| `defineLogicFunction()` | Definitionen von Logikfunktionen |
|
||||
| `defineFrontComponent()` | Definitionen von Frontend-Komponenten |
|
||||
| `defineRole()` | Rollendefinitionen |
|
||||
| `defineField()` | Felderweiterungen für bestehende Objekte |
|
||||
| `defineView()` | Gespeicherte View-Definitionen |
|
||||
| `defineNavigationMenuItem()` | Definitionen von Navigationsmenüeinträgen |
|
||||
|
||||
<Note>
|
||||
**Dateibenennung ist flexibel.** Die Entitätserkennung ist AST-basiert — das SDK durchsucht Ihre Quelldateien nach dem Muster `export default define<Entity>({...})`. Sie können Ihre Dateien und Ordner nach Belieben organisieren. Die Gruppierung nach Entitätstyp (z. B. `logic-functions/`, `roles/`) ist lediglich eine Konvention zur Codeorganisation, keine Voraussetzung.
|
||||
@@ -187,14 +212,16 @@ Das twenty-sdk stellt typisierte Bausteine und Hilfsfunktionen bereit, die Sie i
|
||||
|
||||
Das SDK stellt Hilfsfunktionen bereit, um die Entitäten Ihrer App zu definieren. Wie in [Entitätserkennung](#entity-detection) beschrieben, müssen Sie `export default define<Entity>({...})` verwenden, damit Ihre Entitäten erkannt werden:
|
||||
|
||||
| Funktion | Zweck |
|
||||
| ------------------------ | -------------------------------------------------------------- |
|
||||
| `defineApplication()` | Anwendungsmetadaten konfigurieren (erforderlich, eine pro App) |
|
||||
| `defineObject()` | Benutzerdefinierte Objekte mit Feldern definieren |
|
||||
| `defineLogicFunction()` | Logikfunktionen mit Handlern definieren |
|
||||
| `defineFrontComponent()` | Frontend-Komponenten für benutzerdefinierte UI definieren |
|
||||
| `defineRole()` | Rollenberechtigungen und Objektzugriff konfigurieren |
|
||||
| `defineField()` | Bestehende Objekte mit zusätzlichen Feldern erweitern |
|
||||
| Funktion | Zweck |
|
||||
| ---------------------------- | -------------------------------------------------------------- |
|
||||
| `defineApplication()` | Anwendungsmetadaten konfigurieren (erforderlich, eine pro App) |
|
||||
| `defineObject()` | Benutzerdefinierte Objekte mit Feldern definieren |
|
||||
| `defineLogicFunction()` | Logikfunktionen mit Handlern definieren |
|
||||
| `defineFrontComponent()` | Frontend-Komponenten für benutzerdefinierte UI definieren |
|
||||
| `defineRole()` | Rollenberechtigungen und Objektzugriff konfigurieren |
|
||||
| `defineField()` | Bestehende Objekte mit zusätzlichen Feldern erweitern |
|
||||
| `defineView()` | Gespeicherte Views für Objekte definieren |
|
||||
| `defineNavigationMenuItem()` | Seitenleisten-Navigationslinks definieren |
|
||||
|
||||
Diese Funktionen validieren Ihre Konfiguration zur Build-Zeit und bieten IDE-Autovervollständigung sowie Typsicherheit.
|
||||
|
||||
@@ -470,7 +497,7 @@ Notizen:
|
||||
|
||||
Eine Post-Installationsfunktion ist eine Logikfunktion, die automatisch ausgeführt wird, nachdem Ihre App in einem Arbeitsbereich installiert wurde. Dies ist nützlich für einmalige Einrichtungsvorgänge wie das Befüllen mit Standarddaten, das Erstellen erster Datensätze oder das Konfigurieren von Arbeitsbereichseinstellungen.
|
||||
|
||||
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
|
||||
Wenn du mit `create-twenty-app` eine neue App erstellst, wird für dich eine Post-Installationsfunktion unter `src/logic-functions/post-install.ts` erzeugt:
|
||||
|
||||
```typescript
|
||||
// src/logic-functions/post-install.ts
|
||||
@@ -491,7 +518,7 @@ export default defineLogicFunction({
|
||||
});
|
||||
```
|
||||
|
||||
The function is wired into your app by referencing its universal identifier in `application-config.ts`:
|
||||
Die Funktion wird in deine App eingebunden, indem ihr universeller Bezeichner in `application-config.ts` referenziert wird:
|
||||
|
||||
```typescript
|
||||
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
|
||||
@@ -502,7 +529,7 @@ export default defineApplication({
|
||||
});
|
||||
```
|
||||
|
||||
You can also manually execute the post-install function at any time using the CLI:
|
||||
Du kannst die Post-Installationsfunktion auch jederzeit manuell über die CLI ausführen:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty function:execute --postInstall
|
||||
@@ -510,10 +537,10 @@ yarn twenty function:execute --postInstall
|
||||
|
||||
Hauptpunkte:
|
||||
|
||||
* Post-install functions are standard logic functions — they use `defineLogicFunction()` like any other function.
|
||||
* The `postInstallLogicFunctionUniversalIdentifier` field in `defineApplication()` is optional. If omitted, no function runs after installation.
|
||||
* The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
|
||||
* Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
|
||||
* Post-Installationsfunktionen sind Standard-Logikfunktionen — sie verwenden `defineLogicFunction()` wie jede andere Funktion.
|
||||
* Das Feld `postInstallLogicFunctionUniversalIdentifier` in `defineApplication()` ist optional. Wenn es weggelassen wird, wird nach der Installation keine Funktion ausgeführt.
|
||||
* Das standardmäßige Timeout ist auf 300 Sekunden (5 Minuten) festgelegt, um längere Einrichtungsvorgänge wie Daten-Seeding zu ermöglichen.
|
||||
* Post-Installationsfunktionen benötigen keine Trigger — sie werden von der Plattform während der Installation oder manuell über `function:execute --postInstall` aufgerufen.
|
||||
|
||||
### Routen-Trigger-Payload
|
||||
|
||||
|
||||
@@ -27,21 +27,34 @@ Le app ti consentono di creare e gestire le personalizzazioni di Twenty **come c
|
||||
Crea una nuova app utilizzando lo scaffolder ufficiale, quindi autenticati e inizia a sviluppare:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Crea lo scaffold di una nuova app
|
||||
# Scaffold a new app (includes all examples by default)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# Se non usi yarn@4
|
||||
# If you don't use yarn@4
|
||||
corepack enable
|
||||
yarn install
|
||||
|
||||
# Autenticati usando la tua API key (ti verrà richiesto)
|
||||
# Authenticate using your API key (you'll be prompted)
|
||||
yarn twenty auth:login
|
||||
|
||||
# Avvia la modalità di sviluppo: sincronizza automaticamente le modifiche locali con il tuo workspace
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
The scaffolder supports three modes for controlling which example files are included:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
|
||||
# Interactive: select which examples to include
|
||||
npx create-twenty-app@latest my-app --interactive
|
||||
```
|
||||
|
||||
Da qui puoi:
|
||||
|
||||
```bash filename="Terminal"
|
||||
@@ -73,9 +86,9 @@ Quando esegui `npx create-twenty-app@latest my-twenty-app`, lo scaffolder:
|
||||
* Copia un'applicazione base minimale in `my-twenty-app/`
|
||||
* Aggiunge una dipendenza locale `twenty-sdk` e la configurazione di Yarn 4
|
||||
* Crea file di configurazione e script collegati alla CLI `twenty`
|
||||
* Genera una configurazione applicativa predefinita, un ruolo funzione predefinito e una funzione post-installazione
|
||||
* Generates core files (application config, default function role, post-install function) plus example files based on the scaffolding mode
|
||||
|
||||
Un'app appena generata dallo scaffolder si presenta così:
|
||||
A freshly scaffolded app with the default `--exhaustive` mode looks like this:
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
@@ -89,18 +102,28 @@ my-twenty-app/
|
||||
eslint.config.mjs
|
||||
tsconfig.json
|
||||
README.md
|
||||
public/ # Cartella delle risorse pubbliche (immagini, font, ecc.)
|
||||
public/ # Public assets folder (images, fonts, etc.)
|
||||
src/
|
||||
├── application-config.ts # Obbligatorio - configurazione principale dell'applicazione
|
||||
├── application-config.ts # Required - main application configuration
|
||||
├── roles/
|
||||
│ └── default-role.ts # Ruolo predefinito per le funzioni logiche
|
||||
│ └── default-role.ts # Default role for logic functions
|
||||
├── objects/
|
||||
│ └── example-object.ts # Example custom object definition
|
||||
├── fields/
|
||||
│ └── example-field.ts # Example standalone field definition
|
||||
├── logic-functions/
|
||||
│ ├── hello-world.ts # Funzione logica di esempio
|
||||
│ └── post-install.ts # Funzione logica post-installazione
|
||||
└── front-components/
|
||||
└── hello-world.tsx # Componente front-end di esempio
|
||||
│ ├── hello-world.ts # Example logic function
|
||||
│ └── post-install.ts # Post-install logic function
|
||||
├── front-components/
|
||||
│ └── hello-world.tsx # Example front component
|
||||
├── views/
|
||||
│ └── example-view.ts # Example saved view definition
|
||||
└── navigation-menu-items/
|
||||
└── example-navigation-menu-item.ts # Example sidebar navigation link
|
||||
```
|
||||
|
||||
With `--minimal`, only the core files are created (`application-config.ts`, `roles/default-role.ts`, and `logic-functions/post-install.ts`). With `--interactive`, you choose which example files to include.
|
||||
|
||||
A livello generale:
|
||||
|
||||
* **package.json**: Dichiara il nome dell'app, la versione, i motori (Node 24+, Yarn 4) e aggiunge `twenty-sdk` più uno script `twenty` che delega alla CLI locale `twenty`. Esegui `yarn twenty help` per elencare tutti i comandi disponibili.
|
||||
@@ -116,13 +139,15 @@ A livello generale:
|
||||
|
||||
L'SDK rileva le entità analizzando i tuoi file TypeScript alla ricerca di chiamate **`export default define<Entity>({...})`**. Ogni tipo di entità ha una corrispondente funzione helper esportata da `twenty-sdk`:
|
||||
|
||||
| Funzione helper | Tipo di entità |
|
||||
| ------------------------ | ----------------------------------------- |
|
||||
| `defineObject()` | Definizioni di oggetti personalizzati |
|
||||
| `defineLogicFunction()` | Definizioni di funzioni logiche |
|
||||
| `defineFrontComponent()` | Definizioni dei componenti front-end |
|
||||
| `defineRole()` | Definizioni di ruoli |
|
||||
| `defineField()` | Estensioni di campo per oggetti esistenti |
|
||||
| Funzione helper | Tipo di entità |
|
||||
| ---------------------------- | ----------------------------------------- |
|
||||
| `defineObject()` | Definizioni di oggetti personalizzati |
|
||||
| `defineLogicFunction()` | Definizioni di funzioni logiche |
|
||||
| `defineFrontComponent()` | Definizioni dei componenti front-end |
|
||||
| `defineRole()` | Definizioni di ruoli |
|
||||
| `defineField()` | Estensioni di campo per oggetti esistenti |
|
||||
| `defineView()` | Saved view definitions |
|
||||
| `defineNavigationMenuItem()` | Navigation menu item definitions |
|
||||
|
||||
<Note>
|
||||
**La denominazione dei file è flessibile.** Il rilevamento delle entità è basato sull'AST — l'SDK esegue la scansione dei file sorgente alla ricerca del pattern `export default define<Entity>({...})`. Puoi organizzare file e cartelle come preferisci. Raggruppare per tipo di entità (ad es., `logic-functions/`, `roles/`) è solo una convenzione per l'organizzazione del codice, non un requisito.
|
||||
@@ -187,14 +212,16 @@ Il pacchetto twenty-sdk fornisce blocchi tipizzati e funzioni helper da usare ne
|
||||
|
||||
L'SDK fornisce funzioni helper per definire le entità della tua app. Come descritto in [Rilevamento delle entità](#entity-detection), devi usare `export default define<Entity>({...})` affinché le tue entità vengano rilevate:
|
||||
|
||||
| Funzione | Scopo |
|
||||
| ------------------------ | ----------------------------------------------------------------------- |
|
||||
| `defineApplication()` | Configura i metadati dell'applicazione (obbligatorio, uno per app) |
|
||||
| `defineObject()` | Definisci oggetti personalizzati con campi |
|
||||
| `defineLogicFunction()` | Definisci funzioni logiche con handler |
|
||||
| `defineFrontComponent()` | Definisci componenti front-end per un'interfaccia utente personalizzata |
|
||||
| `defineRole()` | Configura i permessi dei ruoli e l'accesso agli oggetti |
|
||||
| `defineField()` | Estendi gli oggetti esistenti con campi aggiuntivi |
|
||||
| Funzione | Scopo |
|
||||
| ---------------------------- | ----------------------------------------------------------------------- |
|
||||
| `defineApplication()` | Configura i metadati dell'applicazione (obbligatorio, uno per app) |
|
||||
| `defineObject()` | Definisci oggetti personalizzati con campi |
|
||||
| `defineLogicFunction()` | Definisci funzioni logiche con handler |
|
||||
| `defineFrontComponent()` | Definisci componenti front-end per un'interfaccia utente personalizzata |
|
||||
| `defineRole()` | Configura i permessi dei ruoli e l'accesso agli oggetti |
|
||||
| `defineField()` | Estendi gli oggetti esistenti con campi aggiuntivi |
|
||||
| `defineView()` | Define saved views for objects |
|
||||
| `defineNavigationMenuItem()` | Define sidebar navigation links |
|
||||
|
||||
Queste funzioni convalidano la configurazione in fase di build e offrono il completamento automatico nell'IDE e la sicurezza dei tipi.
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ Os aplicativos permitem criar e gerenciar personalizações do Twenty **como có
|
||||
Crie um novo aplicativo usando o gerador oficial, depois autentique-se e comece a desenvolver:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Scaffold a new app
|
||||
# Scaffold a new app (includes all examples by default)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
@@ -42,25 +42,38 @@ yarn twenty auth:login
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
The scaffolder supports three modes for controlling which example files are included:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
|
||||
# Interactive: select which examples to include
|
||||
npx create-twenty-app@latest my-app --interactive
|
||||
```
|
||||
|
||||
A partir daqui você pode:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Add a new entity to your application (guided)
|
||||
# Adicionar uma nova entidade à sua aplicação (assistido)
|
||||
yarn twenty entity:add
|
||||
|
||||
# Watch your application's function logs
|
||||
# Acompanhar os logs das funções da sua aplicação
|
||||
yarn twenty function:logs
|
||||
|
||||
# Execute a function by name
|
||||
yarn twenty function:execute -n my-function -p '{"name": "test"}'
|
||||
# Executar uma função pelo nome
|
||||
yarn twenty function:execute -n my-function -p '{\"name\": \"test\"}'
|
||||
|
||||
# Execute the post-install function
|
||||
# Executar a função de pós-instalação
|
||||
yarn twenty function:execute --postInstall
|
||||
|
||||
# Uninstall the application from the current workspace
|
||||
# Desinstalar a aplicação do espaço de trabalho atual
|
||||
yarn twenty app:uninstall
|
||||
|
||||
# Display commands' help
|
||||
# Exibir a ajuda dos comandos
|
||||
yarn twenty help
|
||||
```
|
||||
|
||||
@@ -73,9 +86,9 @@ Ao executar `npx create-twenty-app@latest my-twenty-app`, o gerador:
|
||||
* Copia um aplicativo base mínimo para `my-twenty-app/`
|
||||
* Adiciona uma dependência local `twenty-sdk` e a configuração do Yarn 4
|
||||
* Cria arquivos de configuração e scripts conectados à CLI `twenty`
|
||||
* Generates a default application config, a default function role, and a post-install function
|
||||
* Generates core files (application config, default function role, post-install function) plus example files based on the scaffolding mode
|
||||
|
||||
Um aplicativo recém-criado pelo scaffold fica assim:
|
||||
A freshly scaffolded app with the default `--exhaustive` mode looks like this:
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
@@ -94,13 +107,23 @@ my-twenty-app/
|
||||
├── application-config.ts # Required - main application configuration
|
||||
├── roles/
|
||||
│ └── default-role.ts # Default role for logic functions
|
||||
├── objects/
|
||||
│ └── example-object.ts # Example custom object definition
|
||||
├── fields/
|
||||
│ └── example-field.ts # Example standalone field definition
|
||||
├── logic-functions/
|
||||
│ ├── hello-world.ts # Example logic function
|
||||
│ └── post-install.ts # Post-install logic function
|
||||
└── front-components/
|
||||
└── hello-world.tsx # Example front component
|
||||
├── front-components/
|
||||
│ └── hello-world.tsx # Example front component
|
||||
├── views/
|
||||
│ └── example-view.ts # Example saved view definition
|
||||
└── navigation-menu-items/
|
||||
└── example-navigation-menu-item.ts # Example sidebar navigation link
|
||||
```
|
||||
|
||||
With `--minimal`, only the core files are created (`application-config.ts`, `roles/default-role.ts`, and `logic-functions/post-install.ts`). With `--interactive`, you choose which example files to include.
|
||||
|
||||
Em alto nível:
|
||||
|
||||
* **package.json**: Declara o nome do app, versão, engines (Node 24+, Yarn 4), e adiciona `twenty-sdk` além de um script `twenty` que delega para a CLI `twenty` local. Execute `yarn twenty help` para listar todos os comandos disponíveis.
|
||||
@@ -116,13 +139,15 @@ Em alto nível:
|
||||
|
||||
O SDK detecta entidades analisando seus arquivos TypeScript em busca de chamadas **`export default define<Entity>({...})`**. Cada tipo de entidade tem uma função utilitária correspondente exportada de `twenty-sdk`:
|
||||
|
||||
| Função utilitária | Tipo de entidade |
|
||||
| ------------------------ | ------------------------------------------- |
|
||||
| `defineObject()` | Definições de objetos personalizados |
|
||||
| `defineLogicFunction()` | Definições de funções de lógica |
|
||||
| `defineFrontComponent()` | Definições de componentes de front-end |
|
||||
| `defineRole()` | Definições de papéis |
|
||||
| `defineField()` | Extensões de campos para objetos existentes |
|
||||
| Função utilitária | Tipo de entidade |
|
||||
| ---------------------------- | ------------------------------------------- |
|
||||
| `defineObject()` | Definições de objetos personalizados |
|
||||
| `defineLogicFunction()` | Definições de funções de lógica |
|
||||
| `defineFrontComponent()` | Definições de componentes de front-end |
|
||||
| `defineRole()` | Definições de papéis |
|
||||
| `defineField()` | Extensões de campos para objetos existentes |
|
||||
| `defineView()` | Saved view definitions |
|
||||
| `defineNavigationMenuItem()` | Navigation menu item definitions |
|
||||
|
||||
<Note>
|
||||
**A nomeação de arquivos é flexível.** A detecção de entidades é baseada em AST — o SDK varre seus arquivos fonte em busca do padrão `export default define<Entity>({...})`. Você pode organizar seus arquivos e pastas como quiser. Agrupar por tipo de entidade (por exemplo, `logic-functions/`, `roles/`) é apenas uma convenção para organização do código, não um requisito.
|
||||
@@ -187,14 +212,16 @@ O twenty-sdk fornece blocos de construção tipados e funções utilitárias que
|
||||
|
||||
O SDK fornece funções utilitárias para definir as entidades do seu app. Conforme descrito em [Detecção de entidades](#entity-detection), você deve usar `export default define<Entity>({...})` para que suas entidades sejam detectadas:
|
||||
|
||||
| Função | Finalidade |
|
||||
| ------------------------ | ------------------------------------------------------------ |
|
||||
| `defineApplication()` | Configurar metadados do aplicativo (obrigatório, um por app) |
|
||||
| `defineObject()` | Define objetos personalizados com campos |
|
||||
| `defineLogicFunction()` | Defina funções de lógica com handlers |
|
||||
| `defineFrontComponent()` | Definir componentes de front-end para UI personalizada |
|
||||
| `defineRole()` | Configura permissões de papéis e acesso a objetos |
|
||||
| `defineField()` | Estender objetos existentes com campos adicionais |
|
||||
| Função | Finalidade |
|
||||
| ---------------------------- | ------------------------------------------------------------ |
|
||||
| `defineApplication()` | Configurar metadados do aplicativo (obrigatório, um por app) |
|
||||
| `defineObject()` | Define objetos personalizados com campos |
|
||||
| `defineLogicFunction()` | Defina funções de lógica com handlers |
|
||||
| `defineFrontComponent()` | Definir componentes de front-end para UI personalizada |
|
||||
| `defineRole()` | Configura permissões de papéis e acesso a objetos |
|
||||
| `defineField()` | Estender objetos existentes com campos adicionais |
|
||||
| `defineView()` | Define saved views for objects |
|
||||
| `defineNavigationMenuItem()` | Define sidebar navigation links |
|
||||
|
||||
Essas funções validam sua configuração em tempo de compilação e oferecem autocompletar na IDE e segurança de tipos.
|
||||
|
||||
@@ -294,7 +321,7 @@ Todo aplicativo tem um único arquivo `application-config.ts` que descreve:
|
||||
* **O que é o aplicativo**: identificadores, nome de exibição e descrição.
|
||||
* **Como suas funções são executadas**: qual papel usam para permissões.
|
||||
* **Variáveis (opcional)**: pares chave–valor expostos às suas funções como variáveis de ambiente.
|
||||
* **(Optional) post-install function**: a logic function that runs after the app is installed.
|
||||
* **(Opcional) função de pós-instalação**: uma função de lógica que é executada após a instalação da aplicação.
|
||||
|
||||
Use `defineApplication()` to define your application configuration:
|
||||
|
||||
@@ -327,7 +354,7 @@ Notas:
|
||||
* `universalIdentifier` são IDs determinísticos que você controla; gere-os uma vez e mantenha-os estáveis entre sincronizações.
|
||||
* `applicationVariables` tornam-se variáveis de ambiente para suas funções (por exemplo, `DEFAULT_RECIPIENT_NAME` fica disponível como `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
* `defaultRoleUniversalIdentifier` deve corresponder ao arquivo do papel (veja abaixo).
|
||||
* `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#post-install-functions).
|
||||
* `postInstallLogicFunctionUniversalIdentifier` (opcional) aponta para uma função de lógica que é executada automaticamente após a instalação da aplicação. Consulte [Funções de pós-instalação](#post-install-functions).
|
||||
|
||||
#### Papéis e permissões
|
||||
|
||||
@@ -466,9 +493,9 @@ Notas:
|
||||
* O array `triggers` é opcional. Funções sem gatilhos podem ser usadas como funções utilitárias chamadas por outras funções.
|
||||
* Você pode misturar vários tipos de gatilho em uma única função.
|
||||
|
||||
### Post-install functions
|
||||
### Funções de pós-instalação
|
||||
|
||||
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
|
||||
Uma função de pós-instalação é uma função de lógica que é executada automaticamente após a sua aplicação ser instalada em um espaço de trabalho. Isso é útil para tarefas de configuração únicas, como preencher dados padrão, criar registros iniciais ou configurar as configurações do espaço de trabalho.
|
||||
|
||||
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ Aplicațiile vă permit să construiți și să gestionați personalizările Twe
|
||||
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
|
||||
# Creează scheletul unei aplicații noi (include toate exemplele în mod implicit)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
@@ -42,6 +42,19 @@ yarn twenty auth:login
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
Generatorul de schelet acceptă trei moduri pentru a controla ce fișiere de exemplu sunt incluse:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Implicit (exhaustiv): toate exemplele (obiect, câmp, funcție logică, componentă de interfață, vizualizare, element de meniu de navigare)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: doar fișierele de bază (application-config.ts și default-role.ts)
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
|
||||
# Interactiv: selectezi ce exemple să incluzi
|
||||
npx create-twenty-app@latest my-app --interactive
|
||||
```
|
||||
|
||||
De aici puteți:
|
||||
|
||||
```bash filename="Terminal"
|
||||
@@ -73,9 +86,9 @@ Când rulați `npx create-twenty-app@latest my-twenty-app`, generatorul:
|
||||
* Copiază o aplicație de bază minimală în `my-twenty-app/`
|
||||
* Adaugă o dependență locală `twenty-sdk` și configurația Yarn 4
|
||||
* Creează fișiere de configurare și scripturi conectate la CLI-ul `twenty`
|
||||
* Generează o configurație implicită a aplicației, un rol implicit pentru funcții și o funcție post-instalare
|
||||
* Generează fișierele de bază (configurația aplicației, rolul implicit al funcțiilor, funcția post-instalare) plus fișiere de exemplu în funcție de modul de generare a scheletului
|
||||
|
||||
O aplicație nou generată arată astfel:
|
||||
O aplicație proaspăt generată cu modul implicit `--exhaustive` arată astfel:
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
@@ -94,13 +107,23 @@ my-twenty-app/
|
||||
├── application-config.ts # Obligatoriu - configurația principală a aplicației
|
||||
├── roles/
|
||||
│ └── default-role.ts # Rol implicit pentru funcțiile logice
|
||||
├── objects/
|
||||
│ └── example-object.ts # Exemplu de definiție a unui obiect personalizat
|
||||
├── fields/
|
||||
│ └── example-field.ts # Exemplu de definiție de câmp independent
|
||||
├── logic-functions/
|
||||
│ ├── hello-world.ts # Exemplu de funcție logică
|
||||
│ └── post-install.ts # Funcție logică post-instalare
|
||||
└── front-components/
|
||||
└── hello-world.tsx # Exemplu de componentă de interfață
|
||||
├── front-components/
|
||||
│ └── hello-world.tsx # Exemplu de componentă de interfață
|
||||
├── views/
|
||||
│ └── example-view.ts # Exemplu de definiție a unei vizualizări salvate
|
||||
└── navigation-menu-items/
|
||||
└── example-navigation-menu-item.ts # Exemplu de link de navigare în bara laterală
|
||||
```
|
||||
|
||||
Cu `--minimal`, sunt create doar fișierele de bază (`application-config.ts`, `roles/default-role.ts` și `logic-functions/post-install.ts`). Cu `--interactive`, alegi ce fișiere de exemplu să incluzi.
|
||||
|
||||
Pe scurt:
|
||||
|
||||
* **package.json**: Declară numele aplicației, versiunea, motoarele (Node 24+, Yarn 4) și adaugă `twenty-sdk` plus un script `twenty` care deleagă către CLI-ul local `twenty`. Rulează `yarn twenty help` pentru a lista toate comenzile disponibile.
|
||||
@@ -116,13 +139,15 @@ Pe scurt:
|
||||
|
||||
SDK-ul detectează entitățile analizând fișierele TypeScript pentru apeluri **`export default define<Entity>({...})`**. Fiecare tip de entitate are o funcție ajutătoare corespunzătoare, exportată din `twenty-sdk`:
|
||||
|
||||
| Funcție ajutătoare | Tipul entității |
|
||||
| ------------------------ | ------------------------------------------- |
|
||||
| `defineObject()` | Definiții de obiecte personalizate |
|
||||
| `defineLogicFunction()` | Definiții de funcții de logică |
|
||||
| `defineFrontComponent()` | Definiții ale componentelor de interfață |
|
||||
| `defineRole()` | Definiții de rol |
|
||||
| `defineField()` | Extensii de câmp pentru obiectele existente |
|
||||
| Funcție ajutătoare | Tipul entității |
|
||||
| ---------------------------- | ---------------------------------------------- |
|
||||
| `defineObject()` | Definiții de obiecte personalizate |
|
||||
| `defineLogicFunction()` | Definiții de funcții de logică |
|
||||
| `defineFrontComponent()` | Definiții ale componentelor de interfață |
|
||||
| `defineRole()` | Definiții de rol |
|
||||
| `defineField()` | Extensii de câmp pentru obiectele existente |
|
||||
| `defineView()` | Definiții pentru vizualizări salvate |
|
||||
| `defineNavigationMenuItem()` | Definiții pentru elemente de meniu de navigare |
|
||||
|
||||
<Note>
|
||||
**Denumirea fișierelor este flexibilă.** Detectarea entităților se bazează pe AST — SDK-ul scanează fișierele sursă pentru tiparul `export default define<Entity>({...})`. Puteți organiza fișierele și folderele cum doriți. Gruparea după tipul de entitate (de exemplu, `logic-functions/`, `roles/`) este doar o convenție pentru organizarea codului, nu o cerință.
|
||||
@@ -187,14 +212,16 @@ Biblioteca twenty-sdk oferă blocuri de bază tipizate și funcții ajutătoare
|
||||
|
||||
SDK-ul oferă funcții ajutătoare pentru definirea entităților aplicației. După cum este descris în [Detectarea entităților](#entity-detection), trebuie să folosiți `export default define<Entity>({...})` pentru ca entitățile să fie detectate:
|
||||
|
||||
| Funcție | Scop |
|
||||
| ------------------------ | ---------------------------------------------------------------------- |
|
||||
| `defineApplication()` | Configurați metadatele aplicației (obligatoriu, una per aplicație) |
|
||||
| `defineObject()` | Definiți obiecte personalizate cu câmpuri |
|
||||
| `defineLogicFunction()` | Definiți funcții de logică cu handleri |
|
||||
| `defineFrontComponent()` | Definiți componente Front pentru interfața de utilizator personalizată |
|
||||
| `defineRole()` | Configurați permisiunile rolurilor și accesul la obiecte |
|
||||
| `defineField()` | Extindeți obiectele existente cu câmpuri suplimentare |
|
||||
| Funcție | Scop |
|
||||
| ---------------------------- | ---------------------------------------------------------------------- |
|
||||
| `defineApplication()` | Configurați metadatele aplicației (obligatoriu, una per aplicație) |
|
||||
| `defineObject()` | Definiți obiecte personalizate cu câmpuri |
|
||||
| `defineLogicFunction()` | Definiți funcții de logică cu handleri |
|
||||
| `defineFrontComponent()` | Definiți componente Front pentru interfața de utilizator personalizată |
|
||||
| `defineRole()` | Configurați permisiunile rolurilor și accesul la obiecte |
|
||||
| `defineField()` | Extindeți obiectele existente cu câmpuri suplimentare |
|
||||
| `defineView()` | Definește vizualizări salvate pentru obiecte |
|
||||
| `defineNavigationMenuItem()` | Definește linkuri de navigare în bara laterală |
|
||||
|
||||
Aceste funcții validează configurația în timpul build-ului și oferă completare automată în IDE și siguranța tipurilor.
|
||||
|
||||
@@ -470,7 +497,7 @@ Notițe:
|
||||
|
||||
O funcție post-instalare este o funcție logică care rulează automat după instalarea aplicației într-un spațiu de lucru. Aceasta este utilă pentru sarcini de configurare unice, cum ar fi popularea cu date implicite, crearea înregistrărilor inițiale sau configurarea setărilor spațiului de lucru.
|
||||
|
||||
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
|
||||
Când creezi scheletul unei aplicații noi cu `create-twenty-app`, este generată o funcție post-instalare la `src/logic-functions/post-install.ts`:
|
||||
|
||||
```typescript
|
||||
// src/logic-functions/post-install.ts
|
||||
@@ -491,7 +518,7 @@ export default defineLogicFunction({
|
||||
});
|
||||
```
|
||||
|
||||
The function is wired into your app by referencing its universal identifier in `application-config.ts`:
|
||||
Funcția este integrată în aplicația ta prin referirea la identificatorul său universal în `application-config.ts`:
|
||||
|
||||
```typescript
|
||||
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
|
||||
@@ -502,7 +529,7 @@ export default defineApplication({
|
||||
});
|
||||
```
|
||||
|
||||
You can also manually execute the post-install function at any time using the CLI:
|
||||
Poți, de asemenea, să execuți manual funcția post-instalare oricând folosind CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty function:execute --postInstall
|
||||
@@ -510,10 +537,10 @@ yarn twenty function:execute --postInstall
|
||||
|
||||
Puncte cheie:
|
||||
|
||||
* Post-install functions are standard logic functions — they use `defineLogicFunction()` like any other function.
|
||||
* The `postInstallLogicFunctionUniversalIdentifier` field in `defineApplication()` is optional. If omitted, no function runs after installation.
|
||||
* The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
|
||||
* Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
|
||||
* Funcțiile post-instalare sunt funcții logice standard — folosesc `defineLogicFunction()` la fel ca orice altă funcție.
|
||||
* Câmpul `postInstallLogicFunctionUniversalIdentifier` din `defineApplication()` este opțional. Dacă este omis, nu rulează nicio funcție după instalare.
|
||||
* Timpul de expirare implicit este setat la 300 de secunde (5 minute) pentru a permite sarcini de configurare mai lungi, cum ar fi popularea datelor.
|
||||
* Funcțiile post-instalare nu au nevoie de declanșatoare — sunt invocate de platformă în timpul instalării sau manual prin `function:execute --postInstall`.
|
||||
|
||||
### Payload-ul declanșatorului de rută
|
||||
|
||||
|
||||
@@ -27,40 +27,53 @@ description: Создавайте и управляйте настройками
|
||||
Создайте новое приложение с помощью официального генератора, затем выполните аутентификацию и начните разработку:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Создать каркас нового приложения
|
||||
# Scaffold a new app (includes all examples by default)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# Если вы не используете yarn@4
|
||||
# If you don't use yarn@4
|
||||
corepack enable
|
||||
yarn install
|
||||
|
||||
# Аутентифицироваться с помощью вашего API-ключа (вам будет предложено)
|
||||
# Authenticate using your API key (you'll be prompted)
|
||||
yarn twenty auth:login
|
||||
|
||||
# Запустить режим разработки: автоматически синхронизирует локальные изменения с вашим рабочим пространством
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
The scaffolder supports three modes for controlling which example files are included:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
|
||||
# Interactive: select which examples to include
|
||||
npx create-twenty-app@latest my-app --interactive
|
||||
```
|
||||
|
||||
Отсюда вы можете:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Add a new entity to your application (guided)
|
||||
# Добавить новую сущность в ваше приложение (с мастером)
|
||||
yarn twenty entity:add
|
||||
|
||||
# Watch your application's function logs
|
||||
# Просматривать логи функций вашего приложения
|
||||
yarn twenty function:logs
|
||||
|
||||
# Execute a function by name
|
||||
yarn twenty function:execute -n my-function -p '{"name": "test"}'
|
||||
# Выполнить функцию по имени
|
||||
yarn twenty function:execute -n my-function -p '{\"name\": \"test\"}'
|
||||
|
||||
# Execute the post-install function
|
||||
# Выполнить послеустановочную функцию
|
||||
yarn twenty function:execute --postInstall
|
||||
|
||||
# Uninstall the application from the current workspace
|
||||
# Удалить приложение из текущего рабочего пространства
|
||||
yarn twenty app:uninstall
|
||||
|
||||
# Display commands' help
|
||||
# Показать справку по командам
|
||||
yarn twenty help
|
||||
```
|
||||
|
||||
@@ -73,9 +86,9 @@ yarn twenty help
|
||||
* Копирует минимальное базовое приложение в `my-twenty-app/`
|
||||
* Добавляет локальную зависимость `twenty-sdk` и конфигурацию Yarn 4
|
||||
* Создаёт файлы конфигурации и скрипты, подключённые к CLI `twenty`
|
||||
* Generates a default application config, a default function role, and a post-install function
|
||||
* Generates core files (application config, default function role, post-install function) plus example files based on the scaffolding mode
|
||||
|
||||
Свежесгенерированное приложение выглядит так:
|
||||
A freshly scaffolded app with the default `--exhaustive` mode looks like this:
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
@@ -94,13 +107,23 @@ my-twenty-app/
|
||||
├── application-config.ts # Required - main application configuration
|
||||
├── roles/
|
||||
│ └── default-role.ts # Default role for logic functions
|
||||
├── objects/
|
||||
│ └── example-object.ts # Example custom object definition
|
||||
├── fields/
|
||||
│ └── example-field.ts # Example standalone field definition
|
||||
├── logic-functions/
|
||||
│ ├── hello-world.ts # Example logic function
|
||||
│ └── post-install.ts # Post-install logic function
|
||||
└── front-components/
|
||||
└── hello-world.tsx # Example front component
|
||||
├── front-components/
|
||||
│ └── hello-world.tsx # Example front component
|
||||
├── views/
|
||||
│ └── example-view.ts # Example saved view definition
|
||||
└── navigation-menu-items/
|
||||
└── example-navigation-menu-item.ts # Example sidebar navigation link
|
||||
```
|
||||
|
||||
With `--minimal`, only the core files are created (`application-config.ts`, `roles/default-role.ts`, and `logic-functions/post-install.ts`). With `--interactive`, you choose which example files to include.
|
||||
|
||||
В общих чертах:
|
||||
|
||||
* **package.json**: Объявляет имя приложения, версию, движки (Node 24+, Yarn 4) и добавляет `twenty-sdk`, а также скрипт `twenty`, который делегирует выполнение локальному CLI `twenty`. Выполните `yarn twenty help`, чтобы вывести список всех доступных команд.
|
||||
@@ -116,13 +139,15 @@ my-twenty-app/
|
||||
|
||||
SDK обнаруживает сущности, разбирая ваши файлы TypeScript в поисках вызовов **`export default define<Entity>({...})`**. Для каждого типа сущности существует соответствующая вспомогательная функция, экспортируемая из `twenty-sdk`:
|
||||
|
||||
| Вспомогательная функция | Тип сущности |
|
||||
| ------------------------ | ------------------------------------------ |
|
||||
| `defineObject()` | Определения пользовательских объектов |
|
||||
| `defineLogicFunction()` | Определения логических функций |
|
||||
| `defineFrontComponent()` | Определения компонентов фронтенда |
|
||||
| `defineRole()` | Определения ролей |
|
||||
| `defineField()` | Расширения полей для существующих объектов |
|
||||
| Вспомогательная функция | Тип сущности |
|
||||
| ---------------------------- | ------------------------------------------ |
|
||||
| `defineObject()` | Определения пользовательских объектов |
|
||||
| `defineLogicFunction()` | Определения логических функций |
|
||||
| `defineFrontComponent()` | Определения компонентов фронтенда |
|
||||
| `defineRole()` | Определения ролей |
|
||||
| `defineField()` | Расширения полей для существующих объектов |
|
||||
| `defineView()` | Saved view definitions |
|
||||
| `defineNavigationMenuItem()` | Navigation menu item definitions |
|
||||
|
||||
<Note>
|
||||
**Имена файлов заданы гибко.** Обнаружение сущностей основано на AST — SDK сканирует ваши исходные файлы в поисках шаблона `export default define<Entity>({...})`. Вы можете организовывать файлы и папки как угодно. Группировка по типу сущности (например, `logic-functions/`, `roles/`) — это лишь соглашение для организации кода, а не требование.
|
||||
@@ -187,14 +212,16 @@ yarn twenty auth:status
|
||||
|
||||
SDK предоставляет вспомогательные функции для определения сущностей вашего приложения. Как описано в [Обнаружение сущностей](#entity-detection), вы должны использовать `export default define<Entity>({...})`, чтобы ваши сущности были обнаружены:
|
||||
|
||||
| Функция | Назначение |
|
||||
| ------------------------ | ---------------------------------------------------------------------- |
|
||||
| `defineApplication()` | Настройка метаданных приложения (обязательно, по одному на приложение) |
|
||||
| `defineObject()` | Определяет пользовательские объекты с полями |
|
||||
| `defineLogicFunction()` | Определение логических функций с обработчиками |
|
||||
| `defineFrontComponent()` | Определение фронт-компонентов для настраиваемого интерфейса |
|
||||
| `defineRole()` | Настраивает права роли и доступ к объектам |
|
||||
| `defineField()` | Расширение существующих объектов дополнительными полями |
|
||||
| Функция | Назначение |
|
||||
| ---------------------------- | ---------------------------------------------------------------------- |
|
||||
| `defineApplication()` | Настройка метаданных приложения (обязательно, по одному на приложение) |
|
||||
| `defineObject()` | Определяет пользовательские объекты с полями |
|
||||
| `defineLogicFunction()` | Определение логических функций с обработчиками |
|
||||
| `defineFrontComponent()` | Определение фронт-компонентов для настраиваемого интерфейса |
|
||||
| `defineRole()` | Настраивает права роли и доступ к объектам |
|
||||
| `defineField()` | Расширение существующих объектов дополнительными полями |
|
||||
| `defineView()` | Define saved views for objects |
|
||||
| `defineNavigationMenuItem()` | Define sidebar navigation links |
|
||||
|
||||
Эти функции проверяют вашу конфигурацию на этапе сборки и обеспечивают автодополнение в IDE и безопасность типов.
|
||||
|
||||
@@ -294,7 +321,7 @@ export default defineObject({
|
||||
* **Что это за приложение**: идентификаторы, отображаемое имя и описание.
|
||||
* **Как запускаются его функции**: какую роль они используют для прав доступа.
|
||||
* **(Необязательно) переменные**: пары ключ-значение, предоставляемые вашим функциям как переменные окружения.
|
||||
* **(Optional) post-install function**: a logic function that runs after the app is installed.
|
||||
* **(Необязательно) послеустановочная функция**: функция логики, которая запускается после установки приложения.
|
||||
|
||||
Используйте `defineApplication()` для определения конфигурации вашего приложения:
|
||||
|
||||
@@ -327,7 +354,7 @@ export default defineApplication({
|
||||
* `universalIdentifier` — это детерминированные идентификаторы, которыми вы управляете; сгенерируйте их один раз и сохраняйте стабильными между синхронизациями.
|
||||
* `applicationVariables` становятся переменными окружения для ваших функций (например, `DEFAULT_RECIPIENT_NAME` доступна как `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
* `defaultRoleUniversalIdentifier` должен соответствовать файлу роли (см. ниже).
|
||||
* `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#post-install-functions).
|
||||
* `postInstallLogicFunctionUniversalIdentifier` (необязательно) указывает на логическую функцию, которая автоматически выполняется после установки приложения. См. [Послеустановочные функции](#post-install-functions).
|
||||
|
||||
#### Роли и разрешения
|
||||
|
||||
@@ -466,9 +493,9 @@ export default defineLogicFunction({
|
||||
* Массив `triggers` необязателен. Функции без триггеров можно использовать как вспомогательные, вызываемые другими функциями.
|
||||
* Вы можете сочетать несколько типов триггеров в одной функции.
|
||||
|
||||
### Post-install functions
|
||||
### Послеустановочные функции
|
||||
|
||||
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
|
||||
Послеустановочная функция — это функция логики, которая автоматически выполняется после установки вашего приложения в рабочем пространстве. Это полезно для одноразовых задач настройки, таких как инициализация данных по умолчанию, создание начальных записей или настройка параметров рабочего пространства.
|
||||
|
||||
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ Uygulamalar, Twenty özelleştirmelerini **kod olarak** oluşturup yönetmenizi
|
||||
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"
|
||||
# Scaffold a new app
|
||||
# Scaffold a new app (includes all examples by default)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
@@ -42,6 +42,19 @@ yarn twenty auth:login
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
The scaffolder supports three modes for controlling which example files are included:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
|
||||
# Interactive: select which examples to include
|
||||
npx create-twenty-app@latest my-app --interactive
|
||||
```
|
||||
|
||||
Buradan şunları yapabilirsiniz:
|
||||
|
||||
```bash filename="Terminal"
|
||||
@@ -73,9 +86,9 @@ Ayrıca bkz.: [create-twenty-app](https://www.npmjs.com/package/create-twenty-ap
|
||||
* Minimal bir temel uygulamayı `my-twenty-app/` içine kopyalar
|
||||
* Yerel bir `twenty-sdk` bağımlılığı ve Yarn 4 yapılandırması ekler
|
||||
* `twenty` CLI ile bağlantılı yapılandırma dosyaları ve betikler oluşturur
|
||||
* Generates a default application config, a default function role, and a post-install function
|
||||
* Generates core files (application config, default function role, post-install function) plus example files based on the scaffolding mode
|
||||
|
||||
Yeni şablondan oluşturulan bir uygulama şöyle görünür:
|
||||
A freshly scaffolded app with the default `--exhaustive` mode looks like this:
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
@@ -94,13 +107,23 @@ my-twenty-app/
|
||||
├── application-config.ts # Required - main application configuration
|
||||
├── roles/
|
||||
│ └── default-role.ts # Default role for logic functions
|
||||
├── objects/
|
||||
│ └── example-object.ts # Example custom object definition
|
||||
├── fields/
|
||||
│ └── example-field.ts # Example standalone field definition
|
||||
├── logic-functions/
|
||||
│ ├── hello-world.ts # Example logic function
|
||||
│ └── post-install.ts # Post-install logic function
|
||||
└── front-components/
|
||||
└── hello-world.tsx # Example front component
|
||||
├── front-components/
|
||||
│ └── hello-world.tsx # Example front component
|
||||
├── views/
|
||||
│ └── example-view.ts # Example saved view definition
|
||||
└── navigation-menu-items/
|
||||
└── example-navigation-menu-item.ts # Example sidebar navigation link
|
||||
```
|
||||
|
||||
With `--minimal`, only the core files are created (`application-config.ts`, `roles/default-role.ts`, and `logic-functions/post-install.ts`). With `--interactive`, you choose which example files to include.
|
||||
|
||||
Genel hatlarıyla:
|
||||
|
||||
* **package.json**: Uygulama adını, sürümünü, motorları (Node 24+, Yarn 4) bildirir ve `twenty-sdk` ile yerel `twenty` CLI'sine yetki devreden bir `twenty` betiği ekler. Tüm mevcut komutları listelemek için `yarn twenty help` komutunu çalıştırın.
|
||||
@@ -116,13 +139,15 @@ Genel hatlarıyla:
|
||||
|
||||
SDK, TypeScript dosyalarınızı **`export default define<Entity>({...})`** çağrılarını arayarak ayrıştırıp varlıkları algılar. Her varlık türünün, `twenty-sdk` tarafından dışa aktarılan karşılık gelen bir yardımcı fonksiyonu vardır:
|
||||
|
||||
| Yardımcı fonksiyon | Varlık türü |
|
||||
| ------------------------ | ---------------------------------------- |
|
||||
| `defineObject()` | Özel nesne tanımları |
|
||||
| `defineLogicFunction()` | Mantık fonksiyon tanımları |
|
||||
| `defineFrontComponent()` | Front component definitions |
|
||||
| `defineRole()` | Rol tanımları |
|
||||
| `defineField()` | Mevcut nesneler için alan genişletmeleri |
|
||||
| Yardımcı fonksiyon | Varlık türü |
|
||||
| ---------------------------- | ---------------------------------------- |
|
||||
| `defineObject()` | Özel nesne tanımları |
|
||||
| `defineLogicFunction()` | Mantık fonksiyon tanımları |
|
||||
| `defineFrontComponent()` | Front component definitions |
|
||||
| `defineRole()` | Rol tanımları |
|
||||
| `defineField()` | Mevcut nesneler için alan genişletmeleri |
|
||||
| `defineView()` | Saved view definitions |
|
||||
| `defineNavigationMenuItem()` | Navigation menu item definitions |
|
||||
|
||||
<Note>
|
||||
**Dosya adlandırma esnektir.** Varlık algılama AST tabanlıdır — SDK, kaynak dosyalarınızı `export default define<Entity>({...})` desenini bulmak için tarar. Dosyalarınızı ve klasörlerinizi dilediğiniz gibi düzenleyebilirsiniz. Varlık türüne göre gruplama (örn. `logic-functions/`, `roles/`) bir gereklilik değil, yalnızca kod organizasyonu için bir gelenektir.
|
||||
@@ -187,14 +212,16 @@ twenty-sdk, uygulamanız içinde kullandığınız türlendirilmiş yapı taşla
|
||||
|
||||
SDK, uygulama varlıklarınızı tanımlamak için yardımcı fonksiyonlar sağlar. [Varlık algılama](#entity-detection) bölümünde açıklandığı gibi, varlıklarınızın algılanması için `export default define<Entity>({...})` kullanmalısınız:
|
||||
|
||||
| Fonksiyon | Amaç |
|
||||
| ------------------------ | ------------------------------------------------------------------------- |
|
||||
| `defineApplication()` | Uygulama meta verilerini yapılandırın (zorunlu, uygulama başına bir adet) |
|
||||
| `defineObject()` | Alanlara sahip özel nesneler tanımlayın |
|
||||
| `defineLogicFunction()` | İşleyicilerle mantık fonksiyonları tanımlayın |
|
||||
| `defineFrontComponent()` | Özel kullanıcı arayüzü için ön uç bileşenlerini tanımlayın |
|
||||
| `defineRole()` | Rol izinlerini ve nesne erişimini yapılandırın |
|
||||
| `defineField()` | Mevcut nesneleri ek alanlarla genişletin |
|
||||
| Fonksiyon | Amaç |
|
||||
| ---------------------------- | ------------------------------------------------------------------------- |
|
||||
| `defineApplication()` | Uygulama meta verilerini yapılandırın (zorunlu, uygulama başına bir adet) |
|
||||
| `defineObject()` | Alanlara sahip özel nesneler tanımlayın |
|
||||
| `defineLogicFunction()` | İşleyicilerle mantık fonksiyonları tanımlayın |
|
||||
| `defineFrontComponent()` | Özel kullanıcı arayüzü için ön uç bileşenlerini tanımlayın |
|
||||
| `defineRole()` | Rol izinlerini ve nesne erişimini yapılandırın |
|
||||
| `defineField()` | Mevcut nesneleri ek alanlarla genişletin |
|
||||
| `defineView()` | Define saved views for objects |
|
||||
| `defineNavigationMenuItem()` | Define sidebar navigation links |
|
||||
|
||||
Bu fonksiyonlar, derleme zamanında yapılandırmanızı doğrular ve IDE otomatik tamamlama ile tür güvenliği sağlar.
|
||||
|
||||
@@ -294,7 +321,7 @@ Her uygulamanın aşağıdakileri açıklayan tek bir `application-config.ts` do
|
||||
* **Uygulamanın kim olduğu**: tanımlayıcılar, görünen ad ve açıklama.
|
||||
* **Fonksiyonlarının nasıl çalıştığı**: izinler için hangi rolü kullandıkları.
|
||||
* **(İsteğe bağlı) değişkenler**: fonksiyonlarınıza ortam değişkenleri olarak sunulan anahtar–değer çiftleri.
|
||||
* **(Optional) post-install function**: a logic function that runs after the app is installed.
|
||||
* **(İsteğe bağlı) kurulum sonrası işlev**: uygulama yüklendikten sonra çalışan bir mantık işlevi.
|
||||
|
||||
Uygulama yapılandırmanızı tanımlamak için `defineApplication()` kullanın:
|
||||
|
||||
@@ -327,7 +354,7 @@ Notlar:
|
||||
* `universalIdentifier` alanları size ait belirleyici kimliklerdir; bunları bir kez oluşturun ve eşitlemeler boyunca kararlı tutun.
|
||||
* `applicationVariables`, fonksiyonlarınız için ortam değişkenlerine dönüşür (örneğin, `DEFAULT_RECIPIENT_NAME` değeri `process.env.DEFAULT_RECIPIENT_NAME` olarak kullanılabilir).
|
||||
* `defaultRoleUniversalIdentifier`, rol dosyasıyla eşleşmelidir (aşağıya bakın).
|
||||
* `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#post-install-functions).
|
||||
* `postInstallLogicFunctionUniversalIdentifier` (isteğe bağlı), uygulama yüklendikten sonra otomatik olarak çalışan bir mantık işlevine işaret eder. Bkz. [Kurulum sonrası işlevler](#post-install-functions).
|
||||
|
||||
#### Roller ve izinler
|
||||
|
||||
@@ -466,9 +493,9 @@ Notlar:
|
||||
* `triggers` dizisi isteğe bağlıdır. Tetikleyicisi olmayan fonksiyonlar, diğer fonksiyonlar tarafından çağrılan yardımcı fonksiyonlar olarak kullanılabilir.
|
||||
* Tek bir fonksiyonda birden çok tetikleyici türünü birleştirebilirsiniz.
|
||||
|
||||
### Post-install functions
|
||||
### Kurulum sonrası işlevler
|
||||
|
||||
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
|
||||
Kurulum sonrası işlev, uygulamanız bir çalışma alanına yüklendikten sonra otomatik olarak çalışan bir mantık işlevidir. Bu, varsayılan verileri tohumlama, ilk kayıtları oluşturma veya çalışma alanı ayarlarını yapılandırma gibi tek seferlik kurulum görevleri için yararlıdır.
|
||||
|
||||
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
|
||||
|
||||
|
||||
@@ -27,40 +27,53 @@ description: 以代码的形式构建并管理 Twenty 自定义项。
|
||||
使用官方脚手架创建一个新应用,然后进行身份验证并开始开发:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# 搭建一个新应用
|
||||
# Scaffold a new app (includes all examples by default)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# 如果你不使用 yarn@4
|
||||
# If you don't use yarn@4
|
||||
corepack enable
|
||||
yarn install
|
||||
|
||||
# 使用你的 API 密钥进行身份验证(系统会提示你)
|
||||
# Authenticate using your API key (you'll be prompted)
|
||||
yarn twenty auth:login
|
||||
|
||||
# 启动开发模式:会将本地更改自动同步到你的工作区
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
The scaffolder supports three modes for controlling which example files are included:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
|
||||
# Interactive: select which examples to include
|
||||
npx create-twenty-app@latest my-app --interactive
|
||||
```
|
||||
|
||||
从这里您可以:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Add a new entity to your application (guided)
|
||||
# 向你的应用添加一个新实体(引导式)
|
||||
yarn twenty entity:add
|
||||
|
||||
# Watch your application's function logs
|
||||
# 监听你的应用函数日志
|
||||
yarn twenty function:logs
|
||||
|
||||
# Execute a function by name
|
||||
yarn twenty function:execute -n my-function -p '{"name": "test"}'
|
||||
# 按名称执行一个函数
|
||||
yarn twenty function:execute -n my-function -p '{\"name\": \"test\"}'
|
||||
|
||||
# Execute the post-install function
|
||||
# 执行安装后函数
|
||||
yarn twenty function:execute --postInstall
|
||||
|
||||
# Uninstall the application from the current workspace
|
||||
# 从当前工作区卸载该应用
|
||||
yarn twenty app:uninstall
|
||||
|
||||
# Display commands' help
|
||||
# 显示命令帮助
|
||||
yarn twenty help
|
||||
```
|
||||
|
||||
@@ -73,9 +86,9 @@ yarn twenty help
|
||||
* 将一个最小的基础应用复制到 `my-twenty-app/` 中
|
||||
* 添加本地 `twenty-sdk` 依赖和 Yarn 4 配置
|
||||
* 创建与 `twenty` CLI 关联的配置文件和脚本
|
||||
* Generates a default application config, a default function role, and a post-install function
|
||||
* Generates core files (application config, default function role, post-install function) plus example files based on the scaffolding mode
|
||||
|
||||
一个新生成的脚手架应用如下所示:
|
||||
A freshly scaffolded app with the default `--exhaustive` mode looks like this:
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
@@ -94,13 +107,23 @@ my-twenty-app/
|
||||
├── application-config.ts # Required - main application configuration
|
||||
├── roles/
|
||||
│ └── default-role.ts # Default role for logic functions
|
||||
├── objects/
|
||||
│ └── example-object.ts # Example custom object definition
|
||||
├── fields/
|
||||
│ └── example-field.ts # Example standalone field definition
|
||||
├── logic-functions/
|
||||
│ ├── hello-world.ts # Example logic function
|
||||
│ └── post-install.ts # Post-install logic function
|
||||
└── front-components/
|
||||
└── hello-world.tsx # Example front component
|
||||
├── front-components/
|
||||
│ └── hello-world.tsx # Example front component
|
||||
├── views/
|
||||
│ └── example-view.ts # Example saved view definition
|
||||
└── navigation-menu-items/
|
||||
└── example-navigation-menu-item.ts # Example sidebar navigation link
|
||||
```
|
||||
|
||||
With `--minimal`, only the core files are created (`application-config.ts`, `roles/default-role.ts`, and `logic-functions/post-install.ts`). With `--interactive`, you choose which example files to include.
|
||||
|
||||
总体来说:
|
||||
|
||||
* **package.json**:声明应用名称、版本、引擎(Node 24+、Yarn 4),并添加 `twenty-sdk` 以及一个 `twenty` 脚本,该脚本会委托给本地的 `twenty` CLI。 运行 `yarn twenty help` 以列出所有可用命令。
|
||||
@@ -116,13 +139,15 @@ my-twenty-app/
|
||||
|
||||
该 SDK 通过在你的 TypeScript 文件中解析 **`export default define<Entity>({...})`** 调用来检测实体。 每种实体类型都有一个从 `twenty-sdk` 导出的对应辅助函数:
|
||||
|
||||
| 辅助函数 | 实体类型 |
|
||||
| ------------------------ | --------- |
|
||||
| `defineObject()` | 自定义对象定义 |
|
||||
| `defineLogicFunction()` | 逻辑函数定义 |
|
||||
| `defineFrontComponent()` | 前端组件定义 |
|
||||
| `defineRole()` | 角色定义 |
|
||||
| `defineField()` | 现有对象的字段扩展 |
|
||||
| 辅助函数 | 实体类型 |
|
||||
| ---------------------------- | -------------------------------- |
|
||||
| `defineObject()` | 自定义对象定义 |
|
||||
| `defineLogicFunction()` | 逻辑函数定义 |
|
||||
| `defineFrontComponent()` | 前端组件定义 |
|
||||
| `defineRole()` | 角色定义 |
|
||||
| `defineField()` | 现有对象的字段扩展 |
|
||||
| `defineView()` | Saved view definitions |
|
||||
| `defineNavigationMenuItem()` | Navigation menu item definitions |
|
||||
|
||||
<Note>
|
||||
**文件命名是灵活的。** 实体检测基于 AST — SDK 会扫描你的源文件以查找 `export default define<Entity>({...})` 模式。 你可以按照自己的喜好组织文件和文件夹。 按实体类型分组(例如 `logic-functions/`、`roles/`)只是代码组织的一种约定,并非必需。
|
||||
@@ -187,14 +212,16 @@ twenty-sdk 提供你在应用中使用的类型化构件和辅助函数。 以
|
||||
|
||||
该 SDK 提供辅助函数用于定义你的应用实体。 如 [实体检测](#entity-detection) 中所述,你必须使用 `export default define<Entity>({...})` 才能让你的实体被检测到:
|
||||
|
||||
| 函数 | 目的 |
|
||||
| ------------------------ | ------------------ |
|
||||
| `defineApplication()` | 配置应用元数据(必需,每个应用一个) |
|
||||
| `defineObject()` | 定义带字段的自定义对象 |
|
||||
| `defineLogicFunction()` | 定义带处理程序的逻辑函数 |
|
||||
| `defineFrontComponent()` | 为自定义 UI 定义前端组件 |
|
||||
| `defineRole()` | 配置角色权限和对象访问 |
|
||||
| `defineField()` | 为现有对象扩展额外字段 |
|
||||
| 函数 | 目的 |
|
||||
| ---------------------------- | ------------------------------- |
|
||||
| `defineApplication()` | 配置应用元数据(必需,每个应用一个) |
|
||||
| `defineObject()` | 定义带字段的自定义对象 |
|
||||
| `defineLogicFunction()` | 定义带处理程序的逻辑函数 |
|
||||
| `defineFrontComponent()` | 为自定义 UI 定义前端组件 |
|
||||
| `defineRole()` | 配置角色权限和对象访问 |
|
||||
| `defineField()` | 为现有对象扩展额外字段 |
|
||||
| `defineView()` | Define saved views for objects |
|
||||
| `defineNavigationMenuItem()` | Define sidebar navigation links |
|
||||
|
||||
这些函数会在构建时校验你的配置,并提供 IDE 自动补全和类型安全。
|
||||
|
||||
@@ -294,7 +321,7 @@ export default defineObject({
|
||||
* **应用的身份**:标识符、显示名称和描述。
|
||||
* **函数如何运行**:它们用于权限的角色。
|
||||
* **(可选)变量**:以环境变量形式提供给函数的键值对。
|
||||
* **(Optional) post-install function**: a logic function that runs after the app is installed.
|
||||
* **(可选)安装后函数**:在应用安装后运行的逻辑函数。
|
||||
|
||||
使用 `defineApplication()` 定义你的应用配置:
|
||||
|
||||
@@ -327,7 +354,7 @@ export default defineApplication({
|
||||
* `universalIdentifier` 字段是你拥有的确定性 ID;生成一次并在多次同步中保持稳定。
|
||||
* `applicationVariables` 会变成函数可用的环境变量(例如,`DEFAULT_RECIPIENT_NAME` 可作为 `process.env.DEFAULT_RECIPIENT_NAME` 使用)。
|
||||
* `defaultRoleUniversalIdentifier` 必须与角色文件一致(见下文)。
|
||||
* `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#post-install-functions).
|
||||
* `postInstallLogicFunctionUniversalIdentifier`(可选)指向一个在应用安装后自动运行的逻辑函数。 参见 [安装后函数](#post-install-functions)。
|
||||
|
||||
#### 角色和权限
|
||||
|
||||
@@ -466,9 +493,9 @@ export default defineLogicFunction({
|
||||
* `triggers` 数组是可选的。 没有触发器的函数可作为实用函数,被其他函数调用。
|
||||
* 你可以在单个函数中混用多种触发器类型。
|
||||
|
||||
### Post-install functions
|
||||
### 安装后函数
|
||||
|
||||
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
|
||||
安装后函数是在你的应用安装到工作区后自动运行的逻辑函数。 这对于一次性设置任务很有用,例如填充默认数据、创建初始记录或配置工作区设置。
|
||||
|
||||
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
|
||||
|
||||
|
||||
@@ -1045,7 +1045,7 @@ export type CreateNavigationMenuItemInput = {
|
||||
icon?: InputMaybe<Scalars['String']>;
|
||||
link?: InputMaybe<Scalars['String']>;
|
||||
name?: InputMaybe<Scalars['String']>;
|
||||
position?: InputMaybe<Scalars['Int']>;
|
||||
position?: InputMaybe<Scalars['Float']>;
|
||||
targetObjectMetadataId?: InputMaybe<Scalars['UUID']>;
|
||||
targetRecordId?: InputMaybe<Scalars['UUID']>;
|
||||
userWorkspaceId?: InputMaybe<Scalars['UUID']>;
|
||||
@@ -4583,7 +4583,7 @@ export type UpdateNavigationMenuItemInput = {
|
||||
icon?: InputMaybe<Scalars['String']>;
|
||||
link?: InputMaybe<Scalars['String']>;
|
||||
name?: InputMaybe<Scalars['String']>;
|
||||
position?: InputMaybe<Scalars['Int']>;
|
||||
position?: InputMaybe<Scalars['Float']>;
|
||||
};
|
||||
|
||||
export type UpdateObjectPayload = {
|
||||
|
||||
+1
-13
@@ -2,24 +2,16 @@ import { Action } from '@/action-menu/actions/components/Action';
|
||||
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
|
||||
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
|
||||
import { useCreateNavigationMenuItem } from '@/navigation-menu-item/hooks/useCreateNavigationMenuItem';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
export const AddToFavoritesSingleRecordAction = () => {
|
||||
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
|
||||
|
||||
const recordId = useSelectedRecordIdOrThrow();
|
||||
|
||||
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
);
|
||||
|
||||
const { createFavorite } = useCreateFavorite();
|
||||
const { createNavigationMenuItem } = useCreateNavigationMenuItem();
|
||||
|
||||
const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
|
||||
|
||||
@@ -28,11 +20,7 @@ export const AddToFavoritesSingleRecordAction = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNavigationMenuItemEnabled) {
|
||||
createNavigationMenuItem(selectedRecord, objectMetadataItem.nameSingular);
|
||||
} else {
|
||||
createFavorite(selectedRecord, objectMetadataItem.nameSingular);
|
||||
}
|
||||
createFavorite(selectedRecord, objectMetadataItem.nameSingular);
|
||||
};
|
||||
|
||||
return <Action onClick={handleClick} />;
|
||||
|
||||
+7
-13
@@ -8,9 +8,7 @@ import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
|
||||
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
export const DeleteSingleRecordAction = () => {
|
||||
const { recordIndexId, objectMetadataItem } =
|
||||
@@ -29,9 +27,7 @@ export const DeleteSingleRecordAction = () => {
|
||||
|
||||
const { sortedFavorites: favorites } = useFavorites();
|
||||
const { deleteFavorite } = useDeleteFavorite();
|
||||
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
);
|
||||
|
||||
const { navigationMenuItems, workspaceNavigationMenuItems } =
|
||||
usePrefetchedNavigationMenuItemsData();
|
||||
const { removeNavigationMenuItemsByTargetRecordIds } =
|
||||
@@ -50,15 +46,13 @@ export const DeleteSingleRecordAction = () => {
|
||||
deleteFavorite(foundFavorite.id);
|
||||
}
|
||||
|
||||
if (isNavigationMenuItemEnabled) {
|
||||
const foundNavigationMenuItem = [
|
||||
...navigationMenuItems,
|
||||
...workspaceNavigationMenuItems,
|
||||
].find((item) => item.targetRecordId === recordId);
|
||||
const foundNavigationMenuItem = [
|
||||
...navigationMenuItems,
|
||||
...workspaceNavigationMenuItems,
|
||||
].find((item) => item.targetRecordId === recordId);
|
||||
|
||||
if (isDefined(foundNavigationMenuItem)) {
|
||||
removeNavigationMenuItemsByTargetRecordIds([recordId]);
|
||||
}
|
||||
if (isDefined(foundNavigationMenuItem)) {
|
||||
removeNavigationMenuItemsByTargetRecordIds([recordId]);
|
||||
}
|
||||
|
||||
await deleteOneRecord(recordId);
|
||||
|
||||
+10
-22
@@ -5,9 +5,7 @@ import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
|
||||
import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
export const RemoveFromFavoritesSingleRecordAction = () => {
|
||||
const recordId = useSelectedRecordIdOrThrow();
|
||||
@@ -16,9 +14,6 @@ export const RemoveFromFavoritesSingleRecordAction = () => {
|
||||
const { sortedFavorites: favorites } = useFavorites();
|
||||
const { navigationMenuItems, workspaceNavigationMenuItems } =
|
||||
usePrefetchedNavigationMenuItemsData();
|
||||
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
);
|
||||
|
||||
const { deleteFavorite } = useDeleteFavorite();
|
||||
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
|
||||
@@ -27,28 +22,21 @@ export const RemoveFromFavoritesSingleRecordAction = () => {
|
||||
(favorite) => favorite.recordId === recordId,
|
||||
);
|
||||
|
||||
const foundNavigationMenuItem = isNavigationMenuItemEnabled
|
||||
? [...navigationMenuItems, ...workspaceNavigationMenuItems].find(
|
||||
(item) =>
|
||||
item.targetRecordId === recordId &&
|
||||
item.targetObjectMetadataId === objectMetadataItem.id,
|
||||
)
|
||||
: undefined;
|
||||
const foundNavigationMenuItem = [
|
||||
...navigationMenuItems,
|
||||
...workspaceNavigationMenuItems,
|
||||
].find(
|
||||
(item) =>
|
||||
item.targetRecordId === recordId &&
|
||||
item.targetObjectMetadataId === objectMetadataItem.id,
|
||||
);
|
||||
|
||||
const handleClick = () => {
|
||||
if (isNavigationMenuItemEnabled) {
|
||||
if (!isDefined(foundNavigationMenuItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteNavigationMenuItem(foundNavigationMenuItem.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDefined(foundFavorite)) {
|
||||
if (!isDefined(foundNavigationMenuItem) || !isDefined(foundFavorite)) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteNavigationMenuItem(foundNavigationMenuItem.id);
|
||||
deleteFavorite(foundFavorite.id);
|
||||
};
|
||||
|
||||
|
||||
+4
-4
@@ -29,8 +29,8 @@ export const useShouldActionBeRegisteredParams = ({
|
||||
}): ShouldBeRegisteredFunctionParams => {
|
||||
const { sortedFavorites: favorites } = useFavorites();
|
||||
const { navigationMenuItems } = usePrefetchedNavigationMenuItemsData();
|
||||
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
|
||||
const contextStoreTargetedRecordsRule = useRecoilComponentValue(
|
||||
@@ -47,7 +47,7 @@ export const useShouldActionBeRegisteredParams = ({
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isNavigationMenuItemEnabled && isDefined(objectMetadataItem)) {
|
||||
if (isNavigationMenuItemEditingEnabled && isDefined(objectMetadataItem)) {
|
||||
const foundNavigationMenuItem = navigationMenuItems?.find(
|
||||
(item) =>
|
||||
item.targetRecordId === recordId &&
|
||||
@@ -62,7 +62,7 @@ export const useShouldActionBeRegisteredParams = ({
|
||||
return !!foundFavorite;
|
||||
}, [
|
||||
recordId,
|
||||
isNavigationMenuItemEnabled,
|
||||
isNavigationMenuItemEditingEnabled,
|
||||
objectMetadataItem,
|
||||
navigationMenuItems,
|
||||
favorites,
|
||||
|
||||
@@ -22,8 +22,8 @@ import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFla
|
||||
import { IconCalendar, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { isNavigationModifierPressed } from 'twenty-ui/utilities';
|
||||
import {
|
||||
PermissionFlagType,
|
||||
FeatureFlagKey,
|
||||
PermissionFlagType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { formatToHumanReadableDate } from '~/utils/date-utils';
|
||||
import { getFileNameAndExtension } from '~/utils/file/getFileNameAndExtension';
|
||||
@@ -96,7 +96,11 @@ export const AttachmentRow = ({
|
||||
);
|
||||
|
||||
const { name: originalFileName, extension: attachmentFileExtension } =
|
||||
getFileNameAndExtension(attachment.name);
|
||||
getFileNameAndExtension(
|
||||
isFilesFieldMigrated
|
||||
? (attachment.file?.[0]?.label as string)
|
||||
: attachment.name,
|
||||
);
|
||||
|
||||
const [attachmentFileName, setAttachmentFileName] =
|
||||
useState(originalFileName);
|
||||
@@ -206,7 +210,9 @@ export const AttachmentRow = ({
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<OverflowingTextWithTooltip text={attachment.name} />
|
||||
<OverflowingTextWithTooltip
|
||||
text={`${attachmentFileName}${attachmentFileExtension}`}
|
||||
/>
|
||||
</StyledLink>
|
||||
</StyledLinkContainer>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { generateCreateOneRecordMutation } from '@/object-metadata/utils/generat
|
||||
import { generateUpdateOneRecordMutation } from '@/object-metadata/utils/generateUpdateOneRecordMutation';
|
||||
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { AvatarType } from 'twenty-ui/display';
|
||||
import { CreateNavigationMenuItemDocument } from '~/generated-metadata/graphql';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
|
||||
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
|
||||
|
||||
@@ -105,7 +106,8 @@ export const sortedFavorites = [
|
||||
},
|
||||
];
|
||||
|
||||
const favoriteObjectMetadataItem = getMockObjectMetadataItemOrThrow('favorite')
|
||||
const favoriteObjectMetadataItem = getMockObjectMetadataItemOrThrow('favorite');
|
||||
const personObjectMetadataItem = getMockObjectMetadataItemOrThrow('person');
|
||||
|
||||
const UPDATE_ONE_FAVORITE_MUTATION = generateUpdateOneRecordMutation(
|
||||
{
|
||||
@@ -240,6 +242,41 @@ export const mocks = [
|
||||
},
|
||||
})),
|
||||
},
|
||||
|
||||
{
|
||||
request: {
|
||||
query: CreateNavigationMenuItemDocument,
|
||||
variables: {
|
||||
input: {
|
||||
targetRecordId: favoriteTargetObjectId,
|
||||
targetObjectMetadataId: personObjectMetadataItem.id,
|
||||
userWorkspaceId: '1',
|
||||
folderId: undefined,
|
||||
position: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
createNavigationMenuItem: {
|
||||
__typename: 'NavigationMenuItem',
|
||||
id: mockId,
|
||||
userWorkspaceId: '1',
|
||||
targetRecordId: favoriteTargetObjectId,
|
||||
targetObjectMetadataId: personObjectMetadataItem.id,
|
||||
viewId: null,
|
||||
folderId: null,
|
||||
name: null,
|
||||
link: null,
|
||||
icon: null,
|
||||
position: 1,
|
||||
applicationId: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
export const mockWorkspaceMember = {
|
||||
|
||||
+17
@@ -23,6 +23,23 @@ jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
|
||||
useFindManyRecords: () => ({ records: initialFavorites }),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData',
|
||||
() => ({
|
||||
usePrefetchedNavigationMenuItemsData: () => ({
|
||||
navigationMenuItems: [],
|
||||
workspaceNavigationMenuItems: [],
|
||||
currentWorkspaceMemberId: '1',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('@/favorites/hooks/usePrefetchedFavoritesFoldersData', () => ({
|
||||
usePrefetchedFavoritesFoldersData: () => ({
|
||||
favoriteFolders: [],
|
||||
}),
|
||||
}));
|
||||
|
||||
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: mocks,
|
||||
});
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useCreateNavigationMenuItemMutation } from '~/generated-metadata/graphql';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
||||
import { usePrefetchedFavoritesFoldersData } from './usePrefetchedFavoritesFoldersData';
|
||||
|
||||
export const useCreateFavorite = () => {
|
||||
const { favorites, currentWorkspaceMemberId } = usePrefetchedFavoritesData();
|
||||
const { favoriteFolders } = usePrefetchedFavoritesFoldersData();
|
||||
const { navigationMenuItems } = usePrefetchedNavigationMenuItemsData();
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const { createOneRecord: createOneFavorite } = useCreateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||
});
|
||||
|
||||
const createFavorite = (
|
||||
const [createNavigationMenuItemMutation] =
|
||||
useCreateNavigationMenuItemMutation({
|
||||
refetchQueries: ['FindManyNavigationMenuItems'],
|
||||
});
|
||||
|
||||
const createFavorite = async (
|
||||
targetRecord: ObjectRecord,
|
||||
targetObjectNameSingular: string,
|
||||
favoriteFolderId?: string,
|
||||
@@ -25,12 +41,78 @@ export const useCreateFavorite = () => {
|
||||
0,
|
||||
);
|
||||
|
||||
createOneFavorite({
|
||||
await createOneFavorite({
|
||||
[`${targetObjectNameSingular}Id`]: targetRecord.id,
|
||||
position: maxPosition + 1,
|
||||
forWorkspaceMemberId: currentWorkspaceMemberId,
|
||||
favoriteFolderId,
|
||||
});
|
||||
|
||||
let navigationMenuItemFolderId: string | undefined;
|
||||
|
||||
if (isDefined(favoriteFolderId)) {
|
||||
const favoriteFolder = favoriteFolders.find(
|
||||
(folder) => folder.id === favoriteFolderId,
|
||||
);
|
||||
|
||||
if (isDefined(favoriteFolder)) {
|
||||
const matchingNavigationMenuItemFolder = navigationMenuItems.find(
|
||||
(item) =>
|
||||
isNavigationMenuItemFolder(item) &&
|
||||
item.name === favoriteFolder.name &&
|
||||
item.userWorkspaceId === currentWorkspaceMemberId,
|
||||
);
|
||||
|
||||
navigationMenuItemFolderId = matchingNavigationMenuItemFolder?.id;
|
||||
}
|
||||
}
|
||||
|
||||
const relevantNavigationMenuItems = navigationMenuItemFolderId
|
||||
? navigationMenuItems.filter(
|
||||
(item) => item.folderId === navigationMenuItemFolderId,
|
||||
)
|
||||
: navigationMenuItems.filter(
|
||||
(item) =>
|
||||
!item.folderId && item.userWorkspaceId === currentWorkspaceMemberId,
|
||||
);
|
||||
|
||||
const maxNavigationMenuItemPosition = Math.max(
|
||||
...relevantNavigationMenuItems.map((item) => item.position),
|
||||
0,
|
||||
);
|
||||
|
||||
const isView = targetObjectNameSingular === 'view';
|
||||
|
||||
if (isView) {
|
||||
await createNavigationMenuItemMutation({
|
||||
variables: {
|
||||
input: {
|
||||
viewId: targetRecord.id,
|
||||
userWorkspaceId: currentWorkspaceMemberId,
|
||||
folderId: navigationMenuItemFolderId,
|
||||
position: maxNavigationMenuItemPosition + 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.nameSingular === targetObjectNameSingular,
|
||||
);
|
||||
|
||||
if (isDefined(objectMetadataItem)) {
|
||||
await createNavigationMenuItemMutation({
|
||||
variables: {
|
||||
input: {
|
||||
targetRecordId: targetRecord.id,
|
||||
targetObjectMetadataId: objectMetadataItem.id,
|
||||
userWorkspaceId: currentWorkspaceMemberId,
|
||||
folderId: navigationMenuItemFolderId,
|
||||
position: maxNavigationMenuItemPosition + 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { createFavorite };
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useCreateNavigationMenuItemMutation } from '~/generated-metadata/graphql';
|
||||
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
||||
import { usePrefetchedFavoritesFoldersData } from './usePrefetchedFavoritesFoldersData';
|
||||
|
||||
@@ -10,6 +13,12 @@ export const useCreateFavoriteFolder = () => {
|
||||
|
||||
const { currentWorkspaceMemberId } = usePrefetchedFavoritesData();
|
||||
const { favoriteFolders } = usePrefetchedFavoritesFoldersData();
|
||||
const { navigationMenuItems } = usePrefetchedNavigationMenuItemsData();
|
||||
|
||||
const [createNavigationMenuItemMutation] =
|
||||
useCreateNavigationMenuItemMutation({
|
||||
refetchQueries: ['FindManyNavigationMenuItems'],
|
||||
});
|
||||
|
||||
const createNewFavoriteFolder = async (name: string): Promise<void> => {
|
||||
if (!name || !currentWorkspaceMemberId) {
|
||||
@@ -25,6 +34,34 @@ export const useCreateFavoriteFolder = () => {
|
||||
name,
|
||||
position: maxPosition + 1,
|
||||
});
|
||||
|
||||
const folderNavigationMenuItems = navigationMenuItems.filter(
|
||||
(item) =>
|
||||
isDefined(item.name) &&
|
||||
!item.folderId &&
|
||||
!item.targetRecordId &&
|
||||
!item.targetObjectMetadataId &&
|
||||
!item.viewId &&
|
||||
item.userWorkspaceId === currentWorkspaceMemberId,
|
||||
);
|
||||
|
||||
const maxNavigationMenuItemPosition = Math.max(
|
||||
...folderNavigationMenuItems.map((item) => item.position),
|
||||
0,
|
||||
);
|
||||
|
||||
await createNavigationMenuItemMutation({
|
||||
variables: {
|
||||
input: {
|
||||
name,
|
||||
targetRecordId: null,
|
||||
targetObjectMetadataId: null,
|
||||
userWorkspaceId: currentWorkspaceMemberId,
|
||||
folderId: null,
|
||||
position: maxNavigationMenuItemPosition + 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return { createNewFavoriteFolder };
|
||||
|
||||
+3
-3
@@ -5,11 +5,11 @@ import { CurrentWorkspaceMemberNavigationMenuItemFolders } from '@/navigation-me
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
|
||||
export const CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher = () => {
|
||||
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
|
||||
if (isNavigationMenuItemEnabled) {
|
||||
if (isNavigationMenuItemEditingEnabled) {
|
||||
return <CurrentWorkspaceMemberNavigationMenuItemFolders />;
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -20,11 +20,11 @@ export const NavigationMenuItemFolderContentDispatcherEffect = ({
|
||||
favorites,
|
||||
navigationMenuItems,
|
||||
}: NavigationMenuItemFolderContentDispatcherEffectProps) => {
|
||||
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
|
||||
if (isNavigationMenuItemEnabled && isDefined(navigationMenuItems)) {
|
||||
if (isNavigationMenuItemEditingEnabled && isDefined(navigationMenuItems)) {
|
||||
return (
|
||||
<NavigationMenuItemFolderContent
|
||||
folderId={folderId}
|
||||
|
||||
+3
-3
@@ -5,11 +5,11 @@ import { WorkspaceNavigationMenuItems } from '@/navigation-menu-item/components/
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
|
||||
export const WorkspaceNavigationMenuItemsDispatcher = () => {
|
||||
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
|
||||
if (isNavigationMenuItemEnabled) {
|
||||
if (isNavigationMenuItemEditingEnabled) {
|
||||
return <WorkspaceNavigationMenuItems />;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useFavoritesByFolder } from '@/favorites/hooks/useFavoritesByFolder';
|
||||
@@ -31,8 +31,8 @@ export const MainNavigationDrawer = ({ className }: { className?: string }) => {
|
||||
);
|
||||
const { favoritesByFolder } = useFavoritesByFolder();
|
||||
const { navigationMenuItemsByFolder } = useNavigationMenuItemsByFolder();
|
||||
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
|
||||
const openedFavoriteFolder = favoritesByFolder.find(
|
||||
@@ -43,7 +43,7 @@ export const MainNavigationDrawer = ({ className }: { className?: string }) => {
|
||||
(f) => f.id === currentNavigationMenuItemFolderId,
|
||||
);
|
||||
|
||||
const openedFolder = isNavigationMenuItemEnabled
|
||||
const openedFolder = isNavigationMenuItemEditingEnabled
|
||||
? openedNavigationMenuItemFolder
|
||||
: openedFavoriteFolder;
|
||||
|
||||
@@ -59,7 +59,7 @@ export const MainNavigationDrawer = ({ className }: { className?: string }) => {
|
||||
</NavigationDrawerFixedContent>
|
||||
|
||||
<NavigationDrawerScrollableContent>
|
||||
{isNavigationMenuItemEnabled ? (
|
||||
{isNavigationMenuItemEditingEnabled ? (
|
||||
<StyledScrollableContent>
|
||||
{openedFolder ? (
|
||||
<NavigationMenuItemFolderContentDispatcherEffect
|
||||
|
||||
@@ -20,8 +20,8 @@ type NavbarDragProviderProps = {
|
||||
};
|
||||
|
||||
export const NavbarDragProvider = ({ children }: NavbarDragProviderProps) => {
|
||||
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [sourceDroppableId, setSourceDroppableId] = useState<string | null>(
|
||||
@@ -42,7 +42,7 @@ export const NavbarDragProvider = ({ children }: NavbarDragProviderProps) => {
|
||||
setIsDragging(false);
|
||||
setSourceDroppableId(null);
|
||||
|
||||
if (isNavigationMenuItemEnabled) {
|
||||
if (isNavigationMenuItemEditingEnabled) {
|
||||
const isWorkspaceDrop =
|
||||
(result.source?.droppableId?.startsWith('workspace-') ?? false) &&
|
||||
(result.destination?.droppableId?.startsWith('workspace-') ?? false);
|
||||
|
||||
@@ -35,8 +35,8 @@ type PageDragDropProviderProps = {
|
||||
export const PageDragDropProvider = ({
|
||||
children,
|
||||
}: PageDragDropProviderProps) => {
|
||||
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [sourceDroppableId, setSourceDroppableId] = useState<string | null>(
|
||||
@@ -136,7 +136,7 @@ export const PageDragDropProvider = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNavigationMenuItemEnabled) {
|
||||
if (isNavigationMenuItemEditingEnabled) {
|
||||
const isWorkspaceDrop =
|
||||
isWorkspaceDroppableId(result.source?.droppableId) &&
|
||||
isWorkspaceDroppableId(result.destination?.droppableId);
|
||||
|
||||
+73
@@ -1,3 +1,5 @@
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
|
||||
@@ -6,6 +8,11 @@ import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/l
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
|
||||
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { coreViewsFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreViewsFromObjectMetadataItemFamilySelector';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
@@ -13,6 +20,7 @@ import { useRecoilValue } from 'recoil';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { getAppPath, isDefined } from 'twenty-shared/utils';
|
||||
import { Avatar, useIcons } from 'twenty-ui/display';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
@@ -45,6 +53,17 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
||||
lastVisitedViewPerObjectMetadataItemState,
|
||||
);
|
||||
|
||||
const views = useRecoilValue(
|
||||
coreViewsFromObjectMetadataItemFamilySelector({
|
||||
objectMetadataItemId: objectMetadataItem.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const contextStoreCurrentViewId = useRecoilComponentValue(
|
||||
contextStoreCurrentViewIdComponentState,
|
||||
MAIN_CONTEXT_STORE_INSTANCE_ID,
|
||||
);
|
||||
|
||||
const lastVisitedViewId =
|
||||
lastVisitedViewPerObjectMetadataItem?.[objectMetadataItem.id];
|
||||
|
||||
@@ -136,6 +155,60 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
||||
? objectMetadataItem.labelSingular
|
||||
: undefined;
|
||||
|
||||
const shouldSubItemsBeDisplayed =
|
||||
!isNavigationMenuItemEditingEnabled && isActive && views.length > 1;
|
||||
|
||||
const sortedObjectMetadataViews = [...views].sort(
|
||||
(viewA, viewB) => viewA.position - viewB.position,
|
||||
);
|
||||
|
||||
const selectedSubItemIndex = sortedObjectMetadataViews.findIndex(
|
||||
(view) => contextStoreCurrentViewId === view.id,
|
||||
);
|
||||
|
||||
const subItemArrayLength = sortedObjectMetadataViews.length;
|
||||
|
||||
if (!isNavigationMenuItemEditingEnabled) {
|
||||
return (
|
||||
<NavigationDrawerItemsCollapsableContainer
|
||||
isGroup={shouldSubItemsBeDisplayed}
|
||||
>
|
||||
<NavigationDrawerItem
|
||||
key={objectMetadataItem.id}
|
||||
label={objectMetadataItem.labelPlural}
|
||||
to={navigationPath}
|
||||
Icon={getIcon(objectMetadataItem.icon)}
|
||||
active={isActive}
|
||||
/>
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={shouldSubItemsBeDisplayed}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
>
|
||||
{sortedObjectMetadataViews.map((view, index) => (
|
||||
<NavigationDrawerSubItem
|
||||
label={view.name}
|
||||
to={getAppPath(
|
||||
AppPath.RecordIndexPage,
|
||||
{ objectNamePlural: objectMetadataItem.namePlural },
|
||||
{ viewId: view.id },
|
||||
)}
|
||||
active={contextStoreCurrentViewId === view.id}
|
||||
subItemState={getNavigationSubItemLeftAdornment({
|
||||
index,
|
||||
arrayLength: subItemArrayLength,
|
||||
selectedIndex: selectedSubItemIndex,
|
||||
})}
|
||||
Icon={getIcon(view.icon)}
|
||||
key={view.id}
|
||||
/>
|
||||
))}
|
||||
</AnimatedExpandableContainer>
|
||||
</NavigationDrawerItemsCollapsableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationDrawerItem
|
||||
label={label}
|
||||
|
||||
+3
-3
@@ -29,8 +29,8 @@ export const NavigationDrawerOpenedSection = () => {
|
||||
const { workspaceFavoritesObjectMetadataItems } = useWorkspaceFavorites();
|
||||
const { workspaceNavigationMenuItemsObjectMetadataItems } =
|
||||
useWorkspaceNavigationMenuItems();
|
||||
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -52,7 +52,7 @@ export const NavigationDrawerOpenedSection = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const workspaceItemsToExclude = isNavigationMenuItemEnabled
|
||||
const workspaceItemsToExclude = isNavigationMenuItemEditingEnabled
|
||||
? workspaceNavigationMenuItemsObjectMetadataItems
|
||||
: workspaceFavoritesObjectMetadataItems;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { triggerUpdateRecordOptimisticEffectByBatch } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffectByBatch';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { apiConfigState } from '@/client-config/states/apiConfigState';
|
||||
import { useRemoveNavigationMenuItemByTargetRecordId } from '@/navigation-menu-item/hooks/useRemoveNavigationMenuItemByTargetRecordId';
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
@@ -15,12 +16,9 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
|
||||
type useDeleteManyRecordProps = {
|
||||
@@ -60,9 +58,7 @@ export const useDeleteManyRecords = ({
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
const { refetchAggregateQueries } = useRefetchAggregateQueries();
|
||||
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
);
|
||||
|
||||
const { removeNavigationMenuItemsByTargetRecordIds } =
|
||||
useRemoveNavigationMenuItemByTargetRecordId();
|
||||
|
||||
@@ -231,9 +227,7 @@ export const useDeleteManyRecords = ({
|
||||
objectMetadataNamePlural: objectMetadataItem.namePlural,
|
||||
});
|
||||
|
||||
if (isNavigationMenuItemEnabled) {
|
||||
removeNavigationMenuItemsByTargetRecordIds(recordIdsToDelete);
|
||||
}
|
||||
removeNavigationMenuItemsByTargetRecordIds(recordIdsToDelete);
|
||||
|
||||
dispatchObjectRecordOperationBrowserEvent({
|
||||
objectMetadataItem,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
|
||||
import { triggerDestroyRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDestroyRecordsOptimisticEffect';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { apiConfigState } from '@/client-config/states/apiConfigState';
|
||||
import { useRemoveNavigationMenuItemByTargetRecordId } from '@/navigation-menu-item/hooks/useRemoveNavigationMenuItemByTargetRecordId';
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
@@ -11,12 +13,8 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField';
|
||||
import { useRemoveNavigationMenuItemByTargetRecordId } from '@/navigation-menu-item/hooks/useRemoveNavigationMenuItemByTargetRecordId';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
|
||||
@@ -55,9 +53,6 @@ export const useDestroyManyRecords = ({
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
const { refetchAggregateQueries } = useRefetchAggregateQueries();
|
||||
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
);
|
||||
const { removeNavigationMenuItemsByTargetRecordIds } =
|
||||
useRemoveNavigationMenuItemByTargetRecordId();
|
||||
|
||||
@@ -152,9 +147,7 @@ export const useDestroyManyRecords = ({
|
||||
objectMetadataNamePlural: objectMetadataItem.namePlural,
|
||||
});
|
||||
|
||||
if (isNavigationMenuItemEnabled) {
|
||||
removeNavigationMenuItemsByTargetRecordIds(recordIdsToDestroy);
|
||||
}
|
||||
removeNavigationMenuItemsByTargetRecordIds(recordIdsToDestroy);
|
||||
|
||||
dispatchObjectRecordOperationBrowserEvent({
|
||||
objectMetadataItem,
|
||||
|
||||
+2
-9
@@ -1,4 +1,5 @@
|
||||
import { triggerUpdateRecordOptimisticEffectByBatch } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffectByBatch';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { useRemoveNavigationMenuItemByTargetRecordId } from '@/navigation-menu-item/hooks/useRemoveNavigationMenuItemByTargetRecordId';
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
@@ -16,11 +17,8 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
|
||||
const DEFAULT_DELAY_BETWEEN_MUTATIONS_MS = 50;
|
||||
@@ -64,9 +62,6 @@ export const useIncrementalDeleteManyRecords = <T>({
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
const { refetchAggregateQueries } = useRefetchAggregateQueries();
|
||||
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
);
|
||||
const { removeNavigationMenuItemsByTargetRecordIds } =
|
||||
useRemoveNavigationMenuItemByTargetRecordId();
|
||||
|
||||
@@ -244,9 +239,7 @@ export const useIncrementalDeleteManyRecords = <T>({
|
||||
objectMetadataNamePlural: objectMetadataItem.namePlural,
|
||||
});
|
||||
|
||||
if (isNavigationMenuItemEnabled) {
|
||||
removeNavigationMenuItemsByTargetRecordIds(allDeletedRecordIds);
|
||||
}
|
||||
removeNavigationMenuItemsByTargetRecordIds(allDeletedRecordIds);
|
||||
|
||||
dispatchObjectRecordOperationBrowserEvent({
|
||||
objectMetadataItem,
|
||||
|
||||
+9
@@ -1,5 +1,6 @@
|
||||
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
@@ -15,6 +16,7 @@ import { RecordFieldListComponentInstanceContext } from '@/object-record/record-
|
||||
import { recordFieldListHoverPositionComponentState } from '@/object-record/record-field-list/states/recordFieldListHoverPositionComponentState';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { isJunctionRelationForbidden } from '@/object-record/record-field/ui/utils/junction/isJunctionRelationForbidden';
|
||||
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
|
||||
import { PropertyBoxSkeletonLoader } from '@/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader';
|
||||
@@ -53,6 +55,7 @@ export const RecordFieldList = ({
|
||||
});
|
||||
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
|
||||
objectNameSingular,
|
||||
@@ -187,6 +190,12 @@ export const RecordFieldList = ({
|
||||
fieldName: fieldMetadataItem.name,
|
||||
prefix: instanceId,
|
||||
})}`,
|
||||
isForbidden: isJunctionRelationForbidden({
|
||||
fieldMetadataItem,
|
||||
sourceObjectMetadataId: objectMetadataItem.id,
|
||||
objectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
|
||||
+5
@@ -10,6 +10,7 @@ import { isActivityTargetField } from '@/object-record/record-field-list/utils/c
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { useFieldFocus } from '@/object-record/record-field/ui/hooks/useFieldFocus';
|
||||
import { useRelationFromManyFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/useRelationFromManyFieldDisplay';
|
||||
import { ForbiddenFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/ForbiddenFieldDisplay';
|
||||
import { extractTargetRecordsFromJunction } from '@/object-record/record-field/ui/utils/junction/extractTargetRecordsFromJunction';
|
||||
import { getJunctionConfig } from '@/object-record/record-field/ui/utils/junction/getJunctionConfig';
|
||||
import { hasJunctionConfig } from '@/object-record/record-field/ui/utils/junction/hasJunctionConfig';
|
||||
@@ -144,6 +145,10 @@ export const RelationFromManyFieldDisplay = () => {
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
if (fieldValue.some(isDefined) && targetRecordsWithMetadata.length === 0) {
|
||||
return <ForbiddenFieldDisplay />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ExpandableList isChipCountDisplayed={isFocused}>
|
||||
{targetRecordsWithMetadata.map(({ record, objectMetadata }) => (
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
|
||||
import { getJunctionConfig } from '@/object-record/record-field/ui/utils/junction/getJunctionConfig';
|
||||
import { getTargetObjectMetadataIdsFromField } from '@/object-record/record-field/ui/utils/junction/getTargetObjectMetadataIdsFromField';
|
||||
import { hasJunctionConfig } from '@/object-record/record-field/ui/utils/junction/hasJunctionConfig';
|
||||
import { type ObjectPermissions } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type JunctionObjectMetadataItem } from './getJunctionConfig';
|
||||
|
||||
type ObjectPermissionsByObjectMetadataId = Record<
|
||||
string,
|
||||
ObjectPermissions & { objectMetadataId: string }
|
||||
>;
|
||||
|
||||
// Returns true if a junction field's intermediate or final target object
|
||||
// is not readable by the current user.
|
||||
export const isJunctionRelationForbidden = ({
|
||||
fieldMetadataItem,
|
||||
sourceObjectMetadataId,
|
||||
objectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
}: {
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'settings' | 'relation'>;
|
||||
sourceObjectMetadataId: string;
|
||||
objectMetadataItems: JunctionObjectMetadataItem[];
|
||||
objectPermissionsByObjectMetadataId: ObjectPermissionsByObjectMetadataId;
|
||||
}): boolean => {
|
||||
if (!hasJunctionConfig(fieldMetadataItem.settings)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const junctionObjectMetadataId =
|
||||
fieldMetadataItem.relation?.targetObjectMetadata.id;
|
||||
|
||||
if (!isDefined(junctionObjectMetadataId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const junctionPermissions = getObjectPermissionsForObject(
|
||||
objectPermissionsByObjectMetadataId,
|
||||
junctionObjectMetadataId,
|
||||
);
|
||||
|
||||
if (!junctionPermissions.canReadObjectRecords) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const junctionConfig = getJunctionConfig({
|
||||
settings: fieldMetadataItem.settings,
|
||||
relationObjectMetadataId: junctionObjectMetadataId,
|
||||
sourceObjectMetadataId,
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
if (!isDefined(junctionConfig)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetObjectMetadataIds = junctionConfig.targetFields.flatMap(
|
||||
getTargetObjectMetadataIdsFromField,
|
||||
);
|
||||
|
||||
if (targetObjectMetadataIds.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !targetObjectMetadataIds.some(
|
||||
(targetId) =>
|
||||
getObjectPermissionsForObject(
|
||||
objectPermissionsByObjectMetadataId,
|
||||
targetId,
|
||||
).canReadObjectRecords,
|
||||
);
|
||||
};
|
||||
+22
-1
@@ -1,6 +1,8 @@
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { useIsRecordFieldReadOnly } from '@/object-record/read-only/hooks/useIsRecordFieldReadOnly';
|
||||
import {
|
||||
@@ -10,6 +12,7 @@ import {
|
||||
} from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { FieldFocusContextProvider } from '@/object-record/record-field/ui/contexts/FieldFocusContextProvider';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { isJunctionRelationForbidden } from '@/object-record/record-field/ui/utils/junction/isJunctionRelationForbidden';
|
||||
import { RecordInlineCellAnchoredPortalContext } from '@/object-record/record-inline-cell/components/RecordInlineCellAnchoredPortalContext';
|
||||
import { RecordInlineCellCloseOnCommandMenuOpeningEffect } from '@/object-record/record-inline-cell/components/RecordInlineCellCloseOnCommandMenuOpeningEffect';
|
||||
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
@@ -19,7 +22,14 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
type RecordInlineCellAnchoredPortalProps = {
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'id' | 'name' | 'type' | 'createdAt' | 'updatedAt' | 'label'
|
||||
| 'id'
|
||||
| 'name'
|
||||
| 'type'
|
||||
| 'createdAt'
|
||||
| 'updatedAt'
|
||||
| 'label'
|
||||
| 'settings'
|
||||
| 'relation'
|
||||
>;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
recordId: string;
|
||||
@@ -52,6 +62,16 @@ export const RecordInlineCellAnchoredPortal = ({
|
||||
recordId: recordId ?? '',
|
||||
});
|
||||
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const isForbidden = isJunctionRelationForbidden({
|
||||
fieldMetadataItem,
|
||||
sourceObjectMetadataId: objectMetadataItem.id,
|
||||
objectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
});
|
||||
|
||||
const { updateOneRecord } = useUpdateOneRecord();
|
||||
|
||||
const useUpdateOneObjectRecordMutation: RecordUpdateHook = () => {
|
||||
@@ -88,6 +108,7 @@ export const RecordInlineCellAnchoredPortal = ({
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
isDisplayModeFixHeight: true,
|
||||
isRecordFieldReadOnly,
|
||||
isForbidden,
|
||||
onCloseEditMode,
|
||||
}}
|
||||
>
|
||||
|
||||
+8
@@ -1,8 +1,11 @@
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { FieldDisplay } from '@/object-record/record-field/ui/components/FieldDisplay';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { useFieldFocus } from '@/object-record/record-field/ui/hooks/useFieldFocus';
|
||||
import { ForbiddenFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/ForbiddenFieldDisplay';
|
||||
import { useRecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext';
|
||||
import { RecordInlineCellDisplayMode } from '@/object-record/record-inline-cell/components/RecordInlineCellDisplayMode';
|
||||
import { RecordInlineCellSkeletonLoader } from '@/object-record/record-inline-cell/components/RecordInlineCellSkeletonLoader';
|
||||
@@ -33,11 +36,16 @@ export const RecordInlineCellValue = () => {
|
||||
const { readonly, loading, isCentered, onOpenEditMode } =
|
||||
useRecordInlineCellContext();
|
||||
const { isFocused } = useFieldFocus();
|
||||
const { isForbidden } = useContext(FieldContext);
|
||||
|
||||
if (loading === true) {
|
||||
return <RecordInlineCellSkeletonLoader />;
|
||||
}
|
||||
|
||||
if (isForbidden === true) {
|
||||
return <ForbiddenFieldDisplay />;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledClickableContainer readonly={readonly} isCentered={isCentered}>
|
||||
<RecordInlineCellDisplayMode
|
||||
|
||||
+35
@@ -1,3 +1,4 @@
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
|
||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
|
||||
@@ -5,11 +6,15 @@ import { type RecordField } from '@/object-record/record-field/types/RecordField
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { isFieldRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOne';
|
||||
import { isFieldRelationOneToMany } from '@/object-record/record-field/ui/types/guards/isFieldRelationOneToMany';
|
||||
import { getJunctionConfig } from '@/object-record/record-field/ui/utils/junction/getJunctionConfig';
|
||||
import { getTargetObjectMetadataIdsFromField } from '@/object-record/record-field/ui/utils/junction/getTargetObjectMetadataIdsFromField';
|
||||
import { hasJunctionConfig } from '@/object-record/record-field/ui/utils/junction/hasJunctionConfig';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { useContext, type ReactNode } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type RecordTableCellFieldContextGenericProps = {
|
||||
recordField: RecordField;
|
||||
@@ -30,6 +35,8 @@ export const RecordTableCellFieldContextGeneric = ({
|
||||
fieldDefinitionByFieldMetadataItemId,
|
||||
} = useRecordIndexContextOrThrow();
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const fieldDefinition =
|
||||
fieldDefinitionByFieldMetadataItemId[recordField.fieldMetadataItemId];
|
||||
|
||||
@@ -51,6 +58,34 @@ export const RecordTableCellFieldContextGeneric = ({
|
||||
);
|
||||
|
||||
hasObjectReadPermissions = relationObjectPermissions.canReadObjectRecords;
|
||||
|
||||
if (
|
||||
hasObjectReadPermissions &&
|
||||
hasJunctionConfig(fieldDefinition.metadata.settings)
|
||||
) {
|
||||
const junctionConfig = getJunctionConfig({
|
||||
settings: fieldDefinition.metadata.settings,
|
||||
relationObjectMetadataId,
|
||||
sourceObjectMetadataId: objectMetadataItem.id,
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
if (isDefined(junctionConfig)) {
|
||||
const targetObjectMetadataIds = junctionConfig.targetFields.flatMap(
|
||||
getTargetObjectMetadataIdsFromField,
|
||||
);
|
||||
|
||||
if (targetObjectMetadataIds.length > 0) {
|
||||
hasObjectReadPermissions = targetObjectMetadataIds.some(
|
||||
(targetId) =>
|
||||
getObjectPermissionsForObject(
|
||||
objectPermissionsByObjectMetadataId,
|
||||
targetId,
|
||||
).canReadObjectRecords,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+9
@@ -1,5 +1,6 @@
|
||||
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
@@ -11,6 +12,7 @@ import { recordFieldListHoverPositionComponentState } from '@/object-record/reco
|
||||
import { isActivityTargetField } from '@/object-record/record-field-list/utils/categorizeRelationFields';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { isJunctionRelationForbidden } from '@/object-record/record-field/ui/utils/junction/isJunctionRelationForbidden';
|
||||
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||
import { PropertyBoxSkeletonLoader } from '@/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader';
|
||||
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
|
||||
@@ -73,6 +75,7 @@ export const FieldsWidget = ({ widget }: FieldsWidgetProps) => {
|
||||
});
|
||||
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
|
||||
objectNameSingular: targetRecord.targetObjectNameSingular,
|
||||
@@ -181,6 +184,12 @@ export const FieldsWidget = ({ widget }: FieldsWidgetProps) => {
|
||||
fieldName: fieldMetadataItem.name,
|
||||
prefix: instanceId,
|
||||
})}`,
|
||||
isForbidden: isJunctionRelationForbidden({
|
||||
fieldMetadataItem,
|
||||
sourceObjectMetadataId: objectMetadataItem.id,
|
||||
objectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{isActivityTarget ? (
|
||||
|
||||
+4
-4
@@ -23,8 +23,8 @@ import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const PrefetchRunFavoriteQueriesEffect = () => {
|
||||
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
const showAuthModal = useShowAuthModal();
|
||||
const isSettingsPage = useIsSettingsPage();
|
||||
@@ -64,7 +64,7 @@ export const PrefetchRunFavoriteQueriesEffect = () => {
|
||||
showAuthModal ||
|
||||
isSettingsPage ||
|
||||
!isWorkspaceActive ||
|
||||
isNavigationMenuItemEnabled,
|
||||
isNavigationMenuItemEditingEnabled,
|
||||
});
|
||||
|
||||
const { records: favoriteFolders } = useFindManyRecords({
|
||||
@@ -75,7 +75,7 @@ export const PrefetchRunFavoriteQueriesEffect = () => {
|
||||
showAuthModal ||
|
||||
isSettingsPage ||
|
||||
!isWorkspaceActive ||
|
||||
isNavigationMenuItemEnabled,
|
||||
isNavigationMenuItemEditingEnabled,
|
||||
});
|
||||
|
||||
const setPrefetchFavoritesState = useRecoilCallback(
|
||||
|
||||
+3
-3
@@ -18,8 +18,8 @@ import {
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const PrefetchRunNavigationMenuItemQueriesEffect = () => {
|
||||
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
|
||||
const showAuthModal = useShowAuthModal();
|
||||
@@ -37,7 +37,7 @@ export const PrefetchRunNavigationMenuItemQueriesEffect = () => {
|
||||
showAuthModal ||
|
||||
isSettingsPage ||
|
||||
!isWorkspaceActive ||
|
||||
!isNavigationMenuItemEnabled,
|
||||
!isNavigationMenuItemEditingEnabled,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
|
||||
+5
-11
@@ -1,6 +1,5 @@
|
||||
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { useCreateNavigationMenuItem } from '@/navigation-menu-item/hooks/useCreateNavigationMenuItem';
|
||||
import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData';
|
||||
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
@@ -24,8 +23,8 @@ import {
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
ViewVisibility,
|
||||
PermissionFlagType,
|
||||
ViewVisibility,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type ViewPickerOptionDropdownProps = {
|
||||
@@ -63,10 +62,9 @@ export const ViewPickerOptionDropdown = ({
|
||||
|
||||
const { sortedFavorites: favorites } = useFavorites();
|
||||
const { createFavorite } = useCreateFavorite();
|
||||
const isNavigationMenuItemEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
const { createNavigationMenuItem } = useCreateNavigationMenuItem();
|
||||
const { navigationMenuItems, currentWorkspaceMemberId } =
|
||||
usePrefetchedNavigationMenuItemsData();
|
||||
|
||||
@@ -75,7 +73,7 @@ export const ViewPickerOptionDropdown = ({
|
||||
const canEditView =
|
||||
hasViewsPermission || view.visibility === ViewVisibility.UNLISTED;
|
||||
|
||||
const isFavorite = isNavigationMenuItemEnabled
|
||||
const isFavorite = isNavigationMenuItemEditingEnabled
|
||||
? navigationMenuItems.some(
|
||||
(item) =>
|
||||
item.viewId === view.id &&
|
||||
@@ -94,11 +92,7 @@ export const ViewPickerOptionDropdown = ({
|
||||
|
||||
const handleAddToFavorites = () => {
|
||||
if (!isFavorite) {
|
||||
if (isNavigationMenuItemEnabled) {
|
||||
createNavigationMenuItem(view, 'view');
|
||||
} else {
|
||||
createFavorite(view, 'view');
|
||||
}
|
||||
createFavorite(view, 'view');
|
||||
} else {
|
||||
setViewPickerReferenceViewId(view.id);
|
||||
setViewPickerMode('favorite-folders-picker');
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
export class ChangeNavigationMenuItemPositionToDoublePrecision1771499112046
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'ChangeNavigationMenuItemPositionToDoublePrecision1771499112046';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" ALTER COLUMN "position" TYPE double precision`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" ALTER COLUMN "position" TYPE integer`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+18
-1
@@ -12,6 +12,8 @@ import { GoogleAPIsOauthExchangeCodeForTokenStrategy } from 'src/engine/core-mod
|
||||
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
|
||||
import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util';
|
||||
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -27,6 +29,7 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
private readonly workspaceDomainsService: WorkspaceDomainsService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -46,7 +49,21 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
|
||||
);
|
||||
}
|
||||
|
||||
new GoogleAPIsOauthExchangeCodeForTokenStrategy(this.twentyConfigService);
|
||||
const { workspaceId } =
|
||||
await this.transientTokenService.verifyTransientToken(
|
||||
state.transientToken,
|
||||
);
|
||||
|
||||
const isDraftEmailEnabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_DRAFT_EMAIL_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
new GoogleAPIsOauthExchangeCodeForTokenStrategy(
|
||||
this.twentyConfigService,
|
||||
isDraftEmailEnabled,
|
||||
);
|
||||
|
||||
setRequestExtraParams(request, {
|
||||
transientToken: state.transientToken,
|
||||
|
||||
+13
-1
@@ -12,6 +12,8 @@ import { GoogleAPIsOauthRequestCodeStrategy } from 'src/engine/core-modules/auth
|
||||
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
|
||||
import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util';
|
||||
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -25,6 +27,7 @@ export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') {
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
private readonly workspaceDomainsService: WorkspaceDomainsService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {
|
||||
super({
|
||||
prompt: 'select_account',
|
||||
@@ -68,7 +71,16 @@ export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') {
|
||||
);
|
||||
}
|
||||
|
||||
new GoogleAPIsOauthRequestCodeStrategy(this.twentyConfigService);
|
||||
const isDraftEmailEnabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_DRAFT_EMAIL_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
new GoogleAPIsOauthRequestCodeStrategy(
|
||||
this.twentyConfigService,
|
||||
isDraftEmailEnabled,
|
||||
);
|
||||
|
||||
return (await super.canActivate(context)) as boolean;
|
||||
} catch (err) {
|
||||
|
||||
+19
-3
@@ -92,8 +92,24 @@ describe('GoogleAPIScopesService', () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should work with the current Google API scopes', () => {
|
||||
// What is currently returned by Google
|
||||
it('should work with the current Google API scopes when draft email is disabled', () => {
|
||||
const actualGoogleScopes = [
|
||||
'https://www.googleapis.com/auth/calendar.events',
|
||||
'https://www.googleapis.com/auth/gmail.readonly',
|
||||
'https://www.googleapis.com/auth/gmail.send',
|
||||
'https://www.googleapis.com/auth/profile.emails.read',
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'openid',
|
||||
];
|
||||
const expectedScopes = getGoogleApisOauthScopes(false);
|
||||
|
||||
const result = includesExpectedScopes(actualGoogleScopes, expectedScopes);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should work with the current Google API scopes when draft email is enabled', () => {
|
||||
const actualGoogleScopes = [
|
||||
'https://www.googleapis.com/auth/calendar.events',
|
||||
'https://www.googleapis.com/auth/gmail.readonly',
|
||||
@@ -104,7 +120,7 @@ describe('GoogleAPIScopesService', () => {
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'openid',
|
||||
];
|
||||
const expectedScopes = getGoogleApisOauthScopes();
|
||||
const expectedScopes = getGoogleApisOauthScopes(true);
|
||||
|
||||
const result = includesExpectedScopes(actualGoogleScopes, expectedScopes);
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export class GoogleAPIScopesService {
|
||||
|
||||
public async getScopesFromGoogleAccessTokenAndCheckIfExpectedScopesArePresent(
|
||||
accessToken: string,
|
||||
isDraftEmailEnabled = false,
|
||||
): Promise<{ scopes: string[]; isValid: boolean }> {
|
||||
try {
|
||||
const httpClient = this.secureHttpClientService.getHttpClient();
|
||||
@@ -40,7 +41,7 @@ export class GoogleAPIScopesService {
|
||||
);
|
||||
|
||||
const scopes = response.data.scope.split(' ');
|
||||
const expectedScopes = getGoogleApisOauthScopes();
|
||||
const expectedScopes = getGoogleApisOauthScopes(isDraftEmailEnabled);
|
||||
|
||||
return {
|
||||
scopes,
|
||||
|
||||
+7
@@ -10,6 +10,7 @@ import { GoogleAPIScopesService } from 'src/engine/core-modules/auth/services/go
|
||||
import { GoogleApisServiceAvailabilityService } from 'src/engine/core-modules/auth/services/google-apis-service-availability.service';
|
||||
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
|
||||
import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
@@ -181,6 +182,12 @@ describe('GoogleAPIsService', () => {
|
||||
provide: getQueueToken(MessageQueue.calendarQueue),
|
||||
useValue: mockCalendarQueueService,
|
||||
},
|
||||
{
|
||||
provide: FeatureFlagService,
|
||||
useValue: {
|
||||
isFeatureEnabled: jest.fn().mockResolvedValue(false),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ import { CreateMessageChannelService } from 'src/engine/core-modules/auth/servic
|
||||
import { GoogleAPIScopesService } from 'src/engine/core-modules/auth/services/google-apis-scopes';
|
||||
import { GoogleApisServiceAvailabilityService } from 'src/engine/core-modules/auth/services/google-apis-service-availability.service';
|
||||
import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
@@ -63,6 +65,7 @@ export class GoogleAPIsService {
|
||||
private readonly updateConnectedAccountOnReconnectService: UpdateConnectedAccountOnReconnectService,
|
||||
private readonly googleAPIScopesService: GoogleAPIScopesService,
|
||||
private readonly googleApisServiceAvailabilityService: GoogleApisServiceAvailabilityService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
async refreshGoogleRefreshToken(input: {
|
||||
@@ -92,9 +95,15 @@ export class GoogleAPIsService {
|
||||
'MESSAGING_PROVIDER_GMAIL_ENABLED',
|
||||
);
|
||||
|
||||
const isDraftEmailEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_DRAFT_EMAIL_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const { scopes, isValid } =
|
||||
await this.googleAPIScopesService.getScopesFromGoogleAccessTokenAndCheckIfExpectedScopesArePresent(
|
||||
input.accessToken,
|
||||
isDraftEmailEnabled,
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
|
||||
+5
-2
@@ -14,8 +14,11 @@ export abstract class GoogleAPIsOauthCommonStrategy extends PassportStrategy(
|
||||
Strategy,
|
||||
'google-apis',
|
||||
) {
|
||||
constructor(twentyConfigService: TwentyConfigService) {
|
||||
const scopes = getGoogleApisOauthScopes();
|
||||
constructor(
|
||||
twentyConfigService: TwentyConfigService,
|
||||
isDraftEmailEnabled = false,
|
||||
) {
|
||||
const scopes = getGoogleApisOauthScopes(isDraftEmailEnabled);
|
||||
|
||||
super({
|
||||
clientID: twentyConfigService.get('AUTH_GOOGLE_CLIENT_ID'),
|
||||
|
||||
+5
-2
@@ -12,8 +12,11 @@ export type GoogleAPIScopeConfig = {
|
||||
|
||||
@Injectable()
|
||||
export class GoogleAPIsOauthExchangeCodeForTokenStrategy extends GoogleAPIsOauthCommonStrategy {
|
||||
constructor(twentyConfigService: TwentyConfigService) {
|
||||
super(twentyConfigService);
|
||||
constructor(
|
||||
twentyConfigService: TwentyConfigService,
|
||||
isDraftEmailEnabled = false,
|
||||
) {
|
||||
super(twentyConfigService, isDraftEmailEnabled);
|
||||
}
|
||||
|
||||
async validate(
|
||||
|
||||
+5
-2
@@ -12,8 +12,11 @@ export type GoogleAPIScopeConfig = {
|
||||
|
||||
@Injectable()
|
||||
export class GoogleAPIsOauthRequestCodeStrategy extends GoogleAPIsOauthCommonStrategy {
|
||||
constructor(twentyConfigService: TwentyConfigService) {
|
||||
super(twentyConfigService);
|
||||
constructor(
|
||||
twentyConfigService: TwentyConfigService,
|
||||
isDraftEmailEnabled = false,
|
||||
) {
|
||||
super(twentyConfigService, isDraftEmailEnabled);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
+4
-2
@@ -1,7 +1,7 @@
|
||||
/** email, profile and openid permission can be called without the https://www.googleapis.com/auth/ prefix
|
||||
* see https://developers.google.com/identity/protocols/oauth2/scopes
|
||||
*/
|
||||
export const getGoogleApisOauthScopes = () => {
|
||||
export const getGoogleApisOauthScopes = (isDraftEmailEnabled = false) => {
|
||||
return [
|
||||
'email',
|
||||
'profile',
|
||||
@@ -9,6 +9,8 @@ export const getGoogleApisOauthScopes = () => {
|
||||
'https://www.googleapis.com/auth/calendar.events',
|
||||
'https://www.googleapis.com/auth/profile.emails.read',
|
||||
'https://www.googleapis.com/auth/gmail.send',
|
||||
'https://www.googleapis.com/auth/gmail.compose',
|
||||
...(isDraftEmailEnabled
|
||||
? ['https://www.googleapis.com/auth/gmail.compose']
|
||||
: []),
|
||||
];
|
||||
};
|
||||
|
||||
+2
-7
@@ -8,7 +8,7 @@ import ms from 'ms';
|
||||
import { SendEmailVerificationLinkEmail } from 'twenty-emails';
|
||||
import { type APP_LOCALES } from 'twenty-shared/translations';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
@@ -138,12 +138,7 @@ export class EmailVerificationService {
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(user)) {
|
||||
throw new EmailVerificationException(
|
||||
'User not found for the given email',
|
||||
EmailVerificationExceptionCode.INVALID_EMAIL,
|
||||
);
|
||||
}
|
||||
assertIsDefinedOrThrow(user);
|
||||
|
||||
if (user.isEmailVerified) {
|
||||
throw new EmailVerificationException(
|
||||
|
||||
+4
-5
@@ -1,6 +1,6 @@
|
||||
import { Field, InputType, Int } from '@nestjs/graphql';
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsInt, IsOptional, IsString, IsUUID, Min } from 'class-validator';
|
||||
import { IsNumber, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@@ -46,9 +46,8 @@ export class CreateNavigationMenuItemInput {
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
folderId?: string;
|
||||
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Field(() => Int, { nullable: true })
|
||||
@Field({ nullable: true })
|
||||
position?: number;
|
||||
}
|
||||
|
||||
+4
-6
@@ -1,13 +1,12 @@
|
||||
import { Field, InputType, Int } from '@nestjs/graphql';
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
Min,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
@@ -20,10 +19,9 @@ export class UpdateNavigationMenuItemInput {
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
folderId?: string | null;
|
||||
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Field(() => Int, { nullable: true })
|
||||
@Field({ nullable: true })
|
||||
position?: number;
|
||||
|
||||
@IsOptional()
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ export class NavigationMenuItemEntity
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
folderId: string | null;
|
||||
|
||||
@Column({ nullable: false })
|
||||
@Column({ nullable: false, type: 'double precision' })
|
||||
position: number;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
|
||||
+12
-19
@@ -473,19 +473,14 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
});
|
||||
}
|
||||
|
||||
const isNavigationMenuItemEnabled =
|
||||
existingFeatureFlagsMap[FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED] ??
|
||||
false;
|
||||
|
||||
const flatNavigationMenuItemToCreate = isNavigationMenuItemEnabled
|
||||
? await this.computeFlatNavigationMenuItemToCreate({
|
||||
view: flatDefaultViewToCreate,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
workspaceCustomApplicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
})
|
||||
: null;
|
||||
const flatNavigationMenuItemToCreate =
|
||||
await this.computeFlatNavigationMenuItemToCreate({
|
||||
view: flatDefaultViewToCreate,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
workspaceCustomApplicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
@@ -592,12 +587,10 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
);
|
||||
}
|
||||
|
||||
if (!isNavigationMenuItemEnabled) {
|
||||
await this.createWorkspaceFavoriteForNewObjectDefaultView({
|
||||
view: flatDefaultViewToCreate,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
await this.createWorkspaceFavoriteForNewObjectDefaultView({
|
||||
view: flatDefaultViewToCreate,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return createdFlatObjectMetadata;
|
||||
}
|
||||
|
||||
-1
@@ -4,7 +4,6 @@ export const DEFAULT_FEATURE_FLAGS = [
|
||||
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
|
||||
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
|
||||
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
FeatureFlagKey.IS_CORE_PICTURE_MIGRATED,
|
||||
FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
|
||||
FeatureFlagKey.IS_OTHER_FILE_MIGRATED,
|
||||
|
||||
+6
-10
@@ -177,13 +177,12 @@ export class FlatNavigationMenuItemValidatorService {
|
||||
|
||||
if (
|
||||
isDefined(flatNavigationMenuItem.position) &&
|
||||
(!Number.isInteger(flatNavigationMenuItem.position) ||
|
||||
flatNavigationMenuItem.position < 0)
|
||||
!Number.isFinite(flatNavigationMenuItem.position)
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
||||
message: t`Position must be a non-negative integer`,
|
||||
userFriendlyMessage: msg`Position must be a non-negative integer`,
|
||||
message: t`Position must be a finite number`,
|
||||
userFriendlyMessage: msg`Position must be a finite number`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -307,14 +306,11 @@ export class FlatNavigationMenuItemValidatorService {
|
||||
|
||||
const positionUpdate = flatEntityUpdate.position;
|
||||
|
||||
if (
|
||||
isDefined(positionUpdate) &&
|
||||
(!Number.isInteger(positionUpdate) || positionUpdate < 0)
|
||||
) {
|
||||
if (isDefined(positionUpdate) && !Number.isFinite(positionUpdate)) {
|
||||
validationResult.errors.push({
|
||||
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
||||
message: t`Position must be a non-negative integer`,
|
||||
userFriendlyMessage: msg`Position must be a non-negative integer`,
|
||||
message: t`Position must be a finite number`,
|
||||
userFriendlyMessage: msg`Position must be a finite number`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -186,7 +186,10 @@ export class MatchParticipantService<
|
||||
await participantRepository.updateMany(
|
||||
partipantsToBeUpdated.map((participant) => ({
|
||||
criteria: participant.id,
|
||||
partialEntity: participant,
|
||||
partialEntity: {
|
||||
personId: participant.personId,
|
||||
workspaceMemberId: participant.workspaceMemberId,
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
|
||||
-11
@@ -142,14 +142,3 @@ exports[`NavigationMenuItem creation should fail when creating with missing targ
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`NavigationMenuItem creation should fail when creating with negative position 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "NOT_FOUND",
|
||||
"subCode": "RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Could not find objectMetadata for given targetObjectMetadataId",
|
||||
"name": "NotFoundError",
|
||||
}
|
||||
`;
|
||||
|
||||
-35
@@ -71,38 +71,3 @@ exports[`NavigationMenuItem update should fail when updating with missing id 1`]
|
||||
"name": "ValidationError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`NavigationMenuItem update should fail when updating with negative position 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"navigationMenuItem": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_NAVIGATION_MENU_ITEM_INPUT",
|
||||
"message": "Position must be a non-negative integer",
|
||||
"userFriendlyMessage": "Position must be a non-negative integer",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "navigationMenuItem",
|
||||
"status": "fail",
|
||||
"type": "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 navigationMenuItem",
|
||||
"summary": {
|
||||
"navigationMenuItem": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Metadata validation failed",
|
||||
},
|
||||
"message": "Multiple validation errors occurred while updating navigation menu item",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
-10
@@ -86,16 +86,6 @@ const failingNavigationMenuItemCreationTestCases: EachTestingContext<TestContext
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when creating with negative position',
|
||||
context: {
|
||||
input: {
|
||||
targetRecordId: faker.string.uuid(),
|
||||
targetObjectMetadataId: faker.string.uuid(),
|
||||
position: -1,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('NavigationMenuItem creation should fail', () => {
|
||||
|
||||
-11
@@ -130,17 +130,6 @@ describe('NavigationMenuItem update should fail', () => {
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when updating with negative position',
|
||||
context: {
|
||||
input: (testSetup) => ({
|
||||
id: testSetup.testNavigationMenuItemId,
|
||||
update: {
|
||||
position: -1,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it.each(eachTestingContextFilter(failingNavigationMenuItemUpdateTestCases))(
|
||||
|
||||
Reference in New Issue
Block a user