i18n - docs translations (#21789)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com>
This commit is contained in:
committed by
GitHub
parent
22baf2c6c5
commit
2b3b2362db
+298
-298
File diff suppressed because it is too large
Load Diff
@@ -37,7 +37,7 @@ import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
|
||||
Authorization: Bearer YOUR_API_KEY
|
||||
```
|
||||
|
||||
أنشئ مفتاح API من **الإعدادات → واجهات برمجة التطبيقات وخطافات الويب → + Create key**. انسخه فورًا — يُعرَض مرة واحدة فقط. يمكن تقييد نطاق المفاتيح بدور محدد ضمن **الإعدادات → الأدوار → علامة التبويب Assignment** للحد مما يمكنها الوصول إليه.
|
||||
أنشئ مفتاح API من **الإعدادات → واجهات برمجة التطبيقات وخطافات الويب → + Create key**. انسخه فورًا — يُعرَض مرة واحدة فقط. يمكن تقييد نطاق المفاتيح بدور محدد ضمن **الإعدادات → الأعضاء → الأدوار → علامة التبويب Assignment** للحد مما يمكنها الوصول إليه.
|
||||
|
||||
<VimeoEmbed videoId="928786722" title="إنشاء مفتاح API" />
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: تكوين التطبيق
|
||||
description: عرّف هوية تطبيقك، والدور الافتراضي، والمتغيرات، وبيانات التعريف لسوق التطبيقات باستخدام defineApplication.
|
||||
icon: rocket
|
||||
---
|
||||
|
||||
يجب أن يحتوي كل تطبيق على استدعاء واحد فقط لـ `defineApplication`. يحدّد ما يلي:
|
||||
|
||||
* **الهوية** — المعرّف الشامل، واسم العرض، والوصف.
|
||||
* **الأذونات** — الدور الذي تعمل بموجبه دوال المنطق والمكوّنات الأمامية الخاصة به.
|
||||
* **المتغيرات** *(اختياري)* — أزواج مفتاح–قيمة تُتاح لكودك كمتغيرات بيئة.
|
||||
* **خطافات ما قبل التثبيت/ما بعد التثبيت** *(اختياري)* — راجع [Logic Functions](/l/ar/developers/extend/apps/logic/logic-functions).
|
||||
|
||||
```ts src/application-config.ts
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
|
||||
displayName: 'My Twenty App',
|
||||
description: 'My first Twenty app',
|
||||
applicationVariables: {
|
||||
DEFAULT_RECIPIENT_NAME: {
|
||||
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
|
||||
description: 'Default recipient name for postcards',
|
||||
value: 'Jane Doe',
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
الملاحظات:
|
||||
|
||||
* حقول `universalIdentifier` هي معرّفات حتمية تملكها أنت. أنشِئها مرة واحدة واحتفظ بها ثابتة عبر عمليات المزامنة.
|
||||
* `applicationVariables` تصبح متغيرات بيئة لوظائفك ومكوّناتك الأمامية. في وظائف المنطق (على جانب الخادم)، تكون متاحة على شكل `process.env.VARIABLE_NAME`. في المكوّنات الأمامية، استخدم `getApplicationVariable('VARIABLE_NAME')` من `twenty-sdk/front-component`. يتم حقن المتغيّرات المعلَّمة بـ `isSecret: true` في وظائف المنطق فقط. المكوّنات الأمامية تتلقّى المتغيّرات غير السرّية فقط.
|
||||
* يتم اكتشاف الدور الافتراضي تلقائيًا من ملف الدور المميز بـ [`defineApplicationRole()`](/l/ar/developers/extend/apps/config/roles) — لست بحاجة إلى الإشارة إليه من `defineApplication()`.
|
||||
* يتم اكتشاف دوال ما قبل التثبيت وما بعده تلقائيًا أثناء بناء البيان — لا حاجة للإشارة إليها في `defineApplication()`.
|
||||
* لا يزال تمرير `defaultRoleUniversalIdentifier` بشكل صريح مدعومًا من أجل التوافق مع الإصدارات السابقة، ولكنه مُهمل لصالح `defineApplicationRole()`.
|
||||
|
||||
## الدور الافتراضي للوظيفة
|
||||
|
||||
يتحكم الدور المعلن باستخدام [`defineApplicationRole()`](/l/ar/developers/extend/apps/config/roles) في ما يمكن لوظائف منطق التطبيق ومكوّنات الواجهة الوصول إليه:
|
||||
|
||||
* رمز وقت التشغيل المحقون باسم `TWENTY_APP_ACCESS_TOKEN` مستمد من هذا الدور.
|
||||
* يكون عميل واجهة برمجة التطبيقات مضبوط الأنواع مقيّدًا بالأذونات الممنوحة لذلك الدور.
|
||||
* اتبع مبدأ أقل امتياز: صرّح فقط عن الأذونات التي تحتاجها دوالك.
|
||||
|
||||
عند إنشاء هيكل لتطبيق جديد، ينشئ CLI ملف دور مبدئي في `src/roles/default-role.ts`. راجع [Roles & Permissions](/l/ar/developers/extend/apps/config/roles) للاطلاع على المرجع الكامل.
|
||||
|
||||
## بيانات التعريف لسوق التطبيقات
|
||||
|
||||
إذا كنت تخطط لـ [نشر تطبيقك](/l/ar/developers/extend/apps/operations/publishing)، فإن هذه الحقول الاختيارية تتحكّم في كيفية ظهوره في السوق:
|
||||
|
||||
| الحقل | الوصف |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------ |
|
||||
| `author` | اسم المؤلف أو الشركة |
|
||||
| `category` | فئة التطبيق لتصفية سوق التطبيقات |
|
||||
| `logoUrl` | مسار شعار تطبيقك (مثلًا، `public/logo.png`) |
|
||||
| `screenshots` | مصفوفة لمسارات لقطات الشاشة (مثلًا، `public/screenshot-1.png`) |
|
||||
| `aboutDescription` | وصف ماركداون أطول لعلامة التبويب "حول". إذا لم يتم تضمينه، يستخدم السوق ملف `README.md` الخاص بالحزمة من npm |
|
||||
| `websiteUrl` | رابط إلى موقعك الإلكتروني |
|
||||
| `termsUrl` | رابط إلى شروط الخدمة |
|
||||
| `emailSupport` | عنوان البريد الإلكتروني للدعم |
|
||||
| `issueReportUrl` | رابط إلى متتبّع المشاكل |
|
||||
@@ -0,0 +1,206 @@
|
||||
---
|
||||
title: خطافات التثبيت
|
||||
description: شغّل منطقًا قبل التثبيت أو بعده — لتهيئة البيانات، أو نسخ السجلات احتياطيًا، أو التحقّق من صحة الترقية.
|
||||
icon: wrench
|
||||
---
|
||||
|
||||
خطافات التثبيت هي دوال منطقية خاصة تعمل أثناء دورة حياة التثبيت أو الترقية. تستخدم نفس وقت تشغيل المعالج مثل [دوال المنطق](/l/ar/developers/extend/apps/logic/logic-functions) العادية وتتلقى `InstallPayload`، ولكن يتم التصريح عنها بدوال تعريف خاصة بها — `definePostInstallLogicFunction()` و`definePreInstallLogicFunction()` — وتعمل خارج نموذج المشغّل المعتاد (HTTP، وcron، وأحداث قاعدة البيانات).
|
||||
|
||||
يمكن لكل تطبيق تعريف دالة واحدة على الأكثر لما قبل التثبيت ودالة واحدة على الأكثر لما بعد التثبيت. سيُنتِج إنشاء ملف البيان خطأً إذا تم اكتشاف أكثر من واحدة من أيٍّ منهما.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ install flow │
|
||||
│ │
|
||||
│ upload package → [pre-install] → metadata migration → │
|
||||
│ generate SDK → [post-install] │
|
||||
│ │
|
||||
│ old schema visible new schema visible │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="definePostInstallLogicFunction" description="تعمل بعد تطبيق ترحيل البيانات الوصفية لمساحة العمل">
|
||||
|
||||
تعمل دالة ما بعد التثبيت تلقائيًا بمجرد انتهاء تثبيت تطبيقك على مساحة عمل. ينفّذه الخادم **بعد** مزامنة البيانات الوصفية للتطبيق وإنشاء عميل SDK، بحيث تكون مساحة العمل جاهزة تمامًا للاستخدام ويكون المخطط الجديد مطبَّقًا. تشمل حالات الاستخدام النموذجية تهيئة البيانات الافتراضية، وإنشاء السجلات الأولية، وتكوين إعدادات مساحة العمل، أو توفير الموارد على خدمات جهات خارجية.
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Post install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
shouldRunSynchronously: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
يمكنك أيضًا تنفيذ دالة ما بعد التثبيت يدويًا في أي وقت باستخدام CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:function:exec --postInstall
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
* تستخدم دوال ما بعد التثبيت `definePostInstallLogicFunction()` — إصدارًا متخصصًا يستبعد إعدادات المُشغِّل (`cronTriggerSettings` و`databaseEventTriggerSettings` و`httpRouteTriggerSettings` و`toolTriggerSettings` و`workflowActionTriggerSettings`).
|
||||
* يتلقى المعالج `InstallPayload` يحتوي على `{ previousVersion?: string; newVersion: string }` — حيث إن `newVersion` هو الإصدار الجاري تثبيته، و`previousVersion` هو الإصدار الذي كان مُثبّتًا سابقًا (أو `undefined` عند التثبيت الأولي). استخدم هذه القيم للتمييز بين عمليات التثبيت الجديدة والترقيات ولتشغيل منطق الترحيل الخاص بالإصدار.
|
||||
* **موعد تشغيل الخطاف**: في عمليات التثبيت الجديدة فقط، افتراضيًا. مرّر `shouldRunOnVersionUpgrade: true` إذا كنت تريد تشغيله أيضًا عند ترقية التطبيق من إصدار سابق. عند إغفاله، تكون القيمة الافتراضية للعلم `false`، وتتجاوز الترقيات هذا الخطاف.
|
||||
* **نموذج التنفيذ — غير متزامن افتراضيًا، والتزامني اختياري**: يتحكّم العلم `shouldRunSynchronously` في كيفية تنفيذ ما بعد التثبيت.
|
||||
* `shouldRunSynchronously: false` *(الإعداد الافتراضي)* — يتم **إدراج الخطاف في قائمة الرسائل** مع `retryLimit: 3` ويعمل بشكل غير متزامن داخل عامل عمل. يعود ردّ التثبيت بمجرد وضع المهمة في الطابور، لذا فإن معالجًا بطيئًا أو متعطلًا لا يحجب المستدعي. سيُجرِّب العامل إعادة المحاولة حتى ثلاث مرات. **استخدم هذا للمهام طويلة التشغيل** — بَذر مجموعات بيانات كبيرة، استدعاء واجهات برمجة تطبيقات خارجية بطيئة، تهيئة موارد خارجية، أو أي شيء قد يتجاوز نافذة استجابة HTTP المعقولة.
|
||||
* `shouldRunSynchronously: true` — يُنفّذ الخطاف **ضمن تدفّق التثبيت مباشرةً** (نفس المنفِّذ كما قبل التثبيت). يَحجُب طلب التثبيت حتى ينتهي المعالج، وإذا رمى استثناءً، سيتلقى مستدعي التثبيت `POST_INSTALL_ERROR`. لا توجد محاولات إعادة تلقائية. **استخدم هذا للمهام السريعة التي يجب إكمالها قبل الاستجابة** — مثل إظهار خطأ تحقق للمستخدم، أو إعداد سريع سيعتمد عليه العميل مباشرةً بعد عودة نداء التثبيت. ضع في اعتبارك أن ترحيل البيانات الوصفية يكون قد طُبِّق بالفعل عند تشغيل ما بعد التثبيت، لذلك فإن فشل الوضع المتزامن **لا** يعيد التغييرات على المخطط إلى الوراء — بل يكتفي بإبراز الخطأ.
|
||||
* تأكّد من أن معالجك قابل للتنفيذ المتكرر دون آثار جانبية. في الوضع غير المتزامن قد تُعيد قائمة الانتظار المحاولة حتى ثلاث مرات؛ وفي أي من الوضعين قد يعمل الخطاف مجددًا أثناء الترقيات عند ضبط `shouldRunOnVersionUpgrade: true`.
|
||||
* متغيرات البيئة `APPLICATION_ID` و`APP_ACCESS_TOKEN` و`API_URL` متاحة داخل المعالج (كما في أي دالة منطق أخرى)، لذا يمكنك استدعاء واجهة Twenty API باستخدام رمز وصول للتطبيق مقيّد بنطاق تطبيقك.
|
||||
* يُسمح بدالة ما بعد التثبيت واحدة فقط لكل تطبيق. سيُنتج إنشاء ملف البيان خطأً إذا تم اكتشاف أكثر من واحدة.
|
||||
* تُرفَق خصائص الدالة `universalIdentifier` و`shouldRunOnVersionUpgrade` و`shouldRunSynchronously` تلقائيًا ببيان التطبيق ضمن الحقل `postInstallLogicFunction` أثناء عملية البناء — ولا تحتاج إلى الإشارة إليها في [`defineApplication()`](/l/ar/developers/extend/apps/config/application).
|
||||
* تم تعيين مهلة افتراضية إلى 300 ثانية (5 دقائق) للسماح بمهام الإعداد الأطول مثل تهيئة البيانات.
|
||||
* **لا يُنفَّذ في وضع التطوير**: عند تسجيل تطبيق محليًا (عبر `yarn twenty dev`)، يتجاوز الخادم تدفّق التثبيت بالكامل ويُزامن الملفات مباشرةً عبر مراقِب CLI — لذا لن يعمل ما بعد التثبيت في وضع التطوير مطلقًا، بغضّ النظر عن `shouldRunSynchronously`. استخدم `yarn twenty dev:function:exec --postInstall` لتشغيله يدويًا على مساحة عمل قيد التشغيل.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePreInstallLogicFunction" description="تعمل قبل تطبيق ترحيل البيانات الوصفية لمساحة العمل">
|
||||
|
||||
تعمل دالة ما قبل التثبيت تلقائيًا أثناء التثبيت، **قبل تطبيق ترحيل البيانات الوصفية لمساحة العمل**. تتشارك نفس بنية الحمولة مع ما بعد التثبيت (`InstallPayload`)، لكنها موضوعة أبكر في تدفّق التثبيت كي تجهّز حالة يعتمد عليها الترحيل القادم — ومن الاستخدامات الشائعة: نسخ البيانات احتياطيًا، التحقق من التوافق مع المخطط الجديد، أو أرشفة السجلات التي ستُعاد هيكلتها أو ستُحذف.
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Pre install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before installation to prepare the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
يمكنك أيضًا تنفيذ دالة ما قبل التثبيت يدويًا في أي وقت باستخدام CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:function:exec --preInstall
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
* تستخدم دوال ما قبل التثبيت `definePreInstallLogicFunction()` — نفس الإعدادات المتخصصة كما في ما بعد التثبيت، لكنها مرتبطة بموضع مختلف ضمن دورة الحياة.
|
||||
* يتلقّى كلٌّ من معالجي ما قبل التثبيت وما بعد التثبيت النوع نفسه `InstallPayload`: `{ previousVersion?: string; newVersion: string }`. استورده مرة واحدة وأعد استخدامه لكلا الخطافين.
|
||||
* **موعد تشغيل الخطاف**: موضوع مباشرةً قبل ترحيل البيانات الوصفية لمساحة العمل (`synchronizeFromManifest`). قبل التنفيذ، يُشغِّل الخادم مزامنة "pared-down sync" ذات طابع إضافي فقط تقوم بتسجيل دالة ما قبل التثبيت للإصدار **الجديد** في البيانات الوصفية لمساحة العمل — دون لمس أي شيء آخر — ثم يُنفّذها. لأن هذه المزامنة «إضافية فقط»، تبقى كائنات وحقول وبيانات الإصدار السابق سليمة عند تشغيل معالجك: يمكنك قراءة حالة ما قبل الترحيل ونسخها احتياطيًا بأمان.
|
||||
* **نموذج التنفيذ**: يُنفَّذ ما قبل التثبيت **بشكل متزامن** و**يحجب عملية التثبيت**. إذا رمى المعالج استثناءً، تُلغى عملية التثبيت قبل تطبيق أي تغييرات على المخطط — وتبقى مساحة العمل على الإصدار السابق بحالة متّسقة. هذا مقصود: ما قبل التثبيت هو فرصتك الأخيرة لرفض ترقية تنطوي على مخاطر.
|
||||
* كما هو الحال مع ما بعد التثبيت، يُسمح بدالة ما قبل التثبيت واحدة فقط لكل تطبيق. تُربَط تلقائيًا ببيان التطبيق تحت `preInstallLogicFunction` أثناء عملية البناء.
|
||||
* **لا يُنفَّذ في وضع التطوير**: كما في ما بعد التثبيت — يتم تجاوز تدفّق التثبيت بالكامل للتطبيقات المسجّلة محليًا، لذا لن يعمل ما قبل التثبيت مطلقًا عند `yarn twenty dev`. استخدم `yarn twenty dev:function:exec --preInstall` لتشغيله يدويًا.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="ما قبل التثبيت مقابل ما بعد التثبيت: متى تستخدم أيّهما" description="اختيار خطاف التثبيت المناسب">
|
||||
|
||||
كلا الخطافين جزء من تدفّق التثبيت نفسه ويتلقّيان نفس `InstallPayload`. الاختلاف يكمن في **موعد** تشغيلهما نسبةً إلى ترحيل البيانات الوصفية لمساحة العمل، وهذا يغيّر البيانات التي يمكنهما التعامل معها بأمان.
|
||||
|
||||
ما قبل التثبيت دائمًا **متزامن** (يحجب التثبيت ويمكنه إحباطه). ما بعد التثبيت **غير متزامن افتراضيًا** — يُدرج على عامل مع محاولات إعادة تلقائية — لكن يمكن التبديل إلى تنفيذ متزامن عبر `shouldRunSynchronously: true`. راجع الأكورديون `definePostInstallLogicFunction` أعلاه لمعرفة متى تستخدم كل وضع.
|
||||
|
||||
**استخدم `post-install` لأي شيء يتطلّب وجود المخطط الجديد.** وهذا هو السيناريو الشائع:
|
||||
|
||||
* بَذر بيانات افتراضية (إنشاء سجلات أولية وعروض افتراضية ومحتوى تجريبي) للكائنات والحقول المضافة حديثًا.
|
||||
* تسجيل خطافات الويب مع خدمات أطراف ثالثة بعد أن حصل التطبيق على بيانات الاعتماد الخاصة به.
|
||||
* استدعاء واجهة برمجة التطبيقات الخاصة بك لإكمال إعداد يعتمد على البيانات الوصفية المتزامنة.
|
||||
* منطق قابل للتنفيذ المتكرر دون آثار جانبية لتحقيق "تأكّد من وجود هذا" والذي ينبغي مواءمة الحالة في كل ترقية — بالاقتران مع `shouldRunOnVersionUpgrade: true`.
|
||||
|
||||
مثال — بَذر سجل `PostCard` افتراضي بعد التثبيت:
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
|
||||
if (previousVersion) return; // fresh installs only
|
||||
|
||||
const client = createClient();
|
||||
await client.postCard.create({
|
||||
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
|
||||
});
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
|
||||
name: 'post-install',
|
||||
description: 'Seeds a welcome post card after install.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**استخدم `pre-install` عندما قد يُتلف الترحيل أو يدمّر البيانات الحالية.** لأن ما قبل التثبيت يعمل مقابل المخطط *السابق* وفشله يُرجِع الترقية إلى الوراء، فهو المكان المناسب لأي شيء محفوف بالمخاطر:
|
||||
|
||||
* **نسخ البيانات احتياطيًا قبل حذفها أو إعادة هيكلتها** — مثل إزالة حقل في v2 وتحتاج إلى نسخ قيمه إلى حقل آخر أو تصديرها إلى التخزين قبل تشغيل الترحيل.
|
||||
* **أرشفة السجلات التي سيبطلها قيد جديد** — مثل أن يصبح حقل ما `NOT NULL` وتحتاج أولًا إلى حذف الصفوف ذات القيم الفارغة أو إصلاحها.
|
||||
* **التحقق من التوافق ورفض الترقية إذا تعذّر ترحيل البيانات الحالية بسلاسة** — ارمِ من داخل المعالج وسيُلغى التثبيت دون تطبيق أي تغييرات. هذا أكثر أمانًا من اكتشاف عدم التوافق في منتصف الترحيل.
|
||||
* **إعادة تسمية البيانات أو إعادة تعيين مفاتيحها** قبل تغيير في المخطط قد يؤدي إلى فقدان الارتباط.
|
||||
|
||||
مثال — أرشف السجلات قبل ترحيل هدّام:
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
|
||||
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
|
||||
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = createClient();
|
||||
const legacyRecords = await client.postCard.findMany({
|
||||
where: { notes: { isNotNull: true } },
|
||||
});
|
||||
|
||||
if (legacyRecords.length === 0) return;
|
||||
|
||||
// Copy legacy `notes` into the new `description` field before the migration
|
||||
// drops the `notes` column. If this fails, the upgrade is aborted and the
|
||||
// workspace stays on v1 with all data intact.
|
||||
await Promise.all(
|
||||
legacyRecords.map((record) =>
|
||||
client.postCard.update({
|
||||
where: { id: record.id },
|
||||
data: { description: record.notes },
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Backs up legacy notes into description before the v2 migration.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**قاعدة عامة:**
|
||||
|
||||
| ترغب في... | استخدام |
|
||||
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
|
||||
| بذر بيانات افتراضية، تهيئة مساحة العمل، تسجيل موارد خارجية | `post-install` |
|
||||
| تشغيل بذر طويل الأمد أو استدعاءات أطراف ثالثة لا ينبغي أن تحجب استجابة التثبيت | `post-install` (الإعداد الافتراضي — `shouldRunSynchronously: false`، مع محاولات إعادة من العامل) |
|
||||
| تشغيل إعداد سريع سيعتمد عليه المستدعي مباشرةً بعد عودة نداء التثبيت | `post-install` مع `shouldRunSynchronously: true` |
|
||||
| قراءة البيانات أو نسخها احتياطيًا والتي قد يفقدها الترحيل القادم | `pre-install` |
|
||||
| رفض ترقية قد تُفسد البيانات الحالية | `pre-install` (ارمِ من المعالج) |
|
||||
| تنفيذ مواءمة في كل ترقية | `post-install` مع `shouldRunOnVersionUpgrade: true` |
|
||||
| تنفيذ إعداد لمرة واحدة في التثبيت الأول فقط | `post-install` مع `shouldRunOnVersionUpgrade: false` (الإعداد الافتراضي) |
|
||||
|
||||
<Note>
|
||||
إذا ساورك الشك، فاجعل الافتراضي هو **post-install**. الجأ إلى ما قبل التثبيت فقط عندما يكون الترحيل نفسه هدّامًا وتحتاج إلى التقاط الحالة السابقة قبل أن تزول.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: نظرة عامة
|
||||
description: قم بتهيئة التطبيق نفسه — هويته، والأذونات الافتراضية، وما الذي يعمل في وقت التثبيت.
|
||||
icon: screwdriver-wrench
|
||||
---
|
||||
|
||||
طبقة **الإعدادات (config layer)** لتطبيق Twenty هي ما يصف التطبيق *للمنصة* — هويته، والأذونات التي يمتلكها، والكود الذي يعمل أثناء التثبيت أو الترقية. هذه التصريحات لا تضيف أشكال بيانات جديدة أو سلوكًا وقت التشغيل؛ بل تخبر Twenty *من هو التطبيق* و*كيفية إعداده*.
|
||||
|
||||
```text
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ Application — identity, default role, variables, │
|
||||
│ marketplace metadata │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ Role — what the app's logic functions can read │ │
|
||||
│ │ and write (referenced by Application) │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼ (at install / upgrade time)
|
||||
┌──────────────────────────────────┐
|
||||
│ Pre-install hook │ before metadata migration
|
||||
└──────────────────────────────────┘
|
||||
┌──────────────────────────────────┐
|
||||
│ Post-install hook │ after metadata migration
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
## في هذا القسم
|
||||
|
||||
<CardGroup cols={٢}>
|
||||
<Card title="تكوين التطبيق" icon="rocket" href="/l/ar/developers/extend/apps/config/application">
|
||||
`defineApplication` — الهوية، الدور الافتراضي، المتغيرات، والبيانات الوصفية لسوق التطبيقات.
|
||||
</Card>
|
||||
<Card title="الأدوار والصلاحيات" icon="shield-halved" href="/l/ar/developers/extend/apps/config/roles">
|
||||
`defineRole` — حدِّد ما يمكن لوظائف منطق التطبيق قراءته وكتابته.
|
||||
</Card>
|
||||
<Card title="خطافات التثبيت" icon="wrench" href="/l/ar/developers/extend/apps/config/install-hooks">
|
||||
`definePreInstallLogicFunction` و`definePostInstallLogicFunction` — نسخ البيانات احتياطيًا، تهيئة القيم الافتراضية، والتحقق من صحة الترقيات.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## كيفية ترابط الأجزاء
|
||||
|
||||
* **التطبيق (Application)** هو نقطة الدخول. يحتوي كل تطبيق على استدعاء واحد فقط `defineApplication()`، ويشير إلى **دور (Role)** واحد باعتباره الدور الافتراضي له.
|
||||
* يتحكم **الدور** في ما يمكن لوظائف منطق التطبيق ومكوّنات الواجهة الأمامية قراءته وكتابته. اتبع مبدأ أقل امتياز ممكن: امنح فقط الصلاحيات التي يحتاجها الكود فعليًا.
|
||||
* تعمل **خطافات التثبيت** أثناء التثبيت أو الترقية — ما قبل التثبيت قبل ترحيل البيانات الوصفية (كي تتمكن من رفض ترقية محفوفة بالمخاطر)، وما بعد التثبيت بعد الترحيل (كي تتمكن من تهيئة بيانات افتراضية وفق المخطط الجديد).
|
||||
|
||||
<Note>
|
||||
تشارك خطافات التثبيت وقت تشغيل [وظيفة المنطق](/l/ar/developers/extend/apps/logic/logic-functions) — نفس توقيع المعالج (handler signature)، ونفس متغيرات البيئة، ونفس عميل واجهة برمجة التطبيقات (typed API client) — لكنها تُصرّح باستخدام دوال تعريف خاصة بها وتوجد خارج نموذج المشغلات العادي (HTTP، و cron، وأحداث قاعدة البيانات).
|
||||
</Note>
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: الأصول العامة
|
||||
description: وزّع الملفات الثابتة — الصور والأيقونات والخطوط — مع تطبيقك عبر مجلد public/.
|
||||
icon: folder-open
|
||||
---
|
||||
|
||||
يحتوي مجلد `public/` في جذر تطبيقك على ملفات ثابتة — صور وأيقونات وخطوط وأي أصول أخرى يحتاجها تطبيقك وقت التشغيل. تُدرج هذه الملفات تلقائيًا في عمليات البناء، وتُزامَن أثناء وضع التطوير، وتُرفَع إلى الخادم.
|
||||
|
||||
الملفات الموضوعة في `public/` هي:
|
||||
|
||||
* **متاحة للعامة** — بمجرد مزامنتها إلى الخادم، تُقدَّم الأصول عبر عنوان URL عام. لا حاجة إلى مصادقة للوصول إليها.
|
||||
* **متاحة في المكوّنات الأمامية** — استخدم عناوين الأصول لعرض الصور أو الأيقونات أو أي وسائط داخل مكوّنات React لديك.
|
||||
* **متاحة في الدوال المنطقية** — أشِر إلى عناوين الأصول في رسائل البريد الإلكتروني أو استجابات واجهات البرمجة أو أي منطق على جهة الخادم.
|
||||
* **مستخدمة لبيانات تعريف السوق** — يشير حقلا `logoUrl` و`screenshots` في `defineApplication()` إلى ملفات من هذا المجلد (مثل `public/logo.png`). تُعرَض هذه عند نشر تطبيقك في السوق.
|
||||
* **تُزامَن تلقائيًا في وضع التطوير** — عند إضافة ملف في `public/` أو تحديثه أو حذفه، تتم مزامنته إلى الخادم تلقائيًا. لا حاجة لإعادة التشغيل.
|
||||
* **مضمَّنة في عمليات البناء** — يقوم `yarn twenty dev:build` بتجميع جميع الأصول العامة ضمن مخرجات التوزيع.
|
||||
|
||||
## الوصول إلى الأصول العامة باستخدام `getPublicAssetUrl`
|
||||
|
||||
استخدم المساعد `getPublicAssetUrl` من `twenty-sdk` للحصول على العنوان الكامل لملف في دليل `public/` لديك. يعمل ذلك في كلٍ من الدوال المنطقية والمكوّنات الأمامية.
|
||||
|
||||
**في دالة منطقية:**
|
||||
|
||||
```ts src/logic-functions/send-invoice.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { getPublicAssetUrl } from 'twenty-sdk/utils';
|
||||
|
||||
const handler = async (): Promise<any> => {
|
||||
const logoUrl = getPublicAssetUrl('logo.png');
|
||||
const invoiceUrl = getPublicAssetUrl('templates/invoice.png');
|
||||
|
||||
// Fetch the file content (no auth required — public endpoint)
|
||||
const response = await fetch(invoiceUrl);
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
return { logoUrl, size: buffer.byteLength };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-...',
|
||||
name: 'send-invoice',
|
||||
description: 'Sends an invoice with the app logo',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**في مكوّن أمامي:**
|
||||
|
||||
```tsx src/front-components/company-card.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { getPublicAssetUrl } from 'twenty-sdk/utils';
|
||||
|
||||
const CompanyCard = () => {
|
||||
const logoUrl = getPublicAssetUrl('logo.png');
|
||||
|
||||
return <img src={logoUrl} alt="App logo" />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'company-card',
|
||||
component: CompanyCard,
|
||||
});
|
||||
```
|
||||
|
||||
وسيطة `path` نسبية إلى مجلد `public/` الخاص بتطبيقك. كلٌّ من `getPublicAssetUrl('logo.png')` و`getPublicAssetUrl('public/logo.png')` يُحلاّن إلى العنوان نفسه — تتم إزالة بادئة `public/` تلقائيًا إن وُجدت.
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: الأدوار والصلاحيات
|
||||
description: حدِّد الكائنات والحقول التي يمكن لوظائف منطق تطبيقك ومكوّنات الواجهة الأمامية قراءتها وكتابتها.
|
||||
icon: shield-halved
|
||||
---
|
||||
|
||||
**الدور** هو مجموعة من الأذونات: الكائنات التي يمكن لتطبيق ما قراءتها أو كتابتها، والحقول التي يمكنه رؤيتها، والقدرات على مستوى المنصّة التي يمكنه استخدامها. ترث جميع وظائف منطق كل تطبيق ومكوّنات الواجهة الأمامية الأذونات الخاصة بالدور المُعلَّم باستخدام `defineApplicationRole()` (انظر [دور الدالة الافتراضي](#the-default-function-role) أدناه).
|
||||
|
||||
```ts src/roles/restricted-company-role.ts
|
||||
import {
|
||||
defineRole,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
SystemPermissionFlag,
|
||||
} from 'twenty-sdk/define';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
|
||||
label: 'My new role',
|
||||
description: 'A role that can be used in your workspace',
|
||||
canReadAllObjectRecords: false,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canUpdateAllSettings: false,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToUsers: false,
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
],
|
||||
fieldPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
|
||||
.universalIdentifier,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
],
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
|
||||
});
|
||||
```
|
||||
|
||||
## الدور الافتراضي للوظيفة
|
||||
|
||||
عند إنشاء هيكل لتطبيق جديد، ينشئ CLI ملف دور افتراضي مُصرَّحًا به باستخدام `defineApplicationRole()`:
|
||||
|
||||
```ts src/roles/default-role.ts
|
||||
import { defineApplicationRole } from 'twenty-sdk/define';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'b648f87b-1d26-4961-b974-0908fd991061';
|
||||
|
||||
export default defineApplicationRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Default function role',
|
||||
description: 'Default role for function Twenty client',
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canUpdateAllSettings: false,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToUsers: false,
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [],
|
||||
fieldPermissions: [],
|
||||
permissionFlagUniversalIdentifiers: [],
|
||||
});
|
||||
```
|
||||
|
||||
تُعد `defineApplicationRole()` غلافًا بسيطًا حول `defineRole()` يشير إلى الدور المستخدم كإعداد افتراضي لتطبيقك وقت التثبيت. يتطابق التحقق من الصحة مع `defineRole`، لكن خط تجميع البناء يربط تلقائيًا قيمة `universalIdentifier` بصفة `defaultRoleUniversalIdentifier` في بيان التطبيق (manifest)، وبالتالي لا تحتاج إلى الإشارة إليه من [`defineApplication`](/l/ar/developers/extend/apps/config/application) بنفسك.
|
||||
|
||||
الملاحظات:
|
||||
|
||||
* يُسمح بوجود **استدعاء واحد فقط** لـ `defineApplicationRole(...)` لكل تطبيق — سيفشل إنشاء بيان التطبيق (manifest) إذا عثر على أكثر من واحد.
|
||||
* استخدم `defineRole()` (وليس `defineApplicationRole()`) لأي أدوار **إضافية** يأتي بها تطبيقك.
|
||||
* لا يزال تعيين `defaultRoleUniversalIdentifier` صراحةً على `defineApplication()` مدعومًا للتوافق مع الإصدارات السابقة، ولكنه مُهمَل لصالح `defineApplicationRole()`.
|
||||
|
||||
## أفضل الممارسات
|
||||
|
||||
* ابدأ من الدور المُنشأ تلقائيًا، ثم قم بتقييده تدريجيًا — إذ يمنح الإعداد الافتراضي صلاحيات قراءة واسعة، وهو ما نادرًا ما تريده في بيئة الإنتاج.
|
||||
* استبدل `objectPermissions` و`fieldPermissions` بالكائنات والحقول الدقيقة التي تحتاجها وظائفك فعليًا.
|
||||
* `permissionFlagUniversalIdentifiers` تتحكم في الوصول إلى القدرات على مستوى المنصة. اجعلها في حدّها الأدنى.
|
||||
* اطّلع على مثال عملي: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: توسيع الكائنات
|
||||
description: أضِف حقولًا إلى كائنات Twenty القياسية (Person، Company، …) أو إلى كائنات من تطبيقات أخرى باستخدام defineField.
|
||||
icon: wand-magic-sparkles
|
||||
---
|
||||
|
||||
استخدم `defineField()` لإضافة حقل إلى كائن لا تملكه — كائن Twenty قياسي مثل Person أو Company، أو كائن يتم توفيره بواسطة تطبيق آخر مُثبَّت. على خلاف الحقول المضمّنة داخل [`defineObject`](/l/ar/developers/extend/apps/data/objects)، تتطلّب الحقول المستقلة `objectUniversalIdentifier` لتحديد الكائن الذي تقوم بتوسيعه.
|
||||
|
||||
```ts src/fields/company-loyalty-tier.field.ts
|
||||
import { defineField, FieldType } from 'twenty-sdk/define';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
|
||||
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
|
||||
name: 'loyaltyTier',
|
||||
type: FieldType.SELECT,
|
||||
label: 'Loyalty Tier',
|
||||
icon: 'IconStar',
|
||||
options: [
|
||||
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
|
||||
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
|
||||
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## النقاط الرئيسية
|
||||
|
||||
* `objectUniversalIdentifier` يحدّد الكائن الهدف. بالنسبة لكائنات Twenty القياسية، استورد الثابت من `twenty-sdk`:
|
||||
|
||||
```ts
|
||||
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
|
||||
|
||||
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier
|
||||
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier
|
||||
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier
|
||||
// …
|
||||
```
|
||||
|
||||
* عند تعريف الحقول بشكل مضمّن **داخل `defineObject()`**، **لا** تحتاج إلى `objectUniversalIdentifier` — إذ يُورَّث من الكائن الأب.
|
||||
|
||||
* `defineField()` هي الطريقة الوحيدة لإضافة حقول إلى كائنات لم تُنشئها باستخدام `defineObject()`.
|
||||
|
||||
* موقع الملف متروك لك. المتعارف عليه هو `src/fields/\<name>.field.ts`، لكن حزمة SDK تكتشف الحقول في أي مكان داخل `src/`.
|
||||
|
||||
* لإضافة علامة تبويب إلى تخطيط صفحة قياسي (مثل صفحة تفاصيل Task أو Company)، استخدم [`definePageLayoutTab`](/l/ar/developers/extend/apps/layout/page-layouts#definepagelayouttab) مع `STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS` من `twenty-sdk/define`.
|
||||
|
||||
## إضافة علاقة إلى كائن موجود
|
||||
|
||||
لإضافة حقل علاقة (مثل ربط الكائن المخصّص بكائن قياسي `Person`)، استخدم `defineField()` مع `FieldType.RELATION`. النمط هو نفسه الخاص بالعلاقات المضمّنة لكن مع تعيين `objectUniversalIdentifier` صراحةً. اطّلع على [Relations](/l/ar/developers/extend/apps/data/relations) للنمط ثنائي الاتجاه.
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: كائنات
|
||||
description: عرّف أنواعًا جديدة من السجلات — جداول مخصصة بحقولها الخاصة — باستخدام defineObject.
|
||||
icon: جدول
|
||||
---
|
||||
|
||||
تُعد **الكائنات** المخصصة أنواع سجلات جديدة يضيفها تطبيقك إلى مساحة العمل — مثل بطاقة بريدية، أو فاتورة، أو اشتراك، أو أي شيء خاص بالمجال الذي تعمل فيه. يعلن كل كائن عن مخططه (الحقول، والعلاقات، والقيم الافتراضية) ومعرّف عالمي ثابت يستمر عبر عمليات المزامنة والنشر.
|
||||
|
||||
```ts src/objects/post-card.object.ts
|
||||
import { defineObject, FieldType } from 'twenty-sdk/define';
|
||||
|
||||
enum PostCardStatus {
|
||||
DRAFT = 'DRAFT',
|
||||
SENT = 'SENT',
|
||||
DELIVERED = 'DELIVERED',
|
||||
RETURNED = 'RETURNED',
|
||||
}
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||||
nameSingular: 'postCard',
|
||||
namePlural: 'postCards',
|
||||
labelSingular: 'Post Card',
|
||||
labelPlural: 'Post Cards',
|
||||
description: 'A post card object',
|
||||
icon: 'IconMail',
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
|
||||
name: 'content',
|
||||
type: FieldType.TEXT,
|
||||
label: 'Content',
|
||||
description: "Postcard's content",
|
||||
icon: 'IconAbc',
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
|
||||
name: 'recipientName',
|
||||
type: FieldType.FULL_NAME,
|
||||
label: 'Recipient name',
|
||||
icon: 'IconUser',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
|
||||
name: 'recipientAddress',
|
||||
type: FieldType.ADDRESS,
|
||||
label: 'Recipient address',
|
||||
icon: 'IconHome',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
|
||||
name: 'status',
|
||||
type: FieldType.SELECT,
|
||||
label: 'Status',
|
||||
icon: 'IconSend',
|
||||
defaultValue: `'${PostCardStatus.DRAFT}'`,
|
||||
options: [
|
||||
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
|
||||
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
|
||||
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
|
||||
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
|
||||
name: 'deliveredAt',
|
||||
type: FieldType.DATE_TIME,
|
||||
label: 'Delivered at',
|
||||
icon: 'IconCheck',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## النقاط الرئيسية
|
||||
|
||||
* `universalIdentifier` يجب أن يكون فريدًا وثابتًا عبر عمليات النشر.
|
||||
* يتطلب كل حقل `name` و`type` و`label` ومعرّف `universalIdentifier` ثابتًا خاصًا به.
|
||||
* المصفوفة `fields` اختيارية — يمكنك تعريف كائنات بدون حقول مخصصة.
|
||||
* لا تحتاج الحقول المضمّنة المُعرَّفة هنا إلى `objectUniversalIdentifier` — إذ تُورَّث من الكائن الأب. استخدم [`defineField()`](/l/ar/developers/extend/apps/data/extending-objects) لإضافة حقول إلى كائنات لا تمتلكها.
|
||||
* يمكنك إنشاء كائنات جديدة باستخدام `yarn twenty dev:add object`، والذي يرشدك خلال التسمية والحقول والعلاقات. راجع [Architecture → Scaffolding entities](/l/ar/developers/extend/apps/getting-started/scaffolding).
|
||||
|
||||
<Note>
|
||||
**تُضاف الحقول الأساسية تلقائيًا.** عند تعريف كائن مخصص، ينشئ Twenty حقولًا قياسية مثل `id` و`name` و`createdAt` و`updatedAt` و`createdBy` و`updatedBy` و`deletedAt` من أجلك. لا تحتاج إلى تعريفها في مصفوفة `fields` — أضف فقط حقولك المخصصة. يمكنك تجاوز حقلًا افتراضيًا بتعريف حقل يحمل الاسم نفسه، لكن هذا نادرًا ما يكون فكرة جيدة.
|
||||
</Note>
|
||||
|
||||
## القيم الافتراضية
|
||||
|
||||
يجب تضمين القيم النصية الافتراضية بين علامات اقتباس أحادية **داخل** السلسلة — `defaultValue: "'Draft'"`، وليس `defaultValue: "Draft"`. لهذا السبب يستخدم الحقل `status` أعلاه `` `'${PostCardStatus.DRAFT}'` ``.
|
||||
|
||||
السلاسل غير المحاطة بعلامات اقتباس محجوزة للقيم الافتراضية المحسوبة، والتي يتم تقييمها عند إنشاء سجل:
|
||||
|
||||
* `'uuid'` — يُنشِئ UUID (لحقول `UUID`)
|
||||
* `'now'` — الطابع الزمني الحالي (لحقول `DATE_TIME`)
|
||||
|
||||
ينطبق نفس الاصطلاح على الحقول الفرعية النصية للقيم الافتراضية المركّبة (على سبيل المثال `{ source: "'MANUAL'" }` في حقل `ACTOR`) وكذلك على قيم `SELECT`/`MULTI_SELECT`. سيتسبّب ترك قيمة افتراضية نصية حرفية بدون علامات اقتباس في ظهور تحذير عند إنشاء التطبيق.
|
||||
|
||||
## ماذا بعد؟
|
||||
|
||||
* **اربط هذا الكائن بغيره من الكائنات** — راجع صفحة [Relations](/l/ar/developers/extend/apps/data/relations) لمعرفة نمط العلاقة ثنائية الاتجاه.
|
||||
* **أضف حقولًا إلى الكائنات التابعة لتطبيقات أخرى** — راجع [Extending Objects](/l/ar/developers/extend/apps/data/extending-objects) لمعرفة المزيد حول `defineField()`.
|
||||
* **اعرض هذا الكائن في واجهة المستخدم** — راجع [Views](/l/ar/developers/extend/apps/layout/views) و[Navigation Menu Items](/l/ar/developers/extend/apps/layout/navigation-menu-items) لإظهاره في الشريط الجانبي.
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: نظرة عامة
|
||||
description: شكّل البيانات التي يضيفها تطبيقك إلى مساحة العمل — الكائنات والحقول والعلاقات.
|
||||
icon: database
|
||||
---
|
||||
|
||||
طبقة **البيانات** في تطبيق Twenty هي البيانات التي *يضيفها* تطبيقك إلى مساحة العمل — أنواع السجلات الجديدة التي يصرّح عنها، والأعمدة التي يضيفها إلى الكائنات الموجودة، وكيف ترتبط هذه السجلات ببعضها البعض.
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ Object — a record type, e.g. PostCard │
|
||||
│ ├─ Field (name, type, label) │
|
||||
│ ├─ Field │
|
||||
│ └─ Relation (link to another object) │
|
||||
└──────────────────────────────────────────────────┘
|
||||
│
|
||||
├── lives in your app, OR
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ Standard / other apps' objects │
|
||||
│ └─ Field added by your app via defineField │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## في هذا القسم
|
||||
|
||||
<CardGroup cols={٢}>
|
||||
<Card title="كائنات" icon="جدول" href="/l/ar/developers/extend/apps/data/objects">
|
||||
`defineObject` — صرّح عن أنواع سجلات جديدة مع حقولها الخاصة.
|
||||
</Card>
|
||||
<Card title="توسيع الكائنات" icon="wand-magic-sparkles" href="/l/ar/developers/extend/apps/data/extending-objects">
|
||||
`defineField` — أضِف حقولًا إلى كائنات قياسية أو كائنات تطبيقات أخرى.
|
||||
</Card>
|
||||
<Card title="العلاقات" icon="diagram-project" href="/l/ar/developers/extend/apps/data/relations">
|
||||
اتصالات ثنائية الاتجاه من نوع `MANY_TO_ONE` / `ONE_TO_MANY` بين الكائنات.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## الكيانات بنظرة سريعة
|
||||
|
||||
| كيان | الغرض | مُعرَّفة باستخدام |
|
||||
| ---------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------- |
|
||||
| **الكائن** | نوع سجل مخصص جديد (مثل PostCard أو Invoice) مع حقوله الخاصة | `defineObject()` |
|
||||
| **الحقل** | عمود على كائن. يمكن للحقول المستقلة توسيع الكائنات التي لم تنشئها (مثل إضافة `loyaltyTier` إلى Company) | `defineField()` |
|
||||
| **علاقة** | ارتباط ثنائي الاتجاه بين كائنين — يُصرّح عن كلا الجانبين كحقول | `defineField()` مع `FieldType.RELATION` |
|
||||
| **الفهرس** | فهرس قاعدة بيانات لتسريع استعلام متكرر على أحد الكائنات لديك | `defineIndex()` |
|
||||
|
||||
يكتشف SDK هذه العناصر عبر تحليل AST أثناء وقت البناء، لذا تنظيم الملفات يعود إليك — القاعدة المتّبعة هي `src/objects/` و `src/fields/` و `src/indexes/`. معرّفات UUID ثابتة من نوع `universalIdentifier` تربط كل شيء معًا عبر عمليات النشر.
|
||||
|
||||
## الفهارس (اختياري)
|
||||
|
||||
يمكن للتطبيقات إرفاق الفهارس مع الكائنات الخاصة بها للحفاظ على سرعة الاستعلامات المتكررة. أكثر الحالات شيوعًا هي عمود حالة أو مفتاح أجنبي تقوم بقراءته بشكل متكرر.
|
||||
|
||||
```ts src/indexes/post-card-status.index.ts
|
||||
import { defineIndex } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from '../objects/post-card.object';
|
||||
|
||||
export default defineIndex({
|
||||
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
|
||||
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
|
||||
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### الفهارس الفريدة
|
||||
|
||||
تقبل `defineIndex` الخيار `isUnique: true` لكلٍّ من التفرد على عمود واحد أو على عدّة أعمدة. هذه هي البنية الموصى بها — إن `defineField({ isUnique: true })` مهملة وسيتم إزالتها في إصدار قادم.
|
||||
|
||||
```ts
|
||||
defineIndex({
|
||||
universalIdentifier: '…',
|
||||
objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER,
|
||||
isUnique: true,
|
||||
fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }],
|
||||
});
|
||||
```
|
||||
|
||||
### قيود أخرى
|
||||
|
||||
* تظل عبارات `WHERE` الجزئية تحت تحكم المشرف — لا يمكن للتطبيقات التصريح بها.
|
||||
* يتم تقييد كل كائن بعدد 10 فهارس مخصصة (لا تُحتسب فهارس الإطار نفسه).
|
||||
|
||||
رتّب مصفوفة `fields` بالطريقة التي ينبغي على Postgres استخدامها — العمود في أقصى اليسار أولًا، مثل دليل الهاتف. الفهارس ليست مجانية: كل عملية كتابة في الجدول تقوم بتحديثها. أضِف واحدًا فقط عندما يكون لديك استعلام يحتاج إليه.
|
||||
|
||||
<Note>
|
||||
هل تبحث عن **Application Config** أو **Roles & Permissions**؟ هذه تصف التطبيق نفسه بدلًا من البيانات التي يضيفها — وتوجد ضمن [Config](/l/ar/developers/extend/apps/config/overview). هل تبحث عن **Connections** (Linear, GitHub, Slack OAuth)؟ هذه وُجِدت ليتم استدعاؤها *من* دوال المنطق وتوجد ضمن [Logic](/l/ar/developers/extend/apps/logic/connections).
|
||||
</Note>
|
||||
@@ -0,0 +1,160 @@
|
||||
---
|
||||
title: العلاقات
|
||||
description: وصِل الكائنات معًا بعلاقات MANY_TO_ONE / ONE_TO_MANY ثنائية الاتجاه.
|
||||
icon: diagram-project
|
||||
---
|
||||
|
||||
تربط العلاقات كائنين معًا. في Twenty، تكون العلاقات دائمًا **ثنائية الاتجاه** — لكل علاقة جانبَان، ويُصرَّح عن كل جانب كحقل يُشير إلى الآخر.
|
||||
|
||||
| نوع العلاقة | الوصف | هل لديه مفتاح خارجي؟ |
|
||||
| ------------- | ------------------------------------------------------ | ---------------------- |
|
||||
| `MANY_TO_ONE` | تشير العديد من سجلات هذا الكائن إلى سجل واحد من الهدف | نعم (`joinColumnName`) |
|
||||
| `ONE_TO_MANY` | يحتوي سجل واحد من هذا الكائن على العديد من سجلات الهدف | لا (الجانب العكسي) |
|
||||
|
||||
## كيف تعمل العلاقات
|
||||
|
||||
تتطلّب كل علاقة **حقلين** يشيران إلى بعضهما البعض:
|
||||
|
||||
1. جانب **MANY_TO_ONE** — يوجد على الكائن الذي يحمل المفتاح الخارجي.
|
||||
2. جانب **ONE_TO_MANY** — يوجد على الكائن الذي يملك المجموعة.
|
||||
|
||||
يستخدم كلا الحقلين `FieldType.RELATION` ويُحيل كلٌ منهما إلى الآخر عبر `relationTargetFieldMetadataUniversalIdentifier`.
|
||||
|
||||
## مثال: البطاقة البريدية لديها العديد من المستلمين
|
||||
|
||||
يمكن إرسال `PostCard` إلى العديد من سجلات `PostCardRecipient`. ينتمي كل مستلم إلى بطاقة بريدية واحدة بالضبط.
|
||||
|
||||
**الخطوة 1: عرّف جانب ONE_TO_MANY على PostCard** (جانب "الواحد"):
|
||||
|
||||
```ts src/fields/post-card-recipients-on-post-card.field.ts
|
||||
import { defineField, FieldType, RelationType } from 'twenty-sdk/define';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
|
||||
|
||||
// Export so the other side can reference it
|
||||
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
|
||||
// Import from the other side
|
||||
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
|
||||
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'postCardRecipients',
|
||||
label: 'Post Card Recipients',
|
||||
icon: 'IconUsers',
|
||||
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**الخطوة 2: عرّف جانب MANY_TO_ONE على PostCardRecipient** (جانب "العديد" — يحمل المفتاح الخارجي):
|
||||
|
||||
```ts src/fields/post-card-on-post-card-recipient.field.ts
|
||||
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
|
||||
|
||||
// Export so the other side can reference it
|
||||
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
|
||||
// Import from the other side
|
||||
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: POST_CARD_FIELD_ID,
|
||||
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'postCard',
|
||||
label: 'Post Card',
|
||||
icon: 'IconMail',
|
||||
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.CASCADE,
|
||||
joinColumnName: 'postCardId',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
**الاستيرادات الدائرية:** كلا حقلي العلاقة يُشير كلٌّ منهما إلى `universalIdentifier` الخاص بالآخر. لتجنّب مشكلات الاستيراد الدائري، صدِّر معرّفات الحقول كثوابت مسمّاة من كل ملف، واستورِدها في الملف الآخر. يقوم نظام البناء بحلّها في وقت التجميع.
|
||||
</Note>
|
||||
|
||||
## الربط مع الكائنات القياسية
|
||||
|
||||
لإنشاء علاقة مع كائن Twenty مضمّن (Person, Company, etc.)، استخدم `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`:
|
||||
|
||||
```ts src/fields/person-on-self-hosting-user.field.ts
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
RelationType,
|
||||
OnDeleteAction,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk/define';
|
||||
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
|
||||
|
||||
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
|
||||
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PERSON_FIELD_ID,
|
||||
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'person',
|
||||
label: 'Person',
|
||||
description: 'Person matching with the self hosting user',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'personId',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## خصائص حقل العلاقة
|
||||
|
||||
| الخاصية | مطلوب | الوصف |
|
||||
| ------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------- |
|
||||
| `type` | نعم | يجب أن يكون `FieldType.RELATION` |
|
||||
| `relationTargetObjectMetadataUniversalIdentifier` | نعم | قيمة `universalIdentifier` للكائن الهدف |
|
||||
| `relationTargetFieldMetadataUniversalIdentifier` | نعم | قيمة `universalIdentifier` للحقل المطابق على الكائن الهدف |
|
||||
| `universalSettings.relationType` | نعم | `RelationType.MANY_TO_ONE` أو `RelationType.ONE_TO_MANY` |
|
||||
| `universalSettings.onDelete` | MANY_TO_ONE فقط | ماذا يحدث عند حذف السجل المشار إليه: `CASCADE`، `SET_NULL`، `RESTRICT`، أو `NO_ACTION` |
|
||||
| `universalSettings.joinColumnName` | MANY_TO_ONE فقط | اسم عمود قاعدة البيانات للمفتاح الخارجي (مثل `postCardId`) |
|
||||
|
||||
## حقول العلاقات المضمَّنة
|
||||
|
||||
يمكنك أيضًا إعلان علاقة مباشرة داخل [`defineObject`](/l/ar/developers/extend/apps/data/objects). عند كونها مضمَّنة، احذِف `objectUniversalIdentifier` — إذ يُورَّث من الكائن الأب:
|
||||
|
||||
```ts
|
||||
export default defineObject({
|
||||
universalIdentifier: '...',
|
||||
nameSingular: 'postCardRecipient',
|
||||
// ...
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: POST_CARD_FIELD_ID,
|
||||
type: FieldType.RELATION,
|
||||
name: 'postCard',
|
||||
label: 'Post Card',
|
||||
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.CASCADE,
|
||||
joinColumnName: 'postCardId',
|
||||
},
|
||||
},
|
||||
// … other fields
|
||||
],
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: المفاهيم
|
||||
description: كيفية عمل تطبيقات Twenty — نموذج الكيان، العزل (sandboxing)، ودورة حياة التثبيت.
|
||||
icon: sitemap
|
||||
---
|
||||
|
||||
تطبيقات Twenty هي حزم TypeScript توسّع مساحة عملك بكائنات مخصّصة، ومنطق، ومكوّنات واجهة مستخدم (UI)، وقدرات ذكاء اصطناعي. تعمل على منصة Twenty مع عزل كامل وضوابط الأذونات.
|
||||
|
||||
## كيف تعمل التطبيقات
|
||||
|
||||
التطبيق عبارة عن مجموعة من **الكيانات** يتم إعلانها باستخدام دوال `defineEntity()` من حزمة `twenty-sdk`. يكتشف SDK هذه التصريحات عبر تحليل AST وقت البناء وينتج **ملف بيان** — وصفًا كاملًا لما يضيفه تطبيقك إلى مساحة العمل. تتحقق هذه الدوال من تكوينك وقت البناء وتوفّر إكمالًا تلقائيًا في بيئة التطوير وأمان الأنواع.
|
||||
|
||||
```
|
||||
your-app/
|
||||
├── src/
|
||||
│ ├── application-config.ts ← defineApplication (required, one per app)
|
||||
│ ├── roles/ ← defineRole
|
||||
│ ├── objects/ ← defineObject
|
||||
│ ├── fields/ ← defineField
|
||||
│ ├── logic-functions/ ← defineLogicFunction
|
||||
│ ├── front-components/ ← defineFrontComponent
|
||||
│ ├── skills/ ← defineSkill
|
||||
│ ├── agents/ ← defineAgent
|
||||
│ ├── views/ ← defineView
|
||||
│ ├── navigation-menu-items/ ← defineNavigationMenuItem
|
||||
│ └── page-layouts/ ← definePageLayout
|
||||
├── public/ ← Static assets (images, icons)
|
||||
└── package.json
|
||||
```
|
||||
|
||||
<Note>
|
||||
**تنظيم الملفات متروك لك.** يعتمد اكتشاف الكيانات على AST — يعثر SDK على استدعاءات `export default defineEntity(...)` بغض النظر عن مكان وجود الملف. بنية المجلدات أعلاه هي اصطلاح وليست متطلبًا.
|
||||
</Note>
|
||||
|
||||
## أنواع الكيانات
|
||||
|
||||
| كيان | الغرض | وثائق |
|
||||
| ---------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| **تطبيق** | هوية التطبيق، الدور الافتراضي، والمتغيرات | [تهيئة التطبيق](/l/ar/developers/extend/apps/config/application) |
|
||||
| **دور** | مجموعات الأذونات للكائنات والحقول | [الأدوار والأذونات](/l/ar/developers/extend/apps/config/roles) |
|
||||
| **الكائن** | أنواع سجلات مخصّصة مع حقول | [الكائنات](/l/ar/developers/extend/apps/data/objects) |
|
||||
| **الحقل** | إضافة حقول إلى الكائنات من تطبيقات أخرى | [توسيع الكائنات](/l/ar/developers/extend/apps/data/extending-objects) |
|
||||
| **علاقة** | روابط ثنائية الاتجاه بين الكائنات | [العلاقات](/l/ar/developers/extend/apps/data/relations) |
|
||||
| **دالة منطقية** | TypeScript على جانب الخادم مع مشغّلات | [الوظائف المنطقية](/l/ar/developers/extend/apps/logic/logic-functions) |
|
||||
| **مهارة** | تعليمات قابلة لإعادة الاستخدام لوكلاء الذكاء الاصطناعي | [المهارات والوكلاء](/l/ar/developers/extend/apps/logic/skills-and-agents) |
|
||||
| **وكيل** | مساعدو الذكاء الاصطناعي بموجهات مخصّصة | [المهارات والوكلاء](/l/ar/developers/extend/apps/logic/skills-and-agents) |
|
||||
| **موفر الاتصال** | بيانات اعتماد OAuth لواجهات برمجة التطبيقات التابعة لجهات خارجية | [الاتصالات](/l/ar/developers/extend/apps/logic/connections) |
|
||||
| **عرض** | عروض قوائم السجلات المكوّنة مسبقًا | [العروض](/l/ar/developers/extend/apps/layout/views) |
|
||||
| **عنصر قائمة التنقّل** | عناصر الشريط الجانبي المخصّصة | [عناصر قائمة التنقّل](/l/ar/developers/extend/apps/layout/navigation-menu-items) |
|
||||
| **تخطيط الصفحة** | علامات التبويب وعناصر الواجهة في صفحة تفاصيل السجل | [تخطيطات الصفحات](/l/ar/developers/extend/apps/layout/page-layouts) |
|
||||
| **مكوّن أمامي** | واجهة مستخدم React معزولة داخل Twenty | [المكوّنات الأمامية](/l/ar/developers/extend/apps/layout/front-components) |
|
||||
| **عنصر قائمة الأوامر** | إجراءات سريعة ومدخلات Cmd+K | [عناصر قائمة الأوامر](/l/ar/developers/extend/apps/layout/command-menu-items) |
|
||||
|
||||
## العزل
|
||||
|
||||
* **الدوال المنطقية** تعمل في عمليات Node.js معزولة على الخادم. لا تصل إلى البيانات إلا عبر عميل API مضبوط الأنواع، ومقيَّد بأذونات دور التطبيق.
|
||||
* **المكوّنات الأمامية** تعمل ضمن Web Workers باستخدام Remote DOM — معزولة عن الصفحة الرئيسية لكنها تعرض عناصر DOM الأصلية (وليس iframes). تتواصل مع Twenty عبر واجهة API للمضيف تعتمد تمرير الرسائل.
|
||||
* **الأذونات** تُطبَّق على مستوى واجهة API. يُشتق رمز وقت التشغيل (`TWENTY_APP_ACCESS_TOKEN`) من الدور المعرَّف في `defineApplication()`.
|
||||
|
||||
## دورة حياة التطبيق
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Development │
|
||||
│ npx create-twenty-app → yarn twenty dev (live sync) │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Build & Deploy │
|
||||
│ yarn twenty dev:build → yarn twenty app:publish │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Install flow │
|
||||
│ upload → [pre-install] → metadata migration → │
|
||||
│ generate SDK → [post-install] │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Publish │
|
||||
│ npm publish → appears in Twenty marketplace │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
* **`yarn twenty dev`** — يراقب ملفات المصدر لديك ويزامن التغييرات مباشرةً إلى خادم Twenty متصل. يُعاد توليد عميل API مضبوط الأنواع تلقائيًا عند تغيّر المخطط.
|
||||
* **`yarn twenty dev:build`** — يجمّع TypeScript، ويضمّن الدوال المنطقية والمكوّنات الأمامية باستخدام esbuild، وينتج ملف بيان.
|
||||
* **خطّافات ما قبل/ما بعد التثبيت** — دوال اختيارية تعمل أثناء التثبيت. راجع [خطّافات التثبيت](/l/ar/developers/extend/apps/config/install-hooks) للتفاصيل.
|
||||
|
||||
## الخطوات التالية
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="التهيئة" icon="screwdriver-wrench" href="/l/ar/developers/extend/apps/config/overview">
|
||||
هوية التطبيق، الدور الافتراضي، وخطّافات التثبيت.
|
||||
</Card>
|
||||
<Card title="بيانات" icon="database" href="/l/ar/developers/extend/apps/data/overview">
|
||||
الكائنات، الحقول، والعلاقات ثنائية الاتجاه.
|
||||
</Card>
|
||||
<Card title="المنطق" icon="bolt" href="/l/ar/developers/extend/apps/logic/overview">
|
||||
دوال منطقية، مهارات، وكلاء، واتصالات OAuth.
|
||||
</Card>
|
||||
<Card title="التخطيط" icon="table-columns" href="/l/ar/developers/extend/apps/layout/overview">
|
||||
العروض، التنقّل، تخطيطات الصفحات، ومكوّنات الواجهة الأمامية.
|
||||
</Card>
|
||||
<Card title="العمليات" icon="rocket" href="/l/ar/developers/extend/apps/operations/overview">
|
||||
سطر الأوامر (CLI)، الاختبار، المستودعات البعيدة (remotes)، التكامل المستمر (CI)، ونشر تطبيقك.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: الخادم المحلي
|
||||
description: إدارة خادم Twenty المحلي المستند إلى Docker — بدء التشغيل، الإيقاف، الترقية، مثيل اختبار متوازٍ، وإعداد SDK اليدوي.
|
||||
icon: server
|
||||
---
|
||||
|
||||
## إدارة الخادم المحلي
|
||||
|
||||
استخدم `yarn twenty docker:*` للتحكّم في حاوية Twenty المحلية:
|
||||
|
||||
| أمر | ماذا يفعل |
|
||||
| -------------------------------------- | -------------------------------------------------- |
|
||||
| `yarn twenty docker:start` | بدء تشغيل الخادم (يسحب الصورة إذا لزم الأمر) |
|
||||
| `yarn twenty docker:start 2.2.0` | بدء إصدار محدد من الخادم |
|
||||
| `yarn twenty docker:start --port 3030` | بدء التشغيل على منفذ مخصّص |
|
||||
| `yarn twenty docker:stop` | إيقاف الخادم (مع الحفاظ على البيانات) |
|
||||
| `yarn twenty docker:status` | عرض عنوان URL والإصدار وبيانات اعتماد تسجيل الدخول |
|
||||
| `yarn twenty docker:logs` | بث سجلات الخادم |
|
||||
| `yarn twenty docker:reset` | مسح البيانات والبدء من جديد |
|
||||
| `yarn twenty docker:upgrade` | سحب أحدث صورة `twenty-app-dev` |
|
||||
| `yarn twenty docker:upgrade 2.2.0` | الترقية إلى إصدار محدد |
|
||||
|
||||
تظل البيانات محفوظة عبر عمليات إعادة التشغيل في وحدتي تخزين Docker (`twenty-app-dev-data` لـ PostgreSQL، و`twenty-app-dev-storage` للملفات). استخدم `reset` لمسح كل شيء.
|
||||
|
||||
## تثبيت إصدار الخادم
|
||||
|
||||
عند عدم تمرير أي إصدار، يقوم `docker:start` باشتقاق الإصدار من النطاق `engines.twenty` في ملف `package.json` لتطبيقك — وهو نفس النطاق الذي يتحقق منه الخادم عند تثبيت تطبيقك. يشغّل أحدث صورة منشورة لـ `twenty-app-dev` التي تُلبي هذا النطاق، مع الرجوع إلى `latest` عندما يكون الحقل غير موجودًا أو لا يتطابق أي إصدار منشور:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"engines": {
|
||||
"twenty": ">=2.2.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
مرِّر إصدارًا بشكلٍ صريح لتجاوز النطاق لتشغيلٍ واحد فقط: `yarn twenty docker:start 2.3.0`. إذا كانت هناك حاوية موجودة بالفعل على إصدار مختلف، يقوم `docker:start` بترقيتها في مكانها (مع إعادة إنشاء الحاوية مع الحفاظ على وحدات تخزين بياناتك).
|
||||
|
||||
## ترقية صورة الخادم
|
||||
|
||||
يقوم `yarn twenty docker:upgrade` بسحب أحدث صورة، ومقارنة التجزئات، ولا يعيد إنشاء الحاوية إلا إذا حدث تغيير فعلي. تظل وحدات التخزين محفوظة — ويتم استبدال الحاوية فقط. إذا تم سحب صورة جديدة وكانت الحاوية تعمل، فستبدأ عملية الترقية تلقائيًا حاوية جديدة؛ شغّل بعد ذلك `yarn twenty docker:start` للانتظار حتى تصبح سليمة.
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty docker:upgrade # Latest
|
||||
yarn twenty docker:upgrade 2.2.0 # Specific version
|
||||
```
|
||||
|
||||
تحقّق من الإصدار الجاري باستخدام `yarn twenty docker:status` (يعرض قيمة `APP_VERSION` المضمنة في الحاوية).
|
||||
|
||||
## تشغيل مثيل اختبار متوازٍ
|
||||
|
||||
مرّر `--test` إلى أي أمر `docker:*` لإدارة مثيل ثانٍ معزول تمامًا — مفيد لاختبارات التكامل أو للتجربة من دون لمس بيانات التطوير الرئيسية لديك:
|
||||
|
||||
| أمر | ماذا يفعل |
|
||||
| ----------------------------------- | ----------------------------------------- |
|
||||
| `yarn twenty docker:start --test` | بدء مثيل الاختبار (المنفذ الافتراضي 2021) |
|
||||
| `yarn twenty docker:stop --test` | إيقافه |
|
||||
| `yarn twenty docker:status --test` | عرض حالته |
|
||||
| `yarn twenty docker:logs --test` | بث سجلاته |
|
||||
| `yarn twenty docker:reset --test` | مسح بياناته |
|
||||
| `yarn twenty docker:upgrade --test` | ترقية صورته |
|
||||
|
||||
يملك مثيل الاختبار حاويته الخاصة (`twenty-app-dev-test`) ووحدات التخزين الخاصة به (`twenty-app-dev-test-data`، `twenty-app-dev-test-storage`) وتهيئته — ويعمل جنبًا إلى جنب مع مثيلك الرئيسي بدون تعارضات. اجمع `--test` مع `--port` لتجاوز المنفذ 2021.
|
||||
|
||||
## إعداد يدوي (بدون أداة توليد الهيكل)
|
||||
|
||||
تجاوز أداة توليد الهيكل إذا كنت تضيف SDK إلى مشروع قائم:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add twenty-sdk twenty-client-sdk
|
||||
```
|
||||
|
||||
أضِف النص البرمجي إلى `package.json`:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"scripts": {
|
||||
"twenty": "twenty"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
يمكنك الآن تشغيل `yarn twenty dev`، و`yarn twenty docker:start`، والبقية.
|
||||
|
||||
<Note>
|
||||
لا تثبّت `twenty-sdk` عالميًا — ثبِّته لكل مشروع بحيث يستخدم كل تطبيق إصداره الخاص.
|
||||
</Note>
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: هيكل المشروع
|
||||
description: ما الذي يوجد داخل تطبيق Twenty المُهيكل مسبقًا — الملفات والمجلدات، وما الذي يفعله كلٌّ منها.
|
||||
icon: folder-tree
|
||||
---
|
||||
|
||||
يبدو التطبيق الجديد الذي يتم إنشاؤه بواسطة `npx create-twenty-app` كما يلي:
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
package.json
|
||||
src/
|
||||
application-config.ts # Required — your app's entry point
|
||||
default-role.ts # Permissions for logic functions
|
||||
constants/
|
||||
universal-identifiers.ts # Auto-generated UUIDs and metadata
|
||||
__tests__/
|
||||
setup-test.ts
|
||||
app-install.integration-test.ts
|
||||
.github/workflows/ci.yml # GitHub Actions
|
||||
public/ # Static assets
|
||||
vitest.config.ts # Test runner config
|
||||
tsconfig.json, tsconfig.spec.json
|
||||
.nvmrc, .yarnrc.yml, .oxlintrc.json
|
||||
README.md, LLMS.md
|
||||
```
|
||||
|
||||
## الملفات الرئيسية
|
||||
|
||||
| ملف / مجلد | الغرض |
|
||||
| ---------------------------------------- | ------------------------------------------------------------------- |
|
||||
| `src/application-config.ts` | **مطلوب.** ملف الإعداد الرئيسي لتطبيقك. |
|
||||
| `src/default-role.ts` | دور افتراضي يتحكّم بما يمكن لدوال المنطق الوصول إليه. |
|
||||
| `src/constants/universal-identifiers.ts` | معرّفات UUID وبيانات تعريف يتم توليدها تلقائيًا (اسم العرض، الوصف). |
|
||||
| `src/__tests__/` | اختبارات تكامل (إعداد + اختبار مثال). |
|
||||
| `public/` | أصول ثابتة (صور، خطوط) تُقدَّم مع تطبيقك. |
|
||||
|
||||
<Note>
|
||||
**تنظيم الملفات متروك لك.** المجلدات المذكورة أعلاه هي أعراف متَّبعة — يكتشف SDK الكيانات عبر تحليل AST على استدعاءات `export default defineEntity(...)` بغض النظر عن مكان وجود الملف.
|
||||
</Note>
|
||||
|
||||
## التبعيات
|
||||
|
||||
ينبغي أن تكون حزمتا SDK الخاصتان بـ Twenty ضمن `devDependencies`، وليس ضمن `dependencies`:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"twenty-client-sdk": "^2.13.0",
|
||||
"twenty-sdk": "^2.13.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* توفّر **`twenty-sdk`** أداة `twenty` CLI وأدوات البناء/إنشاء الهياكل (scaffolding). يعمل فقط أثناء التطوير ووقت البناء، ولا يتم استيراده أبدًا في وقت تشغيل تطبيقك المنشور.
|
||||
* يتم استيراد **`twenty-client-sdk`** بواسطة كود تطبيقك (`CoreApiClient`، `MetadataApiClient`، `RestApiClient`)؛ لكن Twenty توفّره في وقت التشغيل — حيث تحصل عليه دوال المنطق من طبقة SDK مُولَّدة، وتحصل عليه مكوّنات الواجهة من وحدات يتم تقديمها من الخادم. يُستخدَم الإصدار المثبّت لديك فقط لفحص الأنواع (typechecking) ولبناء النشر (deploy-time build)، لذا لا يلزم أبدًا أن يتم تضمينه في حزمة النشر.
|
||||
|
||||
الاحتفاظ بأي من الحزمتين ضمن `dependencies` يؤدي إلى سحبها داخل حزمة وقت تشغيل التطبيق المثبّت، حيث تكون عبئًا زائدًا بلا فائدة. يُطلق `twenty build` تحذيرًا عندما تكون أيٌّ منهما ما تزال مدرجة ضمن `dependencies`.
|
||||
|
||||
أضِف تبعيات وقت التشغيل الخاصة بتطبيقك (المكتبات التي تستوردها دوال المنطق لديك فعلًا في وقت التشغيل) ضمن `dependencies` كالمعتاد.
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
title: البدء السريع
|
||||
icon: rocket
|
||||
description: أنشئ أول تطبيق Twenty خلال دقائق.
|
||||
---
|
||||
|
||||
## المتطلبات الأساسية
|
||||
|
||||
* **Node.js 24+** — [تنزيل](https://nodejs.org/)
|
||||
* **Yarn 4** — يأتي مع Node.js عبر Corepack. قم بتمكينه: `corepack enable`
|
||||
* **Docker** — [تنزيل](https://www.docker.com/products/docker-desktop/). مطلوب لتشغيل خادم Twenty محليًا. تخطَّ ذلك إذا كان لديك Twenty يعمل في مكان آخر.
|
||||
|
||||
يتكوّن إنشاء تطبيق Twenty من ثلاث مراحل. تقوم أداة توليد الهيكل بدمجها في أمر واحد لمسار الاستخدام المثالي، لكن كل مرحلة تمثّل مفهومًا منفصلًا — وعند حدوث فشل، فإن معرفة المرحلة التي أنت فيها تُخبرك بما ينبغي إصلاحه.
|
||||
|
||||
| المرحلة | ماذا تفعل | الأداة | النتيجة |
|
||||
| ------------------- | ---------------------------------- | ----------------------------- | ------------------------------- |
|
||||
| **1. تهيئة الهيكل** | توليد الشفرة المصدرية للتطبيق | `npx create-twenty-app` | مشروع TypeScript على القرص |
|
||||
| **2. تشغيل خادم** | بدء تشغيل خادم Twenty للمزامنة معه | Docker + `yarn twenty server` | مثيل Twenty قيد التشغيل |
|
||||
| **3. مزامنة** | قم بمزامنة شفرتك مباشرةً مع الخادم | `yarn twenty dev` | تظهر تغييراتك في واجهة المستخدم |
|
||||
|
||||
---
|
||||
|
||||
## المرحلة 1 — تهيئة هيكل مشروعك
|
||||
|
||||
أنشئ تطبيقًا جديدًا من القالب:
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
```
|
||||
|
||||
ستتم مطالبتك باسم ووصف — اضغط **Enter** للقيم الافتراضية. يُنشئ هذا مشروع TypeScript في `my-twenty-app/` يتضمن ملف بداية `application-config.ts`، ودورًا افتراضيًا، وسير عمل CI، واختبار تكامل.
|
||||
|
||||
**بعد هذه المرحلة:** سيكون لديك الشفرة المصدرية لتطبيق على جهازك. ليس قيد التشغيل بعد — وهذه هي المرحلة 2.
|
||||
|
||||
---
|
||||
|
||||
## المرحلة 2 — تشغيل خادم Twenty محلي
|
||||
|
||||
يحتاج تطبيقك إلى خادم Twenty للمزامنة معه. الخادم هو مثيل Twenty كامل — واجهة مستخدم، واجهة برمجة تطبيقات GraphQL، PostgreSQL — يعمل محليًا داخل Docker. ترفع شفرتك المحلية تعريفاتها إلى ذلك الخادم، مما يجعلها تظهر في واجهة المستخدم.
|
||||
|
||||
تقترح أداة توليد الهيكل تشغيل خادم لك:
|
||||
|
||||
> **هل ترغب في إعداد مثيل محلي من Twenty؟**
|
||||
|
||||
* **نعم (موصى به)** — ستسحب صورة Docker `twentycrm/twenty-app-dev` وتبدأ تشغيلها على المنفذ `2020`. تأكّد أولًا من أن Docker قيد التشغيل.
|
||||
* **لا** — اختر هذا إذا كان لديك بالفعل خادم Twenty تريد الاتصال به. يمكنك ربطه لاحقًا باستخدام `yarn twenty remote:add`.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="هل يجب بدء المثيل المحلي؟" />
|
||||
</div>
|
||||
|
||||
بمجرد أن يصبح الخادم جاهزًا، سيفتح المتصفح لإجراء تسجيل الدخول. استخدم حساب العرض التوضيحي المُجهَّز مسبقًا:
|
||||
|
||||
* **البريد الإلكتروني:** `tim@apple.dev`
|
||||
* **كلمة المرور:** `tim@apple.dev`
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/login.png" alt="شاشة تسجيل الدخول إلى Twenty" />
|
||||
</div>
|
||||
|
||||
انقر **Authorize** في الشاشة التالية — يمنح هذا واجهة سطر الأوامر CLI حق الوصول إلى مساحة العمل الخاصة بك.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/authorize.png" alt="شاشة تفويض واجهة الأوامر (CLI) الخاصة بـ Twenty" />
|
||||
</div>
|
||||
|
||||
ستؤكّد الطرفية أن كل شيء قد تم إعداده.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="تم إنشاء هيكل التطبيق بنجاح" />
|
||||
</div>
|
||||
|
||||
**بعد هذه المرحلة:** لديك خادم Twenty قيد التشغيل على [http://localhost:2020](http://localhost:2020)، مع تفويض CLI لديك للمزامنة معه.
|
||||
|
||||
<Note>
|
||||
إذا لم يكن Docker مثبتًا أو قيد التشغيل، فستخبرك أداة توليد الهيكل بأمر البدء المناسب لنظام التشغيل لديك. عند تشغيل Docker، يمكنك المتابعة باستخدام `yarn twenty docker:start` — لا حاجة لإعادة إنشاء الهيكل.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## المرحلة 3 — مزامنة تغييراتك
|
||||
|
||||
هذه هي الحلقة الداخلية التي ستقضي معظم وقتك فيها.
|
||||
|
||||
```bash filename="Terminal"
|
||||
cd my-twenty-app
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
يراقب هذا المجلد `src/`، ويُعيد البناء عند كل تغيير، ويزامن الناتج إلى الخادم. حرّر ملفًا، واحفظه، وخلال بضع ثوانٍ سينعكس التغيير على الخادم. سترى لوحة حالة مباشرة في الطرفية.
|
||||
|
||||
للحصول على مخرجات أكثر تفصيلاً (سجلات البناء، طلبات المزامنة، تتبعات الأخطاء)، أضِف `--verbose`.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/dev.png" alt="مخرجات الطرفية في وضع التطوير" />
|
||||
</div>
|
||||
|
||||
افتح [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). يفترض أن ترى تطبيقك ضمن **Your Apps**.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="قائمة "Your Apps" تعرض "My twenty app"" />
|
||||
</div>
|
||||
|
||||
انقر **My twenty app** لعرض **تسجيل التطبيق** — وهو سجل على مستوى الخادم يصف تطبيقك (الاسم، المعرّف، بيانات اعتماد OAuth، المصدر). يمكن تثبيت تسجيل واحد عبر عدة مساحات عمل على الخادم نفسه.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="تفاصيل تسجيل التطبيق" />
|
||||
</div>
|
||||
|
||||
انقر **View installed app** لعرض التثبيت في مساحة العمل. تعرض علامة التبويب **About** الإصدار وخيارات الإدارة.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="التطبيق المثبت" />
|
||||
</div>
|
||||
|
||||
**بعد هذه المرحلة:** لديك دورة تطوير حيّة. حرّر أي ملف في `src/` وسيظهر في واجهة المستخدم.
|
||||
|
||||
### مزامنة لمرة واحدة لـ CI والبرامج النصية
|
||||
|
||||
مرّر `--once` لتشغيل عملية بناء واحدة + مزامنة واحدة ثم الخروج — نفس خط الأنابيب، من دون مراقِب:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --once
|
||||
```
|
||||
|
||||
| أمر | السلوك | متى يُستخدم |
|
||||
| ---------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| `yarn twenty dev` | يراقب ويعيد المزامنة عند كل تغيير. يستمر في العمل حتى توقفه. | تطوير محلي تفاعلي. |
|
||||
| `yarn twenty dev --once` | بناء واحد + مزامنة واحدة، يخرج برمز `0` عند النجاح، و`1` عند الفشل. | CI، وخطافات ما قبل الالتزام، ووكلاء الذكاء الاصطناعي، وسير عمل مكتوب بنصوص. |
|
||||
| `yarn twenty dev --once --dry-run` | يبني تغييرات البيانات الوصفية ويطبعها **من دون تطبيقها**. | فحص ما الذي سيُغيِّره التزامن قبل تطبيقه. |
|
||||
|
||||
كلا الوضعين يحتاجان إلى جهة بعيدة موثَّقة. راجع قسم [المزامنة والاستعادة](/l/ar/developers/extend/apps/operations/sync-and-recovery#previewing-changes-dry-run) للحصول على المزيد من المعلومات حول `--dry-run`.
|
||||
|
||||
### خيارات وضع التطوير
|
||||
|
||||
| خيار | الوصف |
|
||||
| ------------------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| `--once` | قم بالإنشاء والمزامنة مرة واحدة، ثم اخرج. |
|
||||
| `--dry-run` | باستخدام `--once`، يمكنك معاينة تغييرات البيانات الوصفية دون تطبيقها. لا يكتب أي شيء. |
|
||||
| `--debounceMs \<ms>` | اضبط مهلة إزالة الارتداد لتغييرات الملفات بالميلي ثانية (القيمة الافتراضية: `2000`). |
|
||||
| `--verbose` / `--debug` | إظهار سجلات إنشاء تفصيلية، وطلبات المزامنة، وتتبع الأخطاء. |
|
||||
|
||||
## ما الذي يمكنك بناؤه
|
||||
|
||||
تتكون التطبيقات من **كيانات** — يُعرَّف كل منها كملف TypeScript يحتوي على `export default` واحد:
|
||||
|
||||
| كيان | ماذا يفعل |
|
||||
| ---------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| **الكائنات والحقول** | نماذج بيانات مخصّصة (بطاقة بريدية، فاتورة، إلخ) بحقول ذات أنواع محددة |
|
||||
| **الوظائف المنطقية** | TypeScript على جانب الخادم يتم تشغيله عبر مسارات HTTP، أو جداول cron، أو أحداث قاعدة البيانات |
|
||||
| **المكوّنات الأمامية** | مكوّنات React تُعرَض داخل واجهة مستخدم Twenty (اللوحة الجانبية، الودجات، قائمة الأوامر) |
|
||||
| **المهارات والوكلاء** | قدرات الذكاء الاصطناعي — تعليمات قابلة لإعادة الاستخدام ومساعدون مستقلون ذاتيًا |
|
||||
| **طرق العرض والتنقّل** | طرق عرض قوائم مُعدّة مسبقًا وعناصر قائمة الشريط الجانبي |
|
||||
| **تخطيطات الصفحات** | صفحات تفاصيل سجلات مخصصة تتضمن علامات تبويب وعناصر واجهة |
|
||||
|
||||
مرجع كامل: [المفاهيم](/l/ar/developers/extend/apps/getting-started/concepts).
|
||||
|
||||
## الخطوات التالية
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="التهيئة" icon="screwdriver-wrench" href="/l/ar/developers/extend/apps/config/overview">
|
||||
هوية التطبيق، الدور الافتراضي، وخطّافات التثبيت، والأصول العامة.
|
||||
</Card>
|
||||
<Card title="بيانات" icon="database" href="/l/ar/developers/extend/apps/data/overview">
|
||||
الكائنات، الحقول، والعلاقات ثنائية الاتجاه.
|
||||
</Card>
|
||||
<Card title="المنطق" icon="bolt" href="/l/ar/developers/extend/apps/logic/overview">
|
||||
الوظائف المنطقية، المهارات، الوكلاء، واتصالات OAuth.
|
||||
</Card>
|
||||
<Card title="التخطيط" icon="table-columns" href="/l/ar/developers/extend/apps/layout/overview">
|
||||
العروض، التنقل، تخطيطات الصفحات، ومكوّنات الواجهة الأمامية.
|
||||
</Card>
|
||||
<Card title="العمليات" icon="rocket" href="/l/ar/developers/extend/apps/operations/overview">
|
||||
سطر الأوامر (CLI)، الاختبار، الوجهات البعيدة، التكامل المستمر (CI)، ونشر تطبيقك.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: إنشاء القوالب
|
||||
description: أنشئ ملفات الكيانات بشكل تفاعلي باستخدام yarn twenty dev:add — بما في ذلك الكائنات والحقول والعروض والدوال المنطقية والمزيد.
|
||||
icon: wand-magic-sparkles
|
||||
---
|
||||
|
||||
بدلًا من إنشاء ملفات الكيانات يدويًا، استخدم أداة القوالب التفاعلية:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:add
|
||||
```
|
||||
|
||||
ستطلب منك اختيار نوع الكيان وتُرشدك عبر الحقول المطلوبة، ثم تُنشئ ملفًا جاهزًا للاستخدام يحتوي على `universalIdentifier` ثابت واستدعاء `defineEntity()` الصحيح.
|
||||
|
||||
يمكنك أيضًا تمرير نوع الكيان مباشرة لتخطي المطالبة الأولى:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:add object
|
||||
yarn twenty dev:add logicFunction
|
||||
yarn twenty dev:add frontComponent
|
||||
```
|
||||
|
||||
## أنواع الكيانات المتاحة
|
||||
|
||||
| نوع الكيان | أمر | الملف المُولَّد |
|
||||
| ------------------ | ---------------------------------------- | ------------------------------------------------------- |
|
||||
| كائن | `yarn twenty dev:add object` | `src/objects/\<name>.ts` |
|
||||
| الحقل | `yarn twenty dev:add field` | `src/fields/\<name>.ts` |
|
||||
| دالة منطقية | `yarn twenty dev:add logicFunction` | `src/logic-functions/\<name>.ts` |
|
||||
| مكوّن أمامي | `yarn twenty dev:add frontComponent` | `src/front-components/\<name>.tsx` |
|
||||
| دور | `yarn twenty dev:add role` | `src/roles/\<name>.ts` |
|
||||
| مهارة | `yarn twenty dev:add skill` | `src/skills/\<name>.ts` |
|
||||
| وكيل | `yarn twenty dev:add agent` | `src/agents/\<name>.ts` |
|
||||
| عرض | `yarn twenty dev:add view` | `src/views/\<name>.ts` |
|
||||
| عنصر قائمة التنقّل | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/\<name>.ts` |
|
||||
| تخطيط الصفحة | `yarn twenty dev:add pageLayout` | `src/page-layouts/\<name>.ts` |
|
||||
|
||||
## ما الذي تُنشئه أداة القوالب
|
||||
|
||||
لكل نوع كيان قالب خاص به. على سبيل المثال، يسأل `yarn twenty dev:add object` عن:
|
||||
|
||||
1. **الاسم (مفرد)** — مثل `invoice`
|
||||
2. **الاسم (جمع)** — مثل `invoices`
|
||||
3. **التسمية (مفرد)** — تُستمد تلقائيًا من الاسم (مثل `Invoice`)
|
||||
4. **التسمية (جمع)** — تُملأ تلقائيًا (مثل `Invoices`)
|
||||
5. **إنشاء عرض وعنصر تنقّل؟** — إذا أجبت بنعم، فستُنشئ أداة القوالب أيضًا عرضًا مطابقًا ورابط شريط جانبي للكائن الجديد.
|
||||
|
||||
أنواع الكيانات الأخرى لها مطالبات أبسط — فمعظمها يطلب اسمًا فقط.
|
||||
|
||||
نوع الكيان `field` أكثر تفصيلاً: يطلب اسم الحقل وتسمية الحقل ونوعه (من قائمة بكل أنواع الحقول المتاحة مثل `TEXT` و`NUMBER` و`SELECT` و`RELATION` وغيرها)، ومعرّف `universalIdentifier` للكائن الهدف.
|
||||
|
||||
## مسار خرج مخصّص
|
||||
|
||||
استخدم العلم `--path` لوضع الملف المُولَّد في موقع مخصّص:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:add logicFunction --path src/custom-folder
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: استكشاف الأخطاء وإصلاحها
|
||||
description: مشكلات التشغيل الأول الشائعة — Docker، إصدار Node، Yarn، والتبعيات.
|
||||
icon: wrench
|
||||
---
|
||||
|
||||
* **أخطاء Docker** — تأكّد من أن Docker Desktop (أو الـ daemon) قيد التشغيل قبل `yarn twenty docker:start`. ستعرض رسالة الخطأ أمر البدء المناسب لنظام التشغيل لديك.
|
||||
* **إصدار Node غير صحيح** — نحتاج 24 أو أحدث. تحقّق باستخدام `node -v`.
|
||||
* **Yarn 4 غير موجود** — شغّل `corepack enable`.
|
||||
* **تبعيات تالفة** — `rm -rf node_modules && yarn install`.
|
||||
* **أخطاء `twenty-sdk` بعد الترقية إلى v2.8.0** — تم نقله من `dependencies` إلى `devDependencies` في الإصدار v2.8.0. انظر إلى [بنية المشروع → التبعيات](/l/ar/developers/extend/apps/getting-started/project-structure#dependencies).
|
||||
* **`twenty build` يُصدر تحذيرًا بشأن `twenty-client-sdk` تحت `dependencies`** — يتم توفيره في وقت التشغيل بواسطة Twenty، لذلك يجب نقله إلى `devDependencies` جنبًا إلى جنب مع `twenty-sdk`. انظر إلى [بنية المشروع → التبعيات](/l/ar/developers/extend/apps/getting-started/project-structure#dependencies).
|
||||
|
||||
هل علِقت؟ اطلب المساعدة على [خادم Twenty على Discord](https://discord.com/channels/1130383047699738754/1130386664812982322).
|
||||
@@ -0,0 +1,148 @@
|
||||
---
|
||||
title: عناصر قائمة الأوامر
|
||||
description: اعرض مكوّنات الواجهة الأمامية كإجراءات سريعة ومدخلات في قائمة الأوامر (Cmd+K) باستخدام defineCommandMenuItem.
|
||||
icon: الطرفية
|
||||
---
|
||||
|
||||
يُعَدّ **عنصر قائمة الأوامر** الجسر بين المستخدم و[مكوّن الواجهة الأمامية](/l/ar/developers/extend/apps/layout/front-components). يسجّل هذا العنصر المكوّن في قائمة الأوامر في Twenty (Cmd+K)، وبشكل اختياري، كزر إجراء سريع مُثبَّت في الزاوية العلوية اليمنى من الصفحة.
|
||||
|
||||
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
label: 'Open Dashboard',
|
||||
shortLabel: 'Dashboard',
|
||||
icon: 'IconLayoutDashboard',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
});
|
||||
```
|
||||
|
||||
## حقول التكوين
|
||||
|
||||
| الحقل | مطلوب | الوصف |
|
||||
| --------------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `universalIdentifier` | نعم | معرّف فريد ثابت للأمر |
|
||||
| `label` | نعم | التسمية الكاملة المعروضة في قائمة الأوامر (Cmd+K) |
|
||||
| `frontComponentUniversalIdentifier` | نعم | قيمة `universalIdentifier` للمكوّن الأمامي الذي يفتحه هذا الأمر |
|
||||
| `shortLabel` | لا | تسمية أقصر تُعرَض على زر الإجراء السريع المثبّت |
|
||||
| `icon` | لا | اسم الأيقونة المعروض بجانب التسمية (مثل `'IconBolt'` و`'IconSend'`) |
|
||||
| `isPinned` | لا | عند كونها `true`، يعرض الأمر كزر إجراء سريع في الزاوية العلوية اليمنى من الصفحة |
|
||||
| `availabilityType` | لا | تتحكّم في مكان ظهور الأمر: `'GLOBAL'` (متاح دائمًا)، و`'RECORD_SELECTION'` (فقط عند تحديد سجلات)، أو `'FALLBACK'` (يُعرَض عند عدم تطابق أي أوامر أخرى) |
|
||||
| `availabilityObjectUniversalIdentifier` | لا | تقييد الأمر بصفحات نوع كائن معيّن (مثل سجلات Company فقط) |
|
||||
| `conditionalAvailabilityExpression` | لا | تعبير منطقي يتحكّم ديناميكيًا في الظهور (انظر أدناه) |
|
||||
|
||||
## أوامر بدون واجهة
|
||||
|
||||
يُعَدّ عنصر قائمة الأوامر المقترن بـ[مكوّن واجهة أمامية بدون واجهة](/l/ar/developers/extend/apps/layout/front-components#headless-vs-non-headless) الطريقة القياسية لتوفير إجراء بنقرة واحدة — لتشغيل الشفرة أو التنقّل أو التأكيد ثم التنفيذ. تغطي صفحة مكوّنات الواجهة الأمامية [مكوّنات الأوامر في SDK](/l/ar/developers/extend/apps/layout/front-components#sdk-command-components) (`Command`, `CommandLink`, `CommandModal`, `CommandOpenSidePanelPage`) التي تتعامل مع نمط الإجراء-ثم-إلغاء التركيب.
|
||||
|
||||
تدفق نموذجي:
|
||||
|
||||
```tsx src/front-components/run-action.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const RunAction = () => {
|
||||
const execute = async () => {
|
||||
const client = new CoreApiClient();
|
||||
await client.mutation({
|
||||
createTask: {
|
||||
__args: { data: { title: 'Created by my app' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return <Command execute={execute} />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
name: 'run-action',
|
||||
description: 'Creates a task from the command menu',
|
||||
component: RunAction,
|
||||
isHeadless: true,
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/command-menu-items/run-action.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
});
|
||||
```
|
||||
|
||||
## تعابير الإتاحة الشرطية
|
||||
|
||||
يتيح لك الحقل `conditionalAvailabilityExpression` التحكّم في وقت ظهور الأمر بناءً على سياق الصفحة الحالي. استورد متغيّرات ومشغّلات مضبوطة الأنواع من `twenty-sdk` لبناء التعابير:
|
||||
|
||||
```ts src/command-menu-items/bulk-update.command-menu-item.ts
|
||||
import {
|
||||
defineCommandMenuItem,
|
||||
objectPermissions,
|
||||
everyEquals,
|
||||
} from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: '...',
|
||||
label: 'Bulk Update',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
frontComponentUniversalIdentifier: '...',
|
||||
conditionalAvailabilityExpression: everyEquals(
|
||||
objectPermissions,
|
||||
'canUpdateObjectRecords',
|
||||
true,
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`RECORD_SELECTION` تعني بالفعل وجود تحديد غير فارغ — استخدم `numberOfSelectedRecords` فقط لعرض الأعداد المحددة (على سبيل المثال `>= 2`).
|
||||
</Note>
|
||||
|
||||
### متغيّرات السياق
|
||||
|
||||
تُمثّل هذه المتغيّرات الحالة الحالية للصفحة:
|
||||
|
||||
| المتغيّر | النوع | الوصف |
|
||||
| ------------------------------ | --------- | --------------------------------------------------------------- |
|
||||
| `pageType` | `string` | نوع الصفحة الحالي (مثل `'RecordIndexPage'` و`'RecordShowPage'`) |
|
||||
| `isInSidePanel` | `boolean` | ما إذا كان المكوّن معروضًا في لوحة جانبية |
|
||||
| `numberOfSelectedRecords` | `number` | عدد السجلات المحدّدة حاليًا |
|
||||
| `isSelectAll` | `boolean` | ما إذا كان "تحديد الكل" مفعّلًا |
|
||||
| `selectedRecords` | `array` | كائنات السجلات المحدّدة |
|
||||
| `favoriteRecordIds` | `array` | معرّفات السجلات المفضّلة |
|
||||
| `objectPermissions` | `object` | الأذونات الخاصة بنوع الكائن الحالي |
|
||||
| `targetObjectReadPermissions` | `object` | أذونات القراءة للكائن الهدف |
|
||||
| `targetObjectWritePermissions` | `object` | أذونات الكتابة للكائن الهدف |
|
||||
| `featureFlags` | `object` | أعلام الميزات المفعَّلة |
|
||||
| `objectMetadataItem` | `object` | بيانات التعريف لنوع الكائن الحالي |
|
||||
| `hasAnySoftDeleteFilterOnView` | `boolean` | ما إذا كان العرض الحالي يحتوي على مرشّح حذف منطقي |
|
||||
|
||||
### المُشغِّلات
|
||||
|
||||
جمّع المتغيّرات في تعابير منطقية:
|
||||
|
||||
| المُشغِّل | الوصف |
|
||||
| ----------------------------------- | ------------------------------------------------------------------ |
|
||||
| `isDefined(value)` | `true` إذا لم تكن القيمة null/undefined |
|
||||
| `isNonEmptyString(value)` | `true` إذا كانت القيمة سلسلة غير فارغة |
|
||||
| `includes(array, value)` | `true` إذا كانت المصفوفة تحتوي على القيمة |
|
||||
| `includesEvery(array, prop, value)` | `true` إذا كانت خاصية كل عنصر تتضمن القيمة |
|
||||
| `every(array, prop)` | `true` إذا كانت الخاصية تُقيَّم كقيمة صادقة في كل عنصر |
|
||||
| `everyDefined(array, prop)` | `true` إذا كانت الخاصية معرّفة في كل عنصر |
|
||||
| `everyEquals(array, prop, value)` | `true` إذا كانت الخاصية تساوي القيمة في كل عنصر |
|
||||
| `some(array, prop)` | `true` إذا كانت الخاصية تُقيَّم كقيمة صادقة في عنصر واحد على الأقل |
|
||||
| `someDefined(array, prop)` | `true` إذا كانت الخاصية معرّفة في عنصر واحد على الأقل |
|
||||
| `someEquals(array, prop, value)` | `true` إذا كانت الخاصية تساوي القيمة في عنصر واحد على الأقل |
|
||||
| `someNonEmptyString(array, prop)` | `true` إذا كانت الخاصية سلسلة غير فارغة في عنصر واحد على الأقل |
|
||||
| `none(array, prop)` | `true` إذا كانت الخاصية تُقيَّم كقيمة زائفة في كل عنصر |
|
||||
| `noneDefined(array, prop)` | `true` إذا كانت الخاصية غير معرّفة في كل عنصر |
|
||||
| `noneEquals(array, prop, value)` | `true` إذا لم تكن الخاصية تساوي القيمة في أي عنصر |
|
||||
@@ -0,0 +1,545 @@
|
||||
---
|
||||
title: المكوّنات الأمامية
|
||||
description: أنشئ مكونات React تُعرَض داخل واجهة مستخدم Twenty ضمن بيئة معزولة (sandbox).
|
||||
icon: window-maximize
|
||||
---
|
||||
|
||||
المكوّنات الأمامية هي مكوّنات React تُعرَض مباشرة داخل واجهة مستخدم Twenty. تعمل ضمن **Web Worker** معزول باستخدام Remote DOM — تكون شيفرتك في صندوق عزل لكنها تُعرَض أصيلًا داخل الصفحة، وليس ضمن iframe.
|
||||
|
||||
## أين يمكن استخدام مكوّنات الواجهة الأمامية
|
||||
|
||||
يمكن عرض مكوّنات الواجهة الأمامية في موقعين داخل Twenty:
|
||||
|
||||
* **اللوحة الجانبية** — المكوّنات غير عديمة الرأس تفتح في اللوحة الجانبية اليمنى. هذا هو السلوك الافتراضي عندما يتم تشغيل مكوّن واجهة أمامية من قائمة الأوامر.
|
||||
* **الويدجت (لوحات المعلومات وصفحات السجلات)** — يمكن تضمين مكوّنات الواجهة الأمامية كويدجت داخل [تخطيطات الصفحات](/l/ar/developers/extend/apps/layout/page-layouts). عند تكوين لوحة معلومات أو تخطيط صفحة سجل، يمكن للمستخدمين إضافة ويدجت لمكوّن واجهة أمامية.
|
||||
|
||||
مكوّن الواجهة الأمامية بمفرده لا يمكن الوصول إليه من واجهة المستخدم — تحتاج إلى *عرضه*. هناك طريقتان للقيام بذلك:
|
||||
|
||||
* **إقرانه مع [عنصر قائمة الأوامر](/l/ar/developers/extend/apps/layout/command-menu-items)** — يقوم بتسجيله في قائمة الأوامر (Cmd+K) واختياريًا كإجراء سريع مُثبّت.
|
||||
* **تضمينه كويدجت في [تخطيط صفحة](/l/ar/developers/extend/apps/layout/page-layouts)** — يضعه في صفحة تفاصيل السجل أو لوحة المعلومات.
|
||||
|
||||
## مثال أساسي
|
||||
|
||||
أسرع طريقة لرؤية مكوّن الواجهة الأمامية أثناء العمل هي إقرانه مع [`defineCommandMenuItem`](/l/ar/developers/extend/apps/layout/command-menu-items)، بحيث يظهر كزر إجراء سريع في الزاوية العلوية اليمنى من الصفحة:
|
||||
|
||||
```tsx src/front-components/hello-world.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
const HelloWorld = () => {
|
||||
return (
|
||||
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
|
||||
<h1>Hello from my app!</h1>
|
||||
<p>This component renders inside Twenty.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
name: 'hello-world',
|
||||
description: 'A simple front component',
|
||||
component: HelloWorld,
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/command-menu-items/hello-world.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
|
||||
shortLabel: 'Hello',
|
||||
label: 'Hello World',
|
||||
icon: 'IconBolt',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
});
|
||||
```
|
||||
|
||||
بعد المزامنة باستخدام `yarn twenty dev` (أو تشغيل الأمر لمرة واحدة `yarn twenty dev --once`)، يظهر الإجراء السريع في الزاوية العلوية اليمنى من الصفحة:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="زر إجراء سريع في الزاوية العلوية اليمنى" />
|
||||
</div>
|
||||
|
||||
انقره لعرض المكوّن مضمنًا داخل الصفحة.
|
||||
|
||||
## حقول التكوين
|
||||
|
||||
| الحقل | مطلوب | الوصف |
|
||||
| --------------------- | ----- | ------------------------------------------------------------- |
|
||||
| `universalIdentifier` | نعم | معرّف فريد ثابت لهذا المكوّن |
|
||||
| `component` | نعم | دالة مكوّن React |
|
||||
| `name` | لا | الاسم المعروض |
|
||||
| `description` | لا | وصف لما يفعله المكوّن |
|
||||
| `isHeadless` | لا | عيّنه على `true` إذا كان المكوّن بلا واجهة مرئية (انظر أدناه) |
|
||||
|
||||
## وضع مكوّن أمامي على صفحة
|
||||
|
||||
إضافةً إلى الأوامر، يمكنك تضمين مكوّن أمامي مباشرةً في صفحة سجل عبر إضافته كودجت في **تخطيط صفحة**. لمزيد من التفاصيل، راجع [تخطيطات الصفحات](/l/ar/developers/extend/apps/layout/page-layouts).
|
||||
|
||||
## عديم الرأس مقابل غير عديم الرأس
|
||||
|
||||
تأتي مكوّنات الواجهة الأمامية بوضعَي عرض يتحكّم بهما الخيار `isHeadless`:
|
||||
|
||||
**غير عديم الرأس (افتراضي)** — يعرض المكوّن واجهة مستخدم مرئية. عند تشغيله من قائمة الأوامر يفتح في اللوحة الجانبية. هذا هو السلوك الافتراضي عندما تكون `isHeadless` تساوي `false` أو يتم تجاهلها.
|
||||
|
||||
**عديم الرأس (`isHeadless: true`)** — يتم تركيب المكوّن بشكل غير مرئي في الخلفية. لا يفتح اللوحة الجانبية. تم تصميم المكوّنات عديمة الرأس لإجراءات تنفّذ منطقًا ثم تُزيل تركيبها ذاتيًا — على سبيل المثال، تشغيل مهمة غير متزامنة، أو الانتقال إلى صفحة، أو إظهار نافذة تأكيد منبثقة. تتوافق بشكل طبيعي مع مكوّنات Command في SDK الموصوفة أدناه.
|
||||
|
||||
```tsx src/front-components/sync-tracker.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const SyncTracker = () => {
|
||||
const recordId = useRecordId();
|
||||
|
||||
useEffect(() => {
|
||||
enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
|
||||
}, [recordId]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'sync-tracker',
|
||||
description: 'Tracks record views silently',
|
||||
isHeadless: true,
|
||||
component: SyncTracker,
|
||||
});
|
||||
```
|
||||
|
||||
نظرًا لأن المكوّن يُرجع `null`، فإن Twenty يتخطّى عرض حاوية له — ولن تظهر مساحة فارغة في التخطيط. لا يزال لدى المكوّن إمكانية الوصول إلى جميع الخطافات وواجهة برمجة الاتصال مع المضيف.
|
||||
|
||||
## مكوّنات Command في SDK
|
||||
|
||||
توفر حزمة `twenty-sdk` أربعة مكوّنات مساعدة من نوع Command مصممة للمكوّنات عديمة الرأس في الواجهة الأمامية. كل مكوّن ينفّذ إجراءً عند التركيب، ويتعامل مع الأخطاء بعرض إشعار Snackbar، ويزيل تركيب مكوّن الواجهة الأمامية تلقائيًا عند الانتهاء.
|
||||
|
||||
استوردها من `twenty-sdk/command`:
|
||||
|
||||
* **`Command`** — يشغّل رد نداء غير متزامن عبر الخاصية `execute`.
|
||||
* **`CommandLink`** — ينتقل إلى مسار في التطبيق. الخصائص: `to`، `params`، `queryParams`، `options`.
|
||||
* **`CommandModal`** — يفتح نافذة تأكيد منبثقة. إذا أكّد المستخدم، ينفّذ رد النداء `execute`. الخصائص: `title`، `subtitle`، `execute`، `confirmButtonText`، `confirmButtonAccent`.
|
||||
* **`CommandOpenSidePanelPage`** — يفتح صفحة محدّدة في اللوحة الجانبية. الخصائص: `page`، `pageTitle`، `pageIcon`.
|
||||
|
||||
فيما يلي مثال كامل لمكوّن واجهة أمامية عديم الرأس يستخدم `Command` لتشغيل إجراء من قائمة الأوامر:
|
||||
|
||||
```tsx src/front-components/run-action.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const RunAction = () => {
|
||||
const execute = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
await client.mutation({
|
||||
createTask: {
|
||||
__args: { data: { title: 'Created by my app' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return <Command execute={execute} />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
name: 'run-action',
|
||||
description: 'Creates a task from the command menu',
|
||||
component: RunAction,
|
||||
isHeadless: true,
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/command-menu-items/run-action.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
});
|
||||
```
|
||||
|
||||
ومثال يستخدم `CommandModal` لطلب التأكيد قبل التنفيذ:
|
||||
|
||||
```tsx src/front-components/delete-draft.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { CommandModal } from 'twenty-sdk/command';
|
||||
|
||||
const DeleteDraft = () => {
|
||||
const execute = async () => {
|
||||
// perform the deletion
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandModal
|
||||
title="Delete draft?"
|
||||
subtitle="This action cannot be undone."
|
||||
execute={execute}
|
||||
confirmButtonText="Delete"
|
||||
confirmButtonAccent="danger"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
|
||||
name: 'delete-draft',
|
||||
description: 'Deletes a draft with confirmation',
|
||||
component: DeleteDraft,
|
||||
isHeadless: true,
|
||||
});
|
||||
```
|
||||
|
||||
## استدعاء دالة منطقية
|
||||
|
||||
تعمل مكونات الواجهة الأمامية في المتصفح داخل Web Worker معزول، بينما تعمل [الدوال المنطقية](/l/ar/developers/extend/apps/logic/logic-functions) على جانب الخادم. لا توجد استدعاءات مباشرة ضمن العملية بين الاثنين — بدلاً من ذلك، يصل مكون الواجهة الأمامية إلى الدالة المنطقية عبر HTTP.
|
||||
|
||||
يتم إتاحة الدالة المنطقية المُعلَنة باستخدام `httpRouteTriggerSettings` تحت نقطة النهاية `/s/` عند `${TWENTY_API_URL}/s\<path>`. يستدعي مكون الواجهة الأمامية ذلك المسار باستخدام `RestApiClient` من `twenty-client-sdk/rest`، والذي يقوم بالمصادقة باستخدام `TWENTY_APP_ACCESS_TOKEN` الذي تقوم Twenty بحقنه في الـ worker.
|
||||
|
||||
تم تصميم `RestApiClient` خصيصًا لهذا الغرض. يقوم بقراءة `TWENTY_API_URL` و`TWENTY_APP_ACCESS_TOKEN` من بيئة الـ worker، وإرفاق ترويسة `Authorization: Bearer`، وتسلسل وتحليل JSON، وإثارة `RestApiClientError` عندما يكون الرمز المميز أو عنوان URL مفقودًا أو عندما يكون الرد غير 2xx — حتى لا تعيد تنفيذ هذا الـ boilerplate في كل مكون.
|
||||
|
||||
يمكن لمكون واجهة أمامية عديم الرأس تنفيذ الاستدعاء عند التركيب عبر مكون `Command`، ثم إلغاء التركيب تلقائيًا:
|
||||
|
||||
```tsx src/front-components/sync-prs.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { RestApiClient } from 'twenty-client-sdk/rest';
|
||||
|
||||
const SyncPrs = () => {
|
||||
const execute = async () => {
|
||||
const client = new RestApiClient();
|
||||
|
||||
await client.post('/s/github/fetch-prs', {
|
||||
owner: 'twentyhq',
|
||||
repo: 'twenty',
|
||||
});
|
||||
};
|
||||
|
||||
return <Command execute={execute} />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'sync-prs',
|
||||
description: 'Triggers the fetch-prs logic function',
|
||||
isHeadless: true,
|
||||
component: SyncPrs,
|
||||
});
|
||||
```
|
||||
|
||||
المسار المُمرَّر إلى العميل هو المسار العام للمسار (route) — قيمة `httpRouteTriggerSettings.path` الخاصة بدالة المنطق (logic function) مع إضافة البادئة `/s`. أبقِ `isAuthRequired: true`؛ يزوّد العميل مكوّنك برمز وصول التطبيق الذي تُصدِره Twenty:
|
||||
|
||||
```ts src/logic-functions/fetch-prs.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string };
|
||||
// ...fetch from GitHub and persist records...
|
||||
return { ok: true };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '...',
|
||||
name: 'fetch-prs',
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/github/fetch-prs',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
يتم حقن `TWENTY_API_URL` و`TWENTY_APP_ACCESS_TOKEN` تلقائيًا — انظر [متغيرات التطبيق](#application-variables). نظرًا لأن متغيرات التطبيق السرية لا تُعرَض أبدًا على مكونات الواجهة الأمامية، احتفِظ بمفاتيح واجهة برمجة التطبيقات والمنطق الحساس الآخر داخل الدالة المنطقية، وليس في مكون الواجهة الأمامية.
|
||||
</Note>
|
||||
|
||||
### مرجع RestApiClient
|
||||
|
||||
استورد `RestApiClient` من `twenty-client-sdk/rest`. ينتمي إلى نفس عائلة العملاء مثل `CoreApiClient` و`MetadataApiClient`، لكنه يستهدف مسارات HTTP الخاصة بتطبيقك بدلاً من واجهة GraphQL API.
|
||||
|
||||
| طريقة | الوصف |
|
||||
| --------------------------------- | -------------------------- |
|
||||
| `get(path, options?)` | يرسل طلبًا من نوع `GET` |
|
||||
| `post(path, body?, options?)` | يرسل طلبًا من نوع `POST` |
|
||||
| `put(path, body?, options?)` | يرسل طلبًا من نوع `PUT` |
|
||||
| `patch(path, body?, options?)` | يرسل طلبًا من نوع `PATCH` |
|
||||
| `delete(path, options?)` | يرسل طلبًا من نوع `DELETE` |
|
||||
| `request(method, path, options?)` | طلب عام بأي طريقة HTTP |
|
||||
|
||||
تدعم `options` كلًا من `headers` و`query` (سجل لمعاملات query-string؛ يتم تخطي القيم nullish) و`AbortSignal` عبر `signal`. يتم تسلسل كائن `body` غير من النوع `FormData` إلى JSON تلقائيًا. عند حدوث `401`، يقوم العميل بتحديث رمز الوصول مرة واحدة عبر المضيف ثم يعيد محاولة الطلب.
|
||||
|
||||
يتم تحديد عنوان URL الأساسي والرمز من بيئة التشغيل بشكل افتراضي. مرِّر معاملات تجاوز (overrides) إلى المُنشئ (constructor) عند الحاجة — على سبيل المثال في الاختبارات:
|
||||
|
||||
```ts
|
||||
const client = new RestApiClient({
|
||||
baseUrl: 'https://api.example.com',
|
||||
token: 'my-token',
|
||||
});
|
||||
```
|
||||
|
||||
ترمي الطلبات الفاشلة خطأً من نوع `RestApiClientError` يعرِّض خصائص `status` و`statusText` و`url` بالإضافة إلى `body` بعد تحليله (parsed):
|
||||
|
||||
```tsx
|
||||
import { RestApiClient, RestApiClientError } from 'twenty-client-sdk/rest';
|
||||
|
||||
const client = new RestApiClient();
|
||||
|
||||
try {
|
||||
const prs = await client.get('/s/github/fetch-prs', {
|
||||
query: { state: 'open' },
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof RestApiClientError) {
|
||||
console.error(error.status, error.body);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## الوصول إلى سياق وقت التشغيل
|
||||
|
||||
داخل مكوّنك، استخدم خطافات SDK للوصول إلى المستخدم الحالي، والسجل، ومثيل المكوّن:
|
||||
|
||||
```tsx src/front-components/record-info.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
useUserId,
|
||||
useRecordId,
|
||||
useFrontComponentId,
|
||||
} from 'twenty-sdk/front-component';
|
||||
|
||||
const RecordInfo = () => {
|
||||
const userId = useUserId();
|
||||
const recordId = useRecordId();
|
||||
const componentId = useFrontComponentId();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>User: {userId}</p>
|
||||
<p>Record: {recordId ?? 'No record context'}</p>
|
||||
<p>Component: {componentId}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
|
||||
name: 'record-info',
|
||||
component: RecordInfo,
|
||||
});
|
||||
```
|
||||
|
||||
الخطافات المتاحة:
|
||||
|
||||
| الخطّاف | القيم المعادة | الوصف |
|
||||
| --------------------------------------------- | --------------------- | ---------------------------------------------------------------------- |
|
||||
| `useUserId()` | `string` أو `null` | معرّف المستخدم الحالي |
|
||||
| `useSelectedRecordIds()` | `string[]` | جميع معرّفات السجلات المحددة (مصفوفة فارغة إذا لم يتم تحديد أي منها) |
|
||||
| `useRecordId()` | `string` أو `null` | **مهمل.** استخدم `useSelectedRecordIds()` بدلاً من ذلك |
|
||||
| `useFrontComponentId()` | `string` | معرّف مثيل هذا المكوّن |
|
||||
| `useColorScheme()` | `'light'` أو `'dark'` | نظام الألوان النشط لواجهة المستخدم المضيفة (`System` تم تحديده بالفعل) |
|
||||
| `useFrontComponentExecutionContext(selector)` | يختلف | الوصول إلى سياق التنفيذ الكامل عبر دالة محدِّد |
|
||||
|
||||
## متغيرات التطبيق
|
||||
|
||||
متغيرات التطبيق المُعرَّفة في [`defineApplication()`](/l/ar/developers/extend/apps/config/application) مع `isSecret: false` تكون متاحة داخل مكوّنات الواجهة عبر أداة `getApplicationVariable`:
|
||||
|
||||
```tsx src/front-components/greeting.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { getApplicationVariable } from 'twenty-sdk/front-component';
|
||||
|
||||
const Greeting = () => {
|
||||
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
|
||||
|
||||
return <p>Hello, {recipientName}!</p>;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'greeting',
|
||||
component: Greeting,
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
المتغيرات السرّية (`isSecret: true`) **لا** يتم كشفها لمكوّنات الواجهة. هي متاحة فقط في [دوال المنطق](/l/ar/developers/extend/apps/logic/logic-functions)، التي تعمل على جهة الخادم. هذا يمنع إرسال القيم الحساسة مثل مفاتيح API إلى المتصفح.
|
||||
</Warning>
|
||||
|
||||
متغيرات النظام التالية تكون متاحة دائمًا عبر `process.env`:
|
||||
|
||||
| المتغيّر | الوصف |
|
||||
| ------------------------- | --------------------------------------- |
|
||||
| `TWENTY_API_URL` | عنوان URL الأساسي لـ Twenty API |
|
||||
| `TWENTY_APP_ACCESS_TOKEN` | رمز مميز قصير العمر مُقيَّد بدور تطبيقك |
|
||||
|
||||
## واجهة الاتصال مع المضيف
|
||||
|
||||
يمكن للمكوّنات الأمامية تشغيل التنقّل والنوافذ المنبثقة والإشعارات باستخدام دوال من `twenty-sdk`:
|
||||
|
||||
| دالة | الوصف |
|
||||
| ----------------------------------------------- | ------------------------------ |
|
||||
| `navigate(to, params?, queryParams?, options?)` | الانتقال إلى صفحة داخل التطبيق |
|
||||
| `openSidePanelPage(params)` | فتح لوحة جانبية |
|
||||
| `closeSidePanel()` | إغلاق اللوحة الجانبية |
|
||||
| `openCommandConfirmationModal(params)` | عرض مربع حوار تأكيد |
|
||||
| `enqueueSnackbar(params)` | عرض إشعار توست |
|
||||
| `unmountFrontComponent()` | إلغاء تركيب المكوّن |
|
||||
| `updateProgress(progress)` | تحديث مؤشّر التقدّم |
|
||||
|
||||
فيما يلي مثال يستخدم واجهة برمجة تطبيقات المضيف لعرض Snackbar وإغلاق اللوحة الجانبية بعد اكتمال الإجراء:
|
||||
|
||||
```tsx src/front-components/archive-record.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useRecordId } from 'twenty-sdk/front-component';
|
||||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const ArchiveRecord = () => {
|
||||
const recordId = useRecordId();
|
||||
|
||||
const handleArchive = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
await client.mutation({
|
||||
updateTask: {
|
||||
__args: { id: recordId, data: { status: 'ARCHIVED' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
await enqueueSnackbar({
|
||||
message: 'Record archived',
|
||||
variant: 'success',
|
||||
});
|
||||
|
||||
await closeSidePanel();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<p>Archive this record?</p>
|
||||
<button onClick={handleArchive}>Archive</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
|
||||
name: 'archive-record',
|
||||
description: 'Archives the current record',
|
||||
component: ArchiveRecord,
|
||||
});
|
||||
```
|
||||
|
||||
### العمل مع سجلات متعددة
|
||||
|
||||
استخدم `useSelectedRecordIds()` لمعالجة عدة سجلات محددة. هذا مفيد للعمليات المجمّعة:
|
||||
|
||||
```tsx src/front-components/bulk-export.tsx
|
||||
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
|
||||
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
|
||||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const BulkExport = () => {
|
||||
const selectedRecordIds = useSelectedRecordIds();
|
||||
|
||||
const handleExport = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
for (const recordId of selectedRecordIds) {
|
||||
await client.mutation({
|
||||
updateTask: {
|
||||
__args: { id: recordId, data: { exported: true } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await enqueueSnackbar({
|
||||
message: `Exported ${selectedRecordIds.length} records`,
|
||||
variant: 'success',
|
||||
});
|
||||
|
||||
await closeSidePanel();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<p>Export {selectedRecordIds.length} selected record(s)?</p>
|
||||
<button onClick={handleExport}>Export</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
|
||||
name: 'bulk-export',
|
||||
description: 'Export selected records',
|
||||
component: BulkExport,
|
||||
command: {
|
||||
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
|
||||
label: 'Bulk Export',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
conditionalAvailabilityExpression: numberOfSelectedRecords > 0,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## الأصول العامة
|
||||
|
||||
يمكن للمكوّنات الأمامية الوصول إلى ملفات من دليل `public/` للتطبيق باستخدام `getPublicAssetUrl`:
|
||||
|
||||
```tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { getPublicAssetUrl } from 'twenty-sdk/utils';
|
||||
|
||||
const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'logo',
|
||||
component: Logo,
|
||||
});
|
||||
```
|
||||
|
||||
راجع [قسم الأصول العامة](/l/ar/developers/extend/apps/config/public-assets) للتفاصيل.
|
||||
|
||||
## التنسيق
|
||||
|
||||
تدعم المكوّنات الأمامية عدة أساليب للتنسيق. يمكنك استخدام:
|
||||
|
||||
* **أنماط مضمنة** — `style={{ color: 'red' }}`
|
||||
* **مكوّنات Twenty لواجهة المستخدم** — استورد من `twenty-sdk/ui` (Button وTag وStatus وChip وAvatar وغيرها)
|
||||
* **Emotion** — CSS-in-JS مع `@emotion/react`
|
||||
* **Styled-components** — أنماط `styled.div`
|
||||
* **Tailwind CSS** — أصناف مساعدة
|
||||
* **أي مكتبة CSS-in-JS** متوافقة مع React
|
||||
|
||||
```tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Button, Tag, Status } from 'twenty-sdk/ui';
|
||||
|
||||
const StyledWidget = () => {
|
||||
return (
|
||||
<div style={{ padding: '16px', display: 'flex', gap: '8px' }}>
|
||||
<Button title="Click me" onClick={() => alert('Clicked!')} />
|
||||
<Tag text="Active" color="green" />
|
||||
<Status color="green" text="Online" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456',
|
||||
name: 'styled-widget',
|
||||
component: StyledWidget,
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: عناصر قائمة التنقّل
|
||||
description: أضِف إدخالات مخصّصة إلى الشريط الجانبي لمساحة العمل — روابط إلى العروض المحفوظة أو عناوين URL خارجية.
|
||||
icon: bars
|
||||
---
|
||||
|
||||
يُعَدّ **عنصر قائمة التنقّل** إدخالًا في الشريط الجانبي الأيسر. استخدم `defineNavigationMenuItem()` لتوفير روابط شريط جانبي مخصّصة — عادةً واحدًا لكل [عرض](/l/ar/developers/extend/apps/layout/views) توفّره — أو للإشارة إلى عناوين URL خارجية.
|
||||
|
||||
```ts src/navigation-menu-items/example-navigation-menu-item.ts
|
||||
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c',
|
||||
name: 'example-navigation-menu-item',
|
||||
icon: 'IconList',
|
||||
color: 'blue',
|
||||
position: 0,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
## النقاط الرئيسية
|
||||
|
||||
* يحدّد `type` ما الذي يرتبط به عنصر القائمة. كل نوع يقترن بحقل معرّف محدّد:
|
||||
|
||||
| النوع | ماذا يفعل | حقل مطلوب |
|
||||
| ------------------------------------ | ------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `NavigationMenuItemType.VIEW` | يفتح عرضًا محفوظًا | `viewUniversalIdentifier` |
|
||||
| `NavigationMenuItemType.LINK` | يفتح عنوان URL خارجيًا | `link` |
|
||||
| `NavigationMenuItemType.FOLDER` | يجمع العناصر المتداخلة تحت تسمية | `name` (وتشير العناصر الفرعية إلى المجلّد عبر `folderUniversalIdentifier`) |
|
||||
| `NavigationMenuItemType.OBJECT` | يفتح صفحة الفهرس الافتراضية لكائنٍ ما | `targetObjectUniversalIdentifier` |
|
||||
| `NavigationMenuItemType.PAGE_LAYOUT` | يفتح مخطّط صفحة مستقلًا | `pageLayoutUniversalIdentifier` |
|
||||
|
||||
* `position` يتحكّم في الترتيب ضمن الشريط الجانبي.
|
||||
|
||||
* `icon` و`color` اختياريان ويخصّصان مظهر الإدخال.
|
||||
|
||||
* `folderUniversalIdentifier` متاح أيضًا على أي عنصر لوضعه متداخلًا داخل عنصر أب من النوع `FOLDER`.
|
||||
|
||||
<Note>
|
||||
**مشكلة شائعة:** إنشاء كائن بدون عرض مرتبط + عنصر قائمة تنقّل يجعل ذلك الكائن غير مرئي للمستخدمين. ما لم يكن كائنًا تقنيًا/داخليًا، يجب أن يحتوي كل كائن مخصّص على عرض افتراضي *وعنصر* في الشريط الجانبي يشير إليه.
|
||||
</Note>
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: نظرة عامة
|
||||
description: ضع تطبيقك داخل واجهة مستخدم Twenty — إدخالات الشريط الجانبي، العروض المحفوظة، علامات تبويب صفحة السجل، ومكوّنات React المعزولة (sandboxed).
|
||||
icon: table-columns
|
||||
---
|
||||
|
||||
تمثّل **طبقة التخطيط** في تطبيق Twenty كل ما يراه المستخدم: مكان ظهور التطبيق في الشريط الجانبي، العروض القائِمية التي يوفّرها، كيفية ترتيب صفحات تفاصيل السجل، وأي مكوّنات React مخصّصة يتم عرضها داخل تلك الصفحات.
|
||||
|
||||
```text
|
||||
Sidebar Record list Record detail page
|
||||
─────── ─────────── ──────────────────
|
||||
[📋 My View] ────▶ ┌──────────┐ ┌─────────────────────┐
|
||||
[📋 Drafts ] │ Companies│ │ Tabs: [Overview ] │
|
||||
[📋 Inbox ] │ ──────── │ │ [Notes ] │
|
||||
▲ │ Apple │ │ [Hello ]◀──── definePageLayoutTab
|
||||
│ │ Acme │ │ │ adds a tab...
|
||||
└ defineNavi- │ … │ │ ┌────────────────┐ │
|
||||
gationMenu- └────▲─────┘ │ │ │ │
|
||||
Item points │ │ │ React UI │◀── …with a
|
||||
to a defineView │ │ │ (sandboxed in │ │ defineFrontComponent
|
||||
└ defineView │ │ a Worker) │ │ widget inside
|
||||
picks columns │ └────────────────┘ │
|
||||
and filters └─────────────────────┘
|
||||
```
|
||||
|
||||
## في هذا القسم
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="العروض" icon="list" href="/l/ar/developers/extend/apps/layout/views">
|
||||
`defineView` — تكوينات قوائم محفوظة: الأعمدة الظاهرة، وعوامل التصفية، والمجموعات.
|
||||
</Card>
|
||||
<Card title="عناصر قائمة التنقّل" icon="bars" href="/l/ar/developers/extend/apps/layout/navigation-menu-items">
|
||||
`defineNavigationMenuItem` — إدخالات في الشريط الجانبي تشير إلى العروض أو عناوين URL الخارجية.
|
||||
</Card>
|
||||
<Card title="تخطيطات الصفحات" icon="table-columns" href="/l/ar/developers/extend/apps/layout/page-layouts">
|
||||
`definePageLayout` و`definePageLayoutTab` — علامات التبويب والويدجتات في صفحة تفاصيل السجل.
|
||||
</Card>
|
||||
<Card title="المكوّنات الأمامية" icon="window-maximize" href="/l/ar/developers/extend/apps/layout/front-components">
|
||||
`defineFrontComponent` — مكوّنات React معزولة (sandboxed) يتم عرضها داخل Twenty.
|
||||
</Card>
|
||||
<Card title="عناصر قائمة الأوامر" icon="terminal" href="/l/ar/developers/extend/apps/layout/command-menu-items">
|
||||
`defineCommandMenuItem` — تسجيل مكوّنات الواجهة الأمامية كإدخالات Cmd+K وإجراءات سريعة.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## أين يظهر التطبيق
|
||||
|
||||
| موضع الظهور | ما الذي يتحكّم فيه | كيان |
|
||||
| ------------------------- | ------------------------------------------------------------------------- | ----------------------------------------- |
|
||||
| **الشريط الجانبي** | إدخال مخصّص يربط بعرض محفوظ أو عنوان URL خارجي | `defineNavigationMenuItem` |
|
||||
| **قائمة السجلات** | تكوين محفوظ لكائن — الأعمدة الظاهرة، والترتيب، وعوامل التصفية، والمجموعات | `defineView` |
|
||||
| **صفحة تفاصيل السجل** | علامات التبويب والويدجتات في صفحة السجل (لكائنك الخاص أو لكائن قياسي) | `definePageLayout`, `definePageLayoutTab` |
|
||||
| **داخل أي مما سبق** | ويدجت React مخصّص — أزرار، نماذج، لوحات بيانات، تكاملات | `defineFrontComponent` |
|
||||
| **قائمة الأوامر (Cmd+K)** | إجراء سريع مُثبّت أو أمر مخفي | `defineCommandMenuItem` |
|
||||
|
||||
تعمل مكوّنات الواجهة الأمامية داخل Web Worker معزول باستخدام Remote DOM — يتم عرضها بشكل أصيل داخل الصفحة (وليس داخل iframe)، لكنها لا تستطيع الوصول مباشرةً إلى صفحة المضيف أو إلى DOM. يحدث التواصل مع Twenty من خلال واجهة API للمضيف تعتمد تمرير الرسائل.
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
title: تخطيطات الصفحات
|
||||
description: خصص صفحات تفاصيل السجل — الألسنة، وعناصر الواجهة (widgets)، وأماكن عرض مكوّنات الواجهة الأمامية (front components) — باستخدام `definePageLayout` و `definePageLayoutTab`.
|
||||
icon: table-columns
|
||||
---
|
||||
|
||||
يتحكم **تخطيط الصفحة** في كيفية ترتيب صفحة تفاصيل السجل: ما هي الألسنة التي تظهر وما عناصر الواجهة (widgets) التي تحتوي عليها. استخدم `definePageLayout()` للتصريح عن تخطيط لكائن تملكه، أو `definePageLayoutTab()` لإضافة لسان واحد إلى تخطيط موجود مسبقًا (سواء كان مِلكك أو تخطيط Twenty قياسيًا).
|
||||
|
||||
| حالة استخدام | كيان |
|
||||
| -------------------------------------------------------------- | --------------------- |
|
||||
| عرِّف التخطيط الكامل لصفحة سجل على كائن تملكه | `definePageLayout` |
|
||||
| أضف لسانًا واحدًا إلى تخطيط موجود (لكائن تملكه أو تخطيط قياسي) | `definePageLayoutTab` |
|
||||
|
||||
## definePageLayout
|
||||
|
||||
استخدم هذا عندما تملك صفحة التفاصيل بالكامل — عادةً لكائن مخصص قمت بتعريفه بنفسك.
|
||||
|
||||
```ts src/page-layouts/example-record-page-layout.ts
|
||||
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
|
||||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
||||
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
||||
|
||||
export default definePageLayout({
|
||||
universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
|
||||
name: 'Example Record Page',
|
||||
type: 'RECORD_PAGE',
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
tabs: [
|
||||
{
|
||||
universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
|
||||
title: 'Hello World',
|
||||
position: 50,
|
||||
icon: 'IconWorld',
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
widgets: [
|
||||
{
|
||||
universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
|
||||
title: 'Hello World',
|
||||
type: 'FRONT_COMPONENT',
|
||||
configuration: {
|
||||
configurationType: 'FRONT_COMPONENT',
|
||||
frontComponentUniversalIdentifier:
|
||||
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### النقاط الرئيسية
|
||||
|
||||
* `type` يكون عادة `'RECORD_PAGE'` لتخصيص عرض التفاصيل لكائن محدّد.
|
||||
* `objectUniversalIdentifier` يحدّد الكائن الذي ينطبق عليه هذا التخطيط.
|
||||
* يُعرّف كل `tab` قسمًا من الصفحة مع `title` و`position` و`layoutMode` (`CANVAS` لتخطيط حرّ).
|
||||
* يمكن لكل `widget` داخل لسان أن يعرض [front component](/l/ar/developers/extend/apps/layout/front-components)، أو قائمة علاقات، أو أنواعًا أخرى من عناصر الواجهة (widgets) المدمجة.
|
||||
* `position` على الألسنة يتحكّم في ترتيبها. استخدم قيمًا أعلى (مثل 50) لوضع الألسنة المخصّصة بعد الألسنة المدمجة.
|
||||
|
||||
## definePageLayoutTab
|
||||
|
||||
استخدم هذا عندما تريد فقط **إضافة** لسان إلى تخطيط موجود — على سبيل المثال، لسان تحليلات في صفحة Company القياسية، أو لسان ملخص بالذكاء الاصطناعي مرفق بتخطيط الكائن الخاص بك.
|
||||
|
||||
```ts src/page-layouts/example-extra-tab.ts
|
||||
import {
|
||||
definePageLayoutTab,
|
||||
PageLayoutTabLayoutMode,
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk/define';
|
||||
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
||||
|
||||
export default definePageLayoutTab({
|
||||
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
|
||||
pageLayoutUniversalIdentifier:
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage
|
||||
.universalIdentifier,
|
||||
title: 'Hello World',
|
||||
position: 1000,
|
||||
icon: 'IconWorld',
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
widgets: [
|
||||
{
|
||||
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002',
|
||||
title: 'Hello World',
|
||||
type: 'FRONT_COMPONENT',
|
||||
configuration: {
|
||||
configurationType: 'FRONT_COMPONENT',
|
||||
frontComponentUniversalIdentifier:
|
||||
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### النقاط الرئيسية
|
||||
|
||||
* إن `pageLayoutUniversalIdentifier` **مطلوب** ويجب أن يشير إلى تخطيط صفحة موجود بالفعل وقت التثبيت — سواء كان تخطيط Twenty قياسيًا أو تخطيطًا معرّفًا بواسطة تطبيقك الخاص. المراجع المتقاطعة بين التطبيقات إلى التخطيطات المملوكة لتطبيق آخر مُثبَّت غير مدعومة حاليًا. عند فقدان التخطيط الأب، يفشل التثبيت مع ظهور خطأ تحقق واضح.
|
||||
|
||||
* بالنسبة لتخطيطات Twenty القياسية، استورد المعرفات من `twenty-sdk/define`:
|
||||
|
||||
```ts
|
||||
import { STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
|
||||
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.personRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.opportunityRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.noteRecordPage.universalIdentifier
|
||||
// …
|
||||
```
|
||||
|
||||
كل إدخال تخطيط يوفّر أيضًا `tabs` الخاصة به و`widgets` التابعة لها، بحيث يمكنك الرجوع إلى أي مستوى:
|
||||
|
||||
```ts
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.universalIdentifier
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets.fields.universalIdentifier
|
||||
```
|
||||
|
||||
يتوفر أيضًا اسم قصير `STANDARD_PAGE_LAYOUT`:
|
||||
|
||||
```ts
|
||||
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
|
||||
|
||||
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
|
||||
```
|
||||
|
||||
* يكون نطاق `widgets` مقتصرًا على هذا اللسان فقط — فهي تشير إلى [front components](/l/ar/developers/extend/apps/layout/front-components)، والعروض، وما إلى ذلك تمامًا مثل عناصر الواجهة (widgets) المُعرَّفة مضمّنة داخل `definePageLayout`.
|
||||
|
||||
* `position` يتحكّم في الترتيب مقارنةً بعلامات التبويب الموجودة على التخطيط المستهدف. اختر قيمة تضع علامة التبويب الخاصة بك في الموضع الذي تريده بالنسبة إلى علامات التبويب المضمنة.
|
||||
|
||||
* استخدم هذا بدلًا من `definePageLayout` عندما تريد فقط الإضافة إلى تخطيط موجود. استخدم `definePageLayout` عندما تملك التخطيط بالكامل.
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: العروض
|
||||
description: قم بتوفير عروض محفوظة مُعدّة مسبقًا — ترتيب الأعمدة، المرشّحات، المجموعات — للكائنات في تطبيقك.
|
||||
icon: list
|
||||
---
|
||||
|
||||
يُعد **العرض** تكوينًا محفوظًا لكيفية عرض سجلات كائن معيّن: ما هي الحقول التي تظهر، وترتيبها، وما إذا كانت مرئية، وأي عوامل تصفية أو مجموعات مُطبَّقة. استخدم `defineView()` لتوفير عروض مُعدّة مسبقًا مع تطبيقك — عادةً عرض فهرس افتراضي لكل كائن مخصص تقوم بإنشائه.
|
||||
|
||||
```ts src/views/example-view.ts
|
||||
import { defineView, ViewKey } from 'twenty-sdk/define';
|
||||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
||||
import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
||||
|
||||
export default defineView({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'All example items',
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
icon: 'IconList',
|
||||
key: ViewKey.INDEX,
|
||||
position: 0,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
|
||||
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## النقاط الرئيسية
|
||||
|
||||
* `objectUniversalIdentifier` يحدّد الكائن الذي ينطبق عليه هذا العرض. يمكن أن يكون كائنًا مخصصًا قمتَ بتعريفه أو كائن Twenty قياسيًا.
|
||||
* يحدّد `key` نوع العرض — يمثّل `ViewKey.INDEX` عرض القائمة الرئيسي للكائن.
|
||||
* يتحكّم `fields` في الأعمدة التي تظهر وترتيبها. يشير كل حقل إلى `fieldMetadataUniversalIdentifier`.
|
||||
* يمكنك أيضًا تعريف `filters` و`filterGroups` و`groups` و`fieldGroups` لتكوينات أكثر تقدمًا.
|
||||
* يتحكّم `position` في الترتيب عند وجود عدة عروض لنفس الكائن.
|
||||
|
||||
## الفلاتر
|
||||
|
||||
يمكن أن تأتي طريقة العرض مع عوامل تصفية مُطبَّقة مسبقًا. لكل عامل تصفية ثلاثة مكونات: **الحقل** الذي تُطبَّق عليه التصفية، و**المعامل** (كيفية المقارنة)، و**القيمة** (ما تتم المقارنة به). يجب أن تتطابق العناصر الثلاثة جميعًا — حيث سيتم رفض استخدام معامل لا ينطبق على نوع الحقل في وقت المزامنة.
|
||||
|
||||
```ts
|
||||
import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
|
||||
filters: [
|
||||
{
|
||||
universalIdentifier: '...',
|
||||
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: ['ACTIVE'],
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
### المعاملات المدعومة حسب نوع الحقل
|
||||
|
||||
| نوع الحقل | العوامل المدعومة |
|
||||
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `BOOLEAN` | `IS` |
|
||||
| `UUID` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `TS_VECTOR` | `VECTOR_SEARCH` |
|
||||
|
||||
> يمكن لأنواع الحقول ذات الأسماء المتشابهة أن تستخدم عوامل مختلفة تمامًا — حيث يُعد `SELECT` و`MULTI_SELECT` حالة شائعة.
|
||||
|
||||
### شكل القيمة لكل عامل
|
||||
|
||||
حقل `value` هو دائمًا قيمة قابلة للتسلسل إلى JSON، لكن شكله المتوقَّع يعتمد على العامل:
|
||||
|
||||
| عائلة العامل | شكل القيمة | مثال |
|
||||
| ------------------------------------------------------- | -------------------------------------- | ------------------------ |
|
||||
| `IS`, `IS_NOT` على `SELECT` | مصفوفة من مفاتيح الخيارات (سلاسل نصية) | `['ACTIVE', 'PENDING']` |
|
||||
| `CONTAINS`, `DOES_NOT_CONTAIN` على `MULTI_SELECT` | مصفوفة من مفاتيح الخيارات (سلاسل نصية) | `['TAG_A']` |
|
||||
| `IS`, `IS_NOT` على `RELATION` | مصفوفة من معرّفات السجلات (uuids) | `['c5a1...']` |
|
||||
| `CONTAINS`, `DOES_NOT_CONTAIN` على الحقول المشابهة للنص | سلسلة نصية | `'acme'` |
|
||||
| `IS`, `IS_NOT` على `NUMBER` | سلسلة نصية (القيمة) | `'5'` |
|
||||
| `IS` على `RATING` / `UUID` | سلسلة نصية (القيمة) | `'5'` |
|
||||
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | سلسلة نصية (الحد) | `'10'` |
|
||||
| `IS`, `IS_BEFORE`, `IS_AFTER` على `DATE` / `DATE_TIME` | سلسلة نصية بتنسيق ISO 8601 | `'2025-01-01T00:00:00Z'` |
|
||||
| `IS_EMPTY`, `IS_NOT_EMPTY` | سلسلة فارغة | `''` |
|
||||
| `IS` على `BOOLEAN` | `'true'` أو `'false'` | `'true'` |
|
||||
|
||||
## كيفية ظهور العروض في واجهة المستخدم
|
||||
|
||||
لا يمكن الوصول إلى العرض بمفرده من الشريط الجانبي. لجعله يظهر هناك، قم بربطه مع [عنصر قائمة تنقّل](/l/ar/developers/extend/apps/layout/navigation-menu-items) من النوع `VIEW` يشير إلى قيمة `universalIdentifier` الخاصة بالعرض. هذا هو النمط القياسي: عادةً ما يوفّر كل كائن مخصص عرضًا افتراضيًا + إدخالًا في الشريط الجانبي يفتحه.
|
||||
@@ -0,0 +1,192 @@
|
||||
---
|
||||
title: الاتصالات
|
||||
description: اسمح لتطبيقك بالتصرف نيابةً عن المستخدم في خدمات الجهات الخارجية عبر OAuth.
|
||||
icon: plug
|
||||
---
|
||||
|
||||
الاتصالات هي بيانات اعتماد يحتفظ بها المستخدم لخدمة خارجية (Linear وGitHub وSlack، ...). يحدّد تطبيقك **كيف** يتم الحصول على تلك بيانات الاعتماد — **موفّر اتصال** — ويستخدمها وقت التشغيل لإجراء استدعاءات مُصادَقة إلى واجهة برمجة تطبيقات الطرف الثالث.
|
||||
|
||||
حاليًا لا يُدعَم سوى OAuth 2.0. ستندمج الأنواع المستقبلية من بيانات الاعتماد (رموز الوصول الشخصية، مفاتيح API، المصادقة الأساسية) مع نفس الواجهة — التطبيقات التي تستخدم بالفعل `defineConnectionProvider({ type: 'oauth', ... })` لن تحتاج إلى الترحيل.
|
||||
|
||||
<AccordionGroup>
|
||||
|
||||
<Accordion title="defineConnectionProvider" description="عرّف كيفية الحصول على اتصالات تطبيقك">
|
||||
|
||||
يصف موفّر الاتصال عملية المصافحة الخاصة بـ OAuth التي يحتاجها تطبيقك. ينقر المستخدم على "إضافة اتصال" في إعدادات تطبيقك، ويُكمل شاشة موافقة المزوّد، ثم يتم إنشاء صف `ConnectedAccount` في مساحة عمله.
|
||||
|
||||
يتطلّب الإعداد العملي **ملفّين** — موفّر الاتصال، وتصريح `serverVariables` مطابق في `defineApplication` يحتفظ ببيانات اعتماد عميل OAuth.
|
||||
|
||||
```ts src/connection-providers/linear-connection.ts
|
||||
import { defineConnectionProvider } from 'twenty-sdk/define';
|
||||
|
||||
export default defineConnectionProvider({
|
||||
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
|
||||
name: 'linear',
|
||||
displayName: 'Linear',
|
||||
icon: 'IconBrandLinear',
|
||||
type: 'oauth',
|
||||
oauth: {
|
||||
authorizationEndpoint: 'https://linear.app/oauth/authorize',
|
||||
tokenEndpoint: 'https://api.linear.app/oauth/token',
|
||||
scopes: ['read', 'write'],
|
||||
// These must match keys in `defineApplication.serverVariables` below.
|
||||
clientIdVariable: 'LINEAR_CLIENT_ID',
|
||||
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
|
||||
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
|
||||
// 'form-urlencoded' for the token request.
|
||||
tokenRequestContentType: 'form-urlencoded',
|
||||
// Optional: defaults to true. Disable only if the provider rejects PKCE.
|
||||
usePkce: false,
|
||||
// Optional: extra query params on the authorize URL.
|
||||
// authorizationParams: { prompt: 'consent' },
|
||||
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
|
||||
// revokeEndpoint: 'https://example.com/oauth/revoke',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/application.config.ts
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '...',
|
||||
displayName: 'Linear',
|
||||
description: 'Connect Linear to Twenty.',
|
||||
// OAuth client credentials live on the app registration (one OAuth app per
|
||||
// Twenty server, configured by the admin) — not per-workspace. Declare them
|
||||
// as serverVariables so the admin can fill them in once for all installs.
|
||||
serverVariables: {
|
||||
LINEAR_CLIENT_ID: {
|
||||
description: 'OAuth client ID from your Linear OAuth application.',
|
||||
isSecret: false,
|
||||
isRequired: true,
|
||||
},
|
||||
LINEAR_CLIENT_SECRET: {
|
||||
description: 'OAuth client secret from your Linear OAuth application.',
|
||||
isSecret: true,
|
||||
isRequired: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
|
||||
* `name` هي سلسلة المعرّف الفريدة المستخدمة في `listConnections({ providerName })` (بصيغة kebab-case، ويجب أن تطابق `^[a-z][a-z0-9-]*$`).
|
||||
* `displayName` يظهر في علامة تبويب إعدادات كل تطبيق وفي قائمة أدوات الذكاء الاصطناعي.
|
||||
* `clientIdVariable` / `clientSecretVariable` هي **أسماء**، وليست قيماً — ويجب أن تطابق المفاتيح المصرَّح بها في `defineApplication.serverVariables`. يُدخِل مسؤول الخادم القيم الفعلية `client_id` و`client_secret` عبر واجهة تسجيل التطبيق، ولا تُضمَّن أبدًا في مستودعك.
|
||||
* استخدم `serverVariables` (وليس `applicationVariables`) — بيانات اعتماد OAuth على مستوى الخادم، ويوجد تطبيق OAuth واحد لكل خادم Twenty.
|
||||
* إلى أن يتم ملء كلا `serverVariables`، تعرض علامة تبويب إعدادات كل تطبيق تلميح "بحاجة إلى مسؤول الخادم" ويكون زر "إضافة اتصال" معطّلًا.
|
||||
* `type: 'oauth'` هي القيمة الوحيدة المدعومة حاليًا. المميِّز متوافق مع الإصدارات المستقبلية: الأنواع المستقبلية (`'pat'`، `'api-key'`، ...) ستضيف كُتل تهيئة فرعية جديدة إلى جانب `oauth`.
|
||||
|
||||
عنوان URL لردّ النداء الخاص بـ OAuth الذي يحتاج موفّرك إلى إضافته إلى قائمة السماح هو:
|
||||
|
||||
```
|
||||
https://<your-twenty-server>/auth/apps/callback
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="listConnections / getConnection" description="استخدم الاتصالات من دالة منطقية">
|
||||
|
||||
داخل معالج دالة منطقية، تُرجِع `listConnections({ providerName })` صفوف `ConnectedAccount` الخاصة بهذا التطبيق للمزوّد المحدَّد، مع رموز وصول محدَّثة.
|
||||
|
||||
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
|
||||
import { listConnections } from 'twenty-sdk/logic-function';
|
||||
|
||||
export const createLinearIssueHandler = async (input: {
|
||||
teamId?: string;
|
||||
title?: string;
|
||||
}) => {
|
||||
if (!input.teamId || !input.title) {
|
||||
return { success: false, error: 'teamId and title are required' };
|
||||
}
|
||||
|
||||
const connections = await listConnections({ providerName: 'linear' });
|
||||
|
||||
// Workspace-shared credentials win when present; fall back to the first
|
||||
// user-visibility one. For HTTP-route triggers you typically pick the
|
||||
// request user's connection via event.userWorkspaceId instead.
|
||||
const connection =
|
||||
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
|
||||
|
||||
if (!connection) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'Linear is not connected. Open the app settings and click "Add connection".',
|
||||
};
|
||||
}
|
||||
|
||||
// Use connection.accessToken to call the third-party API.
|
||||
const response = await fetch('https://api.linear.app/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${connection.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
|
||||
}),
|
||||
});
|
||||
|
||||
return { success: response.ok };
|
||||
};
|
||||
```
|
||||
|
||||
يحتوي كل اتصال على:
|
||||
|
||||
| الحقل | الوصف |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| `id` | معرّف صف فريد؛ مرّره إلى `getConnection(id)` لإعادة جلب واحد فقط |
|
||||
| `visibility` | `'user'` (خاص بعضو واحد في مساحة العمل) أو `'workspace'` (مشترك مع جميع الأعضاء) |
|
||||
| `scopes` | أذونات OAuth الممنوحة من قِبل المزوّد الأصلي (مختلفة عن `visibility` — ولا علاقة لها به) |
|
||||
| `userWorkspaceId` | معرّف userWorkspace للمالك — مفيد لاختيار "اتصال مستخدم الطلب" في مشغّلات مسارات HTTP |
|
||||
| `accessToken` | رمز وصول OAuth حديث (يُحدَّث تلقائيًا إذا انتهت صلاحيته) |
|
||||
| `name` / `handle` | الاسم المعروض للاتصال (يُستمد تلقائيًا عند ردّ نداء OAuth، وقابل لإعادة التسمية من قِبل المستخدم) |
|
||||
| `authFailedAt` | يُضبط عند فشل أحدث عملية تحديث؛ يجب على المستخدم إعادة الاتصال |
|
||||
|
||||
النقاط الرئيسية:
|
||||
|
||||
* مرّر `{ providerName }` للتصفية حسب المزوّد؛ واحذفه للحصول على كل الاتصالات التي يملكها هذا التطبيق عبر جميع المزوّدين.
|
||||
* يقوم الخادم بتحديث رمز الوصول بشفافية قبل الإرجاع. يرى معالجك دائمًا رمزًا صالحًا للاستخدام (أو سيكون `authFailedAt` مُعيّنًا).
|
||||
* `getConnection(id)` هي المعادِل لصف واحد.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="رؤية خاصة بالمستخدم مقابل المشاركة على مستوى مساحة العمل" description="كيف يختار المستخدمون بين بيانات اعتماد خاصة ومشتركة">
|
||||
|
||||
عند نقر المستخدم "إضافة اتصال"، سيُطلب منه اختيار مستوى الرؤية:
|
||||
|
||||
* **لي فقط** — بيانات الاعتماد خاصة بالمستخدم الذي قام بالاتصال. ستتمكّن أي دالة منطقية تُستدعى بالنيابة عنه (مشغّل مسار HTTP مع `isAuthRequired: true`) من رؤيتها؛ أمّا مشغّلات cron وأحداث قاعدة البيانات فلا.
|
||||
* **مشتركة على مستوى مساحة العمل** — يمكن لأي عضو في مساحة العمل استخدام بيانات الاعتماد. يمكن لمشغّلات cron/قاعدة البيانات رؤيتها أيضًا، لأنها لا تملك مستخدم طلب.
|
||||
|
||||
استخدم الخيار المناسب لكل معالج:
|
||||
|
||||
```ts
|
||||
// HTTP-route trigger — prefer the request user's own connection.
|
||||
const conn =
|
||||
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
|
||||
connections.find((c) => c.visibility === 'workspace');
|
||||
|
||||
// Cron trigger — no request user; only shared credentials are sensible.
|
||||
const conn = connections.find((c) => c.visibility === 'workspace');
|
||||
```
|
||||
|
||||
يُسمح بوجود اتصالات متعددة لكل (مستخدم، مزوّد)، لذا يمكن للمستخدم نفسه امتلاك "Linear شخصي" و"Linear للعمل" جنبًا إلى جنب.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="إعداد المزوّد لمرة واحدة" description="سجّل تطبيق OAuth الخاص بك لدى خدمة الطرف الثالث">
|
||||
|
||||
بالنسبة لكل موفّر اتصال، يحتاج مسؤول الخادم أولًا إلى تسجيل تطبيق OAuth لدى الطرف الثالث.
|
||||
|
||||
1. انتقل إلى إعدادات المطوّر لدى المزوّد (مثل https://linear.app/settings/api/applications/new).
|
||||
2. عيّن **Redirect URI** إلى `\<SERVER_URL>/auth/apps/callback`.
|
||||
3. انسخ **Client ID** و**Client Secret** المُنشأين.
|
||||
4. افتح التطبيق المُثبَّت في Twenty كمسؤول خادم → عيّن القيم على `serverVariables` المقابلة.
|
||||
5. بعد ذلك، يمكن لأعضاء مساحة العمل إضافة الاتصالات من قسم **الاتصالات** الخاص بكل تطبيق.
|
||||
|
||||
</Accordion>
|
||||
|
||||
</AccordionGroup>
|
||||
@@ -0,0 +1,514 @@
|
||||
---
|
||||
title: الوظائف المنطقية
|
||||
description: عرّف دوال TypeScript على جانب الخادم مع HTTP وcron ومشغّلات أحداث قاعدة البيانات.
|
||||
icon: bolt
|
||||
---
|
||||
|
||||
دوال المنطق هي دوال TypeScript على جانب الخادم تعمل على منصة Twenty. يمكن تشغيلها بواسطة طلبات HTTP أو جداول cron أو أحداث قاعدة البيانات — كما يمكن إتاحتها كأدوات لوكلاء الذكاء الاصطناعي.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="defineLogicFunction" description="عرّف الدوال المنطقية ومشغّلاتها">
|
||||
|
||||
كل ملف وظيفة يستخدم `defineLogicFunction()` لتصدير تكوين مع معالج ومشغّلات اختيارية.
|
||||
|
||||
```ts src/logic-functions/createPostCard.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const handler = async (params: RoutePayload) => {
|
||||
const client = new CoreApiClient();
|
||||
const body = (params.body ?? {}) as { name?: string };
|
||||
const name = body.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world';
|
||||
|
||||
const result = await client.mutation({
|
||||
createPostCard: {
|
||||
__args: { data: { name } },
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'create-new-post-card',
|
||||
timeoutSeconds: 2,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/post-card/create',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
/*databaseEventTriggerSettings: {
|
||||
eventName: 'people.created',
|
||||
},*/
|
||||
/*cronTriggerSettings: {
|
||||
pattern: '0 0 1 1 *',
|
||||
},*/
|
||||
});
|
||||
```
|
||||
|
||||
أنواع المشغّلات المتاحة:
|
||||
* **httpRoute**: يعرِض وظيفتك على مسار وطريقة HTTP **تحت نقطة النهاية `/s/`**:
|
||||
> مثال: `path: '/post-card/create'` يمكن استدعاؤه عبر `https://your-twenty-server.com/s/post-card/create`
|
||||
|
||||
<Note>
|
||||
لاستدعاء دالة منطقية يتم تشغيلها بواسطة مسار من مكون واجهة (بدون واجهة رسومية)، راجع قسم [استدعاء دالة منطقية](/l/ar/developers/extend/apps/layout/front-components#calling-a-logic-function).
|
||||
</Note>
|
||||
* **cron**: يشغّل وظيفتك على جدول باستخدام تعبير CRON.
|
||||
* **databaseEvent**: يعمل على أحداث دورة حياة كائنات مساحة العمل. عندما تكون عملية الحدث هي `updated`، يمكن تحديد الحقول المحددة المراد الاستماع إليها في مصفوفة `updatedFields`. إذا تُركت غير معرّفة أو فارغة، فسيؤدي أي تحديث إلى تشغيل الدالة.
|
||||
> مثال: `person.updated`، `*.created`، `company.*`
|
||||
|
||||
<Note>
|
||||
يمكنك أيضًا تنفيذ دالة يدويًا باستخدام CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:function:exec -n create-new-post-card -p '{"key": "value"}'
|
||||
```
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
```
|
||||
|
||||
يمكنك متابعة السجلات باستخدام:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:function:logs
|
||||
```
|
||||
</Note>
|
||||
|
||||
#### حمولة مشغل المسار
|
||||
|
||||
عندما يستدعي مُشغِّل المسار وظيفتك المنطقية، فإنها تتلقّى كائن `RoutePayload` الذي يتبع [صيغة AWS HTTP API v2](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html).
|
||||
استورد نوع `RoutePayload` من `twenty-sdk/logic-function`:
|
||||
|
||||
```ts
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const { headers, queryStringParameters, pathParameters, body } = event;
|
||||
const { method, path } = event.requestContext.http;
|
||||
|
||||
return { message: 'Success' };
|
||||
};
|
||||
```
|
||||
|
||||
يحتوي نوع `RoutePayload` على البنية التالية:
|
||||
|
||||
| الخاصية | النوع | الوصف | مثال |
|
||||
| ---------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `headers` | `Record\<string, string \| undefined>` | رؤوس HTTP (فقط تلك المدرجة في `forwardedRequestHeaders`) | انظر القسم أدناه |
|
||||
| `queryStringParameters` | `Record\<string, string \| undefined>` | معلمات سلسلة الاستعلام (تُضمّ القيم المتعددة باستخدام فواصل) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
|
||||
| `pathParameters` | `Record\<string, string \| undefined>` | معلمات المسار المستخرجة من نمط المسار | `/users/:id`, `/users/123` -> `{ id: '123' }` |
|
||||
| `body` | `object \| null` | جسم الطلب المُحلَّل (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||||
| `rawBody` | `string \| undefined` | نص الطلب الأصلي بترميز UTF-8، قبل تحليل JSON. مفيد للتحقق من تواقيع خطافات الويب على نمط HMAC (مثل `X-Hub-Signature-256` الخاص بـ GitHub وStripe). `undefined` عندما لم يحتفظ وقت التشغيل بها. | |
|
||||
| `isBase64Encoded` | `boolean` | ما إذا كان جسم الطلب مُرمَّزًا بترميز base64 | |
|
||||
| `requestContext.http.method` | `string` | طريقة HTTP (GET, POST, PUT, PATCH, DELETE) | |
|
||||
| `requestContext.http.path` | `string` | المسار الخام للطلب | |
|
||||
|
||||
|
||||
#### forwardedRequestHeaders
|
||||
|
||||
افتراضيًا، **لا** تُمرَّر رؤوس HTTP من الطلبات الواردة إلى دالتك المنطقية لأسباب أمنية.
|
||||
للوصول إلى رؤوس محددة، أدرِجها في مصفوفة `forwardedRequestHeaders`:
|
||||
|
||||
```ts
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'webhook-handler',
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/webhook',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: false,
|
||||
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
في معالجك، يمكنك الوصول إلى الرؤوس المُمرَّرة بهذه الطريقة:
|
||||
|
||||
```ts
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const signature = event.headers['x-webhook-signature'];
|
||||
const contentType = event.headers['content-type'];
|
||||
|
||||
// Validate webhook signature...
|
||||
return { received: true };
|
||||
};
|
||||
```
|
||||
|
||||
<Note>
|
||||
تُحوَّل أسماء الرؤوس إلى أحرف صغيرة. يمكنك الوصول إليها باستخدام مفاتيح بأحرف صغيرة (على سبيل المثال، `event.headers['content-type']`).
|
||||
</Note>
|
||||
|
||||
#### استجابة HTTP مخصصة
|
||||
|
||||
بشكل افتراضي، فإن إرجاع قيمة بسيطة من المعالج الخاص بك يعيدها كاستجابة `200` (بصيغة JSON للكائنات و`text/plain` للسلاسل النصية). للتحكم في رمز الحالة ورؤوس الاستجابة، أعد كائن `Response` من `twenty-sdk/logic-function`:
|
||||
|
||||
```ts
|
||||
import { Response } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
return new Response('<h1>Hello</h1>', {
|
||||
status: 201,
|
||||
headers: { 'content-type': 'text/html' },
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
لأسباب أمنية، يتم تقييد ترويسات الاستجابة بقائمة مسموح بها. يتم إسقاط أي ترويسة ليست في القائمة (مثل `Set-Cookie`، وترويسات CORS مثل `Access-Control-Allow-Origin`، أو ترويسات `X-*` المخصصة) بصمت قبل إرسال الاستجابة. ترويسات الاستجابة المسموح بها هي:
|
||||
|
||||
* `content-type`
|
||||
* `content-language`
|
||||
* `content-disposition`
|
||||
* `cache-control`
|
||||
* `retry-after`
|
||||
|
||||
<Note>
|
||||
يجب أن يكون رمز الحالة رمز حالة HTTP صالحًا (بين 100 و599). تتم مطابقة أسماء ترويسات الاستجابة دون حساسية لحالة الأحرف.
|
||||
</Note>
|
||||
|
||||
#### حمولة مُحفِّز حدث قاعدة البيانات
|
||||
|
||||
عندما يستدعي مُحفِّز حدث قاعدة البيانات دالة المنطق الخاصة بك، فإنه يستقبل كائن `DatabaseEventPayload` واحدًا لكل سجل تم تغييره. تجمع الحمولة بين البيانات الوصفية حول مساحة العمل والكائن المصدر وبين الحدث على مستوى السجل.
|
||||
|
||||
```ts
|
||||
import type {
|
||||
DatabaseEventPayload,
|
||||
ObjectRecordCreateEvent,
|
||||
ObjectRecordDestroyEvent,
|
||||
ObjectRecordUpdateEvent,
|
||||
} from 'twenty-sdk/logic-function';
|
||||
|
||||
type Person = {
|
||||
id: string;
|
||||
emails?: { primaryEmail?: string };
|
||||
};
|
||||
```
|
||||
|
||||
تتضمن الحمولة ما يلي:
|
||||
|
||||
| الخاصية | الوصف |
|
||||
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
|
||||
| `name` | اسم الحدث، مثل `person.updated`. |
|
||||
| `workspaceId` | مساحة العمل التي وقع فيها الحدث. |
|
||||
| `objectMetadata` | بيانات وصفية للكائن الذي تم تغييره. |
|
||||
| `recordId` | معرّف السجل الذي تم تغييره. |
|
||||
| `userId`, `userWorkspaceId`, `workspaceMemberId` | حقول الفاعل عندما يكون الحدث ناتجًا عن مستخدم في مساحة العمل. |
|
||||
| `properties` | بيانات السجل الخاصة بالحدث، مع `before` و`after` و`diff` و`updatedFields` اعتمادًا على العملية. |
|
||||
|
||||
| حدث | بيانات السجل |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------------------------- |
|
||||
| `person.created` | `event.properties.after` |
|
||||
| `person.updated` | `event.properties.before`, `event.properties.after`, `event.properties.diff`, `event.properties.updatedFields` |
|
||||
| `person.destroyed` | `event.properties.before` |
|
||||
|
||||
في عمليات الحذف اللين (soft deletes)، يتبع `.deleted` بنية نمط التحديث لأن حقل `deletedAt` في السجل يتغيّر.
|
||||
في عمليات الحذف الدائم، استخدم `.destroyed`.
|
||||
|
||||
<Note>
|
||||
`databaseEventTriggerSettings.updatedFields` يرشّح أيّ أحداث التحديث التي تُشغِّل الدالة.
|
||||
`event.properties.updatedFields` يوضّح لك أي الحقول تغيّرت فعليًا في الحدث الحالي.
|
||||
</Note>
|
||||
|
||||
مثال على حدث الإنشاء:
|
||||
|
||||
```ts
|
||||
type PersonCreatedEvent = DatabaseEventPayload<
|
||||
ObjectRecordCreateEvent<Person>
|
||||
>;
|
||||
|
||||
const handler = async (event: PersonCreatedEvent) => {
|
||||
const person = event.properties.after;
|
||||
|
||||
return {
|
||||
personId: event.recordId,
|
||||
email: person.emails?.primaryEmail,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
مثال على حدث التحديث:
|
||||
|
||||
```ts
|
||||
type PersonUpdatedEvent = DatabaseEventPayload<
|
||||
ObjectRecordUpdateEvent<Person>
|
||||
>;
|
||||
|
||||
const handler = async (event: PersonUpdatedEvent) => {
|
||||
const { before, after, diff, updatedFields } = event.properties;
|
||||
|
||||
return {
|
||||
personId: event.recordId,
|
||||
updatedFields,
|
||||
previousEmail: before.emails?.primaryEmail,
|
||||
currentEmail: after.emails?.primaryEmail,
|
||||
emailDiff: diff.emails,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
تشغيل المشغّل فقط عند تحديثات البريد الإلكتروني:
|
||||
|
||||
```ts
|
||||
export default defineLogicFunction({
|
||||
...,
|
||||
databaseEventTriggerSettings: {
|
||||
eventName: 'person.updated',
|
||||
updatedFields: ['emails'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
مثال على حدث الحذف:
|
||||
|
||||
```ts
|
||||
type PersonDestroyedEvent = DatabaseEventPayload<
|
||||
ObjectRecordDestroyEvent<Person>
|
||||
>;
|
||||
|
||||
const handler = async (event: PersonDestroyedEvent) => {
|
||||
const personBeforeDestroy = event.properties.before;
|
||||
|
||||
return {
|
||||
personId: event.recordId,
|
||||
email: personBeforeDestroy.emails?.primaryEmail,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
#### إتاحة دالة كأداة ذكاء اصطناعي أو كإجراء ضمن سير العمل
|
||||
|
||||
يمكن إتاحة دوال المنطق على واجهتين، ولكلٍ منهما مشغِّل خاص به:
|
||||
|
||||
* **`toolTriggerSettings`** — يجعل الدالة قابلة للاكتشاف عبر ميزات الذكاء الاصطناعي الخاصة بـ Twenty (الدردشة، MCP، استدعاء الدوال). يستخدم JSON Schema القياسي، وهو التنسيق الذي تفهمه LLMs أصلاً.
|
||||
* **`workflowActionTriggerSettings`** — يجعل الدالة تظهر كخطوة في منشئ سير العمل المرئي. يستخدم `InputSchema` الغني الخاص بـ Twenty لكي يتمكن المُنشئ من عرض محرّرات الحقول المناسبة، وأدوات انتقاء المتغيّرات، والتسميات.
|
||||
|
||||
يمكن للدالة اختيار أحدهما، أو الآخر، أو كليهما. توجد جنبًا إلى جنب مع `cronTriggerSettings` و`databaseEventTriggerSettings` و`httpRouteTriggerSettings` — النمط نفسه، والشكل نفسه.
|
||||
|
||||
```ts src/logic-functions/enrich-company.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const handler = async (params: { companyName: string; domain?: string }) => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
const result = await client.mutation({
|
||||
createTask: {
|
||||
__args: {
|
||||
data: {
|
||||
title: `Enrich data for ${params.companyName}`,
|
||||
body: `Domain: ${params.domain ?? 'unknown'}`,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { taskId: result.createTask.id };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
|
||||
name: 'enrich-company',
|
||||
description: 'Enrich a company record with external data',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
toolTriggerSettings: {},
|
||||
});
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
|
||||
* يمكن للدالة مزج الواجهات — صرِّح بكلٍ من `toolTriggerSettings` و`workflowActionTriggerSettings` لإتاحتها في الدردشة وفي منشئ سير العمل.
|
||||
* `toolTriggerSettings.inputSchema` و`workflowActionTriggerSettings.inputSchema` كلاهما اختياري. عند الإغفال، يستنتج مُنشئ البيان هذه المخططات من الشيفرة المصدرية للمعالج (JSON Schema لأداة الذكاء الاصطناعي، و`InputSchema` الخاصة بـ Twenty لإجراء سير العمل). قدّم واحدًا صراحةً عندما ترغب في أنواع أكثر ثراءً — على سبيل المثال، مع حقول واعية بـ `FieldMetadataType` مثل `CURRENCY` أو `RELATION` لمنشئ سير العمل، أو مع حقول `description` التي يمكن لوكيل الذكاء الاصطناعي قراءتها:
|
||||
|
||||
```ts
|
||||
export default defineLogicFunction({
|
||||
...,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company to enrich',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The company website domain (optional)',
|
||||
},
|
||||
},
|
||||
required: ['companyName'],
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
**اكتب `description` جيدًا.** يعتمد وكلاء الذكاء الاصطناعي على حقل `description` الخاص بالدالة لتحديد وقت استخدام الأداة. كن محددًا بشأن ما تفعله الأداة ومتى ينبغي استدعاؤها.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
<Note>
|
||||
**خطافات التثبيت** — معالجات ما قبل التثبيت وما بعد التثبيت — تشترك في وقت التشغيل نفسه، ولكن يُصرَّح عنها بدوال تعريف خاصة بها ولا تأخذ إعدادات المشغّلات. راجع [خطافات التثبيت (Install Hooks)](/l/ar/developers/extend/apps/config/install-hooks) لمعرفة `definePreInstallLogicFunction` و `definePostInstallLogicFunction`.
|
||||
</Note>
|
||||
|
||||
## عملاء واجهة برمجة تطبيقات مضبوطة الأنواع (`twenty-client-sdk`)
|
||||
|
||||
توفر حزمة `twenty-client-sdk` عميلين لـ GraphQL ذوي أنواع ثابتة للتفاعل مع واجهة Twenty البرمجية من وظائفك المنطقية ومكوّنات الواجهة الأمامية.
|
||||
|
||||
| العميل | استيراد | نقطة النهاية | مُولَّد؟ |
|
||||
| ------------------- | ---------------------------- | --------------------------------------------------- | -------------------------- |
|
||||
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — بيانات مساحة العمل (السجلات، الكائنات) | نعم، في وقت التطوير/البناء |
|
||||
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — تكوين مساحة العمل، رفع الملفات | لا، يأتي مُجهزًا مسبقًا |
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="CoreApiClient" description="استعلام وتعديل بيانات مساحة العمل (السجلات، الكائنات)">
|
||||
|
||||
`CoreApiClient` هو العميل الرئيسي للاستعلام وتعديل بيانات مساحة العمل. يُولَّد **من مخطط مساحة العمل لديك** أثناء `yarn twenty dev` أو `yarn twenty dev:build`، لذا فهو مضبوط الأنواع بالكامل ليتوافق مع كائناتك وحقولك.
|
||||
|
||||
```ts
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const client = new CoreApiClient();
|
||||
|
||||
// Query records
|
||||
const { companies } = await client.query({
|
||||
companies: {
|
||||
edges: {
|
||||
node: {
|
||||
id: true,
|
||||
name: true,
|
||||
domainName: {
|
||||
primaryLinkLabel: true,
|
||||
primaryLinkUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Create a record
|
||||
const { createCompany } = await client.mutation({
|
||||
createCompany: {
|
||||
__args: {
|
||||
data: {
|
||||
name: 'Acme Corp',
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
يستخدم العميل صياغة مجموعة اختيار: مرِّر `true` لتضمين حقل، واستخدم `__args` للوسيطات، وعشّش الكائنات للعلاقات. ستحصل على إكمال تلقائي كامل وفحص للأنواع يعتمد على مخطط مساحة العمل لديك.
|
||||
|
||||
<Note>
|
||||
**يتم توليد CoreApiClient في وقت التطوير/البناء.** إذا استخدمته دون تشغيل `yarn twenty dev` أو `yarn twenty dev:build` أولًا، فسيؤدي ذلك إلى خطأ. تحدث عملية التوليد تلقائيًا — إذ يستطلع CLI مخطط GraphQL لمساحة عملك وينشئ عميلًا مضبوط الأنواع باستخدام `@genql/cli`.
|
||||
</Note>
|
||||
|
||||
#### استخدام CoreSchema للتعليقات التوضيحية للأنواع
|
||||
|
||||
`CoreSchema` يوفّر أنواع TypeScript المطابقة لكائنات مساحة العمل لديك — مفيد لتعيين أنواع حالة المكوّن أو معاملات الدوال:
|
||||
|
||||
```ts
|
||||
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
|
||||
import { useState } from 'react';
|
||||
|
||||
const [company, setCompany] = useState<
|
||||
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
|
||||
>(undefined);
|
||||
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.query({
|
||||
company: {
|
||||
__args: { filter: { position: { eq: 1 } } },
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
setCompany(result.company);
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="MetadataApiClient" description="إعدادات مساحة العمل، والتطبيقات، ورفع الملفات">
|
||||
|
||||
يأتي `MetadataApiClient` مُجهّزًا مسبقًا مع SDK (لا حاجة للتوليد). يستعلم عن نقطة النهاية `/metadata` للحصول على تكوين مساحة العمل والتطبيقات ورفع الملفات.
|
||||
|
||||
```ts
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
// List first 10 objects in the workspace
|
||||
const { objects } = await metadataClient.query({
|
||||
objects: {
|
||||
edges: {
|
||||
node: {
|
||||
id: true,
|
||||
nameSingular: true,
|
||||
namePlural: true,
|
||||
labelSingular: true,
|
||||
isCustom: true,
|
||||
},
|
||||
},
|
||||
__args: {
|
||||
filter: {},
|
||||
paging: { first: 10 },
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### رفع الملفات
|
||||
|
||||
يتضمن `MetadataApiClient` طريقة `uploadFile` لإرفاق الملفات بالحقول من نوع الملف:
|
||||
|
||||
```ts
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
const fileBuffer = fs.readFileSync('./invoice.pdf');
|
||||
|
||||
const uploadedFile = await metadataClient.uploadFile(
|
||||
fileBuffer, // file contents as a Buffer
|
||||
'invoice.pdf', // filename
|
||||
'application/pdf', // MIME type
|
||||
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
|
||||
);
|
||||
|
||||
console.log(uploadedFile);
|
||||
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
|
||||
```
|
||||
|
||||
| المعلمة | النوع | الوصف |
|
||||
| ---------------------------------- | -------- | ---------------------------------------------------------------------- |
|
||||
| `fileBuffer` | `Buffer` | المحتوى الخام للملف |
|
||||
| `filename` | `string` | اسم الملف (يُستخدم للتخزين والعرض) |
|
||||
| `contentType` | `string` | نوع MIME (القيمة الافتراضية `application/octet-stream` إذا لم يُحدَّد) |
|
||||
| `fieldMetadataUniversalIdentifier` | `string` | قيمة `universalIdentifier` لحقل نوع الملف في كائنك |
|
||||
|
||||
النقاط الرئيسية:
|
||||
* يستخدم `universalIdentifier` الخاص بالحقل (وليس معرّفه الخاص بمساحة العمل)، بحيث يعمل كود الرفع لديك عبر أي مساحة عمل مُثبَّت فيها تطبيقك.
|
||||
* العنوان `url` المُعاد هو عنوان URL موقّع يمكنك استخدامه للوصول إلى الملف المرفوع.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
<Note>
|
||||
عند تشغيل كودك على Twenty (وظائف منطقية أو مكوّنات أمامية)، يقوم النظام الأساسي بحقن بيانات الاعتماد كمتغيرات بيئية:
|
||||
|
||||
* `TWENTY_API_URL` — عنوان URL الأساسي لواجهة Twenty البرمجية
|
||||
* `TWENTY_APP_ACCESS_TOKEN` — مفتاح قصير العمر ذو نطاق يقتصر على الدور الافتراضي لوظيفة تطبيقك
|
||||
|
||||
لست **بحاجة** إلى تمرير هذه القيم إلى العملاء — فهي تُقرأ تلقائيًا من `process.env`. تُحدَّد أذونات مفتاح واجهة برمجة التطبيقات بواسطة الدور المُعلن باستخدام `defineApplicationRole()` (أو المشار إليه عبر `defaultRoleUniversalIdentifier` في `application-config.ts`).
|
||||
</Note>
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: نظرة عامة
|
||||
description: TypeScript على جانب الخادم الذي يعمل داخل Twenty — يتم تشغيله بواسطة مسارات HTTP، وجداول كرون، وأحداث قاعدة البيانات، وأدوات الذكاء الاصطناعي، أو إجراءات سير العمل.
|
||||
icon: bolt
|
||||
---
|
||||
|
||||
**طبقة المنطق** في تطبيق Twenty هي الشيفرة التي *تعمل* — معالِجات TypeScript على جانب الخادم تستجيب لطلبات HTTP، وجداول كرون، وتغييرات السجلات؛ ومهارات ووكلاء الذكاء الاصطناعي التي تعمل داخل مساحة العمل؛ واتصالات OAuth التي تتيح لدوالّك العمل نيابةً عن المستخدم في الخدمات الخارجية.
|
||||
|
||||
```text
|
||||
┌─ HTTP route ──┐
|
||||
│ Cron schedule │
|
||||
│ Database event │ ┌────────────────────┐
|
||||
triggers ─┤ AI tool call ├─────▶│ Logic function │
|
||||
│ Workflow action │ │ (your handler) │
|
||||
│ Manual exec │ └────────────────────┘
|
||||
└────────────────────┘ │
|
||||
▼
|
||||
┌────────────────────────────┐
|
||||
│ Twenty API (records) │
|
||||
│ Third-party API │
|
||||
│ (via Connection token) │
|
||||
└────────────────────────────┘
|
||||
```
|
||||
|
||||
## في هذا القسم
|
||||
|
||||
<CardGroup cols={٢}>
|
||||
<Card title="الوظائف المنطقية" icon="bolt" href="/l/ar/developers/extend/apps/logic/logic-functions">
|
||||
لبنة البناء الأساسية — أنواع المشغلات، والحمولات، وعميل واجهة برمجة التطبيقات ذو الأنواع.
|
||||
</Card>
|
||||
<Card title="المهارات والوكلاء" icon="robot" href="/l/ar/developers/extend/apps/logic/skills-and-agents">
|
||||
تعليمات قابلة لإعادة الاستخدام لوكلاء الذكاء الاصطناعي ومساعدين مع مطالبات نظام مخصّصة.
|
||||
</Card>
|
||||
<Card title="الاتصالات" icon="قابس" href="/l/ar/developers/extend/apps/logic/connections">
|
||||
بيانات اعتماد OAuth التي يحتفظ بها تطبيقك للخدمات الخارجية — مثل Linear وGitHub وSlack وغيرها.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## لمحة عن أنواع المشغلات
|
||||
|
||||
دالة المنطق تختار واحدًا أو أكثر من المشغلات — كل إدخال أدناه هو حقل منفصل في `defineLogicFunction()`:
|
||||
|
||||
| المشغّل | متى يعمل | الإعداد |
|
||||
| ---------------------- | -------------------------------------------------------------- | ------------------------------- |
|
||||
| **مسار HTTP** | طلب يصل إلى نقطة نهاية `/s/\<path>` الخاصة بك | `httpRouteTriggerSettings` |
|
||||
| **كرون** | عند تطابق تعبير CRON | `cronTriggerSettings` |
|
||||
| **حدث قاعدة البيانات** | يتم إنشاء سجل في مساحة العمل أو تحديثه أو حذفه | `databaseEventTriggerSettings` |
|
||||
| **أداة ذكاء اصطناعي** | ميزة ذكاء اصطناعي في Twenty تقرر استدعاء دالتك | `toolTriggerSettings` |
|
||||
| **إجراء سير العمل** | تستدعي خطوة في سير العمل دالتك | `workflowActionTriggerSettings` |
|
||||
|
||||
تعمل الدوال ضمن عمليات Node.js معزولة، وتصل إلى مساحة العمل عبر عميل واجهة برمجة تطبيقات مضبوط الأنواع ومحدّد النطاق بالدور المصرّح عنه في [`defineApplication()`](/l/ar/developers/extend/apps/config/application).
|
||||
|
||||
<Note>
|
||||
**خطافات وقت التثبيت** — الشيفرة التي تعمل قبل التثبيت أو بعده — تشارك بيئة التشغيل هذه ولكنها تستخدم دوال تعريف خاصة بها وتوجد ضمن [Config → Install Hooks](/l/ar/developers/extend/apps/config/install-hooks).
|
||||
</Note>
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
title: المهارات والوكلاء
|
||||
description: عرّف مهارات ووكلاء الذكاء الاصطناعي لتطبيقك.
|
||||
icon: robot
|
||||
---
|
||||
|
||||
<Warning>
|
||||
المهارات والوكلاء حاليًا في مرحلة الألفا. الميزة تعمل لكنها لا تزال قيد التطور.
|
||||
</Warning>
|
||||
|
||||
يمكن للتطبيقات تعريف قدرات ذكاء اصطناعي تعمل داخل مساحة العمل — تعليمات مهارات قابلة لإعادة الاستخدام ووكلاء بموجهات نظام مخصّصة.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="defineSkill" description="عرّف مهارات وكلاء الذكاء الاصطناعي">
|
||||
|
||||
تُحدِّد المهارات تعليمات وإمكانات قابلة لإعادة الاستخدام يمكن لوكلاء الذكاء الاصطناعي استخدامها داخل مساحة العمل لديك. استخدم `defineSkill()` لتعريف مهارات مع تحقّق مدمج:
|
||||
|
||||
```ts src/skills/example-skill.ts
|
||||
import { defineSkill } from 'twenty-sdk/define';
|
||||
|
||||
export default defineSkill({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'sales-outreach',
|
||||
label: 'Sales Outreach',
|
||||
description: 'Guides the AI agent through a structured sales outreach process',
|
||||
icon: 'IconBrain',
|
||||
content: `You are a sales outreach assistant. When reaching out to a prospect:
|
||||
1. Research the company and recent news
|
||||
2. Identify the prospect's role and likely pain points
|
||||
3. Draft a personalized message referencing specific details
|
||||
4. Keep the tone professional but conversational`,
|
||||
});
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
* `name` هي سلسلة معرّف فريدة للمهارة (يُنصَح باستخدام kebab-case).
|
||||
* `label` هو اسم العرض المقروء للبشر الظاهر في واجهة المستخدم.
|
||||
* `content` يحتوي على تعليمات المهارة — وهو النص الذي يستخدمه وكيل الذكاء الاصطناعي.
|
||||
* `icon` (اختياري) يحدّد الأيقونة المعروضة في واجهة المستخدم.
|
||||
* `description` (اختياري) يوفّر سياقًا إضافيًا حول غرض المهارة.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineAgent" description="عرِّف وكلاء الذكاء الاصطناعي باستخدام موجهات مخصّصة">
|
||||
|
||||
الوكلاء هم مساعدون ذكاء اصطناعي يعيشون داخل مساحة العمل لديك. استخدم `defineAgent()` لإنشاء وكلاء بموجه نظام مخصّص:
|
||||
|
||||
```ts src/agents/example-agent.ts
|
||||
import { defineAgent } from 'twenty-sdk/define';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
description: 'Helps the sales team draft outreach emails and research prospects',
|
||||
icon: 'IconRobot',
|
||||
prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.',
|
||||
});
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
* `name` هي سلسلة معرّف فريدة للوكيل (يُنصح باستخدام kebab-case).
|
||||
* `label` هو اسم العرض الظاهر في واجهة المستخدم.
|
||||
* `prompt` هو موجه النظام الذي يحدّد سلوك الوكيل.
|
||||
* `description` (اختياري) يوفّر سياقًا حول ما يفعله الوكيل.
|
||||
* `icon` (اختياري) يحدّد الأيقونة المعروضة في واجهة المستخدم.
|
||||
* `modelId` (اختياري) يتجاوز نموذج الذكاء الاصطناعي الافتراضي الذي يستخدمه الوكيل.
|
||||
* `responseFormat` (اختياري) يتحكم في شكل مخرجات الوكيل. القيمة الافتراضية هي `{ type: 'text' }` للنص الحر. استخدم `{ type: 'json', schema }` لفرض مخرجات JSON منظمة.
|
||||
|
||||
بشكل افتراضي، يعيد الوكيل نصًا حرًا. للحصول على مخرجات منظمة، عيّن `responseFormat` إلى `{ type: 'json' }` ووفّر `schema`:
|
||||
|
||||
```ts src/agents/structured-agent.ts
|
||||
import { defineAgent } from 'twenty-sdk/define';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: 'c4d5e6f7-a8b9-0123-cdef-456789012345',
|
||||
name: 'lead-scorer',
|
||||
label: 'Lead Scorer',
|
||||
prompt: 'Score the lead and explain your reasoning.',
|
||||
responseFormat: {
|
||||
type: 'json',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
score: { type: 'number', description: 'Lead score from 0 to 100' },
|
||||
summary: { type: 'string', description: 'Short reasoning for the score' },
|
||||
},
|
||||
required: ['score', 'summary'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
ملاحظات حول المخطط:
|
||||
* المخطط كائن مسطح: يجب أن يكون `type` لكل خاصية نوعًا بدائيًا (`string` أو `number` أو `boolean`). الكائنات المتداخلة والمصفوفات غير مدعومة.
|
||||
* `description` (اختياري) على كل خاصية يوجه النموذج لما يجب وضعه هناك.
|
||||
* `required` (اختياري) يسرد الخصائص التي يجب على النموذج إرجاعها دائمًا.
|
||||
* `additionalProperties: false` (اختياري) يمنع أي خاصية غير معرّفة في `properties`.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="runAgent" description="تشغيل وكيل من دالة منطقية">
|
||||
|
||||
تتيح `runAgent()` لدالة منطقية تشغيل أحد وكلاء تطبيقك (مع مهاراته وأدواته). عرِّف الوكيل عن طريق `universalIdentifier` الذي مررته إلى `defineAgent()`:
|
||||
|
||||
```ts src/logic-functions/run-enricher.ts
|
||||
import { runAgent } from 'twenty-sdk/logic-function';
|
||||
|
||||
const { result, error, success } = await runAgent({
|
||||
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
|
||||
prompt: 'Enrich House Ad <recordId>: fill empty fields from its listing URL.',
|
||||
});
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
* يعمل الوكيل **بشكل متزامن** ويمكنه قراءة/تحديث السجلات بنفسه عبر أدواته الخاصة — يتم حل `runAgent()` بمجرد اكتمال التشغيل.
|
||||
* لا يمكن للتطبيق تشغيل سوى وكلائه الخاصين.
|
||||
* يجب أن يمنح [الدور الافتراضي](/l/ar/developers/extend/apps/config/roles) للتطبيق علامة الإذن `AI` — أضِف `SystemPermissionFlag.AI` إلى `permissionFlagUniversalIdentifiers` الخاصة به (أو عيِّن `canAccessAllTools: true`).
|
||||
بدون ذلك، تفشل `runAgent()` بخطأ في الأذونات.
|
||||
* اضبط قيمة كبيرة لـ `timeoutSeconds` على الدالة المنطقية — قد يستغرق تشغيل الوكيل عدة ثوانٍ.
|
||||
* يكون `success` بقيمة `true` و`result` غير فارغ عند اكتمال التشغيل؛ في حال الفشل يكون `success` بقيمة `false`، و`result` بقيمة `null`، وتحتوي `error` على السبب (على سبيل المثال، عندما تنفد أرصدة الذكاء الاصطناعي الخاصة بمساحة العمل أثناء التشغيل).
|
||||
|
||||
```ts src/roles/default-role.ts
|
||||
import { defineApplicationRole, SystemPermissionFlag } from 'twenty-sdk/define';
|
||||
|
||||
export default defineApplicationRole({
|
||||
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
|
||||
label: 'Default function role',
|
||||
// runAgent() requires the AI permission flag on the app's default role.
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.AI],
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
**تجنب الحلقات:** إذا استدعيت `runAgent()` من مشغل حدث قاعدة بيانات من نوع `*.updated` وقام الوكيل بتحديث نفس السجل، فحدد نطاق المشغل باستخدام `updatedFields` إلى حقل لا يكتبه الوكيل أبدًا (مثل عنوان URL المصدر)، أو تحقَّق مما إذا كان أي حقل مستهدف لا يزال فارغًا قبل استدعاء `runAgent()`.
|
||||
</Warning>
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: CLI
|
||||
description: أوامر yarn twenty لتنفيذ الدوال، وبثّ السجلات، وإدارة تثبيتات التطبيقات، والتبديل بين الريموتات.
|
||||
icon: الطرفية
|
||||
---
|
||||
|
||||
إلى جانب `dev` و`dev:build` و`dev:add` و`dev:typecheck`، يوفّر `yarn twenty` CLI أوامر لتنفيذ الدوال، وعرض السجلات، وإدارة تثبيتات التطبيقات.
|
||||
|
||||
## تنفيذ الدوال (`yarn twenty dev:function:exec`)
|
||||
|
||||
تشغيل دالة منطقية يدويًا دون تشغيلها عبر HTTP أو cron أو حدث قاعدة بيانات:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Execute by function name
|
||||
yarn twenty dev:function:exec -n create-new-post-card
|
||||
|
||||
# Execute by universalIdentifier
|
||||
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
|
||||
# Pass a JSON payload
|
||||
yarn twenty dev:function:exec -n create-new-post-card -p '{"name": "Hello"}'
|
||||
|
||||
# Execute the post-install function
|
||||
yarn twenty dev:function:exec --postInstall
|
||||
```
|
||||
|
||||
## عرض سجلات الدوال (`yarn twenty dev:function:logs`)
|
||||
|
||||
بثّ سجلات التنفيذ لدوال تطبيقك المنطقية:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Stream all function logs
|
||||
yarn twenty dev:function:logs
|
||||
|
||||
# Filter by function name
|
||||
yarn twenty dev:function:logs -n create-new-post-card
|
||||
|
||||
# Filter by universalIdentifier
|
||||
yarn twenty dev:function:logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
```
|
||||
|
||||
<Note>
|
||||
يختلف هذا عن `yarn twenty docker:logs`، الذي يعرض سجلات حاوية Docker. يعرض `yarn twenty dev:function:logs` سجلات تنفيذ دوال تطبيقك من خادم Twenty.
|
||||
</Note>
|
||||
|
||||
## توليد العميل محدد الأنواع (`yarn twenty dev:generate-client`)
|
||||
|
||||
أعد توليد عميل واجهة برمجة التطبيقات محدد الأنواع (`twenty-client-sdk`) من مخطط الجهة البعيدة النشطة، دون بناء تطبيق أو مزامنته. استخدمه للحصول على عميل محدد الأنواع في أي مشروع — مثل خدمة خلفية موجودة في مستودع منفصل — يتواصل مع مثيل Twenty الخاص بك:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# In your project (no Twenty app definition required)
|
||||
yarn add twenty-sdk twenty-client-sdk
|
||||
|
||||
# Connect to the Twenty instance to generate the client from
|
||||
yarn twenty remote:add
|
||||
|
||||
# Generate the typed client into node_modules/twenty-client-sdk
|
||||
yarn twenty dev:generate-client
|
||||
```
|
||||
|
||||
ثم استورد العميل في شيفرتك:
|
||||
|
||||
```typescript
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
```
|
||||
|
||||
أعد تشغيل الأمر كلما تغيّر نموذج البيانات لديك لتحديث الأنواع المُولَّدة.
|
||||
|
||||
<Note>
|
||||
يتم إنشاء العميل البرمجي داخل `node_modules`، لذا لا يُدرج مع شيفرتك في الالتزامات (commits). شغّل `yarn twenty dev:generate-client` بعد كل عملية تثبيت (على سبيل المثال في سكربت `postinstall` أو في CI).
|
||||
</Note>
|
||||
|
||||
## إلغاء تثبيت تطبيق (`yarn twenty app:uninstall`)
|
||||
|
||||
أزل تطبيقك من مساحة العمل النشطة:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty app:uninstall
|
||||
|
||||
# Skip the confirmation prompt
|
||||
yarn twenty app:uninstall --yes
|
||||
```
|
||||
|
||||
## إدارة الريموتات
|
||||
|
||||
**الريموت** هو خادم Twenty يتصل به تطبيقك. أثناء الإعداد، تُنشئ أداة إنشاء الهيكل واحدًا لك تلقائيًا. يمكنك إضافة ريموتات أخرى أو التبديل بينها في أي وقت.
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Add a new remote (opens a browser for OAuth login)
|
||||
yarn twenty remote:add
|
||||
|
||||
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
|
||||
yarn twenty remote:add --local
|
||||
|
||||
# Add a remote non-interactively (useful for CI)
|
||||
yarn twenty remote:add --url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
|
||||
|
||||
# List all configured remotes
|
||||
yarn twenty remote:list
|
||||
|
||||
# Set the active remote
|
||||
yarn twenty remote:use <name>
|
||||
```
|
||||
|
||||
تُخزَّن بيانات اعتمادك في `~/.twenty/config.json`.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: نظرة عامة
|
||||
description: قم ببناء تطبيقك واختباره وإصداره — أوامر CLI، واختبارات التكامل، وCI، والنشر إلى خادم أو إلى npm.
|
||||
icon: rocket
|
||||
---
|
||||
|
||||
**طبقة العمليات** هي كل ما تفعله *على* تطبيقك وليس *به*: استدعاء أوامر CLI، وتشغيل اختبارات التكامل على خادم Twenty حقيقي، وإعداد CI، وإطلاق الإصدارات — إما كملف tarball يُنشر على خادم واحد أو كحزمة npm مُدرجة في المتجر.
|
||||
|
||||
```text
|
||||
develop ─▶ test ─▶ build ─▶ deploy / publish
|
||||
─────── ──── ───── ─────────────────
|
||||
yarn yarn yarn yarn twenty app:publish --private (tarball → one server)
|
||||
twenty test twenty
|
||||
dev dev:build yarn twenty app:publish (npm → marketplace)
|
||||
```
|
||||
|
||||
## في هذا القسم
|
||||
|
||||
<CardGroup cols={٢}>
|
||||
<Card title="CLI" icon="الطرفية" href="/l/ar/developers/extend/apps/operations/cli">
|
||||
مرجع `yarn twenty` — exec، logs، uninstall، remotes.
|
||||
</Card>
|
||||
<Card title="المزامنة والاستعادة" icon="بوصلة" href="/l/ar/developers/extend/apps/operations/sync-and-recovery">
|
||||
أي أمر يُستخدم ومتى، وكيفية قراءة فروق المزامنة، وسلّم الاستعادة.
|
||||
</Card>
|
||||
<Card title="الاختبار" icon="flask" href="/l/ar/developers/extend/apps/operations/testing">
|
||||
إعداد Vitest، اختبارات التكامل، فحص الأنواع، سير عمل CI.
|
||||
</Card>
|
||||
<Card title="النشر" icon="رفع" href="/l/ar/developers/extend/apps/operations/publishing">
|
||||
البناء، نشر ملف tarball، النشر إلى npm، التثبيت.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,294 @@
|
||||
---
|
||||
title: النشر
|
||||
icon: رفع
|
||||
description: وزّع تطبيق Twenty الخاص بك على سوق Twenty أو انشره داخليًا.
|
||||
---
|
||||
|
||||
## نظرة عامة
|
||||
|
||||
بمجرد أن يكون تطبيقك [مبنيًا ومختبرًا محليًا](/l/ar/developers/extend/apps/getting-started/concepts)، لديك مساران لتوزيعه:
|
||||
|
||||
* **نشر أرشيف tar** — ارفع تطبيقك مباشرةً إلى خادم Twenty محدد للاستخدام الداخلي أو الخاص.
|
||||
* **النشر على npm** — أدرج تطبيقك في سوق Twenty ليتسنى لأي مساحة عمل اكتشافه وتثبيته.
|
||||
|
||||
كلا المسارين يبدآن من نفس خطوة **build**.
|
||||
|
||||
## بناء تطبيقك
|
||||
|
||||
شغّل أمر build لتجميع تطبيقك وإنشاء ملف `manifest.json` جاهز للتوزيع:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:build
|
||||
```
|
||||
|
||||
يقوم هذا بتجميع مصادر TypeScript، وتحويل دوال المنطق ومكوّنات الواجهة الأمامية، وكتابة كل شيء إلى `.twenty/output/`. أضِف `--tarball` لإنتاج حزمة `.tgz` أيضًا للتوزيع اليدوي أو لأمر publish.
|
||||
|
||||
## النشر إلى خادم (tarball)
|
||||
|
||||
بالنسبة للتطبيقات التي لا تريد إتاحتها للعامة — مثل الأدوات المملوكة، أو عمليات التكامل الخاصة بالمؤسسات فقط، أو الإصدارات التجريبية — يمكنك نشر tarball مباشرةً إلى خادم Twenty.
|
||||
|
||||
### المتطلبات الأساسية
|
||||
|
||||
قبل النشر، تحتاج إلى remote مُعدّ يشير إلى خادم الهدف. تُخزّن remotes عنوان URL للخادم وبيانات اعتماد المصادقة محليًا في `~/.twenty/config.json`.
|
||||
|
||||
أضِف remote:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty remote:add --url https://your-twenty-server.com --as production
|
||||
```
|
||||
|
||||
### النشر
|
||||
|
||||
بناء تطبيقك ورفعه إلى الخادم في خطوة واحدة:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty app:publish --private
|
||||
# To deploy to a specific remote:
|
||||
# yarn twenty app:publish --private --remote production
|
||||
```
|
||||
|
||||
### مشاركة تطبيق منشور
|
||||
|
||||
<Warning>
|
||||
تُعد مشاركة التطبيقات الخاصة (tarball) عبر مساحات العمل ميزة ضمن **Enterprise**. ستعرض علامة التبويب **التوزيع** مطالبة بالترقية بدلًا من عناصر التحكم في المشاركة حتى تحتوي مساحة العمل لديك على مفتاح Enterprise صالح. اطلع على [الإعدادات > لوحة الإدارة > Enterprise](/settings/admin-panel#enterprise) لتنشيطه.
|
||||
</Warning>
|
||||
|
||||
تطبيقات tarball لا تُدرَج في السوق العامة، لذا لن تكتشفها مساحات العمل الأخرى على الخادم نفسه عبر الاستعراض. بمجرد أن تصبح مساحة العمل لديك ضمن خطة Enterprise، يمكنك مشاركة تطبيق تم نشره كما يلي:
|
||||
|
||||
1. اذهب إلى **الإعدادات > التطبيقات > التسجيلات** وافتح تطبيقك
|
||||
2. في علامة التبويب **التوزيع**، انقر **نسخ رابط المشاركة**
|
||||
3. شارك هذا الرابط مع المستخدمين في مساحات عمل أخرى — سيأخذهم مباشرةً إلى صفحة تثبيت التطبيق
|
||||
|
||||
يستخدم رابط المشاركة عنوان URL الأساسي للخادم (من دون أي نطاق فرعي لمساحة عمل)، لذا يعمل مع أي مساحة عمل على الخادم.
|
||||
|
||||
### إدارة الإصدارات
|
||||
|
||||
عند تحديث تطبيق tarball منشور مسبقًا، يشترط الخادم أن تكون قيمة `version` في `package.json` **أعلى قطعًا** (وفق ترتيب [الإصدار الدلالي](https://semver.org)) من الإصدار المنشور حاليًا. إعادة نشر الإصدار نفسه، أو دفع إصدار أدنى، يُرفَض قبل تخزين ملف tarball — سترى خطأ `VERSION_ALREADY_EXISTS` من CLI.
|
||||
|
||||
لطرح تحديث:
|
||||
|
||||
1. قم بزيادة الحقل `version` في ملف `package.json` (مثلًا: `1.2.3` → `1.2.4`، `1.3.0`، أو `2.0.0`)
|
||||
2. شغّل `yarn twenty app:publish --private` (أو `yarn twenty app:publish --private --remote production`)
|
||||
3. سترى مساحات العمل التي ثبّتت التطبيق الترقية متاحة في إعداداتها
|
||||
|
||||
<Note>
|
||||
علامات ما قبل الإصدار تعمل كما هو متوقع: زيادة `1.0.0-rc.1` → `1.0.0-rc.2` مسموح بها، ويُعترَف بالإصدار النهائي مثل `1.0.0` على أنه أعلى من `1.0.0-rc.5`. يجب أن يكون الإصدار في `package.json` بنفسه سلسلة semver صالحة.
|
||||
</Note>
|
||||
|
||||
{/* TODO: add screenshot of the Upgrade button */}
|
||||
|
||||
### توافق إصدار الخادم
|
||||
|
||||
إذا كان تطبيقك يستخدم ميزة تم تقديمها في إصدار معيّن من خادم Twenty (على سبيل المثال، موفّرو OAuth الذين أضيفوا في v2.3.0)، فيجب عليك التصريح بأدنى إصدار من الخادم يتطلبه تطبيقك باستخدام الحقل `engines.twenty` في `package.json`:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"name": "twenty-my-app",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"twenty": ">=2.3.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
القيمة عبارة عن [نطاق semver](https://github.com/npm/node-semver#ranges) قياسي. أنماط شائعة:
|
||||
|
||||
| النطاق | المعنى |
|
||||
| ---------------------------------- | ------------------------------------------------ |
|
||||
| `>=2.3.0` | أي خادم من 2.3.0 فصاعدًا |
|
||||
| `>=2.3.0 \<3.0.0` | 2.3.0 أو أحدث، لكن أقل من الإصدار الرئيسي التالي |
|
||||
| `^2.3.0` | مماثل لـ `>=2.3.0 \<3.0.0` |
|
||||
|
||||
**ماذا يحدث وقت النشر والتثبيت:**
|
||||
|
||||
* إذا تم تعيين `engines.twenty` ولم يستوفِ إصدار الخادم الهدف النطاق، فسيتم رفض النشر (tarball upload) أو التثبيت بخطأ `SERVER_VERSION_INCOMPATIBLE` ورسالة تُشير إلى كلٍ من النطاق المطلوب وإصدار الخادم الفعلي.
|
||||
* إذا كان `engines.twenty` **غير مُعين**، فسيُقبل التطبيق على أي إصدار من الخادم (متوافق مع الإصدارات السابقة للتطبيقات الحالية).
|
||||
* إذا لم يكن لدى الخادم قيمة `APP_VERSION` مُكوَّنة، فسيتم تخطي الفحص.
|
||||
|
||||
<Note>
|
||||
الخادم هو الجهة المرجعية للفحص — إذ يتحقق من `engines.twenty` عند كلٍ من رفع tarball وتثبيت مساحة العمل. إذا قمت بنشر tarball خارج القناة المعتادة أو التثبيت من السوق، فسيظل الخادم يفرض التوافق.
|
||||
</Note>
|
||||
|
||||
## CI/CD المؤتمتة (مهام سير عمل مُولَّدة بالقوالب)
|
||||
|
||||
التطبيقات المُولَّدة باستخدام `create-twenty-app` تأتي افتراضيًا مع مهمَّتي سير عمل من GitHub Actions ضمن `.github/workflows/`. هي جاهزة للتشغيل بمجرد دفع المستودع إلى GitHub — لا حاجة لأي إعداد إضافي لـ CI، وCD يتطلّب سرًّا واحدًا فقط.
|
||||
|
||||
### CI — `ci.yml`
|
||||
|
||||
يشغّل اختبارات التكامل عند كل دفع إلى `main` وعند كل طلب سحب.
|
||||
|
||||
**ماذا يفعل:**
|
||||
|
||||
1. يجلب مصدر تطبيقك.
|
||||
2. ينشئ مثيلاً اختبارياً معزولاً من Twenty باستخدام الإجراء المركّب `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` (المكافئ في CI للأمر `yarn twenty docker:start --test`).
|
||||
3. يُفعِّل Corepack، ويُعدّ Node.js من ملف `.nvmrc` لديك، ويثبّت التبعيات بواسطة `yarn install --immutable`.
|
||||
4. يشغّل `yarn test`، ويمرّر `TWENTY_API_URL` و`TWENTY_API_KEY` من المثيل الذي تم إنشاؤه بحيث تتمكّن اختباراتك من التواصل مع خادم حقيقي.
|
||||
|
||||
**خيارات التكوين:**
|
||||
|
||||
* `TWENTY_VERSION` (متغيّر بيئة، القيمة الافتراضية `latest`) — ثبّت نسخة خادم Twenty المستخدمة في CI عبر تعديل هذا في `ci.yml`.
|
||||
* يتم تجميع التشغيل المتزامن حسب `github.ref` ويلغي التشغيلات قيد التقدّم عند أي دفع جديد.
|
||||
|
||||
لا تتطلّب أي أسرار — مثيل الاختبار مؤقّت ويستمر فقط طوال مدّة المهمّة.
|
||||
|
||||
### CD — `cd.yml`
|
||||
|
||||
ينشر تطبيقك إلى خادم Twenty مُهيّأ عند كل دفع إلى `main`، وبشكل اختياري من طلب سحب عند تطبيق الوسم `deploy`.
|
||||
|
||||
**ماذا يفعل:**
|
||||
|
||||
1. يجلب رأس طلب السحب (للطلبات الموسومة) أو الالتزام المدفوع.
|
||||
2. يشغّل `twentyhq/twenty/.github/actions/deploy-twenty-app@main` — وهو المكافئ في CI للأمر `yarn twenty app:publish --private`.
|
||||
3. يشغّل `twentyhq/twenty/.github/actions/install-twenty-app@main` بحيث تُثبَّت النسخة المُنشَرة حديثًا في مساحة العمل المستهدفة.
|
||||
|
||||
**التكوين المطلوب:**
|
||||
|
||||
| الإعداد | حيث | الغرض |
|
||||
| ----------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `TWENTY_DEPLOY_URL` | `env` في `cd.yml` (القيمة الافتراضية `http://localhost:3000`) | خادم Twenty الذي سيتم النشر إليه. غيّر هذا إلى عنوان URL لخادمك الحقيقي قبل أول استخدام. |
|
||||
| `TWENTY_DEPLOY_API_KEY` | في مستودع GitHub **Settings → Secrets and variables → Actions** | مفتاح API يمتلك إذن النشر على الخادم المستهدف. |
|
||||
|
||||
<Note>
|
||||
القيمة الافتراضية لـ `TWENTY_DEPLOY_URL` وهي `http://localhost:3000` مجرد عنصر نائب — لن تصل إلى أي شيء من مُشغِّل مستضاف لدى GitHub. حدّثها إلى عنوان URL العام لخادمك (أو استخدم مُشغِّلًا مستضافًا ذاتيًا مع وصول شبكي) قبل تمكين CD.
|
||||
</Note>
|
||||
|
||||
**تشغيل نشر معاينة من طلب سحب:**
|
||||
|
||||
أضِف الوسم `deploy` إلى طلب سحب. الشرط `if:` في `cd.yml` سيشغّل المهمّة لذلك الطلب مستخدمًا التزام رأس الطلب، مما يتيح لك التحقّق من التغيير على الخادم المستهدف قبل الدمج.
|
||||
|
||||
### تثبيت الإجراءات القابلة لإعادة الاستخدام
|
||||
|
||||
يشير كلا سيرَي العمل إلى إجراءات قابلة لإعادة الاستخدام عند `@main`، لذا تُلتقط تحديثات الإجراءات في مستودع `twentyhq/twenty` تلقائيًا. إذا كنت تريد بناءات حتمية، فاستبدِل `@main` بقيمة SHA لالتزام أو بوسم إصدار في كل سطر `uses:`.
|
||||
|
||||
## النشر على npm
|
||||
|
||||
يُتيح النشر على npm إمكانية العثور على تطبيقك في سوق Twenty. يمكن لأي مساحة عمل في Twenty استعراض تطبيقات السوق وتثبيتها وترقيتها مباشرةً من واجهة المستخدم.
|
||||
|
||||
### المتطلبات
|
||||
|
||||
* حساب على [npm](https://www.npmjs.com)
|
||||
* الكلمة المفتاحية `twenty-app` في مصفوفة `keywords` في `package.json` (أضفها يدويًا — فهي غير مضمنة افتراضيًا في قالب `create-twenty-app`)
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"name": "twenty-app-postcard-sender",
|
||||
"version": "1.0.0",
|
||||
"keywords": ["twenty-app"]
|
||||
}
|
||||
```
|
||||
|
||||
### بيانات التعريف لسوق التطبيقات
|
||||
|
||||
يدعم إعداد `defineApplication()` حقولًا اختيارية تتحكم في كيفية ظهور تطبيقك في السوق. استخدم `logoUrl` و`screenshots` للإشارة إلى الصور من مجلد `public/`:
|
||||
|
||||
```ts src/application-config.ts
|
||||
export default defineApplication({
|
||||
universalIdentifier: '...',
|
||||
displayName: 'My App',
|
||||
description: 'A great app',
|
||||
logoUrl: 'public/logo.png',
|
||||
screenshots: [
|
||||
'public/screenshot-1.png',
|
||||
'public/screenshot-2.png',
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
اطّلع على [أكورديون defineApplication](/l/ar/developers/extend/apps/config/application#marketplace-metadata) في صفحة بناء التطبيقات للاطلاع على القائمة الكاملة لحقول السوق (`author` و`category` و`aboutDescription` و`websiteUrl` و`termsUrl` وغيرها).
|
||||
|
||||
#### أبعاد لقطات الشاشة الموصى بها
|
||||
|
||||
يعرض السوق `screenshots` داخل حاوية ثابتة بنسبة `8:5` (على سبيل المثال، `1600×1000 px`).
|
||||
|
||||
<Note>
|
||||
تعرض لقطات الشاشة بأي نسبة عرض إلى ارتفاع بالكامل ولن يتم اقتطاعها مطلقًا، ولكن أي شيء أطول أو أضيق بكثير من `8:5` سيظهر مساحات فارغة على الجانبين.
|
||||
</Note>
|
||||
|
||||
### النشر
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty app:publish
|
||||
```
|
||||
|
||||
للنشر تحت dist-tag معيّن (مثلًا: `beta` أو `next`):
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty app:publish --tag beta
|
||||
```
|
||||
|
||||
### كيف تعمل آلية الاكتشاف في السوق
|
||||
|
||||
يقوم خادم Twenty بمزامنة كتالوج السوق من سجل npm **كل ساعة**.
|
||||
|
||||
يمكنك تشغيل المزامنة فورًا بدلًا من الانتظار:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:catalog-sync
|
||||
# To target a specific remote:
|
||||
# yarn twenty dev:catalog-sync --remote production
|
||||
```
|
||||
|
||||
تأتي بيانات التعريف المعروضة في السوق من إعداد `defineApplication()` — حقول مثل `displayName` و`description` و`author` و`category` و`logoUrl` و`screenshots` و`aboutDescription` و`websiteUrl` و`termsUrl`.
|
||||
|
||||
<Note>
|
||||
إذا لم يحدد تطبيقك `aboutDescription` في `defineApplication()`، فسيستخدم السوق تلقائيًا ملف `README.md` الخاص بحزمتك من npm كمحتوى لصفحة حول. هذا يعني أنه يمكنك الاحتفاظ بملف README واحد لكل من npm وسوق Twenty. إذا كنت تريد وصفًا مختلفًا في السوق، فقم بتعيين `aboutDescription` بشكل صريح.
|
||||
</Note>
|
||||
|
||||
### النشر عبر CI
|
||||
|
||||
استخدم سير عمل GitHub Actions هذا للنشر تلقائيًا مع كل إصدار (يستخدم [OIDC](https://docs.npmjs.com/trusted-publishers)):
|
||||
|
||||
```yaml filename=".github/workflows/publish.yml"
|
||||
name: Publish
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: https://registry.npmjs.org
|
||||
- run: yarn install --immutable
|
||||
- run: npx twenty dev:build
|
||||
- run: npm publish --provenance --access public
|
||||
working-directory: .twenty/output
|
||||
```
|
||||
|
||||
بالنسبة لأنظمة CI الأخرى (GitLab CI، وCircleCI، إلخ)، تنطبق الأوامر الثلاثة نفسها: `yarn install`، ثم `yarn twenty dev:build`، ثم `npm publish` من `.twenty/output`.
|
||||
|
||||
<Note>
|
||||
**npm provenance** اختياري ولكنه موصى به. يضيف النشر باستخدام `--provenance` شارة ثقة إلى إدراجك على npm، مما يتيح للمستخدمين التحقق من أن الحزمة تم بناؤها من التزام محدد ضمن خط أنابيب CI عام. راجع [وثائق npm provenance](https://docs.npmjs.com/generating-provenance-statements) للحصول على تعليمات الإعداد.
|
||||
</Note>
|
||||
|
||||
## تثبيت التطبيقات
|
||||
|
||||
بعد نشر التطبيق (npm) أو نشره (tarball)، يمكن لمساحات العمل تثبيته عبر واجهة المستخدم.
|
||||
|
||||
اذهب إلى صفحة **الإعدادات > التطبيقات** في Twenty، حيث يمكن استعراض تطبيقات السوق والتطبيقات المنشورة عبر tarball وتثبيتها.
|
||||
|
||||
{/* TODO: add screenshot of the UI when the app is registered */}
|
||||
|
||||
يمكنك أيضًا تثبيت التطبيقات من سطر الأوامر:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty app:install
|
||||
```
|
||||
|
||||
<Note>
|
||||
يفرض الخادم اعتماد إصدارات semver عند التثبيت، بما يعكس القواعد المطبّقة عند النشر:
|
||||
|
||||
* تثبيت الإصدار نفسه المثبّت بالفعل في مساحة عملك يُرفَض بخطأ `APP_ALREADY_INSTALLED`.
|
||||
* تثبيت إصدار أدنى من الإصدار المثبّت حاليًا يُرفَض بخطأ `CANNOT_DOWNGRADE_APPLICATION`.
|
||||
|
||||
لتثبيت إصدار أحدث، انشره (deploy) أو انشره إلى السجل (publish) أولًا، ثم أعد تشغيل `yarn twenty app:install`.
|
||||
</Note>
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: المزامنة والاستعادة
|
||||
description: أي أمر تستخدمه ومتى، وكيفية قراءة مخرجات المزامنة، وسلّم استعادة لما يجب فعله عندما تنحرف البيانات الوصفية المحلية — قبل الوصول إلى إعادة تعيين كاملة.
|
||||
icon: بوصلة
|
||||
---
|
||||
|
||||
يدور تطوير التطبيقات محليًا حول **المزامنة**: يقوم الـ CLI بإعادة إنشاء ملف manifest الخاص بك ويطبّق الخادم فقط الفرق بينه وبين البيانات الوصفية الموجودة بالفعل في مساحة العمل لديك. تغطي هذه الصفحة الأمر الذي ينبغي استخدامه، وكيفية قراءة ما غيّرته المزامنة، وما الذي يجب فعله — بالترتيب — عندما تبدو الحالة المحلية غير متسقة.
|
||||
|
||||
## أي أمر، ومتى
|
||||
|
||||
<Note>
|
||||
للتكرار اليومي المحلي ستحتاج تقريبًا دائمًا إلى `yarn twenty dev`. يُستخدم النشر والإصدار لإطلاق الإصدارات، **وليس** للحلقة المحلية.
|
||||
</Note>
|
||||
|
||||
| ترغب في… | أمر | الملاحظات |
|
||||
| ------------------------------------------------ | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| التكرار محليًا مع المزامنة الحية | `yarn twenty dev` | يراقب ملفاتك ويجري مزامنة عند كل تغيير. |
|
||||
| مزامنة واحدة ثم إنهاء (CI، السكربتات، الخطّافات) | `yarn twenty dev --once` | عملية إنشاء واحدة + مزامنة، ثم إنهاء. |
|
||||
| معاينة التغييرات **بدون تطبيقها** | `yarn twenty dev --once --dry-run` | يحتسب الفرق ويطبعه؛ ولا يكتب أي شيء. |
|
||||
| إزالة التطبيق من مساحة العمل | `yarn twenty app:uninstall` | أضف `--yes` لتخطي رسالة التأكيد. |
|
||||
| إرسال ملف tarball إلى خادم | `yarn twenty app:publish --private` | يتطلّب إصدارًا **أعلى بشكل صارم** في `package.json` — راجع قسم [النشر](/l/ar/developers/extend/apps/operations/publishing). |
|
||||
| النشر في السوق (npm) | `yarn twenty app:publish` | — |
|
||||
| تثبيت / ترقية إصدار منشور | `yarn twenty app:install` | يُثبّت الإصدار المنشور حاليًا. |
|
||||
| مسح الخادم المحلي والبدء من جديد | `yarn twenty docker:reset` | يحذف **كل** البيانات المحلية — كملاذ أخير. |
|
||||
|
||||
### لا تحتاج المزامنة المحلية إلى زيادة في الإصدار
|
||||
|
||||
تنطبق قاعدة `version` المتزايدة بدقة (`VERSION_ALREADY_EXISTS` عند النشر، و`APP_ALREADY_INSTALLED` / `CANNOT_DOWNGRADE_APPLICATION` عند التثبيت) على **`app:publish` / `app:install`** — مسار الإصدارات. يقوم `yarn twenty dev` بمزامنة ملف manifest في مكانه ولا يتطلّب تغيير الإصدار أبدًا، لذا لست بحاجة إلى تعديل `package.json` للتكرار. إذا وجدت نفسك تزيد الإصدار لاختبار تغيير محلي، فأنت تستخدم مسار الإصدارات بينما ما تريده هو حلقة التطوير.
|
||||
|
||||
## قراءة مخرجات المزامنة
|
||||
|
||||
كل عملية مزامنة تطبع التغييرات في البيانات الوصفية التي تم تطبيقها (أو التي سيتم تطبيقها، مع خيار `--dry-run`):
|
||||
|
||||
```text filename="Terminal"
|
||||
Metadata changes: 2 created, 1 updated, 1 deleted
|
||||
created objectMetadata rocket
|
||||
created fieldMetadata timelineActivities
|
||||
updated fieldMetadata launchedAt
|
||||
deleted pageLayout legacyTab
|
||||
✓ Synced
|
||||
```
|
||||
|
||||
هذه أداتك الأولى للتشخيص: تُخبرك بدقة ما الكائنات والحقول والتخطيطات التي تغيّرت، بحيث يمكنك التأكد من أن المزامنة أنجزت ما توقّعته قبل التحقّق من واجهة المستخدم.
|
||||
|
||||
عندما تفشل المزامنة على كيان واحد، يذكر الخطأ اسم الكيان المسبب للمشكلة و`universalIdentifier` الخاص به، على سبيل المثال:
|
||||
|
||||
```text
|
||||
Migration action 'create' for 'fieldMetadata' (universalIdentifier: 2020...4337) failed
|
||||
```
|
||||
|
||||
استخدم ذلك المعرّف للعثور على الكيان في ملف manifest الخاص بك (وإن لزم الأمر، في مساحة العمل) بدلًا من تخمين أيّها يتعارض.
|
||||
|
||||
## معاينة التغييرات (تشغيل تجريبي dry run)
|
||||
|
||||
يبني `yarn twenty dev --once --dry-run` ملف manifest الخاص بك، ويطلب من الخادم خطة الترحيل، ويطبعها — **بدون تطبيق أي شيء**. إنها الطريقة الآمنة للإجابة عن سؤال "ما الذي ستغيّره هذه المزامنة؟" قبل الالتزام بها.
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --once --dry-run
|
||||
```
|
||||
|
||||
```text filename="Terminal"
|
||||
Building manifest...
|
||||
Computing metadata diff (dry run, nothing will be applied)...
|
||||
Metadata changes: 1 created, 1 updated
|
||||
created fieldMetadata timelineActivities
|
||||
updated objectMetadata rocket
|
||||
✓ Dry run complete for My App — no changes were applied
|
||||
```
|
||||
|
||||
تشغيل تجريبي:
|
||||
|
||||
* **لا يكتب أي شيء** — لا ترحيل لبيانات وصفية، ولا تحديث لسجل التطبيق، ولا تغييرات في الأدوار/التبويبات الافتراضية، ولا توليد لعميل API.
|
||||
* يُرجع **نفس الفرق** الذي ستُطبِّقه مزامنة حقيقية، حتى تتمكن من مراجعة الكيانات التي سيتم إنشاؤها/تحديثها/حذفها مسبقًا.
|
||||
* يكون مفيدًا قبل إجراء تغيير محفوف بالمخاطر، أو عند مراجعة تغيير تم إنشاؤه بواسطة الذكاء الاصطناعي، أو في سكربت يجب أن يفشل إذا كان تغيير غير متوقَّع على وشك الحدوث.
|
||||
|
||||
<Note>
|
||||
يُعاين التشغيل التجريبي فقط **تغييرات البيانات الوصفية**، ويتطلّب أن يكون التطبيق قد تمت مزامنته مرة واحدة على الأقل (حتى تعرف به مساحة العمل). إذا شغّلته ضد تطبيق لم تتم مزامنته من قبل، سيبلغ الخادم أن التطبيق غير مُثبّت — شغّل `yarn twenty dev` مرة واحدة أولًا.
|
||||
</Note>
|
||||
|
||||
## سلّم الاستعادة
|
||||
|
||||
عندما تبدو البيانات الوصفية المحلية غير صحيحة، صعِّد الإجراءات بهذا الترتيب وتوقّف بمجرد زوال العائق. كل خطوة أكثر إرباكًا من التي قبلها.
|
||||
|
||||
1. **أعد المزامنة.** شغّل `yarn twenty dev --once` مرة أخرى. عمليات المزامنة متطابِقة الأثر (idempotent) — إعادة تشغيل ملف manifest النظيف آمنة وغالبًا ما تحل تعثرًا عابرًا.
|
||||
2. **عاين الخطة.** شغّل `yarn twenty dev --once --dry-run` لرؤية ما الذي تنوي المزامنة التالية تغييره بالضبط، بدون تطبيقه.
|
||||
3. **اقرأ الخطأ المسمّى.** إذا فشلت المزامنة، لاحظ نوع البيانات الوصفية و`universalIdentifier` في الرسالة (انظر أعلاه) وحدّد ذلك الكيان في ملف manifest الخاص بك. يشير التعارض عادةً إلى معرّف مكرر أو مُعاد استخدامه.
|
||||
4. **إلغاء التثبيت وإعادة التثبيت.** شغّل `yarn twenty app:uninstall`، ثم أجرِ مزامنة مرة أخرى (`yarn twenty dev`). هذا يعيد بناء بيانات التطبيق الوصفية من نقطة بداية نظيفة مع إبقاء باقي مساحة العمل سليمة.
|
||||
5. **إعادة تعيين كاملة (الملاذ الأخير).** شغّل `yarn twenty docker:reset`، ثم أعد التهيئة والمزامنة.
|
||||
|
||||
<Warning>
|
||||
يقوم `yarn twenty docker:reset` بحذف **كل** البيانات في النسخة المحلية لديك — كل مساحة عمل، وكل سجل، وكل تطبيق. استخدمه فقط بعد فشل الخطوات السابقة.
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
هل واجهت خطأ في البيانات الوصفية؟ يرجى [فتح مشكلة](https://github.com/twentyhq/twenty/issues/new/choose) وإرفاق رسالة الترحيل الفاشلة (بما في ذلك نوع البيانات الوصفية و`universalIdentifier`)، ومخرجات `Metadata changes` من عملية المزامنة، والأوامر التي شغّلتها.
|
||||
</Note>
|
||||
|
||||
## تجنّب إجراء مزامنات متزامنة على مساحة عمل واحدة
|
||||
|
||||
تطبّق المزامنة عمليات ترحيل للبيانات الوصفية. قد يؤدّي تشغيل عدة عمليات مزامنة أو نشر أو تثبيت ضد **نفس** مساحة العمل في الوقت نفسه — على سبيل المثال، عدّة نوافذ طرفية أو وكلاء ذكاء اصطناعي يتكرّرون بالتوازي — إلى تداخل عمليات الترحيل تلك وترك البيانات الوصفية في حالة مطبَّقة جزئيًا.
|
||||
|
||||
يقوم الخادم بتسلسل عمليات المزامنة لكل مساحة عمل لمنع ذلك، لكن ما زال ينبغي عليك تمرير عمليات البيانات الوصفية الحساسة عبر عملية **واحدة** بدلًا من تنفيذها بالتوازي. إذا كنت تنظّم التطوير باستخدام عدة وكلاء، فمرّر استدعاءات المزامنة/النشر/التثبيت عبر طابور واحد حتى تعمل واحدة فقط في الوقت نفسه.
|
||||
|
||||
## تمييز أنواع الإخفاقات
|
||||
|
||||
عندما يحدث خلل ما، يتيح لك فرق البيانات الوصفية والأخطاء المسمّاة تحديد موضع الفشل:
|
||||
|
||||
* **خطأ في إنشاء ملف manifest** — يفشل الـ CLI قبل إجراء المزامنة (`MANIFEST_BUILD_FAILED`، `TYPECHECK_FAILED`)؛ أصلِح كود التطبيق لديك.
|
||||
* **خطأ في المزامنة / الترحيل** — تنجح عملية الإنشاء لكن يفشل تطبيق الفرق، مع تسمية الكيان و`universalIdentifier`؛ أصلِح البيانات الوصفية المتعارِضة.
|
||||
* **خطأ في وقت تشغيل كود التطبيق** — تتم المزامنة بنجاح، ولكن دوال المنطق أو المكوّنات لديك لا تعمل بشكل صحيح أثناء وقت التشغيل؛ تحقّق من [سجلات الدوال](/l/ar/developers/extend/apps/operations/cli).
|
||||
* **حالة المثيل المحلي** — لا ينطبق أيّ مما سبق وما زالت مساحة العمل تبدو غير صحيحة؛ تابع النزول في سلّم الاستعادة.
|
||||
@@ -0,0 +1,301 @@
|
||||
---
|
||||
title: الاختبار
|
||||
description: إعداد Vitest، واختبارات تكامل مقابل خادم Twenty حقيقي، والتحقق من الأنواع، والتكامل المستمر (CI) باستخدام GitHub Actions.
|
||||
icon: flask
|
||||
---
|
||||
|
||||
يوفّر SDK واجهات برمجة قابلة للتنفيذ برمجيًا تمكّنك من بناء تطبيقك ونشره وتثبيته وإلغاء تثبيته من شيفرة الاختبار. بالاقتران مع [Vitest](https://vitest.dev/) وعملاء واجهة البرمجة مضبوطي الأنواع، يمكنك كتابة اختبارات تكامل تتحقّق من أن تطبيقك يعمل من البداية إلى النهاية مقابل خادم Twenty حقيقي.
|
||||
|
||||
## استخدام حِزَم npm
|
||||
|
||||
يمكنك تثبيت واستخدام أي حزمة npm في تطبيقك. يتم تجميع كلٍ من الدوال المنطقية والمكوّنات الأمامية باستخدام [esbuild](https://esbuild.github.io/)، والذي يُضمّن جميع التبعيات ضمن المخرجات — لا حاجة إلى `node_modules` وقت التشغيل.
|
||||
|
||||
### تثبيت حزمة
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add axios
|
||||
```
|
||||
|
||||
ثم استوردها في شيفرتك:
|
||||
|
||||
```ts src/logic-functions/fetch-data.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import axios from 'axios';
|
||||
|
||||
const handler = async (): Promise<any> => {
|
||||
const { data } = await axios.get('https://api.example.com/data');
|
||||
|
||||
return { data };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '...',
|
||||
name: 'fetch-data',
|
||||
description: 'Fetches data from an external API',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
وينطبق الأمر نفسه على المكوّنات الأمامية:
|
||||
|
||||
```tsx src/front-components/chart.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
const DateWidget = () => {
|
||||
return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'date-widget',
|
||||
component: DateWidget,
|
||||
});
|
||||
```
|
||||
|
||||
### كيف يعمل التجميع
|
||||
|
||||
تستخدم خطوة البناء أداة esbuild لإنتاج ملف واحد مستقل لكل دالة منطقية ولكل مكوّن أمامي. تُضمَّن جميع الحزم المستوردة داخل الحزمة.
|
||||
|
||||
**الدوال المنطقية** تعمل في بيئة Node.js. الوحدات المدمجة في Node (`fs` و`path` و`crypto` و`http` وغيرها) متاحة ولا تحتاج إلى تثبيت.
|
||||
|
||||
**المكوّنات الأمامية** تعمل ضمن Web Worker. وحدات Node المدمجة غير متاحة — المتاح فقط واجهات برمجة المتصفّح وحِزَم npm التي تعمل في بيئة المتصفّح.
|
||||
|
||||
كلتا البيئتين تحتويان على `twenty-client-sdk/core` و`twenty-client-sdk/metadata` كوحدات متاحة مُسبقًا — لا تُضمَّن هذه ضمن الحزم بل تُحلّ وقت التشغيل بواسطة الخادم.
|
||||
|
||||
## إعداد
|
||||
|
||||
يتضمّن التطبيق المُولَّد بالقالب بالفعل Vitest. إذا أعددته يدويًا، فثبّت التبعيات:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add -D vitest vite-tsconfig-paths
|
||||
```
|
||||
|
||||
أنشئ `vitest.config.ts` في جذر تطبيقك:
|
||||
|
||||
```ts vitest.config.ts
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
projects: ['tsconfig.spec.json'],
|
||||
ignoreConfigErrors: true,
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
testTimeout: 120_000,
|
||||
hookTimeout: 120_000,
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL: 'http://localhost:2020',
|
||||
TWENTY_API_KEY: 'your-api-key',
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
أنشئ ملف إعداد يتحقّق من إمكانية الوصول إلى الخادم قبل تشغيل الاختبارات:
|
||||
|
||||
```ts src/__tests__/setup-test.ts
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { beforeAll } from 'vitest';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
|
||||
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
|
||||
|
||||
beforeAll(async () => {
|
||||
// Verify the server is running
|
||||
const response = await fetch(`${TWENTY_API_URL}/healthz`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
|
||||
'Start the server before running integration tests.',
|
||||
);
|
||||
}
|
||||
|
||||
// Write a temporary config for the SDK
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(TEST_CONFIG_DIR, 'config.json'),
|
||||
JSON.stringify({
|
||||
remotes: {
|
||||
local: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
}, null, 2),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
## واجهات SDK البرمجية
|
||||
|
||||
يُصدِّر المسار الفرعي `twenty-sdk/cli` دوالًا يمكنك استدعاؤها مباشرةً من شيفرة الاختبار:
|
||||
|
||||
| دالة | الوصف |
|
||||
| -------------- | ----------------------------------------- |
|
||||
| `appBuild` | بناء التطبيق واختياريًا حزم ملف tarball |
|
||||
| `appDeploy` | رفع ملف tarball إلى الخادم |
|
||||
| `appInstall` | تثبيت التطبيق على مساحة العمل النشطة |
|
||||
| `appUninstall` | إلغاء تثبيت التطبيق من مساحة العمل النشطة |
|
||||
|
||||
تُرجع كل دالة كائن نتيجة يحتوي على `success: boolean` وعلى إمّا `data` أو `error`.
|
||||
|
||||
## كتابة اختبار تكامل
|
||||
|
||||
إليك مثالًا كاملًا يبني التطبيق وينشره ويثبّته، ثم يتحقّق من ظهوره في مساحة العمل:
|
||||
|
||||
```ts src/__tests__/app-install.integration-test.ts
|
||||
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
|
||||
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const APP_PATH = process.cwd();
|
||||
|
||||
describe('App installation', () => {
|
||||
beforeAll(async () => {
|
||||
const buildResult = await appBuild({
|
||||
appPath: APP_PATH,
|
||||
tarball: true,
|
||||
onProgress: (message: string) => console.log(`[build] ${message}`),
|
||||
});
|
||||
|
||||
if (!buildResult.success) {
|
||||
throw new Error(`Build failed: ${buildResult.error?.message}`);
|
||||
}
|
||||
|
||||
const deployResult = await appDeploy({
|
||||
tarballPath: buildResult.data.tarballPath!,
|
||||
onProgress: (message: string) => console.log(`[deploy] ${message}`),
|
||||
});
|
||||
|
||||
if (!deployResult.success) {
|
||||
throw new Error(`Deploy failed: ${deployResult.error?.message}`);
|
||||
}
|
||||
|
||||
const installResult = await appInstall({ appPath: APP_PATH });
|
||||
|
||||
if (!installResult.success) {
|
||||
throw new Error(`Install failed: ${installResult.error?.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await appUninstall({ appPath: APP_PATH });
|
||||
});
|
||||
|
||||
it('should find the installed app in the workspace', async () => {
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
const result = await metadataClient.query({
|
||||
findManyApplications: {
|
||||
id: true,
|
||||
name: true,
|
||||
universalIdentifier: true,
|
||||
},
|
||||
});
|
||||
|
||||
const installedApp = result.findManyApplications.find(
|
||||
(app: { universalIdentifier: string }) =>
|
||||
app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
expect(installedApp).toBeDefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## تشغيل الاختبارات
|
||||
|
||||
تأكّد من تشغيل خادم Twenty المحلي لديك، ثم:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn test
|
||||
```
|
||||
|
||||
أو في وضع المراقبة أثناء التطوير:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn test:watch
|
||||
```
|
||||
|
||||
## التحقق من الأنواع
|
||||
|
||||
يمكنك أيضًا تشغيل التحقق من الأنواع على تطبيقك دون تشغيل الاختبارات:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:typecheck
|
||||
```
|
||||
|
||||
يشغِّل هذا الأمر `tsc --noEmit` ويبلغ عن أي أخطاء في الأنواع.
|
||||
|
||||
## التكامل المستمر (CI) باستخدام GitHub Actions
|
||||
|
||||
تولّد أداة إنشاء الهيكل سير عمل GitHub Actions جاهزًا للاستخدام في `.github/workflows/ci.yml`. يشغّل اختبارات التكامل لديك تلقائيًا عند كل دفع إلى `main` وعلى طلبات السحب.
|
||||
|
||||
سير العمل:
|
||||
|
||||
1. يجلب الشيفرة الخاصة بك
|
||||
2. يشغّل خادم Twenty مؤقتًا باستخدام الإجراء `twentyhq/twenty/.github/actions/spawn-twenty-docker-image`
|
||||
3. يثبّت التبعيات باستخدام `yarn install --immutable`
|
||||
4. يشغّل `yarn test` مع حقن `TWENTY_API_URL` و`TWENTY_API_KEY` من مخرجات الإجراء
|
||||
|
||||
```yaml .github/workflows/ci.yml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
env:
|
||||
TWENTY_VERSION: latest
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Spawn Twenty instance
|
||||
id: twenty
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
|
||||
with:
|
||||
twenty-version: ${{ env.TWENTY_VERSION }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run integration tests
|
||||
run: yarn test
|
||||
env:
|
||||
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
|
||||
```
|
||||
|
||||
لا تحتاج إلى تهيئة أي أسرار — إذ يبدأ إجراء `spawn-twenty-docker-image` خادم Twenty عابرًا مباشرة في المشغّل ويُخرِج تفاصيل الاتصال. يتم توفير السر `GITHUB_TOKEN` تلقائيًا من قِبل GitHub.
|
||||
|
||||
لتثبيت إصدار محدّد من Twenty بدلًا من `latest`، غيّر متغير البيئة `TWENTY_VERSION` في أعلى سير العمل.
|
||||
@@ -88,7 +88,7 @@ Authorization: Bearer YOUR_API_KEY
|
||||
|
||||
لتحسين الأمان، عيّن دوراً محدداً لتقييد الوصول:
|
||||
|
||||
1. اذهب إلى **الإعدادات → الأدوار**
|
||||
1. انتقل إلى **الإعدادات → الأعضاء → الأدوار**
|
||||
2. انقر على الدور الذي ترغب في تعيينه
|
||||
3. افتح علامة التبويب **التعيين**
|
||||
4. ضمن **مفاتيح API**، انقر على **+ تعيين إلى مفتاح API**
|
||||
|
||||
@@ -51,7 +51,7 @@ VERSION=vx.y.z BRANCH=branch-name bash <(curl -sL https://raw.githubusercontent.
|
||||
curl -o .env https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/.env.example
|
||||
```
|
||||
|
||||
2. **إنشاء رموز سرية**
|
||||
2. **إنشاء مفتاح تشفير**
|
||||
|
||||
قم بتشغيل الأمر التالي لإنشاء سلسلة عشوائية فريدة:
|
||||
|
||||
@@ -59,16 +59,18 @@ VERSION=vx.y.z BRANCH=branch-name bash <(curl -sL https://raw.githubusercontent.
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
**مهم:** احتفظ بهذه القيمة سرية ولا تشاركها.
|
||||
**مهم:** احتفظ بهذه القيمة سرية ولا تشاركها. فقدان `ENCRYPTION_KEY` يعني فقدان الوصول إلى كل سر مخزَّن في قاعدة البيانات (رموز OAuth، متغيرات التطبيق، أسرار TOTP، إلخ).
|
||||
|
||||
3. **تحديث الـ `.env`**
|
||||
|
||||
استبدل قيمة النائب في ملف .env بالقيمة الرمزية المولدة:
|
||||
|
||||
```ini
|
||||
APP_SECRET=first_random_string
|
||||
ENCRYPTION_KEY=random_string
|
||||
```
|
||||
|
||||
راجع [دليل تدوير المفاتيح](/l/ar/developers/self-host/capabilities/key-rotation) للحصول على إرشادات حول تدويره بدون توقّف عن العمل.
|
||||
|
||||
4. **تعيين كلمة مرور PostgreSQL**
|
||||
|
||||
قم بتحديث قيمة `PG_DATABASE_PASSWORD` في ملف .env باستخدام كلمة مرور قوية بدون أحرف خاصة.
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: تدوير المفاتيح
|
||||
icon: rotate
|
||||
---
|
||||
|
||||
يمتلك Twenty عائلتين مستقلتين من المفاتيح:
|
||||
|
||||
* **مفاتيح توقيع JWT** — أزواج مفاتيح غير متماثلة ES256 (مع علامة `kid`) مُخزَّنة في `core."signingKey"`، تُستخدم لتوقيع والتحقق من رموز الوصول / التحديث.
|
||||
* **مفتاح التشفير أثناء السكون (At-rest encryption key)** — `ENCRYPTION_KEY`، يُستخدم لتشفير رموز OAuth، ومتغيرات التطبيق، ومفاتيح التوقيع الخاصة، وقيم الإعدادات الحساسة، وأسرار TOTP داخل غلاف `enc:v2:`.
|
||||
|
||||
يُعد `APP_SECRET` سراً قديماً مُحتفَظاً به لأغراض التوافق مع الإصدارات السابقة: عندما لا يكون `ENCRYPTION_KEY` مضبوطاً، فإنه يعمل كحل احتياطي لمفتاح التشفير أثناء السكون / ملفات تعريف الارتباط للجلسة، ولا يزال يتحقق من رموز الوصول HS256 الموجودة مسبقاً. سيتم إهماله (إيقاف دعمه).
|
||||
|
||||
## مفاتيح توقيع JWT
|
||||
|
||||
يحمل كل مفتاح قيمة `publicKey` (يُحتفَظ بها إلى أجل غير مسمى حتى يمكنها التحقق من الرموز المصدرة مسبقاً)، و`privateKey` مُشفَّراً (يُستخدم فقط أثناء كون المفتاح حالياً)، وراية `isCurrent` (صف واحد فقط في كل وقت)، وحقل `revokedAt` اختياري.
|
||||
|
||||
### تدوير المفتاح الحالي
|
||||
|
||||
اضبط `SIGNING_KEY_ROTATION_DAYS` للتفعيل: عندها تصدر مهمة cron يومية مفتاحًا حاليًا جديدًا بمجرد أن يصبح المفتاح القائم أقدم من تلك العتبة. لا يتم إبطال المفاتيح السابقة، لذلك تستمر الرموز الموقعة تحتها في التحقق. اترك المتغير غير معيّن لتعطيل التدوير التلقائي.
|
||||
|
||||
<Note>ميزة التدوير التلقائي متوفّرة ابتداءً من الإصدار v2.6+.</Note>
|
||||
|
||||
### إبطال مفتاح (في حالات التسريب / الطوارئ فقط)
|
||||
|
||||
**Settings → Admin Panel → Signing keys → Revoke** على صف غير حالي. تعمل على مسح المادة الخاصة المشفرة، وتعيين `revokedAt`، ورفض كل رمز حالي موقَّع تحت هذا الـ `kid`.
|
||||
|
||||
## تدوير `ENCRYPTION_KEY`
|
||||
|
||||
<Note>أمر `secret-encryption:rotate` الموضَّح أدناه متوفر ابتداءً من الإصدار v2.6+.</Note>
|
||||
|
||||
يُغلَّف كل مقدار مُشفَّر بالشكل `enc:v2:\<keyId>:\<payload>`، حيث إن `\<keyId>` هو بادئة مكوَّنة من 8 أرقام ست عشرية مشتقة من المفتاح الخام. تتم عملية التدوير (الاستبدال) أثناء العمل، وقابلة للاستئناف.
|
||||
|
||||
1. **إنشاء مفتاح جديد**: `openssl rand -base64 32`.
|
||||
|
||||
2. **ضَبْط المفتاحين معاً جنباً إلى جنب** في ملف `.env`، ثم إعادة تشغيل الخدمة:
|
||||
```ini
|
||||
ENCRYPTION_KEY=NEW_VALUE
|
||||
FALLBACK_ENCRYPTION_KEY=OLD_VALUE
|
||||
```
|
||||
تستخدم عمليات الكتابة الجديدة المفتاح الجديد، بينما لا تزال الصفوف الحالية تُفك تشفيرها عبر المفتاح الاحتياطي (fallback).
|
||||
|
||||
3. **إعادة تشفير الصفوف الحالية**:
|
||||
|
||||
```bash
|
||||
docker exec -it {server_container} yarn command:prod secret-encryption:rotate
|
||||
```
|
||||
|
||||
يتناول الأمر ستة مواقع (`connected-account-tokens`، `application-variable`، `application-registration-variable`، `signing-key-private-keys`، `sensitive-config-storage`، `totp-secrets`). يُهمِل عامل تصفية SQL الصفوف الموجودة بالفعل على `\<keyId>` الجديد، لذلك يكون الأمر عديم الأثر التكراري (idempotent): يمكنك مقاطعته وإعادة تشغيله حسب الحاجة. يخرج بقيمة مختلفة عن الصفر إذا فشل أي صف — أعد تشغيله لإعادة المحاولة.
|
||||
|
||||
| خيار | الوصف |
|
||||
| ---------------------------------------- | ------------------------------------------------------------- |
|
||||
| `-s, --site \<site>` | حصر التنفيذ على موقع واحد فقط. |
|
||||
| `-b, --batch-size \<n>` | عدد الصفوف في كل دفعة (الافتراضي `200`، والحد الأقصى `5000`). |
|
||||
| `-d, --dry-run` | فك التشفير + إعادة التشفير في الذاكرة، مع تخطي جملة `UPDATE`. |
|
||||
|
||||
4. **إزالة المفتاح الاحتياطي (fallback)** بمجرد أن يُظهِر الخيار `--dry-run` عدم وجود صفوف متبقية: أزِل `FALLBACK_ENCRYPTION_KEY` وأعد التشغيل.
|
||||
|
||||
## دعم `APP_SECRET` القديم
|
||||
|
||||
تستخدم النُسخ الأقدم التي لم تُعيِّن `ENCRYPTION_KEY` مطلقاً المتغيِّر `APP_SECRET` كمفتاح التشفير أثناء السكون (وكسرّ ملف تعريف الارتباط الخاص بالجلسة، المشتقّ منه). يُحفَظ هذا المسار لأغراض التوافق مع الإصدارات السابقة ولكنه **مهمل (موقوف الدعم)** — عيِّن متغير `ENCRYPTION_KEY` مخصَّصاً واتبع إجراء التدوير الموضَّح أعلاه للانتقال بعيداً عنه. يبقى `APP_SECRET` نفسه قيد الاستخدام للتحقق من رموز الوصول HS256 القديمة.
|
||||
@@ -43,11 +43,26 @@ IS_CONFIG_VARIABLES_IN_DB_ENABLED=true # افتراضي
|
||||
|
||||
<Warning>
|
||||
كل متغير موثق بوصف في لوحة الإدارة الخاصة بك في **الإعدادات → لوحة الإدارة → متغيرات التكوين**.
|
||||
بعض إعدادات البنية التحتية مثل اتصالات قاعدة البيانات (`PG_DATABASE_URL`)، عناوين الخوادم (`SERVER_URL`)، وأسرار التطبيقات (`APP_SECRET`) يمكن ضبطها فقط عبر ملف `.env`.
|
||||
بعض إعدادات البنية التحتية مثل اتصالات قاعدة البيانات (`PG_DATABASE_URL`)، عناوين الخوادم (`SERVER_URL`)، والأسرار (`ENCRYPTION_KEY`, `FALLBACK_ENCRYPTION_KEY`) يمكن ضبطها فقط عبر ملف `.env`.
|
||||
|
||||
[مرجع تقني كامل →](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts)
|
||||
</Warning>
|
||||
|
||||
## مفاتيح التشفير
|
||||
|
||||
تستخدم Twenty مفتاحَي تشفير يحددان حصراً عبر متغيرات البيئة:
|
||||
|
||||
| المتغيّر | الغرض | مطلوب |
|
||||
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `ENCRYPTION_KEY` | المفتاح الأساسي المستخدم لتشفير الأسرار أثناء التخزين (رموز OAuth، متغيرات التطبيق، المفاتيح الخاصة لمفاتيح التوقيع، أسرار TOTP، قيم الإعدادات الحساسة). | نعم في عمليات التثبيت الجديدة (قد تعتمد عمليات التثبيت القديمة بدلاً من ذلك على `APP_SECRET` — انظر أدناه) |
|
||||
| `FALLBACK_ENCRYPTION_KEY` | مفتاح مخصص للتحقق فقط. يتم تعيينه أثناء التدوير ليكون `ENCRYPTION_KEY` السابق حتى تظل الصفوف الحالية قابلة لفك التشفير. | فقط أثناء التدوير |
|
||||
|
||||
لضمان التوافق مع الإصدارات السابقة، إذا لم يتم تعيين `ENCRYPTION_KEY`، فإن Twenty تستخدم `APP_SECRET` كبديل لتشفير البيانات أثناء التخزين — بما يطابق سلوك الإصدارات القديمة في عمليات النشر الأقدم. يجب على عمليات التثبيت الجديدة دائماً تعيين قيمة مخصصة لـ `ENCRYPTION_KEY`.
|
||||
|
||||
قم بإنشاء القيم باستخدام الأمر `openssl rand -base64 32` وخزنها في مكان آمن (مثل مدير الأسرار، إعدادات مُشفَّرة، إلخ). فقدان `ENCRYPTION_KEY` يعني فقدان الوصول إلى كل سر مخزن في قاعدة البيانات.
|
||||
|
||||
لتدوير `ENCRYPTION_KEY` بدون وقت توقف، راجع [دليل تدوير المفاتيح](/l/ar/developers/self-host/capabilities/key-rotation).
|
||||
|
||||
## 2. إعداد بيئي فقط
|
||||
|
||||
```bash
|
||||
|
||||
@@ -31,6 +31,18 @@ cat databases_backup.sql | docker exec -i {db_container_name_or_id} psql -U {pos
|
||||
|
||||
على سبيل المثال، الترقية من v1.22 مباشرةً إلى v2.0 مدعومة بالكامل.
|
||||
|
||||
## الترقية إلى v2.5+ — غلاف التشفير للبيانات الساكنة (at-rest)
|
||||
|
||||
بدءًا من **v2.5**، يقوم Twenty بتخزين الأسرار أثناء السكون (at-rest) — مثل رموز OAuth، ومتغيرات التطبيق، ومفاتيح التوقيع الخاصة، وقيم الإعدادات الحساسة، وأسرار TOTP — داخل غلاف ذي إصدار `enc:v2:` ومشفّر باستخدام `ENCRYPTION_KEY` (أو `APP_SECRET` إذا لم يتم تعيين `ENCRYPTION_KEY`).
|
||||
|
||||
أول إقلاع على v2.5 يشغّل أوامر ترقية بطيئة تقوم بإجراء **ملء رجعي** للصفوف الموجودة داخل الغلاف الجديد. هذه الأوامر عديمة الأثر عند التكرار (idempotent) — إيقاف الخادم وإعادة تشغيله يستأنف من حيث توقّف — لكنها قد تستغرق بعض الوقت على قواعد البيانات الكبيرة. يمكنك متابعة التقدّم باستخدام `upgrade:status`.
|
||||
|
||||
يجب تعيين `ENCRYPTION_KEY` مخصص **قبل** الترقية إلى v2.5 لكي يكتب الملء الرجعي الصفوف باستخدامه منذ البداية. يتطلّب تبديل المفاتيح بعد الملء الرجعي إجراء [تدوير](/l/ar/developers/self-host/capabilities/key-rotation).
|
||||
|
||||
## تدوير الأسرار ومفاتيح التوقيع
|
||||
|
||||
لمهام التشغيل اليومية مثل تدوير `ENCRYPTION_KEY`، أو تدوير مفتاح توقيع JWT، أو إبطال مفتاح توقيع تم تسريبه، راجع [دليل تدوير المفاتيح (Key rotation guide)](/l/ar/developers/self-host/capabilities/key-rotation).
|
||||
|
||||
## التحقق من حالة الترقية
|
||||
|
||||
يتيح لك الأمر `upgrade:status` فحص الحالة الحالية لمثيلك وعمليات ترحيل مساحات العمل. يكون مفيدًا لاستكشاف مشكلات الترقية وإصلاحها أو عند تقديم طلب دعم.
|
||||
|
||||
@@ -155,7 +155,27 @@
|
||||
"label": "نظرة عامة"
|
||||
},
|
||||
"apps": {
|
||||
"label": "التطبيقات"
|
||||
"label": "التطبيقات",
|
||||
"groups": {
|
||||
"appsGettingStarted": {
|
||||
"label": "البدء"
|
||||
},
|
||||
"appsConfig": {
|
||||
"label": "التهيئة"
|
||||
},
|
||||
"appsData": {
|
||||
"label": "بيانات"
|
||||
},
|
||||
"appsLogic": {
|
||||
"label": "المنطق"
|
||||
},
|
||||
"appsLayout": {
|
||||
"label": "التخطيط"
|
||||
},
|
||||
"appsOperations": {
|
||||
"label": "العمليات"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api": {
|
||||
"label": "واجهة برمجة التطبيقات"
|
||||
|
||||
@@ -13,7 +13,7 @@ icon: مؤشر اليد
|
||||
|
||||
<Tabs>
|
||||
|
||||
<Tab title="27332A2E2F2745">
|
||||
<Tab title="Usage">
|
||||
|
||||
```jsx
|
||||
import { Button } from "@/ui/input/button/components/Button";
|
||||
|
||||
@@ -12,7 +12,7 @@ icon: لوحة الألوان
|
||||
يمثل مخططات ألوان مختلفة ومخصص بشكل خاص للمواضيع الفاتحة والداكنة.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="27332A2E2F2745">
|
||||
<Tab title="Usage">
|
||||
|
||||
```jsx
|
||||
import { ColorSchemeCard } from "twenty-ui/display";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
title: "\x062A\x062F\x062E\x064A\x0644 \x0627\x0644\x0635\x0648\x0631\x0629"
|
||||
title: Image Input
|
||||
---
|
||||
|
||||
<Frame>
|
||||
<img src="/images/user-guide/objects/objects.png" alt="رأس الصفحة" />
|
||||
</Frame>
|
||||
|
||||
4A4F33452D 44445245332A2E2F454A46 28452F 482532274429 35483129.
|
||||
Allows users to upload and remove an image.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
@@ -25,7 +25,7 @@ export const MyComponent = () => {
|
||||
|
||||
| الخصائص | النوع | الوصف |
|
||||
| ------------ | ----------- | --------------------------------------------------------------------------------- |
|
||||
| صورة | نص | 3946482746 45352F31 274435483129 27442544432A3148464A |
|
||||
| صورة | نص | The image source URL |
|
||||
| onUpload | دالة | الدالة التي تُستدعى عند قيام المستخدم بتحميل صورة جديدة. تستقبل كائن `File` كوسيط |
|
||||
| onRemove | دالة | الدالة التي تُستدعى عند نقر المستخدم على زر الإزالة |
|
||||
| onAbort | دالة | الدالة التي تُستدعى عند نقر المستخدم على زر الإلغاء أثناء تحميل الصورة |
|
||||
|
||||
@@ -38,7 +38,7 @@ export const MyComponent = () => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
},{
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ title: نص
|
||||
|
||||
<Tabs>
|
||||
|
||||
<Tab title="27332A2E2F2745">
|
||||
<Tab title="Usage">
|
||||
|
||||
```jsx
|
||||
import { TextInput } from "@/ui/input/components/TextInput";
|
||||
|
||||
@@ -12,7 +12,7 @@ icon: رابط
|
||||
مكون رابط منمق لعرض معلومات الاتصال.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="27332A2E2F2745">
|
||||
<Tab title="Usage">
|
||||
|
||||
```jsx
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
@@ -35,7 +35,7 @@ export const MyComponent = () => {
|
||||
</ContactLink>
|
||||
</Router>
|
||||
);
|
||||
};},{
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -103,11 +103,10 @@ MCP حاليًا في مرحلة **ألفا** وهو متاح فقط في بعض
|
||||
|
||||
بعد الاتصال، يوفّر خادم MCP أدوات تعكس واجهة برمجة تطبيقات Twenty (API). سير العمل الموصى به هو:
|
||||
|
||||
1. **`get_tool_catalog`** — اكتشف جميع الأدوات المتاحة
|
||||
2. **`learn_tools`** — احصل على مخطط الإدخال لأدوات محددة
|
||||
3. **`execute_tool`** — شغّل أداة
|
||||
1. **`learn_tools`** — احصل على مخطط الإدخال لأدوات محددة
|
||||
2. **`execute_tool`** — شغّل أداة
|
||||
|
||||
لا تحتاج إلى تذكّر أسماء الأدوات. اسأل مساعد الذكاء الاصطناعي عمّا يمكنه فعله وسيستدعي `get_tool_catalog` تلقائيًا.
|
||||
لا تحتاج إلى تذكّر أسماء الأدوات. اسأل مساعد الذكاء الاصطناعي عمّا يمكنه فعله وسيستدعي `learn_tools` تلقائيًا.
|
||||
|
||||
## الصلاحيات
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ description: تحكّم بما يمكن لوكلاء الذكاء الاصطنا
|
||||
|
||||
## تعيين دور لوكيل ذكاء اصطناعي
|
||||
|
||||
1. اذهب إلى **الإعدادات → الأدوار**
|
||||
1. انتقل إلى **الإعدادات → الأعضاء → الأدوار**
|
||||
2. انقر على الدور الذي ترغب في تعيينه
|
||||
3. افتح علامة التبويب **التعيين**
|
||||
4. ضمن **وكلاء الذكاء الاصطناعي**، انقر **+ تعيين لوكيل ذكاء اصطناعي**
|
||||
|
||||
@@ -17,7 +17,7 @@ description: الأسئلة الشائعة حول ميزات الذكاء الا
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="هل سيكون لوكلاء الذكاء الاصطناعي حق الوصول إلى جميع بياناتي؟">
|
||||
سيعمل وكلاء الذكاء الاصطناعي ضمن نظام الأذونات. يمكنك تعيين أدوار محددة لوكلاء الذكاء الاصطناعي ضمن **الإعدادات → الأدوار**، مما يمنحك سيطرة كاملة على البيانات التي يمكنهم الوصول إليها والإجراءات التي يمكنهم القيام بها.
|
||||
سيعمل وكلاء الذكاء الاصطناعي ضمن نظام الأذونات. يمكنك تعيين أدوار محددة لوكلاء الذكاء الاصطناعي ضمن **الإعدادات → الأعضاء → الأدوار**، مما يمنحك سيطرة كاملة على البيانات التي يمكنهم الوصول إليها والإجراءات التي يمكنهم القيام بها.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="كيف ستعمل أرصدة الذكاء الاصطناعي؟">
|
||||
|
||||
@@ -48,7 +48,7 @@ description: ميزات مدعومة بالذكاء الاصطناعي قادم
|
||||
|
||||
سيُدار وكلاء الذكاء الاصطناعي عبر نظام الأذونات الحالي:
|
||||
|
||||
1. اذهب إلى **الإعدادات → الأدوار**
|
||||
1. انتقل إلى **الإعدادات → الأعضاء → الأدوار**
|
||||
2. حدِّد البيانات التي يمكن لكل وكيل ذكاء اصطناعي الوصول إليها
|
||||
3. عيّن أذونات القراءة/الكتابة لكل كائن
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ description: تعرّف على خطط تسعير Twenty وكيفية التبد
|
||||
* دعم قياسي
|
||||
|
||||
<Note>
|
||||
Premium features (SSO, row-level permissions and AI usage data) are not included in the Pro plan.
|
||||
الميزات المتميزة (SSO، أذونات على مستوى الصف وبيانات استخدام الذكاء الاصطناعي) غير مشمولة في خطة Pro.
|
||||
</Note>
|
||||
|
||||
### المؤسسة (سحابي)
|
||||
@@ -27,7 +27,7 @@ Premium features (SSO, row-level permissions and AI usage data) are not included
|
||||
للفرق الأكبر ذات الاحتياجات المتقدّمة:
|
||||
|
||||
* كل ما في Pro
|
||||
* **Premium features**: SSO integration, row-level permissions and AI usage data
|
||||
* **الميزات المتميزة**: تكامل SSO، أذونات على مستوى الصف وبيانات استخدام الذكاء الاصطناعي
|
||||
* دعم متميز
|
||||
|
||||
## خطط الاستضافة الذاتية
|
||||
@@ -45,7 +45,7 @@ Premium features (SSO, row-level permissions and AI usage data) are not included
|
||||
للفرق التي تحتاج إلى ميزات متميزة أثناء الاستضافة الذاتية:
|
||||
|
||||
* جميع ميزات Pro
|
||||
* **Premium features**: SSO integration, row-level permissions and AI usage data
|
||||
* **الميزات المتميزة**: تكامل SSO، أذونات على مستوى الصف وبيانات استخدام الذكاء الاصطناعي
|
||||
* دعم فريق Twenty
|
||||
* لا يُشترط نشر الشيفرة المخصّصة كمفتوح المصدر قبل التوزيع
|
||||
|
||||
@@ -55,7 +55,7 @@ Premium features (SSO, row-level permissions and AI usage data) are not included
|
||||
|
||||
* **تكامل SSO**: تسجيل دخول أحادي مع موفّر الهوية لديك
|
||||
* **أذونات على مستوى الصف**: تحكّم دقيق في الوصول على مستوى السجل
|
||||
* **AI usage data**: Track AI consumption across the workspace
|
||||
* **بيانات استخدام الذكاء الاصطناعي**: تتبع استهلاك الذكاء الاصطناعي عبر مساحة العمل
|
||||
|
||||
## التبديل بين الخطط
|
||||
|
||||
@@ -79,14 +79,14 @@ Premium features (SSO, row-level permissions and AI usage data) are not included
|
||||
|
||||
تواصل مع الدعم للعودة إلى الفوترة الشهرية.
|
||||
|
||||
## Obtain an Enterprise Key for Organization (Self-Hosted)
|
||||
## احصل على مفتاح Enterprise لخطة Organization (Self-Hosted)
|
||||
|
||||
To use the Organization (Self-Hosted) plan, you need to obtain an Enterprise key:
|
||||
لاستخدام خطة Organization (Self-Hosted)، تحتاج إلى الحصول على مفتاح Enterprise:
|
||||
|
||||
1. Go to **Settings → Admin Panel → Enterprise**
|
||||
1. اذهب إلى **الإعدادات → لوحة الإدارة → Enterprise**
|
||||
|
||||
<img src="/images/user-guide/billing/enterprise-key.png" alt="Enterprise key" />
|
||||
<img src="/images/user-guide/billing/enterprise-key.png" alt="مفتاح Enterprise" />
|
||||
|
||||
2. Click **Get Enterprise Key**
|
||||
3. When you are redirected to Stripe, enter your payment details and confirm
|
||||
4. When your Enterprise key is displayed, paste it into the Enterprise settings page and activate the Organization license
|
||||
2. انقر **احصل على مفتاح Enterprise**
|
||||
3. عند إعادة توجيهك إلى Stripe، أدخل تفاصيل الدفع الخاصة بك وأكّد
|
||||
4. عند عرض مفتاح Enterprise الخاص بك، الصقه في صفحة إعدادات Enterprise وقم بتفعيل ترخيص Organization
|
||||
|
||||
@@ -25,15 +25,25 @@ description: Connect your email and calendar accounts to Twenty.
|
||||
6. Configure calendar sync settings (visibility, auto-creation) → click **Add Account**
|
||||
7. ستبدأ رسائل البريد الإلكتروني وفعاليات التقويم بالمزامنة تلقائيًا
|
||||
|
||||
### إعداد SMTP/CalDAV (مزودون آخرون)
|
||||
### إعداد IMAP/SMTP/CalDAV (مزودون آخرون)
|
||||
|
||||
بالنسبة لمزودي البريد الإلكتروني والتقويم الآخرين:
|
||||
|
||||
1. اذهب إلى **الإعدادات → الحسابات**
|
||||
2. قم بتكوين إعدادات SMTP للبريد الإلكتروني
|
||||
2. قم بتهيئة إعدادات IMAP لمزامنة رسائل البريد الإلكتروني الواردة وإعدادات SMTP لإرسال البريد الإلكتروني
|
||||
3. قم بتكوين إعدادات CalDAV للتقويم
|
||||
4. اختبر الاتصال
|
||||
|
||||
<Note>
|
||||
**الاستضافة الذاتية على شبكة معزولة هوائيًا أو شبكة داخلية**: بشكل افتراضي، يرفض Twenty الاتصالات الصادرة إلى عناوين IP الخاصة/الداخلية (حماية SSRF). إذا كان خادم البريد أو التقويم يعمل على عنوان IP محلي/خاص (مثل خادم داخلي على شبكة LAN)، فسيتم حظر الاتصالات به. للسماح بهذه الاتصالات، قم بتعيين متغير البيئة التالي على الخادم:
|
||||
|
||||
```
|
||||
OUTBOUND_HTTP_SAFE_MODE_ENABLED=false
|
||||
```
|
||||
|
||||
هذا يعطّل الوضع الآمن لكل طلبات الإرسال الصادرة (**HTTP workflow actions**، و **webhooks**، واتصالات **IMAP/SMTP/CalDAV**)، لذا لا تقم بتفعيله إلا على الشبكات المعزولة والموثوقة حيث لا تكون حماية SSRF مطلوبة.
|
||||
</Note>
|
||||
|
||||
### صناديق بريد متعددة
|
||||
|
||||
* **حسابات غير محدودة**: ربط حسابات بريد إلكتروني متعددة لكل مستخدم
|
||||
@@ -59,10 +69,19 @@ description: Connect your email and calendar accounts to Twenty.
|
||||
* **معطل**: لا يتم إنشاء جهات اتصال تلقائيًا
|
||||
* **للرسائل المرسلة والمستلمة**: إنشاء جهات اتصال لجميع التفاعلات البريدية الخارجية
|
||||
* **للرسائل المرسلة فقط**: إنشاء جهات اتصال فقط لرسائل البريد التي ترسلها
|
||||
* **ملاحظة**: لا تتم مزامنة رسائل البريد الداخلية (نفس النطاق) للحفاظ على الخصوصية
|
||||
* **ملاحظة**: بشكل افتراضي، لا تتم مزامنة الرسائل الإلكترونية الداخلية (عندما يشترك جميع المشاركين في نفس النطاق) لحماية الخصوصية
|
||||
|
||||
<Note>When enabled, contacts are automatically linked to their Company records based on their email domain. If the company doesn't exist yet, Twenty creates it for you.</Note>
|
||||
|
||||
<Note>
|
||||
**مزامنة الرسائل الإلكترونية الداخلية**: سلوك "عدم مزامنة الرسائل الإلكترونية الداخلية" هو السلوك الافتراضي، ولكن يمكن إيقافه. مفتاح التبديل موجود في الإعدادات المتقدمة:
|
||||
1. افتح **الإعدادات** وفعّل مفتاح التبديل **المتقدمة** في أسفل صفحة الإعدادات
|
||||
2. اذهب إلى **عام → الأمان**
|
||||
3. فعّل مفتاح التبديل **مزامنة الرسائل الإلكترونية الداخلية** لتضمين الرسائل الإلكترونية التي يشترك فيها جميع المشاركين في نفس النطاق
|
||||
|
||||
هذا إعداد على مستوى مساحة العمل (مفيد للجامعات أو المؤسسات ذات النطاق المشترك).
|
||||
</Note>
|
||||
|
||||
### التحكم بالرسائل التي تتم مزامنتها من خلال اختيار مجلد الرسائل
|
||||
|
||||
تحكم بما تم مزامنته من مجلدات البريد الإلكتروني مع Twenty:
|
||||
@@ -79,7 +98,7 @@ description: Connect your email and calendar accounts to Twenty.
|
||||
**ما الذي يتم مزامنته:**
|
||||
|
||||
* **رسائل البريد الخارجية**: جميع رسائل البريد الإلكتروني مع جهات اتصال خارجية من المجلدات المحددة
|
||||
* **الرسائل الداخلية**: لا يتم مزامنتها (تبقى رسائل البريد من نفس النطاق خاصة)
|
||||
* **الرسائل الداخلية**: لا تتم مزامنتها بشكل افتراضي (تبقى رسائل البريد من نفس النطاق خاصة). فعّل مفتاح التبديل **المتقدمة** في أسفل **الإعدادات**، ثم شغّل **مزامنة الرسائل الإلكترونية الداخلية** ضمن **عام → الأمان** لتضمينها على مستوى مساحة العمل.
|
||||
* **المرفقات**: ستأتي في النصف الأول من 2026
|
||||
|
||||
**ملاحظة**: لا نقدم عنوان بريد إلكتروني لنسخة كربونية للمزامنة الانتقائية. بدلاً من ذلك، استخدم ميزة مجلد الرسائل المذكورة أعلاه لتحقيق نفس مستوى التحكم حول أي الرسائل يتم مزامنتها مع Twenty.
|
||||
|
||||
@@ -56,7 +56,7 @@ import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
|
||||
استخدم التنسيق التالي:
|
||||
|
||||
```
|
||||
[\"value1\",\"value2\"]
|
||||
["value1","value2"]
|
||||
```
|
||||
|
||||
### حقول القيم المنطقية
|
||||
@@ -94,7 +94,7 @@ import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
|
||||
* للبريد الإلكتروني الإضافي: استخدم **Emails / Primary Email** للبريد الرئيسي، و**Emails / Additional Emails** بهذا التنسيق:
|
||||
|
||||
```
|
||||
[\"jane@twenty.com\",\"jane.doe@twenty.com\"]
|
||||
["jane@twenty.com","jane.doe@twenty.com"]
|
||||
```
|
||||
|
||||
### حقول المعرّف
|
||||
@@ -125,7 +125,7 @@ import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
|
||||
* للروابط الثانوية، استخدم عمود **Links / Secondary Links** بهذا التنسيق:
|
||||
|
||||
```
|
||||
[{\"url\":\"https://twenty.com\",\"label\":\"Twenty\"}]
|
||||
[{"url":"https://twenty.com","label":"Twenty"}]
|
||||
```
|
||||
|
||||
### حقول التحديد المتعدد
|
||||
@@ -133,7 +133,7 @@ import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
|
||||
استخدم **أسماء واجهة برمجة التطبيقات (API)** (وليس تسميات العرض) بالتنسيق التالي:
|
||||
|
||||
```
|
||||
[\"VALUE1\",\"VALUE2\"]
|
||||
["VALUE1","VALUE2"]
|
||||
```
|
||||
|
||||
اطّلع [هنا](#finding-api-names-for-select-fields) لمعرفة مكان العثور على أسماء واجهة برمجة التطبيقات.
|
||||
@@ -143,7 +143,7 @@ import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
|
||||
<Warning>
|
||||
**الاستيراد يستبدل، لا يضيف.**
|
||||
|
||||
إذا كان السجل يحتوي بالفعل على `VALUE2` و`VALUE3` محدّدين، ثم استوردت `[\"VALUE1\"]`، فسيحتوي السجل على `VALUE1` فقط بعد الاستيراد. يتم استبدال التحديدات السابقة، وليس دمجها.
|
||||
إذا كان السجل يحتوي بالفعل على `VALUE2` و`VALUE3` محدّدين، ثم استوردت `["VALUE1"]`، فسيحتوي السجل على `VALUE1` فقط بعد الاستيراد. يتم استبدال التحديدات السابقة، وليس دمجها.
|
||||
</Warning>
|
||||
|
||||
### حقول الأرقام
|
||||
|
||||
@@ -25,7 +25,7 @@ description: تنسيقات الملفات المدعومة لاستيراد ا
|
||||
## أفضل الممارسات لملفات CSV
|
||||
|
||||
* **المحدد**: استخدم الفاصلة (`,`) أو الفاصلة المنقوطة (`;`)
|
||||
* **محدد النص**: استخدم علامات الاقتباس المزدوجة (`\"`) للنص الذي يحتوي على فواصل
|
||||
* **محدد النص**: استخدم علامات الاقتباس المزدوجة (`"`) للنص الذي يحتوي على فواصل
|
||||
* **نهايات الأسطر**: Windows (CRLF) أو Unix (LF) كلاهما مدعومان
|
||||
* **القيم الفارغة**: اترك الخلايا فارغة، لا تستخدم "NULL" أو "N/A"
|
||||
|
||||
|
||||
+1
-1
@@ -214,7 +214,7 @@ Jane,Doe,jane@widgets.co,https://widgets.co
|
||||
|
||||
### تكوين الأدوار والصلاحيات
|
||||
|
||||
* قم بإعداد الأدوار في **الإعدادات → الأدوار**
|
||||
* قم بإعداد الأدوار في **الإعدادات → الأعضاء → الأدوار**
|
||||
* عيّن المستخدمين إلى الأدوار المناسبة
|
||||
|
||||
### اربط البريد الإلكتروني والتقويم
|
||||
|
||||
+1
-1
@@ -127,7 +127,7 @@ Acme Corp,https://acme.com,john@yourcompany.com
|
||||
|
||||
### الأدوار والصلاحيات
|
||||
|
||||
* قم بتكوين الأدوار في **الإعدادات → الأدوار**
|
||||
* قم بتكوين الأدوار في **الإعدادات → الأعضاء → الأدوار**
|
||||
* عيّن المستخدمين إلى الأدوار المناسبة
|
||||
|
||||
### التكاملات
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ description: دليل كامل خطوة بخطوة لتنسيق بياناتك
|
||||
* للعناوين الإضافية للبريد الإلكتروني، استخدم هذا التنسيق في عمود **Emails / Additional Emails**:
|
||||
|
||||
```
|
||||
[\"jane@twenty.com\",\"jane.doe@twenty.com\"]
|
||||
["jane@twenty.com","jane.doe@twenty.com"]
|
||||
```
|
||||
|
||||
### حقول النطاق
|
||||
|
||||
+2
-2
@@ -27,9 +27,9 @@ description: دليل كامل خطوة بخطوة لتحديث السجلات
|
||||
<Warning>
|
||||
**يتم استبدال حقول الاختيار المتعدد، ولا يتم دمجها.**
|
||||
|
||||
إذا كان السجل محدّدًا فيه `Option A` و`Option B`، وقمتَ باستيراد `[\"Option C\"]`، فسيحتوي السجل على `Option C` فقط بعد الاستيراد. تستبدل عملية الاستيراد جميع الاختيارات السابقة — ولا تضيف إليها.
|
||||
إذا كان السجل محدّدًا فيه `Option A` و`Option B`، وقمتَ باستيراد `["Option C"]`، فسيحتوي السجل على `Option C` فقط بعد الاستيراد. تستبدل عملية الاستيراد جميع الاختيارات السابقة — ولا تضيف إليها.
|
||||
|
||||
للاحتفاظ بالقيم الحالية، ضمّنها جميعًا في عملية الاستيراد: `[\"Option A\",\"Option B\",\"Option C\"]`
|
||||
للاحتفاظ بالقيم الحالية، ضمّنها جميعًا في عملية الاستيراد: `["Option A","Option B","Option C"]`
|
||||
</Warning>
|
||||
|
||||
## الخطوة 1: تصدير بياناتك الحالية
|
||||
|
||||
@@ -101,6 +101,10 @@ import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
|
||||
|
||||
إذا ظهرت لك رسالة خطأ عند تعيين خاصية التفرد، فتحقق من وجود قيم مكررة في بياناتك (بما في ذلك السجلات المحذوفة).
|
||||
|
||||
## الفهارس (متقدّم)
|
||||
|
||||
تُدار فهارس قاعدة البيانات تلقائيًا — ونادرًا ما تكون إضافة فهارسك الخاصة ضرورية، ومن السهل الوقوع في الأخطاء عند القيام بذلك. مع تفعيل الوضع المتقدّم، يكون لكل كائن قسم **Indexes** تحت `Settings → Data Model → <object>` للحالات التي تعلم فيها أنك بحاجة إلى فهرس.
|
||||
|
||||
## أفضل ممارسات تكوين الحقول
|
||||
|
||||
### اتفاقيات التسمية والقيود
|
||||
|
||||
+23
-7
@@ -13,7 +13,7 @@ description: تحكّم في الوصول إلى الكائنات والحقول
|
||||
|
||||
لإنشاء دور جديد:
|
||||
|
||||
1. اذهب إلى **الإعدادات → الأدوار**
|
||||
1. انتقل إلى **الإعدادات → الأعضاء → الأدوار**
|
||||
2. تحت **كل الأدوار**، انقر على **+ إنشاء دور**
|
||||
3. أدخل اسم الدور
|
||||
4. في علامة التبويب الافتراضية **الأذونات**، [كوّن الأذونات](#customize-permissions)
|
||||
@@ -23,7 +23,7 @@ description: تحكّم في الوصول إلى الكائنات والحقول
|
||||
|
||||
لحذف دور:
|
||||
|
||||
1. اذهب إلى **الإعدادات → الأدوار**
|
||||
1. انتقل إلى **الإعدادات → الأعضاء → الأدوار**
|
||||
2. انقر على الدور الذي ترغب في إزالته
|
||||
3. افتح علامة التبويب **الإعدادات**، ثم انقر على **حذف الدور**
|
||||
4. انقر على **تأكيد** في النافذة المنبثقة
|
||||
@@ -36,13 +36,13 @@ description: تحكّم في الوصول إلى الكائنات والحقول
|
||||
|
||||
### عرض التعيينات الحالية
|
||||
|
||||
* اذهب إلى **الإعدادات → الأدوار**
|
||||
* انتقل إلى **الإعدادات → الأعضاء → الأدوار**
|
||||
* رؤية جميع الأدوار وعدد الأعضاء المعينين لكل منها
|
||||
* عرض الأعضاء الذين لديهم الأدوار المختلفة
|
||||
|
||||
### تعيين دور لعضو
|
||||
|
||||
1. اذهب إلى **الإعدادات → الأدوار**
|
||||
1. انتقل إلى **الإعدادات → الأعضاء → الأدوار**
|
||||
2. انقر على الدور الذي ترغب في تعيينه
|
||||
3. افتح علامة التبويب **التعيين**
|
||||
4. انقر على **+ تعيين لعضو**
|
||||
@@ -51,7 +51,7 @@ description: تحكّم في الوصول إلى الكائنات والحقول
|
||||
|
||||
### تعيين الدور الافتراضي
|
||||
|
||||
1. اذهب إلى **الإعدادات → الأدوار**
|
||||
1. انتقل إلى **الإعدادات → الأعضاء → الأدوار**
|
||||
2. في قسم **الخيارات**، اعثر على **الدور الافتراضي**
|
||||
3. اختر أي دور يجب أن يحصل عليه الأعضاء الجدد تلقائيًا
|
||||
4. سيتم تعيين أعضاء مساحة العمل الجدد هذا الدور عند انضمامهم
|
||||
@@ -98,6 +98,22 @@ description: تحكّم في الوصول إلى الكائنات والحقول
|
||||
| الفرص → تعطيل "عرض السجلات" | لا يمكن للمتدرب رؤية كائن الفرص إطلاقًا |
|
||||
| الأشخاص → تفعيل "تحرير السجلات" | يمكن للمتدرب تحرير سجلات الأشخاص (ولكن ليس الكائنات الأخرى) |
|
||||
|
||||
### أذونات مستوى الصف
|
||||
|
||||
<Note>
|
||||
أذونات مستوى الصف هي **ميزة Premium** متاحة ضمن خطة **Organization** (السحابي والمستضاف ذاتيًا).
|
||||
</Note>
|
||||
|
||||
تتيح لك أذونات مستوى الصف تقييد السجلات الفردية التي يمكن للدور عرضها أو تعديلها، بناءً على معايير ديناميكية. على عكس أذونات الكائن (التي تنطبق على نوع الكائن بالكامل)، تقوم أذونات مستوى الصف بتقييم كل سجل بشكل مستقل.
|
||||
|
||||
**أمثلة لحالات الاستخدام:**
|
||||
|
||||
* يمكن لمندوبي المبيعات رؤية الفرص الخاصة بهم فقط
|
||||
* يمكن للمديرين رؤية جميع السجلات في منطقتهم
|
||||
* يمكن لوكلاء الدعم عرض التذاكر المخصصة لهم فقط
|
||||
|
||||
لتهيئة أذونات مستوى الصف، افتح دورًا معيّنًا، وانتقل إلى علامة تبويب **Objects**، واستخدم قسم **Row-Level** لتحديد شروط التصفية لكائن معيّن.
|
||||
|
||||
### أذونات الحقول
|
||||
|
||||
داخل كل قاعدة على مستوى الكائن، يمكنك المتابعة أبعد من ذلك وتكوين **أذونات على مستوى الحقل** للتحكم في الوصول إلى حقول محددة.
|
||||
@@ -168,7 +184,7 @@ description: تحكّم في الوصول إلى الكائنات والحقول
|
||||
|
||||
### تعيين دور لمفتاح API
|
||||
|
||||
1. اذهب إلى **الإعدادات → الأدوار**
|
||||
1. انتقل إلى **الإعدادات → الأعضاء → الأدوار**
|
||||
2. انقر على الدور الذي ترغب في تعيينه
|
||||
3. افتح علامة التبويب **التعيين**
|
||||
4. ضمن **مفاتيح API**، انقر على **+ تعيين إلى مفتاح API**
|
||||
@@ -183,7 +199,7 @@ description: تحكّم في الوصول إلى الكائنات والحقول
|
||||
|
||||
### تعيين دور لوكيل ذكاء اصطناعي
|
||||
|
||||
1. اذهب إلى **الإعدادات → الأدوار**
|
||||
1. انتقل إلى **الإعدادات → الأعضاء → الأدوار**
|
||||
2. انقر على الدور الذي ترغب في تعيينه
|
||||
3. افتح علامة التبويب **التعيين**
|
||||
4. ضمن **وكلاء الذكاء الاصطناعي**، انقر على **+ تعيين إلى وكيل ذكاء اصطناعي**
|
||||
|
||||
@@ -19,7 +19,7 @@ description: الأسئلة الشائعة حول الأدوار والصلاح
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="كيف أعيّن دورًا افتراضيًا للأعضاء الجدد؟">
|
||||
انتقل إلى **Settings → Roles**، وابحث عن خيار **Default Role**، ثم اختر الدور الذي يجب أن يحصل عليه الأعضاء الجدد تلقائيًا عند انضمامهم.
|
||||
انتقل إلى **الإعدادات → الأعضاء → الأدوار**، وابحث عن خيار **Default Role**، ثم اختر الدور الذي يجب أن يحصل عليه الأعضاء الجدد تلقائيًا عند انضمامهم.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="هل يمكنني تعيين عدة أدوار لمستخدم واحد؟">
|
||||
@@ -60,11 +60,11 @@ description: الأسئلة الشائعة حول الأدوار والصلاح
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="هل يمكنني تقييد الوصول إلى سجلات محددة (صلاحيات على مستوى الصف)؟">
|
||||
ستكون الصلاحيات على مستوى الصف متاحة ضمن خطة **Organization** بحلول الربع الأول من عام 2026. يتيح لك ذلك تقييد الوصول إلى سجلات محددة بناءً على معايير معينة (مثل: رؤية فرصك الخاصة فقط).
|
||||
تتوفر الصلاحيات على مستوى الصف ضمن خطة **Organization**. يتيح لك ذلك تقييد الوصول إلى سجلات محددة بناءً على معايير معينة (مثل: رؤية فرصك الخاصة فقط).
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="كيف أجعل حقلًا للقراءة فقط لمستخدمين معيّنين؟">
|
||||
1. اذهب إلى **الإعدادات → الأدوار**
|
||||
1. انتقل إلى **الإعدادات → الأعضاء → الأدوار**
|
||||
2. اختر الدور
|
||||
3. انتقل إلى الكائن الذي يحتوي على الحقل
|
||||
4. عيّن صلاحية الحقل إلى **See Field** (من دون Edit Field)
|
||||
|
||||
@@ -3,10 +3,12 @@ title: إعدادات النطاق
|
||||
description: قم بتكوين نطاق مساحة العمل، ونطاقات الوصول المعتمدة، والنطاقات العامة.
|
||||
---
|
||||
|
||||
قم بتكوين إعدادات النطاق ضمن **الإعدادات → النطاقات**.
|
||||
إعدادات النطاق موجودة في ثلاثة أماكن، حسب ما تريد تكوينه.
|
||||
|
||||
## نطاق مساحة العمل
|
||||
|
||||
قم بالتكوين ضمن **الإعدادات → عام → نطاق مساحة العمل**.
|
||||
|
||||
قم بتعديل اسم النطاق الفرعي الخاص بك أو عيّن نطاقًا مخصصًا لمساحة العمل.
|
||||
|
||||
### تخصيص النطاق
|
||||
@@ -19,6 +21,8 @@ description: قم بتكوين نطاق مساحة العمل، ونطاقات
|
||||
|
||||
## النطاقات المعتمدة
|
||||
|
||||
قم بالتكوين ضمن **الإعدادات → الأعضاء → دعوة**.
|
||||
|
||||
يُسمح لأي شخص لديه عنوان بريد إلكتروني ضمن هذه النطاقات بالتسجيل تلقائيًا في مساحة العمل هذه.
|
||||
|
||||
### إضافة نطاق وصول معتمد
|
||||
@@ -35,13 +39,16 @@ description: قم بتكوين نطاق مساحة العمل، ونطاقات
|
||||
|
||||
## النطاقات العامة
|
||||
|
||||
توفير بيئة استضافة كاملة وآمنة على هذه النطاقات.
|
||||
قم بالتكوين ضمن **الإعدادات → التطبيقات → المطور**.
|
||||
|
||||
توفير بيئة استضافة كاملة وآمنة على هذه النطاقات. يمكن ربط نطاق عام بتطبيق معيّن — عند ربطه، تكون دوال منطق HTTP الموجهة لهذا التطبيق وحدها قابلة للوصول على هذا النطاق. اترك الربط فارغًا لكشف جميع مسارات HTTP في مساحة العمل.
|
||||
|
||||
### إضافة نطاق عام
|
||||
|
||||
1. انقر **إضافة نطاق عام**
|
||||
2. أدخل النطاق الذي تريد استخدامه
|
||||
3. قم بتكوين إعدادات DNS وفق التعليمات
|
||||
4. تحقق من النطاق
|
||||
3. اربطه اختياريًا بتطبيق
|
||||
4. قم بتكوين إعدادات DNS وفق التعليمات
|
||||
5. تحقق من النطاق
|
||||
|
||||
يتم توفير شهادات SSL تلقائيًا للنطاقات العامة.
|
||||
|
||||
@@ -77,7 +77,7 @@ description: دعوة أعضاء الفريق وإدارة الوصول إلى
|
||||
|
||||
السماح لأعضاء الفريق بالانضمام تلقائيًا بناءً على نطاق بريدهم الإلكتروني:
|
||||
|
||||
1. اذهب إلى **الإعدادات → النطاقات**
|
||||
1. انتقل إلى **الإعدادات → الأعضاء → دعوة**
|
||||
2. أضف نطاق شركتك (مثل: `yourcompany.com`)
|
||||
3. يمكن لأي شخص ينتمي بريده الإلكتروني إلى ذلك النطاق الانضمام من دون دعوة
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ description: الأسئلة الشائعة حول إعدادات Twenty.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="هل يمكنني تخصيص دومين مساحة العمل الخاصة بي؟">
|
||||
نعم! اذهب إلى **الإعدادات → النطاقات** ثم انقر **تخصيص النطاق**. لديك خياران:
|
||||
نعم! اذهب إلى **الإعدادات → عام → نطاق مساحة العمل** ثم انقر **تخصيص النطاق**. لديك خياران:
|
||||
|
||||
* **النطاق الفرعي**: استخدم نطاقًا فرعيًا من Twenty مثل `yourcompany.twenty.com`
|
||||
* **نطاق مخصص**: استخدم نطاقك الخاص مثل `crm.yourcompany.com` (يتطلب تهيئة DNS)
|
||||
@@ -146,7 +146,7 @@ description: الأسئلة الشائعة حول إعدادات Twenty.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="كيف تعمل نطاقات الوصول المعتمدة؟">
|
||||
يمكنك تكوين نطاقات الوصول المعتمدة بحيث يتمكن أعضاء الفريق ذوو عناوين البريد الإلكتروني الخاصة بالشركة من الانضمام تلقائيًا إلى مساحة العمل الخاصة بك. اذهب إلى **الإعدادات → النطاقات** وأضف نطاق شركتك (مثلًا، `yourcompany.com`).
|
||||
يمكنك تكوين نطاقات الوصول المعتمدة بحيث يتمكن أعضاء الفريق ذوو عناوين البريد الإلكتروني الخاصة بالشركة من الانضمام تلقائيًا إلى مساحة العمل الخاصة بك. اذهب إلى **الإعدادات → الأعضاء → دعوة** وأضف نطاق شركتك (مثلًا، `yourcompany.com`).
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ description: قم بإعداد مساحة عملك في Twenty باستخدام
|
||||
4. عيّن الأدوار المناسبة
|
||||
|
||||
<Note>
|
||||
قبل دعوة فريقك، تحقّق من الدور الافتراضي ضمن **الإعدادات → الأدوار**. يتم تعيين هذا الدور تلقائيًا للأعضاء الجدد عند انضمامهم.
|
||||
قبل دعوة فريقك، تحقّق من الدور الافتراضي ضمن **الإعدادات → الأعضاء → الأدوار**. يتم تعيين هذا الدور تلقائيًا للأعضاء الجدد عند انضمامهم.
|
||||
</Note>
|
||||
|
||||
## قائمة التحقق لإعدادات مساحة العمل
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ description: احسب واعرض قيم الصفقات الموزونة استن
|
||||
|
||||
إذا كنت لا تريد أن يحرّر المستخدمون هذه الحقول المحسوبة يدويًا:
|
||||
|
||||
1. اذهب إلى **الإعدادات → الأدوار**
|
||||
1. انتقل إلى **الإعدادات → الأعضاء → الأدوار**
|
||||
2. حدِّد الدور لتكوينه
|
||||
3. اعثر على كائن «الفرص»
|
||||
4. عيّن حقلي **الاحتمال** و**المبلغ المتوقع** كحقول للقراءة فقط
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ description: راقِب سرعة الصفقات بتتبُّع وقت دخول
|
||||
|
||||
إذا كنت لا تريد أن يحرّر المستخدمون هذه الحقول المحسوبة يدويًا:
|
||||
|
||||
1. اذهب إلى **الإعدادات → الأدوار**
|
||||
1. انتقل إلى **الإعدادات → الأعضاء → الأدوار**
|
||||
2. حدِّد الدور لتهيئته
|
||||
3. اعثر على كائن الفرص
|
||||
4. عيِّن حقول "آخر دخول" و"الأيام في" كحقول للقراءة فقط
|
||||
|
||||
@@ -307,5 +307,5 @@ import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
يحترم وكلاء الذكاء الاصطناعي الأذونات المستندة إلى الأدوار. يمكنك تعيين أدوار محددة للوكلاء ضمن **الإعدادات → الأدوار** للتحكم في البيانات التي يمكنهم الوصول إليها. راجع [الأذونات](/l/ar/user-guide/permissions-access/capabilities/permissions) للحصول على التفاصيل.
|
||||
يحترم وكلاء الذكاء الاصطناعي الأذونات المستندة إلى الأدوار. يمكنك تعيين أدوار محددة للوكلاء ضمن **الإعدادات → الأعضاء → الأدوار** للتحكم في البيانات التي يمكنهم الوصول إليها. راجع [الأذونات](/l/ar/user-guide/permissions-access/capabilities/permissions) للحصول على التفاصيل.
|
||||
</Note>
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ description: الأسئلة الشائعة حول سير العمل في Twenty.
|
||||
<Accordion title="لماذا لا أستطيع تفعيل سير عمل؟">
|
||||
من المحتمل أن تكون مشكلة أذونات. تحتاج إلى صلاحية الوصول إلى سير العمل لإنشائها وتفعيلها.
|
||||
|
||||
**الحل**: تواصل مع مسؤول مساحة العمل لمنحك صلاحية الوصول إلى سير العمل ضمن **الإعدادات → الأدوار**.
|
||||
**الحل**: تواصل مع مسؤول مساحة العمل لمنحك صلاحية الوصول إلى سير العمل ضمن **الإعدادات → الأعضاء → الأدوار**.
|
||||
|
||||
إذا لم ترَ قسم سير العمل إطلاقاً في الشريط الجانبي لديك، فهذا يؤكد أنها مشكلة أذونات.
|
||||
</Accordion>
|
||||
|
||||
@@ -37,7 +37,7 @@ Both are available as REST and GraphQL. GraphQL adds batch upserts and the abili
|
||||
Authorization: Bearer YOUR_API_KEY
|
||||
```
|
||||
|
||||
Create an API key in **Settings → API & Webhooks → + Create key**. Copy it immediately — it's shown once. Keys can be scoped to a specific role under **Settings → Roles → Assignment tab** to limit what they can access.
|
||||
Create an API key in **Settings → API & Webhooks → + Create key**. Copy it immediately — it's shown once. API klíče lze omezit na konkrétní roli v **Settings → Members → Roles → Assignment tab**, aby se omezilo, k čemu mají přístup.
|
||||
|
||||
<VimeoEmbed videoId="928786722" title="Vytvoření klíče API" />
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: Konfigurace aplikace
|
||||
description: Deklarujte identitu své aplikace, výchozí roli, proměnné a metadata tržiště pomocí defineApplication.
|
||||
icon: rocket
|
||||
---
|
||||
|
||||
Každá aplikace musí mít právě jedno volání `defineApplication`. Deklaruje:
|
||||
|
||||
* **Identita** — univerzální identifikátor, zobrazovaný název, popis.
|
||||
* **Oprávnění** — pod jakou rolí běží její logické funkce a frontendové komponenty.
|
||||
* **Proměnné** *(volitelné)* — páry klíč–hodnota zpřístupněné vašemu kódu jako proměnné prostředí.
|
||||
* **Předinstalační / postinstalační hooky** *(volitelné)* — viz [Logické funkce](/l/cs/developers/extend/apps/logic/logic-functions).
|
||||
|
||||
```ts src/application-config.ts
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
|
||||
displayName: 'My Twenty App',
|
||||
description: 'My first Twenty app',
|
||||
applicationVariables: {
|
||||
DEFAULT_RECIPIENT_NAME: {
|
||||
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
|
||||
description: 'Default recipient name for postcards',
|
||||
value: 'Jane Doe',
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Poznámky:
|
||||
|
||||
* Pole `universalIdentifier` jsou deterministické identifikátory, které vlastníte. Vygenerujte je jednou a zachovejte je stabilní napříč synchronizacemi.
|
||||
* `applicationVariables` se stanou proměnnými prostředí pro vaše funkce a frontendové komponenty. V logických funkcích (na straně serveru) jsou dostupné jako `process.env.VARIABLE_NAME`. Ve frontendových komponentách použijte `getApplicationVariable('VARIABLE_NAME')` z `twenty-sdk/front-component`. Proměnné označené jako `isSecret: true` jsou předávány pouze do logických funkcí. Frontendové komponenty přijímají pouze proměnné, které nejsou tajné.
|
||||
* Výchozí role je automaticky detekována ze souboru role označeného pomocí [`defineApplicationRole()`](/l/cs/developers/extend/apps/config/roles) — není potřeba na ni odkazovat z `defineApplication()`.
|
||||
* Předinstalační a postinstalační funkce jsou při sestavení manifestu detekovány automaticky — není třeba na ně odkazovat v `defineApplication()`.
|
||||
* Předávání `defaultRoleUniversalIdentifier` explicitně je stále podporováno kvůli zpětné kompatibilitě, ale je zastaralé ve prospěch `defineApplicationRole()`.
|
||||
|
||||
## Výchozí role funkce
|
||||
|
||||
Role deklarovaná pomocí [`defineApplicationRole()`](/l/cs/developers/extend/apps/config/roles) určuje, k čemu mají přístup logické funkce a front-endové komponenty aplikace:
|
||||
|
||||
* Běhový token vložený jako `TWENTY_APP_ACCESS_TOKEN` je odvozen z této role.
|
||||
* Typovaný klient API je omezen na oprávnění udělená této roli.
|
||||
* Dodržujte princip nejmenších oprávnění: deklarujte pouze ta oprávnění, která vaše funkce potřebují.
|
||||
|
||||
Když vygenerujete novou aplikaci, CLI vytvoří úvodní soubor role v `src/roles/default-role.ts`. Úplnou referenci najdete v části [Role a oprávnění](/l/cs/developers/extend/apps/config/roles).
|
||||
|
||||
## Metadata tržiště
|
||||
|
||||
Pokud plánujete [zveřejnit svou aplikaci](/l/cs/developers/extend/apps/operations/publishing), tato volitelná pole určují, jak se vaše aplikace zobrazuje v tržišti:
|
||||
|
||||
| Pole | Popis |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------- |
|
||||
| `author` | Jméno autora nebo název společnosti |
|
||||
| `category` | Kategorie aplikace pro filtrování v tržišti |
|
||||
| `logoUrl` | Cesta k logu vaší aplikace (např. `public/logo.png`) |
|
||||
| `screenshots` | Pole cest ke snímkům obrazovky (např. `public/screenshot-1.png`) |
|
||||
| `aboutDescription` | Delší popis v Markdownu pro kartu "O aplikaci". Pokud je vynecháno, tržiště použije `README.md` balíčku z npm |
|
||||
| `websiteUrl` | Odkaz na váš web |
|
||||
| `termsUrl` | Odkaz na podmínky služby |
|
||||
| `emailSupport` | E-mailová adresa podpory |
|
||||
| `issueReportUrl` | Odkaz na nástroj pro sledování problémů |
|
||||
@@ -0,0 +1,206 @@
|
||||
---
|
||||
title: Instalační hooky
|
||||
description: Spouštějte logiku před instalací nebo po ní — naplňte data, zazálohujte záznamy, ověřte aktualizaci.
|
||||
icon: klíč
|
||||
---
|
||||
|
||||
Instalační hooky jsou speciální logické funkce, které se spouštějí během životního cyklu instalace nebo upgradu. Sdílí stejný runtime handleru jako běžné [logické funkce](/l/cs/developers/extend/apps/logic/logic-functions) a přijímají `InstallPayload`, ale deklarují se pomocí vlastních definičních funkcí — `definePostInstallLogicFunction()` a `definePreInstallLogicFunction()` — a fungují mimo běžný model triggerů (HTTP, cron, databázové události).
|
||||
|
||||
Každá aplikace může definovat **nanejvýš jednu pre-install** a **nanejvýš jednu post-install** funkci. Sestavení manifestu skončí chybou, pokud je zjištěno více než jedno.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ install flow │
|
||||
│ │
|
||||
│ upload package → [pre-install] → metadata migration → │
|
||||
│ generate SDK → [post-install] │
|
||||
│ │
|
||||
│ old schema visible new schema visible │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="definePostInstallLogicFunction" description="Spouští se po aplikování migrace metadat pracovního prostoru">
|
||||
|
||||
Postinstalační funkce se spustí automaticky, jakmile je instalace vaší aplikace v pracovním prostoru dokončena. Server ji provede **poté**, co byla synchronizována metadata aplikace a vygenerován klient SDK, takže je pracovní prostor plně připraven k použití a nové schéma je zavedeno. Mezi typické případy použití patří naplnění výchozími daty, vytvoření počátečních záznamů, konfigurace nastavení pracovního prostoru nebo zřizování prostředků ve službách třetích stran.
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Post install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
shouldRunSynchronously: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
Postinstalační funkci můžete také kdykoli spustit ručně pomocí CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:function:exec --postInstall
|
||||
```
|
||||
|
||||
Hlavní body:
|
||||
* Postinstalační funkce používají `definePostInstallLogicFunction()` — specializovanou variantu, která vynechává nastavení spouštěčů (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`).
|
||||
* Obslužná funkce obdrží `InstallPayload` s `{ previousVersion?: string; newVersion: string }` — `newVersion` je verze, která se instaluje, a `previousVersion` je verze, která byla nainstalována dříve (nebo `undefined` při čisté instalaci). Tyto hodnoty použijte k rozlišení čistých instalací od aktualizací a ke spuštění migrační logiky specifické pro verzi.
|
||||
* **Kdy se hook spouští**: ve výchozím nastavení pouze při čistých instalacích. Předejte `shouldRunOnVersionUpgrade: true`, pokud chcete, aby se spouštěl i při aktualizaci aplikace z předchozí verze. Pokud je vynechán, příznak má výchozí hodnotu `false` a při aktualizacích se hook přeskočí.
|
||||
* **Model provádění — ve výchozím nastavení asynchronní, synchronní volitelně**: příznak `shouldRunSynchronously` určuje *jak* se spouští post-install.
|
||||
* `shouldRunSynchronously: false` *(výchozí)* — hook je **zařazen do fronty zpráv** s `retryLimit: 3` a běží asynchronně ve workeru. Odezva instalace se vrátí hned po zařazení úlohy do fronty, takže pomalá nebo chybující obslužná funkce neblokuje volajícího. Worker se pokusí o opakování až třikrát. **Použijte pro dlouho běžící úlohy** — plnění velkých datových sad, volání pomalých externích API, zřizování externích prostředků, cokoli, co by mohlo přesáhnout rozumné časové okno HTTP odezvy.
|
||||
* `shouldRunSynchronously: true` — hook se provádí **inline během instalačního procesu** (stejný vykonavatel jako pre-install). Instalační požadavek blokuje, dokud obslužná funkce nedokončí, a pokud vyvolá výjimku, volající instalace obdrží `POST_INSTALL_ERROR`. Žádné automatické opakování. **Použijte pro rychlé úlohy, které se musí dokončit před odpovědí** — například vrácení validační chyby uživateli nebo rychlé nastavení, na kterém bude klient záviset ihned po návratu volání instalace. Mějte na paměti, že v době, kdy se spustí post-install, už byla migrace metadat aplikována, takže selhání v synchronním režimu změny schématu **ne**vrací zpět — pouze odhalí chybu.
|
||||
* Ujistěte se, že vaše obslužná funkce je idempotentní. V asynchronním režimu se může fronta pokusit až třikrát; v obou režimech se může hook znovu spustit při aktualizacích, pokud je `shouldRunOnVersionUpgrade: true`.
|
||||
* Proměnné prostředí `APPLICATION_ID`, `APP_ACCESS_TOKEN` a `API_URL` jsou dostupné uvnitř obslužné funkce (stejně jako u jakékoli jiné logické funkce), takže můžete volat Twenty API s aplikačním přístupovým tokenem omezeným na vaši aplikaci.
|
||||
* Na jednu aplikaci je povolena pouze jedna postinstalační funkce. Sestavení manifestu skončí chybou, pokud je zjištěna více než jedna.
|
||||
* Atributy funkce `universalIdentifier`, `shouldRunOnVersionUpgrade` a `shouldRunSynchronously` jsou během buildu automaticky připojeny k manifestu aplikace do pole `postInstallLogicFunction` — není potřeba je uvádět v [`defineApplication()`](/l/cs/developers/extend/apps/config/application).
|
||||
* Výchozí časový limit je nastaven na 300 sekund (5 minut), aby umožnil delší úlohy nastavení, jako je naplnění daty.
|
||||
* **Nespouští se v režimu dev**: když je aplikace registrována lokálně (pomocí `yarn twenty dev`), server zcela přeskočí instalační tok a synchronizuje soubory přímo prostřednictvím sledovače CLI — takže se post-install v režimu dev nikdy nespustí bez ohledu na `shouldRunSynchronously`. Použijte `yarn twenty dev:function:exec --postInstall` k ručnímu spuštění nad běžícím pracovním prostorem.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePreInstallLogicFunction" description="Spouští se před aplikováním migrace metadat pracovního prostoru">
|
||||
|
||||
Předinstalační funkce se během instalace spouští automaticky, **před aplikováním migrace metadat pracovního prostoru**. Má stejný tvar payloadu jako post-install (`InstallPayload`), ale je zařazena dříve v instalačním toku, aby mohla připravit stav, na němž nadcházející migrace závisí — typické použití zahrnuje zálohování dat, ověření kompatibility s novým schématem nebo archivaci záznamů, které se chystají přeuspořádat nebo odstranit.
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Pre install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before installation to prepare the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
Předinstalační funkci můžete také kdykoli spustit ručně pomocí CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:function:exec --preInstall
|
||||
```
|
||||
|
||||
Hlavní body:
|
||||
* Funkce pre-install používají `definePreInstallLogicFunction()` — stejné specializované nastavení jako u post-install, pouze připojené k jiné fázi životního cyklu.
|
||||
* Obě obslužné funkce pre- i post-install přijímají stejný typ `InstallPayload`: `{ previousVersion?: string; newVersion: string }`. Importujte jej jednou a znovu použijte pro oba hooky.
|
||||
* **Kdy se hook spouští**: umístěn těsně před migrací metadat pracovního prostoru (`synchronizeFromManifest`). Před spuštěním server provede čistě aditivní "zjednodušenou synchronizaci", která v metadatech pracovního prostoru zaregistruje pre-install funkci **nové** verze — ničeho dalšího se nedotkne — a poté ji spustí. Protože tato synchronizace je pouze aditivní, objekty, pole a data předchozí verze zůstávají při spuštění vaší obslužné funkce zachována: můžete bezpečně číst a zálohovat stav před migrací.
|
||||
* **Model provádění**: pre-install se provádí **synchronně** a **blokuje instalaci**. Pokud obslužná funkce vyvolá výjimku, instalace se přeruší ještě před aplikováním jakýchkoli změn schématu — pracovní prostor zůstane na předchozí verzi v konzistentním stavu. Je to záměrné: pre-install je vaše poslední šance odmítnout rizikovou aktualizaci.
|
||||
* Stejně jako u post-install je na jednu aplikaci povolena pouze jedna funkce pre-install. Během buildu je automaticky připojena k manifestu aplikace pod `preInstallLogicFunction`.
|
||||
* **Nespouští se v režimu dev**: stejně jako u post-install — u lokálně registrovaných aplikací je instalační tok zcela přeskočen, takže se pre-install pod `yarn twenty dev` nikdy nespustí. Použijte `yarn twenty dev:function:exec --preInstall` k ručnímu spuštění.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Pre-install vs post-install: kdy použít který" description="Výběr správného instalačního hooku">
|
||||
|
||||
Oba hooky jsou součástí téhož instalačního toku a přijímají stejný `InstallPayload`. Rozdíl je v tom, **kdy** se spouštějí vzhledem k migraci metadat pracovního prostoru, a to určuje, jakých dat se mohou bezpečně dotýkat.
|
||||
|
||||
Pre-install je vždy **synchronní** (blokuje instalaci a může ji přerušit). Post-install je **ve výchozím nastavení asynchronní** — zařazen do workeru s automatickými pokusy o opakování — ale může přejít na synchronní provádění pomocí `shouldRunSynchronously: true`. Viz accordion `definePostInstallLogicFunction` výše, kdy použít jednotlivé režimy.
|
||||
|
||||
**Použijte `post-install` pro cokoli, co vyžaduje existenci nového schématu.** To je běžný případ:
|
||||
|
||||
* Plnění výchozími daty (vytváření počátečních záznamů, výchozích pohledů, demo obsahu) vůči nově přidaným objektům a polím.
|
||||
* Registrace webhooků u služeb třetích stran poté, co má aplikace své přihlašovací údaje.
|
||||
* Volání vlastního API k dokončení nastavení, které závisí na synchronizovaných metadatech.
|
||||
* Idempotentní logika "zajisti, že to existuje", která má při každé aktualizaci uvést stav do souladu — kombinujte s `shouldRunOnVersionUpgrade: true`.
|
||||
|
||||
Příklad — po instalaci naplňte výchozí záznam `PostCard`:
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
|
||||
if (previousVersion) return; // fresh installs only
|
||||
|
||||
const client = createClient();
|
||||
await client.postCard.create({
|
||||
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
|
||||
});
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
|
||||
name: 'post-install',
|
||||
description: 'Seeds a welcome post card after install.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**Použijte `pre-install`, pokud by migrace jinak zničila nebo poškodila existující data.** Protože pre-install běží proti *předchozímu* schématu a jeho selhání vrací aktualizaci zpět, je to správné místo pro cokoli rizikového:
|
||||
|
||||
* **Zálohování dat, která se chystají odstranit nebo přeuspořádat** — např. odstraňujete pole ve verzi v2 a potřebujete jeho hodnoty zkopírovat do jiného pole nebo je před spuštěním migrace exportovat do úložiště.
|
||||
* **Archivace záznamů, které by nové omezení zneplatnilo** — např. pole se stává `NOT NULL` a je třeba nejprve smazat nebo opravit řádky s hodnotami null.
|
||||
* **Ověření kompatibility a odmítnutí aktualizace, pokud nelze aktuální data čistě migrovat** — vyhoďte výjimku z obslužné funkce a instalace se ukončí bez provedených změn. Je to bezpečnější, než zjistit nekompatibilitu uprostřed migrace.
|
||||
* **Přejmenování nebo změna klíčů dat** před změnou schématu, která by ztratila vazby.
|
||||
|
||||
Příklad — archivujte záznamy před destruktivní migrací:
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
|
||||
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
|
||||
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = createClient();
|
||||
const legacyRecords = await client.postCard.findMany({
|
||||
where: { notes: { isNotNull: true } },
|
||||
});
|
||||
|
||||
if (legacyRecords.length === 0) return;
|
||||
|
||||
// Copy legacy `notes` into the new `description` field before the migration
|
||||
// drops the `notes` column. If this fails, the upgrade is aborted and the
|
||||
// workspace stays on v1 with all data intact.
|
||||
await Promise.all(
|
||||
legacyRecords.map((record) =>
|
||||
client.postCard.update({
|
||||
where: { id: record.id },
|
||||
data: { description: record.notes },
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Backs up legacy notes into description before the v2 migration.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**Zlaté pravidlo:**
|
||||
|
||||
| Chcete... | Použít |
|
||||
| ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| Naplňte výchozí data, nakonfigurujte pracovní prostor, zaregistrujte externí prostředky | `post-install` |
|
||||
| Spusťte dlouho běžící plnění nebo volání třetích stran, která by neměla blokovat odezvu instalace | `post-install` (výchozí — `shouldRunSynchronously: false`, s opakovanými pokusy workeru) |
|
||||
| Spusťte rychlé nastavení, na které bude volající spoléhat ihned po návratu volání instalace | `post-install` s `shouldRunSynchronously: true` |
|
||||
| Čtěte nebo zálohujte data, která by nadcházející migrace ztratila | `pre-install` |
|
||||
| Odmítněte aktualizaci, která by poškodila existující data | `pre-install` (vyhoďte výjimku z obslužné funkce) |
|
||||
| Spouštějte srovnání stavu při každé aktualizaci | `post-install` s `shouldRunOnVersionUpgrade: true` |
|
||||
| Proveďte jednorázové nastavení pouze při první instalaci | `post-install` s `shouldRunOnVersionUpgrade: false` (výchozí) |
|
||||
|
||||
<Note>
|
||||
Pokud si nejste jisti, výchozí volbou je **post-install**. Po pre-install sáhněte pouze tehdy, když je samotná migrace destruktivní a potřebujete zachytit předchozí stav, než zmizí.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: Přehled
|
||||
description: Nakonfigurujte samotnou aplikaci – její identitu, výchozí oprávnění a to, co se spouští při instalaci.
|
||||
icon: screwdriver-wrench
|
||||
---
|
||||
|
||||
**Konfigurační vrstva** aplikace Twenty popisuje aplikaci *platformě* – její identitu, oprávnění, která má, a kód, který se spouští během instalace nebo aktualizace. Tato deklarativní nastavení nepřidávají nové datové struktury ani chování za běhu; říkají Twenty, *co je aplikace zač* a *jak ji nastavit*.
|
||||
|
||||
```text
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ Application — identity, default role, variables, │
|
||||
│ marketplace metadata │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ Role — what the app's logic functions can read │ │
|
||||
│ │ and write (referenced by Application) │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼ (at install / upgrade time)
|
||||
┌──────────────────────────────────┐
|
||||
│ Pre-install hook │ before metadata migration
|
||||
└──────────────────────────────────┘
|
||||
┌──────────────────────────────────┐
|
||||
│ Post-install hook │ after metadata migration
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
## V této části
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Konfigurace aplikace" icon="rocket" href="/l/cs/developers/extend/apps/config/application">
|
||||
`defineApplication` – identita, výchozí role, proměnné, metadata pro marketplace.
|
||||
</Card>
|
||||
<Card title="Role a oprávnění" icon="shield-halved" href="/l/cs/developers/extend/apps/config/roles">
|
||||
`defineRole` – deklaruje, co mohou logické funkce vaší aplikace číst a zapisovat.
|
||||
</Card>
|
||||
<Card title="Instalační hooky" icon="wrench" href="/l/cs/developers/extend/apps/config/install-hooks">
|
||||
`definePreInstallLogicFunction` a `definePostInstallLogicFunction` – zálohují data, nastavují výchozí hodnoty, validují aktualizace.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Jak spolu části souvisejí
|
||||
|
||||
* **Aplikace** je vstupní bod. Každá aplikace má právě jedno volání `defineApplication()`, které ukazuje na jednu **roli** jako výchozí.
|
||||
* **Role** určuje, co mohou logické funkce aplikace a front-endové komponenty číst a zapisovat. Dodržujte zásadu nejmenších oprávnění: udělujte pouze ta oprávnění, která váš kód skutečně potřebuje.
|
||||
* **Instalační hooky** se spouštějí během instalace nebo aktualizace – pre-install před migrací metadat (aby mohly odmítnout rizikovou aktualizaci), post-install po migraci (aby mohly proti novému schématu naplnit výchozí data).
|
||||
|
||||
<Note>
|
||||
Instalační hooky sdílejí běhové prostředí [logických funkcí](/l/cs/developers/extend/apps/logic/logic-functions) – stejný podpis handleru, stejné proměnné prostředí, stejný typovaný klient API – ale deklarují se pomocí vlastních funkcí define a fungují mimo běžný model spouštěčů (HTTP, cron, databázové události).
|
||||
</Note>
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: Veřejné soubory
|
||||
description: Doručujte statické soubory — obrázky, ikony, písma — spolu se svou aplikací prostřednictvím složky public/.
|
||||
icon: folder-open
|
||||
---
|
||||
|
||||
Složka `public/` v kořenu vaší aplikace obsahuje statické soubory — obrázky, ikony, písma a další prostředky, které vaše aplikace potřebuje za běhu. Tyto soubory jsou automaticky zahrnuty do buildů, synchronizovány během vývojového režimu a nahrávány na server.
|
||||
|
||||
Soubory umístěné v `public/` jsou:
|
||||
|
||||
* **Veřejně přístupné** — po synchronizaci na server jsou prostředky dostupné na veřejné URL. K přístupu k nim není potřeba žádná autentizace.
|
||||
* **Dostupné ve frontendových komponentách** — použijte URL prostředků k zobrazení obrázků, ikon či jiných médií uvnitř komponent Reactu.
|
||||
* **Dostupné v logických funkcích** — odkazujte na URL prostředků v e-mailech, odpovědích API či jiné serverové logice.
|
||||
* **Používány pro metadata Marketplace** — pole `logoUrl` a `screenshots` v `defineApplication()` odkazují na soubory z této složky (např. `public/logo.png`). Zobrazují se v Marketplace, když je vaše aplikace zveřejněna.
|
||||
* **Automaticky synchronizované ve vývojovém režimu** — když v `public/` přidáte, aktualizujete nebo smažete soubor, je automaticky synchronizován na server. Není potřeba restartovat.
|
||||
* **Zahrnuté do buildů** — `yarn twenty dev:build` zabalí všechny veřejné prostředky do distribučního výstupu.
|
||||
|
||||
## Přístup k veřejným prostředkům pomocí `getPublicAssetUrl`
|
||||
|
||||
K získání plné URL souboru ve vaší složce `public/` použijte pomocnou funkci `getPublicAssetUrl` z `twenty-sdk`. Funguje jak v logických funkcích, tak ve frontendových komponentách.
|
||||
|
||||
**V logické funkci:**
|
||||
|
||||
```ts src/logic-functions/send-invoice.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { getPublicAssetUrl } from 'twenty-sdk/utils';
|
||||
|
||||
const handler = async (): Promise<any> => {
|
||||
const logoUrl = getPublicAssetUrl('logo.png');
|
||||
const invoiceUrl = getPublicAssetUrl('templates/invoice.png');
|
||||
|
||||
// Fetch the file content (no auth required — public endpoint)
|
||||
const response = await fetch(invoiceUrl);
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
return { logoUrl, size: buffer.byteLength };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-...',
|
||||
name: 'send-invoice',
|
||||
description: 'Sends an invoice with the app logo',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**Ve frontendové komponentě:**
|
||||
|
||||
```tsx src/front-components/company-card.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { getPublicAssetUrl } from 'twenty-sdk/utils';
|
||||
|
||||
const CompanyCard = () => {
|
||||
const logoUrl = getPublicAssetUrl('logo.png');
|
||||
|
||||
return <img src={logoUrl} alt="App logo" />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'company-card',
|
||||
component: CompanyCard,
|
||||
});
|
||||
```
|
||||
|
||||
Argument `path` je relativní ke složce `public/` vaší aplikace. Jak `getPublicAssetUrl('logo.png')`, tak `getPublicAssetUrl('public/logo.png')` se vyhodnotí na stejnou URL — předpona `public/` je, je-li přítomna, automaticky odstraněna.
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: Role a oprávnění
|
||||
description: Určete, které objekty a pole mohou funkce aplikační logiky a front-endové komponenty vaší aplikace číst a zapisovat.
|
||||
icon: shield-halved
|
||||
---
|
||||
|
||||
**Role** je sada oprávnění: které objekty může aplikace číst nebo zapisovat, která pole může vidět a jaké schopnosti na úrovni platformy může používat. Všechny logické funkce aplikace a front-endové komponenty dědí oprávnění role označené pomocí `defineApplicationRole()` (viz [Výchozí role funkce](#the-default-function-role) níže).
|
||||
|
||||
```ts src/roles/restricted-company-role.ts
|
||||
import {
|
||||
defineRole,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
SystemPermissionFlag,
|
||||
} from 'twenty-sdk/define';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
|
||||
label: 'My new role',
|
||||
description: 'A role that can be used in your workspace',
|
||||
canReadAllObjectRecords: false,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canUpdateAllSettings: false,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToUsers: false,
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
],
|
||||
fieldPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
|
||||
.universalIdentifier,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
],
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
|
||||
});
|
||||
```
|
||||
|
||||
## Výchozí role funkce
|
||||
|
||||
Když vygenerujete novou aplikaci, CLI vytvoří výchozí soubor role deklarovaný pomocí `defineApplicationRole()`:
|
||||
|
||||
```ts src/roles/default-role.ts
|
||||
import { defineApplicationRole } from 'twenty-sdk/define';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'b648f87b-1d26-4961-b974-0908fd991061';
|
||||
|
||||
export default defineApplicationRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Default function role',
|
||||
description: 'Default role for function Twenty client',
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canUpdateAllSettings: false,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToUsers: false,
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [],
|
||||
fieldPermissions: [],
|
||||
permissionFlagUniversalIdentifiers: [],
|
||||
});
|
||||
```
|
||||
|
||||
`defineApplicationRole()` je tenký wrapper kolem `defineRole()`, který označuje **tu** roli, jež je při instalaci použita jako výchozí role vaší aplikace. Validace je shodná s `defineRole`, ale build pipeline automaticky propojí její `universalIdentifier` s `defaultRoleUniversalIdentifier` v manifestu aplikace — takže na něj nemusíte v [`defineApplication`](/l/cs/developers/extend/apps/config/application) sami odkazovat.
|
||||
|
||||
Poznámky:
|
||||
|
||||
* Na jednu aplikaci je povolena přesně **jedna** definice `defineApplicationRole(...)` — pokud build manifestu najde více než jednu, sestavení selže.
|
||||
* Pro všechny **další** role, které vaše aplikace poskytuje, použijte `defineRole()` (nikoli `defineApplicationRole()`).
|
||||
* Explicitní nastavení `defaultRoleUniversalIdentifier` v `defineApplication()` je stále podporováno kvůli zpětné kompatibilitě, ale je označeno jako zastaralé ve prospěch `defineApplicationRole()`.
|
||||
|
||||
## Osvědčené postupy
|
||||
|
||||
* Začněte od vygenerované role a postupně ji omezujte — výchozí nastavení poskytuje široká oprávnění pro čtení, což je v produkci zřídka žádoucí.
|
||||
* Nahraďte `objectPermissions` a `fieldPermissions` přesně těmi objekty a poli, které vaše funkce skutečně potřebují.
|
||||
* `permissionFlagUniversalIdentifiers` řídí přístup k schopnostem na úrovni platformy. Udržujte je co nejmenší.
|
||||
* Podívejte se na funkční příklad: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: Rozšiřování objektů
|
||||
description: Přidávejte pole ke standardním objektům Twenty (Person, Company, …) nebo k objektům z jiných aplikací pomocí `defineField`.
|
||||
icon: wand-magic-sparkles
|
||||
---
|
||||
|
||||
Použijte `defineField()` pro přidání pole k objektu, který nevlastníte — standardnímu objektu Twenty, jako je Person nebo Company, nebo objektu dodanému jinou nainstalovanou aplikací. Na rozdíl od inline polí deklarovaných uvnitř [`defineObject`](/l/cs/developers/extend/apps/data/objects) vyžadují samostatná pole `objectUniversalIdentifier` k určení, který objekt rozšiřují.
|
||||
|
||||
```ts src/fields/company-loyalty-tier.field.ts
|
||||
import { defineField, FieldType } from 'twenty-sdk/define';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
|
||||
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
|
||||
name: 'loyaltyTier',
|
||||
type: FieldType.SELECT,
|
||||
label: 'Loyalty Tier',
|
||||
icon: 'IconStar',
|
||||
options: [
|
||||
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
|
||||
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
|
||||
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Hlavní body
|
||||
|
||||
* `objectUniversalIdentifier` identifikuje cílový objekt. Pro standardní objekty Twenty importujte konstantu z `twenty-sdk`:
|
||||
|
||||
```ts
|
||||
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
|
||||
|
||||
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier
|
||||
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier
|
||||
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier
|
||||
// …
|
||||
```
|
||||
|
||||
* Při definování polí **inline uvnitř `defineObject()`** `objectUniversalIdentifier` **nepotřebujete** — dědí se z nadřazeného objektu.
|
||||
|
||||
* `defineField()` je jediný způsob, jak přidat pole k objektům, které jste nevytvořili pomocí `defineObject()`.
|
||||
|
||||
* Umístění souboru je na vás. Konvence je `src/fields/\<name>.field.ts`, ale SDK rozpozná pole kdekoli v `src/`.
|
||||
|
||||
* Chcete-li přidat kartu ke standardnímu rozvržení stránky (např. na detailní stránku Task nebo Company), použijte [`definePageLayoutTab`](/l/cs/developers/extend/apps/layout/page-layouts#definepagelayouttab) s `STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS` z `twenty-sdk/define`.
|
||||
|
||||
## Přidání relace k existujícímu objektu
|
||||
|
||||
Chcete-li přidat relační pole (např. pro propojení vlastního objektu se standardním `Person`), použijte `defineField()` s `FieldType.RELATION`. Vzor je stejný jako u inline relací, ale s `objectUniversalIdentifier` nastaveným explicitně. Obousměrný vzor najdete v části [Relations](/l/cs/developers/extend/apps/data/relations).
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: Objekty
|
||||
description: Deklarujte nové typy záznamů – vlastní tabulky s jejich vlastními poli – pomocí defineObject.
|
||||
icon: tabulka
|
||||
---
|
||||
|
||||
Vlastní **objekty** jsou nové typy záznamů, které vaše aplikace přidává do pracovního prostoru — pohlednice, faktura, předplatné, cokoli specifického pro vaši doménu. Každý objekt definuje své schéma (pole, vztahy, výchozí hodnoty) a stabilní univerzální identifikátor, který přetrvá mezi synchronizacemi a nasazeními.
|
||||
|
||||
```ts src/objects/post-card.object.ts
|
||||
import { defineObject, FieldType } from 'twenty-sdk/define';
|
||||
|
||||
enum PostCardStatus {
|
||||
DRAFT = 'DRAFT',
|
||||
SENT = 'SENT',
|
||||
DELIVERED = 'DELIVERED',
|
||||
RETURNED = 'RETURNED',
|
||||
}
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||||
nameSingular: 'postCard',
|
||||
namePlural: 'postCards',
|
||||
labelSingular: 'Post Card',
|
||||
labelPlural: 'Post Cards',
|
||||
description: 'A post card object',
|
||||
icon: 'IconMail',
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
|
||||
name: 'content',
|
||||
type: FieldType.TEXT,
|
||||
label: 'Content',
|
||||
description: "Postcard's content",
|
||||
icon: 'IconAbc',
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
|
||||
name: 'recipientName',
|
||||
type: FieldType.FULL_NAME,
|
||||
label: 'Recipient name',
|
||||
icon: 'IconUser',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
|
||||
name: 'recipientAddress',
|
||||
type: FieldType.ADDRESS,
|
||||
label: 'Recipient address',
|
||||
icon: 'IconHome',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
|
||||
name: 'status',
|
||||
type: FieldType.SELECT,
|
||||
label: 'Status',
|
||||
icon: 'IconSend',
|
||||
defaultValue: `'${PostCardStatus.DRAFT}'`,
|
||||
options: [
|
||||
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
|
||||
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
|
||||
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
|
||||
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
|
||||
name: 'deliveredAt',
|
||||
type: FieldType.DATE_TIME,
|
||||
label: 'Delivered at',
|
||||
icon: 'IconCheck',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Hlavní body
|
||||
|
||||
* Hodnota `universalIdentifier` musí být jedinečná a stabilní napříč nasazeními.
|
||||
* Každé pole vyžaduje `name`, `type`, `label` a svůj vlastní stabilní `universalIdentifier`.
|
||||
* Pole `fields` je volitelné — objekty můžete definovat i bez vlastních polí.
|
||||
* Pole definovaná zde inline **nepotřebují** `objectUniversalIdentifier` — dědí se z nadřazeného objektu. Pomocí [`defineField()`](/l/cs/developers/extend/apps/data/extending-objects) můžete přidávat pole k objektům, které nevlastníte.
|
||||
* Nové objekty můžete vygenerovat pomocí `yarn twenty dev:add object`, který vás provede pojmenováním, poli a vztahy. Viz [Architektura → Scaffolding entit](/l/cs/developers/extend/apps/getting-started/scaffolding).
|
||||
|
||||
<Note>
|
||||
**Základní pole jsou přidána automaticky.** Když definujete vlastní objekt, Twenty pro vás vytvoří standardní pole jako `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` a `deletedAt`. Nemusíte je uvádět v poli `fields` — pouze svá vlastní pole. Výchozí pole můžete přepsat tak, že deklarujete pole se stejným názvem, ale jen zřídka je to dobrý nápad.
|
||||
</Note>
|
||||
|
||||
## Výchozí hodnoty
|
||||
|
||||
Výchozí textové hodnoty musí být uzavřené v jednoduchých uvozovkách **uvnitř** řetězce — `defaultValue: "'Draft'"`, ne `defaultValue: "Draft"`. Proto pole `status` výše používá `` `'${PostCardStatus.DRAFT}'` ``.
|
||||
|
||||
Neuzavřené (necitované) řetězce jsou vyhrazené pro vypočítané výchozí hodnoty, které se vyhodnocují při vytvoření záznamu:
|
||||
|
||||
* `'uuid'` — generuje UUID (pro pole `UUID`)
|
||||
* `'now'` — aktuální časové razítko (pro pole `DATE_TIME`)
|
||||
|
||||
Stejná konvence platí pro řetězcová podpola složených výchozích hodnot (např. `{ source: "'MANUAL'" }` u pole `ACTOR`) a pro hodnoty `SELECT`/`MULTI_SELECT`. Doslovná řetězcová výchozí hodnota ponechaná bez uvozovek vyvolá při sestavení aplikace varování.
|
||||
|
||||
## Co dál
|
||||
|
||||
* **Propojte tento objekt s ostatními** — vzor obousměrných vztahů najdete v části [Relations](/l/cs/developers/extend/apps/data/relations).
|
||||
* **Přidávejte pole k objektům z jiných aplikací** — viz [Extending Objects](/l/cs/developers/extend/apps/data/extending-objects) pro `defineField()`.
|
||||
* **Zobrazte tento objekt v uživatelském rozhraní** — viz [Views](/l/cs/developers/extend/apps/layout/views) a [Navigation Menu Items](/l/cs/developers/extend/apps/layout/navigation-menu-items) pro umístění do postranního panelu.
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: Přehled
|
||||
description: Formujte data, která vaše aplikace přidává do pracovního prostoru — objekty, pole a vztahy.
|
||||
icon: database
|
||||
---
|
||||
|
||||
**Datová vrstva** aplikace Twenty představuje data, která vaše aplikace *přidává* do pracovního prostoru — nové typy záznamů, které deklaruje, sloupce, které přidává k existujícím objektům a způsob, jakým se tyto záznamy vzájemně propojují.
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ Object — a record type, e.g. PostCard │
|
||||
│ ├─ Field (name, type, label) │
|
||||
│ ├─ Field │
|
||||
│ └─ Relation (link to another object) │
|
||||
└──────────────────────────────────────────────────┘
|
||||
│
|
||||
├── lives in your app, OR
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ Standard / other apps' objects │
|
||||
│ └─ Field added by your app via defineField │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## V této části
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Objekty" icon="tabulka" href="/l/cs/developers/extend/apps/data/objects">
|
||||
`defineObject` — deklarujte nové typy záznamů s jejich vlastními poli.
|
||||
</Card>
|
||||
<Card title="Rozšiřování objektů" icon="wand-magic-sparkles" href="/l/cs/developers/extend/apps/data/extending-objects">
|
||||
`defineField` — přidejte pole ke standardním objektům nebo objektům jiných aplikací.
|
||||
</Card>
|
||||
<Card title="Vztahy" icon="diagram-project" href="/l/cs/developers/extend/apps/data/relations">
|
||||
Obousměrná `MANY_TO_ONE` / `ONE_TO_MANY` propojení mezi objekty.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Entity v kostce
|
||||
|
||||
| Entita | Účel | Definováno pomocí |
|
||||
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
|
||||
| **Objekt** | Nový vlastní typ záznamu (např. PostCard, Invoice) s vlastními poli | `defineObject()` |
|
||||
| **Pole** | Sloupec v objektu. Samostatná pole mohou rozšiřovat objekty, které jste nevytvořili (např. přidat `loyaltyTier` k objektu Company) | `defineField()` |
|
||||
| **Vztah** | Obousměrné propojení mezi dvěma objekty — obě strany deklarované jako pole | `defineField()` s `FieldType.RELATION` |
|
||||
| **Index** | Databázový index pro zrychlení opakovaného dotazu na jeden z vašich objektů | `defineIndex()` |
|
||||
|
||||
SDK je detekuje pomocí analýzy AST v době buildu, takže organizace souborů je na vás — zažitou konvencí je `src/objects/`, `src/fields/` a `src/indexes/`. Stabilní UUID `universalIdentifier` vše propojují napříč nasazeními.
|
||||
|
||||
## Indexy (volitelné)
|
||||
|
||||
Aplikace mohou dodávat indexy společně se svými objekty, aby udržely opakované dotazy rychlými. Nejčastějším případem je sloupec se stavem nebo cizím klíčem, který často čtete.
|
||||
|
||||
```ts src/indexes/post-card-status.index.ts
|
||||
import { defineIndex } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from '../objects/post-card.object';
|
||||
|
||||
export default defineIndex({
|
||||
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
|
||||
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
|
||||
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Jedinečné indexy
|
||||
|
||||
`defineIndex` přijímá `isUnique: true` jak pro jedinečnost jednoho sloupce, tak vícesloupcovou jedinečnost. Toto je doporučené primitivum — `defineField({ isUnique: true })` je zastaralé a bude odstraněno v některém z příštích vydání.
|
||||
|
||||
```ts
|
||||
defineIndex({
|
||||
universalIdentifier: '…',
|
||||
objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER,
|
||||
isUnique: true,
|
||||
fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }],
|
||||
});
|
||||
```
|
||||
|
||||
### Další omezení
|
||||
|
||||
* Částečné klauzule `WHERE` zůstávají pod kontrolou administrátora — aplikace je nemohou deklarovat.
|
||||
* Každý objekt je omezen na 10 vlastních indexů (indexy samotného frameworku se nepočítají).
|
||||
|
||||
Seřaďte pole `fields` tak, jak je má Postgres používat — nejlevější sloupec jako první, jako v telefonním seznamu. Indexy nejsou zadarmo: každý zápis do tabulky je aktualizuje. Přidávejte je jen tehdy, když máte dotaz, který je potřebuje.
|
||||
|
||||
<Note>
|
||||
Hledáte **Application Config** nebo **Roles & Permissions**? Ty popisují samotnou aplikaci, nikoli data, která přidává — najdete je pod [Config](/l/cs/developers/extend/apps/config/overview). Hledáte **Connections** (Linear, GitHub, Slack OAuth)? Ty existují proto, aby byly volány *z* logických funkcí, a najdete je pod [Logic](/l/cs/developers/extend/apps/logic/connections).
|
||||
</Note>
|
||||
@@ -0,0 +1,160 @@
|
||||
---
|
||||
title: Vztahy
|
||||
description: Propojte objekty obousměrnými relacemi MANY_TO_ONE / ONE_TO_MANY.
|
||||
icon: diagram-project
|
||||
---
|
||||
|
||||
Relace propojují dva objekty. Ve Twenty jsou relace vždy **obousměrné** — každá relace má dvě strany a každá strana je deklarována jako pole, které odkazuje na tu druhou.
|
||||
|
||||
| Typ vztahu | Popis | Má cizí klíč? |
|
||||
| ------------- | --------------------------------------------------------------------- | ---------------------- |
|
||||
| `MANY_TO_ONE` | Mnoho záznamů tohoto objektu ukazuje na jeden záznam cílového objektu | Ano (`joinColumnName`) |
|
||||
| `ONE_TO_MANY` | Jeden záznam tohoto objektu má mnoho záznamů cílového objektu | Ne (inverzní strana) |
|
||||
|
||||
## Jak fungují relace
|
||||
|
||||
Každá relace vyžaduje dvě pole, která na sebe vzájemně odkazují:
|
||||
|
||||
1. Strana MANY_TO_ONE — je na objektu, který drží cizí klíč.
|
||||
2. Strana ONE_TO_MANY — je na objektu, který vlastní kolekci.
|
||||
|
||||
Obě pole používají `FieldType.RELATION` a vzájemně se odkazují prostřednictvím `relationTargetFieldMetadataUniversalIdentifier`.
|
||||
|
||||
## Příklad: Pohlednice má mnoho příjemců
|
||||
|
||||
`PostCard` lze odeslat mnoha záznamům `PostCardRecipient`. Každý příjemce náleží přesně jedné pohlednici.
|
||||
|
||||
**Krok 1: Definujte stranu ONE_TO_MANY na PostCard** (strana "one"):
|
||||
|
||||
```ts src/fields/post-card-recipients-on-post-card.field.ts
|
||||
import { defineField, FieldType, RelationType } from 'twenty-sdk/define';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
|
||||
|
||||
// Export so the other side can reference it
|
||||
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
|
||||
// Import from the other side
|
||||
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
|
||||
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'postCardRecipients',
|
||||
label: 'Post Card Recipients',
|
||||
icon: 'IconUsers',
|
||||
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Krok 2: Definujte stranu MANY_TO_ONE na PostCardRecipient** (strana "many" — drží cizí klíč):
|
||||
|
||||
```ts src/fields/post-card-on-post-card-recipient.field.ts
|
||||
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
|
||||
|
||||
// Export so the other side can reference it
|
||||
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
|
||||
// Import from the other side
|
||||
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: POST_CARD_FIELD_ID,
|
||||
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'postCard',
|
||||
label: 'Post Card',
|
||||
icon: 'IconMail',
|
||||
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.CASCADE,
|
||||
joinColumnName: 'postCardId',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
**Cyklické importy:** obě relační pole odkazují na `universalIdentifier` toho druhého. Abyste předešli problémům s cyklickými importy, exportujte ID polí jako pojmenované konstanty z každého souboru a v druhém je importujte. Build systém je vyřeší v době kompilace.
|
||||
</Note>
|
||||
|
||||
## Vazby na standardní objekty
|
||||
|
||||
Chcete-li vytvořit relaci s vestavěným objektem Twenty (Person, Company atd.), použijte `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`:
|
||||
|
||||
```ts src/fields/person-on-self-hosting-user.field.ts
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
RelationType,
|
||||
OnDeleteAction,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk/define';
|
||||
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
|
||||
|
||||
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
|
||||
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PERSON_FIELD_ID,
|
||||
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'person',
|
||||
label: 'Person',
|
||||
description: 'Person matching with the self hosting user',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'personId',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Vlastnosti relačních polí
|
||||
|
||||
| Vlastnost | Povinné | Popis |
|
||||
| ------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| `type` | Ano | Musí být `FieldType.RELATION` |
|
||||
| `relationTargetObjectMetadataUniversalIdentifier` | Ano | `universalIdentifier` cílového objektu |
|
||||
| `relationTargetFieldMetadataUniversalIdentifier` | Ano | `universalIdentifier` odpovídajícího pole na cílovém objektu |
|
||||
| `universalSettings.relationType` | Ano | `RelationType.MANY_TO_ONE` nebo `RelationType.ONE_TO_MANY` |
|
||||
| `universalSettings.onDelete` | Pouze MANY_TO_ONE | Co se stane, když je smazán odkazovaný záznam: `CASCADE`, `SET_NULL`, `RESTRICT` nebo `NO_ACTION` |
|
||||
| `universalSettings.joinColumnName` | Pouze MANY_TO_ONE | Název databázového sloupce pro cizí klíč (např. `postCardId`) |
|
||||
|
||||
## Vložená relační pole
|
||||
|
||||
Relaci můžete také deklarovat přímo uvnitř [`defineObject`](/l/cs/developers/extend/apps/data/objects). Pokud je pole vložené, vynechejte `objectUniversalIdentifier` — dědí se z nadřazeného objektu:
|
||||
|
||||
```ts
|
||||
export default defineObject({
|
||||
universalIdentifier: '...',
|
||||
nameSingular: 'postCardRecipient',
|
||||
// ...
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: POST_CARD_FIELD_ID,
|
||||
type: FieldType.RELATION,
|
||||
name: 'postCard',
|
||||
label: 'Post Card',
|
||||
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.CASCADE,
|
||||
joinColumnName: 'postCardId',
|
||||
},
|
||||
},
|
||||
// … other fields
|
||||
],
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: Pojmy
|
||||
description: Jak aplikace Twenty fungují — model entit, sandboxing a životní cyklus instalace.
|
||||
icon: sitemap
|
||||
---
|
||||
|
||||
Aplikace Twenty jsou balíčky TypeScriptu, které rozšiřují váš pracovní prostor o vlastní objekty, logiku, komponenty UI a funkce AI. Běží na platformě Twenty s plnou izolací (sandboxingem) a řízením oprávnění.
|
||||
|
||||
## Jak aplikace fungují
|
||||
|
||||
Aplikace je kolekce **entit** deklarovaných pomocí funkcí `defineEntity()` z balíčku `twenty-sdk`. SDK tyto deklarace detekuje pomocí analýzy AST při sestavení a vytváří **manifest** — úplný popis toho, co vaše aplikace přidává do pracovního prostoru. Tyto funkce validují vaši konfiguraci v době sestavení a poskytují automatické doplňování v IDE a typovou bezpečnost.
|
||||
|
||||
```
|
||||
your-app/
|
||||
├── src/
|
||||
│ ├── application-config.ts ← defineApplication (required, one per app)
|
||||
│ ├── roles/ ← defineRole
|
||||
│ ├── objects/ ← defineObject
|
||||
│ ├── fields/ ← defineField
|
||||
│ ├── logic-functions/ ← defineLogicFunction
|
||||
│ ├── front-components/ ← defineFrontComponent
|
||||
│ ├── skills/ ← defineSkill
|
||||
│ ├── agents/ ← defineAgent
|
||||
│ ├── views/ ← defineView
|
||||
│ ├── navigation-menu-items/ ← defineNavigationMenuItem
|
||||
│ └── page-layouts/ ← definePageLayout
|
||||
├── public/ ← Static assets (images, icons)
|
||||
└── package.json
|
||||
```
|
||||
|
||||
<Note>
|
||||
**Uspořádání souborů je na vás.** Detekce entit je založená na AST — SDK najde volání `export default defineEntity(...)` bez ohledu na to, kde se soubor nachází. Výše uvedená struktura složek je konvence, nikoli požadavek.
|
||||
</Note>
|
||||
|
||||
## Typy entit
|
||||
|
||||
| Entita | Účel | Dokumentace |
|
||||
| ----------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| **Aplikace** | Identita aplikace, výchozí role, proměnné | [Application Config](/l/cs/developers/extend/apps/config/application) |
|
||||
| **Role** | Sady oprávnění pro objekty a pole | [Roles & Permissions](/l/cs/developers/extend/apps/config/roles) |
|
||||
| **Objekt** | Vlastní typy záznamů s poli | [Objects](/l/cs/developers/extend/apps/data/objects) |
|
||||
| **Pole** | Přidání polí k objektům z jiných aplikací | [Extending Objects](/l/cs/developers/extend/apps/data/extending-objects) |
|
||||
| **Vztah** | Obousměrná propojení mezi objekty | [Relations](/l/cs/developers/extend/apps/data/relations) |
|
||||
| **Logická funkce** | TypeScript na straně serveru se spouštěči | [Logické funkce](/l/cs/developers/extend/apps/logic/logic-functions) |
|
||||
| **Dovednost** | Znovupoužitelné pokyny pro AI agenty | [Dovednosti a agenti](/l/cs/developers/extend/apps/logic/skills-and-agents) |
|
||||
| **Agent** | AI asistenti s vlastními prompty | [Dovednosti a agenti](/l/cs/developers/extend/apps/logic/skills-and-agents) |
|
||||
| **Poskytovatel připojení** | Přihlašovací údaje OAuth pro externí rozhraní API třetích stran | [Connections](/l/cs/developers/extend/apps/logic/connections) |
|
||||
| **Zobrazení** | Předkonfigurovaná zobrazení seznamu záznamů | [Views](/l/cs/developers/extend/apps/layout/views) |
|
||||
| **Položka navigační nabídky** | Vlastní položky postranního panelu | [Navigation Menu Items](/l/cs/developers/extend/apps/layout/navigation-menu-items) |
|
||||
| **Rozvržení stránky** | Karty a widgety na stránce s podrobnostmi záznamu | [Page Layouts](/l/cs/developers/extend/apps/layout/page-layouts) |
|
||||
| **Frontendová komponenta** | Izolované React UI uvnitř Twenty | [Frontendové komponenty](/l/cs/developers/extend/apps/layout/front-components) |
|
||||
| **Položka příkazové nabídky** | Rychlé akce a položky Cmd+K | [Command Menu Items](/l/cs/developers/extend/apps/layout/command-menu-items) |
|
||||
|
||||
## Izolace (sandboxing)
|
||||
|
||||
* **Logické funkce** běží v izolovaných procesech Node.js na serveru. K datům přistupují pouze prostřednictvím typovaného klienta API, a to v rozsahu oprávnění role aplikace.
|
||||
* **Frontendové komponenty** běží ve Web Workerech s využitím Remote DOM — jsou oddělené od hlavní stránky, ale vykreslují nativní prvky DOM (nikoli iframy). Komunikují s Twenty prostřednictvím hostitelského API pro předávání zpráv.
|
||||
* **Oprávnění** jsou vynucována na úrovni API. Běhový token (`TWENTY_APP_ACCESS_TOKEN`) je odvozen z role definované v `defineApplication()`.
|
||||
|
||||
## Životní cyklus aplikace
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Development │
|
||||
│ npx create-twenty-app → yarn twenty dev (live sync) │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Build & Deploy │
|
||||
│ yarn twenty dev:build → yarn twenty app:publish │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Install flow │
|
||||
│ upload → [pre-install] → metadata migration → │
|
||||
│ generate SDK → [post-install] │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Publish │
|
||||
│ npm publish → appears in Twenty marketplace │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
* **`yarn twenty dev`** — sleduje vaše zdrojové soubory a průběžně synchronizuje změny s připojeným serverem Twenty. Typovaný klient API se při změně schématu automaticky znovu vygeneruje.
|
||||
* **`yarn twenty dev:build`** — zkompiluje TypeScript, zabalí logické funkce a frontendové komponenty pomocí esbuild a vytvoří manifest.
|
||||
* **Pre/post-install hooks** — volitelné funkce, které běží během instalace. Podrobnosti viz [Install Hooks](/l/cs/developers/extend/apps/config/install-hooks).
|
||||
|
||||
## Další kroky
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Konfigurace" icon="screwdriver-wrench" href="/l/cs/developers/extend/apps/config/overview">
|
||||
Identita aplikace, výchozí role a instalační hooky.
|
||||
</Card>
|
||||
<Card title="Data" icon="database" href="/l/cs/developers/extend/apps/data/overview">
|
||||
Objekty, pole a obousměrné relace.
|
||||
</Card>
|
||||
<Card title="Logika" icon="bolt" href="/l/cs/developers/extend/apps/logic/overview">
|
||||
Logické funkce, dovednosti, agenti a připojení přes OAuth.
|
||||
</Card>
|
||||
<Card title="Rozvržení" icon="table-columns" href="/l/cs/developers/extend/apps/layout/overview">
|
||||
Zobrazení, navigace, rozvržení stránek, frontendové komponenty.
|
||||
</Card>
|
||||
<Card title="Operace" icon="rocket" href="/l/cs/developers/extend/apps/operations/overview">
|
||||
CLI, testování, vzdálené repozitáře, CI a publikování vaší aplikace.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: Lokální server
|
||||
description: Spravujte lokální Docker server Twenty – spouštění, zastavení, upgrade, paralelní testovací instance a ruční nastavení SDK.
|
||||
icon: server
|
||||
---
|
||||
|
||||
## Správa lokálního serveru
|
||||
|
||||
K ovládání lokálního kontejneru Twenty použijte `yarn twenty docker:*`:
|
||||
|
||||
| Příkaz | K čemu slouží |
|
||||
| -------------------------------------- | ---------------------------------------------- |
|
||||
| `yarn twenty docker:start` | Spustí server (v případě potřeby stáhne image) |
|
||||
| `yarn twenty docker:start 2.2.0` | Spustit konkrétní verzi serveru |
|
||||
| `yarn twenty docker:start --port 3030` | Spustí na vlastním portu |
|
||||
| `yarn twenty docker:stop` | Zastaví server (zachová data) |
|
||||
| `yarn twenty docker:status` | Zobrazí URL, verzi a přihlašovací údaje |
|
||||
| `yarn twenty docker:logs` | Streamuje protokoly serveru |
|
||||
| `yarn twenty docker:reset` | Vymaže data a začne znovu |
|
||||
| `yarn twenty docker:upgrade` | Stáhne nejnovější image `twenty-app-dev` |
|
||||
| `yarn twenty docker:upgrade 2.2.0` | Aktualizuje na konkrétní verzi |
|
||||
|
||||
Data přetrvávají při restartech ve dvou svazcích Dockeru (`twenty-app-dev-data` pro PostgreSQL, `twenty-app-dev-storage` pro soubory). Pomocí `reset` vymažte vše.
|
||||
|
||||
## Fixace verze serveru
|
||||
|
||||
Když není předána žádná verze, `docker:start` určí verzi z rozsahu `engines.twenty` vaší aplikace v `package.json` — stejného rozsahu, vůči kterému server ověřuje, když je vaše aplikace nainstalována. Spustí nejnovější publikovaný image `twenty-app-dev`, který splňuje tento rozsah, a pokud pole chybí nebo žádná publikovaná verze neodpovídá, použije `latest`:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"engines": {
|
||||
"twenty": ">=2.2.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Předáním verze můžete rozsah pro jedno spuštění přepsat: `yarn twenty docker:start 2.3.0`. Pokud už kontejner existuje v jiné verzi, `docker:start` jej na místě aktualizuje (znovu vytvoří kontejner při zachování vašich datových svazků).
|
||||
|
||||
## Aktualizace obrazu serveru
|
||||
|
||||
`yarn twenty docker:upgrade` stáhne nejnovější image, porovná digesty a znovu vytvoří kontejner pouze v případě, že se skutečně něco změnilo. Svazky zůstanou zachovány — nahradí se pouze kontejner. Pokud byl stažen nový image a kontejner běžel, upgrade automaticky spustí nový kontejner; poté spusťte `yarn twenty docker:start`, abyste počkali, než bude ve stavu 'healthy'.
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty docker:upgrade # Latest
|
||||
yarn twenty docker:upgrade 2.2.0 # Specific version
|
||||
```
|
||||
|
||||
Běžící verzi ověříte pomocí `yarn twenty docker:status` (zobrazí `APP_VERSION` zabudovanou v kontejneru).
|
||||
|
||||
## Spuštění paralelní testovací instance
|
||||
|
||||
Předejte `--test` libovolnému příkazu `docker:*` pro správu druhé, plně izolované instance — užitečné pro integrační testy nebo experimentování bez zásahu do vašich hlavních vývojových dat:
|
||||
|
||||
| Příkaz | K čemu slouží |
|
||||
| ----------------------------------- | ------------------------------------------------ |
|
||||
| `yarn twenty docker:start --test` | Spustí testovací instanci (výchozí port je 2021) |
|
||||
| `yarn twenty docker:stop --test` | Zastaví ji |
|
||||
| `yarn twenty docker:status --test` | Zobrazí její stav |
|
||||
| `yarn twenty docker:logs --test` | Streamuje její protokoly |
|
||||
| `yarn twenty docker:reset --test` | Vymaže její data |
|
||||
| `yarn twenty docker:upgrade --test` | Aktualizuje její image |
|
||||
|
||||
Testovací instance má vlastní kontejner (`twenty-app-dev-test`), svazky (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) a konfiguraci — běží souběžně s vaší hlavní instancí bez konfliktů. Zkombinujte `--test` s `--port` pro změnu výchozího portu 2021.
|
||||
|
||||
## Ruční nastavení (bez generátoru kostry)
|
||||
|
||||
Pokud přidáváte SDK do existujícího projektu, generátor kostry přeskočte:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add twenty-sdk twenty-client-sdk
|
||||
```
|
||||
|
||||
Přidejte skript do `package.json`:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"scripts": {
|
||||
"twenty": "twenty"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Nyní můžete spouštět `yarn twenty dev`, `yarn twenty docker:start` a další.
|
||||
|
||||
<Note>
|
||||
Neinstalujte `twenty-sdk` globálně — nainstalujte jej v každém projektu zvlášť, aby každá aplikace používala svou vlastní verzi.
|
||||
</Note>
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: Struktura projektu
|
||||
description: Co obsahuje vygenerovaná aplikace Twenty — soubory, složky a k čemu každý z nich slouží.
|
||||
icon: folder-tree
|
||||
---
|
||||
|
||||
Nová aplikace vygenerovaná pomocí `npx create-twenty-app` vypadá takto:
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
package.json
|
||||
src/
|
||||
application-config.ts # Required — your app's entry point
|
||||
default-role.ts # Permissions for logic functions
|
||||
constants/
|
||||
universal-identifiers.ts # Auto-generated UUIDs and metadata
|
||||
__tests__/
|
||||
setup-test.ts
|
||||
app-install.integration-test.ts
|
||||
.github/workflows/ci.yml # GitHub Actions
|
||||
public/ # Static assets
|
||||
vitest.config.ts # Test runner config
|
||||
tsconfig.json, tsconfig.spec.json
|
||||
.nvmrc, .yarnrc.yml, .oxlintrc.json
|
||||
README.md, LLMS.md
|
||||
```
|
||||
|
||||
## Klíčové soubory
|
||||
|
||||
| Soubor / Složka | Účel |
|
||||
| ---------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| `src/application-config.ts` | **Povinné.** Hlavní konfigurační soubor vaší aplikace. |
|
||||
| `src/default-role.ts` | Výchozí role, která řídí, k čemu mohou vaše logické funkce přistupovat. |
|
||||
| `src/constants/universal-identifiers.ts` | Automaticky generovaná UUID a metadata (zobrazovaný název, popis). |
|
||||
| `src/__tests__/` | Integrační testy (nastavení + ukázkový test). |
|
||||
| `public/` | Statické soubory (obrázky, písma) doručované vaší aplikací. |
|
||||
|
||||
<Note>
|
||||
**Uspořádání souborů je na vás.** Výše uvedené složky jsou konvence — SDK detekuje entity pomocí analýzy AST u volání `export default defineEntity(...)` bez ohledu na to, kde se soubor nachází.
|
||||
</Note>
|
||||
|
||||
## Závislosti
|
||||
|
||||
Oba balíčky Twenty SDK patří pod `devDependencies`, ne pod `dependencies`:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"twenty-client-sdk": "^2.13.0",
|
||||
"twenty-sdk": "^2.13.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* **`twenty-sdk`** dodává `twenty` CLI a nástroje pro sestavení/scaffolding. Běží pouze při vývoji a sestavování a nikdy není importován za běhu zveřejněné aplikace.
|
||||
* **`twenty-client-sdk`** je importován kódem vaší aplikace (`CoreApiClient`, `MetadataApiClient`, `RestApiClient`), ale Twenty ho poskytuje za běhu — logické funkce ho získávají z vygenerované SDK vrstvy a front-endové komponenty ho načítají z modulů poskytovaných serverem. Vaše nainstalovaná kopie se používá pouze pro kontrolu typů a build v době nasazení, takže ji nikdy není potřeba přibalit do nasazeného balíčku.
|
||||
|
||||
Ponechání kteréhokoli balíčku pod `dependencies` ho vtáhne do runtime balíčku nainstalované aplikace, kde je jen mrtvou vahou. `twenty build` vypíše varování, pokud je kterýkoli z nich stále uveden pod `dependencies`.
|
||||
|
||||
Vlastní runtime závislosti vaší aplikace (knihovny, které vaše logické funkce skutečně importují za běhu) přidejte jako obvykle pod `dependencies`.
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
title: Rychlý start
|
||||
icon: rocket
|
||||
description: Vytvořte svou první aplikaci Twenty během několika minut.
|
||||
---
|
||||
|
||||
## Předpoklady
|
||||
|
||||
* **Node.js 24+** — [Stáhnout](https://nodejs.org/)
|
||||
* **Yarn 4** — je součástí Node.js prostřednictvím Corepacku. Povolte jej: `corepack enable`
|
||||
* **Docker** — [Stáhnout](https://www.docker.com/products/docker-desktop/). Nutné pro spuštění lokálního serveru Twenty. Přeskočte, pokud už máte Twenty spuštěné jinde.
|
||||
|
||||
Vytváření aplikace Twenty má tři fáze. Generátor kostry je spojuje do jediného příkazu pro ideální scénář, ale každá fáze je samostatný koncept — když se něco nepovede, znalost aktuální fáze napoví, co opravit.
|
||||
|
||||
| Fáze | Co děláte | Nástroj | Výsledek |
|
||||
| ---------------------- | -------------------------------------------------------- | ----------------------------- | -------------------------------------------- |
|
||||
| **1. Vytvořit kostru** | Vygenerovat zdrojový kód aplikace | `npx create-twenty-app` | Projekt v TypeScriptu na disku |
|
||||
| **2. Spustit server** | Spustit server Twenty, do kterého se bude synchronizovat | Docker + `yarn twenty server` | Běžící instance Twenty |
|
||||
| **3. Synchronizovat** | Živě synchronizovat kód na server | `yarn twenty dev` | Vaše změny se objeví v uživatelském rozhraní |
|
||||
|
||||
---
|
||||
|
||||
## Fáze 1 — Vytvořte kostru projektu
|
||||
|
||||
Vytvořte novou aplikaci ze šablony:
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
```
|
||||
|
||||
Budete vyzváni k zadání názvu a popisu — výchozí hodnoty potvrdíte klávesou **Enter**. Tím se v `my-twenty-app/` vytvoří projekt v TypeScriptu se startovacím `application-config.ts`, výchozí rolí, CI workflow a integračním testem.
|
||||
|
||||
**Po této fázi:** máte na svém počítači zdrojový kód aplikace. Zatím neběží — to je předmětem Fáze 2.
|
||||
|
||||
---
|
||||
|
||||
## Fáze 2 — Spusťte lokální server Twenty
|
||||
|
||||
Vaše aplikace potřebuje server Twenty, do kterého se bude synchronizovat. Server je plnohodnotná instance Twenty — UI, GraphQL API, PostgreSQL — běžící lokálně v Dockeru. Váš lokální kód nahrává své definice na tento server, díky čemuž se objeví v UI.
|
||||
|
||||
Generátor kostry nabídne, že vám jej spustí:
|
||||
|
||||
> **Chcete nastavit lokální instanci Twenty?**
|
||||
|
||||
* **Ano (doporučeno)** — stáhne Docker image `twentycrm/twenty-app-dev` a spustí jej na portu `2020`. Nejprve se ujistěte, že Docker běží.
|
||||
* **Ne** — zvolte, pokud už máte server Twenty, ke kterému se chcete připojit. Můžete jej propojit později pomocí `yarn twenty remote:add`.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="Spustit lokální instanci?" />
|
||||
</div>
|
||||
|
||||
Jakmile server běží, otevře se prohlížeč pro přihlášení. Použijte předpřipravený demo účet:
|
||||
|
||||
* **E-mail:** `tim@apple.dev`
|
||||
* **Heslo:** `tim@apple.dev`
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/login.png" alt="Přihlašovací obrazovka Twenty" />
|
||||
</div>
|
||||
|
||||
Na další obrazovce klikněte na **Authorize** — tím udělíte nástroji CLI přístup k vašemu pracovnímu prostoru.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Autorizační obrazovka Twenty CLI" />
|
||||
</div>
|
||||
|
||||
Váš terminál potvrdí, že je vše nastaveno.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="Aplikace byla úspěšně vygenerována" />
|
||||
</div>
|
||||
|
||||
**Po této fázi:** máte spuštěný server Twenty na [http://localhost:2020](http://localhost:2020) a vaše CLI má oprávnění se k němu synchronizovat.
|
||||
|
||||
<Note>
|
||||
Pokud Docker není nainstalovaný nebo neběží, generátor kostry vám sdělí správný příkaz pro spuštění ve vašem operačním systému. Jakmile Docker poběží, můžete pokračovat pomocí `yarn twenty docker:start` — není potřeba znovu vytvářet kostru.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Fáze 3 — Synchronizujte své změny
|
||||
|
||||
Toto je vnitřní smyčka, ve které strávíte většinu času.
|
||||
|
||||
```bash filename="Terminal"
|
||||
cd my-twenty-app
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Tento proces sleduje `src/`, při každé změně znovu sestaví a synchronizuje výsledek na server. Upravte soubor, uložte a během několika vteřin se změna projeví na serveru. V terminálu uvidíte panel se stavem v reálném čase.
|
||||
|
||||
Pro podrobnější výstup (protokoly sestavení, požadavky na synchronizaci, stopy chyb) přidejte `--verbose`.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/dev.png" alt="Výstup terminálu ve vývojovém režimu" />
|
||||
</div>
|
||||
|
||||
Otevřete [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). Vaše aplikace by měla být uvedena v části **Your Apps**.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Seznam Your Apps se zobrazenou aplikací My twenty app" />
|
||||
</div>
|
||||
|
||||
Klikněte na **My twenty app** a zobrazí se jeho **registrace aplikace** — záznam na úrovni serveru, který popisuje vaši aplikaci (název, identifikátor, přihlašovací údaje OAuth, zdroj). Jedna registrace může být nainstalována ve více pracovních prostorech na stejném serveru.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Podrobnosti registrace aplikace" />
|
||||
</div>
|
||||
|
||||
Klikněte na **View installed app**, abyste zobrazili instalaci v pracovním prostoru. Karta **About** zobrazuje verzi a možnosti správy.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Nainstalovaná aplikace" />
|
||||
</div>
|
||||
|
||||
**Po této fázi:** máte průběžný vývojový cyklus. Upravte libovolný soubor v `src/` a projeví se to v UI.
|
||||
|
||||
### Jednorázová synchronizace pro CI a skripty
|
||||
|
||||
Předejte `--once` pro provedení jednoho sestavení + synchronizace a ukončení — stejný postup, bez sledování změn:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --once
|
||||
```
|
||||
|
||||
| Příkaz | Chování | Kdy použít |
|
||||
| ---------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
| `yarn twenty dev` | Sleduje změny a při každé změně znovu synchronizuje. Běží, dokud jej nezastavíte. | Interaktivní lokální vývoj. |
|
||||
| `yarn twenty dev --once` | Jedno sestavení + synchronizace, ukončí se s kódem `0` při úspěchu, `1` při chybě. | CI, pre-commit hooky, AI agenti, skriptované pracovní postupy. |
|
||||
| `yarn twenty dev --once --dry-run` | Sestaví a vypíše změny metadat **bez jejich použití**. | Kontrola toho, co by synchronizace změnila, ještě před jejím potvrzením. |
|
||||
|
||||
Oba režimy vyžadují autentizovaný vzdálený server. Více informací o `--dry-run` najdete v části [Synchronizace a obnovení](/l/cs/developers/extend/apps/operations/sync-and-recovery#previewing-changes-dry-run).
|
||||
|
||||
### Možnosti vývojového režimu
|
||||
|
||||
| Přepínač | Popis |
|
||||
| ------------------------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| `--once` | Jednou sestavit a synchronizovat, poté ukončit. |
|
||||
| `--dry-run` | Pomocí `--once` zobrazíte náhled změn metadat, aniž byste je použili. Nic nezapisuje. |
|
||||
| `--debounceMs \<ms>` | Nastaví prodlevu pro potlačení zákmitů při změnách souborů v milisekundách (výchozí: `2000`). |
|
||||
| `--verbose` / `--debug` | Zobrazí podrobné protokoly sestavení, požadavky synchronizace a trasování chyb. |
|
||||
|
||||
## Co můžete vytvořit
|
||||
|
||||
Aplikace se skládají z **entit** — každá je definována jako soubor TypeScriptu s jediným `export default`:
|
||||
|
||||
| Entita | K čemu slouží |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Objekty a pole** | Vlastní datové modely (pohlednice, faktura apod.) s typovanými poli |
|
||||
| **Logické funkce** | Serverový TypeScript spouštěný HTTP trasami, plánovačem cron nebo událostmi databáze |
|
||||
| **Frontendové komponenty** | Komponenty Reactu, které se vykreslují v uživatelském rozhraní Twenty (postranní panel, widgety, příkazová nabídka) |
|
||||
| **Dovednosti a agenti** | Schopnosti AI — opakovaně použitelné pokyny a autonomní asistenti |
|
||||
| **Pohledy a navigace** | Předkonfigurované seznamové pohledy a položky postranní nabídky |
|
||||
| **Rozvržení stránek** | Vlastní stránky detailu záznamu s kartami a widgety |
|
||||
|
||||
Úplná reference: [Koncepty](/l/cs/developers/extend/apps/getting-started/concepts).
|
||||
|
||||
## Další kroky
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Konfigurace" icon="screwdriver-wrench" href="/l/cs/developers/extend/apps/config/overview">
|
||||
Identita aplikace, výchozí role, instalační hooky, veřejná aktiva.
|
||||
</Card>
|
||||
<Card title="Data" icon="database" href="/l/cs/developers/extend/apps/data/overview">
|
||||
Objekty, pole a obousměrné relace.
|
||||
</Card>
|
||||
<Card title="Logika" icon="bolt" href="/l/cs/developers/extend/apps/logic/overview">
|
||||
Logické funkce, dovednosti, agenti a připojení přes OAuth.
|
||||
</Card>
|
||||
<Card title="Rozvržení" icon="table-columns" href="/l/cs/developers/extend/apps/layout/overview">
|
||||
Zobrazení, navigace, rozvržení stránek, frontendové komponenty.
|
||||
</Card>
|
||||
<Card title="Operace" icon="rocket" href="/l/cs/developers/extend/apps/operations/overview">
|
||||
CLI, testování, vzdálené repozitáře, CI a publikování vaší aplikace.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: Vytvoření kostry
|
||||
description: Interaktivně generujte soubory entit pomocí yarn twenty dev:add – objekty, pole, zobrazení, logické funkce a další.
|
||||
icon: wand-magic-sparkles
|
||||
---
|
||||
|
||||
Místo ručního vytváření souborů entit použijte interaktivní generátor:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:add
|
||||
```
|
||||
|
||||
Vyžádá si, abyste vybrali typ entity, provede vás požadovanými poli a poté zapíše připravený soubor s pevně daným `universalIdentifier` a správným voláním `defineEntity()`.
|
||||
|
||||
Můžete také předat typ entity přímo a přeskočit první dotaz:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:add object
|
||||
yarn twenty dev:add logicFunction
|
||||
yarn twenty dev:add frontComponent
|
||||
```
|
||||
|
||||
## Dostupné typy entit
|
||||
|
||||
| Typ entity | Příkaz | Vygenerovaný soubor |
|
||||
| ------------------------- | ---------------------------------------- | ------------------------------------------------------- |
|
||||
| Objekt | `yarn twenty dev:add object` | `src/objects/\<name>.ts` |
|
||||
| Pole | `yarn twenty dev:add field` | `src/fields/\<name>.ts` |
|
||||
| Logická funkce | `yarn twenty dev:add logicFunction` | `src/logic-functions/\<name>.ts` |
|
||||
| Frontendová komponenta | `yarn twenty dev:add frontComponent` | `src/front-components/\<name>.tsx` |
|
||||
| Role | `yarn twenty dev:add role` | `src/roles/\<name>.ts` |
|
||||
| Dovednost | `yarn twenty dev:add skill` | `src/skills/\<name>.ts` |
|
||||
| Agent | `yarn twenty dev:add agent` | `src/agents/\<name>.ts` |
|
||||
| Zobrazení | `yarn twenty dev:add view` | `src/views/\<name>.ts` |
|
||||
| Položka navigační nabídky | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/\<name>.ts` |
|
||||
| Rozvržení stránky | `yarn twenty dev:add pageLayout` | `src/page-layouts/\<name>.ts` |
|
||||
|
||||
## Co generátor vytváří
|
||||
|
||||
Každý typ entity má vlastní šablonu. Například `yarn twenty dev:add object` se zeptá na:
|
||||
|
||||
1. **Název (jednotné číslo)** — např. `invoice`
|
||||
2. **Název (množné číslo)** — např. `invoices`
|
||||
3. **Štítek (jednotné číslo)** — automaticky doplněn z názvu (např. `Invoice`)
|
||||
4. **Štítek (množné číslo)** — automaticky doplněn (např. `Invoices`)
|
||||
5. **Vytvořit zobrazení a položku navigace?** — pokud odpovíte ano, generátor také vytvoří odpovídající zobrazení a odkaz v postranním panelu pro nový objekt.
|
||||
|
||||
Ostatní typy entit mají jednodušší dotazy — většinou se ptají pouze na název.
|
||||
|
||||
Typ entity `field` je podrobnější: ptá se na název pole, štítek, typ (ze seznamu všech dostupných typů polí jako `TEXT`, `NUMBER`, `SELECT`, `RELATION` atd.) a `universalIdentifier` cílového objektu.
|
||||
|
||||
## Vlastní výstupní cesta
|
||||
|
||||
Pomocí příznaku `--path` umístíte vygenerovaný soubor do vlastního umístění:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:add logicFunction --path src/custom-folder
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Řešení potíží
|
||||
description: Časté problémy při prvním spuštění — Docker, verze Node, Yarn, závislosti.
|
||||
icon: klíč
|
||||
---
|
||||
|
||||
* **Chyby Dockeru** — Před spuštěním `yarn twenty docker:start` se ujistěte, že Docker Desktop (nebo démon) běží. Chybová zpráva ukáže správný příkaz pro spuštění pro váš operační systém.
|
||||
* **Nesprávná verze Node** — Je potřeba 24+. Ověřte pomocí `node -v`.
|
||||
* **Chybí Yarn 4** — Spusťte `corepack enable`.
|
||||
* **Rozbité závislosti** — `rm -rf node_modules && yarn install`.
|
||||
* **Chyby `twenty-sdk` po upgradu na v2.8.0** — V 2.8.0 byl přesunut z `dependencies` do `devDependencies`. Viz [Struktura projektu → Závislosti](/l/cs/developers/extend/apps/getting-started/project-structure#dependencies).
|
||||
* **`twenty build` upozorňuje na `twenty-client-sdk` v sekci `dependencies`** — je poskytován za běhu Twenty, takže by měl být přesunut do `devDependencies` vedle `twenty-sdk`. Viz [Struktura projektu → Závislosti](/l/cs/developers/extend/apps/getting-started/project-structure#dependencies).
|
||||
|
||||
Zasekli jste se? Zeptejte se na [Discordu Twenty](https://discord.com/channels/1130383047699738754/1130386664812982322).
|
||||
@@ -0,0 +1,148 @@
|
||||
---
|
||||
title: Položky příkazové nabídky
|
||||
description: Zpřístupněte front komponenty jako rychlé akce a položky příkazového menu (Cmd+K) pomocí defineCommandMenuItem.
|
||||
icon: terminal
|
||||
---
|
||||
|
||||
**Položka příkazového menu** je most mezi uživatelem a [front komponentou](/l/cs/developers/extend/apps/layout/front-components). Registruje komponentu v příkazovém menu Twenty (Cmd+K) a volitelně také jako připnuté tlačítko rychlé akce v pravém horním rohu stránky.
|
||||
|
||||
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
label: 'Open Dashboard',
|
||||
shortLabel: 'Dashboard',
|
||||
icon: 'IconLayoutDashboard',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
});
|
||||
```
|
||||
|
||||
## Konfigurační pole
|
||||
|
||||
| Pole | Povinné | Popis |
|
||||
| --------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `universalIdentifier` | Ano | Stabilní jedinečné ID pro příkaz |
|
||||
| `label` | Ano | Plný popisek zobrazený v příkazovém menu (Cmd+K) |
|
||||
| `frontComponentUniversalIdentifier` | Ano | `universalIdentifier` frontendové komponenty, kterou tento příkaz otevírá |
|
||||
| `shortLabel` | Ne | Kratší popisek zobrazený na připnutém tlačítku rychlé akce |
|
||||
| `icon` | Ne | Název ikony zobrazený vedle popisku (např. `'IconBolt'`, `'IconSend'`) |
|
||||
| `isPinned` | Ne | Pokud je `true`, zobrazí příkaz jako tlačítko rychlé akce v pravém horním rohu stránky |
|
||||
| `availabilityType` | Ne | Určuje, kde se příkaz zobrazuje: `'GLOBAL'` (vždy dostupné), `'RECORD_SELECTION'` (pouze když jsou vybrány záznamy) nebo `'FALLBACK'` (zobrazeno, když neodpovídají žádné jiné příkazy) |
|
||||
| `availabilityObjectUniversalIdentifier` | Ne | Omezí příkaz na stránky konkrétního typu objektu (např. pouze u záznamů Company) |
|
||||
| `conditionalAvailabilityExpression` | Ne | Logický výraz, který dynamicky řídí viditelnost (viz níže) |
|
||||
|
||||
## Příkazy bez rozhraní
|
||||
|
||||
Položka příkazového menu spárovaná s [front komponentou bez rozhraní](/l/cs/developers/extend/apps/layout/front-components#headless-vs-non-headless) je idiomatický způsob, jak dodat akci na jedno kliknutí — spustit kód, přejít na stránku nebo potvrdit a provést. Stránka Front Components popisuje [SDK Command komponenty](/l/cs/developers/extend/apps/layout/front-components#sdk-command-components) (`Command`, `CommandLink`, `CommandModal`, `CommandOpenSidePanelPage`), které obsluhují pattern akce-a-unmount.
|
||||
|
||||
Typický průběh:
|
||||
|
||||
```tsx src/front-components/run-action.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const RunAction = () => {
|
||||
const execute = async () => {
|
||||
const client = new CoreApiClient();
|
||||
await client.mutation({
|
||||
createTask: {
|
||||
__args: { data: { title: 'Created by my app' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return <Command execute={execute} />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
name: 'run-action',
|
||||
description: 'Creates a task from the command menu',
|
||||
component: RunAction,
|
||||
isHeadless: true,
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/command-menu-items/run-action.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
});
|
||||
```
|
||||
|
||||
## Výrazy podmíněné dostupnosti
|
||||
|
||||
Pole `conditionalAvailabilityExpression` vám umožní řídit viditelnost příkazu na základě aktuálního kontextu stránky. Pro sestavení výrazů importujte typované proměnné a operátory z `twenty-sdk`:
|
||||
|
||||
```ts src/command-menu-items/bulk-update.command-menu-item.ts
|
||||
import {
|
||||
defineCommandMenuItem,
|
||||
objectPermissions,
|
||||
everyEquals,
|
||||
} from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: '...',
|
||||
label: 'Bulk Update',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
frontComponentUniversalIdentifier: '...',
|
||||
conditionalAvailabilityExpression: everyEquals(
|
||||
objectPermissions,
|
||||
'canUpdateObjectRecords',
|
||||
true,
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`RECORD_SELECTION` již znamená neprázdný výběr — použijte `numberOfSelectedRecords` pouze pro konkrétní počty (např. `>= 2`).
|
||||
</Note>
|
||||
|
||||
### Kontextové proměnné
|
||||
|
||||
Tyto proměnné reprezentují aktuální stav stránky:
|
||||
|
||||
| Proměnná | Typ | Popis |
|
||||
| ------------------------------ | --------- | -------------------------------------------------------------------- |
|
||||
| `pageType` | `string` | Aktuální typ stránky (např. `'RecordIndexPage'`, `'RecordShowPage'`) |
|
||||
| `isInSidePanel` | `boolean` | Zda je komponenta vykreslena v postranním panelu |
|
||||
| `numberOfSelectedRecords` | `number` | Počet aktuálně vybraných záznamů |
|
||||
| `isSelectAll` | `boolean` | Zda je aktivní "vybrat vše" |
|
||||
| `selectedRecords` | `array` | Vybrané objekty záznamů |
|
||||
| `favoriteRecordIds` | `array` | ID oblíbených záznamů |
|
||||
| `objectPermissions` | `object` | Oprávnění pro aktuální typ objektu |
|
||||
| `targetObjectReadPermissions` | `object` | Oprávnění ke čtení pro cílový objekt |
|
||||
| `targetObjectWritePermissions` | `object` | Oprávnění k zápisu pro cílový objekt |
|
||||
| `featureFlags` | `object` | Aktivní příznaky funkcí |
|
||||
| `objectMetadataItem` | `object` | Metadata aktuálního typu objektu |
|
||||
| `hasAnySoftDeleteFilterOnView` | `boolean` | Zda má aktuální zobrazení filtr soft-delete |
|
||||
|
||||
### Operátory
|
||||
|
||||
Kombinujte proměnné do logických výrazů:
|
||||
|
||||
| Operátor | Popis |
|
||||
| ----------------------------------- | ------------------------------------------------------------------------------ |
|
||||
| `isDefined(value)` | `true`, pokud hodnota není null/undefined |
|
||||
| `isNonEmptyString(value)` | `true`, pokud je hodnota neprázdným řetězcem |
|
||||
| `includes(array, value)` | `true`, pokud pole obsahuje danou hodnotu |
|
||||
| `includesEvery(array, prop, value)` | `true`, pokud vlastnost každé položky zahrnuje danou hodnotu |
|
||||
| `every(array, prop)` | `true`, pokud je vlastnost u každé položky pravdivá (truthy) |
|
||||
| `everyDefined(array, prop)` | `true`, pokud je vlastnost definována u každé položky |
|
||||
| `everyEquals(array, prop, value)` | `true`, pokud se vlastnost rovná hodnotě u každé položky |
|
||||
| `some(array, prop)` | `true`, pokud je vlastnost pravdivá (truthy) alespoň u jedné položky |
|
||||
| `someDefined(array, prop)` | `true`, pokud je vlastnost definována alespoň u jedné položky |
|
||||
| `someEquals(array, prop, value)` | `true`, pokud se vlastnost rovná hodnotě alespoň u jedné položky |
|
||||
| `someNonEmptyString(array, prop)` | `true`, pokud má vlastnost alespoň u jedné položky hodnotu neprázdného řetězce |
|
||||
| `none(array, prop)` | `true`, pokud je vlastnost u všech položek nepravdivá (falsy) |
|
||||
| `noneDefined(array, prop)` | `true`, pokud je vlastnost u všech položek nedefinovaná |
|
||||
| `noneEquals(array, prop, value)` | `true`, pokud se vlastnost nerovná hodnotě u žádné položky |
|
||||
@@ -0,0 +1,545 @@
|
||||
---
|
||||
title: Frontendové komponenty
|
||||
description: Vytvářejte komponenty Reactu, které se vykreslují uvnitř uživatelského rozhraní Twenty se sandboxovou izolací.
|
||||
icon: window-maximize
|
||||
---
|
||||
|
||||
Frontendové komponenty jsou React komponenty, které se vykreslují přímo v uživatelském rozhraní Twenty. Běží v **izolovaném Web Workeru** s využitím Remote DOM — váš kód je sandboxovaný, ale vykresluje se nativně na stránce, nikoli v iframu.
|
||||
|
||||
## Kde lze použít frontendové komponenty
|
||||
|
||||
Frontendové komponenty se mohou vykreslovat na dvou místech v rámci Twenty:
|
||||
|
||||
* **Postranní panel** — Frontendové komponenty, které nejsou headless, se otevírají v pravém postranním panelu. Toto je výchozí chování, když je frontendová komponenta vyvolána z příkazového menu.
|
||||
* **Widgety (nástěnky a stránky záznamů)** — front komponenty lze vkládat jako widgety do [rozložení stránky](/l/cs/developers/extend/apps/layout/page-layouts). Při konfiguraci nástěnky nebo rozložení stránky záznamu mohou uživatelé přidat widget frontendové komponenty.
|
||||
|
||||
Samotná frontendová komponenta není z uživatelského rozhraní dostupná — je potřeba ji zpřístupnit. Dva způsoby, jak to udělat, jsou:
|
||||
|
||||
* **Spárujte ji s [položkou příkazové nabídky](/l/cs/developers/extend/apps/layout/command-menu-items)** — zaregistruje ji v příkazové nabídce (Cmd+K) a volitelně také jako připnutou rychlou akci.
|
||||
* **Vložte ji jako widget do [rozložení stránky](/l/cs/developers/extend/apps/layout/page-layouts)** — umístí ji na detailní stránku záznamu nebo na nástěnku.
|
||||
|
||||
## Základní příklad
|
||||
|
||||
Nejrychlejší způsob, jak vidět front komponentu v akci, je spárovat ji s [`defineCommandMenuItem`](/l/cs/developers/extend/apps/layout/command-menu-items), aby se objevila jako tlačítko rychlé akce v pravém horním rohu stránky:
|
||||
|
||||
```tsx src/front-components/hello-world.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
const HelloWorld = () => {
|
||||
return (
|
||||
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
|
||||
<h1>Hello from my app!</h1>
|
||||
<p>This component renders inside Twenty.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
name: 'hello-world',
|
||||
description: 'A simple front component',
|
||||
component: HelloWorld,
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/command-menu-items/hello-world.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
|
||||
shortLabel: 'Hello',
|
||||
label: 'Hello World',
|
||||
icon: 'IconBolt',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
});
|
||||
```
|
||||
|
||||
Po synchronizaci pomocí `yarn twenty dev` (nebo po jednorázovém spuštění `yarn twenty dev --once`) se rychlá akce zobrazí v pravém horním rohu stránky:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Tlačítko rychlé akce v pravém horním rohu" />
|
||||
</div>
|
||||
|
||||
Kliknutím na něj vykreslíte komponentu přímo ve stránce.
|
||||
|
||||
## Konfigurační pole
|
||||
|
||||
| Pole | Povinné | Popis |
|
||||
| --------------------- | ------- | ----------------------------------------------------------------- |
|
||||
| `universalIdentifier` | Ano | Stabilní jedinečné ID pro tuto komponentu |
|
||||
| `component` | Ano | Funkce komponenty React |
|
||||
| `name` | Ne | Zobrazovaný název |
|
||||
| `description` | Ne | Popis toho, co komponenta dělá |
|
||||
| `isHeadless` | Ne | Nastavte na `true`, pokud komponenta nemá viditelné UI (viz níže) |
|
||||
|
||||
## Umístění frontendové komponenty na stránku
|
||||
|
||||
Mimo příkazy můžete frontendovou komponentu vložit přímo na stránku záznamu přidáním jako widget v **rozvržení stránky**. Podrobnosti viz [Rozložení stránek](/l/cs/developers/extend/apps/layout/page-layouts).
|
||||
|
||||
## Headless vs. ne-headless
|
||||
|
||||
Front-endové komponenty existují ve dvou režimech vykreslování řízených volbou `isHeadless`:
|
||||
|
||||
**Ne-headless (výchozí)** — Komponenta vykreslí viditelné uživatelské rozhraní. Po vyvolání z menu příkazů se otevře v postranním panelu. Toto je výchozí chování, když je `isHeadless` `false` nebo když tato volba není uvedena.
|
||||
|
||||
**Headless (`isHeadless: true`)** — Komponenta se neviditelně inicializuje na pozadí. Neotevírá postranní panel. Headless komponenty jsou určené pro akce, které provedou logiku a poté se odpojí — například spuštění asynchronního úkolu, navigaci na stránku nebo zobrazení potvrzovacího modálního okna. Přirozeně se hodí ke komponentám SDK Command popsaným níže.
|
||||
|
||||
```tsx src/front-components/sync-tracker.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const SyncTracker = () => {
|
||||
const recordId = useRecordId();
|
||||
|
||||
useEffect(() => {
|
||||
enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
|
||||
}, [recordId]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'sync-tracker',
|
||||
description: 'Tracks record views silently',
|
||||
isHeadless: true,
|
||||
component: SyncTracker,
|
||||
});
|
||||
```
|
||||
|
||||
Protože komponenta vrací `null`, Twenty přeskočí vykreslení kontejneru — v rozvržení se neobjeví žádné prázdné místo. Komponenta má však stále přístup ke všem hookům a API komunikace s hostitelem.
|
||||
|
||||
## Komponenty SDK Command
|
||||
|
||||
Balíček `twenty-sdk` poskytuje čtyři pomocné komponenty Command navržené pro headless front-endové komponenty. Každá komponenta při připojení provede akci, chyby zpracuje zobrazením oznámení ve snackbaru a po dokončení automaticky odpojí front-endovou komponentu.
|
||||
|
||||
Importujte je z `twenty-sdk/command`:
|
||||
|
||||
* **`Command`** — Spustí asynchronní callback přes prop `execute`.
|
||||
* **`CommandLink`** — Naviguje na cestu v aplikaci. Props: `to`, `params`, `queryParams`, `options`.
|
||||
* **`CommandModal`** — Otevře potvrzovací modální okno. Pokud uživatel potvrdí, provede callback `execute`. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
|
||||
* **`CommandOpenSidePanelPage`** — Otevře konkrétní stránku postranního panelu. Props: `page`, `pageTitle`, `pageIcon`.
|
||||
|
||||
Zde je kompletní příklad headless front-endové komponenty, která pomocí `Command` spouští akci z menu příkazů:
|
||||
|
||||
```tsx src/front-components/run-action.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const RunAction = () => {
|
||||
const execute = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
await client.mutation({
|
||||
createTask: {
|
||||
__args: { data: { title: 'Created by my app' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return <Command execute={execute} />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
name: 'run-action',
|
||||
description: 'Creates a task from the command menu',
|
||||
component: RunAction,
|
||||
isHeadless: true,
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/command-menu-items/run-action.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
});
|
||||
```
|
||||
|
||||
A příklad s použitím `CommandModal` k vyžádání potvrzení před provedením:
|
||||
|
||||
```tsx src/front-components/delete-draft.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { CommandModal } from 'twenty-sdk/command';
|
||||
|
||||
const DeleteDraft = () => {
|
||||
const execute = async () => {
|
||||
// perform the deletion
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandModal
|
||||
title="Delete draft?"
|
||||
subtitle="This action cannot be undone."
|
||||
execute={execute}
|
||||
confirmButtonText="Delete"
|
||||
confirmButtonAccent="danger"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
|
||||
name: 'delete-draft',
|
||||
description: 'Deletes a draft with confirmation',
|
||||
component: DeleteDraft,
|
||||
isHeadless: true,
|
||||
});
|
||||
```
|
||||
|
||||
## Volání logické funkce
|
||||
|
||||
Front komponenty běží v prohlížeči v izolovaném web workeru, zatímco [logické funkce](/l/cs/developers/extend/apps/logic/logic-functions) běží na serveru. Neexistuje mezi nimi žádné přímé volání v rámci jednoho procesu — místo toho se front komponenta k logické funkci připojuje přes HTTP.
|
||||
|
||||
Logická funkce deklarovaná pomocí `httpRouteTriggerSettings` je vystavena pod endpointem `/s/` na `${TWENTY_API_URL}/s\<path>`. Vaše front komponenta volá tuto trasu pomocí `RestApiClient` z `twenty-client-sdk/rest`, který se autentizuje pomocí `TWENTY_APP_ACCESS_TOKEN`, který Twenty do workeru vkládá.
|
||||
|
||||
`RestApiClient` je přesně pro tento účel. Z worker prostředí čte `TWENTY_API_URL` a `TWENTY_APP_ACCESS_TOKEN`, přidává hlavičku `Authorization: Bearer`, serializuje a parsuje JSON a vyhazuje `RestApiClientError`, pokud token nebo URL chybí nebo je odpověď mimo rozsah 2xx — takže nemusíte tento boilerplate znovu implementovat v každé komponentě.
|
||||
|
||||
Headless front komponenta může volání spustit při mountu přes komponentu `Command` a poté se automaticky odmountovat:
|
||||
|
||||
```tsx src/front-components/sync-prs.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { RestApiClient } from 'twenty-client-sdk/rest';
|
||||
|
||||
const SyncPrs = () => {
|
||||
const execute = async () => {
|
||||
const client = new RestApiClient();
|
||||
|
||||
await client.post('/s/github/fetch-prs', {
|
||||
owner: 'twentyhq',
|
||||
repo: 'twenty',
|
||||
});
|
||||
};
|
||||
|
||||
return <Command execute={execute} />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'sync-prs',
|
||||
description: 'Triggers the fetch-prs logic function',
|
||||
isHeadless: true,
|
||||
component: SyncPrs,
|
||||
});
|
||||
```
|
||||
|
||||
Cesta předaná klientovi je veřejná cesta trasy — `httpRouteTriggerSettings.path` logické funkce s předponou `/s`. Ponechte `isAuthRequired: true`; klient poskytuje pro vaši komponentu přístupový token aplikace vydaný Twenty:
|
||||
|
||||
```ts src/logic-functions/fetch-prs.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string };
|
||||
// ...fetch from GitHub and persist records...
|
||||
return { ok: true };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '...',
|
||||
name: 'fetch-prs',
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/github/fetch-prs',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`TWENTY_API_URL` a `TWENTY_APP_ACCESS_TOKEN` jsou vloženy automaticky — viz [Proměnné aplikace](#application-variables). Protože tajné proměnné aplikace nejsou nikdy vystaveny front komponentám, ponechte API klíče a další citlivou logiku v logické funkci, ne ve front komponentě.
|
||||
</Note>
|
||||
|
||||
### Reference `RestApiClient`
|
||||
|
||||
Importujte `RestApiClient` z `twenty-client-sdk/rest`. Patří do stejné rodiny klientů jako `CoreApiClient` a `MetadataApiClient`, ale cílí na HTTP trasy vaší aplikace místo na GraphQL API.
|
||||
|
||||
| Metoda | Popis |
|
||||
| --------------------------------- | ------------------------------------------ |
|
||||
| `get(path, options?)` | Odešle požadavek `GET` |
|
||||
| `post(path, body?, options?)` | Odešle požadavek `POST` |
|
||||
| `put(path, body?, options?)` | Odešle požadavek `PUT` |
|
||||
| `patch(path, body?, options?)` | Odešle požadavek `PATCH` |
|
||||
| `delete(path, options?)` | Odešle požadavek `DELETE` |
|
||||
| `request(method, path, options?)` | Obecný požadavek s libovolnou metodou HTTP |
|
||||
|
||||
`options` přijímá `headers`, `query` (záznam parametrů dotazovacího řetězce; hodnoty typu nullish jsou vynechány) a `AbortSignal` prostřednictvím `signal`. Objekt `body`, který není typu `FormData`, je automaticky serializován do JSON. Při `401` klient jednou obnoví přístupový token prostřednictvím hostitele a požadavek znovu odešle.
|
||||
|
||||
Základní URL a token jsou ve výchozím nastavení odvozeny z prostředí. Podle potřeby předávejte konstruktoru přepsané hodnoty — například v testech:
|
||||
|
||||
```ts
|
||||
const client = new RestApiClient({
|
||||
baseUrl: 'https://api.example.com',
|
||||
token: 'my-token',
|
||||
});
|
||||
```
|
||||
|
||||
Neúspěšné požadavky vyvolají `RestApiClientError`, který zpřístupňuje `status`, `statusText`, `url` a parsované `body`:
|
||||
|
||||
```tsx
|
||||
import { RestApiClient, RestApiClientError } from 'twenty-client-sdk/rest';
|
||||
|
||||
const client = new RestApiClient();
|
||||
|
||||
try {
|
||||
const prs = await client.get('/s/github/fetch-prs', {
|
||||
query: { state: 'open' },
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof RestApiClientError) {
|
||||
console.error(error.status, error.body);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Přístup k běhovému kontextu
|
||||
|
||||
Uvnitř komponenty použijte hooky SDK pro přístup k aktuálnímu uživateli, záznamu a instanci komponenty:
|
||||
|
||||
```tsx src/front-components/record-info.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
useUserId,
|
||||
useRecordId,
|
||||
useFrontComponentId,
|
||||
} from 'twenty-sdk/front-component';
|
||||
|
||||
const RecordInfo = () => {
|
||||
const userId = useUserId();
|
||||
const recordId = useRecordId();
|
||||
const componentId = useFrontComponentId();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>User: {userId}</p>
|
||||
<p>Record: {recordId ?? 'No record context'}</p>
|
||||
<p>Component: {componentId}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
|
||||
name: 'record-info',
|
||||
component: RecordInfo,
|
||||
});
|
||||
```
|
||||
|
||||
Dostupné hooky:
|
||||
|
||||
| Hook | Vrací | Popis |
|
||||
| --------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------ |
|
||||
| `useUserId()` | `string` nebo `null` | ID aktuálního uživatele |
|
||||
| `useSelectedRecordIds()` | `string[]` | Všechna vybraná ID záznamů (prázdné pole, pokud není nic vybráno) |
|
||||
| `useRecordId()` | `string` nebo `null` | **Zastaralé.** Použijte místo toho `useSelectedRecordIds()` |
|
||||
| `useFrontComponentId()` | `string` | ID této instance komponenty |
|
||||
| `useColorScheme()` | `'light'` nebo `'dark'` | Aktivní barevné schéma uživatelského rozhraní hostitele (`System` je již vyhodnocen) |
|
||||
| `useFrontComponentExecutionContext(selector)` | různé | Přístup k úplnému kontextu běhu pomocí selektorové funkce |
|
||||
|
||||
## Aplikační proměnné
|
||||
|
||||
Aplikační proměnné definované v [`defineApplication()`](/l/cs/developers/extend/apps/config/application) s `isSecret: false` jsou k dispozici ve front-endových komponentách prostřednictvím pomocné funkce `getApplicationVariable`:
|
||||
|
||||
```tsx src/front-components/greeting.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { getApplicationVariable } from 'twenty-sdk/front-component';
|
||||
|
||||
const Greeting = () => {
|
||||
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
|
||||
|
||||
return <p>Hello, {recipientName}!</p>;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'greeting',
|
||||
component: Greeting,
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Tajné proměnné (`isSecret: true`) **nejsou** zpřístupněny front-endovým komponentám. Jsou k dispozici pouze v [logických funkcích](/l/cs/developers/extend/apps/logic/logic-functions), které běží na straně serveru. Tím se zabrání odesílání citlivých hodnot, jako jsou API klíče, do prohlížeče.
|
||||
</Warning>
|
||||
|
||||
Následující systémové proměnné jsou vždy dostupné přes `process.env`:
|
||||
|
||||
| Proměnná | Popis |
|
||||
| ------------------------- | -------------------------------------------------------------- |
|
||||
| `TWENTY_API_URL` | Základní URL Twenty API |
|
||||
| `TWENTY_APP_ACCESS_TOKEN` | Krátkodobý token s oprávněními omezenými na roli vaší aplikace |
|
||||
|
||||
## API komunikace s hostitelem
|
||||
|
||||
Frontendové komponenty mohou pomocí funkcí z `twenty-sdk` vyvolávat navigaci, modály a oznámení:
|
||||
|
||||
| Funkce | Popis |
|
||||
| ----------------------------------------------- | ------------------------------ |
|
||||
| `navigate(to, params?, queryParams?, options?)` | Přejít na stránku v aplikaci |
|
||||
| `openSidePanelPage(params)` | Otevřít postranní panel |
|
||||
| `closeSidePanel()` | Zavřít postranní panel |
|
||||
| `openCommandConfirmationModal(params)` | Zobrazit potvrzovací dialog |
|
||||
| `enqueueSnackbar(params)` | Zobrazit oznámení typu toast |
|
||||
| `unmountFrontComponent()` | Odpojit komponentu |
|
||||
| `updateProgress(progress)` | Aktualizovat indikátor průběhu |
|
||||
|
||||
Zde je příklad, který používá hostitelské API k zobrazení snackbaru a zavření postranního panelu po dokončení akce:
|
||||
|
||||
```tsx src/front-components/archive-record.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useRecordId } from 'twenty-sdk/front-component';
|
||||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const ArchiveRecord = () => {
|
||||
const recordId = useRecordId();
|
||||
|
||||
const handleArchive = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
await client.mutation({
|
||||
updateTask: {
|
||||
__args: { id: recordId, data: { status: 'ARCHIVED' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
await enqueueSnackbar({
|
||||
message: 'Record archived',
|
||||
variant: 'success',
|
||||
});
|
||||
|
||||
await closeSidePanel();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<p>Archive this record?</p>
|
||||
<button onClick={handleArchive}>Archive</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
|
||||
name: 'archive-record',
|
||||
description: 'Archives the current record',
|
||||
component: ArchiveRecord,
|
||||
});
|
||||
```
|
||||
|
||||
### Práce s více záznamy
|
||||
|
||||
Použijte `useSelectedRecordIds()` pro zpracování více vybraných záznamů. To je užitečné pro hromadné operace:
|
||||
|
||||
```tsx src/front-components/bulk-export.tsx
|
||||
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
|
||||
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
|
||||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const BulkExport = () => {
|
||||
const selectedRecordIds = useSelectedRecordIds();
|
||||
|
||||
const handleExport = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
for (const recordId of selectedRecordIds) {
|
||||
await client.mutation({
|
||||
updateTask: {
|
||||
__args: { id: recordId, data: { exported: true } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await enqueueSnackbar({
|
||||
message: `Exported ${selectedRecordIds.length} records`,
|
||||
variant: 'success',
|
||||
});
|
||||
|
||||
await closeSidePanel();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<p>Export {selectedRecordIds.length} selected record(s)?</p>
|
||||
<button onClick={handleExport}>Export</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
|
||||
name: 'bulk-export',
|
||||
description: 'Export selected records',
|
||||
component: BulkExport,
|
||||
command: {
|
||||
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
|
||||
label: 'Bulk Export',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
conditionalAvailabilityExpression: numberOfSelectedRecords > 0,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Veřejné soubory
|
||||
|
||||
Frontendové komponenty mohou přistupovat k souborům ze složky aplikace `public/` pomocí `getPublicAssetUrl`:
|
||||
|
||||
```tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { getPublicAssetUrl } from 'twenty-sdk/utils';
|
||||
|
||||
const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'logo',
|
||||
component: Logo,
|
||||
});
|
||||
```
|
||||
|
||||
Podrobnosti viz [sekci veřejných souborů](/l/cs/developers/extend/apps/config/public-assets).
|
||||
|
||||
## Stylování
|
||||
|
||||
Frontendové komponenty podporují více přístupů ke stylování. Můžete použít:
|
||||
|
||||
* **Inline styly** — `style={{ color: 'red' }}`
|
||||
* **Komponenty Twenty UI** — import z `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar a další)
|
||||
* **Emotion** — CSS-in-JS s `@emotion/react`
|
||||
* **Styled-components** — vzory `styled.div`
|
||||
* **Tailwind CSS** — utilitní třídy
|
||||
* **Jakákoli CSS-in-JS knihovna** kompatibilní s Reactem
|
||||
|
||||
```tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Button, Tag, Status } from 'twenty-sdk/ui';
|
||||
|
||||
const StyledWidget = () => {
|
||||
return (
|
||||
<div style={{ padding: '16px', display: 'flex', gap: '8px' }}>
|
||||
<Button title="Click me" onClick={() => alert('Clicked!')} />
|
||||
<Tag text="Active" color="green" />
|
||||
<Status color="green" text="Online" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456',
|
||||
name: 'styled-widget',
|
||||
component: StyledWidget,
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Položky navigační nabídky
|
||||
description: Přidejte vlastní položky do postranního panelu pracovního prostoru — odkazy na uložená zobrazení nebo externí adresy URL.
|
||||
icon: bars
|
||||
---
|
||||
|
||||
**Položka navigační nabídky** je položka v levém postranním panelu. Použijte `defineNavigationMenuItem()` k přidání vlastních odkazů do postranního panelu — obvykle jeden pro každé [zobrazení](/l/cs/developers/extend/apps/layout/views), které dodáváte — nebo pro odkaz na externí adresy URL.
|
||||
|
||||
```ts src/navigation-menu-items/example-navigation-menu-item.ts
|
||||
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c',
|
||||
name: 'example-navigation-menu-item',
|
||||
icon: 'IconList',
|
||||
color: 'blue',
|
||||
position: 0,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
## Hlavní body
|
||||
|
||||
* `type` určuje, na co položka nabídky odkazuje. Každý typ je spárován s konkrétním identifikačním polem:
|
||||
|
||||
| Typ | K čemu slouží | Povinné pole |
|
||||
| ------------------------------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| `NavigationMenuItemType.VIEW` | Otevře uložené zobrazení | `viewUniversalIdentifier` |
|
||||
| `NavigationMenuItemType.LINK` | Otevře externí adresu URL | `link` |
|
||||
| `NavigationMenuItemType.FOLDER` | Seskupuje vnořené položky pod štítkem | `name` (a podřízené položky odkazují na složku prostřednictvím `folderUniversalIdentifier`) |
|
||||
| `NavigationMenuItemType.OBJECT` | Otevře výchozí indexovou stránku objektu | `targetObjectUniversalIdentifier` |
|
||||
| `NavigationMenuItemType.PAGE_LAYOUT` | Otevře samostatné rozvržení stránky | `pageLayoutUniversalIdentifier` |
|
||||
|
||||
* `position` určuje pořadí v postranním panelu.
|
||||
|
||||
* `icon` a `color` jsou volitelné a upravují, jak položka vypadá.
|
||||
|
||||
* `folderUniversalIdentifier` je k dispozici také na libovolné položce, aby ji bylo možné vložit do nadřazené položky typu `FOLDER`.
|
||||
|
||||
<Note>
|
||||
**Častý problém:** vytvoření objektu bez souvisejícího zobrazení a položky navigační nabídky způsobí, že je tento objekt pro uživatele neviditelný. Pokud nejde o technický/interní objekt, měl by mít každý vlastní objekt výchozí zobrazení *a* položku v postranním panelu, která na něj odkazuje.
|
||||
</Note>
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: Přehled
|
||||
description: Umístěte svou aplikaci do uživatelského rozhraní Twenty – položky v postranním panelu, uložená zobrazení, karty na stránce záznamu a sandboxované komponenty Reactu.
|
||||
icon: table-columns
|
||||
---
|
||||
|
||||
**Vrstva rozvržení** aplikace Twenty zahrnuje vše, co uživatel vidí: kde se aplikace zobrazuje v postranním panelu, jaká seznamová zobrazení obsahuje, jak jsou uspořádány její stránky s podrobnostmi záznamů a které vlastní komponenty Reactu se na těchto stránkách vykreslují.
|
||||
|
||||
```text
|
||||
Sidebar Record list Record detail page
|
||||
─────── ─────────── ──────────────────
|
||||
[📋 My View] ────▶ ┌──────────┐ ┌─────────────────────┐
|
||||
[📋 Drafts ] │ Companies│ │ Tabs: [Overview ] │
|
||||
[📋 Inbox ] │ ──────── │ │ [Notes ] │
|
||||
▲ │ Apple │ │ [Hello ]◀──── definePageLayoutTab
|
||||
│ │ Acme │ │ │ adds a tab...
|
||||
└ defineNavi- │ … │ │ ┌────────────────┐ │
|
||||
gationMenu- └────▲─────┘ │ │ │ │
|
||||
Item points │ │ │ React UI │◀── …with a
|
||||
to a defineView │ │ │ (sandboxed in │ │ defineFrontComponent
|
||||
└ defineView │ │ a Worker) │ │ widget inside
|
||||
picks columns │ └────────────────┘ │
|
||||
and filters └─────────────────────┘
|
||||
```
|
||||
|
||||
## V této části
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Zobrazení" icon="list" href="/l/cs/developers/extend/apps/layout/views">
|
||||
`defineView` — uložené konfigurace seznamu: viditelné sloupce, filtry, skupiny.
|
||||
</Card>
|
||||
<Card title="Položky navigační nabídky" icon="bars" href="/l/cs/developers/extend/apps/layout/navigation-menu-items">
|
||||
`defineNavigationMenuItem` — položky v postranním panelu odkazující na zobrazení nebo externí adresy URL.
|
||||
</Card>
|
||||
<Card title="Rozvržení stránek" icon="table-columns" href="/l/cs/developers/extend/apps/layout/page-layouts">
|
||||
`definePageLayout` a `definePageLayoutTab` — karty a widgety na stránce s podrobnostmi záznamu.
|
||||
</Card>
|
||||
<Card title="Frontendové komponenty" icon="window-maximize" href="/l/cs/developers/extend/apps/layout/front-components">
|
||||
`defineFrontComponent` — sandboxované komponenty Reactu, které se vykreslují uvnitř Twenty.
|
||||
</Card>
|
||||
<Card title="Položky příkazové nabídky" icon="terminal" href="/l/cs/developers/extend/apps/layout/command-menu-items">
|
||||
`defineCommandMenuItem` — zaregistruje frontendové komponenty jako položky Cmd+K a rychlé akce.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Kde se aplikace zobrazuje
|
||||
|
||||
| Umístění | Co řídí | Entita |
|
||||
| --------------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------- |
|
||||
| **Postranní panel** | Vlastní položka odkazující na uložené zobrazení nebo externí adresu URL | `defineNavigationMenuItem` |
|
||||
| **Seznam záznamů** | Uložené nastavení pro objekt — viditelné sloupce, pořadí, filtry, skupiny | `defineView` |
|
||||
| **Stránka s podrobnostmi záznamu** | Karty a widgety na stránce záznamu (vašeho vlastního objektu nebo standardního) | `definePageLayout`, `definePageLayoutTab` |
|
||||
| **Uvnitř kteréhokoli z výše uvedených** | Vlastní widget Reactu — tlačítka, formuláře, přehledové panely, integrace | `defineFrontComponent` |
|
||||
| **Příkazová nabídka (Cmd+K)** | Připnutá rychlá akce nebo skrytý příkaz | `defineCommandMenuItem` |
|
||||
|
||||
Frontendové komponenty běží uvnitř izolovaného Web Workeru pomocí Remote DOM — vykreslují se na stránce nativně (ne uvnitř iframe), ale nemají přímý přístup k hostitelské stránce ani DOM. Komunikace s Twenty probíhá prostřednictvím hostitelského API pro předávání zpráv.
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
title: Rozvržení stránek
|
||||
description: Přizpůsobte stránky s detailem záznamu – karty, widgety a místa, kde se vykreslují frontendové komponenty – pomocí `definePageLayout` a `definePageLayoutTab`.
|
||||
icon: table-columns
|
||||
---
|
||||
|
||||
**Rozvržení stránky** určuje, jak je uspořádána stránka s detailem záznamu: které karty se zobrazí a jaké widgety obsahují. Použijte `definePageLayout()` k deklaraci rozvržení pro objekt, který vlastníte, nebo `definePageLayoutTab()` k přidání jedné karty do rozvržení, které již existuje (vašeho nebo standardního rozvržení Twenty).
|
||||
|
||||
| Případ použití | Entita |
|
||||
| ----------------------------------------------------------------------------------------------------------- | --------------------- |
|
||||
| Definujte celé rozvržení pro stránku záznamu u objektu, který vlastníte | `definePageLayout` |
|
||||
| Přidejte jednu kartu do existujícího rozvržení (k rozvržení vašeho vlastního objektu nebo ke standardnímu). | `definePageLayoutTab` |
|
||||
|
||||
## definePageLayout
|
||||
|
||||
Použijte to, když vlastníte celou stránku s detailem záznamu – typicky pro vlastní objekt, který jste si definovali sami.
|
||||
|
||||
```ts src/page-layouts/example-record-page-layout.ts
|
||||
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
|
||||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
||||
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
||||
|
||||
export default definePageLayout({
|
||||
universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
|
||||
name: 'Example Record Page',
|
||||
type: 'RECORD_PAGE',
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
tabs: [
|
||||
{
|
||||
universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
|
||||
title: 'Hello World',
|
||||
position: 50,
|
||||
icon: 'IconWorld',
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
widgets: [
|
||||
{
|
||||
universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
|
||||
title: 'Hello World',
|
||||
type: 'FRONT_COMPONENT',
|
||||
configuration: {
|
||||
configurationType: 'FRONT_COMPONENT',
|
||||
frontComponentUniversalIdentifier:
|
||||
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Hlavní body
|
||||
|
||||
* `type` je obvykle `'RECORD_PAGE'` pro úpravu detailního zobrazení konkrétního objektu.
|
||||
* `objectUniversalIdentifier` určuje, na který objekt se toto rozvržení vztahuje.
|
||||
* Každá `tab` definuje sekci stránky s `title`, `position` a `layoutMode` (`CANVAS` pro volné rozvržení).
|
||||
* Každý `widget` uvnitř karty může vykreslit [front component](/l/cs/developers/extend/apps/layout/front-components), seznam relací nebo jiné vestavěné typy widgetů.
|
||||
* `position` na kartách určuje jejich pořadí. Použijte vyšší hodnoty (např. 50) pro umístění vlastních karet za vestavěné.
|
||||
|
||||
## definePageLayoutTab
|
||||
|
||||
Použijte to, když chcete do existujícího rozvržení pouze **přidat** kartu – například kartu analytiky na standardní stránce Company nebo kartu se souhrnem AI připojenou k rozvržení vašeho vlastního objektu.
|
||||
|
||||
```ts src/page-layouts/example-extra-tab.ts
|
||||
import {
|
||||
definePageLayoutTab,
|
||||
PageLayoutTabLayoutMode,
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk/define';
|
||||
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
||||
|
||||
export default definePageLayoutTab({
|
||||
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
|
||||
pageLayoutUniversalIdentifier:
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage
|
||||
.universalIdentifier,
|
||||
title: 'Hello World',
|
||||
position: 1000,
|
||||
icon: 'IconWorld',
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
widgets: [
|
||||
{
|
||||
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002',
|
||||
title: 'Hello World',
|
||||
type: 'FRONT_COMPONENT',
|
||||
configuration: {
|
||||
configurationType: 'FRONT_COMPONENT',
|
||||
frontComponentUniversalIdentifier:
|
||||
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Hlavní body
|
||||
|
||||
* `pageLayoutUniversalIdentifier` je **povinný** a musí odkazovat na rozvržení stránky, které již existuje v době instalace – buď na standardní rozvržení Twenty, nebo na rozvržení definované vaší vlastní aplikací. Meziaplikační odkazy na rozvržení ve vlastnictví jiné nainstalované aplikace nejsou v současnosti podporovány. Když nadřazené rozvržení chybí, instalace selže s jasnou validační chybou.
|
||||
|
||||
* Pro standardní rozvržení Twenty importujte identifikátory z `twenty-sdk/define`:
|
||||
|
||||
```ts
|
||||
import { STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
|
||||
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.personRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.opportunityRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.noteRecordPage.universalIdentifier
|
||||
// …
|
||||
```
|
||||
|
||||
Každá položka rozvržení také zpřístupňuje své `tabs` a jejich `widgets`, takže můžete odkazovat na libovolnou úroveň:
|
||||
|
||||
```ts
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.universalIdentifier
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets.fields.universalIdentifier
|
||||
```
|
||||
|
||||
K dispozici je také krátký alias `STANDARD_PAGE_LAYOUT`:
|
||||
|
||||
```ts
|
||||
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
|
||||
|
||||
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
|
||||
```
|
||||
|
||||
* `widgets` mají rozsah pouze pro tuto kartu – odkazují na [front components](/l/cs/developers/extend/apps/layout/front-components), zobrazení apod. úplně stejně jako widgety definované přímo v `definePageLayout`.
|
||||
|
||||
* `position` určuje pořadí vzhledem ke stávajícím kartám v cílovém rozvržení. Zvolte hodnotu, která umístí vaši kartu tam, kde ji chcete mít, relativně k vestavěným kartám.
|
||||
|
||||
* Použijte to místo `definePageLayout`, když chcete do existujícího rozvržení pouze přidat. Použijte `definePageLayout`, když vlastníte celé rozvržení.
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: Zobrazení
|
||||
description: Dodávejte předem nakonfigurovaná uložená zobrazení – pořadí sloupců, filtry, seskupení – pro objekty ve své aplikaci.
|
||||
icon: list
|
||||
---
|
||||
|
||||
**Zobrazení** je uložená konfigurace toho, jak se zobrazují záznamy objektu: která pole se zobrazují, v jakém pořadí, zda jsou viditelná a jaké filtry nebo seskupení jsou použity. Pomocí `defineView()` můžete s aplikací dodávat předem nakonfigurovaná zobrazení – obvykle výchozí indexové zobrazení pro každý vlastní objekt, který vytvoříte.
|
||||
|
||||
```ts src/views/example-view.ts
|
||||
import { defineView, ViewKey } from 'twenty-sdk/define';
|
||||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
||||
import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
||||
|
||||
export default defineView({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'All example items',
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
icon: 'IconList',
|
||||
key: ViewKey.INDEX,
|
||||
position: 0,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
|
||||
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Hlavní body
|
||||
|
||||
* `objectUniversalIdentifier` určuje, na který objekt se toto zobrazení vztahuje. Může to být vlastní objekt, který jste definovali, nebo standardní objekt Twenty.
|
||||
* `key` určuje typ zobrazení — `ViewKey.INDEX` je hlavní seznamové zobrazení pro daný objekt.
|
||||
* `fields` určuje, které sloupce se zobrazí a v jakém pořadí. Každé pole odkazuje na `fieldMetadataUniversalIdentifier`.
|
||||
* Pro pokročilé konfigurace můžete také deklarovat `filters`, `filterGroups`, `groups` a `fieldGroups`.
|
||||
* `position` určuje pořadí, pokud pro stejný objekt existuje více zobrazení.
|
||||
|
||||
## Filtry
|
||||
|
||||
Zobrazení může být dodáno s předem aplikovanými filtry. Každý filtr má tři souřadnice: **pole**, které se filtruje, **operand** (jak porovnávat) a **hodnotu** (proti čemu porovnávat). Všechny tři musí být v souladu — použití operandu, který se nehodí k typu pole, bude při synchronizaci odmítnuto.
|
||||
|
||||
```ts
|
||||
import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
|
||||
filters: [
|
||||
{
|
||||
universalIdentifier: '...',
|
||||
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: ['ACTIVE'],
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
### Podporované operandy podle typu pole
|
||||
|
||||
| Typ pole | Podporované operandy |
|
||||
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `BOOLEAN` | `IS` |
|
||||
| `UUID` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `TS_VECTOR` | `VECTOR_SEARCH` |
|
||||
|
||||
> Typy polí s podobnými názvy mohou používat zcela odlišné operandy — běžným případem jsou `SELECT` a `MULTI_SELECT`.
|
||||
|
||||
### Tvar hodnoty podle operandu
|
||||
|
||||
Pole `value` je vždy hodnota serializovatelná do JSON, ale její očekávaný tvar závisí na operandu:
|
||||
|
||||
| Skupina operandů | Tvar hodnoty | Příklad |
|
||||
| --------------------------------------------------------------------- | ----------------------------- | ------------------------ |
|
||||
| `IS`, `IS_NOT` na `SELECT` | pole klíčů možností (řetězce) | `['ACTIVE', 'PENDING']` |
|
||||
| `CONTAINS`, `DOES_NOT_CONTAIN` na `MULTI_SELECT` | pole klíčů možností (řetězce) | `['TAG_A']` |
|
||||
| `IS`, `IS_NOT` na `RELATION` | pole ID záznamů (uuid) | `['c5a1...']` |
|
||||
| `CONTAINS`, `DOES_NOT_CONTAIN` na textových polích a podobných typech | textový řetězec | `'acme'` |
|
||||
| `IS`, `IS_NOT` na `NUMBER` | řetězec (hodnota) | `'5'` |
|
||||
| `IS` na `RATING` / `UUID` | řetězec (hodnota) | `'5'` |
|
||||
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | řetězec (mezní hodnota) | `'10'` |
|
||||
| `IS`, `IS_BEFORE`, `IS_AFTER` na `DATE` / `DATE_TIME` | řetězec ve formátu ISO 8601 | `'2025-01-01T00:00:00Z'` |
|
||||
| `IS_EMPTY`, `IS_NOT_EMPTY` | prázdný řetězec | `''` |
|
||||
| `IS` na `BOOLEAN` | `'true'` nebo `'false'` | `'true'` |
|
||||
|
||||
## Jak se zobrazení objevují v uživatelském rozhraní
|
||||
|
||||
Samotné zobrazení není z postranního panelu dostupné. Aby se tam zobrazilo, spárujte ho s [položkou navigačního menu](/l/cs/developers/extend/apps/layout/navigation-menu-items) typu `VIEW`, která odkazuje na `universalIdentifier` daného zobrazení. To je kanonický vzor: každý vlastní objekt obvykle dodává výchozí zobrazení + položku v postranním panelu, která ho otevírá.
|
||||
@@ -0,0 +1,192 @@
|
||||
---
|
||||
title: Připojení
|
||||
description: Umožněte své aplikaci jednat jménem uživatele ve službách třetích stran prostřednictvím OAuth.
|
||||
icon: plug
|
||||
---
|
||||
|
||||
Připojení jsou pověření, která uživatel uchovává pro externí službu (Linear, GitHub, Slack, ...). Vaše aplikace deklaruje, **jak** se tato pověření získávají — **poskytovatel připojení** — a za běhu je používá k provádění ověřených volání na rozhraní API třetí strany.
|
||||
|
||||
V současnosti je podporován pouze OAuth 2.0. Budoucí typy pověření (osobní přístupové tokeny, klíče API, základní autentizace) se připojí ke stejnému rozhraní — aplikace, které již používají `defineConnectionProvider({ type: 'oauth', ... })` nebudou muset migrovat.
|
||||
|
||||
<AccordionGroup>
|
||||
|
||||
<Accordion title="defineConnectionProvider" description="Definujte, jak vaše aplikace získává připojení">
|
||||
|
||||
Poskytovatel připojení popisuje OAuth handshake, který vaše aplikace potřebuje. Uživatel klikne v nastavení vaší aplikace na "Přidat připojení", projde souhlasovou obrazovkou poskytovatele a v jeho pracovním prostoru se vytvoří řádek `ConnectedAccount`.
|
||||
|
||||
Funkční nastavení vyžaduje **dva soubory** — poskytovatele připojení a odpovídající deklaraci `serverVariables` v `defineApplication`, která obsahuje klientská pověření OAuth.
|
||||
|
||||
```ts src/connection-providers/linear-connection.ts
|
||||
import { defineConnectionProvider } from 'twenty-sdk/define';
|
||||
|
||||
export default defineConnectionProvider({
|
||||
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
|
||||
name: 'linear',
|
||||
displayName: 'Linear',
|
||||
icon: 'IconBrandLinear',
|
||||
type: 'oauth',
|
||||
oauth: {
|
||||
authorizationEndpoint: 'https://linear.app/oauth/authorize',
|
||||
tokenEndpoint: 'https://api.linear.app/oauth/token',
|
||||
scopes: ['read', 'write'],
|
||||
// These must match keys in `defineApplication.serverVariables` below.
|
||||
clientIdVariable: 'LINEAR_CLIENT_ID',
|
||||
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
|
||||
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
|
||||
// 'form-urlencoded' for the token request.
|
||||
tokenRequestContentType: 'form-urlencoded',
|
||||
// Optional: defaults to true. Disable only if the provider rejects PKCE.
|
||||
usePkce: false,
|
||||
// Optional: extra query params on the authorize URL.
|
||||
// authorizationParams: { prompt: 'consent' },
|
||||
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
|
||||
// revokeEndpoint: 'https://example.com/oauth/revoke',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/application.config.ts
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '...',
|
||||
displayName: 'Linear',
|
||||
description: 'Connect Linear to Twenty.',
|
||||
// OAuth client credentials live on the app registration (one OAuth app per
|
||||
// Twenty server, configured by the admin) — not per-workspace. Declare them
|
||||
// as serverVariables so the admin can fill them in once for all installs.
|
||||
serverVariables: {
|
||||
LINEAR_CLIENT_ID: {
|
||||
description: 'OAuth client ID from your Linear OAuth application.',
|
||||
isSecret: false,
|
||||
isRequired: true,
|
||||
},
|
||||
LINEAR_CLIENT_SECRET: {
|
||||
description: 'OAuth client secret from your Linear OAuth application.',
|
||||
isSecret: true,
|
||||
isRequired: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Hlavní body:
|
||||
|
||||
* `name` je jedinečný identifikátor (řetězec) používaný v `listConnections({ providerName })` (kebab-case, musí odpovídat `^[a-z][a-z0-9-]*$`).
|
||||
* `displayName` se zobrazuje na kartě nastavení jednotlivé aplikace a v seznamu nástrojů AI.
|
||||
* `clientIdVariable` / `clientSecretVariable` jsou názvy, ne hodnoty — musí odpovídat klíčům deklarovaným v `defineApplication.serverVariables`. Skutečné `client_id` a `client_secret` zadává správce serveru prostřednictvím rozhraní pro registraci aplikace, nikdy se necommitují do vašeho repozitáře.
|
||||
* Použijte `serverVariables` (nikoli `applicationVariables`) — pověření OAuth jsou celoserverová a na jeden server Twenty je jedna aplikace OAuth.
|
||||
* Dokud nejsou vyplněny obě `serverVariables`, karta nastavení aplikace zobrazuje nápovědu "vyžaduje správce serveru" a tlačítko "Přidat připojení" je zakázané.
|
||||
* `type: 'oauth'` je dnes jediná podporovaná hodnota. Rozlišovač je kompatibilní do budoucna: budoucí typy (`'pat'`, `'api-key'`, ...) přidají nové podbloky konfigurace vedle `oauth`.
|
||||
|
||||
URL zpětného volání OAuth, kterou musí váš poskytovatel zařadit na seznam povolených, je:
|
||||
|
||||
```
|
||||
https://<your-twenty-server>/auth/apps/callback
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="listConnections / getConnection" description="Použijte připojení z logické funkce">
|
||||
|
||||
Uvnitř handleru logické funkce vrací `listConnections({ providerName })` řádky `ConnectedAccount` této aplikace pro daného poskytovatele s obnovenými přístupovými tokeny.
|
||||
|
||||
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
|
||||
import { listConnections } from 'twenty-sdk/logic-function';
|
||||
|
||||
export const createLinearIssueHandler = async (input: {
|
||||
teamId?: string;
|
||||
title?: string;
|
||||
}) => {
|
||||
if (!input.teamId || !input.title) {
|
||||
return { success: false, error: 'teamId and title are required' };
|
||||
}
|
||||
|
||||
const connections = await listConnections({ providerName: 'linear' });
|
||||
|
||||
// Workspace-shared credentials win when present; fall back to the first
|
||||
// user-visibility one. For HTTP-route triggers you typically pick the
|
||||
// request user's connection via event.userWorkspaceId instead.
|
||||
const connection =
|
||||
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
|
||||
|
||||
if (!connection) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'Linear is not connected. Open the app settings and click "Add connection".',
|
||||
};
|
||||
}
|
||||
|
||||
// Use connection.accessToken to call the third-party API.
|
||||
const response = await fetch('https://api.linear.app/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${connection.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
|
||||
}),
|
||||
});
|
||||
|
||||
return { success: response.ok };
|
||||
};
|
||||
```
|
||||
|
||||
Každé připojení má:
|
||||
|
||||
| Pole | Popis |
|
||||
| ----------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | Jedinečné ID řádku; předejte do `getConnection(id)` pro opětovné načtení jednoho záznamu |
|
||||
| `visibility` | `'user'` (soukromé pro jednoho člena pracovního prostoru) nebo `'workspace'` (sdílené se všemi členy) |
|
||||
| `scopes` | Oprávnění OAuth udělená poskytovatelem (odlišná od `visibility` — ty spolu nesouvisejí) |
|
||||
| `userWorkspaceId` | ID `userWorkspace` vlastníka — užitečné pro výběr "připojení uživatele požadavku" ve spouštěčích tras HTTP |
|
||||
| `accessToken` | Aktuální přístupový token OAuth (v případě vypršení je automaticky obnoven) |
|
||||
| `name` / `handle` | Zobrazovaný název připojení (automaticky odvozený při OAuth callbacku, uživatelem přejmenovatelný) |
|
||||
| `authFailedAt` | Nastaveno, když poslední obnovení selhalo; uživatel se musí znovu připojit |
|
||||
|
||||
Hlavní body:
|
||||
|
||||
* Předejte `{ providerName }` pro filtrování podle poskytovatele; vynechejte jej, chcete-li získat všechna připojení, která tato aplikace vlastní napříč všemi poskytovateli.
|
||||
* Server před vrácením výsledku transparentně obnoví přístupový token. Váš handler vždy uvidí použitelný token (nebo nastavené `authFailedAt`).
|
||||
* `getConnection(id)` je jednořádkový ekvivalent.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Viditelnost pro jednotlivce vs. sdílená v pracovním prostoru" description="Jak si uživatelé vybírají mezi soukromými a sdílenými pověřeními">
|
||||
|
||||
Když uživatel klikne na "Přidat připojení", je vyzván k výběru viditelnosti:
|
||||
|
||||
* **Jen pro mě** — pověření je soukromé pro připojujícího se uživatele. Jakákoli logická funkce volaná jejich jménem (spouštěč HTTP trasy s `isAuthRequired: true`) jej uvidí; spouštěče cron a události databáze nikoli.
|
||||
* **Sdíleno v pracovním prostoru** — jakýkoli člen pracovního prostoru může pověření použít. Spouštěče cron/databáze jej také uvidí, protože nemají žádného uživatele požadavku.
|
||||
|
||||
Pro každý handler použijte tu správnou variantu:
|
||||
|
||||
```ts
|
||||
// HTTP-route trigger — prefer the request user's own connection.
|
||||
const conn =
|
||||
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
|
||||
connections.find((c) => c.visibility === 'workspace');
|
||||
|
||||
// Cron trigger — no request user; only shared credentials are sensible.
|
||||
const conn = connections.find((c) => c.visibility === 'workspace');
|
||||
```
|
||||
|
||||
Více připojení na (uživatele, poskytovatele) je povoleno, takže tentýž uživatel může mít vedle sebe "Personal Linear" a "Work Linear".
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Jednorázové nastavení poskytovatele" description="Zaregistrujte svou aplikaci OAuth u služby třetí strany">
|
||||
|
||||
Pro každého poskytovatele připojení musí správce serveru nejprve zaregistrovat u třetí strany aplikaci OAuth.
|
||||
|
||||
1. Přejděte do vývojářského nastavení poskytovatele (např. https://linear.app/settings/api/applications/new).
|
||||
2. Nastavte **Redirect URI** na `\<SERVER_URL>/auth/apps/callback`.
|
||||
3. Zkopírujte vygenerované **Client ID** a **Client Secret**.
|
||||
4. Otevřete nainstalovanou aplikaci v Twenty jako správce serveru → nastavte hodnoty na odpovídajících `serverVariables`.
|
||||
5. Členové pracovního prostoru pak mohou přidávat připojení v sekci aplikace **Připojení**.
|
||||
|
||||
</Accordion>
|
||||
|
||||
</AccordionGroup>
|
||||
@@ -0,0 +1,515 @@
|
||||
---
|
||||
title: Logické funkce
|
||||
description: Definujte serverové funkce v TypeScriptu se spouštěči pro HTTP, cron a databázové události.
|
||||
icon: bolt
|
||||
---
|
||||
|
||||
Logické funkce jsou serverové funkce v TypeScriptu, které běží na platformě Twenty. Mohou být spouštěny požadavky HTTP, plány cronu nebo databázovými událostmi — a lze je také zpřístupnit jako nástroje pro agenty AI.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="defineLogicFunction" description="Definujte logické funkce a jejich spouštěče">
|
||||
|
||||
Každý soubor funkce používá `defineLogicFunction()` k exportu konfigurace s obslužnou funkcí (handlerem) a volitelnými spouštěči.
|
||||
|
||||
```ts src/logic-functions/createPostCard.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const handler = async (params: RoutePayload) => {
|
||||
const client = new CoreApiClient();
|
||||
const body = (params.body ?? {}) as { name?: string };
|
||||
const name = body.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world';
|
||||
|
||||
const result = await client.mutation({
|
||||
createPostCard: {
|
||||
__args: { data: { name } },
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'create-new-post-card',
|
||||
timeoutSeconds: 2,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/post-card/create',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
/*databaseEventTriggerSettings: {
|
||||
eventName: 'people.created',
|
||||
},*/
|
||||
/*cronTriggerSettings: {
|
||||
pattern: '0 0 1 1 *',
|
||||
},*/
|
||||
});
|
||||
```
|
||||
|
||||
Dostupné typy spouštěčů:
|
||||
* **httpRoute**: Zpřístupní vaši funkci na HTTP cestě a metodě **pod koncovým bodem `/s/`**:
|
||||
> např. `path: '/post-card/create'` je volatelné na `https://your-twenty-server.com/s/post-card/create`
|
||||
|
||||
<Note>
|
||||
Chcete-li vyvolat logickou funkci spuštěnou trasou z (bezhlavé) front-endové komponenty, podívejte se na [Volání logické funkce](/l/cs/developers/extend/apps/layout/front-components#calling-a-logic-function).
|
||||
</Note>
|
||||
* **cron**: Spouští vaši funkci podle plánu pomocí výrazu CRON.
|
||||
* **databaseEvent**: Spouští se při událostech životního cyklu objektů v pracovním prostoru. Když je operace události `updated`, lze konkrétní sledovaná pole určit v poli `updatedFields`. Pokud zůstane nedefinované nebo prázdné, spustí funkci jakákoli aktualizace.
|
||||
> např. `person.updated`, `*.created`, `company.*`
|
||||
|
||||
<Note>
|
||||
Funkci můžete také spustit ručně pomocí CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:function:exec -n create-new-post-card -p '{"key": "value"}'
|
||||
```
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
```
|
||||
|
||||
Logy můžete sledovat pomocí:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:function:logs
|
||||
```
|
||||
</Note>
|
||||
|
||||
#### Payload spouštěče trasy
|
||||
|
||||
Když spouštěč typu route vyvolá vaši logickou funkci, ta obdrží objekt `RoutePayload`, který odpovídá
|
||||
[AWS HTTP API v2 formátu](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html).
|
||||
Importujte typ `RoutePayload` z `twenty-sdk/logic-function`:
|
||||
|
||||
```ts
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const { headers, queryStringParameters, pathParameters, body } = event;
|
||||
const { method, path } = event.requestContext.http;
|
||||
|
||||
return { message: 'Success' };
|
||||
};
|
||||
```
|
||||
|
||||
Typ `RoutePayload` má následující strukturu:
|
||||
|
||||
| Vlastnost | Typ | Popis | Příklad |
|
||||
| ---------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `headers` | `Record\<string, string \| undefined>` | Záhlaví HTTP (pouze ta uvedená v `forwardedRequestHeaders`) | viz sekci níže |
|
||||
| `queryStringParameters` | `Record\<string, string \| undefined>` | Parametry query stringu (více hodnot spojených čárkami) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
|
||||
| `pathParameters` | `Record\<string, string \| undefined>` | Parametry cesty extrahované ze vzoru trasy | `/users/:id`, `/users/123` -> `{ id: '123' }` |
|
||||
| `body` | `object \| null` | Parsované tělo požadavku (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||||
| `rawBody` | `string \| undefined` | Původní tělo požadavku v UTF-8, před parsováním JSONu. Užitečné pro ověřování podpisů webhooků typu HMAC (např. GitHubův `X-Hub-Signature-256`, Stripe). `undefined`, pokud jej běhové prostředí nezachovalo. | |
|
||||
| `isBase64Encoded` | `boolean` | Zda je tělo kódováno base64 | |
|
||||
| `requestContext.http.method` | `string` | Metoda HTTP (GET, POST, PUT, PATCH, DELETE) | |
|
||||
| `requestContext.http.path` | `string` | Nezpracovaná cesta požadavku | |
|
||||
|
||||
|
||||
#### forwardedRequestHeaders
|
||||
|
||||
Ve výchozím nastavení se záhlaví HTTP z příchozích požadavků z bezpečnostních důvodů do vaší logické funkce **ne** předávají.
|
||||
Chcete-li zpřístupnit konkrétní záhlaví, výslovně je uveďte v poli `forwardedRequestHeaders`:
|
||||
|
||||
```ts
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'webhook-handler',
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/webhook',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: false,
|
||||
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Ve vašem handleru k přeposlaným záhlavím přistupujte takto:
|
||||
|
||||
```ts
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const signature = event.headers['x-webhook-signature'];
|
||||
const contentType = event.headers['content-type'];
|
||||
|
||||
// Validate webhook signature...
|
||||
return { received: true };
|
||||
};
|
||||
```
|
||||
|
||||
<Note>
|
||||
Názvy záhlaví jsou normalizovány na malá písmena. Přistupujte k nim pomocí klíčů s malými písmeny (například `event.headers['content-type']`).
|
||||
</Note>
|
||||
|
||||
#### Vlastní odpověď HTTP
|
||||
|
||||
Ve výchozím nastavení vrácení prosté hodnoty z vašeho handleru odešle tuto hodnotu zpět jako odpověď `200` (JSON pro objekty, `text/plain` pro řetězce). Pro kontrolu stavového kódu a hlaviček odpovědi vraťte `Response` z `twenty-sdk/logic-function`:
|
||||
|
||||
```ts
|
||||
import { Response } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
return new Response('<h1>Hello</h1>', {
|
||||
status: 201,
|
||||
headers: { 'content-type': 'text/html' },
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
Z bezpečnostních důvodů jsou hlavičky odpovědi omezeny na seznam povolených položek. Jakákoli hlavička, která není na seznamu (např. `Set-Cookie`, CORS hlavičky jako `Access-Control-Allow-Origin` nebo vlastní hlavičky `X-*`), je tiše zahozena před odesláním odpovědi. Povolené hlavičky odpovědi jsou:
|
||||
|
||||
* `content-type`
|
||||
* `content-language`
|
||||
* `content-disposition`
|
||||
* `cache-control`
|
||||
* `retry-after`
|
||||
|
||||
<Note>
|
||||
Stavový kód musí být platný stavový kód HTTP (mezi 100 a 599). Názvy hlaviček odpovědi se porovnávají bez rozlišení velikosti písmen.
|
||||
</Note>
|
||||
|
||||
#### Payload spouštěče databázové události
|
||||
|
||||
Když spouštěč databázové události vyvolá vaši logickou funkci, obdrží jeden `DatabaseEventPayload` pro každý změněný záznam. Payload kombinuje metadata o zdrojovém pracovním prostoru a objektu s událostí na úrovni záznamu.
|
||||
|
||||
```ts
|
||||
import type {
|
||||
DatabaseEventPayload,
|
||||
ObjectRecordCreateEvent,
|
||||
ObjectRecordDestroyEvent,
|
||||
ObjectRecordUpdateEvent,
|
||||
} from 'twenty-sdk/logic-function';
|
||||
|
||||
type Person = {
|
||||
id: string;
|
||||
emails?: { primaryEmail?: string };
|
||||
};
|
||||
```
|
||||
|
||||
Tělo zprávy obsahuje:
|
||||
|
||||
| Vlastnost | Popis |
|
||||
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
|
||||
| `name` | Název události, například `person.updated`. |
|
||||
| `workspaceId` | Pracovní prostor, ve kterém k události došlo. |
|
||||
| `objectMetadata` | Metadata objektu, který se změnil. |
|
||||
| `recordId` | ID změněného záznamu. |
|
||||
| `userId`, `userWorkspaceId`, `workspaceMemberId` | Pole aktéra, pokud byla událost způsobena uživatelem pracovního prostoru. |
|
||||
| `properties` | Data záznamu pro událost, s `before`, `after`, `diff` a `updatedFields` v závislosti na operaci. |
|
||||
|
||||
| Událost | Data záznamu |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------------------------- |
|
||||
| `person.created` | `event.properties.after` |
|
||||
| `person.updated` | `event.properties.before`, `event.properties.after`, `event.properties.diff`, `event.properties.updatedFields` |
|
||||
| `person.destroyed` | `event.properties.before` |
|
||||
|
||||
U logických smazání má `.deleted` podobu jako u aktualizace, protože se změní pole `deletedAt` záznamu.
|
||||
Pro trvalá smazání použijte `.destroyed`.
|
||||
|
||||
<Note>
|
||||
`databaseEventTriggerSettings.updatedFields` filtruje, které události aktualizace spustí funkci.
|
||||
`event.properties.updatedFields` říká, která pole se v aktuální události skutečně změnila.
|
||||
</Note>
|
||||
|
||||
Příklad události vytvoření:
|
||||
|
||||
```ts
|
||||
type PersonCreatedEvent = DatabaseEventPayload<
|
||||
ObjectRecordCreateEvent<Person>
|
||||
>;
|
||||
|
||||
const handler = async (event: PersonCreatedEvent) => {
|
||||
const person = event.properties.after;
|
||||
|
||||
return {
|
||||
personId: event.recordId,
|
||||
email: person.emails?.primaryEmail,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Příklad události aktualizace:
|
||||
|
||||
```ts
|
||||
type PersonUpdatedEvent = DatabaseEventPayload<
|
||||
ObjectRecordUpdateEvent<Person>
|
||||
>;
|
||||
|
||||
const handler = async (event: PersonUpdatedEvent) => {
|
||||
const { before, after, diff, updatedFields } = event.properties;
|
||||
|
||||
return {
|
||||
personId: event.recordId,
|
||||
updatedFields,
|
||||
previousEmail: before.emails?.primaryEmail,
|
||||
currentEmail: after.emails?.primaryEmail,
|
||||
emailDiff: diff.emails,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Spouštění pouze při aktualizacích e‑mailu:
|
||||
|
||||
```ts
|
||||
export default defineLogicFunction({
|
||||
...,
|
||||
databaseEventTriggerSettings: {
|
||||
eventName: 'person.updated',
|
||||
updatedFields: ['emails'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Příklad události smazání:
|
||||
|
||||
```ts
|
||||
type PersonDestroyedEvent = DatabaseEventPayload<
|
||||
ObjectRecordDestroyEvent<Person>
|
||||
>;
|
||||
|
||||
const handler = async (event: PersonDestroyedEvent) => {
|
||||
const personBeforeDestroy = event.properties.before;
|
||||
|
||||
return {
|
||||
personId: event.recordId,
|
||||
email: personBeforeDestroy.emails?.primaryEmail,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
#### Zpřístupnění funkce jako nástroje AI nebo akce pracovního postupu
|
||||
|
||||
Logické funkce lze zpřístupnit na dvou rozhraních, z nichž každé má vlastní spouštěč:
|
||||
|
||||
* **`toolTriggerSettings`** — zpřístupní funkci AI funkcím Twenty (chat, MCP, volání funkcí). Používá standardní JSON Schema, formát, kterému modely LLM nativně rozumějí.
|
||||
* **`workflowActionTriggerSettings`** — zobrazí funkci jako krok ve vizuálním builderu workflow. Používá bohaté `InputSchema` od Twenty, aby builder mohl vykreslit správné editory polí, voliče proměnných a štítky.
|
||||
|
||||
Funkce se může rozhodnout pro jedno, druhé nebo obě. Stojí po boku `cronTriggerSettings`, `databaseEventTriggerSettings` a `httpRouteTriggerSettings` — stejný vzor, stejná struktura.
|
||||
|
||||
```ts src/logic-functions/enrich-company.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const handler = async (params: { companyName: string; domain?: string }) => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
const result = await client.mutation({
|
||||
createTask: {
|
||||
__args: {
|
||||
data: {
|
||||
title: `Enrich data for ${params.companyName}`,
|
||||
body: `Domain: ${params.domain ?? 'unknown'}`,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { taskId: result.createTask.id };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
|
||||
name: 'enrich-company',
|
||||
description: 'Enrich a company record with external data',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
toolTriggerSettings: {},
|
||||
});
|
||||
```
|
||||
|
||||
Hlavní body:
|
||||
|
||||
* Funkce může míchat rozhraní — deklarujte jak `toolTriggerSettings`, tak `workflowActionTriggerSettings`, abyste ji zpřístupnili v chatu i ve workflow builderu.
|
||||
* `toolTriggerSettings.inputSchema` a `workflowActionTriggerSettings.inputSchema` jsou obě volitelné. Pokud jsou vynechány, sestavovač manifestu je odvodí ze zdrojového kódu handleru (JSON Schema pro nástroj AI, `InputSchema` od Twenty pro akci workflow). Uveďte jej explicitně, když chcete bohatší typování — například u polí s podporou `FieldMetadataType`, jako `CURRENCY` nebo `RELATION` pro workflow builder, nebo s poli `description`, která si AI agent může přečíst:
|
||||
|
||||
```ts
|
||||
export default defineLogicFunction({
|
||||
...,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company to enrich',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The company website domain (optional)',
|
||||
},
|
||||
},
|
||||
required: ['companyName'],
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
**Napište kvalitní `description`.** Agenti AI se spoléhají na pole funkce `description` při rozhodování, kdy nástroj použít. Buďte konkrétní ohledně toho, co nástroj dělá a kdy se má volat.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
<Note>
|
||||
**Instalační hooky** — předinstalační a poinstalační handlery — sdílejí toto běhové prostředí, ale deklarují se vlastními funkcemi `define` a nepřebírají nastavení spouštěče (triggeru). Viz [Instalační hooky](/l/cs/developers/extend/apps/config/install-hooks) pro `definePreInstallLogicFunction` a `definePostInstallLogicFunction`.
|
||||
</Note>
|
||||
|
||||
## Typovaní klienti API (twenty-client-sdk)
|
||||
|
||||
Balíček `twenty-client-sdk` poskytuje dva typované klienty GraphQL pro práci s Twenty API z vašich logických funkcí a frontendových komponent.
|
||||
|
||||
| Klient | Importovat | Koncový bod | Generováno? |
|
||||
| ------------------- | ---------------------------- | ---------------------------------------------------------------- | ------------------------------ |
|
||||
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — data pracovního prostoru (záznamy, objekty) | Ano, při vývoji/sestavení |
|
||||
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — konfigurace pracovního prostoru, nahrávání souborů | Ne, dodává se předem sestavený |
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="CoreApiClient" description="Dotazování a změny dat pracovního prostoru (záznamy, objekty)">
|
||||
|
||||
`CoreApiClient` je hlavní klient pro dotazování a mutace dat pracovního prostoru. Generuje se **z vašeho schématu pracovního prostoru** během `yarn twenty dev` nebo `yarn twenty dev:build`, takže je plně typovaný tak, aby odpovídal vašim objektům a polím.
|
||||
|
||||
```ts
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const client = new CoreApiClient();
|
||||
|
||||
// Query records
|
||||
const { companies } = await client.query({
|
||||
companies: {
|
||||
edges: {
|
||||
node: {
|
||||
id: true,
|
||||
name: true,
|
||||
domainName: {
|
||||
primaryLinkLabel: true,
|
||||
primaryLinkUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Create a record
|
||||
const { createCompany } = await client.mutation({
|
||||
createCompany: {
|
||||
__args: {
|
||||
data: {
|
||||
name: 'Acme Corp',
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Klient používá syntaxi výběrové sady (selection-set): předáním `true` zahrnete pole, pro argumenty použijte `__args` a pro relace vnořujte objekty. Získáte plné automatické doplňování a kontrolu typů založené na schématu vašeho pracovního prostoru.
|
||||
|
||||
<Note>
|
||||
**CoreApiClient je generován při vývoji/sestavení.** Pokud jej použijete bez předchozího spuštění `yarn twenty dev` nebo `yarn twenty dev:build`, vyvolá chybu. Generování probíhá automaticky — CLI prozkoumá GraphQL schéma vašeho pracovního prostoru a vygeneruje typovaného klienta pomocí `@genql/cli`.
|
||||
</Note>
|
||||
|
||||
#### Použití CoreSchema pro anotace typů
|
||||
|
||||
`CoreSchema` poskytuje typy TypeScriptu odpovídající objektům vašeho pracovního prostoru — hodí se pro typování stavu komponent nebo parametrů funkcí:
|
||||
|
||||
```ts
|
||||
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
|
||||
import { useState } from 'react';
|
||||
|
||||
const [company, setCompany] = useState<
|
||||
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
|
||||
>(undefined);
|
||||
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.query({
|
||||
company: {
|
||||
__args: { filter: { position: { eq: 1 } } },
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
setCompany(result.company);
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="MetadataApiClient" description="Konfigurace pracovního prostoru, aplikace a nahrávání souborů">
|
||||
|
||||
`MetadataApiClient` je dodáván předem sestavený v rámci SDK (není vyžadováno žádné generování). Odesílá dotazy na endpoint `/metadata` pro konfiguraci pracovního prostoru, aplikace a nahrávání souborů.
|
||||
|
||||
```ts
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
// List first 10 objects in the workspace
|
||||
const { objects } = await metadataClient.query({
|
||||
objects: {
|
||||
edges: {
|
||||
node: {
|
||||
id: true,
|
||||
nameSingular: true,
|
||||
namePlural: true,
|
||||
labelSingular: true,
|
||||
isCustom: true,
|
||||
},
|
||||
},
|
||||
__args: {
|
||||
filter: {},
|
||||
paging: { first: 10 },
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Nahrávání souborů
|
||||
|
||||
`MetadataApiClient` obsahuje metodu `uploadFile` pro připojování souborů k polím typu souboru:
|
||||
|
||||
```ts
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
const fileBuffer = fs.readFileSync('./invoice.pdf');
|
||||
|
||||
const uploadedFile = await metadataClient.uploadFile(
|
||||
fileBuffer, // file contents as a Buffer
|
||||
'invoice.pdf', // filename
|
||||
'application/pdf', // MIME type
|
||||
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
|
||||
);
|
||||
|
||||
console.log(uploadedFile);
|
||||
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
|
||||
```
|
||||
|
||||
| Parametr | Typ | Popis |
|
||||
| ---------------------------------- | -------- | ------------------------------------------------------------------- |
|
||||
| `fileBuffer` | `Buffer` | Surový obsah souboru |
|
||||
| `filename` | `string` | Název souboru (používá se pro ukládání a zobrazení) |
|
||||
| `contentType` | `string` | Typ MIME (pokud je vynechán, výchozí je `application/octet-stream`) |
|
||||
| `fieldMetadataUniversalIdentifier` | `string` | `universalIdentifier` pole typu souboru ve vašem objektu |
|
||||
|
||||
Hlavní body:
|
||||
* Používá `universalIdentifier` pole (nikoli jeho ID specifické pro pracovní prostor), takže váš kód pro nahrávání funguje v jakémkoli pracovním prostoru, kde je vaše aplikace nainstalována.
|
||||
* Vrácená hodnota `url` je podepsaná adresa URL, kterou můžete použít k přístupu k nahranému souboru.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
<Note>
|
||||
Když váš kód běží na Twenty (logické funkce nebo frontendové komponenty), platforma vloží přihlašovací údaje jako proměnné prostředí:
|
||||
|
||||
* `TWENTY_API_URL` — Základní URL Twenty API
|
||||
* `TWENTY_APP_ACCESS_TOKEN` — krátkodobý klíč s rozsahem omezeným na výchozí roli funkce vaší aplikace
|
||||
|
||||
Není nutné je předávat klientům — čtou je automaticky z `process.env`. Oprávnění API klíče jsou určena rolí deklarovanou pomocí `defineApplicationRole()` (nebo odkazovanou prostřednictvím `defaultRoleUniversalIdentifier` v `application-config.ts`).
|
||||
</Note>
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: Přehled
|
||||
description: Server-side TypeScript, který běží uvnitř Twenty — spouštěný pomocí HTTP rout, plánů CRON, databázových událostí, nástrojů AI nebo akcí pracovního postupu.
|
||||
icon: bolt
|
||||
---
|
||||
|
||||
**Logická vrstva** aplikace Twenty je kód, který *běží* — server-side TypeScript handlery reagující na HTTP požadavky, plány CRON a změny záznamů; AI dovednosti a agenti, kteří fungují uvnitř pracovního prostoru; a připojení OAuth, která umožňují vašim funkcím jednat jménem uživatele ve službách třetích stran.
|
||||
|
||||
```text
|
||||
┌─ HTTP route ──┐
|
||||
│ Cron schedule │
|
||||
│ Database event │ ┌────────────────────┐
|
||||
triggers ─┤ AI tool call ├─────▶│ Logic function │
|
||||
│ Workflow action │ │ (your handler) │
|
||||
│ Manual exec │ └────────────────────┘
|
||||
└────────────────────┘ │
|
||||
▼
|
||||
┌────────────────────────────┐
|
||||
│ Twenty API (records) │
|
||||
│ Third-party API │
|
||||
│ (via Connection token) │
|
||||
└────────────────────────────┘
|
||||
```
|
||||
|
||||
## V této části
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Logické funkce" icon="bolt" href="/l/cs/developers/extend/apps/logic/logic-functions">
|
||||
Základní stavební blok — typy spouštěčů, payloady a typovaný klient API.
|
||||
</Card>
|
||||
<Card title="Dovednosti a agenti" icon="robot" href="/l/cs/developers/extend/apps/logic/skills-and-agents">
|
||||
Opakovaně použitelné pokyny pro agenty AI a asistenti s vlastními systémovými prompty.
|
||||
</Card>
|
||||
<Card title="Připojení" icon="plug" href="/l/cs/developers/extend/apps/logic/connections">
|
||||
Přihlašovací údaje OAuth, které vaše aplikace uchovává pro služby třetích stran — Linear, GitHub, Slack a další.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Přehled typů spouštěčů
|
||||
|
||||
Logická funkce volí jeden nebo více spouštěčů — každá z níže uvedených položek je samostatné pole v `defineLogicFunction()`:
|
||||
|
||||
| Spouštěč | Kdy se spouští | Nastavení |
|
||||
| --------------------------- | ----------------------------------------------------------------- | ------------------------------- |
|
||||
| **HTTP route** | Požadavek dorazí na váš koncový bod `/s/\<path>` | `httpRouteTriggerSettings` |
|
||||
| **Cron** | CRON výraz se shoduje | `cronTriggerSettings` |
|
||||
| **Událost databáze** | Záznam v pracovním prostoru je vytvořen, aktualizován nebo smazán | `databaseEventTriggerSettings` |
|
||||
| **Nástroj AI** | Funkce Twenty AI se rozhodne zavolat vaši funkci | `toolTriggerSettings` |
|
||||
| **Akce pracovního postupu** | Krok pracovního postupu vyvolá vaši funkci | `workflowActionTriggerSettings` |
|
||||
|
||||
Funkce běží v izolovaných sandboxovaných procesech Node.js a přistupují k pracovnímu prostoru přes typovaného klienta API omezeného na roli deklarovanou v [`defineApplication()`](/l/cs/developers/extend/apps/config/application).
|
||||
|
||||
<Note>
|
||||
**Instalační hooky** — kód, který běží před nebo po instalaci — sdílejí toto běhové prostředí, ale používají vlastní funkce `define` a nacházejí se pod [Config → Install Hooks](/l/cs/developers/extend/apps/config/install-hooks).
|
||||
</Note>
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
title: Dovednosti a agenti
|
||||
description: Definujte dovednosti a agenty AI pro svou aplikaci.
|
||||
icon: robot
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Dovednosti a agenti jsou aktuálně v alfa fázi. Funkce funguje, ale stále se vyvíjí.
|
||||
</Warning>
|
||||
|
||||
Aplikace mohou definovat schopnosti AI, které fungují přímo v pracovním prostoru — znovupoužitelné pokyny pro dovednosti a agenty s vlastními systémovými prompty.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="defineSkill" description="Definujte dovednosti agentů AI">
|
||||
|
||||
Dovednosti definují znovupoužitelné pokyny a schopnosti, které mohou agenti AI používat ve vašem pracovním prostoru. K definování dovedností s vestavěnou validací použijte `defineSkill()`:
|
||||
|
||||
```ts src/skills/example-skill.ts
|
||||
import { defineSkill } from 'twenty-sdk/define';
|
||||
|
||||
export default defineSkill({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'sales-outreach',
|
||||
label: 'Sales Outreach',
|
||||
description: 'Guides the AI agent through a structured sales outreach process',
|
||||
icon: 'IconBrain',
|
||||
content: `You are a sales outreach assistant. When reaching out to a prospect:
|
||||
1. Research the company and recent news
|
||||
2. Identify the prospect's role and likely pain points
|
||||
3. Draft a personalized message referencing specific details
|
||||
4. Keep the tone professional but conversational`,
|
||||
});
|
||||
```
|
||||
|
||||
Hlavní body:
|
||||
* `name` je jedinečný identifikátor dovednosti (doporučuje se kebab-case).
|
||||
* `label` je uživatelsky čitelný název zobrazovaný v UI.
|
||||
* `content` obsahuje pokyny dovednosti — je to text, který agent AI používá.
|
||||
* `icon` (volitelné) nastavuje ikonu zobrazovanou v UI.
|
||||
* `description` (volitelné) poskytuje doplňující kontext o účelu dovednosti.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineAgent" description="Definujte AI agenty s vlastními prompty">
|
||||
|
||||
Agenti jsou asistenti AI, kteří běží ve vašem pracovním prostoru. K vytvoření agentů s vlastním systémovým promptem použijte `defineAgent()`:
|
||||
|
||||
```ts src/agents/example-agent.ts
|
||||
import { defineAgent } from 'twenty-sdk/define';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
description: 'Helps the sales team draft outreach emails and research prospects',
|
||||
icon: 'IconRobot',
|
||||
prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.',
|
||||
});
|
||||
```
|
||||
|
||||
Hlavní body:
|
||||
* `name` je jedinečný identifikátor agenta (doporučuje se kebab-case).
|
||||
* `label` je zobrazovaný název v UI.
|
||||
* `prompt` je systémový prompt, který definuje chování agenta.
|
||||
* `description` (volitelné) poskytuje kontext o tom, co agent dělá.
|
||||
* `icon` (volitelné) nastavuje ikonu zobrazovanou v UI.
|
||||
* `modelId` (volitelné) přepíše výchozí model AI používaný agentem.
|
||||
* `responseFormat` (volitelně) určuje tvar výstupu agenta. Výchozí hodnota je `{ type: 'text' }` pro volný text. Použijte `{ type: 'json', schema }` k vynucení strukturovaného výstupu ve formátu JSON.
|
||||
|
||||
Ve výchozím nastavení agent vrací volný text. Chcete-li získat strukturovaný výstup, nastavte `responseFormat` na `{ type: 'json' }` a poskytněte `schema`:
|
||||
|
||||
```ts src/agents/structured-agent.ts
|
||||
import { defineAgent } from 'twenty-sdk/define';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: 'c4d5e6f7-a8b9-0123-cdef-456789012345',
|
||||
name: 'lead-scorer',
|
||||
label: 'Lead Scorer',
|
||||
prompt: 'Score the lead and explain your reasoning.',
|
||||
responseFormat: {
|
||||
type: 'json',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
score: { type: 'number', description: 'Lead score from 0 to 100' },
|
||||
summary: { type: 'string', description: 'Short reasoning for the score' },
|
||||
},
|
||||
required: ['score', 'summary'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Poznámky ke schématu:
|
||||
* Schéma je plochý objekt: `type` každé vlastnosti musí být primitivní typ (`string`, `number` nebo `boolean`). Vnořené objekty a pole nejsou podporovány.
|
||||
* `description` (volitelně) u každé vlastnosti navádí model, co má na toto místo doplnit.
|
||||
* `required` (volitelně) vypisuje vlastnosti, které musí model vždy vrátit.
|
||||
* `additionalProperties: false` (volitelně) zakáže jakoukoli vlastnost, která není deklarována v `properties`.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="runAgent" description="Spuštění agenta z logické funkce">
|
||||
|
||||
`runAgent()` umožňuje logické funkci spustit jednoho z agentů vaší aplikace (s jeho dovednostmi a nástroji). Identifikujte agenta pomocí `universalIdentifier`, který jste předali do `defineAgent()`:
|
||||
|
||||
```ts src/logic-functions/run-enricher.ts
|
||||
import { runAgent } from 'twenty-sdk/logic-function';
|
||||
|
||||
const { result, error, success } = await runAgent({
|
||||
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
|
||||
prompt: 'Enrich House Ad <recordId>: fill empty fields from its listing URL.',
|
||||
});
|
||||
```
|
||||
|
||||
Hlavní body:
|
||||
* Agent běží **synchronně** a může sám číst/aktualizovat záznamy pomocí vlastních nástrojů — `runAgent()` vrátí výsledek až po dokončení běhu.
|
||||
* Aplikace může spouštět pouze své vlastní agenty.
|
||||
* [Výchozí role](/l/cs/developers/extend/apps/config/roles) aplikace musí udělovat příznak oprávnění `AI` — přidejte `SystemPermissionFlag.AI` do `permissionFlagUniversalIdentifiers` (nebo nastavte `canAccessAllTools: true`).
|
||||
Bez něj `runAgent()` selže s chybou oprávnění.
|
||||
* Nastavte u logické funkce velkorysou hodnotu `timeoutSeconds` — běh agenta může trvat několik sekund.
|
||||
* `success` je `true` a `result` není null po dokončení běhu; při chybě je `success` `false`, `result` je `null` a `error` obsahuje důvod (například když během běhu workspace vyčerpal AI kredity).
|
||||
|
||||
```ts src/roles/default-role.ts
|
||||
import { defineApplicationRole, SystemPermissionFlag } from 'twenty-sdk/define';
|
||||
|
||||
export default defineApplicationRole({
|
||||
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
|
||||
label: 'Default function role',
|
||||
// runAgent() requires the AI permission flag on the app's default role.
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.AI],
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
**Vyhněte se smyčkám:** pokud voláte `runAgent()` z databázového triggeru `*.updated` a agent aktualizuje stejný záznam, omezte trigger pomocí `updatedFields` na pole, do kterého agent nikdy nezapisuje (např. zdrojovou URL), nebo před voláním `runAgent()` zkontrolujte, zda je některé cílové pole stále prázdné.
|
||||
</Warning>
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: CLI
|
||||
description: příkazy `yarn twenty` pro spouštění funkcí, streamování logů, správu instalací aplikací a přepínání vzdálených serverů.
|
||||
icon: terminal
|
||||
---
|
||||
|
||||
Kromě `dev`, `dev:build`, `dev:add` a `dev:typecheck` poskytuje `yarn twenty` CLI příkazy pro spouštění funkcí, zobrazení logů a správu instalací aplikací.
|
||||
|
||||
## Spouštění funkcí (`yarn twenty dev:function:exec`)
|
||||
|
||||
Spusťte logickou funkci ručně bez vyvolání přes HTTP, cron nebo databázovou událost:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Execute by function name
|
||||
yarn twenty dev:function:exec -n create-new-post-card
|
||||
|
||||
# Execute by universalIdentifier
|
||||
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
|
||||
# Pass a JSON payload
|
||||
yarn twenty dev:function:exec -n create-new-post-card -p '{"name": "Hello"}'
|
||||
|
||||
# Execute the post-install function
|
||||
yarn twenty dev:function:exec --postInstall
|
||||
```
|
||||
|
||||
## Zobrazení logů funkcí (`yarn twenty dev:function:logs`)
|
||||
|
||||
Streamujte výstupní logy běhu logických funkcí vaší aplikace:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Stream all function logs
|
||||
yarn twenty dev:function:logs
|
||||
|
||||
# Filter by function name
|
||||
yarn twenty dev:function:logs -n create-new-post-card
|
||||
|
||||
# Filter by universalIdentifier
|
||||
yarn twenty dev:function:logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
```
|
||||
|
||||
<Note>
|
||||
To se liší od `yarn twenty docker:logs`, který zobrazuje logy kontejneru Docker. `yarn twenty dev:function:logs` zobrazuje logy běhu funkcí vaší aplikace ze serveru Twenty.
|
||||
</Note>
|
||||
|
||||
## Generování typovaného klienta (`yarn twenty dev:generate-client`)
|
||||
|
||||
Znovu vygenerujte typovaného klienta API (`twenty-client-sdk`) ze schématu aktivního vzdáleného serveru, bez sestavování nebo synchronizace aplikace. Použijte jej k získání typovaného klienta v libovolném projektu – například backendové služby v samostatném repozitáři – který komunikuje s vaší instancí Twenty:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# In your project (no Twenty app definition required)
|
||||
yarn add twenty-sdk twenty-client-sdk
|
||||
|
||||
# Connect to the Twenty instance to generate the client from
|
||||
yarn twenty remote:add
|
||||
|
||||
# Generate the typed client into node_modules/twenty-client-sdk
|
||||
yarn twenty dev:generate-client
|
||||
```
|
||||
|
||||
Poté klienta importujte ve svém kódu:
|
||||
|
||||
```typescript
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
```
|
||||
|
||||
Spusťte příkaz znovu pokaždé, když se změní váš datový model, abyste aktualizovali vygenerované typy.
|
||||
|
||||
<Note>
|
||||
Klient je vygenerován uvnitř `node_modules`, takže není verzován spolu s vaším kódem. Spusťte `yarn twenty dev:generate-client` po každé instalaci (například ve skriptu `postinstall` nebo v CI).
|
||||
</Note>
|
||||
|
||||
## Odinstalace aplikace (`yarn twenty app:uninstall`)
|
||||
|
||||
Odeberte svou aplikaci z aktivního pracovního prostoru:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty app:uninstall
|
||||
|
||||
# Skip the confirmation prompt
|
||||
yarn twenty app:uninstall --yes
|
||||
```
|
||||
|
||||
## Správa vzdálených serverů
|
||||
|
||||
**Remote** je server Twenty, ke kterému se vaše aplikace připojuje. Během nastavení jej generátor kostry automaticky vytvoří. Můžete kdykoli přidat další vzdálené servery nebo mezi nimi přepínat.
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Add a new remote (opens a browser for OAuth login)
|
||||
yarn twenty remote:add
|
||||
|
||||
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
|
||||
yarn twenty remote:add --local
|
||||
|
||||
# Add a remote non-interactively (useful for CI)
|
||||
yarn twenty remote:add --url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
|
||||
|
||||
# List all configured remotes
|
||||
yarn twenty remote:list
|
||||
|
||||
# Set the active remote
|
||||
yarn twenty remote:use <name>
|
||||
```
|
||||
|
||||
Vaše přihlašovací údaje jsou uloženy v `~/.twenty/config.json`.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Přehled
|
||||
description: Sestavte, otestujte a doručte svou aplikaci — příkazy CLI, integrační testy, CI a publikování na server nebo do npm.
|
||||
icon: rocket
|
||||
---
|
||||
|
||||
**Provozní vrstva** je všechno, co děláte *na* své aplikaci, nikoli *pomocí* ní: spouštění příkazů CLI, provádění integračních testů proti reálnému serveru Twenty, konfigurace CI a vydávání verzí — buď jako tarball nasazený na jednom serveru, nebo jako balíček npm uvedený v Marketplace.
|
||||
|
||||
```text
|
||||
develop ─▶ test ─▶ build ─▶ deploy / publish
|
||||
─────── ──── ───── ─────────────────
|
||||
yarn yarn yarn yarn twenty app:publish --private (tarball → one server)
|
||||
twenty test twenty
|
||||
dev dev:build yarn twenty app:publish (npm → marketplace)
|
||||
```
|
||||
|
||||
## V této části
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="CLI" icon="terminal" href="/l/cs/developers/extend/apps/operations/cli">
|
||||
Referenční přehled `yarn twenty` — exec, logs, uninstall, remotes.
|
||||
</Card>
|
||||
<Card title="Synchronizace a obnovení" icon="kompas" href="/l/cs/developers/extend/apps/operations/sync-and-recovery">
|
||||
Který příkaz kdy použít, jak číst diff synchronizace a postup obnovy.
|
||||
</Card>
|
||||
<Card title="Testování" icon="flask" href="/l/cs/developers/extend/apps/operations/testing">
|
||||
Nastavení Vitestu, integrační testy, kontrola typů, workflow CI.
|
||||
</Card>
|
||||
<Card title="Publikování" icon="nahrát" href="/l/cs/developers/extend/apps/operations/publishing">
|
||||
Sestavit, nasadit tarball, publikovat do npm, nainstalovat.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,294 @@
|
||||
---
|
||||
title: Publikování
|
||||
icon: nahrát
|
||||
description: Distribuujte svou aplikaci Twenty do Marketplace nebo ji nasaďte interně.
|
||||
---
|
||||
|
||||
## Přehled
|
||||
|
||||
Jakmile je vaše aplikace [sestavena a otestována lokálně](/l/cs/developers/extend/apps/getting-started/concepts), máte dvě cesty, jak ji distribuovat:
|
||||
|
||||
* **Nasaďte tarball** — nahrajte svou aplikaci přímo na konkrétní server Twenty pro interní nebo soukromé použití.
|
||||
* **Publish to npm** — uveďte svou aplikaci v Marketplace Twenty, aby ji mohl kterýkoli pracovní prostor objevit a nainstalovat.
|
||||
|
||||
Obě cesty začínají stejným krokem **build**.
|
||||
|
||||
## Sestavení vaší aplikace
|
||||
|
||||
Spusťte příkaz build ke zkompilování své aplikace a k vygenerování souboru `manifest.json` připraveného k distribuci:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:build
|
||||
```
|
||||
|
||||
Tím se zkompilují zdrojové soubory TypeScriptu, transpilují logické funkce a frontendové komponenty a vše se zapíše do `.twenty/output/`. Přidejte `--tarball`, abyste také vytvořili balíček `.tgz` pro ruční distribuci nebo příkaz publish.
|
||||
|
||||
## Nasazení na server (tarball)
|
||||
|
||||
U aplikací, které nechcete zpřístupnit veřejně — proprietární nástroje, integrace pouze pro enterprise nebo experimentální buildy — můžete nasadit tarball přímo na server Twenty.
|
||||
|
||||
### Předpoklady
|
||||
|
||||
Před nasazením potřebujete nakonfigurovaný vzdálený cíl směřující na cílový server. Vzdálené cíle ukládají adresu URL serveru a přihlašovací údaje lokálně v `~/.twenty/config.json`.
|
||||
|
||||
Přidat vzdálený cíl:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty remote:add --url https://your-twenty-server.com --as production
|
||||
```
|
||||
|
||||
### Nasazení
|
||||
|
||||
Sestavte a nahrajte svou aplikaci na server v jednom kroku:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty app:publish --private
|
||||
# To deploy to a specific remote:
|
||||
# yarn twenty app:publish --private --remote production
|
||||
```
|
||||
|
||||
### Sdílení nasazené aplikace
|
||||
|
||||
<Warning>
|
||||
Sdílení soukromých (tarball) aplikací napříč pracovními prostory je funkcí **Enterprise**. Karta **Distribuce** bude místo ovládacích prvků sdílení zobrazovat výzvu k upgradu, dokud váš pracovní prostor nebude mít platný klíč Enterprise. Přejděte do [Nastavení > Admin Panel > Enterprise](/settings/admin-panel#enterprise) a aktivujte ji.
|
||||
</Warning>
|
||||
|
||||
Aplikace ve formě tarball nejsou uvedeny ve veřejném tržišti, takže je ostatní pracovní prostory na tomtéž serveru procházením neobjeví. Jakmile je váš pracovní prostor na tarifu Enterprise, můžete sdílet nasazenou aplikaci takto:
|
||||
|
||||
1. Přejděte do **Nastavení > Aplikace > Registrace** a otevřete svou aplikaci
|
||||
2. Na kartě **Distribuce** klikněte na **Zkopírovat odkaz ke sdílení**
|
||||
3. Sdílejte tento odkaz s uživateli v jiných pracovních prostorech — zavede je přímo na instalační stránku aplikace
|
||||
|
||||
Odkaz ke sdílení používá základní adresu URL serveru (bez jakékoli subdomény pracovního prostoru), takže funguje pro libovolný pracovní prostor na serveru.
|
||||
|
||||
### Správa verzí
|
||||
|
||||
Při aktualizaci již nasazené tarballové aplikace server vyžaduje, aby hodnota `version` v `package.json` byla **přísně vyšší** (podle řazení [semver](https://semver.org)) než aktuálně nasazená verze. Opětovné nasazení stejné verze nebo odeslání nižší verze je odmítnuto ještě před uložením tarballu — v CLI uvidíte chybu `VERSION_ALREADY_EXISTS`.
|
||||
|
||||
Chcete-li vydat aktualizaci:
|
||||
|
||||
1. Zvyšte hodnotu pole `version` v souboru `package.json` (např. `1.2.3` → `1.2.4`, `1.3.0` nebo `2.0.0`)
|
||||
2. Spusťte `yarn twenty app:publish --private` (nebo `yarn twenty app:publish --private --remote production`)
|
||||
3. Pracovní prostory, které mají aplikaci nainstalovanou, uvidí dostupnou aktualizaci ve svém nastavení
|
||||
|
||||
<Note>
|
||||
Předběžné tagy fungují podle očekávání: zvýšení z `1.0.0-rc.1` → `1.0.0-rc.2` je povoleno a finální vydání jako `1.0.0` je správně rozpoznáno jako vyšší než `1.0.0-rc.5`. Verze v `package.json` musí být platným řetězcem semver.
|
||||
</Note>
|
||||
|
||||
{/* TODO: add screenshot of the Upgrade button */}
|
||||
|
||||
### Kompatibilita verze serveru
|
||||
|
||||
Pokud vaše aplikace používá funkci zavedenou v konkrétní verzi serveru Twenty (například poskytovatelé OAuth přidaní ve verzi 2.3.0), měli byste deklarovat minimální verzi serveru, kterou vaše aplikace vyžaduje, pomocí pole `engines.twenty` v `package.json`:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"name": "twenty-my-app",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"twenty": ">=2.3.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Hodnota je standardní [rozsah SemVer](https://github.com/npm/node-semver#ranges). Běžné vzory:
|
||||
|
||||
| Rozsah | Význam |
|
||||
| ---------------------------------- | ---------------------------------------------- |
|
||||
| `>=2.3.0` | Jakýkoli server od verze 2.3.0 výše |
|
||||
| `>=2.3.0 \<3.0.0` | 2.3.0 nebo novější, ale pod další hlavní verzí |
|
||||
| `^2.3.0` | Stejné jako `>=2.3.0 \<3.0.0` |
|
||||
|
||||
**Co se děje při nasazení a instalaci:**
|
||||
|
||||
* Pokud je `engines.twenty` nastaveno a verze cílového serveru nevyhovuje rozsahu, nasazení (nahrání tarballu) nebo instalace je odmítnuto chybou `SERVER_VERSION_INCOMPATIBLE` a zprávou, která uvádí jak požadovaný rozsah, tak skutečnou verzi serveru.
|
||||
* Pokud `engines.twenty` **není nastaveno**, aplikace je přijata na jakékoli verzi serveru (zpětně kompatibilní se stávajícími aplikacemi).
|
||||
* Pokud server nemá nakonfigurované `APP_VERSION`, kontrola se přeskočí.
|
||||
|
||||
<Note>
|
||||
Server je rozhodující autoritou — ověřuje `engines.twenty` jak při nahrání tarballu, tak při instalaci do pracovního prostoru. Pokud nasazujete tarball mimo standardní proces nebo instalujete z marketplace, server přesto vynucuje kompatibilitu.
|
||||
</Note>
|
||||
|
||||
## Automatizované CI/CD (předpřipravené workflowy)
|
||||
|
||||
Aplikace vygenerované pomocí `create-twenty-app` jsou hned připravené se dvěma workflowy GitHub Actions ve složce `.github/workflows/`. Jsou připravené ke spuštění hned, jakmile repozitář pushnete na GitHub — pro CI není potřeba žádné další nastavení a CD vyžaduje pouze jeden secret.
|
||||
|
||||
### CI — `ci.yml`
|
||||
|
||||
Automaticky spouští integrační testy při každém pushi do `main` a u pull requestů.
|
||||
|
||||
**K čemu slouží:**
|
||||
|
||||
1. Provede checkout zdrojového kódu vaší aplikace.
|
||||
2. Spustí izolovanou testovací instanci Twenty pomocí složené akce `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` (ekvivalent v CI k `yarn twenty docker:start --test`).
|
||||
3. Povolí Corepack, nastaví Node.js podle vašeho `.nvmrc` a nainstaluje závislosti pomocí `yarn install --immutable`.
|
||||
4. Spustí `yarn test` a předá `TWENTY_API_URL` a `TWENTY_API_KEY` ze spuštěné instance, aby vaše testy mohly komunikovat se skutečným serverem.
|
||||
|
||||
**Konfigurační volby:**
|
||||
|
||||
* `TWENTY_VERSION` (env, výchozí hodnota `latest`) — uzamkněte v CI používanou verzi serveru Twenty úpravou této hodnoty v `ci.yml`.
|
||||
* Souběžné běhy jsou seskupeny podle `github.ref` a při nových pushích ruší právě probíhající běhy.
|
||||
|
||||
Nejsou potřeba žádné secrety — testovací instance je efemérní a existuje pouze po dobu běhu úlohy.
|
||||
|
||||
### CD — `cd.yml`
|
||||
|
||||
Nasazuje vaši aplikaci na nakonfigurovaný server Twenty při každém pushi do `main` a volitelně také z pull requestu, pokud je přidán štítek `deploy`.
|
||||
|
||||
**K čemu slouží:**
|
||||
|
||||
1. Provede checkout headu PR (u označených PR) nebo pushnutého commitu.
|
||||
2. Spustí `twentyhq/twenty/.github/actions/deploy-twenty-app@main` — ekvivalent v CI k `yarn twenty app:publish --private`.
|
||||
3. Spustí `twentyhq/twenty/.github/actions/install-twenty-app@main`, aby se nově nasazená verze nainstalovala do cílového pracovního prostoru.
|
||||
|
||||
**Požadovaná konfigurace:**
|
||||
|
||||
| Nastavení | Kde | Účel |
|
||||
| ----------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| `TWENTY_DEPLOY_URL` | `env` v `cd.yml` (výchozí `http://localhost:3000`) | Server Twenty, na který se nasazuje. Před prvním použitím to změňte na skutečnou URL vašeho serveru. |
|
||||
| `TWENTY_DEPLOY_API_KEY` | GitHub repozitář **Settings → Secrets and variables → Actions** | API klíč s oprávněním k nasazení na cílovém serveru. |
|
||||
|
||||
<Note>
|
||||
Výchozí `TWENTY_DEPLOY_URL` `http://localhost:3000` je pouze zástupná hodnota — z runneru hostovaného GitHubem tato adresa nebude dosažitelná. Před povolením CD ji aktualizujte na veřejnou URL vašeho serveru (nebo použijte self-hosted runner s přístupem do sítě).
|
||||
</Note>
|
||||
|
||||
**Spuštění náhledového nasazení z PR:**
|
||||
|
||||
Přidejte k pull requestu štítek `deploy`. Podmínka `if:` v `cd.yml` spustí úlohu pro dané PR s použitím head commitu PR, což vám umožní ověřit změnu na cílovém serveru před sloučením.
|
||||
|
||||
### Připnutí verzí znovupoužitelných akcí
|
||||
|
||||
Obě workflowy odkazují na znovupoužitelné akce na `@main`, takže aktualizace akcí v repozitáři `twentyhq/twenty` se přeberou automaticky. Pokud chcete deterministická sestavení, nahraďte `@main` v každém řádku `uses:` za commit SHA nebo tag vydání.
|
||||
|
||||
## Publikování na npm
|
||||
|
||||
Publikování na npm zajistí, že bude vaše aplikace dohledatelná v Marketplace Twenty. Jakýkoli pracovní prostor Twenty může procházet, instalovat a aktualizovat aplikace z Marketplace přímo z UI.
|
||||
|
||||
### Požadavky
|
||||
|
||||
* Účet na [npm](https://www.npmjs.com)
|
||||
* Klíčové slovo `twenty-app` ve vašem poli `keywords` v souboru `package.json` (přidejte ho ručně — ve výchozím nastavení není zahrnuto v šabloně `create-twenty-app`)
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"name": "twenty-app-postcard-sender",
|
||||
"version": "1.0.0",
|
||||
"keywords": ["twenty-app"]
|
||||
}
|
||||
```
|
||||
|
||||
### Metadata tržiště
|
||||
|
||||
Konfigurace `defineApplication()` podporuje volitelná pole, která určují, jak se vaše aplikace zobrazuje v tržišti. Použijte `logoUrl` a `screenshots` k odkazování na obrázky ze složky `public/`:
|
||||
|
||||
```ts src/application-config.ts
|
||||
export default defineApplication({
|
||||
universalIdentifier: '...',
|
||||
displayName: 'My App',
|
||||
description: 'A great app',
|
||||
logoUrl: 'public/logo.png',
|
||||
screenshots: [
|
||||
'public/screenshot-1.png',
|
||||
'public/screenshot-2.png',
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Podívejte se na [sekci defineApplication](/l/cs/developers/extend/apps/config/application#marketplace-metadata) na stránce Building Apps pro úplný seznam polí tržiště (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl` atd.).
|
||||
|
||||
#### Doporučené rozměry snímků obrazovky
|
||||
|
||||
Tržiště zobrazuje `screenshots` v pevném kontejneru s poměrem stran `8:5` (například `1600×1000 px`).
|
||||
|
||||
<Note>
|
||||
Snímky obrazovky libovolného poměru stran se zobrazují celé a nikdy se neořezávají, ale cokoli výrazně vyššího nebo užšího než `8:5` bude mít po stranách prázdné pruhy.
|
||||
</Note>
|
||||
|
||||
### Publikování
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty app:publish
|
||||
```
|
||||
|
||||
Chcete-li publikovat pod konkrétním dist-tagem (např. `beta` nebo `next`):
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty app:publish --tag beta
|
||||
```
|
||||
|
||||
### Jak funguje objevování v tržišti
|
||||
|
||||
Server Twenty synchronizuje svůj katalog tržiště z registru npm **každou hodinu**.
|
||||
|
||||
Synchronizaci můžete spustit okamžitě místo čekání:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:catalog-sync
|
||||
# To target a specific remote:
|
||||
# yarn twenty dev:catalog-sync --remote production
|
||||
```
|
||||
|
||||
Metadata zobrazená v tržišti pocházejí z vaší konfigurace `defineApplication()` — z polí jako `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` a `termsUrl`.
|
||||
|
||||
<Note>
|
||||
Pokud vaše aplikace nedefinuje `aboutDescription` v `defineApplication()`, tržiště automaticky použije soubor `README.md` vašeho balíčku z npm jako obsah stránky O aplikaci. To znamená, že můžete spravovat jediný soubor README jak pro npm, tak pro tržiště Twenty. Pokud chcete v tržišti jiný popis, explicitně nastavte `aboutDescription`.
|
||||
</Note>
|
||||
|
||||
### Publikování pomocí CI
|
||||
|
||||
Použijte tento pracovní postup GitHub Actions k automatickému publikování při každém vydání (používá [OIDC](https://docs.npmjs.com/trusted-publishers)):
|
||||
|
||||
```yaml filename=".github/workflows/publish.yml"
|
||||
name: Publish
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: https://registry.npmjs.org
|
||||
- run: yarn install --immutable
|
||||
- run: npx twenty dev:build
|
||||
- run: npm publish --provenance --access public
|
||||
working-directory: .twenty/output
|
||||
```
|
||||
|
||||
Pro jiné systémy CI (GitLab CI, CircleCI atd.) platí stejné tři příkazy: `yarn install`, `yarn twenty dev:build` a poté `npm publish` z `.twenty/output`.
|
||||
|
||||
<Note>
|
||||
**npm provenance** je volitelné, ale doporučené. Publikování s `--provenance` přidá k vašemu záznamu na npm odznak důvěryhodnosti a umožní uživatelům ověřit, že balíček byl sestaven z konkrétního commitu ve veřejné CI pipeline. Pokyny k nastavení najdete v [dokumentaci k npm provenance](https://docs.npmjs.com/generating-provenance-statements).
|
||||
</Note>
|
||||
|
||||
## Instalace aplikací
|
||||
|
||||
Jakmile je aplikace publikována (npm) nebo nasazena (tarball), mohou ji pracovní prostory nainstalovat prostřednictvím uživatelského rozhraní.
|
||||
|
||||
Přejděte na stránku **Nastavení > Aplikace** v Twenty, kde lze procházet a instalovat jak aplikace z tržiště, tak aplikace nasazené jako tarball.
|
||||
|
||||
{/* TODO: add screenshot of the UI when the app is registered */}
|
||||
|
||||
Aplikace můžete nainstalovat také z příkazového řádku:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty app:install
|
||||
```
|
||||
|
||||
<Note>
|
||||
Server při instalaci vynucuje verzování semver a zrcadlí pravidla pro nasazení:
|
||||
|
||||
* Instalace stejné verze, která je již nainstalována ve vašem pracovním prostoru, je odmítnuta s chybou `APP_ALREADY_INSTALLED`.
|
||||
* Instalace nižší verze, než je aktuálně nainstalovaná, je odmítnuta s chybou `CANNOT_DOWNGRADE_APPLICATION`.
|
||||
|
||||
K instalaci novější verze ji nejprve nasaďte nebo publikujte, poté znovu spusťte `yarn twenty app:install`.
|
||||
</Note>
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: Synchronizace a obnovení
|
||||
description: Který příkaz kdy použít, jak číst výstup synchronizace a jak postupovat po jednotlivých krocích při obnově v případě odchýlení lokálních metadat — ještě předtím, než sáhnete k úplnému resetu.
|
||||
icon: kompas
|
||||
---
|
||||
|
||||
Lokální vývoj aplikací se točí kolem **synchronizace**: CLI znovu sestaví váš manifest a server aplikuje pouze rozdíly mezi ním a metadaty, která už jsou ve vašem pracovním prostoru. Tato stránka popisuje, po kterém příkazu sáhnout, jak číst, co synchronizace změnila, a co dělat — v daném pořadí — když lokální stav vypadá nekonzistentně.
|
||||
|
||||
## Jaký příkaz, kdy
|
||||
|
||||
<Note>
|
||||
Pro každodenní lokální iteraci téměř vždy chcete `yarn twenty dev`. Nasazování a publikování slouží k vydávání verzí, **ne** pro lokální vývojovou smyčku.
|
||||
</Note>
|
||||
|
||||
| Chcete… | Příkaz | Poznámky |
|
||||
| --------------------------------------------------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
||||
| Iterujte lokálně s živou synchronizací | `yarn twenty dev` | Sleduje vaše soubory a při každé změně spustí synchronizaci. |
|
||||
| Jednorázová synchronizace a ukončení (CI, skripty, hooky) | `yarn twenty dev --once` | Provede jedno sestavení + synchronizaci a skončí. |
|
||||
| Náhled změn **bez jejich aplikování** | `yarn twenty dev --once --dry-run` | Spočítá a vypíše rozdíly; nic nezapisuje. |
|
||||
| Odebrat aplikaci z pracovního prostoru | `yarn twenty app:uninstall` | Přidejte `--yes` pro přeskočení výzvy. |
|
||||
| Odeslat tarball na server | `yarn twenty app:publish --private` | Vyžaduje **přísně vyšší** verzi v `package.json` — viz [Publikování](/l/cs/developers/extend/apps/operations/publishing). |
|
||||
| Publikovat na marketplace (npm) | `yarn twenty app:publish` | — |
|
||||
| Nainstalovat / aktualizovat nasazenou verzi | `yarn twenty app:install` | Nainstaluje aktuálně nasazenou verzi. |
|
||||
| Vymazat lokální server a začít znovu | `yarn twenty docker:reset` | Smaže **všechna** lokální data — krajní řešení. |
|
||||
|
||||
### Lokální synchronizace nevyžaduje zvýšení verze
|
||||
|
||||
Pravidlo striktně rostoucí `version` (`VERSION_ALREADY_EXISTS` při nasazení, `APP_ALREADY_INSTALLED` / `CANNOT_DOWNGRADE_APPLICATION` při instalaci) platí pro **`app:publish` / `app:install`** — cestu vydání. `yarn twenty dev` synchronizuje váš manifest na místě a nikdy nevyžaduje změnu verze, takže kvůli iteraci nemusíte sahat na `package.json`. Pokud zvyšujete verzi, abyste otestovali lokální změnu, používáte cestu vydání, i když chcete vývojovou smyčku.
|
||||
|
||||
## Čtení výstupu synchronizace
|
||||
|
||||
Každá synchronizace vypíše změny metadat, které aplikovala (nebo by aplikovala s `--dry-run`):
|
||||
|
||||
```text filename="Terminal"
|
||||
Metadata changes: 2 created, 1 updated, 1 deleted
|
||||
created objectMetadata rocket
|
||||
created fieldMetadata timelineActivities
|
||||
updated fieldMetadata launchedAt
|
||||
deleted pageLayout legacyTab
|
||||
✓ Synced
|
||||
```
|
||||
|
||||
To je váš první diagnostický nástroj: přesně ukazuje, které objekty, pole a rozložení se změnily, takže si můžete ověřit, že synchronizace udělala to, co jste očekávali, ještě před kontrolou v UI.
|
||||
|
||||
Když synchronizace selže na jedné entitě, chyba uvede problematickou entitu a její `universalIdentifier`, například:
|
||||
|
||||
```text
|
||||
Migration action 'create' for 'fieldMetadata' (universalIdentifier: 2020...4337) failed
|
||||
```
|
||||
|
||||
Tento identifikátor použijte k nalezení entity ve vašem manifestu (a případně v pracovním prostoru) místo hádání, která je v konfliktu.
|
||||
|
||||
## Náhled změn (dry run)
|
||||
|
||||
`yarn twenty dev --once --dry-run` sestaví váš manifest, požádá server o migrační plán a vypíše ho — aniž by cokoli aplikoval. Je to bezpečný způsob, jak si předem zodpovědět otázku „co by tato synchronizace změnila?“ ještě předtím, než se k ní zavážete.
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --once --dry-run
|
||||
```
|
||||
|
||||
```text filename="Terminal"
|
||||
Building manifest...
|
||||
Computing metadata diff (dry run, nothing will be applied)...
|
||||
Metadata changes: 1 created, 1 updated
|
||||
created fieldMetadata timelineActivities
|
||||
updated objectMetadata rocket
|
||||
✓ Dry run complete for My App — no changes were applied
|
||||
```
|
||||
|
||||
Spuštění nanečisto:
|
||||
|
||||
* **Nic nezapisuje** — žádná migrace metadat, žádná aktualizace záznamu aplikace, žádné změny výchozí role / karty a žádná generace API klienta.
|
||||
* Vrací **stejný diff**, jaký by aplikovala skutečná synchronizace, takže můžete předem zkontrolovat vytvořené / aktualizované / smazané entity.
|
||||
* Je užitečný před rizikovou změnou, při kontrole změny vygenerované pomocí AI nebo ve skriptu, který má selhat, pokud se má provést neočekávaná změna.
|
||||
|
||||
<Note>
|
||||
Režim dry run zobrazuje pouze náhled změn **metadat** a vyžaduje, aby byla aplikace alespoň jednou synchronizovaná (aby o ní pracovní prostor věděl). Pokud jej spustíte proti aplikaci, která nikdy nebyla synchronizovaná, server ohlásí, že aplikace není nainstalovaná — nejprve jednou spusťte `yarn twenty dev`.
|
||||
</Note>
|
||||
|
||||
## Postup obnovy
|
||||
|
||||
Když lokální metadata vypadají špatně, postupujte v tomto pořadí a zastavte se, jakmile se problém vyřeší. Každý další krok je rušivější než ten předchozí.
|
||||
|
||||
1. **Znovu synchronizujte.** Znovu spusťte `yarn twenty dev --once`. Synchronizace jsou idempotentní — znovu spuštěný čistý manifest je bezpečný a často vyřeší přechodný problém.
|
||||
2. **Prohlédněte si plán.** Spusťte `yarn twenty dev --once --dry-run`, abyste přesně viděli, co chce další synchronizace změnit, aniž by to aplikovala.
|
||||
3. **Přečtěte si pojmenovanou chybu.** Pokud synchronizace selže, poznamenejte si typ metadat a `universalIdentifier` ve zprávě (viz výše) a tuto entitu najděte ve svém manifestu. Konflikt obvykle ukazuje na duplicitní nebo znovu použitý identifikátor.
|
||||
4. **Odinstalujte a znovu nainstalujte.** `yarn twenty app:uninstall`, poté znovu synchronizujte (`yarn twenty dev`). Tím znovu vybudujete metadata aplikace z čistého stavu, zatímco zbytek vašeho pracovního prostoru zůstane nedotčený.
|
||||
5. **Úplný reset (krajní řešení).** `yarn twenty docker:reset`, poté znovu naplňte data a synchronizujte.
|
||||
|
||||
<Warning>
|
||||
`yarn twenty docker:reset` smaže **veškerá** data ve vaší lokální instanci — každý pracovní prostor, záznam i aplikaci. Použijte jej až tehdy, když selžou předchozí kroky.
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
Nastala chyba v metadatech? Prosíme, [vytvořte issue](https://github.com/twentyhq/twenty/issues/new/choose) a přiložte chybovou zprávu selhané migrace (s typem metadat a `universalIdentifier`), výstup `Metadata changes` ze synchronizace a příkazy, které jste spustili.
|
||||
</Note>
|
||||
|
||||
## Vyhněte se souběžným synchronizacím v jednom pracovním prostoru
|
||||
|
||||
Synchronizace aplikuje migrace metadat. Spouštění několika synchronizačních, nasazovacích nebo instalačních operací proti **stejnému pracovnímu prostoru ve stejnou dobu** — například z víc terminálů nebo od více AI agentů iterujících paralelně — může tyto migrace prokládat a zanechat metadata v částečně aplikovaném stavu.
|
||||
|
||||
Server serializuje synchronizace pro každý pracovní prostor, aby tomu zabránil, ale přesto byste citlivé operace s metadaty měli směrovat přes **jeden jediný** proces místo toho, abyste je spouštěli souběžně. Pokud orchestrujete vývoj s více agenty, směrujte jejich volání sync/deploy/install přes jednu frontu, aby vždy běžel jen jeden proces.
|
||||
|
||||
## Rozlišení typů selhání
|
||||
|
||||
Když se něco pokazí, diff metadat a pojmenované chyby vám umožní lokalizovat selhání:
|
||||
|
||||
* **Chyba sestavení manifestu** — CLI selže ještě před synchronizací (`MANIFEST_BUILD_FAILED`, `TYPECHECK_FAILED`); opravte zdrojový kód své aplikace.
|
||||
* **Chyba synchronizace / migrace** — sestavení proběhne úspěšně, ale aplikování diffu selže a uvede entitu a `universalIdentifier`; opravte konfliktní metadata.
|
||||
* **Chyba za běhu aplikačního kódu** — synchronizace proběhne úspěšně, ale vaše logické funkce nebo komponenty se za běhu chovají nesprávně; zkontrolujte [protokoly funkcí](/l/cs/developers/extend/apps/operations/cli).
|
||||
* **Lokální stav instance** — neplatí nic z výše uvedeného a pracovní prostor stále vypadá chybně; pokračujte dolů po žebříčku obnovy.
|
||||
@@ -0,0 +1,301 @@
|
||||
---
|
||||
title: Testování
|
||||
description: Nastavení Vitestu, integrační testy proti reálnému serveru Twenty, kontrola typů a CI s GitHub Actions.
|
||||
icon: flask
|
||||
---
|
||||
|
||||
SDK poskytuje programová rozhraní, která vám umožní z testovacího kódu aplikaci sestavit, nasadit, nainstalovat a odinstalovat. V kombinaci s [Vitest](https://vitest.dev/) a typovanými klienty API můžete psát integrační testy, které ověří, že vaše aplikace funguje end-to-end proti reálnému serveru Twenty.
|
||||
|
||||
## Používání balíčků npm
|
||||
|
||||
Ve své aplikaci můžete nainstalovat a používat libovolný balíček npm. Logické funkce i frontendové komponenty se bundlují pomocí [esbuild](https://esbuild.github.io/), který vloží všechny závislosti přímo do výstupu — za běhu nejsou potřeba žádné `node_modules`.
|
||||
|
||||
### Instalace balíčku
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add axios
|
||||
```
|
||||
|
||||
Poté jej importujte ve svém kódu:
|
||||
|
||||
```ts src/logic-functions/fetch-data.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import axios from 'axios';
|
||||
|
||||
const handler = async (): Promise<any> => {
|
||||
const { data } = await axios.get('https://api.example.com/data');
|
||||
|
||||
return { data };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '...',
|
||||
name: 'fetch-data',
|
||||
description: 'Fetches data from an external API',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
Stejně to funguje i pro frontendové komponenty:
|
||||
|
||||
```tsx src/front-components/chart.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
const DateWidget = () => {
|
||||
return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'date-widget',
|
||||
component: DateWidget,
|
||||
});
|
||||
```
|
||||
|
||||
### Jak funguje bundlování
|
||||
|
||||
Krok sestavení používá esbuild k vytvoření jediného samostatného souboru pro každou logickou funkci a každou frontendovou komponentu. Všechny importované balíčky jsou vloženy přímo do bundlu.
|
||||
|
||||
**Logické funkce** běží v prostředí Node.js. Vestavěné moduly Node (`fs`, `path`, `crypto`, `http` atd.) jsou k dispozici a není je třeba instalovat.
|
||||
|
||||
**Frontendové komponenty** běží ve Web Workeru. Vestavěné moduly Node nejsou k dispozici — pouze prohlížečová API a balíčky npm, které fungují v prohlížečovém prostředí.
|
||||
|
||||
V obou prostředích jsou jako předpřipravené moduly k dispozici `twenty-client-sdk/core` a `twenty-client-sdk/metadata` — nejsou součástí bundlu, ale server je za běhu načítá.
|
||||
|
||||
## Nastavení
|
||||
|
||||
Vygenerovaná aplikace již obsahuje Vitest. Pokud to nastavujete ručně, nainstalujte závislosti:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add -D vitest vite-tsconfig-paths
|
||||
```
|
||||
|
||||
Vytvořte `vitest.config.ts` v kořeni vaší aplikace:
|
||||
|
||||
```ts vitest.config.ts
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
projects: ['tsconfig.spec.json'],
|
||||
ignoreConfigErrors: true,
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
testTimeout: 120_000,
|
||||
hookTimeout: 120_000,
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL: 'http://localhost:2020',
|
||||
TWENTY_API_KEY: 'your-api-key',
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Vytvořte soubor nastavení, který před spuštěním testů ověří dostupnost serveru:
|
||||
|
||||
```ts src/__tests__/setup-test.ts
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { beforeAll } from 'vitest';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
|
||||
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
|
||||
|
||||
beforeAll(async () => {
|
||||
// Verify the server is running
|
||||
const response = await fetch(`${TWENTY_API_URL}/healthz`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
|
||||
'Start the server before running integration tests.',
|
||||
);
|
||||
}
|
||||
|
||||
// Write a temporary config for the SDK
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(TEST_CONFIG_DIR, 'config.json'),
|
||||
JSON.stringify({
|
||||
remotes: {
|
||||
local: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
}, null, 2),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
## Programová rozhraní SDK
|
||||
|
||||
Subcesta `twenty-sdk/cli` exportuje funkce, které můžete volat přímo z testovacího kódu:
|
||||
|
||||
| Funkce | Popis |
|
||||
| -------------- | ----------------------------------------------------- |
|
||||
| `appBuild` | Sestaví aplikaci a volitelně zabalí tarball |
|
||||
| `appDeploy` | Nahraje tarball na server |
|
||||
| `appInstall` | Nainstaluje aplikaci do aktivního pracovního prostoru |
|
||||
| `appUninstall` | Odinstaluje aplikaci z aktivního pracovního prostoru |
|
||||
|
||||
Každá funkce vrací objekt výsledku se `success: boolean` a buď `data`, nebo `error`.
|
||||
|
||||
## Psání integračního testu
|
||||
|
||||
Zde je kompletní příklad, který aplikaci sestaví, nasadí a nainstaluje a poté ověří, že se objeví v pracovním prostoru:
|
||||
|
||||
```ts src/__tests__/app-install.integration-test.ts
|
||||
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
|
||||
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const APP_PATH = process.cwd();
|
||||
|
||||
describe('App installation', () => {
|
||||
beforeAll(async () => {
|
||||
const buildResult = await appBuild({
|
||||
appPath: APP_PATH,
|
||||
tarball: true,
|
||||
onProgress: (message: string) => console.log(`[build] ${message}`),
|
||||
});
|
||||
|
||||
if (!buildResult.success) {
|
||||
throw new Error(`Build failed: ${buildResult.error?.message}`);
|
||||
}
|
||||
|
||||
const deployResult = await appDeploy({
|
||||
tarballPath: buildResult.data.tarballPath!,
|
||||
onProgress: (message: string) => console.log(`[deploy] ${message}`),
|
||||
});
|
||||
|
||||
if (!deployResult.success) {
|
||||
throw new Error(`Deploy failed: ${deployResult.error?.message}`);
|
||||
}
|
||||
|
||||
const installResult = await appInstall({ appPath: APP_PATH });
|
||||
|
||||
if (!installResult.success) {
|
||||
throw new Error(`Install failed: ${installResult.error?.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await appUninstall({ appPath: APP_PATH });
|
||||
});
|
||||
|
||||
it('should find the installed app in the workspace', async () => {
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
const result = await metadataClient.query({
|
||||
findManyApplications: {
|
||||
id: true,
|
||||
name: true,
|
||||
universalIdentifier: true,
|
||||
},
|
||||
});
|
||||
|
||||
const installedApp = result.findManyApplications.find(
|
||||
(app: { universalIdentifier: string }) =>
|
||||
app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
expect(installedApp).toBeDefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Spuštění testů
|
||||
|
||||
Ujistěte se, že běží váš lokální server Twenty, a poté:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn test
|
||||
```
|
||||
|
||||
Nebo v režimu watch během vývoje:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn test:watch
|
||||
```
|
||||
|
||||
## Kontrola typů
|
||||
|
||||
Kontrolu typů můžete spustit i na vaší aplikaci bez spuštění testů:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:typecheck
|
||||
```
|
||||
|
||||
Spustí se `tsc --noEmit` a nahlásí se případné chyby typů.
|
||||
|
||||
## CI s GitHub Actions
|
||||
|
||||
Generátor kostry vytvoří připravený k použití workflow GitHub Actions v `.github/workflows/ci.yml`. Automaticky spouští integrační testy při každém pushi do `main` a u pull requestů.
|
||||
|
||||
Workflow:
|
||||
|
||||
1. Načte váš kód (checkout).
|
||||
2. Spustí dočasný server Twenty pomocí akce `twentyhq/twenty/.github/actions/spawn-twenty-docker-image`
|
||||
3. Nainstaluje závislosti pomocí `yarn install --immutable`
|
||||
4. Spustí `yarn test` s proměnnými `TWENTY_API_URL` a `TWENTY_API_KEY` vloženými z výstupů akce
|
||||
|
||||
```yaml .github/workflows/ci.yml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
env:
|
||||
TWENTY_VERSION: latest
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Spawn Twenty instance
|
||||
id: twenty
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
|
||||
with:
|
||||
twenty-version: ${{ env.TWENTY_VERSION }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run integration tests
|
||||
run: yarn test
|
||||
env:
|
||||
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
|
||||
```
|
||||
|
||||
Není potřeba konfigurovat žádné secrets — akce `spawn-twenty-docker-image` spustí dočasný server Twenty přímo v runneru a vypíše podrobnosti připojení. Secret `GITHUB_TOKEN` je poskytován GitHubem automaticky.
|
||||
|
||||
Chcete-li připnout konkrétní verzi Twenty místo `latest`, změňte proměnnou prostředí `TWENTY_VERSION` na začátku workflow.
|
||||
@@ -88,7 +88,7 @@ Váš klíč API poskytuje přístup k citlivým datům. Nesdílejte ho s nedův
|
||||
|
||||
Pro vyšší bezpečnost přiřaďte konkrétní roli, abyste omezili přístup:
|
||||
|
||||
1. Přejděte na **Nastavení → Role**
|
||||
1. Přejděte na **Nastavení → Členové → Role**
|
||||
2. Klikněte na roli, kterou chcete přiřadit
|
||||
3. Otevřete záložku **Přiřazení**
|
||||
4. V části **API Keys** klikněte na **+ Přiřadit ke klíči API**
|
||||
|
||||
@@ -51,7 +51,7 @@ Postupujte podle těchto kroků pro ruční nastavení.
|
||||
curl -o .env https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/.env.example
|
||||
```
|
||||
|
||||
2. **Vygenerujte tajné tokeny**
|
||||
2. **Vygenerujte šifrovací klíč**
|
||||
|
||||
Spusťte následující příkaz k generování jedinečného náhodného řetězce:
|
||||
|
||||
@@ -59,16 +59,18 @@ Postupujte podle těchto kroků pro ruční nastavení.
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
**Důležité:** Udržujte tuto hodnotu v tajnosti / nesdílejte ji.
|
||||
**Důležité:** Udržujte tuto hodnotu v tajnosti / nesdílejte ji. Ztráta `ENCRYPTION_KEY` znamená ztrátu přístupu ke všem tajným údajům uloženým v databázi (OAuth tokeny, aplikační proměnné, TOTP tajemství atd.).
|
||||
|
||||
3. **Aktualizujte `.env` soubor**
|
||||
|
||||
Nahraďte místoblokovou hodnotu ve svém .env souboru vygenerovaným tokenem:
|
||||
|
||||
```ini
|
||||
APP_SECRET=první_náhodný_řetězec
|
||||
ENCRYPTION_KEY=random_string
|
||||
```
|
||||
|
||||
Podívejte se na [průvodce rotací klíče](/l/cs/developers/self-host/capabilities/key-rotation) pro pokyny, jak jej rotovat bez prostojů.
|
||||
|
||||
4. **Nastavte Heslo pro Postgres**
|
||||
|
||||
Aktualizujte hodnotu `PG_DATABASE_PASSWORD` ve vašem .env souboru silným heslem bez speciálních znaků.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user