snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
---
title: واجهات برمجة التطبيقات
icon: plug
description: واجهات برمجة تطبيقات REST وGraphQL مُولَّدة من مخطط مساحة عملك.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
## واجهات برمجة تطبيقات بمخطط لكل مستأجر
لا توجد مرجعية ثابتة لواجهة برمجة التطبيقات الخاصة بـ Twenty. لكل مساحة عمل مخططها الخاص — عند إضافة كائن مخصص (مثل `Invoice`)، يحصل فورًا على نقاط نهاية REST وGraphQL مطابقة لتلك الخاصة بالكائنات المدمجة مثل `Company` أو `Person`. تُولَّد واجهة برمجة التطبيقات من المخطط، لذا تستخدم نقاط النهاية أسماء الكائنات والحقول لديك مباشرة — بدون معرّفات غامضة.
وثائق واجهة برمجة التطبيقات الخاصة بمساحة عملك متاحة ضمن **الإعدادات → واجهات برمجة التطبيقات وخطافات الويب** بعد إنشاء مفتاح API. يتضمن ساحة تجريبية تفاعلية يمكنك من خلالها تنفيذ استدعاءات حقيقية على بياناتك.
## واجهتا برمجة تطبيقات
**واجهة برمجة التطبيقات الأساسية** — `/rest/` و`/graphql/`
عمليات CRUD على السجلات: الأشخاص، الشركات، الفرص، وكائناتك المخصصة. استعلام، تصفية، والتنقل عبر العلاقات.
**واجهة برمجة تطبيقات البيانات الوصفية** — `/rest/metadata/` و`/metadata/`
إدارة المخطط: إنشاء/تعديل/حذف الكائنات والحقول والعلاقات. هذه هي الطريقة لتغيير نموذج بياناتك برمجيًا.
كلاهما متاحان عبر REST وGraphQL. تضيف GraphQL عمليات upsert على دفعات وإمكانية التنقل عبر العلاقات في استعلام واحد. نفس البيانات الأساسية بأي من الطريقتين.
## عناوين URL الأساسية
| البيئة | عنوان URL الأساسي |
| ----------------- | ------------------------- |
| السحابة | `https://api.twenty.com/` |
| الاستضافة الذاتية | `https://{your-domain}/` |
## المصادقة
```
Authorization: Bearer YOUR_API_KEY
```
أنشئ مفتاح API من **الإعدادات → واجهات برمجة التطبيقات وخطافات الويب → + Create key**. انسخه فورًا — يُعرَض مرة واحدة فقط. يمكن تقييد نطاق المفاتيح بدور محدد ضمن **الإعدادات → الأعضاء → الأدوار → علامة التبويب Assignment** للحد مما يمكنها الوصول إليه.
<VimeoEmbed videoId="928786722" title="إنشاء مفتاح API" />
للوصول المعتمد على OAuth (تطبيقات خارجية تتصرف نيابةً عن المستخدمين)، راجع [OAuth](/l/ar/developers/extend/oauth).
## عمليات الدفعات
يدعم كلٌّ من REST وGraphQL التجميع لما يصل إلى 60 سجلًا لكل طلب — إنشاء أو تحديث أو حذف. كما تدعم GraphQL عملية upsert على دفعات (إنشاء-أو-تحديث في استدعاء واحد) باستخدام أسماء جمع مثل `CreateCompanies`.
## حدود المعدل
| الحد | القيمة |
| ---------- | ---------------------- |
| الطلبات | 100 استدعاء في الدقيقة |
| حجم الدفعة | 60 سجل لكل استدعاء |

View File

@@ -0,0 +1,124 @@
---
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()`.
* `serverVariables` هي تكوينات وأسرار بنطاق المثيل (مثل مفاتيح واجهة برمجة التطبيقات). على عكس `applicationVariables`، فهي لا تصرح عن أي قيمة في ملف manifest — حيث يقوم مشغّل مساحة العمل بملئها من إعدادات التطبيق، ويتم حقنها في دوال المنطق فقط بعد تعيينها.
* كلا نوعي المتغير يقبلان ‎`isDeprecated: true`‎. استخدم ذلك لإيقاف استخدام متغير بدلاً من حذفه: إبقاء المفتاح معلناً يحافظ على القيمة المخزنة (حذفه يدمّر القيمة التي أدخلها المشغّل)، ولا يزال يتم حقن المتغير، بحيث يمكن لشفرتك أن تعتمد عليه كخيار احتياطي — ‎`process.env.NEW_API_KEY ?? process.env.API_KEY`‎. يختفي المتغير المهمل من إعدادات التطبيق عند عدم تحديد قيمة له، ولا يؤخذ في الحسبان ضمن فحص تهيئة التطبيق، لذا تكون `isDeprecated` أعلى أولوية من `isRequired`.
* لعرض واجهة مستخدم مخصّصة لإعدادات التكوين داخل علامة تبويب **Settings** في التطبيق (بدلًا من قسم تكوين المتغيّرات الافتراضي)، صرّح بمكوّن واجهة أمامية باستخدام [`defineSettingsFrontComponent()`](/l/ar/developers/extend/apps/layout/front-components#custom-settings-component) في ملفه الخاص. يُسمح بواحد فقط لكل تطبيق. الأقسام التي يديرها النظام (الترقية التلقائية، App URL، الاتصالات) تظل مرئية دائمًا.
## أنواع المتغيرات
كل من `applicationVariables` و`serverVariables` يقبلان حقل `type` اختياريًا (ولـ `SELECT` / `MULTI_SELECT`، قائمة `options`). الأنواع المدعومة: `TEXT` (افتراضي)، `BOOLEAN`، `NUMBER`، `NUMERIC`، `DATE`، `DATE_TIME`، `SELECT`، `MULTI_SELECT`، `ARRAY`، `RAW_JSON`، `RICH_TEXT`.
```ts src/application-config.ts
import { defineApplication, FieldType } from 'twenty-sdk/define';
export default defineApplication({
// ...identity, role...
applicationVariables: {
MAX_POSTCARDS: {
universalIdentifier: '5f4497e4-9030-4085-85eb-2c48b8d53713',
description: 'Maximum postcards per batch',
type: FieldType.NUMBER,
value: 10,
},
DEFAULT_REGION: {
universalIdentifier: '76c5c321-b6b6-46eb-b4fc-f9f04bb04227',
description: 'Default shipping region',
type: FieldType.SELECT,
options: [
{ label: 'Europe', value: 'eu' },
{ label: 'United States', value: 'us' },
],
value: 'eu',
},
},
});
```
إن `type` يؤثر فقط على **العرض والتحقق** — حيث يختار حقل الإدخال المطابق في واجهة إعدادات مساحة العمل (زر تبديل، حقل أرقام، قائمة منسدلة، منتقي تاريخ، محرر JSON، …) ويسمح لعملية البناء بالتحقق من صحة إعداداتك (على سبيل المثال، يجب أن يعلن `SELECT` / `MULTI_SELECT` عن `options` غير فارغة). وهو **لا** يغيّر كيفية وصول القيمة إلى الشيفرة الخاصة بك.
تُحَقَن القيم **دائمًا كسلاسل نصية** — فهذا جزء جوهري من متغيرات البيئة (`process.env.*` نصية فقط). عند تشغيل دالة المنطق الخاصة بك، يقوم المنفّذ بتسلسل كل قيمة وفقًا لـ `type` المعلن أثناء بناء `process.env`، بحيث يكون تنسيق السلسلة النصية متّسقًا بغض النظر عن كيفية تعيين القيمة (قيمة افتراضية في manifest، من واجهة الإعدادات، أو من إصدار سابق):
| النوع | سلسلة نصية في `process.env` |
| ------------------------------------- | -------------------------------------- |
| `TEXT`، `SELECT`، `DATE`، `DATE_TIME` | القيمة الخام (`"eu"`، `"2026-01-01"`) |
| `BOOLEAN` | `"true"` / `"false"` |
| `NUMBER`، `NUMERIC` | سلسلة عشرية (`"10"`، `"2.5"`) |
| `MULTI_SELECT`، `ARRAY` | مصفوفة JSON (`'["email","postcard"]'`) |
| `RAW_JSON`، `RICH_TEXT` | كائن JSON (`'{"retries":3}'`) |
حوّل السلسلة النصية مرة أخرى إلى النوع الذي تتوقعه:
```ts
const maxCards = Number(process.env.MAX_POSTCARDS); // "10" -> 10
const enabled = process.env.ENABLE_TRACKING === 'true'; // "true" -> true
const channels = JSON.parse(process.env.ENABLED_CHANNELS ?? '[]'); // '["email"]' -> ["email"]
const config = JSON.parse(process.env.PROVIDER_CONFIG ?? '{}'); // '{"retries":3}' -> { retries: 3 }
```
ينطبق الأمر نفسه على مكوّنات الواجهة الأمامية التي تقرأ القيم عبر `getApplicationVariable('VARIABLE_NAME')` — فالقيمة المعادة هي سلسلة نصية؛ قم بتحليلها حسب الحاجة.
## الدور الافتراضي للوظيفة
يتحكم الدور المعلن باستخدام [`defineApplicationRole()`](/l/ar/developers/extend/apps/config/roles) في ما يمكن لوظائف منطق التطبيق ومكوّنات الواجهة الوصول إليه:
* رموز وقت التشغيل المحقونة في دوال المنطق لديك مستمدة من هذا الدور. تُقيَّد المكالمة التي تتصرف كشخص بشكل أكبر بما يمكن لذلك الشخص القيام به، لذا لا يمكنها أبدًا تجاوز أيٍّ منهما. راجع [صلاحيات الوصول التي تستخدمها المكالمة](/l/ar/developers/extend/apps/logic/logic-functions#whose-access-a-call-uses).
* يكون عميل واجهة برمجة التطبيقات مضبوط الأنواع مقيّدًا بالأذونات الممنوحة لذلك الدور.
* اتبع مبدأ أقل امتياز: صرّح فقط عن الأذونات التي تحتاجها دوالك.
عند إنشاء هيكل لتطبيق جديد، ينشئ CLI ملف دور مبدئي في `src/roles/default-role.ts`. راجع [Roles & Permissions](/l/ar/developers/extend/apps/config/roles) للاطلاع على المرجع الكامل.
## بيانات التعريف لسوق التطبيقات
إذا كنت تخطط لـ [نشر تطبيقك](/l/ar/developers/extend/apps/operations/publishing)، فإن هذه الحقول الاختيارية تتحكّم في كيفية ظهوره في السوق:
| الحقل | الوصف |
| ------------------ | ------------------------------------------------------------------------------------------------------------ |
| `author` | اسم المؤلف أو الشركة |
| `category` | فئة التطبيق لتصفية سوق التطبيقات |
| `logo` | مسار شعار تطبيقك المضمَّن في `public/` (مثلًا، `public/logo.png`) |
| `galleryImages` | مصفوفة لمسارات صور المعرض المضمَّنة في `public/` (مثلًا، `public/screenshot-1.png`) |
| `aboutDescription` | وصف ماركداون أطول لعلامة التبويب "حول". إذا لم يتم تضمينه، يستخدم السوق ملف `README.md` الخاص بالحزمة من npm |
| `websiteUrl` | رابط إلى موقعك الإلكتروني |
| `termsUrl` | رابط إلى شروط الخدمة |
| `emailSupport` | عنوان البريد الإلكتروني للدعم |
| `issueReportUrl` | رابط إلى متتبّع المشاكل |
<Note>
القيمتان `logoUrl` و`screenshots` هما اسمَان مهملان بديلان لـ`logo` و`galleryImages`. عناوين URLs المطلقة الخارجية (`http://` أو `https://`) غير مدعومة لهذه الحقول: سيتم تجاهلها مع إظهار تحذير وقت البناء. بدلًا من ذلك، ضمِّن الصور في مجلد `public/` الخاص بتطبيقك.
</Note>

View File

@@ -0,0 +1,188 @@
---
title: خطافات التثبيت
description: تشغيل المنطق أثناء دورة حياة التثبيت أو الترقية أو إلغاء التثبيت — مثل تعبئة البيانات الأوّلية، ونسخ السجلات احتياطيًا، والتحقق من الترقية، وتنظيف الموارد الخارجية.
icon: wrench
---
خطافات التثبيت هي دوال منطقية خاصة تعمل أثناء دورة حياة التثبيت أو الترقية أو إلغاء التثبيت. تستخدم نفس وقت تشغيل المعالج مثل [دوال المنطق](/l/ar/developers/extend/apps/logic/logic-functions) العادية، ولكن يتم التصريح عنها بدوال تعريف خاصة بها وتعمل خارج نموذج المشغّل المعتاد (HTTP، وcron، وأحداث قاعدة البيانات). تتلقى خطافات التثبيت `InstallPayload` (`{ previousVersion?: string; newVersion: string }` — تكون قيمة `previousVersion` هي `undefined` في حالة التثبيت الجديد)، بينما يتلقى خطاف إلغاء التثبيت `UninstallPayload` (`{ version?: string }` — الإصدار الذي تتم إزالته).
يمكن لكل تطبيق تعريف **خطاف واحد كحد أقصى** من كل نوع (قبل التثبيت، بعد التثبيت، إلغاء التثبيت). سيُنتِج إنشاء ملف البيان خطأً إذا تم اكتشاف أكثر من خطاف واحد من أي نوع.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
## لمحة سريعة
| | `definePreInstallLogicFunction` | `definePostInstallLogicFunction` |
| ------------------ | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| عمليات التشغيل | قبل ترحيل البيانات الوصفية — لا يزال المخطط والبيانات **السابقة** سليمين | بعد الترحيل وإنشاء الـ SDK — أصبح المخطط **الجديد** في مكانه |
| التنفيذ | دائمًا متزامن؛ يحجب عملية التثبيت | غير متزامن بشكل افتراضي (يُوضَع في قائمة الانتظار، 3 محاولات إعادة)؛ تفعيل التزامن اختياري عبر `shouldRunSynchronously: true` |
| عند الفشل | يتم **إحباط** التثبيت قبل أي تغيير في المخطط | غير متزامن: تُعاد المحاولة حتى 3 مرات. متزامن: يتلقى المستدعي `POST_INSTALL_ERROR` (لن يتم التراجع عن تغييرات المخطط). |
| الاستخدام النموذجي | انسخ البيانات احتياطيًا أو أصلح بيانات قد يفقدها الترحيل؛ ارفض ترقية خطِرة بإلقاء استثناء. | بذر بيانات افتراضية، تهيئة مساحة العمل، تسجيل موارد خارجية |
**قاعدة عامة:** اجعل الافتراضي هو post-install. الجأ إلى ما قبل التثبيت فقط عندما يكون الترحيل نفسه هدّامًا وتحتاج إلى التقاط الحالة السابقة قبل أن تزول.
| ترغب في... | استخدام |
| ---------------------------------------------------------------- | ------------------------------------------------------------------------- |
| بذر البيانات، تهيئة مساحة العمل، تسجيل موارد خارجية | `post-install` |
| عمل طويل الأمد لا ينبغي أن يحجب استجابة التثبيت | `post-install` (الوضع غير المتزامن الافتراضي، مع محاولات إعادة من العامل) |
| إعداد سريع يعتمد عليه المستدعي مباشرةً بعد عودة التثبيت | `post-install` مع `shouldRunSynchronously: true` |
| قراءة البيانات أو نسخها احتياطيًا والتي قد يفقدها الترحيل القادم | `pre-install` |
| رفض ترقية قد تُفسد البيانات الحالية | `pre-install` (ارمِ من المعالج) |
| تنفيذ مواءمة في كل ترقية | أي من الخطافين مع `shouldRunOnVersionUpgrade: true` |
## السلوك المشترك بين كلا الخطافين
* إعداد التهيئة هو إعداد `defineLogicFunction` نفسه مطروحًا منه إعدادات المشغّل، مضافًا إليه `shouldRunOnVersionUpgrade`.
* **موعد تشغيله**: في عمليات التثبيت الجديدة فقط، افتراضيًا. عيِّن `shouldRunOnVersionUpgrade: true` لتشغيله أيضًا عند الترقيات. استخدم `previousVersion` / `newVersion` للتفرع حسب مسار الترقية.
* **أهمية اللاّتغيّر (Idempotency)**: قد يُعاد تشغيل post-install غير المتزامن، وأيٌّ من الخطافين يُعاد تشغيله عند الترقيات عندما يكون `shouldRunOnVersionUpgrade` مفعّلًا.
* يتم حقن بيئة دوال المنطق المعتادة (`APPLICATION_ID`، و`APP_ACCESS_TOKEN`، و`API_URL`)، لذا يمكنك استدعاء Twenty API باستخدام رمز التطبيق الخاص بك.
* يُربَط الخطّاف تلقائيًا بملف بيان التطبيق وقت الإنشاء (`preInstallLogicFunction` / `postInstallLogicFunction`) — لا حاجة للإشارة إليه في [`defineApplication()`](/l/ar/developers/extend/apps/config/application).
* القيمة الافتراضية لـ `timeoutSeconds` هي 300 للسماح بمهام إعداد أطول مثل بذر البيانات.
* **غير منفَّذ في نمط التطوير**: يتخطى `yarn twenty dev` تدفق التثبيت ويزامن الملفات مباشرةً، لذا لا تعمل الخطافات هناك مطلقًا. بدلًا من ذلك، شغّلها يدويًا:
```bash filename="Terminal"
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
```
<AccordionGroup>
<Accordion title="definePostInstallLogicFunction" description="تعمل بعد تطبيق ترحيل البيانات الوصفية لمساحة العمل">
يعمل بعد انتهاء تثبيت تطبيقك: تمت مزامنة البيانات الوصفية، وتم إنشاء عميل SDK، وأصبح من الممكن الاستعلام عن المخطط الجديد. مثال — بذر سجل افتراضي في عمليات التثبيت الجديدة:
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = new CoreApiClient();
await client.mutation({
createPostCard: {
__args: { data: { name: 'Welcome to Postcard', content: 'Your first card!' } },
id: true,
},
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
تتحكم الشارة `shouldRunSynchronously` في نموذج التنفيذ:
* `false` *(الإعداد الافتراضي)* — يُوضَع في قائمة انتظار الرسائل (`retryLimit: 3`) ويُشغِّله عامل. تعود استجابة التثبيت بمجرد وضع المهمة في قائمة الانتظار. **يُستخدم للأعمال طويلة الأمد** — بذر مجموعات بيانات كبيرة، وواجهات برمجة تطبيقات بطيئة لأطراف ثالثة.
* `true` — يُنفَّذ مضمَّنًا أثناء تدفق التثبيت. يحجب طلب التثبيت حتى ينتهي المعالج؛ يظهر الخطأ الذي يتم رميه كـ `POST_INSTALL_ERROR` للمستدعي (بدون محاولات إعادة). **يُستخدم للأعمال السريعة التي يجب إتمامها قبل الاستجابة.** تم تطبيق الترحيل بالفعل في هذه المرحلة، لذا لا يؤدي الفشل إلى التراجع عن تغييرات المخطط — بل يُظهِر الخطأ فقط.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="تعمل قبل تطبيق ترحيل البيانات الوصفية لمساحة العمل">
يعمل قبل ترحيل البيانات الوصفية، مقابل المخطط **السابق** — المكان المناسب لنسخ البيانات احتياطيًا التي قد يفقدها الترحيل، أو لرفض ترقية خطِرة. قبل التنفيذ، يُشغِّل الخادم مزامنة ذات طابع إضافي فقط "pared-down sync" تُسجِّل دالة ما قبل التثبيت للإصدار الجديد فقط؛ أما كل ما عدا ذلك — كائنات الإصدار السابق وحقوله وبياناته — فيبقى دون مساس عندما يعمل المعالج.
ما قبل التثبيت دائمًا **متزامن** ويحجب عملية التثبيت. إذا رمى المعالج استثناءً، تُلغى عملية التثبيت قبل أي تغيير في المخطط — وتبقى مساحة العمل على الإصدار السابق بحالة متّسقة. هذا مقصود: ما قبل التثبيت هو فرصتك الأخيرة لرفض ترقية تنطوي على مخاطر.
مثال — نسخ قيم حقل قديم قبل أن يُسقِطه الترحيل:
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
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 = new CoreApiClient();
const { postCards } = await client.query({
postCards: {
__args: { filter: { notes: { isNot: null } } },
edges: { node: { id: true, notes: true } },
},
});
// Copy legacy `notes` into `description` before the migration drops the
// column. If this fails, the upgrade aborts and the workspace stays on v1.
for (const { node } of postCards.edges) {
await client.mutation({
updatePostCard: {
__args: { id: node.id, data: { description: node.notes } },
id: true,
},
});
}
};
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,
});
```
</Accordion>
</AccordionGroup>
## خطاف إلغاء التثبيت
تُصرّح `defineUninstallLogicFunction` عن خطاف يتم تشغيله عندما يلغي المستخدم تثبيت تطبيقك. يتم تنفيذه **قبل** إزالة بيانات التعريف للتطبيق وبياناته وكوده — بمجرد تشغيل ترحيل الحذف، لن يتبقى أي شيء للتنفيذ — لذلك لا يزال بإمكان معالجك الاستعلام عن كائنات التطبيق وسجلاته. استخدمه لتنظيف الموارد الخارجية: إلغاء توفير موارد واجهة برمجة التطبيقات (API)، وحذف الروبوتات المتبقية، وإبطال خطافات الويب.
الملاحظات:
* الخطاف يعتمد على مبدأ "أقصى جهد ممكن": يتم تشغيله بشكل متزامن، ولكن في حالة الفشل يتم تسجيل الخطأ و**لا يعرقل عملية إلغاء التثبيت مطلقًا** — يجب ألا يجعل التنظيف إزالة التطبيق مستحيلة.
* يتلقى `UninstallPayload` (`{ version?: string }` — الإصدار الذي تتم إزالته).
* لا يتم تشغيله عند التراجع عن عملية تثبيت جديدة فاشلة — حيث لم يكتمل تثبيت التطبيق مطلقًا.
* لا يمكن للخطاف أن يعمل بعد إزالة التطبيق، لذلك يجب أن يتم هنا أي تنظيف خارجي يعتمد على بيانات التطبيق (مثلًا: معرّفات الروبوتات المخزنة في السجلات)، وليس في مهمة مجدولة خارجية.
* ومثل خطافات التثبيت، **لا يتم تنفيذه في وضع التطوير (dev mode)** — بدلًا من ذلك، قم بتشغيله يدويًا:
```bash filename="Terminal"
yarn twenty dev:function:exec --uninstall
```
```ts src/logic-functions/uninstall.ts
import { defineUninstallLogicFunction, type UninstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (_payload: UninstallPayload): Promise<void> => {
const client = new CoreApiClient();
const { meetingBots } = await client.query({
meetingBots: { edges: { node: { id: true, externalBotId: true } } },
});
// Delete the provider-side bots so nothing keeps recording after uninstall.
for (const { node } of meetingBots.edges) {
await fetch(`https://api.recorder.example/bots/${node.externalBotId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.RECORDER_API_KEY}` },
});
}
};
export default defineUninstallLogicFunction({
universalIdentifier: 'b2c3d4e5-6789-01bc-def0-234567890abc',
name: 'uninstall',
description: 'Deletes remaining recorder bots when the app is uninstalled.',
timeoutSeconds: 300,
handler,
});
```

View File

@@ -0,0 +1,56 @@
---
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
└──────────────────────────────────┘
│
▼ (at uninstall time)
┌──────────────────────────────────┐
│ Uninstall hook │ before app removal
└──────────────────────────────────┘
```
## في هذا القسم
<CardGroup cols={2}>
<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` و`defineUninstallLogicFunction` — نسخ البيانات احتياطيًا، تهيئة القيم الافتراضية، التحقق من صحة الترقيات، والتنظيف عند الإزالة.
</Card>
</CardGroup>
## كيفية ترابط الأجزاء
* **التطبيق (Application)** هو نقطة الدخول. يحتوي كل تطبيق على استدعاء واحد فقط `defineApplication()`، ويشير إلى **دور (Role)** واحد باعتباره الدور الافتراضي له.
* يتحكم **الدور** في ما يمكن لوظائف منطق التطبيق ومكوّنات الواجهة الأمامية قراءته وكتابته. اتبع مبدأ أقل امتياز ممكن: امنح فقط الصلاحيات التي يحتاجها الكود فعليًا.
* تعمل **خطافات التثبيت** أثناء التثبيت أو الترقية — ما قبل التثبيت قبل ترحيل البيانات الوصفية (كي تتمكن من رفض ترقية محفوفة بالمخاطر)، وما بعد التثبيت بعد الترحيل (كي تتمكن من تهيئة بيانات افتراضية وفق المخطط الجديد). يعمل خطاف إزالة التثبيت مباشرة قبل إزالة التطبيق، بحيث يمكنه تنظيف الموارد الخارجية بينما تظل بيانات التطبيق قابلة للقراءة.
<Note>
تشارك خطافات التثبيت وقت تشغيل [وظيفة المنطق](/l/ar/developers/extend/apps/logic/logic-functions) — نفس توقيع المعالج (handler signature)، ونفس متغيرات البيئة، ونفس عميل واجهة برمجة التطبيقات (typed API client) — لكنها تُصرّح باستخدام دوال تعريف خاصة بها وتوجد خارج نموذج المشغلات العادي (HTTP، و cron، وأحداث قاعدة البيانات).
</Note>

View File

@@ -0,0 +1,67 @@
---
title: الأصول العامة
description: وزّع الملفات الثابتة — الصور والأيقونات والخطوط — مع تطبيقك عبر مجلد public/.
icon: folder-open
---
يحتوي مجلد `public/` في جذر تطبيقك على ملفات ثابتة — صور وأيقونات وخطوط وأي أصول أخرى يحتاجها تطبيقك وقت التشغيل. تُدرج هذه الملفات تلقائيًا في عمليات البناء، وتُزامَن أثناء وضع التطوير، وتُرفَع إلى الخادم.
الملفات الموضوعة في `public/` هي:
* **متاحة للعامة** — بمجرد مزامنتها إلى الخادم، تُقدَّم الأصول عبر عنوان URL عام. لا حاجة إلى مصادقة للوصول إليها.
* **متاحة في المكوّنات الأمامية** — استخدم عناوين الأصول لعرض الصور أو الأيقونات أو أي وسائط داخل مكوّنات React لديك.
* **متاحة في الدوال المنطقية** — أشِر إلى عناوين الأصول في رسائل البريد الإلكتروني أو استجابات واجهات البرمجة أو أي منطق على جهة الخادم.
* **مستخدمة لبيانات تعريف السوق** — يشير الحقلان `logo` و`galleryImages` في `defineApplication()` إلى ملفات من هذا المجلد (مثل `public/logo.png`). تُعرَض هذه عند نشر تطبيقك في السوق. يتم تجاهل عناوين URL المطلقة الخارجية لهذه الحقول — بدلاً من ذلك، قم بتجميع الصور في `public/`. يجب ألا يتجاوز حجم كل صورة 10 ميغابايت.
* **تُزامَن تلقائيًا في وضع التطوير** — عند إضافة ملف في `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/` تلقائيًا إن وُجدت.

View File

@@ -0,0 +1,201 @@
---
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],
});
```
## أمن على مستوى الصف
تحدّد أذونات الكائن والحقل *أي الكائنات والحقول* يمكن لدورٍ معيّن التفاعل معها. **محددات الأذونات على مستوى الصف** تتعمّق أكثر وتحدّد *أي السجلات* يمكن لدورٍ معيّن رؤيتها والتصرّف فيها — على سبيل المثال، دور للخدمة الذاتية حيث يرى كل مستخدم خارجي سجلاته الخاصة فقط.
عرِّف المحددات باستخدام `rowLevelPermissionPredicates` على الدور. مثل باقي ملف manifest، يحمل كل محدِّد `universalIdentifier` خاصًا به، ويشير إلى كائن وحقل باستخدام `universalIdentifier` الخاصّ بهما، و`operand`، وحقل workspaceMember (اختياري) تُحقن قيمته وقت الاستعلام — بحيث يمكنك التعبير عن: "علاقة مالك السجل **هي** عضو مساحة العمل الحالي":
```ts src/roles/partner-role.ts
import {
defineRole,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/account-owner.field';
export default defineRole({
universalIdentifier: 'c3c1dc2e-1a08-4de5-abb7-2139b3d99343',
label: 'Partner',
description: 'External partner — sees only its own records',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd0f0c1a2-3b4c-4d5e-8f60-111111111111',
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier: ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier,
},
],
});
```
نظرًا إلى أنّ المحددات (predicates) تُشحَن داخل ملف manifest، فإنّه يتم إنشاؤها وتحديثها وإزالتها مع الدور نفسه في كل عملية تثبيت وترقية — ولا توجد خطوة ما بعد التثبيت منفصلة تحتاج إلى مزامنتها.
### دمج المحددات مع المجموعات
افتراضيًا، يتم دمج محددات الدور باستخدام `AND`. لدمج بعضها باستخدام `OR` (أو لتداخل المنطق)، قم بتعريف إدخال `rowLevelPermissionPredicateGroups` وأشِر كل محدد إليه من خلال `predicateGroupUniversalIdentifier`. يُتيح هذا الدور للشريك رؤية فرصة (Opportunity) إذا كان إمّا **مالكها** أو **جهة الاتصال** لها:
```ts src/roles/partner-opportunities-role.ts
import {
defineRole,
RowLevelPermissionPredicateGroupLogicalOperator,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
const OPPORTUNITY = STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity;
const CURRENT_MEMBER =
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier;
export default defineRole({
universalIdentifier: 'b2a1c0d9-8e7f-4a6b-9c5d-222222222222',
label: 'Partner (opportunities)',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
canReadObjectRecords: true,
},
],
rowLevelPermissionPredicateGroups: [
{
universalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator.OR,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd4c3b2a1-0e9f-4c8b-9e7d-444444444444',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier: OPPORTUNITY.fields.owner.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
{
universalIdentifier: 'e5d4c3b2-1f0e-4d9c-8f8e-555555555555',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier:
OPPORTUNITY.fields.pointOfContact.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
],
});
```
الملاحظات:
* امنح كل محدد وكل مجموعة قيمة `universalIdentifier` ثابتة (أي uuid) — فهي المفتاح الذي يعرّف الكيان عبر الترقيات، كما أنّ المحددات تشير إلى المجموعات من خلاله.
* يمكن للمحددات الإشارة إلى الكائنات والحقول التي يملكها تطبيقك أو إلى كائنات Twenty القياسية.
* يتم تطبيق الأمان على مستوى الصف (row-level security) لمساحات العمل في الخطط التي تتضمنه؛ بينما تستمر مزامنة المحددات في الخطط الأخرى ولكن دون تطبيقها فعليًا.
## الدور الافتراضي للوظيفة
عند إنشاء هيكل لتطبيق جديد، ينشئ 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` تتحكم في الوصول إلى القدرات على مستوى المنصة. اجعلها في حدّها الأدنى.
* اطّلع على مثال عملي: [`examples/hello-world/src/roles/default-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/hello-world/src/roles/default-role.ts).

View File

@@ -0,0 +1,52 @@
---
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/`.
* يمكن أيضًا تعيين `writability` على مستوى الحقل: يقتصر السماح بالكتابة في حالة `MetadataWritability.APPLICATION` على دوال منطق تطبيقك نفسها، بينما يُخصَّص `MetadataWritability.SYSTEM` للبيانات المُدارة بواسطة المنصة. لا يمكن أن يكون مستوى قابلية الكتابة للحقل إلا أكثر تقييدًا من مستوى الكائن الذي ينتمي إليه، ولا تتأثر عمليات القراءة بذلك. راجع [Objects](/l/ar/developers/extend/apps/data/objects) للاطّلاع على الدلالات الكاملة.
* لإضافة علامة تبويب إلى تخطيط صفحة قياسي (مثل صفحة تفاصيل 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) للنمط ثنائي الاتجاه.

View File

@@ -0,0 +1,143 @@
---
title: كائنات
description: عرّف أنواعًا جديدة من السجلات — جداول مخصصة بحقولها الخاصة — باستخدام defineObject.
icon: table
---
تُعد **الكائنات** المخصصة أنواع سجلات جديدة يضيفها تطبيقك إلى مساحة العمل — مثل بطاقة بريدية، أو فاتورة، أو اشتراك، أو أي شيء خاص بالمجال الذي تعمل فيه. يعلن كل كائن عن مخططه (الحقول، والعلاقات، والقيم الافتراضية) ومعرّف عالمي ثابت يستمر عبر عمليات المزامنة والنشر.
```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` اختيارية — يمكنك تعريف كائنات بدون حقول مخصصة.
* `openRecordIn` يحدد أين تُفتح سجلات هذا الكائن عند النقر عليها: `ObjectOpenRecordIn.USER_CHOICE` (الإعداد الافتراضي، الذي يتبع تفضيل كل عضو في مساحة العمل من Settings → Experience)، أو `ObjectOpenRecordIn.SIDE_PANEL`، أو `ObjectOpenRecordIn.RECORD_PAGE`. ثبِّتها على `RECORD_PAGE` للسجلات التي تحتاج إلى صفحة كاملة لتكون قابلة للاستخدام، كما هو الحال في سير العمل ولوحات المعلومات، أو على `SIDE_PANEL` للسجلات التي لا تكون منطقية إلا كلوحة سريعة، كما هو الحال في أحداث التقويم.
* تتحكم الخاصية `writability` في من يمكنه كتابة سجلات الكائن من الأساس، قبل تطبيق أذونات الأدوار: `MetadataWritability.OPEN` (القيمة الافتراضية — أدوار مساحة العمل هي التي تقرر)، أو `MetadataWritability.APPLICATION` (يمكن لوظائف منطق تطبيقك فقط إنشاء السجلات أو تحديثها أو حذفها؛ استخدم هذا لكائنات تشبه التهيئة التي تمنح سجلاتها سلوكًا، بحيث لا يتمكن أعضاء مساحة العمل الذين لديهم وصول واسع إلى السجلات من تعديلها عبر واجهة برمجة التطبيقات API)، أو `MetadataWritability.SYSTEM` (محجوزة للبيانات التي تُدار من قبل المنصّة). لا تتأثر عمليات القراءة — يتم فرض هذا من جانب الخادم، على عكس `isUIEditable`، التي تقتصر على إخفاء عناصر واجهة المستخدم. توجد هذه الخاصية أيضًا على مستوى الحقل، حيث لا يمكن أن تكون إلا أكثر تقييدًا من مستوى الكائن.
* لا تحتاج الحقول المضمّنة المُعرَّفة هنا إلى `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>
## أنواع الحقول
مجموعة القيم الكاملة لـ`FieldType`، والمصدَّرة من `twenty-sdk/define`:
| الفئة | الأنواع |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| نص | `TEXT`، `RICH_TEXT`، `ARRAY` (من السلاسل النصية)، `RAW_JSON` |
| عددية | `NUMBER` (`universalSettings.dataType`: `'float'` / `'int'` / `'bigint'`)، `NUMERIC` (بدقة اعتباطية)، `RATING`، `POSITION` |
| التواريخ | `DATE`, `DATE_TIME` |
| اختيار | `BOOLEAN`، `SELECT`، `MULTI_SELECT` |
| مركّبة | `FULL_NAME`، `ADDRESS`، `EMAILS`، `PHONES`، `LINKS`، `CURRENCY`، `ACTOR`، `FILES` |
| المعرِّفات والعلاقات | `UUID`، `RELATION`، `MORPH_RELATION` (انظر [العلاقات](/l/ar/developers/extend/apps/data/relations)) |
| النظام | `TS_VECTOR` (متجه بحث نصي كامل، يُدار بواسطة الخادم) |
تُخزِّن الأنواع المركّبة عدّة حقول فرعية (مثلًا `FULL_NAME` = الاسم الأول + اسم العائلة؛ `CURRENCY` = `amountMicros` + `currencyCode`). `SELECT` و`MULTI_SELECT` يتطلّبان مصفوفة `options` كما في المثال أعلاه.
## القيم الافتراضية
يجب تضمين القيم النصية الافتراضية بين علامات اقتباس أحادية **داخل** السلسلة — `defaultValue: "'Draft'"`، وليس `defaultValue: "Draft"`. لهذا السبب يستخدم الحقل `status` أعلاه `` `'${PostCardStatus.DRAFT}'` ``.
السلاسل غير المحاطة بعلامات اقتباس محجوزة للقيم الافتراضية المحسوبة، والتي يتم تقييمها عند إنشاء سجل:
* `'uuid'` — يُنشِئ UUID (لحقول `UUID`)
* `'now'` — الطابع الزمني الحالي (لحقول `DATE_TIME`)
ينطبق نفس الاصطلاح على الحقول الفرعية النصية للقيم الافتراضية المركّبة (على سبيل المثال `{ source: "'MANUAL'" }` في حقل `ACTOR`) وكذلك على قيم `SELECT`/`MULTI_SELECT`. سيتسبّب ترك قيمة افتراضية نصية حرفية بدون علامات اقتباس في ظهور تحذير عند إنشاء التطبيق.
## قابلية قبول NULL
`isNullable` تتحكم في ما إذا كان الحقل يقبل `NULL`. القيمة الافتراضية هي `true` — يمكنك حذفها من الحقول الاختيارية. عيِّن `isNullable: false` لجعل الحقل مطلوبًا على مستوى قاعدة البيانات.
يتم تطبيق التغييرات على `isNullable` في كل عملية مزامنة، بما في ذلك عمليات المزامنة التي تحدّث حقلًا موجودًا — وبالتالي يمكنك تبديل قابلية الحقل لقبول NULL عن طريق تعديل ملف manifest وإعادة المزامنة.
<Note>
**جعل حقل موجود غير قابل لأن يكون فارغًا يتطلب قيمة افتراضية.** عندما تغيّر حقلًا إلى `isNullable: false`، يجب أيضًا توفير `defaultValue` غير فارغ. القيمة الافتراضية تملأ أي صفوف موجودة تحتوي على `NULL` قبل تطبيق القيد `NOT NULL`؛ من دونها تفشل المزامنة مع الخطأ `Default value cannot be null for non-nullable fields`. حقول العلاقات وحقول `TS_VECTOR` تكون دائمًا قابلة لأن تكون فارغة، لذلك لا يكون لـ `isNullable` أي تأثير عليها.
</Note>
```ts
{
universalIdentifier: 'b1a7c0de-1234-4f00-9abc-000000000000',
name: 'reference',
type: FieldType.TEXT,
label: 'Reference',
isNullable: false,
defaultValue: "'N/A'",
}
```
## ماذا بعد؟
* **اربط هذا الكائن بغيره من الكائنات** — راجع صفحة [Relations](/l/ar/developers/extend/apps/data/relations) لمعرفة نمط العلاقة ثنائية الاتجاه.
* **أضف حقولًا إلى الكائنات التابعة لتطبيقات أخرى** — راجع [Extending Objects](/l/ar/developers/extend/apps/data/extending-objects) لمعرفة المزيد حول `defineField()`.
* **اعرض هذا الكائن في واجهة المستخدم (UI)** — راجع [عناصر قائمة التنقل](/l/ar/developers/extend/apps/layout/navigation-menu-items) لإضافة عنصر في الشريط الجانبي؛ وراجع [طرق العرض](/l/ar/developers/extend/apps/layout/views) لإضافة تكوينات قوائم مخصصة.

View File

@@ -0,0 +1,101 @@
---
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={2}>
<Card title="كائنات" icon="table" 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>
<Card title="أنواع أنشطة الجدول الزمني" icon="clock-rotate-left" href="/l/ar/developers/extend/apps/data/timeline-activity-types">
حدّد أحداث التدقيق التلقائية وأحداث التطبيق المحددة صراحةً للجداول الزمنية للسجلات.
</Card>
</CardGroup>
## الكيانات بنظرة سريعة
| كيان | الغرض | مُعرَّفة باستخدام |
| -------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------- |
| **الكائن** | نوع سجل مخصص جديد (مثل PostCard أو Invoice) مع حقوله الخاصة | `defineObject()` |
| **الحقل** | عمود على كائن. يمكن للحقول المستقلة توسيع الكائنات التي لم تنشئها (مثل إضافة `loyaltyTier` إلى Company) | `defineField()` |
| **علاقة** | ارتباط ثنائي الاتجاه بين كائنين — يُصرّح عن كلا الجانبين كحقول | `defineField()` مع `FieldType.RELATION` |
| **الفهرس** | فهرس قاعدة بيانات لتسريع استعلام متكرر على أحد الكائنات لديك | `defineIndex()` |
| **نوع نشاط الجدول الزمني** | حدث تدقيق تلقائي أو حدث مجال صريح يظهر في المخططات الزمنية للسجلات. | `defineTimelineActivityType()` |
يكتشف SDK هذه العناصر عبر تحليل AST أثناء وقت الإنشاء، لذا تنظيم الملفات يعود إليك — الاصطلاح هو `src/objects/` و `src/fields/` و `src/indexes/` و `src/timeline-activity-types/`. معرّفات 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>

View File

@@ -0,0 +1,184 @@
---
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>
## علاقات الربط
يربط كائن الربط سجل مصدر واحدًا بسجل هدف واحد أو أكثر. على سبيل المثال، يمكن أن يحتوي `PostCardRecipient` على كلٍّ من علاقة `postCard` وعلاقة `recipient`. في حقل `ONE_TO_MANY` الخاص بالكائن المصدر، عيّن `junctionTargetFieldUniversalIdentifier` إلى حقل العلاقة في كائن الربط الذي يشير إلى الهدف:
```ts
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
relationTargetObjectMetadataUniversalIdentifier:
POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
junctionTargetFieldUniversalIdentifier: RECIPIENT_FIELD_ID,
},
});
```
يجب أن يحدد `RECIPIENT_FIELD_ID` علاقة `MANY_TO_ONE` أو علاقة morph في كائن الربط. يخبر Twenty بالسجل المرتبط الذي يمثل الهدف عندما تعبر ميزة مثل [توزيع نشاط المخطط الزمني](/l/ar/developers/extend/apps/data/timeline-activity-types#automatic-and-explicit-events) كائن الربط.
## الربط مع الكائنات القياسية
لإنشاء علاقة مع كائن 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`) |
| `universalSettings.junctionTargetFieldUniversalIdentifier` | `ONE_TO_MANY` للربط فقط | حقل العلاقة في كائن الربط الذي يشير إلى هدف التوزيع |
## حقول العلاقات المضمَّنة
يمكنك أيضًا إعلان علاقة مباشرة داخل [`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
],
});
```

View File

@@ -0,0 +1,207 @@
---
title: استهداف البيانات الوصفية النظامية
description: حلّ المعرّفات العالمية الحتمية للبيانات الوصفية التي يوفّرها Twenty تلقائيًا على كل كائن، بحيث يمكن لتطبيقك الرجوع إليها من دون ترميز ثابت.
icon: gears
---
كل كائن في Twenty يأتي مع **بيانات وصفية نظامية** لا تقوم أنت بتعريفها، مثل مجموعة من الحقول وعرض القائمة الرئيسي مع أعمدته. يقوم الخادم بإنشاء كل ذلك عند توفير الكائن، وتنمو هذه المجموعة مع نمو Twenty.
نظرًا لأنك لا تعرّفها، فلا يوجد ثابت `universalIdentifier` لتقوم باستيراده. بدلًا من ذلك، يقوم الخادم **باستخلاص** كل معرّف بشكل حتمي، ويعرض `twenty-sdk` نفس طريقة الاستخلاص بحيث يمكن لملف البيان (manifest) لديك حلّ القيمة الدقيقة التي يستخدمها الخادم.
## حقول النظام
الحقول البسيطة (scalar) الموجودة على كل كائن، والتي لا تعرّف أيًا منها باستخدام [`defineField()`](/l/ar/developers/extend/apps/data/extending-objects):
`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`
فكيف تُشير إلى `createdAt` كعمود في [عرض](/l/ar/developers/extend/apps/layout/views)؟
### المشكلة
منذ Twenty 2.19، يتم اشتقاق المعرّف الشامل للحقل النظامي **بشكل حتمي** بواسطة الخادم من ثلاثة مُدخلات: المعرّف الشامل للتطبيق، والمعرّف الشامل للكائن، واسم الحقل. ابتكار معرّف (id) وتضمينه بشكل ثابت لن ينجح: لن يطابق أي شيء على الخادم، وسترفض المزامنة المرجع المعلّق:
```
Dev sync failed: viewField: INVALID_VIEW_DATA: Field metadata not found
```
### الحل
<Note>
يتوفر `getFieldUniversalIdentifier` في `twenty-sdk` بدءًا من الإصدار 2.21 فصاعدًا.
</Note>
استخدم `getFieldUniversalIdentifier` لاشتقاق نفس القيمة تمامًا التي يستخدمها الخادم. يأخذ المُدخلات الثلاثة ويُرجِع المعرّف الشامل للحقل:
```ts
import { getFieldUniversalIdentifier } from 'twenty-sdk/define';
const createdAtFieldId = getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
});
```
* `applicationUniversalIdentifier` هو معرّف تطبيقك، وهو نفسه الذي تمرّره إلى [`defineApplication()`](/l/ar/developers/extend/apps/config/application).
* `objectUniversalIdentifier` هو معرّف الكائن الذي ينتمي إليه الحقل.
* `name` هو اسم الحقل النظامي، وهو إحدى القيم المُدرجة أعلاه.
### مثال: عمود `createdAt` في عرض
الحالة النموذجية هي إضافة عمود `createdAt` إلى عرض لأحد الكائنات المخصّصة لديك. اشتقّ معرّف الحقل واستدعِه كما تستدعي أي `fieldMetadataUniversalIdentifier` آخر:
```ts src/views/example-view.ts
import {
defineView,
getFieldUniversalIdentifier,
} from 'twenty-sdk/define';
const APPLICATION_UNIVERSAL_IDENTIFIER =
'0b04e15c-27b2-4741-9046-b32e07469072';
const MY_OBJECT_UNIVERSAL_IDENTIFIER =
'c782b61c-70fd-4c88-9cd6-4e61ab8d7591';
export default defineView({
universalIdentifier: '70f10d44-144a-4da8-8c6f-3ec2422138c0',
name: 'All records',
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
fields: [
{
universalIdentifier: '75a90bc4-d901-4df4-85e0-af29db5e0104',
fieldMetadataUniversalIdentifier: getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
}),
position: 0,
isVisible: true,
size: 200,
},
],
});
```
المعرّف المُشتقّ نفسه يعمل في أي مكان يُتوقَّع فيه `fieldMetadataUniversalIdentifier`: حقول العرض، عوامل التصفية، أوامر الفرز، المجموعات، وعناصر واجهة تخطيط الصفحة.
<Note>
اشتقّ المعرّف، ولا تقُم بتضمينه بشكل ثابت. نظرًا لأن الخادم يشتق القيمة من معرّف التطبيق، ومعرّف الكائن، واسم الحقل، فإن استدعاء
`getFieldUniversalIdentifier` يُبقي مرجعك صحيحًا حتى إذا تغيّرت تلك المُدخلات، ويتجنّب الانحراف إذا تطوّر أسلوب الاشتقاق في أي وقت.
</Note>
### حقول العلاقات الخاصة بالنظام
<Note>
`getSystemRelationFieldUniversalIdentifier` متوفر في `twenty-sdk`
بدءًا من الإصدار 2.23، ويتطلب خادم Twenty بالإصدار 2.23 أو أحدث.
</Note>
بالإضافة إلى حقول النظام البدائية المذكورة أعلاه، يوفِّر الخادم أيضًا أربعة **حقول علاقات للنظام** على كل كائن: `timelineActivities` و`attachments` و`noteTargets` و`taskTargets`، يشير كلٌّ منها إلى كائن العلاقة القياسي المطابِق.
لا يتم حلّ هذه الحقول باستخدام `getFieldUniversalIdentifier`، إذ يتم اشتقاق معرّفها **بدون الاعتماد على الاسم**، من الكائن المستضيف للحقل والكائن الذي يشير إليه الحقل. وبهذه الطريقة، فإن إعادة تسمية كائن ما لا تغيِّر مطلقًا معرّفات حقول العلاقات الخاصة به.
استخدم `getSystemRelationFieldUniversalIdentifier` لحلّها:
```ts
import {
getSystemRelationFieldUniversalIdentifier,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
// rocket.attachments — the relation field hosted on your custom object
const rocketAttachmentsFieldId = getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetObjectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
});
```
* `objectUniversalIdentifier` هو الكائن الذي **يستضيف** الحقل.
* `relationTargetObjectUniversalIdentifier` هو الكائن الذي **يشير إليه** الحقل.
يتم ترميز الاتجاه من خلال ترتيب الوسيطات. لحلّ الجانب العكسي (مثلًا `attachment.targetRocket`، حقل morph الذي يُنشِئه الخادم على كائن العلاقة القياسي)، قم بمبادلة الاثنين:
```ts
// attachment.targetRocket — the reverse morph field on Attachment
const attachmentTargetRocketFieldId =
getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
relationTargetObjectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
});
```
كما هو الحال مع حقول النظام القياسية (scalar)، يعمل المعرّف الناتج في أي موضع يُتوقَّع فيه `fieldMetadataUniversalIdentifier`.
## العروض النظامية
<Note>
إن `getSystemViewUniversalIdentifier` و`getSystemViewFieldUniversalIdentifier`
متوفران في `twenty-sdk` بدءًا من الإصدار 2.26، ويتطلبان خادم Twenty
بالإصدار 2.26 أو أحدث.
</Note>
يقوم الخادم أيضًا بتوفير **عرض نظامي** على كل كائن: عرض القائمة الرئيسي (`All {objectLabelPlural}`، ذو المفتاح `ViewKey.INDEX` (المُشتق باستخدام `SYSTEM_VIEW_KEYS.INDEX`))، مع عمود واحد لكل حقل قابل للعرض. تمامًا مثل حقول العلاقات النظامية، تُشتق معرّفاتها **من دون أسماء**، لذلك لن يؤدي تغيير اسم كائن أو حقل إلى تغييرها مطلقًا.
استخدم `getSystemViewUniversalIdentifier` لحلّ العرض:
```ts
import {
getSystemViewUniversalIdentifier,
SYSTEM_VIEW_KEYS,
} from 'twenty-sdk/define';
const rocketIndexViewId = getSystemViewUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
viewKey: SYSTEM_VIEW_KEYS.INDEX,
});
```
* `objectMetadataApplicationUniversalIdentifier` هو التطبيق الذي يملك **الكائن**، وهو ما يتم بناء نطاق الاسم (namespace) للعرض على أساسه.
* `objectUniversalIdentifier` هو الكائن الذي يعرضه (يسردُه) هذا العرض.
* `viewKey` هو مميِّز عرض النظام: `SYSTEM_VIEW_KEYS.INDEX` لعرض القائمة الرئيسي، و`SYSTEM_VIEW_KEYS.FIELDS_WIDGET` لعرض أداة حقول صفحة السجل. إنه يحدِّد الاشتقاق؛ يُخزَّن أيضًا في صف العرض `INDEX` فقط.
يعمل المعرّف المحلول في أي مكان يُتوقَّع فيه `viewUniversalIdentifier`، مثل إدخال الشريط الجانبي من النوع [`NavigationMenuItemType.VIEW`](/l/ar/developers/extend/apps/layout/navigation-menu-items). لفتح القائمة الرئيسية لكائن ما ببساطة، يُفضَّل استخدام `NavigationMenuItemType.OBJECT` مع `targetObjectUniversalIdentifier`، إذ لا يحتاج ذلك إلى أي اشتقاق.
يقوم `getSystemViewFieldUniversalIdentifier` بحلّ **عمود** واحد في عرض نظامي، انطلاقًا من العرض والحقل الذي يعرضه هذا العمود:
```ts
import { getSystemViewFieldUniversalIdentifier } from 'twenty-sdk/define';
const rocketNameColumnId = getSystemViewFieldUniversalIdentifier({
fieldMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: rocketIndexViewId,
fieldMetadataUniversalIdentifier: ROCKET_NAME_FIELD_UNIVERSAL_IDENTIFIER,
});
```
انتبه إلى الوسيطة الأولى: يتم تحديد نطاق اسم العمود بواسطة التطبيق الذي يملك **الحقل الذي يعرضه**، وليس التطبيق الذي يملك العرض. الحقل الذي يضيفه تطبيقك إلى كائن قياسي يحصل على عموده المشتق ضمن نطاق تطبيقك، على عرض مملوك من قِبَل Twenty.
<Warning>
العروض النظامية وأعمدتها **مملوكة للخادم**: استخدم حلّ معرّفاتها للرجوع إليها، وليس لتعريفها. إن الخاصية `key` في
[`defineView()`](/l/ar/developers/extend/apps/layout/views) مهملة (deprecated) ويتم تجاهلها، لذلك لا يمكن لعرض مُعرَّف في ملف البيان المطالبة بالمفتاح `INDEX` مطلقًا، كما أن الخادم يوفّر بالفعل عمودًا لكل حقل تضيفه، لذا فإن تعريف `defineViewField()` خاصّتك لنفس الحقل على عرض نظامي يتعارض معه.
</Warning>
## الكائنات القياسية في Twenty
بالنسبة إلى كائن Twenty **قياسي** (Person، Company، Opportunity، …)، لا تحتاج إلى اشتقاق أي شيء: فمعرّفات الحقول والعروض النظامية هي ثوابت مُسبقة الحساب يمكنك استيرادها مباشرة.
```ts
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.createdAt.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.updatedAt.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.views.allPeople.universalIdentifier
```
الجأ إلى أدوات المساعدة المذكورة أعلاه عندما يكون الكائن واحدًا مما يعرّفه **تطبيقك** باستخدام [`defineObject()`](/l/ar/developers/extend/apps/data/objects)، حيث لا يوجد مثل هذا الثابت.
<Note>
`name` حقل **افتراضي**، وليس حقلًا نظاميًا. يحتفظ بمعرّفه الشامل الثابت الخاص به ولا يتم اشتقاقه عبر
`getFieldUniversalIdentifier`. في الكائنات التي تعرّفها، استدعِ حقل
`name` باستخدام المعرّف الذي منحته له في `defineObject()`.
</Note>

View File

@@ -0,0 +1,184 @@
---
title: أنواع أنشطة الجدول الزمنيّية العربية؟
description: حدّد أحداث التدقيق التلقائية وأحداث التطبيق الصريحة التي تظهر في الجداول الزمنية للسجلات.
icon: clock-rotate-left
---
يحدّد نوع نشاط الجدول الزمني مفردات ثابتة لحدث معروض في الجدول الزمني لسجل. تستخدم الكائنات القياسية وكائنات التطبيق العقد نفسه: يحتوي النوع على تسمية وأيقونة، ويمكنه اختياريًا الإعلان عن وقت إصداره، ويمكنه اختياريًا العرض عبر أحد [المكوّنات الأمامية](/l/ar/developers/extend/apps/layout/front-components) لتطبيقك.
<Note>
أنواع أنشطة الجدول الزمني في الإصدار التجريبي وستتوفر قريبًا في Twenty 2.34. يمكن أن تتطور
API بينما نتعلم من حالات استخدام مطوري التطبيقات.
</Note>
أنشئ واحدًا باستخدام أداة التوليد:
```bash filename="Terminal"
yarn twenty dev:add timelineActivityType
```
أو عرّفه مباشرةً:
```ts filename="src/timeline-activity-types/post-card-created.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7500',
name: 'postCardCreated',
label: 'created a post card',
icon: 'IconMail',
emit: {
on: 'created',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
},
});
```
## الأحداث التلقائية والصريحة
أضف `emit` عندما ينبغي أن ينشئ Twenty هذا النوع تلقائيًا. يدعم `emit.on` القيم `created` و`updated` و`deleted` و`restored` و`linked` و`unlinked`، بينما يحدد `emit.objectUniversalIdentifier` الكائن المصدر. يمكن لنوع فعّال واحد فقط معالجة مفتاح الإصدار نفسه — `on` والكائن وعلاقة المرور الاختيارية — في مساحة عمل.
من دون `emit.through`، يُكتب الحدث في الجدول الزمني الخاص بالسجل المصدر نفسه. لتغذيته على السجلات ذات الصلة، قم بتعيين `emit.through'. elationFieldUniversalIdentifier` إما إلى علاقة مباشرة متعددة إلى واحدة أو إلى [علاقة تقاطع واحدة إلى كثيرة](/l/ar/developers/extend/apps/data/relations#junction-relations) على الكائن المصدر. وتغذي علاقات مورف كل عضو من أعضاء مجموعتهم الموروفية، بحيث يمكن لإعلان واحد أن يستهدف عدة أنواع من الأجسام.
بالنسبة للعلاقة المباشرة، إنشاء أو استعادة السجل المصدر ينتج 'مرتبط`، حذفه ينتج 'غير مرتبط`، وإعادة توجيه العلاقة ينتج 'غير مرتبط` على الهدف السابق زائدا 'مرتبط` على الهدف الجديد. تحديثات المصدر الأخرى لا تنتج أحداث الرابط. وهذا هو العقد الذي تستخدمه المرفقات، التي يتمثل هدفها في علاقة مباشرة بين الطرفين.
فيما يتعلق بالعلاقة بين الطرفين، يحدد `universalSettings.junctionTargetFieldUniversalIdentifier` العلاقة من جسم التقاطع إلى الهدف. إنشاء أو حذف صف مفترق ينتج "مرتبط" أو "غير مرتبط". كما أن نقل أي من جانبي صف مفترق الحدود يؤدي إلى حدث رابط؛ كما أن التحديثات إلى حقول التقاطع غير ذات الصلة ليست كذلك.
تتطلب الأحداث `المرتبطة` و `غير المرتبطة` `emit.through`، لأن تشغيلها هو تغيير إلى العلاقة المباشرة أو الواجهة التي تم تكوينها.
على سبيل المثال، هذا هو العقد العام نفسه الذي تستخدمه الملاحظات والمهام والرسائل وأحداث التقويم:
```ts filename="src/timeline-activity-types/post-card-linked.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
import { POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/post-card-recipients-on-post-card.field';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7502',
name: 'postCardLinked',
label: 'received a post card',
icon: 'IconMail',
emit: {
on: 'linked',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
through: {
relationFieldUniversalIdentifier:
POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER,
},
},
});
```
يُصدر حدث مرور `updated` لكل تحديث للمصدر افتراضيًا. عيّن `emit.through.triggerFieldUniversalIdentifiers` عندما يجب أن تظهر تغييرات حقول المصدر المحددة فقط في الجداول الزمنية للهدف.
احذف `emit` لحدث نطاق صريح تنشئه دالة المنطق الخاصة بك بنفسها. يمنع هذا إنشاء صف تدقيق تلقائي وصف صريح معًا للعملية نفسها.
```ts filename="src/timeline-activity-types/post-card-sent.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
name: 'postCardSent',
label: 'sent a post card',
icon: 'IconSend',
});
```
أنشئ الأحداث الصريحة باستخدام [`createTimelineActivity()`](/l/ar/developers/extend/apps/logic/logic-functions#create-a-timeline-activity). يستخدم رمز التطبيق معرّفات عامة ثابتة؛ ويحوّل Twenty معرّفات بيانات التعريف الخاصة بالتثبيت.
## عرض مخصص
من دون مكوّن أمامي، يعرض Twenty صفًا عامًا أصليًا استنادًا إلى تسمية النوع وأيقونته وبيانات تعريف الكائن المرتبط. يناسب هذا الكائنات القياسية والمخصصة من دون عارض خاص بالكائن.
للحصول على تفاصيل مخصصة، عيّن `frontComponentUniversalIdentifier` إلى مكوّن أمامي يملكه التطبيق نفسه:
```ts
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
name: 'postCardSent',
label: 'sent a post card',
icon: 'IconSend',
frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```
يبقى الصف الأصلي هو العرض المطوي. لا يركّب Twenty المكوّن الأمامي إلا بعد أن يوسّع المستخدم ذلك الصف، متجنبًا بيئة معزولة وعاملًا لكل حدث ظاهر. داخل المكوّن، استدعِ `useTimelineActivityId()` من `twenty-sdk/front-component` لقراءة معرّف الصف وجلب أي بيانات يحتاجها العرض الخاص بك. يعيد `null` عندما يُعرض المكوّن خارج صف جدول زمني.
## تجاوز عرض تطبيق آخر
يمتلك التطبيق عقود الجدول الزمني التلقائية لكائناته الخاصة. لتخصيص حدث على كائن يملكه تطبيق آخر، عرّف النوع الموجود صراحةً باستخدام `replacesTimelineActivityTypeUniversalIdentifier`:
```ts
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7503',
name: 'companyCreatedWithDeploymentContext',
label: 'was created from a deployment',
emit: {
on: 'created',
objectUniversalIdentifier: COMPANY_UNIVERSAL_IDENTIFIER,
},
replacesTimelineActivityTypeUniversalIdentifier:
RECORD_CREATED_TIMELINE_ACTIVITY_TYPE_UNIVERSAL_IDENTIFIER,
frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```
يجب أن يتضمن التجاوز `emit`، لأنه يستبدل موضع إصدار تلقائي موجودًا بدلًا من نوع صريح فقط. يجب أن ينتمي النوع المشار إليه إلى تطبيق الكائن الهدف وأن يصف الإجراء والمسار نفسيهما. تؤدي إزالة التطبيق المتجاوز إلى استعادة النوع الأساسي؛ وتؤدي إزالة النوع الأساسي إلى تعطيل التجاوز.
## تجاوزات مساحة العمل والكتم
يمكن لمسؤولي مساحة العمل تغيير عرض نوع أو كتم أحداثه التلقائية والصريحة من دون تشعيب التطبيق. استخدم عملية التحويل `updateTimelineActivityType` الخاصة بـ API بيانات التعريف مع معرّف النوع الخاص بالتثبيت:
```graphql
mutation CustomizeTimelineActivityType(
$id: UUID!
$label: String
$icon: String
$isActive: Boolean
) {
updateTimelineActivityType(
input: { id: $id, label: $label, icon: $icon, isActive: $isActive }
) {
id
label
icon
isActive
}
}
```
يُخزَّن `label` و`icon` كتجاوزات لمساحة العمل، لذلك لا تستبدل تحديثات التطبيق اللاحقة خيارات المسؤول. عيّن `isActive` إلى `false` لإيقاف الإصدار التلقائي ورفض الأحداث الصريحة الجديدة من ذلك النوع. تبقى الصفوف الحالية مرئية.
استعِد الإعدادات الافتراضية للتطبيق، بما في ذلك الحالة النشطة، باستخدام:
```graphql
mutation ResetTimelineActivityType($id: UUID!) {
resetTimelineActivityType(id: $id) {
id
label
icon
isActive
}
}
```
## حقول التكوين
| الحقل | مطلوب | الوصف |
| -------------------------------------------------- | ------- | ----------------------------------------------------------------------- |
| `universalIdentifier` | نعم | UUID ثابت للنوع عبر عمليات التثبيت والترقيات |
| `name` | نعم | اسم برمجي محلي للتطبيق؛ يجب أن يكون فريدًا داخل التطبيق |
| `label` | نعم | نص إجراء موجّه للمستخدم يستخدمه العارض الأصلي |
| `icon` | لا | اسم أيقونة Twenty المعروض بجانب الحدث |
| `emit` | لا | تعريف الإصدار التلقائي؛ احذفه للأنواع الصريحة فقط |
| `emit.on` | مع emit | إجراء تدقيق يؤدي إلى كتابة هذا النوع |
| `emit.objectUniversalIdentifier` | مع emit | الكائن الذي تصدر سجلاته هذا النوع |
| `emit.through.relationFieldUniversalIdentifier` | لا | العلاقة المباشرة أو المباشرة المستخدمة لتأجيج الجداول الزمنية ذات الصلة |
| `emit.through.triggerFieldUniversalIdentifiers` | لا | حقول المصدر التي يمكنها تشغيل حدث مرور `updated` |
| `frontComponentUniversalIdentifier` | لا | العنصر الأمامي المملوك للتطبيق مثبت عند توسيع الصف |
| `replaceesTimelineActivityTypeUniversalIdentifier` | لا | استبدال نوع الانبعاث الحالي؛ يتطلب `الانبعاث` |
20 لقطة دلالية وهوية وعشرين وردود عرض متينة عندما يتم إنشاء حدث. صفوف تاريخية تحافظ على عملها الأصلي ومعنى الكائن. في الوقت الذي يتم فيه تثبيت هذا النوع، فإن التسمية الحالية المترجمة، أيقونة ومكوناتها الأمامية تصبح مباشرة؛ وبعد إلغاء التثبيت، تظل اللقطة الاحتياطية مقروءة على الصف. يستخدم الحل المعرف العام، لذلك العرض التقديمي ينجو أيضا من إعادة تثبيت التطبيق.

View File

@@ -0,0 +1,102 @@
---
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) |
| **التبعيات المشتركة** | التبعيات المشتركة بين مكوّنات الواجهة الأمامية للتطبيق، والمُعرَّفة في package.json | [التبعيات المشتركة](/l/ar/developers/extend/apps/layout/front-components#sharing-dependencies-across-front-components) |
| **عنصر قائمة الأوامر** | إجراءات سريعة ومدخلات Cmd+K | [عناصر قائمة الأوامر](/l/ar/developers/extend/apps/layout/command-menu-items) |
## العزل
* **الدوال المنطقية** تعمل في عمليات Node.js معزولة على الخادم. لا تصل إلى البيانات إلا من خلال عميل API مكتوب الأنواع، الذي يعمل إما بصفة الشخص الذي بدأ التشغيل أو بصفة التطبيق نفسه. راجع [صلاحيات الوصول التي تستخدمها الاستدعاءات](/l/ar/developers/extend/apps/logic/logic-functions#whose-access-a-call-uses).
* **المكوّنات الأمامية** تعمل ضمن Web Workers باستخدام Remote DOM — معزولة عن الصفحة الرئيسية لكنها تعرض عناصر DOM الأصلية (وليس iframes). تتواصل مع Twenty عبر واجهة API للمضيف تعتمد تمرير الرسائل.
* **الأذونات** تُطبَّق على مستوى واجهة API. يتم إدخال رمزي تشغيل، وكلاهما مشتق من الدور المُعلن باستخدام `defineApplicationRole()`؛ ويُقيَّد الرمز الافتراضي بشكل أكبر بما يستطيع الشخص الذي بدأ التشغيل القيام به كلما كان هناك شخص وراء التشغيل.
## دورة حياة التطبيق
```
┌─────────────────────────────────────────────────────────┐
│ 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] → … → [uninstall] │
├─────────────────────────────────────────────────────────┤
│ 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>

View File

@@ -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>

View File

@@ -0,0 +1,79 @@
---
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
front-components/
main-page.tsx # Welcome page component
navigation-menu-items/
main-page.navigation-menu-item.ts # Sidebar entry for the welcome page
page-layouts/
main-page.page-layout.ts # Standalone page hosting the component
__tests__/
application-config.test.ts # Unit test
global-setup.ts # Integration test setup (sync + uninstall)
schema.integration-test.ts # Integration test against a live server
.github/workflows/
ci.yml # Lint, typecheck, unit + integration tests
cd.yml # Deploy + install on push to main
publish.yml # Publish to npm on version tags (with provenance)
public/
logo.svg # Static assets
vitest.config.ts # Integration test runner config
vitest.unit.config.ts # Unit test runner config
tsconfig.json, tsconfig.spec.json
.nvmrc, .yarnrc.yml, .oxlintrc.json
README.md, AGENTS.md, CLAUDE.md, CHANGELOG.md, SETUP.md
```
## الملفات الرئيسية
| ملف / مجلد | الغرض |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `src/application-config.ts` | **مطلوب.** ملف الإعداد الرئيسي لتطبيقك. |
| `src/default-role.ts` | دور افتراضي يتحكّم بما يمكن لدوال المنطق الوصول إليه. |
| `src/constants/universal-identifiers.ts` | معرّفات UUID وبيانات تعريف يتم توليدها تلقائيًا (اسم العرض، الوصف). |
| `src/front-components/`, `src/navigation-menu-items/`, `src/page-layouts/` | صفحة ترحيب مبدئية: مكوّن واجهة أمامية يتم تقديمه بواسطة مخطط صفحة مستقل، يمكن الوصول إليه من الشريط الجانبي. |
| `src/__tests__/` | اختبار وحدة بالإضافة إلى اختبار تكامل (مع إعداده العام) يقوم بمزامنة التطبيق مع خادم حقيقي. |
| `public/` | أصول ثابتة (صور، خطوط) تُقدَّم مع تطبيقك. |
| `AGENTS.md` / `CLAUDE.md` | إرشادات لوكلاء برمجة الذكاء الاصطناعي الذين يعملون على التطبيق. |
| `CHANGELOG.md` / `SETUP.md` | سجل بأبرز التغييرات وتعليمات الإعداد للتطوير المحلي. |
<Note>
**تنظيم الملفات متروك لك.** المجلدات المذكورة أعلاه هي أعراف متَّبعة — يكتشف SDK الكيانات عبر تحليل AST على استدعاءات `export default defineEntity(...)` بغض النظر عن مكان وجود الملف.
</Note>
## التبعيات
ينبغي أن تكون حزمتا SDK الخاصتان بـ Twenty ضمن `devDependencies`، وليس ضمن `dependencies`:
```json filename="package.json"
{
"dependencies": {},
"devDependencies": {
"twenty-client-sdk": "2.20.0",
"twenty-sdk": "2.20.0",
"twenty-ui": "1.0.0-alpha.1"
}
}
```
تقوم أداة إنشاء الهيكل بتثبيت الحزمتين `twenty-sdk` و `twenty-client-sdk` على إصدارها الخاص — حافظ على تزامنهما عند الترقية.
* توفّر **`twenty-sdk`** أداة `twenty` CLI وأدوات البناء/إنشاء الهياكل (scaffolding). يعمل فقط أثناء التطوير ووقت البناء، ولا يتم استيراده أبدًا في وقت تشغيل تطبيقك المنشور.
* يتم استيراد **`twenty-client-sdk`** بواسطة كود تطبيقك (`CoreApiClient`، `MetadataApiClient`، `RestApiClient`)؛ لكن Twenty توفّره في وقت التشغيل — حيث تحصل عليه دوال المنطق من طبقة SDK مُولَّدة، وتحصل عليه مكوّنات الواجهة من وحدات يتم تقديمها من الخادم. يُستخدَم الإصدار المثبّت لديك فقط لفحص الأنواع (typechecking) ولبناء النشر (deploy-time build)، لذا لا يلزم أبدًا أن يتم تضمينه في حزمة النشر.
الاحتفاظ بأي من الحزمتين ضمن `dependencies` يؤدي إلى سحبها داخل حزمة وقت تشغيل التطبيق المثبّت، حيث تكون عبئًا زائدًا بلا فائدة. يُطلق `twenty dev:build` تحذيرًا عندما تكون أيٌّ منهما ما تزال مدرجة ضمن `dependencies`.
أضِف تبعيات وقت التشغيل الخاصة بتطبيقك (المكتبات التي تستوردها دوال المنطق لديك فعلًا في وقت التشغيل) ضمن `dependencies` كالمعتاد. حافظ على الشجرة خفيفة: تُثبَّت `dependencies` في طبقة وقت التشغيل بحد أقصى 250MB بعد فك الضغط، وتفشل عملية المزامنة أو التثبيت بخطأ في حجم الاعتمادات عند تجاوز ذلك — تنتمي مكتبات واجهة المستخدم وأدوات التطوير إلى `devDependencies`.

View File

@@ -0,0 +1,166 @@
---
title: البدء السريع
icon: rocket
description: أنشئ أول تطبيق Twenty خلال دقائق.
---
## المتطلبات الأساسية
* **Node.js 24.5+** — [تنزيل](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 docker:start` | مثيل Twenty قيد التشغيل |
| **3. مزامنة** | قم بمزامنة شفرتك مباشرةً مع الخادم | `yarn twenty dev` | تظهر تغييراتك في واجهة المستخدم |
---
## المرحلة 1 — تهيئة هيكل مشروعك
أنشئ تطبيقًا جديدًا من القالب:
```bash filename="Terminal"
npx create-twenty-app@latest my-twenty-app
```
أداة التهيئة غير تفاعلية: يصبح اسم الدليل هو اسم التطبيق. مرِّر `--display-name` و`--description` لتخصيص البيانات الوصفية المُولَّدة (يمكنك أيضًا تعديلها لاحقًا في `src/constants/universal-identifiers.ts`). يُنشئ هذا مشروع TypeScript في `my-twenty-app/` يتضمّن ملف بداية `application-config.ts`، ودورًا افتراضيًا، وسير عمل CI/CD، واختبار تكامل.
**بعد هذه المرحلة:** سيكون لديك الشفرة المصدرية لتطبيق على جهازك. ليس قيد التشغيل بعد — وهذه هي المرحلة 2.
---
## المرحلة 2 — تشغيل خادم Twenty محلي
يحتاج تطبيقك إلى خادم Twenty للمزامنة معه. الخادم هو مثيل Twenty كامل — واجهة مستخدم، واجهة برمجة تطبيقات GraphQL، PostgreSQL — يعمل محليًا داخل Docker. ترفع شفرتك المحلية تعريفاتها إلى ذلك الخادم، مما يجعلها تظهر في واجهة المستخدم.
يقوم مُنشئ الهياكل بتشغيل مثيل لك: مع تشغيل Docker، يسحب صورة `twentycrm/twenty-app-dev`، ويبدأها على المنفذ `2020`، ويُجري مصادقة أداة CLI على مساحة العمل التجريبية المهيأة مسبقًا (`tim@apple.dev`) — دون الحاجة إلى تسجيل الدخول.
للاتصال بخادم Twenty موجود بدلاً من ذلك، مرِّر الخيار `--url \<your-server-url>`. تُجري الخوادم البعيدة المصادقة باستخدام OAuth: يفتح المتصفح حتى تتمكن من تسجيل الدخول والنقر على **Authorize**، مما يمنح أداة CLI حق الوصول إلى مساحة عملك. (يمكنك أيضًا اختيار استخدام OAuth محليًا باستخدام `--authentication-method oauth` — سجِّل الدخول باستخدام `tim@apple.dev` / `tim@apple.dev`.)
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/login.png" alt="شاشة تسجيل الدخول إلى Twenty" />
</div>
<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="قائمة &#x22;Your Apps&#x22; تعرض &#x22;My twenty app&#x22;" />
</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 والبرامج النصية
استخدم `plan` و`apply` لتشغيل نفس خط الأنابيب مرة واحدة، بدون أداة مراقبة (watcher):
```bash filename="Terminal"
yarn twenty plan # preview the metadata changes without applying them
yarn twenty apply # show the plan, then apply it
```
| أمر | السلوك | متى يُستخدم |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `yarn twenty dev` | يراقب ويعيد المزامنة عند كل تغيير. يستمر في العمل حتى توقفه. | تطوير محلي تفاعلي. |
| `yarn twenty apply` | بناء واحد + مزامنة واحدة، يخرج برمز `0` عند النجاح، و`1` عند الفشل. يطلب تأكيدًا عند إجراء تغييرات مدمِّرة (مرِّر `--force` لتجاوز ذلك). | CI، وخطافات ما قبل الالتزام، ووكلاء الذكاء الاصطناعي، وسير عمل مكتوب بنصوص. |
| `yarn twenty plan` | يبني تغييرات البيانات الوصفية ويطبعها **من دون تطبيقها**. | فحص ما الذي سيُغيِّره التزامن قبل تطبيقه. |
جميع الأوضاع تحتاج إلى جهة بعيدة موثَّقة. راجع قسم [المزامنة والاستعادة](/l/ar/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan) للحصول على مزيد من المعلومات حول `plan`.
<Note>
الأوامر `yarn twenty dev --once` و`yarn twenty dev --once --dry-run` هي أسماء بديلة مهملة للأمرين `yarn twenty apply` و`yarn twenty plan`.
</Note>
### خيارات وضع التطوير
| خيار | الوصف |
| ------------------------------------- | ------------------------------------------------------------------------------------ |
| `--force` | تطبيق التغييرات المدمِّرة (الحذف) بدون تأكيد. |
| `--debounceMs \<ms>` | اضبط مهلة إزالة الارتداد لتغييرات الملفات بالميلي ثانية (القيمة الافتراضية: `1000`). |
| `--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>

View File

@@ -0,0 +1,63 @@
---
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 pageLayoutTab` | `src/page-layout-tabs/\<name>.ts` |
| عنصر قائمة الأوامر | `yarn twenty dev:add commandMenuItem` | `src/command-menu-items/\<name>.ts` |
| حقل العرض | `yarn twenty dev:add viewField` | `src/view-fields/\<name>.ts` |
| موفر الاتصال | `yarn twenty dev:add connectionProvider` | `src/connection-providers/\<name>.ts` |
| نوع نشاط الجدول الزمني | `yarn twenty dev:add timelineActivityType` | `src/timeline-activity-types/\<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
```

View File

@@ -0,0 +1,14 @@
---
title: استكشاف الأخطاء وإصلاحها
description: مشكلات التشغيل الأول الشائعة — Docker، إصدار Node، Yarn، والتبعيات.
icon: wrench
---
* **أخطاء Docker** — تأكّد من أن Docker Desktop (أو الـ daemon) قيد التشغيل قبل `yarn twenty docker:start`. ستعرض رسالة الخطأ أمر البدء المناسب لنظام التشغيل لديك.
* **إصدار Node غير صحيح** — تحتاج إلى 24.5+ (`engines.node: ^24.5.0`). تحقّق باستخدام `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 dev: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).

View File

@@ -0,0 +1,118 @@
---
title: عناصر قائمة الأوامر
description: اعرض مكوّنات الواجهة الأمامية كإجراءات سريعة ومدخلات في قائمة الأوامر (Cmd+K) باستخدام ‎defineCommandMenuItem‎.
icon: terminal
---
يُعَدّ **عنصر قائمة الأوامر** الجسر بين المستخدم و[مكوّن الواجهة الأمامية](/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',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
## حقول التكوين
| الحقل | مطلوب | الوصف |
| --------------------------------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `universalIdentifier` | نعم | معرّف فريد ثابت للأمر |
| `label` | نعم | التسمية الكاملة المعروضة في قائمة الأوامر (Cmd+K) |
| `frontComponentUniversalIdentifier` | نعم | قيمة `universalIdentifier` للمكوّن الأمامي الذي يفتحه هذا الأمر |
| `shortLabel` | لا | تسمية أقصر تُعرَض على زر الإجراء السريع المثبّت |
| `icon` | لا | **مهمل** — يتم تجاهله لصالح أيقونة التطبيق؛ يُصدر البناء تحذيرًا إذا تم تعيينه |
| `isPinned` | لا | عند كونها `true`، يعرض الأمر كزر إجراء سريع في الزاوية العلوية اليمنى من الصفحة |
| `availabilityType` | لا | يتحكّم في مكان ظهور الأمر: `'GLOBAL'` (متاح دائمًا)، و`'GLOBAL_OBJECT_CONTEXT'` (فقط في الصفحات ذات سياق الكائن — صفحات الفهرس والسجل)، و`'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`)‎ التي تتعامل مع نمط الإجراء-ثم-إلغاء التركيب.
تدفق نموذجي: يقوم مكوّن عديم الواجهة بعرض `<Command execute={...} />` (اطّلع على [المثال الكامل](/l/ar/developers/extend/apps/layout/front-components#sdk-command-components))، ويشير عنصر قائمة الأوامر إليه:
```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',
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` إذا لم تكن الخاصية تساوي القيمة في أي عنصر |

View File

@@ -0,0 +1,849 @@
---
title: المكوّنات الأمامية
description: أنشئ مكونات React تُعرَض داخل واجهة مستخدم Twenty ضمن بيئة معزولة (sandbox).
icon: window-maximize
---
المكوّنات الأمامية هي مكوّنات React تُعرَض مباشرة داخل واجهة مستخدم Twenty. تعمل ضمن **Web Worker معزول** باستخدام Remote DOM — تُنفَّذ شيفرتك داخل iframe معزول ذو منشأ غير شفاف، ومع ذلك لا تزال واجهتها تُعرَض محليًا داخل الصفحة بدلًا من أن تظل محصورة داخل ذلك الـ iframe.
<Warning>
لا تزال مكوّنات Front قيد التطوير النشط. يعمل الكود الخاص بك على DOM جزئي، وليس على صفحة متصفح حقيقية، لذلك يمكن أن تفشل الاستخدامات المتقدمة، وغالبًا من دون أي إشعار. راجع [القيود الحالية](#current-limitations).
</Warning>
## أين يمكن استخدام مكوّنات الواجهة الأمامية
يمكن عرض مكوّنات الواجهة الأمامية في ثلاثة مواقع داخل Twenty:
* **اللوحة الجانبية** — المكوّنات غير عديمة الرأس تفتح في اللوحة الجانبية اليمنى. هذا هو السلوك الافتراضي عندما يتم تشغيل مكوّن واجهة أمامية من قائمة الأوامر.
* **الويدجت (لوحات المعلومات وصفحات السجلات)** — يمكن تضمين مكوّنات الواجهة الأمامية كويدجت داخل [تخطيطات الصفحات](/l/ar/developers/extend/apps/layout/page-layouts). عند تكوين لوحة معلومات أو تخطيط صفحة سجل، يمكن للمستخدمين إضافة ويدجت لمكوّن واجهة أمامية.
* **App settings** — يتم تعريفها باستخدام [`defineSettingsFrontComponent()`](#custom-settings-component)، حيث يُعرَض مكوّن الواجهة الأمامية كقسم داخل علامة تبويب **Settings** في التطبيق، ليحل محل واجهة مستخدم تكوين المتغيرات الافتراضية.
مكوّن الواجهة الأمامية بمفرده لا يمكن الوصول إليه من واجهة المستخدم — تحتاج إلى *عرضه*. الطرق الثلاث للقيام بذلك هي:
* **إقرانه مع [عنصر قائمة الأوامر](/l/ar/developers/extend/apps/layout/command-menu-items)** — يقوم بتسجيله في قائمة الأوامر (Cmd+K) واختياريًا كإجراء سريع مُثبّت.
* **تضمينه كويدجت في [تخطيط صفحة](/l/ar/developers/extend/apps/layout/page-layouts)** — يضعه في صفحة تفاصيل السجل أو لوحة المعلومات.
* **عرِّفه باستخدام [`defineSettingsFrontComponent()`](#custom-settings-component)** — يعرضه كقسم داخل علامة تبويب **Settings** في التطبيق، ليحل محل واجهة مستخدم تكوين المتغيرات الافتراضية.
## مثال أساسي
أسرع طريقة لرؤية مكوّن الواجهة الأمامية أثناء العمل هي إقرانه مع [`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',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
بعد المزامنة باستخدام `yarn twenty dev` (أو تشغيل الأمر لمرة واحدة `yarn twenty apply`)، يظهر الإجراء السريع في الزاوية العلوية اليمنى من الصفحة:
<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).
## مكوّن إعدادات مخصص
لاستبدال واجهة مستخدم تكوين المتغيرات المولَّدة تلقائيًا في علامة تبويب **Settings** في تطبيقك بمكوّنك الخاص، عرِّفه باستخدام `defineSettingsFrontComponent` بدلًا من `defineFrontComponent`. يستخدم نفس [حقول الإعدادات](#configuration-fields) (باستثناء `isHeadless`، الذي لا يُقبل لأن مكوّن الإعدادات يعرض دائمًا واجهة مستخدم مرئية)، ويُحدِّد أيضًا هذا المكوّن باعتباره واجهة إعدادات التطبيق.
يتم عرض المكوّن كقسم **داخل** علامة تبويب الإعدادات، وليس كبديل لعلامة التبويب بالكامل. الأقسام التي يديرها نظام Twenty — الترقية التلقائية، و App URL، والاتصالات — يتم عرضها دائمًا أعلاه ولا يمكن تجاوزها بواسطة التطبيق.
```tsx src/front-components/app-settings.tsx
import { defineSettingsFrontComponent } from 'twenty-sdk/define';
const AppSettings = () => {
return (
<div style={{ padding: '20px' }}>
<h2>My app settings</h2>
{/* render your own configuration UI here */}
</div>
);
};
export default defineSettingsFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'app-settings',
description: "Custom UI for the app's Settings tab",
component: AppSettings,
});
```
يُسمَح بمكوّن واجهة إعدادات واحد فقط لكل تطبيق؛ إعلان أكثر من واحد يؤدي إلى فشل عملية الإنشاء. عند وجوده، تعرض علامة تبويب **الإعدادات** الخاصة بالتطبيق هذا المكوّن بدلًا من واجهة مستخدم تكوين المتغيّرات الافتراضية.
## عديم الرأس مقابل غير عديم الرأس
تأتي مكوّنات الواجهة الأمامية بوضعَي عرض يتحكّم بهما الخيار `isHeadless`:
**غير عديم الرأس (افتراضي)** — يعرض المكوّن واجهة مستخدم مرئية. عند تشغيله من قائمة الأوامر يفتح في اللوحة الجانبية. هذا هو السلوك الافتراضي عندما تكون `isHeadless` تساوي `false` أو يتم تجاهلها.
**عديم الرأس (`isHeadless: true`)** — يتم تركيب المكوّن بشكل غير مرئي في الخلفية. لا يفتح اللوحة الجانبية. تم تصميم المكوّنات عديمة الرأس لإجراءات تنفّذ منطقًا ثم تُزيل تركيبها ذاتيًا — على سبيل المثال، تشغيل مهمة غير متزامنة، أو الانتقال إلى صفحة، أو إظهار نافذة تأكيد منبثقة. تتوافق بشكل طبيعي مع مكوّنات Command في SDK الموصوفة أدناه.
```tsx src/front-components/sync-tracker.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, enqueueSnackbar } from 'twenty-sdk/front-component';
import { useEffect } from 'react';
const SyncTracker = () => {
const [recordId] = useSelectedRecordIds();
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/front-component`:
* **`Command`** — يشغّل رد نداء غير متزامن عبر الخاصية `execute`.
* **`CommandLink`** — ينتقل إلى مسار في التطبيق. الخصائص: `to`، `params`، `queryParams`، `options`.
* **`CommandModal`** — يفتح نافذة تأكيد منبثقة. إذا أكّد المستخدم، ينفّذ رد النداء `execute`. الخصائص: `title`، `subtitle`، `execute`، `confirmButtonText`، `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — يفتح صفحة في اللوحة الجانبية. الـ props تعتمد على `page` — على سبيل المثال، `ViewRecord` يأخذ `recordId` + `objectNameSingular` (بالإضافة إلى معرّف `tab` اختياري لفتح السجل في تبويب معيّن)، بينما الصفحات الأخرى تأخذ `pageTitle` + `pageIcon`.
فيما يلي مثال كامل لمكوّن واجهة أمامية عديم الرأس يستخدم `Command` لتشغيل إجراء من قائمة الأوامر:
```tsx src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
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',
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/front-component';
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,
});
```
ومثال يستخدم `CommandOpenSidePanelPage` لفتح السجل الحالي في اللوحة الجانبية على تبويب معيّن. `tab` هو معرّف تبويب تخطيط الصفحة (تستخدم التخطيطات الافتراضية معرّفات مثل `company-tab-emails` أو `company-tab-timeline`؛ وتستخدم التخطيطات المخصّصة معرّف التبويب نفسه). إذا لم يكن المعرّف موجودًا في تخطيط السجل، فسيتم فتح التبويب الافتراضي بدلًا منه:
```tsx src/front-components/open-company-emails.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
CommandOpenSidePanelPage,
SidePanelPages,
useSelectedRecordIds,
} from 'twenty-sdk/front-component';
const OpenCompanyEmails = () => {
const selectedRecordIds = useSelectedRecordIds();
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
if (!recordId) {
return null;
}
return (
<CommandOpenSidePanelPage
page={SidePanelPages.ViewRecord}
recordId={recordId}
objectNameSingular="company"
tab="company-tab-emails"
resetNavigationStack={false}
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
name: 'open-company-emails',
description: 'Opens the current company on its Emails tab',
component: OpenCompanyEmails,
isHeadless: true,
});
```
## استدعاء دالة منطقية
تعمل مكونات الواجهة الأمامية على جانب المتصفح داخل Web Worker مُعزَل داخل iframe ذو origin غير شفاف، بينما تعمل [الدوال المنطقية](/l/ar/developers/extend/apps/logic/logic-functions) على جانب الخادم. لا توجد استدعاءات مباشرة ضمن العملية بين الاثنين — بدلاً من ذلك، يصل مكون الواجهة الأمامية إلى الدالة المنطقية عبر HTTP.
يتم الوصول إلى الدالة المنطقية المُعلَنة باستخدام `httpRouteTriggerSettings` عبر HTTP عند مسار التوجيه الخاص بها. يتعامل `RestApiClient` مع المسارات التي تبدأ بـ `/s/` باعتبارها مسارات للتطبيق، ويُحوِّلها إلى عنوان الـ URL الذي تُقدَّم منه الدوال الخاصة بك، ويُجري عملية المصادقة عليها باستخدام `TWENTY_APP_ACCESS_TOKEN`.
> **على Twenty Cloud، يتم تقديم الدوال المنطقية المُفعَّلة عبر HTTP على نطاق مخصص لكل مساحة عمل** عند `https://\<your-workspace-subdomain>.withtwenty.com\<path>`. للمتصلين الخارجيين، انسخ عنوان URL الدقيق من إعدادات **HTTP trigger** الخاصة بالدالة أو من علامة تبويب **Settings** في التطبيق.
يمكن لمكون واجهة أمامية عديم الرأس تنفيذ الاستدعاء عند التركيب عبر مكون `Command`، ثم إلغاء التركيب تلقائيًا:
```tsx src/front-components/sync-prs.tsx
import { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/front-component';
const SyncPrs = () => {
const execute = async () => {
await new RestApiClient().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,
});
```
المسار المُمرَّر إلى `RestApiClient` هو قيمة `httpRouteTriggerSettings.path` الخاصة بدالة المنطق (logic function) مع إضافة البادئة `/s`. أبقِ `isAuthRequired: true`؛ فرمز `TWENTY_APP_ACCESS_TOKEN` الذي تُنشئه 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_APP_ACCESS_TOKEN` تلقائيًا — انظر [متغيرات التطبيق](#application-variables). نظرًا لأن متغيرات التطبيق السرية لا تُعرَض أبدًا على مكونات الواجهة الأمامية، احتفِظ بمفاتيح واجهة برمجة التطبيقات والمنطق الحساس الآخر داخل الدالة المنطقية، وليس في مكون الواجهة الأمامية.
</Note>
### استدعاء واجهة REST API الخاصة بـ Twenty
لاستدعاء مسارات HTTP الخاصة بالتطبيق أو لقراءة سجلات Twenty وكتابتها من مكوِّن واجهة أمامية، استخدم `RestApiClient` من `twenty-client-sdk/rest`. يُرسل المسارات من نوع `/s/...` إلى عنوان URL الأساسي للدوال في مساحة العمل الخاصة بك، ويُرسل أي مسار آخر، بما في ذلك `/rest/...`، إلى `TWENTY_API_URL`.
يتصرف دائمًا بصفته الشخص الذي ينظر إلى الصفحة. `runAs: 'application'` هو خيار لدالة منطقية فقط: لا يتلقى المكوّن أبدًا الرمز المميز الخاص بتطبيقك، لذا فإن طلبه هنا يؤدي إلى طرح استثناء. ضع العمل الذي يحتاج إلى وصول التطبيق الخاص خلف دالة منطقية واستدعها بدلًا من ذلك.
| طريقة | الوصف |
| --------------------------------- | ----------------------------------------------------------------------- |
| `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 |
| `resolveUrl(path, options?)` | يُحوِّل مسارًا إلى عنوان URL كامل بدون إرسال طلب (لاستخدامه في الروابط) |
تدعم `options` كلًا من `headers` و`query` (سجل لمعاملات query-string؛ يتم تخطي القيم nullish) و`AbortSignal` عبر `signal`. يتم تسلسل كائن `body` غير من النوع `FormData` إلى JSON تلقائيًا. عند حدوث `401`، يقوم العميل بتحديث رمز الوصول مرة واحدة عبر المضيف ثم يعيد محاولة الطلب.
يتم تحديد عنوان URL الأساسي والرمز من بيئة التشغيل بشكل افتراضي. مرِّر معاملات تجاوز (overrides) إلى المُنشئ (constructor) عند الحاجة — على سبيل المثال في الاختبارات:
```ts
const client = new RestApiClient({
baseUrl: 'https://myworkspace.twenty.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 people = await client.get('/rest/people', {
query: { limit: 10 },
});
} 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,
useSelectedRecordIds,
useFrontComponentId,
} from 'twenty-sdk/front-component';
const RecordInfo = () => {
const userId = useUserId();
const [recordId] = useSelectedRecordIds();
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` | معرّف مثيل هذا المكوّن |
| `useTimelineActivityId()` | `string` أو `null` | معرّف نشاط المخطط الزمني الحالي عند عرض صف مخصص في المخطط الزمني |
| `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>
تُرجِع الدالة `getApplicationVariable` دائمًا **سلسلة نصية** (أو `undefined`)، بغضّ النظر عن `type` المُعلَن للمتغيّر. تُسلسَل السلسلة النصية بشكل متسق حسب النوع (القيم المنطقية على هيئة "true" / "false"، الأعداد كسلاسل عشرية، والمصفوفات/الكائنات كـ JSON)، وهو نفس التنسيق المستخدم مع `process.env` في وظائف المنطق — قم بتحليلها بنفسك (`Number(...)`، ‏`JSON.parse(...)`، ‏`=== 'true'`). انظر قسم [أنواع المتغيرات](/l/ar/developers/extend/apps/config/application#variable-types).
متغيرات النظام التالية تكون متاحة دائمًا عبر `process.env`:
| المتغيّر | الوصف |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TWENTY_API_URL` | عنوان URL الأساسي لـ Twenty Core API |
| `TWENTY_APP_ACCESS_TOKEN` | رمز مميز قصير العمر يقتصر نطاقه على دور الشخص المسجّل دخوله والمتقاطع مع دور تطبيقك، لذا لا يمكن للمكوّن أبدًا أن يفعل أكثر مما يستطيع الشخص الذي ينظر إليه فعله |
### `TWENTY_FUNCTIONS_URL`
يقوم Twenty أيضًا بحقن `TWENTY_FUNCTIONS_URL` في مكوِّنات الواجهة الأمامية والدوال المنطقية: وهو عنوان URL الأساسي الذي تُقدَّم منه الدوال المنطقية المُفعَّلة عبر HTTP في تطبيقك.
وهو موجود لأن ذلك العنوان (URL) ليس دائمًا هو خادم Twenty نفسه. على Twenty Cloud، يتم تقديم مسارات التطبيق على نطاق مخصص لكل مساحة عمل (`https://\<your-workspace-subdomain>.withtwenty.com`، أو نطاق التطبيق العمومي الأساسي عندما يتم تكوينه) بحيث تعمل الاستجابات المنشأة من التطبيق على أصل (origin) معزول بدلاً من أصل تطبيق Twenty. تُقدِّم النسخ المستضافة ذاتيًا والمحلية مسارات التطبيق تحت البادئة `/s` على الخادم نفسه وقد لا تضبط المتغير إطلاقًا. نظرًا لاختلاف عنوان URL الأساسي حسب مساحة العمل وحسب كل نسخة، لا يمكن لشفرتك (code) أن تُضمِّنه بشكل ثابت (hard-code) — يقوم الخادم بحقن القيمة الصحيحة وقت التشغيل.
نادرًا ما تحتاج إلى قراءته مباشرة. استدعِ مساراتك عبر `RestApiClient` باستخدام مسار يبدأ بالبادئة `/s/`، وسيقوم العميل بحل عنوان URL نيابةً عنك: يزيل بادئة `/s` ويستخدم `TWENTY_FUNCTIONS_URL` كهدف، مع الرجوع إلى `\<TWENTY_API_URL>/s` عندما لا يكون المتغير مضبوطًا. استخدم `resolveUrl('/s/\<path>')` للحصول على عنوان URL مطلق بدون إرسال طلب، على سبيل المثال لاستخدامه في رابط. اقرأ المتغير مباشرةً فقط عند إنشاء عنوان URL يدويًا:
```ts
const routeUrl = `${process.env.TWENTY_FUNCTIONS_URL || `${process.env.TWENTY_API_URL}/s`}/documents/generate`;
```
## واجهة الاتصال مع المضيف
يمكن للمكوّنات الأمامية تشغيل التنقّل والنوافذ المنبثقة والإشعارات باستخدام دوال من `twenty-sdk`:
| دالة | الوصف |
| -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `navigate(to, params?, queryParams?, options?)` | الانتقال إلى صفحة داخل التطبيق |
| `openSidePanelPage(params)` | فتح لوحة جانبية |
| `closeSidePanel()` | إغلاق اللوحة الجانبية |
| `openCommandConfirmationModal(params)` | عرض مربع حوار تأكيد |
| `enqueueSnackbar(params)` | عرض إشعار توست |
| `unmountFrontComponent()` | إلغاء تركيب المكوّن |
| `updateProgress(progress)` | تحديث مؤشّر التقدّم |
| `uploadFile(file, { fieldMetadataId, fileName? })` | تحميل كائن `Blob` إلى حقل من نوع FILES؛ تُعاد نتيجة على شكل `{ status: 'uploaded', file: { fileId, path, url, size, mimeType } }` أو `{ status: 'failed', reason: 'invalid-params' \| 'upload-failed' }` |
فيما يلي مثال يستخدم واجهة برمجة تطبيقات المضيف لعرض Snackbar وإغلاق اللوحة الجانبية بعد اكتمال الإجراء:
```tsx src/front-components/archive-record.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { enqueueSnackbar, closeSidePanel, useSelectedRecordIds } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
const ArchiveRecord = () => {
const [recordId] = useSelectedRecordIds();
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,
});
```
### التخزين
`localStorage` و`sessionStorage` يعملان كما يعملان في صفحة عادية، باستخدام واجهة برمجة التطبيقات المتزامنة القياسية. مفاتيحك يُحدَّد نطاقها بتثبيت تطبيقك والمستخدم المسجَّل دخوله: لا يمكن لأي تطبيق آخر قراءتها، وأي مستخدم آخر يسجّل الدخول في المتصفح نفسه يبدأ من مخزن فارغ. القيم المكتوبة في `localStorage` تبقى على الجهاز عبر عمليات إعادة التحميل؛ أمّا `sessionStorage` فيستمر طوال جلسة المتصفح.
```tsx src/front-components/note-draft.tsx
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
const NoteDraft = () => {
const [draft, setDraft] = useState(() => localStorage.getItem('draft') ?? '');
const handleChange = (nextDraft: string) => {
setDraft(nextDraft);
localStorage.setItem('draft', nextDraft);
};
return (
<textarea value={draft} onChange={(event) => handleChange(event.target.value)} />
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-def0-890123456789',
name: 'note-draft',
description: 'Keeps an unsaved note on this device',
component: NoteDraft,
});
```
Twenty تخزّن القيم نيابةً عن تطبيقك، لذا تُطبَّق عملية الكتابة محليًا فورًا ويتم حفظها في الخلفية. لا تنتظر عمليات القراءة أبدًا على المضيف. لا تتم مزامنة أي شيء: لا تنتقل القيم مع المستخدم إلى متصفح أو جهاز آخر، لذا استخدم [مخزن القيم المفتاحية](/l/ar/developers/extend/apps/logic/key-value-store) لدالة منطقية لأي شيء يجب أن يظل قائمًا بعد تغيير الجهاز.
يتم تقييد عمليات الكتابة، ويُحتسب كل حد بعدد الأحرف بدلاً من البايتات: لا يزيد طول المفاتيح عن 512 حرفًا، ولا تزيد قيمة واحدة عن 262,144 حرفًا، ولا تزيد سعة كل مساحة تخزين عن 1,048,576 حرفًا لكل تطبيق ومستخدم. تؤدي عملية كتابة تتجاوز حدًا معينًا إلى طرح استثناء `QuotaExceededError`، مثل واجهة برمجة تطبيقات المتصفح.
### العمل مع سجلات متعددة
استخدم `useSelectedRecordIds()` لمعالجة عدة سجلات محددة. هذا مفيد للعمليات المجمّعة:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
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,
});
```
أبرِزْه باستخدام [عنصر قائمة الأوامر](/l/ar/developers/extend/apps/layout/command-menu-items) المقيّد بتحديدات السجلات:
```ts src/command-menu-items/bulk-export.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
label: 'Bulk Export',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
});
```
## الأصول العامة
يمكن للمكوّنات الأمامية الوصول إلى ملفات من دليل `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) للتفاصيل.
## مشاركة التبعيات عبر مكوّنات الواجهة الأمامية
افتراضيًا، يضمّن كل مكوّن واجهة أمامية نسخته الخاصة من المكتبات التي يستوردها، لذا فإن تطبيقًا يحتوي على خمسة مكوّنات سيتضمّن React خمس مرات. صرّح بالتبعيات المشتركة في ملف `package.json` الخاص بتطبيقك لبناء تلك المكتبات مرة واحدة وجعل كل مكوّن في التطبيق يحمّلها من ملف واحد مخزَّن في الذاكرة المخبئية:
```json package.json
{
"frontComponentSharedDependencies": ["react", "react-dom/client", "twenty-ui/input"]
}
```
بعد ذلك، يستورد كل مكوّن تبعياته تمامًا كما كان من قبل — دون أي تغيير في كود المكوّن الخاص بك:
```tsx src/front-components/counter.tsx
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'counter',
component: Counter,
});
```
بعض الأمور التي يجب معرفتها:
* **حزمة تبعيات مشتركة واحدة لكل تطبيق.** يتم إنشاء الحزمة من تبعيات تطبيقك نفسه، بحيث تحتفظ بالتحكم الكامل في الإصدارات التي تُصدِرها.
* **اسرد المعرّفات (specifiers) الدقيقة التي تستوردها.** يُعَدّ `twenty-ui/input` و `twenty-ui/display` مدخلين منفصلين؛ اسم الحزمة وحده لا يشمل المسارات الفرعية. إدراج `react` يغطي تلقائيًا `react/jsx-runtime`.
* **شارك `react-dom/client` جنبًا إلى جنب مع `react`.** كل مكوّن يقوم بعملية التصيير من خلال `createRoot`، لذا فإن تركه خارجًا يعني أن كل مكوّن ما زال يضمّن React DOM داخل حزمتِه الخاصة.
* **الحزمة مُخزَّنة في الذاكرة المخبئية.** يتم تقديمها عبر عنوان URL يعتمد على قيمة تجزئة المحتوى (content-hash) مع ذاكرة مخبئية ثابتة طويلة الأجل، لذا يتم تنزيلها مرة واحدة وإعادة استخدامها عبر جميع مكوّنات التطبيق حتى تتغيّر إحدى تبعياتها.
* **المكوّنات التي لا تستورد أيًا من الحزم المشتركة لا تقوم بتنزيلها أبدًا.**
## التنسيق
تدعم المكوّنات الأمامية عدة أساليب للتنسيق. يمكنك استخدام:
* **أنماط مضمنة** — `style={{ color: 'red' }}`
* **مكوّنات Twenty UI** — مكتبة المكوّنات الخاصة بـ Twenty؛ راجع [استخدام مكوّنات Twenty UI](#using-twenty-ui-components) أدناه
* **Emotion** — CSS-in-JS مع `@emotion/react`
* **Styled-components** — أنماط `styled.div`
* **Tailwind CSS** — أصناف مساعدة
* **أي مكتبة CSS-in-JS** متوافقة مع React
## استخدام مكوّنات Twenty UI
توفّر Twenty مكتبة المكوّنات الخاصة بها على شكل حزمة [`twenty-ui`](https://www.npmjs.com/package/twenty-ui/v/1.0.0-alpha.1). يمكن للمكوّنات الأمامية استخدامه للأزرار، والوسوم، وشارات الحالة، والرقاقات، والصور الرمزية، والأيقونات، والطباعة، ورموز السمات التي تتطابق تلقائيًا مع نسق مساحة العمل الفاتح أو الداكن.
### التثبيت
أضِف الحزمة إلى تطبيقك، مع تثبيتها على الإصدار الذي تأتي به نسخة Twenty لديك:
```bash
yarn add twenty-ui@1.0.0-alpha.1
```
`twenty-ui` تكون مضمّنة في المكوّن الأمامي لديك وقت البناء، لذلك تحتاج فقط إلى أن تكون تابعة (dependency) لتطبيقك — ولا يوجد ما يلزم تهيئته وقت التشغيل.
### استيراد المكوّنات
استورِد من المسار الفرعي المطابق بدلًا من جذر الحزمة، حتى لا ينتهي الأمر إلا بالمكوّنات التي تستخدمها داخل حزمة التطبيق (bundle) لديك:
| المسار الفرعي | ما الذي يصدِّره |
| --------------------------- | ----------------------------------------------------- |
| `twenty-ui/input` | `Button` ومدخلات النماذج |
| `twenty-ui/data-display` | `Tag`، و`Status`، و`Chip`، و`Avatar`، وغير ذلك |
| `twenty-ui/feedback` | `Callout`، و`Banner`، و`Info`، وغير ذلك |
| `twenty-ui/typography` | `H1Title`، و`H2Title`، و`H3Title`، و`Label`، وغير ذلك |
| `twenty-ui/icon` | مكوّنات `Icon*` (مثلًا `IconCheck`) |
| `twenty-ui/theme-constants` | `ThemeProvider`، و`themeCssVariables` |
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Status, Tag } from 'twenty-ui/data-display';
import { Button } from 'twenty-ui/input';
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,
});
```
### الأيقونات
استورِد الأيقونات الفردية من `twenty-ui/icon`:
```tsx
import { IconBox, IconCheck } from 'twenty-ui/icon';
```
كل أيقونة مسمّاة قابلة للإزالة عبر تقنية tree-shaking، لذا فإن استيراد عدد قليل منها لا يزيد حجم الحزمة إلا قليلًا. تجنّب استخدام `IconsProvider`، و`useIcons`، و`iconsState` — لأنها تجلب مجموعة أيقونات Tabler الكاملة (عدة ميغابايت).
### التنسيق ورموز السمات
تتكيّف مكوّنات Twenty UI تلقائيًا مع نسق مساحة العمل الفاتح أو الداكن — إذ يطبّق المُصيّر (renderer) مخطط الألوان النشط على المضيف، وتضبط المكوّنات ألوانها وفقًا له.
لاستخدام نفس رموز التصميم في أنماطك المضمّنة (inline styles)، استدعِ الخطّاف `useTheme()`. يُرجِع هذا الخطّاف رموز سمة Twenty (للمسافات، والألوان، وأنصاف الأقطار، والخطوط) المرتبطة بالنسق النشِط، دون الحاجة إلى إعداد `ThemeProvider` في المكوّن لديك:
```tsx
import { useTheme } from 'twenty-ui/theme-constants';
const Card = () => {
const theme = useTheme();
return (
<div
style={{
padding: theme.spacing[4],
background: theme.background.secondary,
color: theme.font.color.primary,
}}
>
Themed card
</div>
);
};
```
نظرًا لأن `useTheme()` خطّاف، فإنك تقرأ الرموز داخل جسم المكوّن، لذا تعكس القيم دائمًا النسق المباشر (الحالي). تُصدَّر خريطة الرموز نفسها أيضًا كثابت `themeCssVariables`، لكن يُفضَّل استخدام `useTheme()` في المكوّنات الأمامية — إذ يمكن أن يكون الثابت على مستوى الوحدة الذي يفكّ مرجعية `themeCssVariables` غير معرَّف أثناء استخراج بيان التطبيق (app manifest).
للتفرّع بناءً على النظام النشِط (active scheme) صراحةً، اقرأه باستخدام `useColorScheme()` من `twenty-sdk/front-component`، والذي يعيد `'light'` أو `'dark'`.
## القيود الحالية
مكوّنات Front قيد التطوير النشط. الترسيم، والتنسيق، والتعامل مع الأحداث، وقياس العناصر، وتخزين المتصفح تعمل جميعها بشكل جيد. أي شيء يتجاوز *تلك* الحدود (استدعاء دالة DOM على ref، مراقبة تغيير أحجام العناصر، إنشاء portal خارج الشجرة الخاصة بك) مفقود أو غير مكتمل حاليًا، ومعظم هذه الحالات تفشل بصمت: لا يتم رمي استثناء، ولا يظهر خطأ TypeScript أيضًا، لأن القالب مكتوب استنادًا إلى DOM الكامل للمتصفح.
إذا كان أحد هذه الأمور يعيقك، [افتح تذكرة](https://github.com/twentyhq/twenty/issues/new/choose) ليتم إعطاؤه أولوية.
### التخطيط والقياس
يمكن للعناصر قياس نفسها: المضيف يعكس الإحداثيات الهندسية داخل وضع الحماية، بحيث تتم الإجابة عن عمليات القراءة محليًا ولكن يمكن أن تتأخر حتى إطار واحد، وتعيد أول عملية قراءة لعنصر لم يُقَس من قبل قيمًا صفرية. بعد الكتابة، أعد القراءة داخل استدعاء `requestAnimationFrame` أو داخل تأثير.
| واجهة برمجة التطبيقات | ما الذي يحدث |
| ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `getBoundingClientRect()`, `offsetWidth`, `clientWidth`, `scrollWidth`, `offsetTop`, ... | يعمل، من المرآة؛ تعيين `scrollTop` / `scrollLeft` بلا تأثير |
| `getClientRects()`, `window.matchMedia()` | يرمي استثناء |
| `window.innerWidth`, `innerHeight`, `devicePixelRatio`, `scrollX`, `scrollY` | يعمل، مع الإبلاغ عن منفذ عرض المتصفح؛ حجم أداتك نفسها هو `document.body.clientWidth` / `clientHeight` |
| `window.getComputedStyle()` | يُرجع أنماط العنصر المضمّنة فقط، وليس سلسلة الأنماط المحسوبة من المضيف مطلقًا |
| `ResizeObserver`, `IntersectionObserver` | `ReferenceError` (`typeof` guards تعمل) |
| `MutationObserver` | يعمل، بما في ذلك `subtree` و `attributeFilter` والقيم القديمة و `takeRecords()` |
أصبح التموضع باستخدام `getBoundingClientRect` يعمل الآن، ولكن أي شيء يراقب تغيّر الأحجام عبر `ResizeObserver` (مثل `ResponsiveContainer` في recharts و `autoUpdate` في Floating UI) ما زال لا يعمل. يفضَّل استخدام CSS للتخطيط على أي حال: ورقة الأنماط الخاصة بك تصل إلى الصفحة الحقيقية، لذا فإن flexbox و grid و `aspect-ratio` و `clamp()` و ‎`@container` تعمل بشكل طبيعي، بدون أي تأخير في الإطار.
<Note>
`requestAnimationFrame`, `fetch`, `setTimeout` و `queueMicrotask` تعمل بدون بادئة `window.`. فقط `window.requestAnimationFrame(...)` وما شابهها ترمي استثناء.
</Note>
### الوصول إلى DOM
يعطيك `ref` عنصرًا في sandbox، وليس `HTMLElement`.
| ما الذي تكتبه | ما الذي يحدث | استخدم بدلًا من ذلك |
| ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ref.current.focus()`, `.click()`, `.select()`, `.setSelectionRange()`, `.scrollIntoView()`, `video.play()` | يرمي استثناء | مكوّنات مضبوطة (Controlled components)؛ اقرأ القيم من `event.target` |
| `element.classList.add(...)` | يرمي استثناء (`classList` هو `undefined`) | قم ببناء سلسلة `className` بنفسك |
| `document.createTreeWalker()` | يرمي استثناء | تعمل `querySelector()` / `querySelectorAll()` و `getElementById()`؛ تعمل `getElementsByClassName()` أيضًا، ولكنها تُرجِع مجموعة ثابتة، وليس `HTMLCollection` حيًّا |
| `document.activeElement` | دائمًا `undefined` | تتبّع التركيز باستخدام `onFocus` / `onBlur` |
| `canvas` | لا يَرسُم أي شيء، بدون خطأ | SVG، أو الرسم خارج الشاشة وعرض النتيجة في عنصر `img` |
| `createPortal(node, document.body)` | لا يَرسُم أي شيء، بينما `isConnected` يُبلغ عن النجاح | استخدم الطبقات العائمة (overlays) داخل التخطيط نفسه مع `position: absolute`، أو مرّر إلى المكتبة عنصر الحاوية الخاص بك |
فجوة الـ portal هي السبب في أن عناصر popover في Radix و Headless UI و MUI و react-select لا ترسُم أي شيء بشكل افتراضي. معظمها يقبل خاصية container؛ وجّهها إلى عنصر قمت بترسيمه.
### الأحداث
أحداث الماوس، والمؤشر، واللمس، والسحب، ولوحة المفاتيح، والتركيز، و`input`/`change`/`submit`، و`scroll`/`wheel`/`contextmenu` و`animationend`/`transitionend` تُمرَّر إلى المضيف، بالإضافة إلى بعض الأحداث الخاصة بكل عنصر: `load`/`error` على `img`، والحافظة والتركيب على `input`/`textarea`، والوسائط على `video`/`audio`، و`toggle` على `details`/`dialog`. أي شيء آخر (`onAuxClick`, `onSelect`, `onInvalid`, `onReset`, `onAnimationStart`, التقاط المؤشر، `onLoad` خارج `img`) يتم إسقاطه دون تحذير.
`document.addEventListener()` و `window.addEventListener()` تُسجِّلان بدون خطأ لكن لا تُطلقان أبدًا، وهذا هو السبب في أن السحب يتوقف بمجرد أن يغادر المؤشر العنصر الذي بدأ عليه. `event.preventDefault()` لا يعبر أيضًا؛ إرسال النماذج، و`dragover`/`drop` ونقرات الروابط محمية مسبقًا من أجلك.
### السمات والأنماط
كل عنصر يمرّر خصائصه الخاصة إلى DOM المضيف (`href` على `a`، و`src`/`alt` على `img`، و`value`/`placeholder`/`disabled` على `input`، وهكذا)، بالإضافة إلى مجموعة مشتركة على كل عنصر: `id`, `className`, `style`, `title`, `tabIndex`, `role`, `draggable` وأي سمة `aria-*` / `data-*` (مفصولة بشرطات، لذا يتم إسقاط `ariaLabel`). أي شيء خارج ذلك يتم تجاهله بصمت، لذا عبّر عن الحالة المخصّصة كـ `data-*`.
يتم حقن CSS الخاص بالمكوّن، سواءً من `import './styles.css'` أو CSS-in-JS أو عنصر `style`، في وسم `head` لصفحة المضيف **بدون نطاق**. لذا تتصادم أسماء الأصناف مع أسماء Twenty نفسها (قم بإضافة بادئة لها، ولا تكتب أبدًا محددًا مثل `div { ... }`)، و `@media` تطابِق نافذة المتصفح بدلاً من الودجت الخاص بك (استخدم `@container` مع `container-type` الخاص بك). خصائص `style` المضمنة لا تتأثر.
### التخزين والشبكة
يتم توفير `localStorage` و`sessionStorage` بواسطة Twenty وليس بواسطة المتصفح: حيث يعمل المكون في عامل (worker) ضمن أصل غير شفاف، لذلك يقوم المضيف بتخزين القيم نيابةً عن تطبيقك. راجع قسم [التخزين](#storage) للاطلاع على نطاقاتها وحدودها. لا تزال IndexedDB وملفات تعريف الارتباط (cookies) وواجهة برمجة تطبيقات Cache و`BroadcastChannel` غير متاحة. للاحتفاظ بالحالة عبر الأجهزة، استدعِ [دالة منطقية](/l/ar/developers/extend/apps/logic/logic-functions) واستخدم [مخزن القيم المفتاحية](/l/ar/developers/extend/apps/logic/key-value-store) الخاص بها.
`fetch` يعمل، مع بعض التحفّظات:
* يتم تمرير الاستدعاءات إلى Twenty API ومسارات تطبيقك عبر المضيف، لذا يُفضَّل استخدام [`RestApiClient`](#calling-the-twenty-rest-api). في الاستدعاءات الممرَّرة عبر الوكيل، يتم إسقاط `AbortSignal` وخيارات `RequestInit` الأخرى، ولا يتم دعم سوى الأجسام من نوع `string` و`URLSearchParams`.
* تغادر النطاقات (origins) الأخرى صندوق العزل مع `Origin: null`، لذلك يجيب طرف ثالث لواجهة برمجة التطبيقات فقط إذا أرسل `Access-Control-Allow-Origin: *`. استدعِها من دالة منطقية بدلًا من ذلك.
* `fetch('/rest/people')` لا يتطابق أبدًا مع Twenty API، لأن صندوق العزل ليس لديه عنوان URL للصفحة لحساب مسار نسبي بناءً عليه.
### التقاط الوسائط
تعمل `navigator.mediaDevices.getUserMedia()` و`MediaRecorder` داخل مكوّنات الواجهة الأمامية من خلال sandbox polyfills، لذا يعمل كود التسجيل القياسي دون تغيير، وتُرجِع `MediaRecorder.isTypeSupported` إجابات لأنواع حاويات/ترميزات شائعة. يتم قبول كائنات قيود `getUserMedia` التفصيلية ولكن لا يتم تمريرها إلى المضيف — حيث يقوم المضيف بالالتقاط باستخدام الإعدادات الافتراضية لأنواع الوسائط المطلوبة — ولا يمكن أن يكون هناك أكثر من عملية التقاط واحدة نشطة في الوقت نفسه عبر التطبيقات. خزِّن كائن `Blob` المسجَّل باستخدام دالة المضيف `uploadFile`.
### فجوات أخرى
* **محتويات الملفات.** يُرجِع `input` من النوع `file` إلى معالجك بيانات تعريف الملف فقط، وليس البايتات، لذا فإن `FileReader` غير متاح. لرفع كائن `Blob` الذي يملكه كودك بالفعل — على سبيل المثال كائن تم إنتاجه بواسطة `MediaRecorder` — استخدم دالة المضيف `uploadFile`.
* **حمولات السحب والإفلات.** أحداث السحب تُطلق، لكن `event.dataTransfer` هي `undefined`.
* **الوحدات المدمجة في Node.** `fs` و`path` و`node:crypto` تفشل في عملية البناء، لذا انقل ذلك العمل إلى [دالة منطقية](/l/ar/developers/extend/apps/logic/logic-functions). Web Crypto و `fetch` و `TextEncoder` و `URL` متوفرة.
* يتم دائمًا إعادة عزل **`iframe`** دون `allow-same-origin`، لذلك يعمل التضمين الذي يعتمد على جلسته الخاصة في وضع "تسجيل الخروج". ليس لديه `onLoad` أيضًا.

View File

@@ -0,0 +1,46 @@
---
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` يتحكّم في الترتيب ضمن الشريط الجانبي.
* يحتوي التعداد أيضًا على `NavigationMenuItemType.RECORD`، ويُستخدم داخليًا للمفضّلات التي ينشئها المستخدم للسجلات — ولا يمكن استخدامه من بيان التطبيق (app manifest) لعدم وجود حقل للإشارة إلى سجل.
* `icon` و`color` اختياريان ويخصّصان مظهر الإدخال.
* `folderUniversalIdentifier` متاح أيضًا على أي عنصر لوضعه متداخلًا داخل عنصر أب من النوع `FOLDER`.
<Note>
**مشكلة شائعة:** إنشاء كائن بدون عنصر قائمة تنقّل مرتبط يجعل ذلك الكائن غير قابل للوصول من الشريط الجانبي. ما لم يكن كائنًا تقنيًا/داخليًا، يجب أن يحتوي كل كائن مخصّص على عنصر في الشريط الجانبي يشير إليه.
</Note>

View File

@@ -0,0 +1,59 @@
---
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` |
| **إعدادات التطبيق** | قسم تهيئة مخصص داخل علامة تبويب الإعدادات في التطبيق، بدلاً من واجهة المستخدم الافتراضية للمتغيرات | `defineSettingsFrontComponent` |
| **قائمة الأوامر (Cmd+K)** | إجراء سريع مُثبّت أو أمر مخفي | `defineCommandMenuItem` |
تعمل مكوّنات الواجهة الأمامية داخل Web Worker معزول باستخدام Remote DOM — يتم عرضها بشكل أصيل داخل الصفحة (وليس داخل iframe)، لكنها لا تستطيع الوصول مباشرةً إلى صفحة المضيف أو إلى DOM. يحدث التواصل مع Twenty من خلال واجهة API للمضيف تعتمد تمرير الرسائل.
لا تزال قيد التطوير النشط: يطبّق الـ sandbox شجرة DOM جزئية، لذلك قد تفشل حالات الاستخدام المتقدمة. راجع [القيود الحالية](/l/ar/developers/extend/apps/layout/front-components#current-limitations).

View File

@@ -0,0 +1,158 @@
---
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.VERTICAL_LIST,
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_INDEX'` أو `'RECORD_PAGE'` أو `'DASHBOARD'` أو `'STANDALONE_PAGE'`. استخدم `'RECORD_PAGE'` لتخصيص عرض التفاصيل لكائن محدّد.
* `objectUniversalIdentifier` يحدّد الكائن الذي ينطبق عليه هذا التخطيط.
* يُعرّف كل `tab` قسمًا من الصفحة يتضمن `title` و`position` و`layoutMode`: استخدم `VERTICAL_LIST` لصفحات السجلات والصفحات المستقلة، و`GRID` للوحات المعلومات، و`CANVAS` لعنصر واجهة واحد ينبغي أن يملأ إطار عرض علامة التبويب. تُرتّب علامة التبويب `VERTICAL_LIST` عناصر الواجهة عموديًا. تملأ عناصر الواجهة المضمنة التي تدير تمريرها بنفسها، مثل الجداول الزمنية والملفات والملاحظات والمهام وسير العمل، إطار عرض واحدًا؛ بينما تُعرض الحقول ومكونات الواجهة الأمامية والرسوم البيانية وغيرها من عناصر الواجهة ذات الحجم الملائم للمحتوى بارتفاع محتواها أو بالارتفاع المُعدّ لها. تُرتّب علامة التبويب `GRID` عناصر واجهتها دائمًا على شكل بطاقات ضمن شبكة من 12 عمودًا. لا تحتوي أداة `CANVAS` على موضع صريح؛ وإذا كانت علامة تبويب اللوحة تحتوي على عدة أدوات، فستُعرض عند ارتفاع محتواها بدلًا من ملء إطار العرض.
* عيِّن `layoutMode` بشكل صريح. إغفاله يمنحك `VERTICAL_LIST` على `STANDALONE_PAGE` و`GRID` في كل مكان آخر، وهو أمر نادرًا ما تريده في صفحة سجل.
* يمكن لكل `widget` داخل لسان أن يعرض [front component](/l/ar/developers/extend/apps/layout/front-components)، أو قائمة علاقات، أو أنواعًا أخرى من عناصر الواجهة (widgets) المدمجة.
* يمكن لعنصر واجهة `FRONT_COMPONENT` تعيين `headerCommandMenuItemUniversalIdentifiers` إلى مصفوفة مرتبة من المعرّفات العامة لعناصر قائمة الأوامر من التطبيق نفسه. تظهر هذه الإجراءات كأزرار أيقونات في رأس بطاقة عنصر الواجهة وتحافظ على فحوصات الإتاحة والأذونات على مستوى الأمر. يجب أن تكون المعرّفات فريدة ويجب أن تُحل عند تثبيت التطبيق.
* `position` على الألسنة يتحكّم في ترتيبها. استخدم قيمًا أعلى (مثل 50) لوضع الألسنة المخصّصة بعد الألسنة المدمجة.
### عناصر واجهة الحقول
عنصر واجهة `FIELD` يعرض حقلاً واحداً من السجل. لحقول العلاقات يمكنه أيضاً تضمين قائمة من السجلات ذات الصلة:
```ts
{
universalIdentifier: 'c1c2c3c4-c5c6-4000-8000-000000000003',
title: 'People → Opportunities',
type: 'FIELD',
configuration: {
configurationType: 'FIELD',
fieldMetadataId: PEOPLE_FIELD_UNIVERSAL_IDENTIFIER,
fieldDisplayMode: 'TABLE',
nestedRelationFieldMetadataId: OPPORTUNITIES_FIELD_UNIVERSAL_IDENTIFIER,
},
}
```
* `fieldMetadataId` يأخذ المعرف العالمي لحقل على كائن المخطط.
* `fieldDisplayMode` يكون واحداً من `'FIELD'`، `'CARD'`، `'EDITOR'`، `'VIEW'` أو `'TABLE'`. `TABLE` يضمّن عرضاً يسرد السجلات لحقل علاقة من نوع واحد إلى متعدد.
* `nestedRelationFieldMetadataId` اختياري، ويأخذ المعرّف العالمي لحقل علاقة من نوع واحد‑إلى‑متعدد على الكائن الهدف للعلاقة، وذلك لسرد السجلات التي تبعد قفزتين في سلسلة العلاقات (مثل صفحة شركة تسرد الفرص الخاصة بأشخاص الشركة، أو صفحة شخص تسرد الفرص الخاصة بشركة الشخص). يمكن أن تكون القفزة الأولى حقل علاقة من نوع واحد‑إلى‑متعدد أو من نوع متعدد‑إلى‑واحد، بينما يجب أن تكون القفزة الثانية من نوع واحد‑إلى‑متعدد (علاقات الوصل غير مدعومة)، ويتطلب ذلك `fieldDisplayMode: 'TABLE'` — إذ يُعد الجمع بينه وبين أي نمط عرض آخر خطأ في التحقق من الصحة، نظرًا لأن الودجة المتداخلة يتم عرضها دائمًا كعرض مضمن.
## 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.VERTICAL_LIST,
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` عندما تملك التخطيط بالكامل.

View File

@@ -0,0 +1,113 @@
---
title: العروض
description: قم بتوفير عروض محفوظة مُعدّة مسبقًا — ترتيب الأعمدة، المرشّحات، المجموعات — للكائنات في تطبيقك.
icon: list
---
يُعد **العرض** تكوينًا محفوظًا لكيفية عرض سجلات كائن معيّن: ما هي الحقول التي تظهر، وترتيبها، وما إذا كانت مرئية، وأي عوامل تصفية أو مجموعات مُطبَّقة. استخدم `defineView()` لتضمين عروض مُهيّأة مسبقًا مع تطبيقك.
يتلقى كل كيان (object) تلقائيًا عرض قائمة رئيسي خاص به (جدول "All ..." الذي يفتحه عنصر التنقل `OBJECT`) يتم توفيره من الخادم — بما في ذلك الكيانات التي ينشئها تطبيقك، مع عمود واحد لكل حقل يعرّفه تطبيقك. تكون العروض (views) التي تعرّفها باستخدام `defineView()` دائمًا عروضا **إضافية** فوق ذلك العرض الافتراضي.
```ts src/views/example-view.ts
import { defineView } 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',
position: 0,
fields: [
{
universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0,
isVisible: true,
size: 200,
},
],
});
```
## النقاط الرئيسية
* `objectUniversalIdentifier` يحدّد الكائن الذي ينطبق عليه هذا العرض. يمكن أن يكون كائنًا مخصصًا قمتَ بتعريفه أو كائن Twenty قياسيًا.
* إن عرض القائمة الرئيسي للكائن مملوك من قبل الخادم: تم إهمال `key` ويتم تجاهله، لذا لا يمكن لعرض البيان المطالبة به مطلقًا. أضف عنصر تنقل `VIEW` إذا كنت تريد أن يكون عرضك على بعد نقرة واحدة في الشريط الجانبي.
* يتحكّم `fields` في الأعمدة التي تظهر وترتيبها. يشير كل حقل إلى `fieldMetadataUniversalIdentifier`. للإشارة إلى حقل نظام يتم إنشاؤه تلقائيًا مثل `createdAt`، راجع قسم [استهداف بيانات النظام الوصفية](/l/ar/developers/extend/apps/data/system-fields).
* يمكنك أيضًا تعريف `filters` و`filterGroups` و`sorts` و`groups` و`fieldGroups` لتكوينات أكثر تقدمًا.
* يتحكّم `position` في الترتيب عند وجود عدة عروض لنفس الكائن.
## الخصائص الاختيارية
| الخاصية | القيم | الوصف |
| ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | `ViewType.TABLE` (الوضع الافتراضي)، `ViewType.LIST`، `ViewType.KANBAN`، `ViewType.CALENDAR` | كيفية ترتيب السجلات في الواجهة. (`FIELDS_WIDGET`, `TABLE_WIDGET`, `KANBAN_WIDGET`, `LIST_WIDGET`, و `CALENDAR_WIDGET` موجودة أيضًا ولكن يتم استخدامها داخليًا بواسطة عناصر واجهة تخطيط الصفحة.) |
| `visibility` | `ViewVisibility.WORKSPACE` (الوضع الافتراضي)، `ViewVisibility.UNLISTED` | ما إذا كان العرض مُدرجًا على مستوى مساحة العمل بالكامل أو مخفيًا من أدوات الاختيار. |
| `openRecordIn` | مهمل | لم يعد يُقرأ: أصبح مكان فتح السجلات الآن خاصية في [object](/l/ar/developers/extend/apps/data/objects) (`openRecordIn` على `defineObject()`)، مع الرجوع إلى تفضيل كل عضو الخاص به. |
| `sorts` | `{ fieldMetadataUniversalIdentifier, direction: ViewSortDirection.ASC \| DESC }[]` | ترتيب الفرز الافتراضي. |
| `isCompact` | `boolean` | عرض الصفوف بشكل مضغوط. |
| `mainGroupByFieldMetadataUniversalIdentifier` + `shouldHideEmptyGroups` | — | تجميع السجلات (مثل أعمدة كانبان) حسب حقل معيّن. |
| `kanbanAggregateOperation`, `kanbanAggregateOperationFieldMetadataUniversalIdentifier`, `kanbanColumnWidth` | `AggregateOperations.*` | تجميعات وأحجام أعمدة كانبان. |
| `calendarLayout`, `calendarFieldMetadataUniversalIdentifier`, `calendarEndFieldMetadataUniversalIdentifier` | `ViewCalendarLayout.DAY` / `WEEK` / `MONTH` | عروض التقويم: التخطيط، حقل التاريخ الذي يحدد موضع السجلات، وحقل تاريخ انتهاء اختياري. |
يتم تصدير جميع التعدادات أعلاه من `twenty-sdk/define`.
## الفلاتر
يمكن أن تأتي طريقة العرض مع عوامل تصفية مُطبَّقة مسبقًا. لكل عامل تصفية ثلاثة مكونات: **الحقل** الذي تُطبَّق عليه التصفية، و**المعامل** (كيفية المقارنة)، و**القيمة** (ما تتم المقارنة به). يجب أن تتطابق العناصر الثلاثة جميعًا — حيث سيتم رفض استخدام معامل لا ينطبق على نوع الحقل في وقت المزامنة.
```ts
import { ViewFilterOperand } from 'twenty-sdk/define';
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` الخاصة بالعرض. هذا هو النمط القياسي: يحصل كل كائن مخصص على عرض فهرس افتراضي يُنشأ تلقائيًا عند الإنشاء، لذا لا يلزمك سوى تضمين عنصر الشريط الجانبي الذي يفتحه.

View File

@@ -0,0 +1,197 @@
---
title: المهام الخلفية
description: قم بإحالة العمل الطويل الأمد أو المقيّد بالمعدل إلى عمّال Twenty عن طريق وضع تشغيل دالة منطقية أخرى في قائمة الانتظار بدلًا من تنفيذ كل شيء ضمن نفس السياق.
icon: layer-group
---
تشغيل الدالة المنطقية محدّد بحده الأقصى `timeoutSeconds` ‏(900 ثانية كحد أقصى). أي شيء لا يمكنه أن ينتهي ضمن تلك المهلة الزمنية — مثل إعادة المزامنة الكاملة، أو التوزيع على مستوى كل سجل، أو واجهة برمجة تطبيقات لطرف ثالث تفرض حدودًا على المعدل — يجب تقسيمه إلى عمليات تشغيل أصغر.
يقوم `enqueueJobs` بذلك بالضبط: فهو يطلب من عمّال Twenty تشغيل إحدى دوال منطق تطبيقك لاحقًا، مرة واحدة لكل حمولة، بحيث يتم كل تشغيل في عملية مستقلة وبمهلة زمنية خاصة به. يعود المستدعي فورًا.
```text
┌─────────────────┐ enqueueJobs(...) ┌──────────────┐ ┌────────────────────┐
│ Logic function │ ─────────────────▶ │ Job queue │──▶│ Logic function │
│ (returns now) │ │ (workers) │ │ (fresh run/timeout)│
└─────────────────┘ └──────────────┘ └────────────────────┘
```
## إضافة عمليات التشغيل إلى قائمة الانتظار
استورد `enqueueJobs` من `twenty-sdk/logic-function`، ووجّهه إلى `universalIdentifier` الخاص بدالة المنطق المراد تشغيلها، ومرّر حمولة واحدة لكل تشغيل لإضافتها إلى قائمة الانتظار.
```ts src/logic-functions/sync-all-contacts.ts
import { enqueueJobs } from 'twenty-sdk/logic-function';
await enqueueJobs({
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
payloads: [{ page: 1 }],
});
```
يتلقى كل تشغيل حمولته كوسيط للمعالج، تمامًا مثل أي مشغّل آخر. يجب أن تنتمي الدالة الهدف إلى **نفس التطبيق** الذي تنتمي إليه الدالة المستدعية — يُرفض وضع دالة تطبيق آخر في قائمة الانتظار مع ظهور `Logic function not found`، ولا تتم إضافة أي شيء إلى قائمة الانتظار. تقبل المكالمة الواحدة ما يصل إلى `200` حمولة.
<Note>
تُرجِع `enqueueJobs` فور قبول المهام، وليس عند تشغيلها. لا تُرجِع نتائج الأهداف — اجعل كل دالة هدف تكتب ما تُنتجه في [مخزن المفاتيح والقيم](/l/ar/developers/extend/apps/logic/key-value-store) أو في سجل مساحة العمل إذا كنت بحاجة إلى قراءته لاحقًا.
</Note>
<Note>
تم إهمال المساعد الأقدم `enqueueJob`، الذي يضيف مهمة واحدة إلى قائمة الانتظار لكل استدعاء. استخدم `enqueueJobs` مع قائمة `payloads` ذات عنصر واحد بدلًا من ذلك.
</Note>
## خيارات المهمة
تنطبق الخيارات على كل عملية تشغيل في الدفعة.
| الخيار | الإعداد الافتراضي | النطاق | ماذا يفعل |
| ------------ | ----------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `retryLimit` | `0` | `0`–`10` | إجمالي محاولات قائمة الانتظار الإضافية. تقتصر عمليات إعادة المحاولة التي يطلبها التطبيق على `3`. لا تقم بزيادة هذه القيمة إلا للمعالجات الآمنة للتشغيل مرتين. |
| `delayMs` | `0` | `0`–`604800000` (7 أيام) | انتظر هذه المدة قبل أن تصبح عمليات التشغيل مؤهلة للتنفيذ. |
```ts
await enqueueJobs({
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
payloads: [{ page: 1 }],
retryLimit: 3,
delayMs: 60_000,
});
```
<Note>
**أولوية التنفيذ غير قابلة للضبط بعد.** عمليات المهام في قائمة الانتظار تعمل دائمًا بأدنى أولوية، بحيث لا يتأخر عمل المنصة خلف مهام التطبيق. سيتم توفير إمكانية التحكّم في الأولوية قريبًا.
</Note>
يرث التشغيل المُضاف إلى قائمة الانتظار المستخدم الفعلي (acting user) الخاص بالدالة التي أضافته، لذلك يعمل بنفس الأذونات.
## أعِد محاولة فشل عابر.
لا يعيد Twenty محاولة كل استثناء صادر عن تعليمات التطبيق البرمجية. يُعامل الخطأ العادي الذي تم طرحه على أنه فشل دائم. بالنسبة إلى فشل عابر، يمكن لدالة منطقية موضوعة في قائمة الانتظار طلب ما يصل إلى ثلاث إعادات محاولة عبر طرح `RetryableLogicFunctionError`.
```ts
import {
type LogicFunctionExecutionContext,
RetryableLogicFunctionError,
} from 'twenty-sdk/logic-function';
export const handler = async (
_payload: unknown,
{ retryCount, maxRetries }: LogicFunctionExecutionContext,
) => {
const response = await fetch('https://api.example.com/contacts');
if (response.status === 429 || response.status >= 500) {
throw new RetryableLogicFunctionError(
`The contacts API is temporarily unavailable (${response.status}); retry ${retryCount} of ${maxRetries}`,
);
}
};
```
اطرح `RetryableLogicFunctionError` مباشرةً عندما يكون ذلك ممكنًا. إذا وسّعته، فلا تستبدل `name` الخاص به: يتعرّف Twenty على الاسم المُسلسل `RetryableLogicFunctionError` عبر بيئات وقت التشغيل المختلفة.
تكون قيمة `retryCount` هي `0` للتنفيذ الأولي، ولا تزداد إلا عندما تطلب تعليمات التطبيق البرمجية إعادة محاولة. تكون قيمة `maxRetries` بحد أقصى `3`، وقد تكون أقل عندما تكون للمهمة الموضوعة في قائمة الانتظار حدود إجمالية أصغر لإعادة المحاولة. لا تؤدي حالات فشل المنصة إلى زيادة `retryCount`، رغم أنها لا تزال تستهلك ميزانية الأمان الإجمالية لقائمة الانتظار.
تؤخر قائمة الانتظار محاولات إعادة المحاولة باستخدام تراجع أسي وتذبذب. لا يُضمن التأخير الدقيق عمدًا، لذا ينبغي ألا تعتمد تعليمات التطبيق البرمجية على حدوث إعادة محاولة في وقت محدد بدقة. بمجرد الوصول إلى `maxRetries`، يُسجَّل `RetryableLogicFunctionError` آخر باعتباره فشل التطبيق النهائي دون تنفيذ آخر.
<Warning>
تعيد عمليات إعادة المحاولة تشغيل المعالج بالكامل، وقد تحدث بعد نجاح بعض الآثار الجانبية. اجعل المعالج متوافقًا مع التكرار قبل طلب إعادات المحاولة.
</Warning>
## طريقة الاستخدام: التمرير عبر مزامنة طويلة
النمط الكلاسيكي هو دالة تضيف *نفسها* إلى قائمة الانتظار مع المؤشر (cursor) التالي. يقوم كل تشغيل بتنفيذ صفحة واحدة من العمل ضمن مهلة التنفيذ الخاصة به، وتتوقّف السلسلة عندما لا يبقى شيء.
```ts src/logic-functions/sync-contacts-page.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { enqueueJobs } from 'twenty-sdk/logic-function';
const SYNC_CONTACTS_PAGE = '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33';
const handler = async (params: { cursor?: string }) => {
const { contacts, nextCursor } = await fetchContactsPage(params.cursor);
await importContacts(contacts);
if (nextCursor) {
await enqueueJobs({
logicFunctionUniversalIdentifier: SYNC_CONTACTS_PAGE,
payloads: [{ cursor: nextCursor }],
delayMs: 2_000,
});
}
return { imported: contacts.length, done: !nextCursor };
};
export default defineLogicFunction({
universalIdentifier: SYNC_CONTACTS_PAGE,
name: 'sync-contacts-page',
timeoutSeconds: 120,
handler,
});
```
## توزيع متشعّب لكل سجل
عندما يكون العمل بطبيعته لكل عنصر، أضِف مهمة واحدة لكل عنصر إلى قائمة الانتظار ضمن استدعاء واحد، ودع العمال يعالجونها بالتوازي بدلًا من استخدام حلقة داخلية.
```ts
const companies = await listCompaniesToEnrich();
await enqueueJobs({
logicFunctionUniversalIdentifier: ENRICH_COMPANY,
payloads: companies.map((company) => ({ companyId: company.id })),
retryLimit: 2,
});
```
## ممارسات جيدة للعمل طويل الأمد
قاعدتان تغطيان تقريبًا كل مهمة طويلة: **استخدم الاستدعاء الذاتي (recursion) بدلًا من الحلقات (looping)**، و**عالِج جزءًا محدود الحجم في كل تشغيل**.
التشغيل الذي يحاول تنفيذ كل شيء دفعة واحدة هو نمط الفشل — يصل إلى مهلة التنفيذ، ومع إعادة المحاولة يبدأ كل شيء من الصفر. بدلًا من ذلك، اضبط حجم الجزء بحيث ينتهي بشكل مريح ضمن `timeoutSeconds`، ثم خزّن موقعك الحالي، وأضِف التشغيل التالي إلى قائمة الانتظار.
```ts src/logic-functions/enrich-companies-batch.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { enqueueJobs, kv } from 'twenty-sdk/logic-function';
const ENRICH_COMPANIES_BATCH = '3f9d1c02-8a44-4f0e-b1d7-9c2e5a7b4f10';
const CHUNK_SIZE = 50;
const handler = async (params: { offset?: number }) => {
const offset = params.offset ?? 0;
const companies = await listCompaniesToEnrich({
offset,
limit: CHUNK_SIZE,
});
for (const company of companies) {
await enrichCompany(company);
}
await kv.set('enrich:progress', { offset: offset + companies.length });
if (companies.length === CHUNK_SIZE) {
await enqueueJobs({
logicFunctionUniversalIdentifier: ENRICH_COMPANIES_BATCH,
payloads: [{ offset: offset + CHUNK_SIZE }],
});
}
return { processed: companies.length, done: companies.length < CHUNK_SIZE };
};
export default defineLogicFunction({
universalIdentifier: ENRICH_COMPANIES_BATCH,
name: 'enrich-companies-batch',
timeoutSeconds: 300,
handler,
});
```
ما الذي يجعل هذا متماسكًا:
* **اضبط حجم الجزء بناءً على أبطأ عنصر، وليس على المتوسّط.** يجب أن يتناسب حاصل ضرب `CHUNK_SIZE × worst-case item time` ضمن `timeoutSeconds` مع هامش احتياطي، وإلا فإن نهاية الجزء ستُفقَد عندما يتم إيقاف التشغيل بسبب انتهاء المهلة.
* **اجعل شرط الإنهاء صريحًا.** استخدم الاستدعاء الذاتي فقط عندما يعود جزء كامل. سلسلة تتوقّف على أساس "عدم وجود نتائج" فقط ستستمر إلى الأبد إذا أعاد المصدر صفحة قصيرة في منتصف الطريق.
* **ثبّت التقدّم قبل إضافة التشغيل التالي إلى قائمة الانتظار،** بحيث يُعاد تشغيل الحلقة الفاشلة من آخر جزء مكتمل بدلًا من البداية.
* **اجعل كل جزء قابلًا للتكرار دون آثار جانبية (idempotent).** يجب ألّا يؤدّي إعادة معالجة جزء واحد بعد إعادة المحاولة إلى كتابة مزدوجة — اربط عمليات الكتابة بالسجل أو المعرّف الخارجي الذي تعالجه.
* **فضّل سلسلة مجزّأة على توزيع واحد ضخم (giant fan-out)** عندما يتعامل العمل مع طرف ثالث محدود المعدّل (rate-limited): سلسلة مع `delayMs` تنظّم نفسها، في حين أن آلاف المهام المضافة إلى قائمة الانتظار دفعة واحدة تصبح جميعها مؤهّلة فورًا.

View File

@@ -0,0 +1,269 @@
---
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',
},
// Optional: a logic function in this app to run right after a connection is
// established. See "Run a logic function on connect".
// onConnectLogicFunction: { universalIdentifier: '3a2b1c0d-...-...' },
// Optional: a logic function in this app to run right after a connection is
// removed. See "Run a logic function on disconnect".
// onDisconnectLogicFunction: { universalIdentifier: '4d5e6f70-...-...' },
});
```
```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="تشغيل دالة منطقية عند الاتصال" description="التفاعل فور إنشاء الاتصال">
بعض المزوّدين يسلّمونك بيانات وقت الاتصال تحتاج إلى الاحتفاظ بها قبل أن يصبح الاتصال قابلاً للاستخدام — المثال الكلاسيكي هو Slack، حيث يحدّد رد OAuth معرّف الفريق `team_id` الخاص بمساحة العمل الذي ستُربط به الأحداث الواردة. عيِّن `onConnectLogicFunction` للإشارة إلى دالة منطقية في التطبيق نفسه (حسب `universalIdentifier` الخاص بها)، وسيتم تشغيلها مباشرة بعد إنشاء `ConnectedAccount`.
```ts src/connection-providers/slack-connection.ts
export default defineConnectionProvider({
universalIdentifier: '...',
name: 'slack',
displayName: 'Slack',
type: 'oauth',
oauth: {
/* ... */
},
// Runs claimSlackTeam after every successful Slack connection.
onConnectLogicFunction: {
universalIdentifier: '3a2b1c0d-1111-4222-8333-444455556666',
},
});
```
يعمل الـ hook **بشكل غير متزامن في مساحة العمل التي تجري الاتصال** (يتم وضعه في قائمة الانتظار، ولا يُنتظر انتهاؤه)، لذلك لن يؤدي hook بطيء أو فاشل إلى حظر أو تعطيل رد نداء OAuth — اجعله قابلاً للتكرار دون آثار جانبية (idempotent) ويتولى إعادة المحاولات بنفسه. يتلقى المعالج:
```ts
type OnConnectPayload = {
connectionProviderId: string;
connectionProviderName: string; // e.g. 'slack'
connectedAccountId: string;
};
```
من هناك استخدم `getConnection(connectedAccountId)` لقراءة رمز الوصول الجديد واستدعاء واجهة برمجة تطبيقات المزوّد (على سبيل المثال، Slack `auth.test`) أو حفظ تعيين باستخدام [مخزن المفاتيح والقيم](/l/ar/developers/extend/apps/logic/key-value-store).
</Accordion>
<Accordion title="تشغيل دالة منطقية عند قطع الاتصال" description="نظّف عند إزالة اتصال">
أي شيء يطالب به تطبيق ما وقت الاتصال يجب تحريره عندما يزول الاتصال. على سبيل المثال، يجب على تكامل Slack الذي يطالب بـ `team_id` عند الاتصال تحرير تلك المطالبة حتى تتمكن مساحة عمل أخرى من ربط فريق Slack نفسه. عيِّن `onDisconnectLogicFunction` للإشارة إلى دالة منطقية في التطبيق نفسه، وسيتم تشغيلها مباشرة بعد حذف `ConnectedAccount`.
```ts src/connection-providers/slack-connection.ts
export default defineConnectionProvider({
universalIdentifier: '...',
name: 'slack',
displayName: 'Slack',
type: 'oauth',
oauth: {
/* ... */
},
// Runs releaseSlackTeam after every Slack disconnection.
onDisconnectLogicFunction: {
universalIdentifier: '4470aba8-5ff5-4800-88db-2a427cd8677c',
},
});
```
تمامًا مثل خطاف on-connect، يتم تشغيله **بشكل غير متزامن في مساحة العمل التي يتم قطع اتصالها** ولا يمنع عملية قطع الاتصال أبدًا. يتلقى المعالج نفس شكل الحمولة:
```ts
type OnDisconnectPayload = {
connectionProviderId: string;
connectionProviderName: string; // e.g. 'slack'
connectedAccountId: string;
};
```
يكون `ConnectedAccount` قد أُزيل بالفعل عندما يتم تشغيل الخطاف، لذلك لم يعد `getConnection(connectedAccountId)` يُرجع نتيجة. أي شيء يحتاجه إجراء التنظيف (مثل `team_id` أو معرّف اشتراك خارجي) يجب أن يكون قد كُتب في [مخزن القيم المفتاحية](/l/ar/developers/extend/apps/logic/key-value-store) في وقت الاتصال، مع استخدام `connectedAccountId` كمفتاح.
يتم إطلاق الخطاف عندما تتم إزالة اتصال بمفرده. يؤدي إلغاء تثبيت التطبيق إلى حذف اتصالاته عبر حذف متسلسل في قاعدة البيانات، لذلك لا يتم تشغيل الخطاف في هذه الحالة. صرّح عن `uninstallLogicFunction` على `defineApplication` لذلك المسار: يتم تشغيله قبل حذف بيانات التعريف الخاصة بالتطبيق، لذا لا يزال بإمكانه استدعاء `listConnections` وتنظيف أي شيء متبقٍّ.
</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` | الاسم المعروض للاتصال (يُستمد تلقائيًا عند ردّ نداء OAuth، وقابل لإعادة التسمية من قِبل المستخدم) |
| `معالجة` | البريد الإلكتروني لحساب التمهيد المتصل ، تم تحديثه على كل إعادة اتصال؛ تم حلها من OIDC 'id_token' العائدة في تبادل الرموز الرمزية ، العودة إلى رسالة توصيل المستخدم العشرين عندما لا يعود مقدم الخدمة واحدة |
| `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>

View File

@@ -0,0 +1,110 @@
---
title: مخزن المفاتيح-القيم
description: احتفظ بالنتائج الوسيطة، وخزّن البيانات مؤقتًا، وشارك الحالة عبر تشغيلات دوال المنطق باستخدام مخزن مفاتيح-قيم مدمج على مستوى التطبيق.
icon: database
---
تعمل دوال المنطق داخل عمليات Node.js معزولة وقصيرة العمر — بمجرد انتهاء التشغيل، لا يظل أي شيء محفوظًا في الذاكرة. عندما تحتاج إلى تذكّر شيءٍ ما بين عمليات التشغيل (تخزين مؤقت لاستجابة واجهة برمجة تطبيقات مكلفة، أو تخزين مؤشر لمزامنات تزايدية، أو تقليل تكرار العمل، أو تمرير الحالة من دالة إلى أخرى)، احتفظ به في مخزن مفاتيح-قيم مدمج.
يحصل كل تطبيق على مساحة أسماء معزولة خاصة به: تتم فهرسة الإدخالات بحسب التطبيق المصادق عليه، لذا لا يمكن أن تتصادم مفاتيحك مع مفاتيح تطبيق آخر — ولا يمكن لتطبيق آخر قراءتها.
```text
┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐
│ Logic function │ ─────────────────────▶ │ Application KV store │
│ (your handler) │ ◀───────────────────── │ key (unique) │ value │
└─────────────────┘ kv.get(key) └──────────────────────────┘
```
## جلب، تعيين، حذف
استورد `kv` من `twenty-sdk/logic-function`. يمكن أن تكون القيم أي حمولة قابلة للتسلسل إلى JSON.
```ts src/logic-functions/sync-linear-issues.ts
import { kv } from 'twenty-sdk/logic-function';
// Read a value. Returns null when the key is missing.
const cursor = await kv.get<string>('sync-cursor:linear');
// Write a value. Creates the entry on first write, updates it afterwards.
await kv.set('sync-cursor:linear', newCursor);
// Delete an entry. Returns true when an entry was removed.
await kv.delete('sync-cursor:linear');
```
## النطاقات
لكل إدخال نطاق (scope)، يتم تمريره كخيار في كل استدعاء. القيمة الافتراضية هي `WORKSPACE`.
* **`WORKSPACE`** (الافتراضي) — يكون الإدخال خاصًا بتثبيت مساحة العمل الحالية لتطبيقك. كل مساحة عمل تقوم بتثبيت التطبيق تحصل على مجموعة مستقلة خاصة بها من المفاتيح. هذا هو الخيار المناسب لبيانات التخزين المؤقت، والمؤشرات (cursors)، والحالة الخاصة بكل مساحة عمل.
* **`SERVER`** — تتم مشاركة الإدخال عبر **كل عملية تثبيت** لتطبيقك على الخادم. تتصرف إدخالات الخادم مثل **مطالبات (claims)**: تكون القيمة المخزّنة دائمًا هي workspaceId لمساحة العمل التي طالبت بالمفتاح (احذف `value` في `set` للمطالبة بالمفتاح لصالح مساحة العمل الحالية)، ولا يمكن سوى لتلك المساحة أن تعيد الكتابة فوقه أو تحذفه. يمكن لأي تثبيت قراءة الإدخال.
توجد مطالبات الخادم من أجل التوجيه عبر مساحات العمل (cross-workspace routing). يعمل [محلّل مسار الخادم](/l/ar/developers/extend/apps/logic/logic-functions#server-route-trigger) في مساحة عمل مالك تسجيل التطبيق، ولكن عادةً لا يحمل خطاف الويب الوارد سوى معرّف حساب خارجي — وليس معرّف مساحة العمل في Twenty. اجعل كل مساحة عمل تطالب بالمعرّف الخارجي الخاص بها في وقت الاتصال، ثم قم بحلّه في المسار (route):
```ts
// In the connected workspace, when the external account is linked:
await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' });
// In the server-route resolver (owner workspace), on each webhook:
const workspaceId = await kv.get<string>(`slack:team:${teamId}`, {
scope: 'SERVER',
});
```
نظرًا إلى أنّه لا يمكن المطالبة بمفتاح خادم إلا لصالح مساحة عمل المتصل نفسه ولا يمكن أبدًا الكتابة فوقه من قِبَل مساحة أخرى، فلن تتمكّن أي مساحة عمل من اختطاف تعيين (mapping) يخص مساحة عمل أخرى. يصدر `kv.set` استثناءً (throws) عندما يكون المفتاح مُطالَبًا به مسبقًا من قِبَل مساحة عمل أخرى.
## استخدمه: خزّن استدعاء مكلفًا في الذاكرة المؤقتة
استخدام شائع هو التخزين المؤقت لاستجابة جهة خارجية بطيئة أو مقيدة بمعدل معيّن حتى تعيد عمليات التشغيل المتكررة استخدامها بدلًا من دفع التكلفة في كل مرة.
```ts src/logic-functions/getExchangeRate.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { kv } from 'twenty-sdk/logic-function';
const ONE_HOUR_MS = 60 * 60 * 1000;
type CachedRate = { rate: number; fetchedAt: number };
const handler = async (params: { from: string; to: string }) => {
const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`;
const cached = await kv.get<CachedRate>(cacheKey);
if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
return { rate: cached.rate, cached: true };
}
const response = await fetch(
`https://api.example.com/rate?from=${params.from}&to=${params.to}`,
);
const { rate } = (await response.json()) as { rate: number };
await kv.set(cacheKey, { rate, fetchedAt: Date.now() });
return { rate, cached: false };
};
export default defineLogicFunction({
universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
name: 'get-exchange-rate',
timeoutSeconds: 10,
handler,
});
```
## أنماط ونصائح
* **مساحات الأسماء.** أضف بادئة إلى المفاتيح للحفاظ على فصل الاهتمامات المختلفة — `sync-cursor:linear` و `cache:exchange-rate:USD:EUR` و `lock:nightly-report`.
* **انتهاء الصلاحية (TTL).** لا يحتوي المخزن على آلية انتهاء صلاحية مدمجة. قم بتخزين طابع زمني داخل القيمة (كما في مثال التخزين المؤقت) وتحقق منه عند القراءة، أو امسح دوريًا المفاتيح القديمة من خلال [دالة يتم تشغيلها بواسطة cron](/l/ar/developers/extend/apps/logic/logic-functions).
* **ما الذي يتم تخزينه.** أي قيمة قابلة للتسلسل إلى JSON — أرقام، سلاسل نصية، مصفوفات، كائنات. حافظ على صِغر الإدخالات؛ فهذا مخصّص للتنسيق والتخزين المؤقت، وليس للملفات الكبيرة أو الكتل الثنائية الضخمة. بالنسبة للملفات، استخدم حقل `FILES` ودالة [`uploadFile`](/l/ar/developers/extend/apps/logic/logic-functions#uploading-files).
* **إمكانية الرؤية.** تعيش الإدخالات في قاعدة بيانات المثيل (instance)، وليس كسجلات مساحة عمل — فهي لا تظهر مطلقًا في واجهة مستخدم مساحة العمل، وليست جزءًا من نموذج بيانات تطبيقك، ولا تحتاج إلى أذونات أدوار أو كائنات.
## بديل: كائن مخزن قابل للاستعلام
المخزن المدمج غامض عمدًا: الإدخالات ليست سجلات، لذلك لا يمكنك استعراضها في واجهة المستخدم، أو ربطها بكائنات أخرى، أو تصفيتها باستعلامات السجلات. عندما تحتاج إلى أيٍّ من ذلك — مثل سجل مزامنة مرئي، أو حالة لكل سجل — عرِّف بدلًا من ذلك كائنًا تقنيًا صغيرًا يحتوي على حقل `key` فريد وحقل `value` من نوع `RAW_JSON`، واستعلم عنه عبر عميل واجهة برمجة التطبيقات محدد الأنواع (typed API client). اطلع على [Objects](/l/ar/developers/extend/apps/data/objects) للرجوع إلى `defineObject` وعلى [Data → Unique indexes](/l/ar/developers/extend/apps/data/overview#unique-indexes) لفرض تفرّد المفاتيح.
* **تحديد النطاق إلى سجل.** أضف [علاقة](/l/ar/developers/extend/apps/data/relations) من كائن المخزن إلى الكائن المستهدف بدلًا من ترميز المعرّف داخل المفتاح.
* **الرؤية والصلاحيات.** تعيش الصفوف في قاعدة بيانات مساحة العمل مثل أي سجل آخر، لذا يمكن الاستعلام عنها عبر واجهة برمجة التطبيقات وتلتزم [بدور](/l/ar/developers/extend/apps/config/roles) التطبيق لديك. لإبقاء المخزن خارج واجهة المستخدم الرئيسية، اتركه خارج [قائمة التنقل](/l/ar/developers/extend/apps/layout/navigation-menu-items).
<Note>
على عكس المخزن المدمج، يكون الكائن المخصص محدَّد النطاق دائمًا بمساحة عمل واحدة — لا يمكنه مشاركة الإدخالات عبر التثبيتات بالطريقة التي تفعلها مفاتيح `SERVER`.
</Note>

View File

@@ -0,0 +1,908 @@
---
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/` إلى مسار التوجيه عند استخدام `RestApiClient`؛ يستخدم عنوان URL المنشور قاعدة `TWENTY_FUNCTIONS_URL` المُحدَّدة (أو `\<server-url>/s` إذا لم تُحدَّد).
<Note>
لاستدعاء دالة منطقية يتم تشغيلها بواسطة مسار من مكون واجهة (بدون واجهة رسومية)، راجع قسم [استدعاء دالة منطقية](/l/ar/developers/extend/apps/layout/front-components#calling-a-logic-function).
</Note>
* **cron**: يشغّل وظيفتك على جدول باستخدام تعبير CRON.
* **databaseEvent**: يعمل على أحداث دورة حياة كائنات مساحة العمل. عندما تكون عملية الحدث هي `updated`، يمكن تحديد الحقول المحددة المراد الاستماع إليها في مصفوفة `updatedFields`. إذا تُركت غير معرّفة أو فارغة، فسيؤدي أي تحديث إلى تشغيل الدالة.
> مثال: `person.updated`، `*.created`، `company.*`
* **serverRoute**: يوفّر مسار HTTP واحدًا بنطاق التسجيل. تعمل دالة **resolver** (المُعلَنة باستخدام `serverRouteTriggerSettings`) في مساحة عمل المالك وتُرجِع إمّا كائن `Response` متزامنًا أو كلاً من مساحة العمل المستهدفة ودالة المنطق المطلوب إدراجها في قائمة الانتظار؛ في مسار الإدراج في قائمة الانتظار يؤكّد النظام الأساسي تلقّي الطلب برمز `202` ويُشغِّل ذلك **الهدف** في طابور العامل (worker queue). راجع [مشغّل مسار الخادم](#server-route-trigger).
<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` | `سلسلة نصية` | طريقة 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>
#### استجابات أخطاء المنصة
إلى جانب استجابات المعالج لديك، تتعامل المنصة مع استدعاءات المسارات مباشرة في بعض الحالات: `404` عندما لا يكون المسار موجودًا أو لا تكون الدالة موجودة، و`403` عندما يكون التطبيق متوقفًا، و`429` عند بلوغ حد معدل التنفيذ، و`422` عندما تكون `dependencies` الخاصة بالإنتاج للتطبيق كبيرة جدًا بحيث يتعذر تثبيتها — راجع [حدود حجم dependencies](/l/ar/developers/extend/apps/getting-started/project-structure#dependencies).
#### مشغّل مسار الخادم
`httpRouteTriggerSettings` يوفّر دالة تحت ‎`/s/`‎ ويحل مساحة العمل من مضيف الطلب — وهذا يعمل عندما تكون لكل مساحة عمل نطاقها الخاص. لكن المزوّدين من جهات خارجية يرسلون أحداث كل مستأجر إلى عنوان URL **واحد**. في هذه الحالة، استخدم `serverRouteTriggerSettings`.
يتكوّن المشغّل من جزأين:
1. دالة منطق **resolver** — يتم التصريح عنها باستخدام `serverRouteTriggerSettings` — تعمل في **مساحة العمل المالكة** (مساحة العمل التي تمتلك تسجيل التطبيق). تفحص الطلب الوارد وتُرجِع إمّا:
* `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — يضع النظام الأساسي ذلك الهدف في قائمة الانتظار في مساحة العمل المُحدَّدة ويؤكّد تلقّي الطلب برمز `202 { queued: true }`، أو
* `Response` من `twenty-sdk/logic-function` — تُعيد المنصّة إرسال تلك الاستجابة عبر HTTP بشكل متزامن ولا تُدرِج هدفًا في قائمة الانتظار (استخدم هذا لمصافحات التحدّي مثل Slack `url_verification`).
يُعَدّ الـ resolver نقطة التفويض الوحيدة — فعنوان URL يحمل فقط معرّف الـ resolver. **هذا هو المكان المفضّل للتحقق من تواقيع الطلبات**: يعمل الـ resolver قبل أي تأثير جانبي، ولديه إمكانية الوصول إلى `rawBody` الأصلي والرؤوس المُمرَّرة، ويمكنه رفض الطلب دون لمس الهدف مطلقًا.
2. دالة منطق **target** — دالة منطق عادية لكل مساحة عمل — تعمل بعد ذلك في مساحة العمل التي تم حلّها باستخدام الحمولة التي أعادها الـ resolver (أو حمولة الطلب الأصلية إذا لم يقم الـ resolver بتحويلها). قيمة الإرجاع الخاصة به **لا** يطّلع عليها مستدعي HTTP عندما يختار الـ resolver مسار الإضافة إلى الطابور (enqueue path).
```ts src/logic-functions/resolve-server-route.logic-function.ts
import { createHmac, timingSafeEqual } from 'crypto';
import { defineLogicFunction } from 'twenty-sdk/define';
import { Response, type RoutePayload } from 'twenty-sdk/logic-function';
// Runs in the owner workspace. Verifies the request signature, picks
// which target function should handle the event, and returns the
// workspace + target the platform should dispatch to.
const handler = async (event: RoutePayload) => {
// Fail closed if the secret isn't configured — never fall back to an
// empty key, which would let any caller forge a matching signature.
const secret = process.env.GITHUB_WEBHOOK_SECRET;
if (!secret) {
throw new Error('GITHUB_WEBHOOK_SECRET is not configured');
}
const signature = event.headers['x-hub-signature-256'] ?? '';
const expected =
'sha256=' +
createHmac('sha256', secret).update(event.rawBody ?? '').digest('hex');
const a = Buffer.from(signature);
const b = Buffer.from(expected);
if (a.length !== b.length || !timingSafeEqual(a, b)) {
throw new Error('invalid signature');
}
const body = (event.body ?? {}) as {
challenge?: string;
metadata?: { twentyWorkspaceId?: string };
type?: string;
};
// Handshakes must be answered on this same response, so reply from the
// resolver instead of returning a dispatch target.
if (body.type === 'url_verification') {
return new Response({ challenge: body.challenge });
}
const workspaceId = body.metadata?.twentyWorkspaceId;
if (!workspaceId) {
throw new Error('event is not linked to a workspace');
}
return {
workspaceId,
// Route different event types to different target functions.
targetLogicFunctionUniversalIdentifier:
body.type === 'invoice.paid'
? 'c4e2a9b1-7d4e-4c9a-9f2b-2e1d6a4c8e10' // handle-invoice-paid
: 'd5f3b0c2-8e5f-5d0b-a0c3-3f2e7b5d9f21', // handle-other-event
};
};
export default defineLogicFunction({
universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
name: 'resolve-server-route',
handler,
serverRouteTriggerSettings: {
forwardedRequestHeaders: ['x-hub-signature-256'],
},
});
```
```ts src/logic-functions/handle-invoice-paid.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
// Runs in the resolved workspace. The resolver has already authenticated
// the request, so this handler can focus on the actual work.
const handler = async (event: RoutePayload) => {
// ...handle the verified event
return { received: true };
};
export default defineLogicFunction({
universalIdentifier: 'c4e2a9b1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
name: 'handle-invoice-paid',
handler,
});
```
يمكن الوصول إلى نقطة النهاية عند:
```
POST https://your-twenty-server.com/webhooks/server/:resolverLogicFunctionUniversalIdentifier
```
المعرّف هو `universalIdentifier` الخاص بالـ resolver من ملف manifest لديك. سجّل عنوان URL هذا لدى المزوّد.
**الرد على طلب GET للتحقق.** يتحقق بعض الموفّرين من نقطة نهاية قبل أن يرسلوا إليها، وذلك بإرسال طلب `GET` يحمل تحديًا إلى عنوان URL نفسه الذي سيرسلون إليه الأحداث لاحقًا عبر `POST` — وتُعد WhatsApp Cloud API من Meta أحدهم. لا يستجيب مسار الخادم إلا لطلبات `POST` ما لم تحدد خلاف ذلك، لذا أعلن الطريقتين معًا:
```ts
serverRouteTriggerSettings: {
forwardedRequestHeaders: ['x-hub-signature-256'],
httpMethods: ['GET', 'POST'],
}
```
يصل التحدي في `event.queryStringParameters`، وتؤدي إعادة `Response` إلى إرجاعه إلى الموفّر ضمن الطلب نفسه. يُرسل نص سلسلة كـ `text/plain`، وهذا ما يتوقعه هؤلاء الموفّرون:
```ts
const handler = async (event: RoutePayload) => {
if (event.requestContext.http.method === 'GET') {
return new Response(event.queryStringParameters['hub.challenge']);
}
// …handle the delivered event
};
```
يستبدل `httpMethods` الإعداد الافتراضي بدلاً من الإضافة إليه، لذا فإن `['GET']` وحده يجعل المسار يرفض `POST`. يتم دعم `GET` و`POST` فقط. اتركه غير معيّن ما لم يكن المزوّد يحتاج إلى الفعل الثاني: فالمسار الذي يعلن `GET` سيُنفَّذ محلّله بواسطة أي مستدعٍ غير موثَّق، بما في ذلك برامج الزحف وأدوات توسيع الروابط التي ترسل `GET` دون طلب مسبق. يُستجاب لكل ما لا تتوفر للمنصة طريقة له بالرمز `405` دون أن يُشغَّل المحلّل مطلقًا.
<Note>
**يجب أن يتم المطالبة بالتطبيق وتثبيته في مساحة عمل المالك الخاصة به.** نظرًا لأن محلِّل الاستدعاء يعمل في **مساحة عمل المالك** (مساحة العمل التي تمتلك تسجيل التطبيق)، فإن مشغّل مسار الخادم يعمل فقط بمجرد أن يكون قد تم *المطالبة* بالتطبيق — أي أصبح لديه مساحة عمل مالكة — **و** تم **تثبيت هذا التطبيق في مساحة عمل المالك**. إلى أن يتحقق الشرطان معًا، فلن يكون لدى محلِّل الاستدعاء مكان يعمل فيه، وبالتالي لا يمكن إرسال المسار. لذلك لا يمكن إدراج أي تطبيق يعرِّض دالة منطقية `serverRouteTriggerSettings` في السوق حتى تتم المطالبة به وتثبيته في مساحة عمل المالك الخاصة به.
</Note>
**عقد الـ Resolver.** يفرض نوع `LogicFunctionConfig` في حزمة SDK هذا في وقت الترجمة: بمجرد تعيينك لـ `serverRouteTriggerSettings`، يُقيَّد الـ handler الخاص بك بأن يُرجِع إما `Response`، أو `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (أو `Promise` لأيٍّ منهما). على مسار الإرسال (dispatch path)، يجب أن يكون `workspaceId` لمساحة عمل تكون الدالة المستهدفة مثبّتة فيها، وإلا فسيتم رفض الطلب مع `404`. أي نتيجة لا تطابق أياً من البنيتين — بما في ذلك تلك التي لا تكون معرّفاتها UUIDs — تُرفَض مع رمز الحالة `502`.
| الحقل | النوع | الملاحظات |
| ---------------------------------------- | ------------------ | ---------------------------------------------------------------------- |
| `workspaceId` | `string` | معرّف UUID لمساحة العمل التي سيعمل فيها الهدف. |
| `targetLogicFunctionUniversalIdentifier` | `string` | `universalIdentifier` لدالة المنطق التي سيتم استدعاؤها في تلك المساحة. |
| `payload` | `object` (اختياري) | إذا تم تعيينه، فإنه يستبدل جسم الطلب المُرسَل إلى الهدف. |
<Warning>
**مسؤولية التحقق من التوقيع تقع عليك — تحقّق في الـ resolver.** المنصّة لا تتحقق من تواقيع الطلبات. يُعَدّ الـ resolver المكان الموصى به للقيام بذلك: فهو يعمل أولًا، مع إمكانية الوصول إلى `event.rawBody` والرؤوس التي أدرجتها في `forwardedRequestHeaders`، وأي خطأ يتم رميه (أو أي `workspaceId` لا يطابق) يوقف عملية الإرسال قبل استدعاء الهدف. إذا دفعت التحقق بدلًا من ذلك إلى داخل الهدف، فيجب على الهدف أن يكون حذرًا حتى لا يفقد `rawBody` والرؤوس — أي يجب ألّا يعيد الـ resolver خاصية `payload`. تحقّق دائمًا **قبل** أي تأثير جانبي، واستخدم مقارنة بزمن ثابت.
</Warning>
بالنسبة لتواقيع الطلبات، يستخدم معظم المزوّدين HMAC-SHA256 للتوقيع؛ الأجزاء التي تختلف هي اسم الرأس وترميز الملخّص وسلسلة الحمولة الموقّعة. بعض الأمثلة:
| المزود | الرؤوس المطلوب تمريرها | السلسلة الموقَّعة | الملخّص |
| ---------------------------- | ------------------------------------------------------ | ---------------------------- | -------------------------------------------------- |
| Svix (Recall, Resend, Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64 (السر يكون بصيغة base64 بعد إزالة `whsec_`) |
| سترايب | `stripe-signature` | `{timestamp}.{rawBody}` | hex |
| جيت هاب | `x-hub-signature-256` | `{rawBody}` | hex (يبدأ بـ `sha256=`) |
| Shopify | `x-shopify-hmac-sha256` | `{rawBody}` | base64 |
| Slack | `x-slack-signature`, `x-slack-request-timestamp` | `v0:{timestamp}:{rawBody}` | hex (يبدأ بـ `v0=`) |
يُظهِر مثال الـ resolver أعلاه بالفعل تدفّق GitHub HMAC-SHA256 — عدِّل اسم الرأس وترميز الملخّص وسلسلة الحمولة الموقّعة بحسب المزوّد الذي تدمجه.
<Note>
عندما يُرجِع الـ resolver كائن إرسال (dispatch object)، يستجيب المسار بـ `202 { queued: true }` وتعمل الدالة المستهدفة على طابور العامل (worker queue) — المتصل لا يطّلع أبدًا على زمن استجابة الدالة المستهدفة أو نتيجتها أو حالات الفشل الخاصة بها (تُسجَّل هذه في سجلات التنفيذ). هذا يمنع عمليات إعادة الإرسال من جهة المرسِل من تضخيم تباطؤ المعالجة، وهو ما تريده عند استيعاب خطافات الويب.
عندما يجب على المتصل قراءة جسم الاستجابة في نفس الطلب (مثل challenge handshakes أو interactive acknowledgements)، أرجِع `Response` من **الـ resolver** بدلًا من ذلك. تعيد المنصّة إرجاعها بشكل متزامن وتتجاوز الطابور؛ تمر ترويساتها (headers) عبر نفس قائمة السماح (allow-list) الخاصة باستجابات مسارات HTTP. احرص على أن تكون دالة resolver سريعة — بعض المزوّدين (مثل Slack) تنتهي مهلة طلباتهم خلال بضع ثوانٍ. نظرًا لأن الـ resolver يمكن الوصول إليه كنقطة نهاية عامة، قم بحمايته من خلال تحديد المعدّل (rate limiting) على الحافة لديك.
</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,
};
};
```
#### سياق التنفيذ
يتلقى كل معالج وسيطًا ثانيًا يصف عملية التشغيل نفسها، بغض النظر عما شغّلها. بينما يتغير شكل الوسيط الأول بحسب المُشغِّل، لا يتغير شكل هذا الوسيط:
```ts
import type { LogicFunctionExecutionContext } from 'twenty-sdk/logic-function';
const handler = async (
event: DatabaseEventPayload,
{ workspaceId, workspaceMemberId }: LogicFunctionExecutionContext,
) => {
if (workspaceMemberId === null) {
throw new Error('This function must be triggered by a person');
}
return { reviewedBy: workspaceMemberId, workspaceId };
};
```
| الحقل | النوع | الوصف |
| ------------------- | ---------------- | --------------------------------------------------------------------------------------------- |
| `workspaceId` | `string` | مساحة العمل التي تنتمي إليها عملية التشغيل. |
| `userWorkspaceId` | `string \| null` | الشخص الذي شغّل عملية التشغيل، ممثلًا بعضويته في مساحة العمل هذه. |
| `workspaceMemberId` | `string \| null` | الشخص نفسه بصفته سجل عضو في مساحة العمل، وهو ما تربط السجلات به. |
| `retryCount` | `number` | `0` في المحاولة الأولى. راجع [المهام الخلفية](/l/ar/developers/extend/apps/logic/background-jobs). |
| `maxRetries` | `number` | الحد الأقصى لعمليات إعادة المحاولة التي يطلبها التطبيق. |
يكون `userWorkspaceId` و`workspaceMemberId` بقيمة `null` عندما لا يشغّل أحد عملية التشغيل: فلا توجد جهة خلف جداول cron وخطافات التثبيت وخطافات الويب غير الموثقة. ويكونان أيضًا بقيمة `null` عندما لا يكون لدى الشخص سجل عضو في مساحة العمل، أو عندما يكون سجله قد حُذف.
<Note>
**يخبرك السياق بمن شغّل عملية التشغيل.** أما ما قد *تفعله* عملية التشغيل فهو تعريف منفصل أدناه.
</Note>
#### أذونات الوصول التي تستخدمها المكالمة
يتصرف كل عميل — `CoreApiClient` و`MetadataApiClient` و`RestApiClient` — بصفته الشخص الذي شغّل عملية التشغيل: إذ يتقاطع دوره مع دور تطبيقك، لذا لا يمكن للمكالمة أبدًا أن تفعل أكثر مما يسمح به أيّ منكما. هذا هو السلوك الافتراضي، ويعني أنه لا يمكن للشخص أبدًا استخدام تطبيقك لتجاوز أذوناته الخاصة.
عندما لا يشغّل أحد عملية التشغيل، لا يوجد شخص للتصرف بصفته، لذا يعود العميل نفسه إلى أذونات الوصول الخاصة بتطبيقك: لا تحتاج جداول cron وخطافات التثبيت وخطافات الويب غير الموثقة إلى معالجة خاصة.
تحتاج بعض المكالمات بصورة مشروعة إلى أذونات الوصول الخاصة بالتطبيق حتى عندما يكون هناك شخص *خلف* عملية التشغيل — مثل قراءة سجلات إعدادات تطبيقك، أو تنفيذ عمل لا يستطيع ذلك الشخص تنفيذه بنفسه. أنشئ عميلاً ثانيًا لهذه الحالات:
```ts
import { CoreApiClient } from 'twenty-client-sdk/core';
const asPerson = new CoreApiClient();
const asApplication = new CoreApiClient({ runAs: 'application' });
const handler = async (event: DatabaseEventPayload) => {
// config records only your app may read
const { slackUserLinks } = await asApplication.query({ ... });
// created as the person, so it is attributed and permission-checked
await asPerson.mutation({ createNote: { ... } });
};
```
أنشئ كليهما مرة واحدة، في نطاق الوحدة، ثم يحدد كل موضع استدعاء أذونات الوصول التي يستخدمها من خلال العميل الذي يستدعيه. يأخذ `RestApiClient` الخيار نفسه:
```ts
import { RestApiClient } from 'twenty-client-sdk/rest';
const asApplication = new RestApiClient({ runAs: 'application' });
```
<Note>
**عملية التشغيل التي لم يشغّلها أحد تتصرف بصفتها تطبيقك.** لا يوجد شخص خلف جداول cron وخطافات التثبيت وخطافات الويب غير الموثقة، لذا يعود العميل الافتراضي إلى أذونات الوصول الخاصة بتطبيقك ويستمر في العمل. لا يلزم استخدام `runAs: 'application'` إلا عندما تريد أذونات الوصول تلك في عملية تشغيل شغّلها شخص *بالفعل*.
تحقق من `context.workspaceMemberId` عندما تتصرف دالة بشكل مختلف بناءً على وجود شخص خلفها، على سبيل المثال لإسناد سجل.
</Note>
تستخدم مساعدات SDK التي تصل إلى موارد تطبيقك الخاصة أذونات الوصول الخاصة به دائمًا وتتجاهل `runAs`: وهي [مخزن المفتاح-القيمة](/l/ar/developers/extend/apps/logic/key-value-store) و[الاتصالات](/l/ar/developers/extend/apps/logic/connections) و`runAgent` و`getPublicAssetUrl` وفرض رسوم الأرصدة.
#### إتاحة دالة كأداة ذكاء اصطناعي أو كإجراء ضمن سير العمل
يمكن إتاحة دوال المنطق على واجهتين، ولكلٍ منهما مشغِّل خاص به:
* **`toolTriggerSettings`** — يجعل الدالة قابلة للاكتشاف عبر ميزات الذكاء الاصطناعي الخاصة بـ Twenty (الدردشة، MCP، استدعاء الدوال). يستخدم JSON Schema القياسي، وهو التنسيق الذي تفهمه LLMs أصلاً.
* **`workflowActionTriggerSettings`** — يجعل الدالة تظهر كخطوة في منشئ سير العمل المرئي. يستخدم `InputSchema` الغني الخاص بـ Twenty لكي يتمكن المُنشئ من عرض محرّرات الحقول المناسبة، وأدوات انتقاء المتغيّرات، والتسميات.
يمكن للدالة اختيار أحدهما، أو الآخر، أو كليهما. توجد جنبًا إلى جنب مع `cronTriggerSettings` و`databaseEventTriggerSettings` و`httpRouteTriggerSettings` — النمط نفسه، والشكل نفسه.
<Note>
**العلاقة بإجراء Code الخاص بسير العمل.** يُعَد إجراء **Code** المضمَّن في منشئ سير العمل دالة منطقية بحد ذاته — حيث ينشئ Twenty واحدًا لكل خطوة Code ويعرض محرره مضمّنًا. تُستخدَم `workflowActionTriggerSettings` لتحويل هذا الكود المضمَّن لمرة واحدة إلى إجراء **قابل لإعادة الاستخدام**: عرِّف الدالة مرة واحدة في تطبيقك وستصبح قابلة للاختيار في أي سير عمل، بدلاً من نسخها ولصقها في كل خطوة Code. راجع [إجراء Code](/l/ar/user-guide/workflows/capabilities/workflow-actions#code) في دليل المستخدم لعرض منظور المستخدم النهائي.
</Note>
```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'],
},
},
});
```
للتصريح بمعاملاتك **مرة واحدة** وخدمة كلتا الواجهتين، عرّف مخطط JSON واحد (`InputJsonSchema`) وحوِّله لاستخدامه في إجراء سير العمل باستخدام `jsonSchemaToInputSchema` من `twenty-sdk/logic-function`. `toolTriggerSettings.inputSchema` يستخدم مخطط JSON مباشرة، بينما `workflowActionTriggerSettings.inputSchema` يتوقّع `InputSchema` الخاص بـ Twenty:
```ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema, type InputJsonSchema } from 'twenty-sdk/logic-function';
const inputSchema: InputJsonSchema = {
type: 'object',
properties: {
companyName: { type: 'string', label: 'Company name' },
domain: { type: 'string', label: 'Domain' },
},
required: ['companyName'],
};
export default defineLogicFunction({
...,
toolTriggerSettings: { inputSchema },
workflowActionTriggerSettings: {
label: 'Enrich Company',
icon: 'IconBuilding',
inputSchema: jsonSchemaToInputSchema(inputSchema),
},
});
```
##### مثال كامل لإجراء سير عمل
تقبل `workflowActionTriggerSettings` أربعة حقول:
| الحقل | الغرض |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `label` | الاسم المعروض للإجراء في منتقّي الخطوات في منشئ سير العمل. الإعداد الافتراضي هو `name` الخاص بالدالة. |
| `icon` | أيقونة تُعرَض بجانب الإجراء (اسم من `tabler-icons`، مثلًا `IconBuilding`). |
| `inputSchema` | مخطط الإدخال الغني الخاص بـ Twenty — ما يعرضه المُنشئ كحقول قابلة للضبط (مع منتقّي المتغيرات). اختياري؛ يستنتج من المعالج عند حذفه. |
| `outputSchema` | يصرِّح بالشكل الذي يعيده المعالج، بحيث **يمكن للخطوات اللاحقة أن تطابق حقول المخرجات الخاصة به**. اختياري؛ من دونه يتم كشف المخرَج كقيمة واحدة غير شفافة. |
تجميع ذلك معًا — دالّة معروضة كإجراء سير عمل، مع مخرَج مُعلَن بحيث يمكن للخطوات اللاحقة الرجوع إلى `taskId`:
```ts src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema, type InputJsonSchema } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
const inputSchema: InputJsonSchema = {
type: 'object',
properties: {
companyName: { type: 'string', label: 'Company name' },
domain: { type: 'string', label: 'Domain' },
},
required: ['companyName'],
};
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,
},
});
// The keys returned here should match the `outputSchema` properties below.
return { taskId: result.createTask.id, enriched: true };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
workflowActionTriggerSettings: {
label: 'Enrich Company',
icon: 'IconBuilding',
inputSchema: jsonSchemaToInputSchema(inputSchema),
outputSchema: [
{
type: 'object',
properties: {
taskId: { type: 'string' },
enriched: { type: 'boolean' },
},
},
],
},
});
```
بمجرد تثبيت التطبيق، سيظهر **Enrich Company** في منتقّي الإجراءات في منشئ سير العمل. يعرض المُنشئ `companyName` و`domain` كحقول إدخال (كلٌّ منهما قادر على سحب القيم من الخطوات السابقة)، ويمكن للخطوات اللاحقة الرجوع إلى مخرجات الخطوة `taskId` و`enriched`.
<Note>
**اكتب `description` جيدًا.** يعتمد وكلاء الذكاء الاصطناعي على حقل `description` الخاص بالدالة لتحديد وقت استخدام الأداة. كن محددًا بشأن ما تفعله الأداة ومتى ينبغي استدعاؤها.
</Note>
</Accordion>
</AccordionGroup>
<Note>
**مساعدات وقت التشغيل.** يقوم `twenty-sdk/utils` بإعادة تصدير مساعدات صغيرة لوقت التشغيل حتى لا تستورد المعالجات مباشرةً من `twenty-shared`. على سبيل المثال، تُرجِع `isDefined(value)` القيمة `false` لكلٍّ من `null` و`undefined` — استخدمها لتضييق نطاق مُدخلات المعالِجات الاختيارية بأمان، والتي يمكن أن تصل كقيمة `null` أثناء وقت التشغيل حتى عندما تكون مكتوبة كـ `T | undefined`:
```ts
import { isDefined } from 'twenty-sdk/utils';
const handler = async (params: { parentMessageId?: string }) => {
if (isDefined(params.parentMessageId)) {
// params.parentMessageId is narrowed to string here
}
};
```
</Note>
<Note>
**خطافات التثبيت** — معالجات ما قبل التثبيت وما بعد التثبيت وإلغاء التثبيت — تشترك في وقت التشغيل نفسه، ولكن يُصرَّح عنها بدوال تعريف خاصة بها ولا تأخذ إعدادات المشغّلات. راجع [خطافات التثبيت (Install Hooks)](/l/ar/developers/extend/apps/config/install-hooks) لمعرفة `definePreInstallLogicFunction` و `definePostInstallLogicFunction` و `defineUninstallLogicFunction`.
</Note>
## إنشاء نشاط في المخطط الزمني
استخدم `createTimelineActivity()` لنشر حدث مجال صريح من دالة منطقية. عرّف الحدث أولاً باعتباره [نوع نشاط في المخطط الزمني](/l/ar/developers/extend/apps/data/timeline-activity-types)، ثم عالج النوع والكائنات باستخدام معرفاتها العامة الثابتة:
```ts
import { createTimelineActivity } from 'twenty-sdk/logic-function';
await createTimelineActivity({
timelineActivityTypeUniversalIdentifier:
'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
targetRecordId: postCardId,
happensAt: new Date().toISOString(),
properties: {
deliveryProvider: 'postal-service',
},
});
```
يحوّل Twenty المعرفات العامة إلى معرّفات البيانات الوصفية الخاصة بالتثبيت، ويتحقق من أن نوع نشاط المخطط الزمني ينتمي إلى التطبيق المستدعي، ويلتقط لقطة من بيانات العرض الوصفية الخاصة به في النشاط الجديد.
المدخلات المطلوبة هي `timelineActivityTypeUniversalIdentifier` و`targetObjectUniversalIdentifier` و`targetRecordId`. يمكنك أيضاً توفير `happensAt` و`properties` و`workspaceMemberId`. يتحكم `happensAt` في موضع الحدث ووقته المعروض في المخطط الزمني؛ ويكون افتراضياً وقت الإنشاء.
لربط سجل آخر بالحدث، وفّر `linkedRecordId` و`linkedObjectMetadataUniversalIdentifier` معاً. يمكنك بالإضافة إلى ذلك توفير `linkedRecordCachedName` كخيار احتياطي للعرض التاريخي:
```ts
await createTimelineActivity({
timelineActivityTypeUniversalIdentifier:
'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
targetRecordId: postCardId,
linkedObjectMetadataUniversalIdentifier: RECIPIENT_UNIVERSAL_IDENTIFIER,
linkedRecordId: recipientId,
linkedRecordCachedName: recipientName,
});
```
يحتاج دور الدالة المنطقية إلى إذن كتابة على كائن `timelineActivity` القياسي. أبقِ أنواع الأحداث الصريحة غير مرتبطة بـ`action`؛ إذ إن النوع المرتبط بإجراء يتلقى بالفعل أحداث تدقيق تلقائية، وإلا فسينتج صفوفاً مكررة.
## عملاء واجهة برمجة تطبيقات مضبوطة الأنواع (`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` للحصول على تكوين مساحة العمل والتطبيقات ورفع الملفات. يأخذ خيار `runAs` نفسه الذي يأخذه `CoreApiClient` — راجع [أذونات الوصول التي تستخدمها المكالمة](#whose-access-a-call-uses).
```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` | `سلسلة نصية` | اسم الملف (يُستخدم للتخزين والعرض) |
| `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` — مفتاح قصير العمر لأذونات الوصول الافتراضية: دور الشخص متقاطعًا مع دور تطبيقك عندما يكون هناك شخص خلف عملية التشغيل، ودور تطبيقك الخاص عندما لا يكون هناك أحد. الشخص هو من شغّل عملية التشغيل في دالة منطقية، أو من ينظر إلى الصفحة في مكوّن أمامي.
* `TWENTY_APP_APPLICATION_ACCESS_TOKEN` — مفتاح قصير العمر يقتصر نطاقه على دور تطبيقك الخاص وحده. للدوال المنطقية فقط، ويُحقن فيها دائمًا، وهو ما يستخدمه `runAs: 'application'`.
**لا** تحتاج إلى تمرير هذه إلى العملاء — فهم يقرؤون من `process.env` تلقائيًا، ويغطي [أذونات الوصول التي تستخدمها المكالمة](#whose-access-a-call-uses) الاختيار بينها. تُحدَّد أذونات تطبيقك الخاصة بواسطة الدور المُعلن باستخدام `defineApplicationRole()` (أو المشار إليه عبر `defaultRoleUniversalIdentifier` في `application-config.ts`)؛ ولا يمكن لعملية تشغيل تتصرف بصفة شخص أن تتجاوز ذلك الدور أو دوره.
</Note>

View File

@@ -0,0 +1,61 @@
---
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={2}>
<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="plug" href="/l/ar/developers/extend/apps/logic/connections">
بيانات اعتماد OAuth التي يحتفظ بها تطبيقك للخدمات الخارجية — مثل Linear وGitHub وSlack وغيرها.
</Card>
<Card title="مخزن المفاتيح-القيم" icon="قاعدة البيانات" href="/l/ar/developers/extend/apps/logic/key-value-store">
الاحتفاظ بالحالة بين تشغيلات دالة المنطق — ذاكرات التخزين المؤقت، والمؤشرات، والمطالبات عبر مساحات العمل.
</Card>
<Card title="المهام الخلفية" icon="layer-group" href="/l/ar/developers/extend/apps/logic/background-jobs">
ضع في قائمة الانتظار تشغيل دالة منطقية على العمال لتجاوز مهلة كل تشغيل.
</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>

View File

@@ -0,0 +1,182 @@
---
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()`. مرر إما سلسلة `prompt` أو سجل المحادثة `messages`
— وليس كليهما معًا:
```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.',
});
```
لبوتات المحادثة متعددة الجولات (Slack, Discord, Teams, …)، مرر سجل سلسلة المحادثة كـ
`messages` بدلًا من استخدام `prompt` واحد فقط:
```ts src/logic-functions/reply-in-thread.ts
import { runAgent } from 'twenty-sdk/logic-function';
const { result, error, success } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
messages: [
{ role: 'user', content: 'Who owns Acme?' },
{ role: 'assistant', content: 'Sarah owns the Acme account.' },
{ role: 'user', content: 'What was the last touchpoint?' },
],
});
```
النقاط الرئيسية:
* قدِّم **واحدًا فقط** من `prompt` (سلسلة نصية) أو `messages` (من 1 إلى 100 عنصر من `{ role: 'user' | 'assistant', content: string }`).
* يعمل الوكيل **بشكل متزامن** ويمكنه قراءة/تحديث السجلات بنفسه عبر أدواته الخاصة — يتم حل `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>
### التشغيل نيابةً عن أحد أعضاء مساحة العمل
مرر `runAsWorkspaceMemberId` عندما يتم تشغيل التنفيذ بواسطة شخص — مثل روبوت دردشة يجيب على رسالة، مثلاً — لكي يتصرف الوكيل بصفة ذلك العضو بدلًا من التطبيق:
```ts src/logic-functions/answer-question.ts
const { result } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
prompt: 'How many open opportunities do we have?',
runAsWorkspaceMemberId: '20202020-0687-4c41-b707-ed1bfca972a7',
});
```
ثم يعمل التنفيذ باستخدام **دور العضو نفسه**: يمكنه القيام بكل ما يمكن
لذلك العضو القيام به ولا أكثر، وتنسب إليه السجلات التي ينشئها، وتُطبَّق أذوناتُه على مستوى الصف. لا يشارك دور الوكيل — فهو الإعداد الافتراضي للتطبيق لعمليات التنفيذ التي لا يقف خلفها أي شخص. احذف الحقل في عمليات التنفيذ الذاتية (المهام المجدولة، مشغلات أحداث قاعدة البيانات): في هذه الحالة يحتفظ الوكيل بدوره الخاص. يبقى التطبيق الفاعل مرتبطًا بسياق التنفيذ لأغراض التتبّع وإثبات المصدر، دون تقييد أذونات العضو.
تطبيقك مسؤول عن تعيين الشخص الذي شغّل التنفيذ إلى عضو في مساحة العمل. يتطلب تسمية عضو ما رمز وصول للتطبيق، وما يمكن للرمز تسميته يعتمد على ما إذا كان يتضمن مستخدمًا أم لا:
* يمكن لرمز **بدون مستخدم مرفق** أن يسمّي أي عضو. تعمل الدالة المنطقية بدون مستخدم، وكذلك الرموز المُنشأة عبر `client_credentials` أو من مفتاح API.
* لا يمكن لرمز مُصدَر **نيابةً عن مستخدم**، كما يستقبله مكوّن الواجهة الأمامية، أن يسمّي إلا العضو الخاص بذلك المستخدم فقط.
لا يمكن لأي شيء آخر — جلسة مستخدم عادية، أو مفتاح API بدون رمز تطبيق — أن يسمّي عضوًا على الإطلاق.
<Warning>
يقوم `runAgent()` بإطلاق استثناء عندما يتعذّر حل عضو مساحة العمل — مثل عضو غير معروف أو تمت إزالته، أو ليس لديه أي دور. لا يعود إطلاقًا إلى دور الوكيل نفسه، لأن ذلك سيمنح صلاحيات وصول أكثر مما طلبه المستدعي.
</Warning>
</Accordion>
</AccordionGroup>

View File

@@ -0,0 +1,131 @@
---
title: CLI
description: أوامر yarn twenty لتنفيذ الدوال، وبثّ السجلات، وإدارة تثبيتات التطبيقات، والتبديل بين الريموتات.
icon: terminal
---
تُعد واجهة سطر الأوامر `yarn twenty` وسيلتك للتعامل مع كل ما يتعلق بالتطبيق. القائمة الكاملة للأوامر:
| أمر | ماذا يفعل | موثَّق في |
| ----------------------------------------------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| `dev` | يراقب ملفات المصدر ويزامن التغييرات مباشرة | [البدء السريع](/l/ar/developers/extend/apps/getting-started/quick-start) |
| `الخطة` | معاينة تغييرات البيانات الوصفية بدون تطبيقها | [المزامنة والاستعادة](/l/ar/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan) |
| `تطبيق` | تطبيق تغييرات البيانات الوصفية بعد عرض الخطة | [المزامنة والاستعادة](/l/ar/developers/extend/apps/operations/sync-and-recovery) |
| `dev:build` | ترجمة التطبيق وإنشاء عميل واجهة برمجة التطبيقات (استخدم `--tarball` لحزم ملف `.tgz`) | [النشر](/l/ar/developers/extend/apps/operations/publishing) |
| `dev:typecheck` | تشغيل فحص الأنواع في TypeScript | [الاختبار](/l/ar/developers/extend/apps/operations/testing) |
| `dev:add` | إنشاء هيكل كيان جديد | [إنشاء الهياكل](/l/ar/developers/extend/apps/getting-started/scaffolding) |
| `dev:generate-client` | إعادة إنشاء عميل واجهة برمجة التطبيقات مضبوط الأنواع | هذه الصفحة |
| `dev:function:exec` / `dev:function:logs` | تنفيذ الدوال وبث سجلاتها | هذه الصفحة |
| `dev:translations-extract` | استخراج السلاسل القابلة للترجمة إلى كتالوجات `locales/` | [الترجمات](/l/ar/developers/extend/apps/operations/translations) |
| `dev:catalog-sync` | تشغيل مزامنة كتالوج السوق | [النشر](/l/ar/developers/extend/apps/operations/publishing#how-marketplace-discovery-works) |
| `app:publish` / `app:install` / `app:uninstall` | دورة حياة الإصدارات | [النشر](/l/ar/developers/extend/apps/operations/publishing) وهذه الصفحة |
| `docker:*` | إدارة حاوية خادم Twenty المحلي | [الخادم المحلي](/l/ar/developers/extend/apps/getting-started/local-server) |
| `remote:*` | إدارة اتصالات الخادم | هذه الصفحة |
كل أمر يقبل الوسيط `-r, --remote \<name>` لاستهداف خادم بعيد معيَّن بدلاً من الخادم الافتراضي.
## تنفيذ الدوال (`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 install and uninstall hooks
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
yarn twenty dev:function:exec --uninstall
```
## عرض سجلات الدوال (`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>
# Check that the active remote's authentication is still valid
yarn twenty remote:status
# Remove a remote
yarn twenty remote:remove <name>
```
تُخزَّن بيانات اعتمادك في `~/.twenty/config.json`.

View File

@@ -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={2}>
<Card title="CLI" icon="terminal" href="/l/ar/developers/extend/apps/operations/cli">
مرجع `yarn twenty` — exec، logs، uninstall، remotes.
</Card>
<Card title="المزامنة والاستعادة" icon="compass" 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="upload" href="/l/ar/developers/extend/apps/operations/publishing">
البناء، نشر ملف tarball، النشر إلى npm، التثبيت.
</Card>
</CardGroup>

View File

@@ -0,0 +1,293 @@
---
title: النشر
icon: upload
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
```
### مشاركة تطبيق منشور
تطبيقات tarball لا تُدرَج في السوق العامة، لذا لن تكتشفها مساحات العمل الأخرى على الخادم نفسه عبر الاستعراض. لمشاركة تطبيق منشور:
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/`. يعمل CI بدون أي إعداد، بينما يتطلّب CD سرًّا واحدًا فقط، ويتطلّب النشر إلى npm إعدادًا لمرة واحدة لـ npm trusted-publisher.
### 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` سيشغّل المهمّة لذلك الطلب مستخدمًا التزام رأس الطلب، مما يتيح لك التحقّق من التغيير على الخادم المستهدف قبل الدمج.
### النشر — `publish.yml`
ينشر تطبيقك إلى npm مع إثبات المصدر (provenance) عند دفع وسم إصدار (مثلًا: `v1.0.0`)، أو عند تشغيل سير العمل يدويًا من علامة التبويب Actions.
**ماذا يفعل:**
1. يجلب شيفرة تطبيقك، ويُعد Node.js، ويُحدِّث npm (يتطلّب النشر الموثوق npm 11.5.1 أو أحدث).
2. يشغّل `yarn twenty app:publish`، الذي يبني التطبيق وينشر `.twenty/output` إلى npm. في بيئة CI يضيف تلقائيًا `--provenance` و`--access public`، لذا لا حاجة إلى أي أعلام في سير العمل.
**إعداد لمرة واحدة:**
على npmjs.com افتح الحزمة الخاصة بك > **Settings → Trusted Publisher** وسجّل هذا المستودع مع سير عمل `publish.yml` (راجع [وثائق النشر الموثوق من npm](https://docs.npmjs.com/trusted-publishers)). يُصادِق النشر مع بيانات provenance على مستودع GitHub الذي بنى الحزمة، وهو أيضًا ما يتيح لك المطالبة بملكية تطبيقك في سوق Twenty.
<Note>
npm يقبل إثبات المصدر (provenance) فقط من مستودعات المصدر **العامّة**. إذا قمت بالنشر من مستودع خاص، يرفض npm حزمة إثبات المصدر عبر OIDC برسالة `E422 ... خطأ Unsupported GitHub Actions source repository visibility: "private"`. لنشر الحزمة (publish) من مستودع خاص، يمكنك إيقاف استخدام إثبات المصدر (provenance) عبر تعيين `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'` في متغيّر `env` لخطوة النشر (publish step) (تلميح مضاف كتعليق موجود في ملف `publish.yml` المُنشأ تلقائيًا):
```yaml filename=".github/workflows/publish.yml"
- name: Publish to npm
env:
TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'
run: yarn twenty app:publish
```
</Note>
### تثبيت الإجراءات القابلة لإعادة الاستخدام
يشير سيرَا العمل `ci.yml` و`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()` حقولًا اختيارية تتحكم في كيفية ظهور تطبيقك في السوق. استخدم `logo` و`galleryImages` للإشارة إلى الصور من مجلد `public/`:
```ts src/application-config.ts
export default defineApplication({
universalIdentifier: '...',
displayName: 'My App',
description: 'A great app',
logo: 'public/logo.png',
galleryImages: [
'public/screenshot-1.png',
'public/screenshot-2.png',
],
});
```
اطّلع على [أكورديون defineApplication](/l/ar/developers/extend/apps/config/application#marketplace-metadata) في صفحة بناء التطبيقات للاطلاع على القائمة الكاملة لحقول السوق (`author` و`category` و`aboutDescription` و`websiteUrl` و`termsUrl` وغيرها).
#### أبعاد صور المعرض الموصى بها
يعرض السوق `galleryImages` داخل حاوية ثابتة بنسبة `8:5` (على سبيل المثال، `1600×1000 px`).
<Note>
تُعرض صور المعرض بأي نسبة عرض إلى ارتفاع بالكامل ولن يتم اقتطاعها مطلقًا، ولكن أي شيء أطول أو أضيق بكثير من `8:5` سيظهر مساحات فارغة على الجانبين.
</Note>
#### الحد الأقصى لحجم الصورة
يجب ألّا يتجاوز حجم ملف `logo` وكل ملف من `galleryImages` **10 MB**. سيتم تخطي الملفات الأكبر حجمًا عندما يقوم سوق النشر بإعادة استضافة أصولك المنشورة، وبالتالي لن يتم عرضها.
### النشر
```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()` الخاصة بك — راجع قسم [البيانات الوصفية في سوق التطبيقات](#marketplace-metadata) أعلاه.
<Note>
إذا لم يحدد تطبيقك `aboutDescription` في `defineApplication()`، فسيستخدم السوق تلقائيًا ملف `README.md` الخاص بحزمتك من npm كمحتوى لصفحة حول. هذا يعني أنه يمكنك الاحتفاظ بملف README واحد لكل من npm وسوق Twenty. إذا كنت تريد وصفًا مختلفًا في السوق، فقم بتعيين `aboutDescription` بشكل صريح.
</Note>
### النشر عبر CI
ينشر سير عمل `publish.yml` المُنشأ تلقائيًا والموصوف أعلاه إلى npm تلقائيًا عند علامات الإصدارات، مع بيانات provenance. نظرًا لأن `yarn twenty app:publish` يضيف `--provenance` و`--access public` نيابةً عنك عند تشغيله في CI، فلا يحتاج سير العمل إلى أي أعلام npm — فقط إعداد الناشر الموثوق لمرة واحدة.
بالنسبة لأنظمة CI الأخرى (GitLab CI وCircleCI وغيرها)، شغّل `yarn install` ثم `yarn twenty app:publish`. يُصدِر بيانات provenance عندما تتمكّن البيئة من إصدار رمز OIDC، ويتم تخطيه تلقائيًا خلاف ذلك.
<Note>
تضيف **npm provenance** شارة ثقة إلى إدراجك على npm، مما يتيح للمستخدمين التحقّق من أن الحزمة تم بناؤها من التزام محدد ضمن خط أنابيب CI عام. وهي أيضًا ما يتيح لك المطالبة بملكية تطبيقك في سوق Twenty. راجع [وثائق 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>

View File

@@ -0,0 +1,126 @@
---
title: المزامنة والاستعادة
description: أي أمر تستخدمه ومتى، وكيفية قراءة مخرجات المزامنة، وسلّم استعادة لما يجب فعله عندما تنحرف البيانات الوصفية المحلية — قبل الوصول إلى إعادة تعيين كاملة.
icon: compass
---
يدور تطوير التطبيقات محليًا حول **المزامنة**: يقوم الـ CLI بإعادة إنشاء ملف manifest الخاص بك ويطبّق الخادم فقط الفرق بينه وبين البيانات الوصفية الموجودة بالفعل في مساحة العمل لديك. تغطي هذه الصفحة الأمر الذي ينبغي استخدامه، وكيفية قراءة ما غيّرته المزامنة، وما الذي يجب فعله — بالترتيب — عندما تبدو الحالة المحلية غير متسقة.
## أي أمر، ومتى
<Note>
للتكرار اليومي المحلي ستحتاج تقريبًا دائمًا إلى `yarn twenty dev`. يُستخدم النشر والإصدار لإطلاق الإصدارات، **وليس** للحلقة المحلية.
</Note>
| ترغب في… | أمر | الملاحظات |
| ------------------------------------------------ | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| التكرار محليًا مع المزامنة الحية | `yarn twenty dev` | يراقب ملفاتك ويجري مزامنة عند كل تغيير. |
| مزامنة واحدة ثم إنهاء (CI، السكربتات، الخطّافات) | `yarn twenty apply` | عملية إنشاء واحدة + مزامنة، ثم إنهاء. أضف `--force` لتخطي تأكيد التغيير التدميري. |
| معاينة التغييرات **بدون تطبيقها** | `yarn twenty plan` | يحتسب الفرق ويطبعه؛ ولا يكتب أي شيء. |
| إزالة التطبيق من مساحة العمل | `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` | يحذف **كل** البيانات المحلية — كملاذ أخير. |
<Note>
الأوامر `yarn twenty dev --once` و`yarn twenty dev --once --dry-run` هي أسماء بديلة مهملة للأمرين `yarn twenty apply` و`yarn twenty plan`.
</Note>
### لا تحتاج المزامنة المحلية إلى زيادة في الإصدار
تنطبق قاعدة `version` المتزايدة بدقة (`VERSION_ALREADY_EXISTS` عند النشر، و`APP_ALREADY_INSTALLED` / `CANNOT_DOWNGRADE_APPLICATION` عند التثبيت) على **`app:publish` / `app:install`** — مسار الإصدارات. يقوم `yarn twenty dev` بمزامنة ملف manifest في مكانه ولا يتطلّب تغيير الإصدار أبدًا، لذا لست بحاجة إلى تعديل `package.json` للتكرار. إذا وجدت نفسك تزيد الإصدار لاختبار تغيير محلي، فأنت تستخدم مسار الإصدارات بينما ما تريده هو حلقة التطوير.
## قراءة مخرجات المزامنة
كل عملية مزامنة تطبع تغييرات البيانات الوصفية التي تم تطبيقها (أو التي سيتم تطبيقها عند استخدام `plan`)، على غرار Terraform — كتلة واحدة لكل كيان مع خصائصه، ثم سطر ملخص:
```text filename="Terminal"
# objectMetadata "rocket" will be created
+ icon = "IconRocket"
+ labelSingular = "Rocket"
+ ...
# fieldMetadata "launchedAt" will be updated
~ isNullable = false -> true
Plan: 2 to add, 1 to change, 1 to destroy.
✓ Synced My App (4 files)
```
هذه أداتك الأولى للتشخيص: تُخبرك بدقة ما الكائنات والحقول والتخطيطات التي تغيّرت، بحيث يمكنك التأكد من أن المزامنة أنجزت ما توقّعته قبل التحقّق من واجهة المستخدم.
التغييرات التدميرية (`to destroy`) تُدرج مع ما تقوم بحذفه (على سبيل المثال `objectMetadata "auditNote" — drops the table and all its rows`) وتتطلب تأكيدًا تفاعليًا، أو استخدام `--force` في السكربتات.
عندما تفشل المزامنة على كيان واحد، يذكر الخطأ اسم الكيان المسبب للمشكلة و`universalIdentifier` الخاص به، على سبيل المثال:
```text
Migration action 'create' for 'fieldMetadata' (universalIdentifier: 2020...4337) failed
```
استخدم ذلك المعرّف للعثور على الكيان في ملف manifest الخاص بك (وإن لزم الأمر، في مساحة العمل) بدلًا من تخمين أيّها يتعارض.
## معاينة التغييرات (plan)
يقوم `yarn twenty plan` ببناء ملف manifest الخاص بك، ويطلب من الخادم خطة الترحيل، ويطبعها — **بدون تطبيق أي شيء**. إنها الطريقة الآمنة للإجابة عن سؤال "ما الذي ستغيّره هذه المزامنة؟" قبل الالتزام بها.
```bash filename="Terminal"
yarn twenty plan
```
```text filename="Terminal"
Building manifest...
Computing metadata plan (read-only, nothing will be applied)...
# fieldMetadata "crewCapacity" will be created
+ ...
Plan: 1 to add, 1 to change, 0 to destroy.
✓ Plan complete for My App — no changes were applied
```
خطة:
* **لا يكتب أي شيء** — لا ترحيل لبيانات وصفية، ولا تحديث لسجل التطبيق، ولا تغييرات في الأدوار/التبويبات الافتراضية، ولا توليد لعميل API.
* يُرجع **نفس الفرق** الذي ستُطبِّقه مزامنة حقيقية، حتى تتمكن من مراجعة الكيانات التي سيتم إنشاؤها/تحديثها/حذفها مسبقًا.
* يكون مفيدًا قبل إجراء تغيير محفوف بالمخاطر، أو عند مراجعة تغيير تم إنشاؤه بواسطة الذكاء الاصطناعي، أو في سكربت يجب أن يفشل إذا كان تغيير غير متوقَّع على وشك الحدوث.
<Note>
تقوم الخطة فقط بمعاينة **تغييرات البيانات الوصفية**، ويتطلّب ذلك أن يكون التطبيق قد تمت مزامنته مرة واحدة على الأقل (حتى تعرف به مساحة العمل). إذا شغّلته ضد تطبيق لم تتم مزامنته من قبل، سيبلغ الخادم أن التطبيق غير مُثبّت — شغّل `yarn twenty dev` مرة واحدة أولًا.
</Note>
## سلّم الاستعادة
عندما تبدو البيانات الوصفية المحلية غير صحيحة، صعِّد الإجراءات بهذا الترتيب وتوقّف بمجرد زوال العائق. كل خطوة أكثر إرباكًا من التي قبلها.
1. **أعد المزامنة.** شغّل `yarn twenty apply` مرة أخرى. عمليات المزامنة متطابِقة الأثر (idempotent) — إعادة تشغيل ملف manifest النظيف آمنة وغالبًا ما تحل تعثرًا عابرًا.
2. **عاين الخطة.** شغّل `yarn twenty plan` لرؤية ما الذي تنوي المزامنة التالية تغييره بالضبط، بدون تطبيقه.
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`؛ أصلِح البيانات الوصفية المتعارِضة.
* **خطأ في حجم التبعيات** — تفشل عملية المزامنة أو التثبيت لأن `dependencies` الخاصة ببيئة الإنتاج في التطبيق كبيرة جدًا بحيث لا يمكن تثبيتها كطبقة وقت التشغيل (`LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED`)؛ انقل الحزم التي لا تستوردها دوال المنطق لديك في وقت التشغيل (مكتبات واجهة المستخدم وأدوات التطوير) إلى `devDependencies`.
* **خطأ في وقت تشغيل كود التطبيق** — تتم المزامنة بنجاح، ولكن دوال المنطق أو المكوّنات لديك لا تعمل بشكل صحيح أثناء وقت التشغيل؛ تحقّق من [سجلات الدوال](/l/ar/developers/extend/apps/operations/cli).
* **حالة المثيل المحلي** — لا ينطبق أيّ مما سبق وما زالت مساحة العمل تبدو غير صحيحة؛ تابع النزول في سلّم الاستعادة.

View File

@@ -0,0 +1,264 @@
---
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 التي تعمل في بيئة المتصفّح. لاحظ أن الـ sandbox يطبّق نموذج DOM *جزئيًا*، لذلك قد تتمكّن الحزمة من البناء بنجاح ولكنها قد تفشل أثناء وقت التشغيل؛ راجع [القيود الحالية](/l/ar/developers/extend/apps/layout/front-components#current-limitations).
كلتا البيئتين تحتويان على `twenty-client-sdk/core` و`twenty-client-sdk/metadata` كوحدات متاحة مُسبقًا — لا تُضمَّن هذه ضمن الحزم بل تُحلّ وقت التشغيل بواسطة الخادم.
يمكن للمكوّنات الأمامية أيضًا مشاركة التبعيات من خلال [حزمة التبعيات المشتركة](/l/ar/developers/extend/apps/layout/front-components#sharing-dependencies-across-front-components). تُبنَى الحزم المدرجة في حقل `frontComponentSharedDependencies` في ملف package.json الخاص بالتطبيق مرة واحدة في ملف منفصل ويتم تحميلها منه أثناء وقت التشغيل بدلًا من تضمينها داخل كل مكوّن، لذا فإن المكوّن الذي يستورد حزمًا مشتركة فقط لا يحمل تقريبًا أي شيفرة تبعيات خاصة به.
## إعداد
يتضمّن التطبيق المُولَّد بالقالب بالفعل 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';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? '<the pre-seeded local dev key>';
// Make env vars available to globalSetup (test.env only applies to workers)
process.env.TWENTY_API_URL = TWENTY_API_URL;
process.env.TWENTY_API_KEY = TWENTY_API_KEY;
export default defineConfig({
plugins: [
tsconfigPaths({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
fileParallelism: false,
include: ['src/**/*.integration-test.ts'],
globalSetup: ['src/__tests__/global-setup.ts'],
env: {
TWENTY_API_URL,
TWENTY_API_KEY,
},
},
});
```
أنشئ ملف إعداد عام يتحقق من إمكانية الوصول إلى الخادم، ويكتب ملف إعداد اختبار لـ SDK (`~/.twenty/config.test.json`)، ويزامن التطبيق قبل تشغيل الاختبارات:
```ts src/__tests__/global-setup.ts
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { appDevOnce, appUninstall } from 'twenty-sdk/cli';
const APP_PATH = process.cwd();
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
export async function setup() {
const apiUrl = process.env.TWENTY_API_URL!;
const apiKey = process.env.TWENTY_API_KEY!;
// Verify the server is running
const response = await fetch(`${apiUrl}/healthz`);
if (!response.ok) {
throw new Error(`Twenty server is not reachable at ${apiUrl}.`);
}
// Write the SDK's test config (the CLI reads config.test.json when NODE_ENV=test)
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(
path.join(CONFIG_DIR, 'config.test.json'),
JSON.stringify({
remotes: { local: { apiUrl, apiKey } },
defaultRemote: 'local',
}, null, 2),
);
// Start from a clean slate, then sync the app
await appUninstall({ appPath: APP_PATH }).catch(() => {});
const result = await appDevOnce({ appPath: APP_PATH });
if (!result.success) {
throw new Error(`Dev sync failed: ${result.error?.message}`);
}
}
export async function teardown() {
await appUninstall({ appPath: APP_PATH });
}
```
## واجهات SDK البرمجية
يُصدِّر المسار الفرعي `twenty-sdk/cli` دوالًا يمكنك استدعاؤها مباشرةً من شيفرة الاختبار:
| دالة | الوصف |
| -------------- | ------------------------------------------------------------------- |
| `appBuild` | بناء التطبيق واختياريًا حزم ملف tarball |
| `appDeploy` | رفع ملف tarball إلى الخادم |
| `appDevOnce` | بناء التطبيق ومزامنته مرة واحدة (نفس الأمر مثل `yarn twenty apply`) |
| `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` على ملف `tsconfig.json` الخاص بتطبيقك ويبلغ عن أي أخطاء في الأنواع. كما تتضمن التطبيقات المُنشأة بالهيكل برنامج نصي `yarn typecheck` يشمل ملفات الاختبار (`tsconfig.spec.json`) أيضًا.
## التكامل المستمر (CI) باستخدام GitHub Actions
تولّد أداة إنشاء الهيكل سير عمل جاهزًا للاستخدام في `.github/workflows/ci.yml`. عند كل دفع إلى الفرع `main` وكل طلب سحب، تُنشئ الأداة خادم Twenty مؤقتًا في بيئة التشغيل (عبر الإجراء `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test`)، ثم تشغِّل الأوامر `yarn lint` و`yarn typecheck` و`yarn test:unit` و`yarn test` مع ضبط المتغيرين `TWENTY_API_URL` و`TWENTY_API_KEY` للإشارة إلى ذلك الخادم. لا تُطلَب أي أسرار، ويمكنك تثبيت إصدار الخادم عبر متغير البيئة `TWENTY_VERSION` في أعلى سير العمل.
راجع قسم [النشر → التكامل/التسليم المستمران الآليان](/l/ar/developers/extend/apps/operations/publishing#automated-cicd-scaffolded-workflows) للاطلاع على شرح كامل لثلاثة مسارات العمل التي تم إنشاؤها تلقائياً (`ci.yml`، وخط أنابيب النشر `cd.yml`، و`publish.yml` للنشر على npm).

View File

@@ -0,0 +1,127 @@
---
title: الترجمات
description: قدّم تطبيقك بعدة لغات — ترجم تسميات ملف البيان وسلاسل واجهة المستخدم عبر كتالوج إعدادات محلية واحد.
icon: language
---
تُكتب تطبيقات Twenty باللغة **الإنجليزية**: السلاسل في شفرتك وملف البيان هي نص المصدر بالإنجليزية، و`en` هي الإعداد المحلي الذي تترجم منه، وأي إعداد محلي يُترك دون ترجمة يعود إليه افتراضيًا.
يحتوي تطبيقك على نوعين من النصوص القابلة للترجمة، وكلاهما يمر عبر نفس
كتالوج `locales/`:
* **تسميات ملف البيان** — أسماء الكائنات والحقول، وعناوين العروض، وعناصر القوائم، وسلاسل أخرى مُعلَنة في بيانات التعريف لتطبيقك.
* **سلاسل الواجهة الأمامية (front-component)** — نص واجهة المستخدم الذي تقوم مكوّنات React الأمامية لديك بعرضه.
تضع علامات على السلاسل القابلة للترجمة، وتستخرجها إلى كتالوجات لكل إعداد محلي، ثم تترجم هذه الكتالوجات، وتتولّى عملية البناء تقديم اللغة الصحيحة للمستخدم الحالي — دون أي إعداد إضافي.
## وضع علامات على سلاسل الواجهة الأمامية (front-component)
قم باستيراد أدوات الترجمة من `twenty-sdk/front-component`:
```tsx
import { Trans, t, msg, useTranslate } from 'twenty-sdk/front-component';
const STATUSES = [
{ id: 'draft', label: msg('Draft') },
{ id: 'sent', label: msg('Sent') },
];
const Card = ({ count, name }: { count: number; name: string }) => {
const { t } = useTranslate();
return (
<section>
{/* Static text — reactive to the user's locale */}
<Trans>Loading postcard…</Trans>
{/* Disambiguate identical sources with a context */}
<Trans context="card-title">Untitled</Trans>
{/* Interpolation: pass values explicitly */}
<p>{t('Hi {name}', { name })}</p>
<p>{t('Saved {count} cards', { count })}</p>
{/* Resolve a lazily-declared descriptor */}
<ul>{STATUSES.map((s) => <li key={s.id}>{t(s.label)}</li>)}</ul>
</section>
);
};
```
### متى تستخدم أيًّا منها
* **`Trans`** — نص ثابت في JSX. استخدم الخاصيتين `message` و`values` من أجل الاستبدال (interpolation)، كما في `Trans message="Hi {name}" values={{ name }}`؛ لا يمكن استخراج الاستبدال مباشرة في العناصر الفرعية (children) بشكل ثابت.
* **`useTranslate().t`** — سلاسل ديناميكية داخل المكوّن. يُعيد التصيير عندما يغيّر المستخدم اللغة. يفضّل استخدامه داخل التصيير (render).
* **`t(...)`** (مستوردة مباشرة) — ترجمة فورية (eager) يمكن استخدامها **في أي مكان**،
بما في ذلك معالجات الأحداث (event handlers)، والبرامج المساعدة (helpers)، ونطاق الوحدة (module scope) — وليس فقط داخل التصيير.
* **`msg(...)`** — واصف مؤجَّل للسلاسل المُعلَنة كبيانات (ثوابت، التهيئة (config)). قم بحلّه لاحقًا باستخدام `t(descriptor)`.
### السياق
مرِّر `context` لتمييز سلاسل المصدر المتطابقة التي تُترجَم بشكل مختلف:
```tsx
t({ message: 'Open', context: 'door' });
t({ message: 'Open', context: 'window' });
<Trans context="card-title">Untitled</Trans>
```
تظهر السلاسل السياقية في كتالوجات اللغة المحلية كمجموعة مفهرسة حسب سياقها:
`"\<context>": { "\<message>": "\<translation>" }`. تكون تسميات الـ Manifest سياقية بطبيعتها — إذ تحمل كل تسمية دورها في البيانات الوصفية (`objectMetadata.labelSingular`، `fieldMetadata.label`، `view.name`، …)، لذلك يمكن أن تُترجَم الكلمة نفسها بشكل مختلف عندما تكون اسم كائن وعندما تكون اسم عرض.
## الاستخراج والترجمة
شغِّل أمر الاستخراج من دليل تطبيقك:
```bash
twenty dev:translations-extract # collect strings into locales/en.json
twenty dev:translations-extract --locale fr-FR # also scaffold a target locale
```
يقوم الاستخراج بجمع كل من تسميات ملف البيان (manifest labels) وسلاسل `t()`/`msg()`/`Trans`
من شفرة الواجهة الأمامية (front-component) في `locales/\<locale>.json`، مفهرسة حسب سلسلة المصدر. املأ الترجمات:
```json
// locales/fr-FR.json
{
"Loading postcard…": "Chargement de la carte…",
"Hi {name}": "Bonjour {name}",
"Saved {count} cards": "{count} cartes enregistrées",
"door": {
"Open": "Ouvrir"
},
"objectMetadata.labelSingular": {
"Postcard": "Carte postale"
}
}
```
السلاسل غير المرتبطة بسياق هي مدخلات مسطَّحة؛ أما السلاسل المرتبطة بسياق — بما في ذلك كل تسمية في ملف manifest — فهي متداخلة تحت مفتاح السياق الخاص بها.
العناصر النائبة مثل `{name}` تُستبدَل وقت التشغيل — احتفظ بها في
الترجمة. أي سلسلة تُترَك فارغة تعود إلى نص المصدر.
## كيفية التشغيل
يقوم `twenty dev:build` بتجميع الكتالوجات وتقديم اللغة الصحيحة للمستخدم
الحالي: يتم حل تسميات ملف البيان (manifest labels) على جانب الخادم، ويتم تضمين كتالوجات الواجهة الأمامية (front-component)
داخل كل حزمة مكوّن. وقت التشغيل يقرأ المكوّن
اللغة (locale) من سياق التنفيذ الخاص به (لغة المضيف الحالية) ويحل كل سلسلة مقابل كتالوجه، مع الرجوع إلى المصدر عندما تكون الترجمة مفقودة. تغيير اللغة في المضيف يعيد تصيير سلاسل `Trans` و
`useTranslate().t` مباشرة.
تُصرَّف تسميات Manifest عند كل مزامنة — `twenty dev:build` و`twenty apply`
وكذلك المراقبة المستمرة `twenty dev` — لذا يكفي تعديل ملف لغة
والمزامنة لرؤية النص الجديد. تُضمَّن كتالوجات مكوّنات Front في
كل حزمة مكوّنات وقت البناء، لذا فإن تغيير أحدها لا يزال يعني
إعادة تشغيل `twenty dev:build` (وإعادة النشر).
قد تمتد عناصر النص التابعة لـ `Trans` على عدة أسطر — يتم ضغط المسافات
بنفس الطريقة التي يقوم JSX بضغطها، لذلك كلاهما يتم استخلاصه إلى المفتاح `Welcome back`:
```tsx
<Trans>Welcome back</Trans>
<Trans>
Welcome
back
</Trans>
```

View File

@@ -0,0 +1,80 @@
---
title: 5. An AI agent
icon: robot
description: السماح لوكيل بتوليد مستندات من محادثة، باستخدام أداتك.
---
لأن `وثيقة توليد الطاقة` مكشوفة كـ **أداة**، يمكن لوكيل الذكاء الاصطناعي أن يطلق عليها.
دعونا نضيف وكيلا ومهارة حتى يمكن للمستخدمين أن يقولوا فقط *"إنشاء اقتراح لـ
Jeffery Griffin"*.
## المهارة
A [skill](/l/ar/developers/extend/apps/logic/skills-and-agents) قابل لإعادة الاستخدام- المعرفة التي تربطها بالوكلاء. علمنا النموذج كيفية استخدام
الأداة.
```ts filename="src/skills/document-drafting.skill.ts"
import { defineSkill } from 'twenty-sdk/define';
export default defineSkill({
universalIdentifier: DOCUMENT_SKILL_UNIVERSAL_IDENTIFIER,
name: 'document-drafting',
label: 'Document drafting',
icon: 'IconFileText',
content: [
'To generate a document, call the `generate-document` tool with:',
'- `templateId`: the id of the document template to use.',
'- `recordId`: the id of the Person or Company the document is for.',
'',
'If the user names a template or person instead of an id, find the record first,',
'then pass its id. Make sure the template target matches the record type.',
].join('\n'),
});
```
## الوكيل
[agent](/l/ar/developers/extend/apps/logic/skills-and-agents) يزوج موجه مع نموذج
. تعيين 'استمارة الاستجابة' بشكل صريح لتجنب تحذير البناء.
```ts filename="src/agents/document-assistant.agent.ts"
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: DOCUMENT_AGENT_UNIVERSAL_IDENTIFIER,
name: 'document-assistant',
label: 'Document Assistant',
description: 'Generates documents from your templates and CRM records.',
icon: 'IconFileText',
responseFormat: { type: 'text' },
prompt: [
'You are the Document Assistant for a CRM.',
'You help users generate personalized documents from reusable templates',
'and the data already in their CRM. Use the generate-document tool, and',
'always confirm what you created.',
].join(' '),
});
```
<Note>
ولا يمكن للوكيل الاتصال بالأداة إلا إذا كان دوره يسمح بذلك. لقد قمنا بالفعل بتعيين
'cancessAllTools: true' و 'canBeAssignedToAgents: true' على دور التطبيق في
[الفصل 2](/l/ar/developers/extend/apps/tutorials/document-generator/generating-documents#grant-it-access).
</Note>
## جرب ذلك
افتح محادثة مع **مساعد مستندات** واطلب منها إعداد وثيقة لشخص
في إدارة علاقات العملاء الخاصة بك. يجد السجل، يستدعي `generate-document`، ويُرجِع
المستند الذي أنشأه — الذي يظهر الآن في عرض **Documents** لديك،
تمامًا مثل مسارات قائمة الأوامر وسير العمل.
هذه هي ثمرة إتاحة المنطق كأداة: **دالة واحدة، وكثير من نقاط الدخول** —
قائمة الأوامر، وHTTP، وخطوة سير العمل، والآن اللغة الطبيعية.
**بعد هذه الخطوة:** التطبيق كامل الميزة ومفيد حقا. حان الوقت
لإطلاقه.
<Card title="التالي: النشر →" icon="rocket" href="/l/ar/developers/extend/apps/tutorials/document-generator/النشر">
إضافة بيانات التعريف للسوق والنشر.
</Card>

View File

@@ -0,0 +1,297 @@
---
title: 4. بناء واجهة المستخدم
icon: table-columns
description: المشاهدة، الملاحة الجانبية، الأوامر، والمكونات الأمامية.
---
الآن يمكن الوصول إلى الكائنات فقط من خلال الإعدادات. لنمنح التطبيق حضورًا حقيقيًا في واجهة المستخدم: طرق عرض القوائم، وعناصر في الشريط الجانبي، وأمر **إنشاء مستند** بنقرة واحدة، ومكوّن واجهة أمامية في صفحة السجل من أجل **معاينة** مستند، وعلامة تبويب **محرر** نص منسق أصلية للقوالب.
## وجهات النظر والملاحة
A [view](/l/ar/developers/extend/apps/layout/views) هي قائمة محفوظة لكائن معين.
ويضع [عنصر قائمة التنقل](/l/ar/developers/extend/apps/layout/navigation-menu-items)
هذا العرض في الشريط الجانبي. يوفر الخادم بالفعل طريقة العرض الافتراضية "All ..." لكل كائن تلقائيًا، لذا فإن طرق العرض التي يعرّفها التطبيق تكون إضافية ومُنتقاة بعناية.
```ts filename="src/views/documents.view.ts"
import { defineView } from 'twenty-sdk/define';
export default defineView({
universalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
name: 'All documents',
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconFile',
position: 0,
fields: [
{ universalIdentifier: DOCUMENTS_VIEW_NAME_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0, isVisible: true, size: 280 },
{ universalIdentifier: DOCUMENTS_VIEW_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
position: 1, isVisible: true, size: 120 },
{ universalIdentifier: DOCUMENTS_VIEW_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
position: 2, isVisible: true, size: 200 },
],
});
```
```ts filename="src/navigation-menu-items/documents.navigation-menu-item.ts"
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
export default defineNavigationMenuItem({
universalIdentifier: DOCUMENTS_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
name: 'Documents',
icon: 'IconFile',
color: 'green',
position: 1,
type: NavigationMenuItemType.VIEW,
viewUniversalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
});
```
أضف نفس زوج للنماذج. ويظهر كلاهما الآن في الشريط الجانبي:
<Frame caption="المستندات والقوالب في الشريط الجانبي، مع إدراج الوثيقة التي تم إنشاؤها.">
<img src="/images/docs/developers/extends/apps/document-Generator/04-documents-view.png" alt="المستندات المعروضة مع مستند تم إنشاؤه" />
</Frame>
## مكون أمامي
[مكون أمامي](/l/ar/developers/extend/apps/layout/front-components) هو مكون React
مربع رملي داخل عشرين. يقرأ السجل المحدد، يحمّل قوالب الشخص
عبر `CoreApiClient`، و POSTs إلى المسار من الفصل
الأخير.
```tsx filename="src/front-components/generate-document-form.front-component.tsx"
import { useEffect, useState } from 'react';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { enqueueSnackbar, useSelectedRecordIds } from 'twenty-sdk/front-component';
const GenerateDocumentForm = () => {
const selectedRecordIds = useSelectedRecordIds();
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
const [templates, setTemplates] = useState<{ id: string; name: string }[]>([]);
const [templateId, setTemplateId] = useState('');
useEffect(() => {
new CoreApiClient()
.query({ documentTemplates: {
__args: { filter: { target: { eq: 'PERSON' } }, first: 100 },
edges: { node: { id: true, name: true } } } })
.then(({ documentTemplates }) => {
const list = documentTemplates?.edges?.map((e) => e.node) ?? [];
setTemplates(list);
if (list[0]) setTemplateId(list[0].id);
});
}, []);
const generate = async () => {
const res = await new RestApiClient().post<{ success: boolean }>(
'/s/documents/generate',
{ templateId, recordId },
);
await enqueueSnackbar({
message: res.success ? 'Document generated.' : 'Generation failed.',
variant: res.success ? 'success' : 'error',
});
};
// ...render a <select> of templates and a Generate button
};
export default defineFrontComponent({
universalIdentifier: GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: 'generate-document-form',
component: GenerateDocumentForm,
});
```
<Warning>
النمط مع متغيرات CSS المضمنة (`var(--t-color-blue)`)، ليس القيم المستوردة من
`XXui`. تمزق SDK تلك الحزمة أثناء البناء، لذا فإن واردات مستوى الوحدة النمطية من ثوابت السمة
ستكون 'غير محددة\`. انظر
[الكامل المكون](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/front-components/generate-document-form.front-component.tsx).
</Warning>
## أمر لفتحه
[أمر قائمة](/l/ar/developers/extend/apps/layout/command-menu-items) مع
\`available ityType: 'RECORD_SELECTION'' يظهر عند اختيار شخص، و
يفتح المكون في اللوحة الجانبية.
```ts filename="src/command-menu-items/generate-document.command-menu-item.ts"
import { defineCommandMenuItem, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: GENERATE_DOCUMENT_COMMAND_UNIVERSAL_IDENTIFIER,
label: 'Generate document',
availabilityType: 'RECORD_SELECTION',
availabilityObjectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
frontComponentUniversalIdentifier:
GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
});
```
## جرب التدفق بأكمله
افتح **People**، حدِّد شخصًا، ثم اضغط <kbd>⌘K</kbd> / <kbd>Ctrl K</kbd>.
يظهر "إنشاء مستند"، وسم مع التطبيق الخاص بك:
<Frame caption="يظهر الأمر عندما يتم اختيار شخص.">
<img src="/images/docs/developers/extends/apps/document-generator/06-command-menu.png" alt="قائمة الأوامر مع توليد المستند" />
</Frame>
قم بتشغيله – يفتح مكونك في اللوحة الجانبية. اختيار قالب، انقر
**إنشاء**، وسجل جديد في **الوثائق**.
<Frame caption="المكون الأمامي، تحميل القوالب والتوليد على النقرة.">
<img src="/images/docs/developers/extends/apps/document-Generator/06b-front-component.png" alt="إنشاء لوحة وثيقة جانبية" />
</Frame>
يسجل كل مستند تم إنشاؤه التطبيق الخاص بك كمؤلف:
<Frame caption="تم إنشاؤها من قبل مولد المستندات، الحالة التي تم إنشاؤها.">
<img src="/images/docs/developers/extends/apps/document-generator/05-document-record.png" alt="سجل مستند تم إنشاؤه" />
</Frame>
## معاينة مستند في صفحة السجل الخاصة به
المكون الأمامي ليس فقط لقوائم الأوامر - يمكنك تحميل واحد كعلامة تبويب \*\*على صفحة تسجيل
\*\*. دعونا نضيف علامة تبويب *Preview* إلى سجل المستند الذي يجعل جسم
Markdown كصفحة مصقولة قابلة للطباعة.
ويقرأ المكون معرف السجل الحالي من سياق التنفيذ، ويحمل المستند
ويضعه. تعمل المكوّنات الأمامية ضمن **sandbox** لا يسمح إلا بقائمة محددة من علامات HTML — يتم حظر حقن HTML الخام (`dangerouslySetInnerHTML`) وعنصر `\<style>` — لذلك نقوم بعرض Markdown كعناصر React مع أنماط مضمنة عبر مساعد [`Markdown`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/markdown-to-react.tsx) صغير.
```tsx filename="src/front-components/document-viewer.front-component.tsx"
import { CoreApiClient } from 'twenty-client-sdk/core';
import { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { useFrontComponentExecutionContext } from 'twenty-sdk/front-component';
import { Markdown } from 'src/utils/markdown-to-react';
const DocumentViewer = () => {
const recordId = useFrontComponentExecutionContext((c) => c.recordId ?? null);
// ...load { content, file } for recordId, then derive the links:
const pdfUrl = document.file?.[0]?.url;
const webUrl = new RestApiClient().resolveUrl('/s/documents/view', {
query: { id: recordId },
});
// Render the template body, plus quick links to the web page and the PDF.
// Links open in a new tab so they don't navigate the embedded component.
return (
<div style={styles.scroll}>
<div style={styles.actions}>
<a style={styles.actionLink} href={webUrl} target="_blank" rel="noopener noreferrer">
Open web page
</a>
{pdfUrl ? (
<a style={styles.actionLink} href={pdfUrl} target="_blank" rel="noopener noreferrer">
Download PDF
</a>
) : null}
</div>
<div style={styles.paper}>
<div style={styles.body}>
<Markdown content={document.content} />
</div>
</div>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: 'document-viewer',
component: DocumentViewer,
});
```
قم بتحميلها مع [تخطيط الصفحة](/l/ar/developers/extend/apps/layout/page-layouts). يضيف تخطيط
`RECORD_PAGE` علامات تبويب إلى عرض سجل الكائن؛ تستضيف علامة تبويب تحتوي على أداة
`FRONT_COMPONENT` واحدة المكوّن بكامل العرض:
```ts filename="src/page-layouts/document-record.page-layout.ts"
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
export default definePageLayout({
universalIdentifier: DOCUMENT_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
name: 'Document record page',
type: 'RECORD_PAGE',
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
tabs: [{
universalIdentifier: DOCUMENT_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
title: 'Preview',
icon: 'IconEye',
position: 50,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [{
universalIdentifier: DOCUMENT_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
title: 'Document preview',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
}],
}],
});
```
افتح أي مستند – علامة تبويب **المعاينة** تجعله جميلا، مع روابط إلى صفحة ويب
القابلة للمشاركة و PDF:
<Frame caption="علامة التبويب المعاينة تجعل المستند بأنماط مضمنة، بالإضافة إلى روابط سريعة.">
<img src="/images/docs/developers/extends/apps/document-generator/09-document-viewer.png" alt="عنصر واجهة عارض المستند في علامة تبويب صفحة التسجيل" />
</Frame>
## تحرير قالب مع محرر النص الغني
القوالب لا تحتاج إلى عنصر مخصص على الإطلاق. لأن `body' هو حقل
`RICH_TEXT' ، يقدم عشرون محرر نص ثري كامل له -
نفس المحرر القياسي للمذكرة وعناصر المهمة. نحن فقط نسطحه على صفحة سجل القالب
إضافة علامة تبويب مع عنصر واجهة المستخدم 'FIELD' في وضع العرض 'EDITOR' ، مع الإشارة إلى حقل 'body'
عبر 'field MetadataId':
```ts filename="src/page-layouts/template-record.page-layout.ts"
{
universalIdentifier: TEMPLATE_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
title: 'Template',
position: 1,
layoutMode: PageLayoutTabLayoutMode.GRID,
widgets: [{
universalIdentifier: TEMPLATE_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
title: 'Template',
type: 'FIELD',
position: { layoutMode: PageLayoutTabLayoutMode.GRID, row: 0, column: 0, rowSpan: 6, columnSpan: 12 },
configuration: {
configurationType: 'FIELD',
fieldMetadataId: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
fieldDisplayMode: 'EDITOR',
},
}],
}
```
يخزن حقل "RICH_TEXT" كتلة المحرر JSON وإسقاط Markdown
على حد سواء. خط أنابيب التوليد يقرأ إسقاط الـ Markdown هذا، لذا فإن العناصر النائبة وملف PDF وصفحة الويب القابلة للمشاركة تستمر جميعها في العمل دون تغيير — راجع الملف الكامل [`template-record.page-layout.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/page-layouts/template-record.page-layout.ts).
تدعم عناصر واجهة المستخدم `FIELD` أوضاع عرض وخيارات أخرى أيضًا، بما في ذلك
سرد سجلات علاقة متداخلة على بعد مستويين — راجع
[مرجع عنصر واجهة مستخدم الحقل](/l/ar/developers/extend/apps/layout/page-layouts#field-widgets).
الآن المحررون يكتبون قوالب في محرر نص غني مناسب:
<Frame caption="علامة التبويب النموذج: محرر النص الغني الأصلي 20 مرتبط بحقل الجسم.">
<img src="/images/docs/developers/extends/apps/document-Generator/10-template-editor.png" alt="سجل القالب مع علامة تبويب محرر النص الغني الأصلي" />
</Frame>
**بعد هذه الخطوة:** معاينة الوثائق بشكل جميل والقوالب قابلة للتحرير
في التطبيق. ثم دعنا وكيل الذكاء الاصطناعي يولدها من محادثة.
<Card title="التالي : وكيل AI →" icon="robot" href="/l/ar/developers/extend/apps/tutorials/document-Generator/ai-agent">
إضافة وكيل ومهارة يتصلان بأداتك.
</Card>

View File

@@ -0,0 +1,133 @@
---
title: ١. نموذج البيانات
icon: database
description: وثائق نموذجية ونماذج ذات كائنات وحقول وعلاقة.
---
يحتاج تطبيقنا إلى عنصرين مخصصين: **قوالب وثيقة** (ما يجب كتابته) و
**وثائق** (النتيجة المنشأة). دعونا نعرفهم.
سكاف كل ملف كيان مع CLI - يخلق مجلد UUID صالح و
الصحيح لك:
```bash filename="Terminal"
yarn twenty dev:add object
```
نعرض أدناه الملفات المكتملة.
<Note>
كل `*_UNIVERSAL_IDENTIFIER` يعيش باستمرار في
`src/constants/universal-Identiers.ts` ويتم استيراده حيثما استخدم. كتل الكود
أسفل أغفل تلك الواردات للإيجاز، أبقيها في ملفاتك.
</Note>
## عنصر القالب
يحتوي القالب على `name`، و `body` يتضمن `{{placeholders}}`، و `target` يحدد ما إذا كان مكتوبًا لشخص أو لشركة. 'body' هو حقل
'RICH_TEXT' لذا فإن عشرون يمنحه محرر نص ثري كامل.
```ts filename="src/objects/document-template.object.ts"
import { defineObject, FieldType } from 'twenty-sdk/define';
export default defineObject({
universalIdentifier: DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
nameSingular: 'documentTemplate',
namePlural: 'documentTemplates',
labelSingular: 'Document template',
labelPlural: 'Document templates',
icon: 'IconFileText',
labelIdentifierFieldMetadataUniversalIdentifier:
TEMPLATE_NAME_FIELD_UNIVERSAL_IDENTIFIER,
fields: [
{ universalIdentifier: TEMPLATE_NAME_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.TEXT, name: 'name', label: 'Name', icon: 'IconAbc' },
{ universalIdentifier: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.RICH_TEXT, name: 'body', label: 'Body', icon: 'IconFileText',
description: 'Use {{placeholders}} like {{name.firstName}} or {{jobTitle}}.' },
{ universalIdentifier: TEMPLATE_TARGET_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.SELECT, name: 'target', label: 'Target', icon: 'IconTarget',
defaultValue: `'PERSON'`,
options: [
{ id: TEMPLATE_TARGET_OPTION_PERSON_UNIVERSAL_IDENTIFIER,
value: 'PERSON', label: 'Person', color: 'blue', position: 0 },
{ id: TEMPLATE_TARGET_OPTION_COMPANY_UNIVERSAL_IDENTIFIER,
value: 'COMPANY', label: 'Company', color: 'green', position: 1 },
] },
],
});
```
<Warning>
يجب أن يكون خيار `SELECT` **القيم** `UPER_CASE` (`PERSON`، وليس `person`)، و
`defaultValue` مغلقاً في اقتباسات إضافية: ` `PERSON`". إن `label\` هو ما يراه المستخدمون.
</Warning>
## كائن المستند
الوثيقة التي تم إنشاؤها تخزن "المحتوى" و "الحالة". قم بتعريفه
بنفس الطريقة، مع اختيار 'الحالة' من 'DRAFT` / 'GENERATED`. الملف الكامل:
[`document.object.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/objects/document.object.ts).
## ربطهم بعلاقة
يجب أن يشير كل مستند مرة أخرى إلى النموذج الذي أتى منه. العلاقات هي
**ثنائية الاتجاه** - أنت تحدد كلا الجانبين، كل منهما في ملف الحقل الخاص به.
```ts filename="src/fields/document-template-relation.field.ts"
import { defineField, FieldType, OnDeleteAction, RelationType } from 'twenty-sdk/define';
// The "many" side: each document belongs to one template.
export default defineField({
universalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'template',
label: 'Template',
relationTargetObjectMetadataUniversalIdentifier:
DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier:
TEMPLATE_DOCUMENTS_FIELD_UNIVERSAL_IDENTIFIER,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'templateId',
},
});
```
الجانب الآخر (`template-documents-relation.field.ts`) هو حقل
`RelationType.ONE_TO_MANY` يسمى `documents` الذي يشير إلى الاتجاه المعاكس.
انظر [Relations](/l/ar/developers/extend/apps/data/relations) للاطلاع على النمط الكامل.
## مشاهدته في العشرين
مع تشغيل 'yarn 20 inenty dev'، افتح **الإعدادات → البيانات النموذجية**. يظهر كلا العنصرين
، وسم مع التطبيق الخاص بك.
<Frame caption="كلا العنصرين المخصصين، مملوكين لتطبيق مولد المستندات.">
<img src="/images/docs/developers/extends/apps/document-Generator/01-data-model.png" alt="إعدادات نموذج البيانات التي تظهر قوالب المستندات والمستندات" />
</Frame>
قم بإنشاء قالب واحد لاختباره - اسمه *اقتراح المبيعات*، وقم بتعيين **الهدف** إلى
*Person*، وقم بلصق جسم مع بعض العناصر النائبة:
```text
Dear {{name.firstName}} {{name.lastName}},
As {{jobTitle}} at {{company.name}}, we think you'll love our product.
Best,
The Team
```
<Frame caption="سجل قالب. وتحافظ هذه الهيئة على الجهات الناقلة لها إلى حين إصدار وثيقة.">
<img src="/images/docs/developers/extends/apps/document-Generator/03-template-record.png" alt="سجل قالب مقترح المبيعات مع الهيئة النائبة" />
</Frame>
**بعد هذه الخطوة:** لديك كائنات 'documentTemplate' و 'document'، مرتبطة بـ
علاقة، و قالب واحد لتوليد منها. وبعد ذلك، المنطق الذي يعبئه
<Card title="التالي: توليد الوثائق →" icon="bolt" href="/l/ar/developers/extend/apps/tutorials/document-Generator/التوليد-المستندات">
اكتب الدالة المنطقية التي تملأ القالب.
</Card>

View File

@@ -0,0 +1,238 @@
---
title: ٢. إصدار الوثائق
icon: bolt
description: وظيفة منطقية واحدة معرّفة كأداة الذكاء الاصطناعي وعملية سير العمل.
---
الآن النواة الأساسية: [دالة المنطقية](/l/ar/developers/extend/apps/logic/logic-functions)
التي تحمّل قالب وسجل، تملأ العناصر النائبة، وتحفظ وثيقة
جديدة.
سنقوم بكتابة منطق العمل مرة واحدة ك\*\*معالج \*\*، ثم نكشف عنه من خلال
عدة مشغلات. هذا الفصل يربط اثنين منهم - أداة **AI** و
**عمل سير العمل**.
## مساعد التقديم
حافظ على المنطق الخالص في ملفه الخاص بحيث أنه من السهل إجراء اختبار الوحدة. هذا يربط سجل
في `{{dot.path}}` رموز ويبدلها.
```ts filename="src/logic-functions/utils/render-template.ts"
const PLACEHOLDER_PATTERN = /\{\{\s*([\w.]+)\s*\}\}/g;
export const renderTemplate = (body: string, values: Record<string, string>) => {
const missingTokens = new Set<string>();
const content = body.replace(PLACEHOLDER_PATTERN, (_m, token: string) => {
const value = values[token];
if (value === undefined || value === '') { missingTokens.add(token); return ''; }
return value;
});
return { content, missingTokens: [...missingTokens] };
};
```
<Tip>
لأن هذا الملف ليس له تأثيرات جانبية، يمكنك تغطيته عن طريق اختبارات الوحدة السريعة
('اختبار yarn:unit\`). انظر [Testing](/l/ar/developers/extend/apps/operations/testing).
</Tip>
## المعالج
يستخدم المعالج [`CoreApiClient`](/l/ar/developers/extend/apps/logic/logic-functions)
لقراءة وكتابة بيانات CRM. يحمّل القالب، ويحمّل السجل المستهدف، ويملأ
الجسم، وينشئ `وثيقة`.
```ts filename="src/logic-functions/handlers/generate-document-handler.ts"
import { CoreApiClient } from 'twenty-client-sdk/core';
import { loadRecordValues } from 'src/logic-functions/utils/load-record-values';
import { renderTemplate } from 'src/logic-functions/utils/render-template';
export const generateDocumentHandler = async (
input: { templateId: string; recordId: string },
) => {
const client = new CoreApiClient();
// Use a filtered list query, not the singular lookup: the singular query
// throws when nothing matches, which would become a 500 instead of a 404.
const { documentTemplates } = await client.query({
documentTemplates: {
__args: { filter: { id: { eq: input.templateId } }, first: 1 },
edges: { node: { id: true, name: true, body: true, target: true } },
},
});
const documentTemplate = documentTemplates?.edges?.[0]?.node;
if (!documentTemplate?.id) return { success: false, status: 404, message: 'Template not found.' };
const record = await loadRecordValues(client, documentTemplate.target, input.recordId);
if (!record.found) return { success: false, status: 404, message: 'Record not found.' };
const { content, missingTokens } = renderTemplate(documentTemplate.body ?? '', record.values);
const { createDocument } = await client.mutation({
createDocument: {
__args: { data: {
name: `${documentTemplate.name} — ${record.displayName}`,
content, status: 'GENERATED', templateId: documentTemplate.id,
} },
id: true, name: true,
},
});
return { success: true, documentId: createDocument.id, content, missingTokens };
};
```
'loadRecordValues' يدير استفسارا مختلفا لشخص ضد شركة ومسطحات
النتيجة - انظر
[\`load-record-values.ts'](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/load-record-values.ts).
## الكشف عنه كأداة وإجراءات سير العمل
يمكن لـ `defineLogicFunction' أن يحمل عدة مشغلات. هنا، "إعدادات الأدوات"
تجعلها قابلة للاستدعاء من قبل عملاء AI ، و `workflowtionTriggertings\` تحولها إلى خطوة
في منشئ سير العمل البصري. ويصف كلاهما إسهاماتهما بمخطط "JSON".
```ts filename="src/logic-functions/generate-document.ts"
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema } from 'twenty-sdk/logic-function';
import { GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { generateDocumentHandler } from 'src/logic-functions/handlers/generate-document-handler';
import { generateDocumentInputSchema } from 'src/logic-functions/schemas/generate-document-input.schema';
export default defineLogicFunction({
universalIdentifier: GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
name: 'generate-document',
description: 'Generate a document from a template and a CRM record.',
timeoutSeconds: 30,
toolTriggerSettings: {
inputSchema: generateDocumentInputSchema,
},
workflowActionTriggerSettings: {
label: 'Generate Document',
icon: 'IconFileText',
inputSchema: jsonSchemaToInputSchema(generateDocumentInputSchema),
outputSchema: [{ type: 'object', properties: {
success: { type: 'boolean' }, documentId: { type: 'string' },
} }],
},
handler: generateDocumentHandler,
});
```
مخطط المدخلات هو مخطط JSON بسيط يصف `templateId` و`recordId` -
أنظر [`generate-document-input.schema.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/schemas/generate-document-input.schema.ts).
## امنح حق الوصول
الوظائف المنطقية تعمل كدور للتطبيق. تحتاج إلى قراءة القوالب والسجلات
وإنشاء المستندات، لذلك اسمح بذلك في `src/roles/default-role.ts`:
```ts
export default defineApplicationRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Document Generator default role',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canAccessAllTools: true,
canBeAssignedToAgents: true,
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.UPLOAD_FILE],
});
```
يتيح "UPLOAD_FILE" للوظيفة تحميل PDF التي تم إنشاؤها في القسم التالي.
انظر [Roles](/l/ar/developers/extend/apps/config/rolesللحصول على أذونات مأخوذة من الحبوب الأكثر.
## إرفاق ملف PDF حقيقي
حقل نص محرر مفيد، لكن المستخدمين يريدون وثيقة حقيقية. دعونا ننشئ
**PDF** ونخزنه في السجل كملف قابل للتنزيل.
أولا، اعطي الكائن 'document' حقل 'FILES' للاحتفاظ بـ PDF. تقوم التطبيقات برفع
في حقول الملفات الخاصة بها **الخاصة**، لذلك هذا الحقل هو مسارات الرفع:
```ts filename="src/objects/document.object.ts"
{
universalIdentifier: DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.FILES,
name: 'file',
label: 'File',
icon: 'IconFileTypePdf',
universalSettings: { maxNumberOfValues: 1 },
}
```
الآن اجعل قوات الدفاع الشعبي تلك. التطبيق هو مشروع عقدة حقيقي، بحيث يمكنك إضافة أي npm
حزمة تحتاج إليها واستيرادها مثل أي مكان آخر. نحن نستخدم **[pdf-lib](https://pdf-lib.js.org/)**
لرسم PDF و **[marked](https://marked.js.org/)** لتحليل جسم Markdown- يقوم CLI بتثبيته في وقت تشغيل الوظيفة لك:
```bash filename="Terminal"
yarn add pdf-lib marked
```
المساعد الكامل هو
[`generate-document-pdf.ts'](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/generate-document-pdf.ts).
إنه يحلل علامة الرموز المميزة مع علامة `علامة'. ممارسة '، ثم تضعها باستخدام
pdf-lib: العناوين الحقيقية، **الجريئة**/*italic*، تدير الرصاصات والقوائم المرقمة،
الكتل والقواعد-عبارة عن مقتطفات متعددة الصفحات A4 لتقديم القالب
نفسه، وليس جدار نص.
<Frame caption="PDF المولدة: الطباعة الحقيقية وتنسيق Markdown، وتقديم نموذج الجسم.">
<img src="/images/docs/developers/extends/apps/document-generator/07b-generated-pdf.png" alt="تم توليد PDF مصقول قابل للتسويق" />
</Frame>
<Note>
خطوط pdf-lib المدمجة تستخدم تشفير WinAnsi ، لذا لهجات غرب أوروبا تقدم
خارج الصندوق؛ خرائط المساعد مقتبسات ذكية والشرطات وتسقط الشخصيات
لا يمكن ترميزها. إن عرض النصوص غير اللاتينية (الصينية، العربية، السيريلية) سيعني
تضمين خط Unicode.
</Note>
ثم قم بتحميله وتخزين المرجع في السجل. مسارات 'uploadFile' بايت
إلى حقل الملفات الذي تملكه التطبيق؛ و\`id' المعاد هو ما قمت بحفظه:
```ts filename="src/logic-functions/handlers/generate-document-handler.ts"
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { generateDocumentPdf } from 'src/logic-functions/utils/generate-document-pdf';
const documentName = `${documentTemplate.name} — ${record.displayName}`;
const bytes = await generateDocumentPdf(documentName, content);
const fileName = 'proposal.pdf';
const uploaded = await new MetadataApiClient().uploadFile(
Buffer.from(bytes),
fileName,
'application/pdf',
DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
);
await client.mutation({
updateDocument: {
__args: {
id: documentId,
data: { file: [{ fileId: uploaded.id, label: fileName }] },
},
id: true,
},
});
```
الوثيقة التي تم إنشاؤها تحمل الآن PDF قابل للتنزيل:
<Frame caption="PDF التي تم إنشاؤها، مخزنة في حقل ملف المستند.">
<img src="/images/docs/developers/extends/apps/document-generator/08-document-with-pdf.png" alt="سجل مستند مع ملف PDF الذي تم إنشاؤه" />
</Frame>
<Note>
'uploadFile' يستهدف فقط حقول الملفات **التي تملكها التطبيقات** (لذلك يتطلب التحميل دائمًا تطبيق
يمتلك الحقل، بالإضافة إلى علم الدور 'UPLOAD_FILE\`). لهذا السبب يهبط PDF
في الحقل 'file' الخاص بالسجل - نفس النمط الذي يستخدمه
[call-record](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/public/call-recorder)
للتسجيلات.
</Note>
**بعد هذه الخطوة:** كل مستند تم إنشاؤه لديه PDF حقيقي، قابل للتنزيل. ولكن
لا يوجد ما يمكنه *استدعاء* المولّد من واجهة المستخدم بعد — ولأجل ذلك نحتاج إلى مسار HTTP.
<Card title="التالي: طرق HTTP →" icon="الكرة الأرضية" href="/l/ar/developers/extend/apps/tutorials/document-Generator/http-routes">
خدمة الوظيفة عبر HTTP وتقديم الوثائق كصفحات على الويب.
</Card>

View File

@@ -0,0 +1,149 @@
---
title: ٣. مسارات HTTP
icon: globe
description: قم بتفعيل الوظيفة على HTTP وتقديم الوثائق كصفحات ويب.
---
نفس المعالج يمكنه أيضا الإجابة على طلبات HTTP. سوف نضيف مسارين:
* نقطة النهاية **POST** مكالمات واجهة المستخدم لإنشاء وثيقة، و
* نقطة نهاية عامة **GET** تجعل الوثيقة صفحة ويب قابلة للطباعة.
وكلاهما يستخدم `httpRouteTriggerSettings`. يتم الوصول إلى مسارات التطبيق باستخدام مسار `/s/`؛ على النسخ المحلية
والمستضافة ذاتيًا، يتم تقديم هذا المسار بواسطة خادم Twenty الخاص بك
(مثلًا: `http://localhost:2020/s/documents/generate`). على Twenty Cloud، يقوم `RestApiClient`
بمعالجة المسار عبر نطاق وظائف مساحة العمل.
## مسار POST - توليد حسب الطلب
هذا يعيد استخدام `GenerateDocumentHandler`، لذلك لا يوجد منطق لتكراره - مجرد محول رقيق
الذي يقرأ الجسم المطلوب.
```ts filename="src/logic-functions/generate-document-route.ts"
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { Response } from 'twenty-sdk/logic-function';
import { generateDocumentHandler } from 'src/logic-functions/handlers/generate-document-handler';
const handler = async (event: RoutePayload): Promise<Response> => {
const body = event.body as Record<string, unknown> | null;
const result = await generateDocumentHandler({
templateId: (body?.templateId as string) ?? '',
recordId: (body?.recordId as string) ?? '',
});
// Map the handler's failure reason onto a real HTTP status (400/404/500)
// instead of always returning 200.
return new Response(JSON.stringify(result), {
status: result.success ? 200 : (result.status ?? 400),
headers: { 'Content-Type': 'application/json' },
});
};
export default defineLogicFunction({
universalIdentifier: GENERATE_DOCUMENT_ROUTE_UNIVERSAL_IDENTIFIER,
name: 'generate-document-route',
timeoutSeconds: 30,
handler,
httpRouteTriggerSettings: {
path: '/documents/generate',
httpMethod: 'POST',
isAuthRequired: true,
},
});
```
يقوم المعالج المشترك بإرجاع 'حالة' مقترحة عند الفشل، بحيث يمكن للطريق
الإجابة مع رمز `4xx`/`5xx`. `isAuthRerequirered: true` يعني أنه يجب على المتصل
أن يقدم رمزا صالحا - المكون الأمامي في الفصل التالي يمر رمز الوصول إلى
المستخدم تلقائيا.
## مسار GET - يقدم كصفحة على الشبكة
لإرجاع HTML بدلاً من JSON، قم بتدوين الجسم في 'استجابة` مع رأس
'محتوى - نوع`. هذا المسار عام ('isAuthRerequirered: false\`) بحيث يمكن مشاركة مستند تم إنشاؤه
كرابط .
```ts filename="src/logic-functions/view-document.ts"
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { Response } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { documentHtmlPage } from 'src/utils/render-document';
const htmlResponse = (html: string, status = 200): Response =>
new Response(html, { status, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
const handler = async (event: RoutePayload): Promise<Response> => {
const documentId = event.queryStringParameters?.id;
if (!documentId) {
return htmlResponse(documentHtmlPage('Missing document id', 'Provide ?id=<documentId>.'), 400);
}
// Filtered list query so an unknown id renders a clean 404 page instead of throwing.
const { documents } = await new CoreApiClient().query({
documents: {
__args: { filter: { id: { eq: documentId } }, first: 1 },
edges: { node: { id: true, name: true, content: true } },
},
});
const document = documents?.edges?.[0]?.node;
if (!document?.id) {
return htmlResponse(documentHtmlPage('Document not found', `No document with id ${documentId}.`), 404);
}
return htmlResponse(documentHtmlPage(document.name ?? 'Document', document.content ?? ''));
};
export default defineLogicFunction({
universalIdentifier: VIEW_DOCUMENT_ROUTE_UNIVERSAL_IDENTIFIER,
name: 'view-document',
timeoutSeconds: 15,
handler,
httpRouteTriggerSettings: {
path: '/documents/view',
httpMethod: 'GET',
isAuthRequired: false,
},
});
```
'documentHtmlPage' يجعل جسم Markdown إلى HTML (مع [marked](https://marked.js.org/)،
محسوسة) ويسقط في نظيف، صفحة قابلة للطباعة تعرض فقط محتوى القالب- نفس مظهر PDF والمعاينة داخل التطبيق.
[انظر المساعد](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/render-document.ts).
## جرب ذلك
مع قالب وشخص في مساحة العمل الخاصة بك، اتصل بالمسار (التقط الرمز المميز من
**الإعدادات → APIs & Webhooks**):
```bash filename="Terminal"
curl -X POST http://localhost:2020/s/documents/generate \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"templateId":"<templateId>","recordId":"<personId>"}'
# → {"success":true,"documentId":"...","content":"Dear Jeffery Griffin, ..."}
```
افتح المستند الذي تم إرجاعه في المتصفح الخاص بك:
```
http://localhost:2020/s/documents/view?id=<documentId>
```
<Frame caption="ويجعل مسار الهيئة العامة للتكنولوجيا من الوثيقة صفحة قابلة للطباعة.">
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="صفحة وثيقة تم إصدارها" />
</Frame>
<Tip>
يمكنك أيضًا بث سجلات الدالة أثناء اختبارها باستخدام
`yarn 20 dev:function:logs`، أو استدعاء ذلك مباشرة باستخدام
`yarn 20 dev:function:exec`.
</Tip>
**بعد هذه الخطوة:** يمكن للتطبيق إنشاء مستندات عبر HTTP وخدمتها كصفحات ويب
الآن دعونا نجعله قابلا للاستخدام بدون "تعطيل".
<Card title="التالي : بناء واجهة المستخدم →" icon="table-columns" href="/l/ar/developers/extend/apps/tutorials/document-Generator/building-the-ui">
المشاهدة، الملاحة، الأوامر، والعنصر الأمامي.
</Card>

View File

@@ -0,0 +1,63 @@
---
title: "دليل تعليمي: مولِّد المستندات"
icon: wand-magic-sparkles
description: أنشئ تطبيق Twenty حقيقيًا يُولِّد مستندات مخصَّصة من بيانات نظام إدارة علاقات العملاء (CRM) الخاصة بك.
---
في هذا الدليل التعليمي ستقوم بإنشاء **مولِّد المستندات** — تطبيق يحوِّل القوالب القابلة لإعادة الاستخدام إلى مستندات مخصَّصة باستخدام البيانات الموجودة مسبقًا في نظام إدارة علاقات العملاء (CRM) لديك.
اكتب قالبًا مرة واحدة باستخدام `{{placeholders}}`، ثم أنشئ مستندًا مكتمل الحقول لأي شخص أو شركة بنقرة واحدة — من قائمة الأوامر، أو من وكيل ذكاء اصطناعي، أو من سير عمل.
<Frame caption="قالب واحد، يتم إنشاؤه لشخص محدد، وفتحه كصفحة قابلة للطباعة.">
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="مستند عرض مبيعات تم إنشاؤه" />
</Frame>
## ما ستتعلّمه
يُضيف كل فصل قدرة واحدة. بنهاية الدليل ستكون قد تعاملت مع معظم حزمة تطوير البرمجيات (SDK).
| الفصل | القدرة | مرجع |
| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
| [١. نموذج البيانات](/l/ar/developers/extend/apps/tutorials/document-generator/data-model) | الكائنات، الحقول، وعلاقة واحدة | [البيانات](/l/ar/developers/extend/apps/data/overview) |
| [٢. إنشاء المستندات](/l/ar/developers/extend/apps/tutorials/document-generator/generating-documents) | دالة منطقية (أداة ذكاء اصطناعي + خطوة سير عمل) تملأ قالب Markdown وتُرفِق ملف PDF مصقولًا | [الوظائف المنطقية](/l/ar/developers/extend/apps/logic/logic-functions) |
| [٣. مسارات HTTP](/l/ar/developers/extend/apps/tutorials/document-generator/http-routes) | تقديم JSON وصفحة HTML قابلة للمشاركة من المسارات | [الوظائف المنطقية](/l/ar/developers/extend/apps/logic/logic-functions) |
| [4. بناء واجهة المستخدم](/l/ar/developers/extend/apps/tutorials/document-generator/building-the-ui) | العروض، التنقل، قائمة الأوامر، ومكوّنات الواجهة الأمامية التي تُعاين مستندًا وتحرّر قالبًا | [التخطيط](/l/ar/developers/extend/apps/layout/overview) |
| [5. وكيل ذكاء اصطناعي](/l/ar/developers/extend/apps/tutorials/document-generator/ai-agent) | وكيل + مهارة | [المهارات والوكلاء](/l/ar/developers/extend/apps/logic/skills-and-agents) |
| [6. النشر](/l/ar/developers/extend/apps/tutorials/document-generator/publishing) | إطلاقه في السوق | [النشر](/l/ar/developers/extend/apps/operations/publishing) |
## المتطلبات الأساسية
يجب أن تكون قد أنهيت قسم [البدء السريع](/l/ar/developers/extend/apps/getting-started/quick-start):
خادم Twenty محلي يعمل على المنفذ `2020` وواجهة سطر أوامر (CLI) موثَّقة عليه.
إذا لم تكن قد فعلت ذلك، فقم بتهيئة واحد وتشغيله الآن:
```bash filename="Terminal"
npx create-twenty-app@latest document-generator
cd document-generator
yarn twenty dev
```
<Note>
تفضّل قراءة الكود النهائي؟ يوجد التطبيق الكامل في
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
كل مقتطف أدناه منسوخ منه.
</Note>
## كيف تتكامل مكوّنات التطبيق معًا
<Frame>
<img src="/images/docs/developers/extends/apps/document-generator/how-it-fits.svg" alt="يُحوَّل قالب يحتوي على عناصر نائبة إلى مستند مصقول مع ملف PDF، ويتم تشغيل ذلك من قائمة الأوامر، أو وكيل ذكاء اصطناعي، أو سير عمل، أو رابط قابل للمشاركة" />
</Frame>
تكتب **قالبًا** مرة واحدة في محرّر نص منسّق، مع `{{placeholders}}`. يؤدي اختيار قالب وسجل في نظام إدارة علاقات العملاء إلى ملء العناصر النائبة وتخزين مستند مصقول (مع ملف PDF). كل ما عدا ذلك — قائمة الأوامر، وكيل الذكاء الاصطناعي،
خطوة سير العمل، الرابط القابل للمشاركة — ما هو إلا طريقة مختلفة لتشغيل هذا
المولّد الواحد.
## أبقِ هذه الحلقة قيد التشغيل
اترك الأمر `yarn twenty dev` يعمل في أحد الطرفيات طوال الدرس التطبيقي. في كل مرة تُضيف أو تعدّل ملفًا ضمن `src/`، تُعاد مزامنته مع خادمك خلال بضع ثوانٍ، لكي تتمكّن من مشاهدة كل قدرة تظهر في واجهة المستخدم أثناء بنائها.
<Card title="ابدأ البناء →" icon="قاعدة البيانات" href="/l/ar/developers/extend/apps/tutorials/document-generator/data-model">
الفصل 1: نمذجة المستندات والقوالب.
</Card>

View File

@@ -0,0 +1,137 @@
---
title: 6. النشر
icon: rocket
description: إضافة بيانات التعريف للسوق ونشر التطبيق الخاص بك.
---
تطبيقك يعمل. والخطوة الأخيرة هي وصف ذلك للسوق والنشر.
## إضافة بيانات التعريف للسوق
[config](/l/ar/developers/extend/apps/config/application) يحمل الهوية
التي تظهر في السوق: روابط المؤلف والفئة والشعار والدعم
. ضع شعارًا في `public/` وارجع إليه باستخدام `logo`.
```ts filename="src/application-config.ts"
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: 'Document Generator',
description:
'Create reusable document templates and generate personalized documents from your CRM records.',
logo: 'public/document-generator.svg',
author: 'Twenty',
category: 'Productivity',
websiteUrl: 'https://docs.twenty.com/l/ar/developers/extend/apps',
termsUrl: 'https://www.twenty.com/terms',
emailSupport: 'contact@twenty.com',
issueReportUrl: 'https://github.com/twentyhq/twenty/issues',
});
```
<Tip>
يتم الإعلان عن الدور الافتراضي مع 'defineApplicationRole()' في ملفه الخاص - أنت
لا تتجاوز 'defaultRoleUniversalIdentifier' هنا بعد الآن.
</Tip>
أضف أيضا الكلمة المفتاحية `٢٢app` إلى `package.json` حتى يكون التطبيق قابلا للاكتشاف:
```json filename="package.json"
{ "keywords": ["twenty-app"] }
```
## إضافة لقطات شاشة معرض الصور
قائمة السوق تبيع نفسها مع لقطات شاشة. ضع بعض ملفات PNG في
`public/gallery/` وارجع إليها باستخدام `galleryImages` — حيث يتم عرضها كمعرض
على صفحة القائمة، بحسب ترتيبها في المصفوفة.
```ts filename="src/application-config.ts"
export default defineApplication({
// ...identity from above
galleryImages: [
'public/gallery/01-generated-document.png',
'public/gallery/02-command-menu.png',
'public/gallery/03-template-editor.png',
'public/gallery/04-documents.png',
],
});
```
<Tip>
الرصاص مع الدفع: اجعل أول لقطة للشاشة النتيجة النهائية (وثيقة
تم إنشاؤها)، ثم اظهر كيف تم تشغيلها وكتابتها. استخدم التقاط
عالي الدقة - إنها أول شيء يراه المستخدم.
</Tip>
إعطاء العلاج نفسه 'README.md' - إنها الصفحة الأولى على npm و GitHub.
افتح مع اقتراح القيمة و لقطة شاشة، قائمة بميزات العنوان،
ثم ابقي تفاصيل البناء تحت الطبع.
## تحقق قبل الشحن
تشغيل نفس البوابات CI :
```bash filename="Terminal"
yarn lint # oxlint
yarn typecheck # tsgo
yarn test:unit # unit tests
yarn twenty plan # preview the metadata diff
```
تعرض الخطة بالضبط ما سيتغيّر على الخادم من دون تطبيقه —
وهي فحص نهائي جيّد للسلامة. انظر
[Testing](/l/ar/developers/extend/apps/operations/testing) و
[المزامنة والاسترداد](/l/ar/developers/extend/apps/operations/sync-and-recovery).
## النشر
```bash filename="Terminal"
# Public app → npm (default)
yarn twenty app:publish
# Or deploy privately to a specific server's registry
yarn twenty app:publish --private -r <remote>
```
'app:publish' يبني وينشر إلى npm بشكل افتراضي؛ '--private' يرفع قاعدة
tarball إلى السجل الخاص لخادم 20 بدلا من ذلك. لتظهر تطبيق منشور
في سوق مثيل ما، قم بتفعيل مزامنة الكتالوج:
```bash filename="Terminal"
yarn twenty dev:catalog-sync -r <remote>
```
التفاصيل الكاملة وقائمة التحقق من الإصدار:
[Publishing](/l/ar/developers/extend/apps/operations/publishing).
## قمت ببناء تطبيق 🎉
في ستة فصول استخدمت معظم سطح SDK:
* **الكائنات، الحقول و العلاقة** لنموذج البيانات
* **دالة منطقية** مكشوفة كأداة \*\*AI \*\*، و **سير العمل**، و **مسارات HTTP**
* **مشاهدات, تصفح, أمر و مكون أمامي** لواجهة المستخدم
* **وكيل + مهارة** لتوليد اللغة الطبيعية
* \*\*البيانات الوصفية للسوق \*\* وتدفق النشر
التطبيق النهائي هو في
[`packages/XXapps/examples/document-Generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
## من أين تذهب بعد
<CardGroup cols={2}>
<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">
مشغلات أحداث Cron وقاعدة البيانات، ومتجر القيمة المفتاح، واتصالات 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>

View File

@@ -0,0 +1,147 @@
---
title: واجهات برمجة التطبيقات
description: استعلم وعدّل بيانات إدارة علاقات العملاء (CRM) لديك برمجياً باستخدام REST أو GraphQL.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
تم تصميم Twenty ليكون صديقًا للمطورين، حيث يوفر واجهات برمجة قوية تتكيف مع نموذج البيانات المخصص. نحن نوفر أربعة أنواع متميزة من واجهات برمجة التطبيقات لتلبية احتياجات التكامل المختلفة.
## النموذج الأول للمطورين
تقوم Twenty بإنشاء واجهات برمجة التطبيقات خصيصاً لنموذج بياناتك:
* **لا حاجة إلى معرفات طويلة**: استخدم أسماء الكائنات والحقول مباشرة في نقاط النهاية
* **معالجة متساوية للأشياء القياسية والمخصصة**: تحصل أشياؤك المخصصة على نفس معاملة واجهة برمجة التطبيقات كما هو الحال مع الأشياء المضمنة
* **نقاط نهاية مخصصة**: يحصل كل كائن وحقل على نقطة نهاية API الخاصة به
* **وثائق مخصصة**: يتم إنشاؤها خصيصًا لنموذج بيانات مساحة عملك
<Note>
وثائق واجهة برمجة التطبيقات المخصصة لك متاحة ضمن **الإعدادات → API & Webhooks** بعد إنشاء مفتاح API. نظرًا لأن Twenty تُنشئ واجهات برمجة تطبيقات تتطابق مع نموذج البيانات المخصص لديك، فإن الوثائق فريدة لمساحة عملك.
</Note>
## نوعا واجهات برمجة التطبيقات
### واجهة برمجة التطبيقات الأساسية
يتم الوصول إليها عبر `/rest/` أو `/graphql/`
تعامَل مع **السجلات** الفعلية لديك (البيانات):
* إنشاء وقراءة وتحديث وحذف الأشخاص والشركات والفرص، إلخ.
* استعلام وتصفية البيانات
* إدارة العلاقات بين السجلات
### واجهة برمجة البيانات الوصفية
يتم الوصول إليها عبر `/rest/metadata/` أو `/metadata/`
إدارة **مساحة العمل ونموذج البيانات** لديك:
* إنشاء أو تعديل أو حذف الكائنات والحقول
* تكوين إعدادات مساحة العمل
* تعريف العلاقات بين الكائنات
## REST مقابل GraphQL
تتوفر واجهات برمجة التطبيقات الأساسية وواجهات البيانات الوصفية بصيغتي REST وGraphQL:
| التنسيق | العمليات المتاحة |
| ----------- | ----------------------------------------------------------------------------- |
| **REST** | CRUD، عمليات الدفعات، إدراج/تحديث |
| **GraphQL** | نفس الشيء + **عمليات إدراج/تحديث مجمعة**، واستعلامات العلاقات في استدعاء واحد |
اختر بناءً على احتياجاتك — كلا الصيغتين تصلان إلى البيانات نفسها.
## نقاط نهاية API
| البيئة | عنوان URL الأساسي |
| --------------------- | ------------------------- |
| **السحابة** | `https://api.twenty.com/` |
| **الاستضافة الذاتية** | `https://{your-domain}/` |
## المصادقة
كل طلب API يتطلب تضمين مفتاح API في رأس الطلب:
```
Authorization: Bearer YOUR_API_KEY
```
### قم بإنشاء مفتاح API
1. انتقل إلى **الإعدادات → APIs & Webhooks**
2. انقر على **+ إنشاء مفتاح**
3. التكوين:
* **الاسم**: اسم وصفي للمفتاح
* **تاريخ الانتهاء**: متى تنتهي صلاحية المفتاح
4. انقر على **حفظ**
5. **انسخه فوراً** — يظهر المفتاح مرة واحدة فقط
<VimeoEmbed videoId="928786722" title="إنشاء مفتاح API" />
<Warning>
يمنح مفتاح API الخاص بك الوصول إلى بيانات حساسة. لا تشاركه مع خدمات غير موثوقة. إذا تم اختراقه، عطّلْه فوراً وأنشئ مفتاحاً جديداً.
</Warning>
### تعيين دور لمفتاح API
لتحسين الأمان، عيّن دوراً محدداً لتقييد الوصول:
1. انتقل إلى **الإعدادات → الأعضاء → الأدوار**
2. انقر على الدور الذي ترغب في تعيينه
3. افتح علامة التبويب **التعيين**
4. ضمن **مفاتيح API**، انقر على **+ تعيين إلى مفتاح API**
5. حدد مفتاح API
سيرث المفتاح أذونات ذلك الدور. راجع [الأذونات](/l/ar/user-guide/permissions-access/capabilities/permissions) للحصول على التفاصيل.
### إدارة مفاتيح API
**إعادة التوليد**: الإعدادات → APIs & Webhooks → انقر على المفتاح → **إعادة التوليد**
**حذف**: الإعدادات → APIs & Webhooks → انقر على المفتاح → **حذف**
## ملعب واجهة برمجة التطبيقات
اختبر واجهات برمجة التطبيقات لديك مباشرة في المتصفح باستخدام الملعب المدمج لدينا — متاح لكلٍ من **REST** و**GraphQL**.
### الوصول إلى الملعب
1. انتقل إلى **الإعدادات → APIs & Webhooks**
2. أنشئ مفتاح API (مطلوب)
3. انقر على **REST API** أو **GraphQL API** لفتح الملعب
### ما الذي ستحصل عليه
* **وثائق تفاعلية**: يتم إنشاؤها لنموذج البيانات المحدد لديك
* **اختبارات حيّة**: تنفيذ استدعاءات API فعلية على مساحة عملك
* **مستكشف المخطط**: تصفح الكائنات والحقول والعلاقات المتاحة
* **منشئ الطلبات**: أنشئ الاستعلامات مع الإكمال التلقائي
يعكس الملعب الكائنات والحقول المخصصة لديك، لذا تكون الوثائق دائماً دقيقة لمساحة عملك.
## عمليات المجموعة
كلٌ من REST وGraphQL يدعمان عمليات الدفعات:
* **حجم المجموعة**: حتى 60 سجل لكل طلب
* **العمليات**: إنشاء وتحديث وحذف سجلات متعددة
**ميزات خاصة بـ GraphQL:**
* **إدراج/تحديث دفعي**: إنشاء أو تحديث في استدعاء واحد
* استخدم الأسماء الجمع للكائنات (على سبيل المثال، `CreateCompanies` بدلاً من `CreateCompany`)
## حدود المعدل
يتم تنظيم طلبات API لضمان استقرار المنصة:
| الحد | القيمة |
| -------------- | ---------------------- |
| **الطلبات** | 100 استدعاء في الدقيقة |
| **حجم الدفعة** | 60 سجل لكل استدعاء |
<Tip>
استخدم عمليات الدفعات لزيادة الإنتاجية — عالج ما يصل إلى 60 سجلًا في استدعاء API واحد بدلاً من إجراء طلبات فردية.
</Tip>

View File

@@ -0,0 +1,112 @@
---
title: الويب هوكس
description: استقبل إشعارات في الوقت الفعلي عند وقوع أحداث في نظام إدارة علاقات العملاء (CRM) الخاص بك.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
تدفع خطافات الويب البيانات إلى أنظمتك في الوقت الفعلي عند وقوع أحداث في Twenty — دون الحاجة إلى الاستطلاع الدوري. استخدمها للحفاظ على تزامن الأنظمة الخارجية، وتشغيل الأتمتة، أو إرسال التنبيهات.
## إنشاء ربط ويب
1. انتقل إلى **الإعدادات → APIs & Webhooks → Webhooks**
2. انقر على **+ إنشاء ربط ويب**
3. أدخل عنوان URL لخطاف الويب الخاص بك (يجب أن يكون قابلاً للوصول علنًا)
4. انقر على **حفظ**
يتم تفعيل خطاف الويب فورًا ويبدأ في إرسال الإشعارات.
<VimeoEmbed videoId="928786708" title="إنشاء خطاف ويب" />
### إدارة Webhooks
**تحرير**: انقر على خطاف الويب → تحديث عنوان URL → **حفظ**
**حذف**: انقر على خطاف الويب → **حذف** → تأكيد
## الأحداث
يرسل Twenty خطافات الويب لأنواع الأحداث التالية:
| حدث | مثال |
| --------------- | ---------------------------------------------------------- |
| **إنشاء سجل** | `person.created`, `company.created`, `note.created` |
| **تحديث السجل** | `person.updated`, `company.updated`, `opportunity.updated` |
| **حذف السجل** | `person.deleted`, `company.deleted` |
يتم إرسال جميع أنواع الأحداث إلى عنوان URL لخطاف الويب الخاص بك. قد تتم إضافة تصفية الأحداث في الإصدارات المستقبلية.
## تنسيق الحمولة
يرسل كل خطاف ويب طلب HTTP من نوع POST يتضمن جسمًا بصيغة JSON:
```json
{
"event": "person.created",
"data": {
"id": "abc12345",
"firstName": "Alice",
"lastName": "Doe",
"email": "alice@example.com",
"createdAt": "2025-02-10T15:30:45Z",
"createdBy": "user_123"
},
"timestamp": "2025-02-10T15:30:50Z"
}
```
| الحقل | الوصف |
| --------------- | ----------------------------------------------- |
| `حدث` | ما الذي حدث (على سبيل المثال، `person.created`) |
| `بيانات` | السجل الكامل الذي تم إنشاؤه/تحديثه/حذفه |
| `الطابع الزمني` | وقت حدوث الحدث (UTC) |
<Note>
استجب بحالة **HTTP 2xx** ‏(200-299) لتأكيد الاستلام. تُسجَّل الاستجابات غير 2xx كإخفاقات في التسليم.
</Note>
## التحقق من صحة خطاف الويب
يقوم Twenty بتوقيع كل طلب خطاف ويب لأغراض الأمان. تحقّق من التواقيع للتأكد من أن الطلبات أصيلة.
### الرؤوس
| رأس الصفحة | الوصف |
| ---------------------------- | ------------------- |
| `X-Twenty-Webhook-Signature` | توقيع HMAC SHA256 |
| `X-Twenty-Webhook-Timestamp` | الطابع الزمني للطلب |
### خطوات التحقق
1. احصل على الطابع الزمني من `X-Twenty-Webhook-Timestamp`
2. أنشئ السلسلة: `{timestamp}:{JSON payload}`
3. احسب HMAC SHA256 باستخدام سر خطاف الويب الخاص بك
4. قارِن مع `X-Twenty-Webhook-Signature`
### مثال (Node.js)
```javascript
const crypto = require("crypto");
const timestamp = req.headers["x-twenty-webhook-timestamp"];
const payload = JSON.stringify(req.body);
const secret = "your-webhook-secret";
const stringToSign = `${timestamp}:${payload}`;
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(stringToSign)
.digest("hex");
const isValid = expectedSignature === req.headers["x-twenty-webhook-signature"];
```
## خطافات الويب مقابل سير العمل
| طريقة | الاتجاه | حالة الاستخدام |
| ----------------------------- | ------- | ----------------------------------------------------------- |
| **خطافات الويب** | OUT | إخطار الأنظمة الخارجية تلقائيًا بأي تغيير في السجل |
| **سير العمل + طلب HTTP** | OUT | إرسال البيانات إلى الخارج بمنطق مخصص (عوامل تصفية، تحويلات) |
| **مشغّل خطاف ويب لسير العمل** | IN | استقبال البيانات في Twenty من الأنظمة الخارجية |
لاستقبال البيانات الخارجية، راجع [إعداد مشغّل خطاف الويب](/l/ar/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger).

View File

@@ -0,0 +1,32 @@
---
title: التوسيع
description: وسّع وظائف Twenty باستخدام واجهات برمجة التطبيقات، وخطافات الويب، والتطبيقات المخصصة.
---
<Frame>
<img src="/images/user-guide/integrations/plug.png" alt="الذكاء الاصطناعي" />
</Frame>
## نظرة عامة
تم تصميم Twenty ليكون قابلاً للتوسعة. استخدم واجهات برمجة التطبيقات وخطافات الويب وإطار عمل التطبيقات لدينا للتكامل مع أدواتك الحالية وبناء وظائف مخصصة.
## ما الذي يمكنك فعله
* **واجهات برمجة التطبيقات**: استعلم وعدّل بيانات إدارة علاقات العملاء (CRM) لديك برمجياً باستخدام REST أو GraphQL
* **خطافات الويب**: استقبل إشعارات في الوقت الفعلي عند وقوع أحداث في Twenty
* **التطبيقات**: أنشئ تطبيقات مخصصة توسّع قدرات Twenty
## البدء
<CardGroup cols={2}>
<Card title="واجهات برمجة التطبيقات" icon="code" href="/l/ar/developers/extend/api">
اتصل بـ Twenty برمجياً
</Card>
<Card title="الويب هوكس" icon="bell" href="/l/ar/developers/extend/webhooks">
احصل على إشعارات بالأحداث في الوقت الفعلي
</Card>
<Card title="التطبيقات" icon="puzzle-piece" href="/l/ar/developers/extend/apps/getting-started/quick-start">
أنشئ تخصيصات كرمز برمجي
</Card>
</CardGroup>

View File

@@ -0,0 +1,189 @@
---
title: OAuth
icon: key
description: تدفق رمز التفويض مع PKCE وبيانات اعتماد العميل للوصول من خادم إلى خادم.
---
تُطبِّق Twenty بروتوكول OAuth 2.0 باستخدام رمز التفويض + PKCE للتطبيقات المواجهة للمستخدم، وبيانات اعتماد العميل للوصول من خادم إلى خادم. يُجرى تسجيل العملاء ديناميكيًا عبر [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) — دون إعداد يدوي في لوحة التحكم.
## متى تستخدم OAuth
| السيناريو | طريقة المصادقة |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| البرامج النصية الداخلية، والأتمتة | [مفتاح API](/l/ar/developers/extend/api#authentication) |
| تطبيق خارجي يعمل نيابةً عن مستخدم | **OAuth — رمز التفويض** |
| من خادم إلى خادم، دون سياق مستخدم | **OAuth — بيانات اعتماد العميل** |
| تطبيق Twenty مع امتدادات واجهة المستخدم (UI) | [التطبيقات](/l/ar/developers/extend/apps/getting-started/quick-start) (يتم التعامل مع OAuth تلقائيًا) |
## تسجيل عميل
تدعم Twenty **التسجيل الديناميكي للعملاء** وفقًا لـ[RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591). لا حاجة إلى إعداد يدوي — سجّل برمجيًا:
```bash
POST /oauth/register
Content-Type: application/json
{
"client_name": "My Integration",
"redirect_uris": ["https://myapp.com/callback"],
"grant_types": ["authorization_code"],
"token_endpoint_auth_method": "client_secret_post"
}
```
**الاستجابة:**
```json
{
"client_id": "abc123",
"client_secret": "secret456",
"client_name": "My Integration",
"redirect_uris": ["https://myapp.com/callback"]
}
```
<Warning>
احفظ `client_secret` بأمان — لا يمكن استرجاعه لاحقًا.
</Warning>
## النطاقات
| النطاق | الوصول |
| --------- | -------------------------------------------------------------- |
| `api` | إمكانية قراءة/كتابة كاملة لواجهات برمجة تطبيقات Core وMetadata |
| `profile` | قراءة معلومات ملف تعريف المستخدم المُصادَق عليه |
اطلب النطاقات كسلسلة مفصولة بمسافات: `scope=api profile`
## تدفق رمز التفويض
استخدم هذا التدفق عندما يعمل تطبيقك نيابةً عن مستخدم Twenty.
### 1. أعد توجيه المستخدم للتفويض
```
GET /oauth/authorize?
client_id=YOUR_CLIENT_ID&
response_type=code&
redirect_uri=https://myapp.com/callback&
scope=api&
state=random_state_value&
code_challenge=CHALLENGE&
code_challenge_method=S256
```
| المعلمة | مطلوب | الوصف |
| ----------------------- | -------- | -------------------------------------------------------- |
| `client_id` | نعم | معرّف العميل المسجّل الخاص بك |
| `response_type` | نعم | يجب أن يكون `code` |
| `redirect_uri` | نعم | يجب أن يطابق عنوان URI لإعادة التوجيه المسجّل |
| `scope` | لا | نطاقات مفصولة بمسافات (القيمة الافتراضية هي `api`) |
| `state` | مُوصى به | سلسلة عشوائية لمنع هجمات CSRF |
| `code_challenge` | مُوصى به | تحدّي PKCE (تجزئة SHA-256 لـ verifier، بترميز base64url) |
| `code_challenge_method` | مُوصى به | يجب أن تكون `S256` عند استخدام PKCE |
يرى المستخدم شاشة موافقة ويوافق على الوصول أو يرفضه.
### ٢. معالجة الاستدعاء المرتجع
بعد التفويض، تعيد Twenty التوجيه إلى `redirect_uri` الخاص بك:
```
https://myapp.com/callback?code=AUTH_CODE&state=random_state_value
```
تحقّق من أن قيمة `state` تطابق ما أرسلته.
### ٣. استبدِل الرمز بالرموز
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE&
redirect_uri=https://myapp.com/callback&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
code_verifier=YOUR_PKCE_VERIFIER
```
**الاستجابة:**
```json
{
"access_token": "eyJhbG...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBh..."
}
```
### 4. استخدم رمز الوصول
```bash
GET /rest/companies
Authorization: Bearer ACCESS_TOKEN
```
### 5. حدِّث عند انتهاء الصلاحية
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=YOUR_REFRESH_TOKEN&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET
```
## تدفق بيانات اعتماد العميل
لعمليات التكامل من خادم إلى خادم دون تفاعل مستخدم:
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
scope=api
```
الرمز المُعاد يمتلك وصولًا على مستوى مساحة العمل، وغير مرتبط بأي مستخدم محدّد.
## اكتشاف الخادم
تنشر Twenty إعدادات OAuth الخاصة بها عند نقطة اكتشاف قياسية:
```
GET /.well-known/oauth-authorization-server
```
يعيد هذا جميع نقاط النهاية وأنواع المنح المدعومة والنطاقات والقدرات — وهو مفيد لبناء عملاء OAuth عامّين.
## ملخص نقاط نهاية API
| نقطة النهاية | الغرض |
| ----------------------------------------- | -------------------------- |
| `/.well-known/oauth-authorization-server` | اكتشاف بيانات تعريف الخادم |
| `/oauth/register` | التسجيل الديناميكي للعميل |
| `/oauth/authorize` | تفويض المستخدم |
| `/oauth/token` | مبادلة الرموز وتحديثها |
| البيئة | عنوان URL الأساسي |
| --------------------- | ------------------------ |
| **السحابة** | `https://api.twenty.com` |
| **الاستضافة الذاتية** | `https://{your-domain}` |
## OAuth مقابل مفاتيح API
| | مفاتيح واجهة برمجة التطبيقات | OAuth |
| ------------------------ | -------------------------------- | ------------------------------------------ |
| **الإعداد** | إنشاء من الإعدادات | تسجيل عميل، وتنفيذ التدفق |
| **سياق المستخدم** | لا يوجد (على مستوى مساحة العمل) | أذونات مستخدم محدّد |
| **الأفضل لـ** | البرامج النصية، الأدوات الداخلية | تطبيقات خارجية، وتكاملات متعددة المستخدمين |
| **تدوير الرموز** | يدوي | تلقائي عبر رموز التحديث |
| **وصول محدود بالنطاقات** | وصول كامل إلى API | تفصيلي عبر النطاقات |

View File

@@ -0,0 +1,117 @@
---
title: خطافات الويب
icon: satellite-dish
description: احصل على إشعار عند تغيّر السجلات — سيتم إرسال طلب HTTP POST إلى endpoint الخاص بك عند كل عملية إنشاء أو تحديث أو حذف.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
يقوم Twenty بإرسال طلب HTTP POST إلى URL الخاص بك كلما تم إنشاء سجل أو تحديثه أو حذفه. جميع أنواع الكائنات مشمولة، بما في ذلك الكائنات المخصصة.
## إنشاء خطاف ويب
1. انتقل إلى **الإعدادات → APIs & Webhooks → Webhooks**
2. انقر على **+ إنشاء خطاف ويب**
3. أدخل عنوان URL لخطاف الويب الخاص بك (يجب أن يكون قابلاً للوصول علنًا)
4. انقر على **حفظ**
يتم تفعيل خطاف الويب فورًا ويبدأ في إرسال الإشعارات.
<VimeoEmbed videoId="928786708" title="إنشاء خطاف ويب" />
### إدارة خطافات الويب
**تحرير**: انقر على خطاف الويب → تحديث عنوان URL → **حفظ**
**حذف**: انقر على خطاف الويب → **حذف** → تأكيد
## الأحداث
يرسل Twenty خطافات الويب لأنواع الأحداث التالية:
| حدث | مثال |
| --------------- | ---------------------------------------------------------- |
| **إنشاء سجل** | `person.created`, `company.created`, `note.created` |
| **تحديث السجل** | `person.updated`, `company.updated`, `opportunity.updated` |
| **حذف السجل** | `person.deleted`, `company.deleted` |
يتم إرسال جميع أنواع الأحداث إلى عنوان URL لخطاف الويب الخاص بك. قد تتم إضافة تصفية الأحداث في الإصدارات المستقبلية.
## تنسيق الحمولة
يرسل كل خطاف ويب طلب HTTP من نوع POST يتضمن جسمًا بصيغة JSON:
```json
{
"event": "person.created",
"data": {
"id": "abc12345",
"firstName": "Alice",
"lastName": "Doe",
"email": "alice@example.com",
"createdAt": "2025-02-10T15:30:45Z",
"createdBy": "user_123"
},
"timestamp": "2025-02-10T15:30:50Z"
}
```
| الحقل | الوصف |
| ----------- | ----------------------------------------------- |
| `event` | ما الذي حدث (على سبيل المثال، `person.created`) |
| `data` | السجل الكامل الذي تم إنشاؤه/تحديثه/حذفه |
| `timestamp` | وقت حدوث الحدث (UTC) |
<Note>
استجب بحالة **HTTP 2xx** ‏(200-299) لتأكيد الاستلام. تُسجَّل الاستجابات غير 2xx كإخفاقات في التسليم.
</Note>
## التحقق من صحة خطاف الويب
يقوم Twenty بتوقيع كل طلب خطاف ويب لأغراض الأمان. تحقّق من التواقيع للتأكد من أن الطلبات أصيلة.
### الرؤوس
| الترويسة | الوصف |
| ---------------------------- | ------------------- |
| `X-Twenty-Webhook-Signature` | توقيع HMAC SHA256 |
| `X-Twenty-Webhook-Timestamp` | الطابع الزمني للطلب |
### خطوات التحقق
1. احصل على الطابع الزمني من `X-Twenty-Webhook-Timestamp`
2. أنشئ السلسلة: `{timestamp}:{JSON payload}`
3. احسب HMAC SHA256 باستخدام سر خطاف الويب الخاص بك
4. قارِن مع `X-Twenty-Webhook-Signature`
### مثال (Node.js)
```javascript
const crypto = require("crypto");
const timestamp = req.headers["x-twenty-webhook-timestamp"];
const payload = JSON.stringify(req.body);
const secret = "your-webhook-secret";
const stringToSign = `${timestamp}:${payload}`;
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(stringToSign)
.digest("hex");
const receivedSignature = req.headers["x-twenty-webhook-signature"];
const isValid = crypto.timingSafeEqual(
Buffer.from(expectedSignature, "hex"),
Buffer.from(receivedSignature, "hex")
);
```
## خطافات الويب مقابل سير العمل
| طريقة | الاتجاه | حالة الاستخدام |
| ----------------------------- | ------- | ----------------------------------------------------------- |
| **خطافات الويب** | OUT | إخطار الأنظمة الخارجية تلقائيًا بأي تغيير في السجل |
| **سير العمل + طلب HTTP** | OUT | إرسال البيانات إلى الخارج بمنطق مخصص (عوامل تصفية، تحويلات) |
| **مشغّل خطاف ويب لسير العمل** | IN | استقبال البيانات في Twenty من الأنظمة الخارجية |
لاستقبال البيانات الخارجية، راجع [إعداد مشغّل خطاف الويب](/l/ar/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger).