الانتقال إلى المحتوى الرئيسي

نظرة عامة

المكوّنات الأمامية هي مكوّنات React تُعرَض مباشرة داخل واجهة مستخدم Twenty. تعمل ضمن Web Worker معزول باستخدام Remote DOM — تُنفَّذ شيفرتك داخل iframe معزول ذو منشأ غير شفاف، ومع ذلك لا تزال واجهتها تُعرَض محليًا داخل الصفحة بدلًا من أن تظل محصورة داخل ذلك الـ iframe.
لا تزال مكوّنات Front قيد التطوير النشط. يعمل الكود الخاص بك على DOM جزئي، وليس على صفحة متصفح حقيقية، لذلك يمكن أن تفشل الاستخدامات المتقدمة، وغالبًا من دون أي إشعار. راجع القيود الحالية.

أين يمكن استخدام مكوّنات الواجهة الأمامية

يمكن عرض مكوّنات الواجهة الأمامية في أربعة مواقع داخل Twenty:
  • اللوحة الجانبية — المكوّنات غير عديمة الرأس تفتح في اللوحة الجانبية اليمنى. هذا هو السلوك الافتراضي عندما يتم تشغيل مكوّن واجهة أمامية من قائمة الأوامر.
  • الويدجت (لوحات المعلومات وصفحات السجلات) — يمكن تضمين مكوّنات الواجهة الأمامية كويدجت داخل تخطيطات الصفحات. عند تكوين لوحة معلومات أو تخطيط صفحة سجل، يمكن للمستخدمين إضافة ويدجت لمكوّن واجهة أمامية.
  • إعدادات التطبيق — يشير إليها تصريح defineSettingsMenuItem()، ويعرض المكوّن الأمامي الصفحة خلف أحد عناصر قائمة إعدادات التطبيق. يمكن للتطبيق أن يصرّح بعدة عناصر.
  • دردشة الذكاء الاصطناعي — يمكن لـ دالة منطق مكشوفة كأداة للذكاء الاصطناعي تسمية مكوّن للواجهة الأمامية، والذي يعرض بعد ذلك استدعاءات تلك الأداة مضمنة في الدردشة بدلاً من صف الخطوات الافتراضي.
مكوّن الواجهة الأمامية بمفرده لا يمكن الوصول إليه من واجهة المستخدم — تحتاج إلى إظهاره. هناك أربع طرق للقيام بذلك:

مثال أساسي

أسرع طريقة لرؤية مكوّن الواجهة الأمامية أثناء العمل هي إقرانه مع defineCommandMenuItem، بحيث يظهر كزر إجراء سريع في الزاوية العلوية اليمنى من الصفحة:
src/front-components/hello-world.tsx
src/command-menu-items/hello-world.command-menu-item.ts
بعد المزامنة باستخدام twenty app dev (أو تشغيل twenty app apply لمرة واحدة)، يظهر الإجراء السريع في الزاوية العلوية اليمنى من الصفحة:
زر إجراء سريع في الزاوية العلوية اليمنى
انقره لعرض المكوّن مضمنًا داخل الصفحة.

حقول التكوين

وضع مكوّن أمامي على صفحة

إضافةً إلى الأوامر، يمكنك تضمين مكوّن أمامي مباشرةً في صفحة سجل عبر إضافته كودجت في تخطيط صفحة. لمزيد من التفاصيل، راجع تخطيطات الصفحات. يحدد widgetAvailability منتقي عناصر واجهة المستخدم الذي يوفّر المكوّن أثناء تحرير تخطيط الصفحة. يُنشأ المكوّن الأمامي إما لصفحات السجل، حيث يعمل على السجل المعروض، أو للوحات المعلومات، لذا يعلن عن pageLayoutType واحد:
اترك widgetAvailability خارجًا لمكوّن لا يُفتح إلا من أمر، أو عنصر قائمة الإعدادات، أو صف مخطط زمني. لا يقيّد مطلقًا مكان عرض المكوّن: تعرضه الأوامر وعناصر قائمة الإعدادات وتخطيطات الصفحة التي يعلنها تطبيقك مهما كانت قيمته. يحذّر الإصدار من وجود مكوّن مرئي لا يعلن عن أي منها ولا يُفتح من أي مكان آخر.

عناصر قائمة الإعدادات

يمكن للتطبيق إضافة إدخالاته الخاصة إلى قائمة الإعدادات، ويعرض كل منها مكوّناً أمامياً. صرّح بواحد باستخدام defineSettingsMenuItem() في ملفه الخاص، مع الإشارة إلى المكوّن الذي يعرضه:
src/settings-menu-items/sync-settings.ts
يشير عنصر القائمة إلى المكوّن الأمامي بدلاً من المكوّن الذي يصرّح عن نفسه كإدخال إعدادات. لذلك يمكن لمكوّن واحد دعم عدة عناصر قائمة، ويمكن إعادة توجيه عنصر إلى مكوّن مختلف دون تعديل أي منهما. يُعرض كل عنصر كعلامة تبويب في صفحة التطبيق ضمن الإعدادات > التطبيقات، بعد عام، وفق ترتيب position. صرّح بالعدد الذي تحتاج إليه. يأتي عنصر عام الخاص بـ Twenty — الترقية التلقائية، وعنوان URL للتطبيق، والاتصالات — دائمًا أولًا ولا يمكن لتطبيق تجاوزه؛ ولا يظهر عنوان URL للتطبيق إلا عندما يحتوي التطبيق على دوال يتم تشغيلها عبر HTTP.

الحقول

position قيمة عشرية، لذا يمكن وضع عنصر بين عنصرين موجودين دون إعادة الترقيم. تُفرز العناصر حسب position تصاعدياً، وتُكسر حالات التعادل باستخدام universalIdentifier بحيث يكون الترتيب نفسه في كل مساحة عمل. لا يمكن لعنصرين من التطبيق نفسه مشاركة موضع في النطاق نفسه — ترفض المزامنة ذلك بدلاً من السماح لترتيب التثبيت بتحديد أيهما يُعرض أولاً.

عناوين URL لعلامات التبويب

يُشار إلى علامة تبويب كل عنصر بعلامة تجزئة في عنوان URL للصفحة، لذا يمكنك الارتباط بها مباشرةً، على سبيل المثال /settings/applications/\<applicationId>#sync أو /settings/app-preferences/\<applicationId>#sync. عيّن slug لاختيار علامة التجزئة تلك؛ ومن دونه، يُشار إلى علامة التبويب بواسطة universalIdentifier الخاص بالعنصر. تؤدي إضافة slug لاحقًا إلى استمرار عمل الروابط التي تستخدم universalIdentifier: إذ تفتح علامة التبويب نفسها. يكون slug بصيغة kebab-case بأحرف صغيرة (حروف وأرقام وواصلات مفردة، مثل sync أو sync-settings) وبحد أقصى 64 حرفًا. general وvariables محجوزان لعلامات التبويب المضمنة، ويُرفض slug الذي يكون على هيئة UUID لأنه قد يحجب رابط علامة تبويب أخرى، ولا يمكن لعنصرين في تطبيق واحد مشاركة slug، حتى عبر النطاقات. ترفض كل من عملية البناء والمزامنة slug الذي يخالف هذه القواعد. بعد أن يربط عضو حسابًا أو يعيد ربطه في الإعدادات > تفضيلات التطبيق، ينتقل إلى أول علامة تبويب عنصر USER في التطبيق، وفق ترتيب position، أو إلى عام عندما لا يعرّف التطبيق أيًا منها.

نطاق مساحة العمل والمستخدم

يوضح scope الجهة التي تُعدّ العنصر:
  • WORKSPACE (افتراضي) — يُعدّ مرة واحدة لمساحة العمل بأكملها، إلى جانب إعدادات مساحة العمل الأخرى. هذه هي العناصر التي تُعرض كعلامات تبويب في صفحة التطبيق.
  • USER — يُعدّه كل عضو لنفسه، ضمن إعدادات المستخدم الخاصة به.
يُعرَض عنصر USER كعلامة تبويب في صفحة التطبيق ضمن الإعدادات > تفضيلات التطبيق، حيث لا يراه إلا العضو الذي سجّل الدخول، وفقط عندما يمنح دوره إذن تطبيقات المستخدم. لم تُفعَّل تلك الصفحة في كل مساحات العمل بعد: وحيثما لم تُفعَّل، يستمر حفظ عنصر USER ومزامنته، ولكن لا يُعرَض في أي مكان.
يعمل عنصر USER باعتبار العضو الذي يعرضه. لتخزين شيء لكل شخص، عرّف المتغير باستخدام scope: 'USER': تُرجع getApplicationVariable قيمة العضو الذي يعرض الصفحة، ويحفظها مكوّنك باستخدام updateMyUserApplicationVariable، الذي يكتب قيمة ذلك العضو فقط ويتطلب SystemPermissionFlag.USER_APPS في دور تطبيقك، إذا كان يعرّف دورًا (راجع قراءة المتغيرات وتحديثها في وقت التشغيل). مثل عنصر WORKSPACE، يستبدل عنصر USER نموذج المتغيرات الذي تم إنشاؤه، لذا يجب أن يتيح للعضو تعيين كل متغير مستخدم يحتاجه تطبيقك. يحتوي متغير مساحة العمل على قيمة واحدة مشتركة بين الجميع، لذا فإن حفظ عضو واحد سيستبدل قيمة عضو آخر.
تم إهمال defineSettingsFrontComponent() لصالح defineSettingsMenuItem(). يستمر التطبيق الذي لا يزال يستخدمها في العمل — إذ يُعامل مكوّنه كعنصر قائمة الإعدادات واحد — لكنه لا يمكنه إلا الإعلان عن عنصر واحد، وهو القيد الذي وُجدت defineSettingsMenuItem() لإزالته.

عديم الرأس مقابل غير عديم الرأس

تأتي مكوّنات الواجهة الأمامية بوضعَي عرض يتحكّم بهما الخيار isHeadless: غير عديم الرأس (افتراضي) — يعرض المكوّن واجهة مستخدم مرئية. عند تشغيله من قائمة الأوامر يفتح في اللوحة الجانبية. هذا هو السلوك الافتراضي عندما تكون isHeadless تساوي false أو يتم تجاهلها. عديم الرأس (isHeadless: true) — يتم تركيب المكوّن بشكل غير مرئي في الخلفية. لا يفتح اللوحة الجانبية. تم تصميم المكوّنات عديمة الرأس لإجراءات تنفّذ منطقًا ثم تُزيل تركيبها ذاتيًا — على سبيل المثال، تشغيل مهمة غير متزامنة، أو الانتقال إلى صفحة، أو إظهار نافذة تأكيد منبثقة. تتوافق بشكل طبيعي مع مكوّنات Command في SDK الموصوفة أدناه.
src/front-components/sync-tracker.tsx
نظرًا لأن المكوّن يُرجع 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 — يفتح محتوى في اللوحة الجانبية. تستخدم الصفحات المُوجَّهة خصائص to وparams وqueryParams المكتوبة نفسها التي يستخدمها CommandLink، بالإضافة إلى خصائص hash وpageTitle وresetNavigationStack الاختيارية. يحدد سجل مسارات مساحة العمل ما إذا كان AppPath المطلوب يدعم اللوحة الجانبية. تتجاوز pageTitle عنوان رأس اللوحة، بينما تمسح resetNavigationStack صفحات اللوحة السابقة قبل فتح المسار. تستخدم أدوات اللوحة المصممة لغرض محدد خصائصها الخاصة بكل صفحة.
فيما يلي مثال كامل لمكوّن واجهة أمامية عديم الرأس يستخدم Command لتشغيل إجراء من قائمة الأوامر:
src/front-components/run-action.tsx
src/command-menu-items/run-action.command-menu-item.ts
ومثال يستخدم CommandModal لطلب التأكيد قبل التنفيذ:
src/front-components/delete-draft.tsx
ومثال يستخدم CommandOpenSidePanelPage لفتح السجل الحالي في اللوحة الجانبية على تبويب معيّن. hash هو معرّف تبويب تخطيط الصفحة دون # في البداية (تستخدم التخطيطات الافتراضية معرّفات مثل company-tab-emails أو company-tab-timeline؛ وتستخدم التخطيطات المخصّصة معرّف التبويب نفسه). إذا لم يكن المعرّف موجودًا في تخطيط السجل، فسيتم فتح التبويب الافتراضي بدلًا منه:
src/front-components/open-company-emails.tsx

استدعاء دالة منطقية

تعمل مكونات الواجهة الأمامية على جانب المتصفح داخل Web Worker مُعزَل داخل iframe ذو origin غير شفاف، بينما تعمل الدوال المنطقية على جانب الخادم. لا توجد استدعاءات مباشرة ضمن العملية بين الاثنين — بدلاً من ذلك، يصل مكون الواجهة الأمامية إلى الدالة المنطقية عبر 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، ثم إلغاء التركيب تلقائيًا:
src/front-components/sync-prs.tsx
المسار المُمرَّر إلى RestApiClient هو قيمة httpRouteTriggerSettings.path الخاصة بدالة المنطق (logic function) مع إضافة البادئة /s. أبقِ isAuthRequired: true؛ فرمز TWENTY_APP_ACCESS_TOKEN الذي تُنشئه Twenty لمكوِّنك هو ما يصادق على الطلب:
src/logic-functions/fetch-prs.logic-function.ts
يتم حقن TWENTY_APP_ACCESS_TOKEN تلقائيًا — انظر متغيرات التطبيق. نظرًا لأن متغيرات التطبيق السرية لا تُعرَض أبدًا على مكونات الواجهة الأمامية، احتفِظ بمفاتيح واجهة برمجة التطبيقات والمنطق الحساس الآخر في دالة المنطق، وليس في مكون الواجهة الأمامية.

استدعاء واجهة REST API الخاصة بـ Twenty

لاستدعاء مسارات HTTP الخاصة بالتطبيق أو لقراءة سجلات Twenty وكتابتها من مكوِّن واجهة أمامية، استخدم RestApiClient من twenty-client-sdk/rest. يُرسل المسارات من نوع /s/... إلى عنوان URL الأساسي للدوال في مساحة العمل الخاصة بك، ويُرسل أي مسار آخر، بما في ذلك /rest/...، إلى TWENTY_API_URL. يتصرف دائمًا بصفته الشخص الذي ينظر إلى الصفحة. runAs: 'application' هو خيار لدالة منطقية فقط: لا يتلقى المكوّن أبدًا الرمز المميز الخاص بتطبيقك، لذا فإن طلبه هنا يؤدي إلى طرح استثناء. ضع العمل الذي يحتاج إلى وصول التطبيق الخاص خلف دالة منطقية واستدعها بدلًا من ذلك. تدعم options كلًا من headers وquery (سجل لمعاملات query-string؛ يتم تخطي القيم nullish) وAbortSignal عبر signal. يتم تسلسل كائن body غير من النوع FormData إلى JSON تلقائيًا. عند حدوث 401، يقوم العميل بتحديث رمز الوصول مرة واحدة عبر المضيف ثم يعيد محاولة الطلب. يتم تحديد عنوان URL الأساسي والرمز من بيئة التشغيل بشكل افتراضي. مرِّر معاملات تجاوز (overrides) إلى المُنشئ (constructor) عند الحاجة — على سبيل المثال في الاختبارات:
ترمي الطلبات الفاشلة خطأً من نوع RestApiClientError يعرِّض خصائص status وstatusText وurl بالإضافة إلى body بعد تحليله (parsed):

النشر في البريد الوارد

يتم أيضًا تصدير sendInboxMessage ومساعدات الإشارة إلى البريد الوارد من twenty-sdk/front-component، لذا يمكن للمكوّن نشر رسالة دون وجود دالة منطقية بينهما. الرسالة من تطبيقك، ويحمل الاستدعاء رمز المكوّن، لذا يحتاج العضو الذي يشاهد الصفحة إلى إذن AI، شأنه شأن كل مستلم. لعرض المحادثة بعد ذلك، انتقل إليها باستخدام threadId الذي يعيده الاستدعاء:
  • يأخذ workspaceMemberIds معرّفات أعضاء مساحة العمل، وليس معرّف المستخدم الذي يعيده useUserId().
  • تُفتح المحادثة لمشاركيها فقط، لذا انتقل إليها عندما يكون العضو الذي يشاهد الصفحة أحد المستلمين.
  • البريد الوارد هو مسار صفحة رئيسية: يفتحه navigate هناك، ولا يمكن لـ openSidePanelPage فتحه في اللوحة الجانبية.

الوصول إلى سياق وقت التشغيل

داخل مكوّنك، استخدم خطافات SDK للوصول إلى المستخدم الحالي، والسجل، ومثيل المكوّن:
src/front-components/record-info.tsx
الخطافات المتاحة:

بيانات الكائن الوصفية

استخدم useSelectedObjectMetadata() لتحديد الكائن المرتبط بعنصر واجهة صفحة سجل، أو صف مخطط زمني مخصص، أو أمر ضمن نطاق كائن، أو مكوّن أمامي مفتوح في اللوحة الجانبية مع كائن (من أمر، أو عبر openSidePanelPage مع objectNameSingular). يتيح هذا لتكامل تحديد القوالب أو الإعدادات الأخرى للكائن الحالي دون البحث عبر الكائنات عن معرّف سجل.
تحتوي النتيجة على id (UUID لبيانات الكائن الوصفية لمساحة العمل)، وnameSingular، وnamePlural. الأسماء هي أسماء بيانات وصفية، مثل person وpeople، وليست تسميات عرض. يمكن أن تكون بيانات الكائن الوصفية متاحة حتى عند عدم تحديد أي سجلات. إنها تنتمي إلى سياق هذا المكوّن الأمامي، لذا يمكن للوحة الجانبية عرض كائن مختلف عن الصفحة الرئيسية. يُحدَّث الخطاف عند تغيّر ذلك السياق ويُرجع null عندما يتعذر حل الكائن، أو عند عدم توفير سياق للكائن، أو عند التشغيل على مضيف أقدم لا يوفر بيانات الكائن الوصفية.

موقع المضيف

استخدم useLocation() لقراءة المسار الذي يستضيف المكوّن الأمامي. تعيد كائن FrontComponentLocation يحتوي على أربعة حقول نصية مطلوبة: pathname وsearch وhash وorigin. على سبيل المثال، توفّر صفحة على https://myworkspace.twenty.com/page/page-layout-id?project=abc#details القيم /page/page-layout-id و?project=abc و#details وhttps://myworkspace.twenty.com، على التوالي. يتضمن hash علامة # البادئة ويكون '' عند عدم وجود جزء. يتبع pathname وsearch وhash سياق React Router الخاص بالمكوّن، بما في ذلك المسار الخاص باللوحة الجانبية ذات المسار. يأتي origin من تطبيق المضيف. يتم تحديث الخطاف عند تغيّر المسار. قد لا تتضمن المضيفات الأقدم executionContext.location، لذا تعيد useLocation() القيمة null عندما لا يكون الموقع متاحًا. بالنسبة إلى معلمات الاستعلام، اقرأ useLocation()?.search ؟؟ '' وحلّل النتيجة باستخدام URLSearchParams. يتضمن search علامة ? البادئة ويكون '' عندما لا يحتوي المسار على سلسلة استعلام. يوفّر البديل أيضًا '' عندما لا يوفّر المضيف الموقع.

متغيرات التطبيق

متغيرات التطبيق المُعرَّفة في defineApplication() مع isSecret: false تكون متاحة داخل مكوّنات الواجهة عبر أداة getApplicationVariable:
src/front-components/greeting.tsx
المتغيرات السرّية (isSecret: true) لا يتم كشفها لمكوّنات الواجهة. هي متاحة فقط في دوال المنطق، التي تعمل على جهة الخادم. يمنع هذا إرسال القيم الحساسة، مثل مفاتيح API، إلى المتصفح.
تُرجِع الدالة getApplicationVariable دائمًا سلسلة نصية (أو undefined)، بغضّ النظر عن type المُعلَن للمتغيّر. تُسلسَل السلسلة النصية بشكل متسق حسب النوع (القيم المنطقية على هيئة “true” / “false”، الأعداد كسلاسل عشرية، والمصفوفات/الكائنات كـ JSON)، وهو نفس التنسيق المستخدم مع process.env في وظائف المنطق — قم بتحليلها بنفسك (Number(...)، ‏JSON.parse(...)، ‏=== 'true'). انظر قسم أنواع المتغيرات.

قراءة المتغيرات وتحديثها أثناء وقت التشغيل.

يعيد getApplicationVariable القيم التي تم حقنها عند تحميل المكوّن، لذا لا تظهر القيمة المكتوبة بعد ذلك إلا عند التحميل التالي. لقراءة القيم المخزنة، أو كتابة قيمة منها (وهو ما يفعله عنصر قائمة الإعدادات عند حفظ ما كتبه المستخدم)، استخدم MetadataApiClient:
src/front-components/app-settings.tsx
الملاحظات:
  • رمز المكوّن الخاص بك مرتبط بتطبيقك الخاص. يستهدف updateOneApplicationVariable هذا التطبيق، لذا يكون applicationId اختياريًا ويُرفض تحديد تطبيق آخر. يُرفض بالطريقة نفسها قراءة applicationVariables لتطبيق آخر، ويُتعامل مع التطبيق الذي ينتمي إلى مساحة عمل أخرى على أنه غير موجود.
  • يجب أن يكون المفتاح من المفاتيح التي يعرّفها تطبيقك في defineApplication(). تُعامل كتابة مفتاح غير مُعرَّف على أنها غير موجودة.
  • تُقرأ المتغيرات السرية مقنّعة، وليس بالقيمة المخزنة مطلقًا. تعمل كتابتها بشكل طبيعي.
  • تتطلب قراءة value الخاص بمتغير وكتابته إذن إعدادات تطبيقات مساحة العمل في كل من دور تطبيقك ودور الشخص الذي سجّل الدخول، لأن الرمز المميز للمكوّن هو تقاطع الاثنين. أضف SystemPermissionFlag.WORKSPACE_APPS إلى permissionFlagUniversalIdentifiers في دور التطبيق.
  • يتم إدراج متغير مستخدم (scope: 'USER') بقيمة العضو الذي يعرض الصفحة، ما لم يكن سريًا. ترفضه updateOneApplicationVariable: احفظه باستخدام updateMyUserApplicationVariable، كما يفعل saveMyLanguage أعلاه. يكتب قيمة العضو الذي يعرض الصفحة فقط ويرفض تسمية تطبيق آخر. لا يتطلب إذن تطبيقات مساحة العمل، لكنه يتطلب إذن تطبيقات المستخدم في دور العضو الذي يعرض الصفحة وفي دور تطبيقك، إذا كان يعرّف دورًا: أضف SystemPermissionFlag.USER_APPS إلى permissionFlagUniversalIdentifiers في دور التطبيق. راجع متغيرات مساحة العمل والمستخدم.

متغيرات النظام

متغيرات النظام التالية تكون متاحة دائمًا عبر process.env:

TWENTY_FUNCTIONS_URL

يقوم Twenty أيضًا بحقن TWENTY_FUNCTIONS_URL في مكوِّنات الواجهة الأمامية والدوال المنطقية: وهو عنوان URL الأساسي الذي تُقدَّم منه الدوال المنطقية المُفعَّلة عبر HTTP في تطبيقك. وهو موجود لأن ذلك العنوان (URL) ليس دائمًا هو خادم Twenty نفسه. على Twenty Cloud، يتم تقديم مسارات التطبيق على نطاق مخصص لكل مساحة عمل (https://\<your-workspace-subdomain>.withtwenty.com، أو النطاق العام الأساسي للتطبيق عند تكوينه) بحيث تعمل الاستجابات التي ينشئها التطبيق على أصل خاص بها بدلاً من أصل تطبيق Twenty. تُقدِّم النسخ المستضافة ذاتيًا والمحلية مسارات التطبيق تحت البادئة /s على الخادم نفسه وقد لا تضبط المتغير إطلاقًا. نظرًا لاختلاف عنوان URL الأساسي حسب مساحة العمل وحسب كل نسخة، لا يمكن لشفرتك (code) أن تُضمِّنه بشكل ثابت (hard-code) — يقوم الخادم بحقن القيمة الصحيحة وقت التشغيل. نادرًا ما تحتاج إلى قراءته مباشرة. استدعِ مساراتك عبر RestApiClient باستخدام مسار يبدأ بالبادئة /s/، وسيقوم العميل بحل عنوان URL نيابةً عنك: يزيل بادئة /s ويستخدم TWENTY_FUNCTIONS_URL كهدف، مع الرجوع إلى \<TWENTY_API_URL>/s عندما لا يكون المتغير مضبوطًا. استخدم resolveUrl('/s/\<path>') للحصول على عنوان URL مطلق بدون إرسال طلب، على سبيل المثال لاستخدامه في رابط. اقرأ المتغير مباشرةً فقط عند إنشاء عنوان URL يدويًا:

واجهة الاتصال مع المضيف

يمكن للمكوّنات الأمامية تشغيل التنقّل والنوافذ المنبثقة والإشعارات باستخدام دوال من twenty-sdk/front-component: استخدم openCommandConfirmationModal من twenty-sdk/front-component لمربعات الحوار. يقبل خيارات منظَّمة للعنوان والعنوان الفرعي والزر، ويُرجع 'confirm' أو 'cancel'. يتولى Twenty إدارة النافذة المنبثقة والتركيز والإغلاق وتمرير الصفحة. يجب ألا تعرض المكوّنات الأمامية عناصر Dialog أو AlertDialog أو dialog الأصلية أو النوافذ المنبثقة الأصلية للمتصفح مباشرةً. لا يؤدي توفير حاوية بوابة محلية إلى دعم النمطية المباشرة التي يملكها التطبيق. فيما يلي مثال يستخدم واجهة برمجة تطبيقات المضيف لعرض Snackbar وإغلاق اللوحة الجانبية بعد اكتمال الإجراء:
src/front-components/archive-record.tsx

التخزين

localStorage وsessionStorage يعملان كما يعملان في صفحة عادية، باستخدام واجهة برمجة التطبيقات المتزامنة القياسية. مفاتيحك يُحدَّد نطاقها بتثبيت تطبيقك والمستخدم المسجَّل دخوله: لا يمكن لأي تطبيق آخر قراءتها، وأي مستخدم آخر يسجّل الدخول في المتصفح نفسه يبدأ من مخزن فارغ. القيم المكتوبة في localStorage تبقى على الجهاز عبر عمليات إعادة التحميل؛ أمّا sessionStorage فيستمر طوال جلسة المتصفح.
src/front-components/note-draft.tsx
Twenty تخزّن القيم نيابةً عن تطبيقك، لذا تُطبَّق عملية الكتابة محليًا فورًا ويتم حفظها في الخلفية. لا تنتظر عمليات القراءة أبدًا على المضيف. لا تتم مزامنة أي شيء: لا تنتقل القيم مع المستخدم إلى متصفح أو جهاز آخر، لذا استخدم مخزن القيم المفتاحية لدالة منطقية لأي شيء يجب أن يظل قائمًا بعد تغيير الجهاز. يتم تقييد عمليات الكتابة، ويُحتسب كل حد بعدد الأحرف بدلاً من البايتات: لا يزيد طول المفاتيح عن 512 حرفًا، ولا تزيد قيمة واحدة عن 262,144 حرفًا، ولا تزيد سعة كل مساحة تخزين عن 1,048,576 حرفًا لكل تطبيق ومستخدم. تؤدي عملية كتابة تتجاوز حدًا معينًا إلى طرح استثناء QuotaExceededError، مثل واجهة برمجة تطبيقات المتصفح.

العمل مع سجلات متعددة

استخدم useSelectedRecordIds() لمعالجة عدة سجلات محددة. هذا مفيد للعمليات المجمّعة:
src/front-components/bulk-export.tsx
أبرِزْه باستخدام عنصر قائمة الأوامر المقيّد بتحديدات السجلات:
src/command-menu-items/bulk-export.command-menu-item.ts

اختر الكل

عندما يختار المستخدم كل سجلات طريقة عرض، تُرجع useSelectedRecordIds() مصفوفة فارغة: يُحدَّد التحديد بواسطة عوامل تصفية طريقة العرض والبحث والصفوف غير المحددة، ويمكن أن يتضمن عددًا من السجلات أكبر بكثير مما يمكن أن يتسع في الذاكرة. تُرجع useSelectedRecordsFilter() عامل تصفية يطابق السجلات المحددة تمامًا في كل حالة، لذا استعلم باستخدامه بدلًا من سرد المعرّفات:
بالنسبة إلى التحديدات الكبيرة، مرّر عامل التصفية إلى دالة منطقية وعالج السجلات هناك، صفحةً بصفحة.

الأصول العامة

يمكن للمكوّنات الأمامية الوصول إلى ملفات من دليل public/ للتطبيق باستخدام getPublicAssetUrl:
راجع قسم الأصول العامة للتفاصيل.

مشاركة التبعيات عبر مكوّنات الواجهة الأمامية

افتراضيًا، يضمّن كل مكوّن واجهة أمامية نسخته الخاصة من المكتبات التي يستوردها، لذا فإن تطبيقًا يحتوي على خمسة مكوّنات سيتضمّن React خمس مرات. صرّح بالتبعيات المشتركة في ملف package.json الخاص بتطبيقك لبناء تلك المكتبات مرة واحدة وجعل كل مكوّن في التطبيق يحمّلها من ملف واحد مخزَّن في الذاكرة المخبئية:
package.json
بعد ذلك، يستورد كل مكوّن تبعياته تمامًا كما كان من قبل — دون أي تغيير في كود المكوّن الخاص بك:
src/front-components/counter.tsx
بعض الأمور التي يجب معرفتها:
  • حزمة تبعيات مشتركة واحدة لكل تطبيق. يتم إنشاء الحزمة من تبعيات تطبيقك نفسه، بحيث تحتفظ بالتحكم الكامل في الإصدارات التي تُصدِرها.
  • اسرد المعرّفات الدقيقة التي تستوردها. يُعَدّ twenty-ui/primitives/input و twenty-ui/primitives/data-display مدخلين؛ اسم الحزمة وحده لا يشمل مساراتها الفرعية. إدراج react يغطي تلقائيًا react/jsx-runtime.
  • شارك react-dom/client جنبًا إلى جنب مع react. كل مكوّن يقوم بعملية التصيير من خلال createRoot، لذا فإن تركه خارجًا يعني أن كل مكوّن ما زال يضمّن React DOM داخل حزمتِه الخاصة.
  • الحزمة مُخزَّنة في الذاكرة المخبئية. يتم تقديمها عبر عنوان URL يعتمد على قيمة تجزئة المحتوى (content-hash) مع ذاكرة مخبئية ثابتة طويلة الأجل، لذا يتم تنزيلها مرة واحدة وإعادة استخدامها عبر جميع مكوّنات التطبيق حتى تتغيّر إحدى تبعياتها.
  • المكوّنات التي لا تستورد أيًا من الحزم المشتركة لا تقوم بتنزيلها أبدًا.

التنسيق

تدعم المكوّنات الأمامية عدة أساليب للتنسيق. يمكنك استخدام:
  • أنماط مضمنة — style={{ color: 'red' }}
  • مكوّنات Twenty UI — مكتبة المكوّنات الخاصة بـ Twenty؛ راجع استخدام مكوّنات Twenty UI أدناه
  • Emotion — CSS-in-JS مع @emotion/react
  • Styled-components — أنماط styled.div
  • Tailwind CSS — أصناف مساعدة
  • أي مكتبة CSS-in-JS متوافقة مع React

استخدام مكوّنات Twenty UI

توفّر Twenty مكتبة المكوّنات الخاصة بها على شكل حزمة twenty-ui. استخدم عناصر التحكم ورموز النسق الخاصة به لمنح مكوّنات الواجهة المظهر والتفاعلات نفسها التي يتمتع بها باقي Twenty. تصفّح المكوّنات المشتركة والعناصر الأولية للاطلاع على أمثلة وخصائص وواجهات API للتركيب.

التثبيت

تتضمن التطبيقات الجديدة المُنشأة باستخدام twenty app init بالفعل twenty-ui. بالنسبة إلى تطبيق موجود، أضِفه كتبعية تطوير، وثبّته على الإصدار نفسه من twenty-sdk وtwenty-client-sdk. على سبيل المثال، إذا كانت حزم SDK لديك تستخدم 2.51.0:
يتم تجميع twenty-ui في وقت البناء، إما ضمن كل مكوّن واجهة أو ضمن حزمة التبعيات المشتركة الخاصة بتطبيقك. يحمّل Twenty أوراق أنماط المكوّنات والسمات ويطبّق نظام الألوان النشط لمساحة العمل. لا تحتاج إلى استيراد twenty-ui/style.css أو أوراق أنماط السمة، ولا إلى إضافة ThemeProvider جذري كما تفعل في تطبيق React مستقل.

استيراد المكوّنات

استورد من المسار الفرعي المتوافق للعائلة العامة للحفاظ على تركيز التبعيات: تُجمَّع عناصر التحكم الأساسية ضمن twenty-ui/primitives/\<family>. تُجمّع إعدادات التصميم المسبقة المشتركة واللبنات الأساسية للتطبيق تحت twenty-ui/components/\<family>. تظل نقطة الإدخال المجمّعة twenty-ui/components متاحة، لكن استيرادات العائلات تسهّل مشاركة التبعيات التي تستخدمها فقط. استخدم MainButton للإجراءات البارزة وLightButton للإجراءات خفيفة الوزن. استخدم Button وField وInput وSelect وغيرها من العناصر الأولية من twenty-ui/primitives/input لعناصر التحكم والنماذج الأساسية. اتبع الأجزاء الموثقة لكل عنصر تحكم مركب، مثل Field.Root وField.Label وField.Description. مرّر alignItemWithTrigger={false} إلى Select.Positioner. تتطلب محاذاة العنصر المحدد مع المشغّل، وهي الإعداد الافتراضي في Base UI، قياسات متزامنة للتخطيط لا تستطيع بيئة الاختبار المعزولة توفيرها، لذا تنهار النافذة المنبثقة عند إعادة فتحها. استخدم Dropdown من twenty-ui/components/navigation لقوائم الإجراءات، ومنتقيات الخيارات، ولوحات النماذج غير المشروطة. استخدم enqueueSnackbar من twenty-sdk/front-component لإشعارات المضيف. تُصدَّر واجهات برمجة التطبيقات المستقلة Dialog وAlertDialog أيضاً بواسطة Twenty UI، لكن يُحظر استخدامها مباشرةً في المكوّنات الأمامية. استخدم واجهة برمجة تطبيقات الاتصال بالمضيف لمربعات الحوار. لا تتطلب نقاط الإدخال هذه Monaco. تعتمد نقطة الإدخال المنفصلة twenty-ui/components/code-editor على Monaco، الذي لا يُدعم مُحمّل نصوصه البرمجية في صندوق حماية المكوّن الأمامي، لذا لا يمكن أن يصبح CodeEditor تفاعليًا هناك.

الأيقونات

استورِد الأيقونات الفردية من twenty-ui/icon:
كل أيقونة مسمّاة قابلة للإزالة عبر تقنية tree-shaking، لذا فإن استيراد عدد قليل منها لا يزيد حجم الحزمة إلا قليلًا. تجنب twenty-ui/icon/provider (‏IconsProvider وuseIcons وIcon بالاسم) في مكوّنات الواجهة. تُجمَّع مكونات الواجهة دون تقسيم الشيفرة، لذا ينتهي كتالوج الأيقونات الديناميكي الخاص بالموفر (مجموعة Tabler الكاملة، بحجم يقارب 2 MB بعد التصغير) ضمن حزمتك. اطّلع على مرجع الأيقونات لمعرفة عمليات التصدير المتاحة والخصائص المدعومة.

التنسيق ورموز السمات

تتوافق مكوّنات واجهة Twenty تلقائيًا مع السمة الفاتحة والداكنة لمساحة العمل. يطبّق العارض نظام الألوان النشط على المضيف، وتحلّ المكوّنات متغيرات CSS الخاصة بها بالرجوع إليه. لاستخدام رموز التصميم نفسها في أنماطك المضمنة، استدعِ useTheme() داخل جسم المكوّن. في المكوّنات الأمامية، يعيد مراجع متغيرات CSS افتراضيًا، لذا تتبع الأنماط السمة النشطة للمضيف دون ThemeProvider محلي:
تُصدّر مراجع CSS نفسها باسم themeCssVariables من twenty-ui/theme. اقرأها داخل جسم المكوّن: تحاكي CLI twenty-ui أثناء استخراج البيان، لذا قد يؤدي إلغاء مرجعية ثوابت السمة المستوردة على نطاق الوحدة إلى فشل البناء. سلاسل مراجع CSS ليست أرقام JavaScript محلولة، حتى للرموز مثل theme.icon.size.md. لا تُجرِ عمليات حسابية عليها ولا تمررها إلى واجهات API التي تتطلب أرقامًا. راجع السمات والرموز لمسارات الدلالات وأسماء متغيرات CSS. للتفرّع بناءً على النظام النشِط (active scheme) صراحةً، اقرأه باستخدام useColorScheme() من twenty-sdk/front-component، والذي يعيد 'light' أو 'dark'.

القيود الحالية

مكوّنات Front قيد التطوير النشط. الترسيم، والتنسيق، والتعامل مع الأحداث، وقياس العناصر، وبوابات body، وتخزين المتصفح تعمل جميعها بشكل جيد. أي شيء يتجاوز تلك الحدود (مثل استدعاء دالة DOM على ref) مفقود أو غير مكتمل حاليًا، ومعظم ذلك يفشل بصمت: لا استثناء، ولا خطأ TypeScript أيضًا، لأن الهيكل مكتوب استنادًا إلى DOM الكامل للمتصفح. إذا كان أحد هذه الأمور يعيقك، افتح تذكرة ليتم إعطاؤه أولوية.

التخطيط والقياس

يمكن للعناصر قياس نفسها: المضيف يعكس الإحداثيات الهندسية داخل وضع الحماية، بحيث تتم الإجابة عن عمليات القراءة محليًا ولكن يمكن أن تتأخر حتى إطار واحد، وتعيد أول عملية قراءة لعنصر لم يُقَس من قبل قيمًا صفرية. بعد الكتابة، أعد القراءة داخل استدعاء requestAnimationFrame أو داخل تأثير. يعمل كل من تحديد الموضع من getBoundingClientRect ومراقبة الحجم عبر ResizeObserver من المرآة نفسها، لذا تتأخر استدعاءات تغيير الحجم عن المضيف بنحو إطار واحد. يفضَّل استخدام CSS للتخطيط على أي حال: ورقة الأنماط الخاصة بك تصل إلى الصفحة الحقيقية، لذا فإن flexbox و grid و aspect-ratio و clamp() و ‎@container تعمل بشكل طبيعي، بدون أي تأخير في الإطار. تقيّم matchMedia() كلاً من width وheight وorientation و-webkit-device-pixel-ratio وresolution وhover وpointer وprefers-color-scheme، بصيغة min-/max-، وصيغة النطاق من المستوى 4 ((width >= 600px), (400px \<= width \<= 800px)) والصيغة بلا قيمة ((width)). يصف العرض والارتفاع وorientation مربع عنصر الواجهة الخاص بك، وليس إطار عرض المتصفح، لذا فهي تتبع document.body.clientWidth بدلاً من window.innerWidth، ويُحتسب كل من em وrem بمقدار 16px. يصف hover وpointer الإدخال الأساسي لجهاز المستخدم. يُعتبر أي شرط آخر غير معروف، بالطريقة التي تتعامل بها المتصفحات مع الميزات غير المعروفة: يُقيَّم العضو المفصول بفواصل الذي يظهر فيه إلى false، حتى ضمن not، ما لم يكن بقية ذلك العضو قد حسم النتيجة بالفعل، كما في not print and (prefers-reduced-motion: reduce). يشمل ذلك الميزات غير المعروفة والصياغة التي لا ينفذها هذا المحلل داخل الشرط، مثل المجموعات المتداخلة كـ ((min-width: 600px))، وnot داخل مجموعة، وقيم calc(). يؤدي وجود or بين الشروط إلى جعل عضوه false. لا تكون البانيات MediaQueryList وMediaQueryListEvent مكشوفة أيضاً، لذا فإن عمليات التحقق باستخدام instanceof مقابلهما ترمي ReferenceError. لأن ورقة الأنماط تلك تصل إلى الصفحة الفعلية، تُقيَّم قاعدة @media (min-width: ...) بواسطة المضيف مقابل إطار عرض المتصفح، بينما يجيب matchMedia('(min-width: ...)') وفقاً لمربع عنصر الواجهة الخاص بك. لجعل CSS يتبع المربع نفسه الذي يراه JavaScript، عرّف container-type خاصاً بك على غلاف واستعلم عنه: تُحل قاعدة @container من دون واحدة مقابل أقرب حاوية للمضيف، وليس عنصر الواجهة الخاص بك. ينطبق الفصل نفسه على نظام الألوان، حيث تتبع ورقة الأنماط @media (prefers-color-scheme: dark) تفضيل المتصفح ونظام التشغيل، بينما يتبع matchMedia('(prefers-color-scheme: dark)') النسق المحدد في Twenty، لذا نسّق باستخدام رموز useTheme() عندما يجب أن يتطابق الاثنان. الارتفاع الذي يُمرَّر إليك هو مربعك الخاص أيضاً، وبحسب مكان تركيبك من قِبل المضيف، يمكن أن يُحدَّد حجم ذلك المربع بواسطة محتواك الخاص. يمكن أن يتسبب التخطيط المتفرع بناءً على min-height أو max-height أو orientation في تغذية راجعة إلى نفسه والتذبذب. فضّل الاستعلامات المستندة إلى العرض، أو @container مع container-type: inline-size، لقرارات التخطيط.
يعمل requestAnimationFrame، مع بادئة window. أو بدونها. لا يشغّل إطار رسوم متحركة حقيقيًا داخل worker سوى Chrome؛ أما Firefox وSafari فيحصلان على مؤقت لمدة 16 ms، لذا استخدمه لتأجيل إعادة القراءة إلى ما بعد الكتابة، وليس لتشغيل رسوم متحركة.

التمرير

يؤدي تعيين scrollTop أو scrollLeft، واستدعاء scrollTo() أو اسمه المستعار scroll()، واستدعاء scrollIntoView() إلى تمرير العنصر المطابق في الصفحة:
  • تُطبَّق تغييرات التمرير بعد التصيير التالي، بالترتيب الذي أجراه الرمز البرمجي الخاص بك. ضمن عملية تصيير واحدة، تُطبَّق تغييرات تحديد الإدخال أولاً، لذلك فإن استدعاء setSelectionRange() متبوعًا بكتابة scrollLeft يُبقي المؤشر مرئيًا.
  • تعيد عمليات القراءة القيمة التي كتبتها إلى أن تُبلّغ الصفحة عن الموضع الحقيقي، الذي يقيّده المتصفح بالنطاق القابل للتمرير. في الحاويات من اليمين إلى اليسار، تكون scrollLeft صفرًا أو سالبة، كما هو الحال في المتصفحات.
  • تقبل scrollIntoView() القيمتَين block وinline مع start وcenter وend وnearest، حيث تحاذي block عموديًا وinline أفقيًا، لذلك لا تُعيَّن أوضاع الكتابة العمودية. إنها تمرّر فقط حاويات التمرير الخاصة بمكوّنك، ولا تمرّر أبدًا إطار الودجت أو الصفحة أو مكوّنًا آخر. لا يزال focus() يكشف العنصر الذي عليه التركيز بالطريقة التي يفعلها المتصفح.
  • يكون التمرير فوريًا: تُتجاهل behavior وخيار container، ولا تتوفر scrollBy().
  • لا تؤدي إعادة كتابة موضع قرأته للتو، مثل element.scrollTop = element.scrollTop، إلى أي شيء، لذا لا يمكنها التراجع عن تمرير أجراه المستخدم في هذه الأثناء. لا ينطبق هذا إلا بعد أن تقيس الصفحة العنصر؛ وحتى ذلك الحين تُطبَّق الكتابة.
  • يبقى تمرير document.documentElement أو document.body أو window داخل بيئة الاختبار المعزولة ولا يمرّر الصفحة أبدًا؛ ولا تؤدي window.scrollTo() وscroll() وscrollBy() إلى أي شيء.
  • تُرجع القراءة الأولى لحاوية لم تُقَس من قبل أصفارًا (راجع التخطيط والقياس)، لذا قد يُبلغ فحص قابلية التمرير الذي يُجرى مباشرة بعد ظهور الحاوية بأنها غير قابلة للتمرير.

النطاق العام

تُضاف أيضًا العناصر الأصلية الخاصة بالعامل كأسماء مستعارة إلى window، لذا تعمل مع بادئة window. أو بدونها: fetch، setTimeout/clearTimeout، setInterval/clearInterval، queueMicrotask، console، crypto، performance، structuredClone، btoa/atob، URL/URLSearchParams، AbortController/AbortSignal، Headers/Request/Response، Blob، File، FormData وTextEncoder/TextDecoder.

الوصول إلى DOM

يعطيك ref عنصرًا في sandbox، وليس HTMLElement. تعمل element.classList بالاستناد إلى السمة class: تتصرف add وremove وtoggle وreplace وcontains وitem وlength وvalue والتكرار جميعها كما تفعل في المتصفح، وتبقى className متزامنة. يعمل element.dataset بالطريقة نفسها، وهو مدعوم بسمات data-*؛ اكتب أسماء data-* بأحرف صغيرة، لأن بيئة الاختبار تحتفظ بأسماء السمات كما هي مكتوبة بدلاً من تحويلها إلى أحرف صغيرة كما يفعل المتصفح. يمكن أن تمتد بوابات body حتى 200px إلى ما بعد كل حافة من حواف المكوّن، بما في ذلك خارج إطار تمرير الأداة. تُقصّ التراكبات الأكبر ضمن هذه المنطقة، لذا امنح القوائم الطويلة حاوية تمرير خاصة بها. يكشف التنقل بلوحة المفاتيح في قوائم واجهة مستخدم Twenty، مثل Autocomplete وSelect، الخيار المميّز داخل تلك الحاوية. تُحظر واجهات برمجة التطبيقات الأصلية للطبقة العليا مثل showModal() وshowPopover() وrequestFullscreen().

الأحداث

أحداث الماوس، والمؤشر، واللمس، والسحب، ولوحة المفاتيح، والتركيز، و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) يتم إسقاطه دون تحذير. تتلقى المعالجات أحداثاً ذات أنواع مبنية من حدث الصفحة، PointerEvent أو MouseEvent أو KeyboardEvent أو InputEvent أو WheelEvent أو FocusEvent، وتوجد تلك المُنشئات في بيئة الاختبار لاستخدام dispatchEvent(). يحمل كل حدث مُمرَّر أيضاً واجهة الحدث الاصطناعي الخاصة بـ React (nativeEvent, isDefaultPrevented(), isPropagationStopped(), persist())، لذا يستمر عمل الرمز المكتوب للتعامل مع أحداث React رغم أن بيئة الاختبار تمرر كائن الحدث مباشرة إلى معالجك. يعبر حدثٌ في الصفحة عندما يكون للعنصر الذي تفاعل معه المستخدم، أو لأحد أسلافه في مكوّنك، معالج لنوع ذلك الحدث، أو عندما يكون ضغطةً تنتظرها مستمعات document لديك (انظر أدناه). يعبر مرة واحدة ثم ينتشر داخل بيئة الاختبار كما يفعل في الصفحة: event.target هو العنصر الذي تفاعل معه المستخدم، وcurrentTarget هو العنصر الذي يعمل معالجه، وstopPropagation() يمنع تشغيل معالجات الأسلاف، وrelatedTarget يشير إلى عنصر بيئة الاختبار الذي انتقل التركيز أو المؤشر منه أو إليه، أو يكون null عندما يكون ذلك العنصر خارج مكوّنك. كما في React، يعمل onFocus وonBlur أيضاً عند تغيّر التركيز داخل العنصر، ويعمل onClickCapture ومعالجات الالتقاط الأخرى أثناء مرحلة الالتقاط. تعمل المعالجات التي تضيفها مكتبة باستخدام React.cloneElement، مثل تلك المستخدمة خلف الخاصية render في Base UI، بعد معالجات العنصر نفسه بدلاً من استبدالها، لذلك فإن تمرير onClick: undefined إلى cloneElement لا يزيل معالج العنصر. لا يزال بإمكان معالج على عنصر render في Base UI استدعاء event.preventBaseUIHandler() لتخطي معالج Base UI الخاص. يعكس event.target.value وchecked حالة عنصر التحكم بعد أن يطبق المتصفح تغيير المستخدم: في معالجات input وchange وclick وblur وfocusout، وفي معالجات keyup بالنسبة إلى value. في معالجات keydown وbeforeinput والمؤشر، يحتفظان بقيمة عنصر التحكم وحالة التحديد فيه كما كانت عند آخر تعديل له، أياً كان ما يستمع إليه مكوّنك: يقرأ معالج Enter النص المكتوب حتى الآن، لكن معالج المفتاح القابل للطباعة لا يرى ذلك المفتاح بعد، لذا اقرأ القيم المباشرة من إدخال متحكم به أو من معالج onChange. تتلقى مستمعات document الأحداث التي تعبر من مكوّنك، مع استثناء واحد للضغطات. بينما يستمع مكوّنك إلى pointerdown أو pointerup أو pointercancel أو mousedown أو mouseup أو click على document، كما تفعل النافذة المنبثقة المفتوحة لكي تُغلق عند الضغطات خارجها، تصل كل ضغطة إلى تلك المستمعات: تُرسَل ضغطة داخل مكوّنك إلى العنصر الذي ضغطه المستخدم حتى عندما لا يتعامل معها شيء، وتُرسَل ضغطة في مكان آخر من الصفحة إلى document.body مع حالة الزر وعدد النقرات ونوع المؤشر الخاص بها، ولكن من دون إحداثيات أو مفاتيح معدِّلة. لا تُرسَل النقرات من دون ضغطة، مثل تفعيل لوحة المفاتيح لعنصر تحكم خارج مكوّنك، والضغطات على أشرطة تمرير الصفحة، ولا تصل إيماءة السحب باللمس خارج مكوّنك إلا على شكل pointerdown وpointercancel، لذا فهي لا تُغلق نافذة منبثقة مفتوحة. لا يعبر أي شيء آخر من بقية الصفحة، لذا لالتقاط اختصارات لوحة المفاتيح باستخدام document.addEventListener('keydown', ...)، ضع onKeyDown على عنصر يغلّف مكوّنك. لا تتلقى مستمعات window سوى error وunhandledrejection. لا تعبر تحركات المؤشر إلا فوق العناصر التي لها معالج حركة، ويبقى التقاط المؤشر في الصفحة، ولهذا يتوقف السحب بمجرد أن يغادر المؤشر تلك العناصر. 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) ضمن أصل غير شفاف، لذلك يقوم المضيف بتخزين القيم نيابةً عن تطبيقك. راجع قسم التخزين للاطلاع على نطاقاتها وحدودها. لا تزال IndexedDB وملفات تعريف الارتباط (cookies) وواجهة برمجة تطبيقات Cache وBroadcastChannel غير متاحة. للاحتفاظ بالحالة عبر الأجهزة، استدعِ دالة منطقية واستخدم مخزن القيم المفتاحية الخاص بها. fetch يعمل، مع بعض التحفّظات:
  • يتم تمرير الاستدعاءات إلى Twenty API ومسارات تطبيقك عبر المضيف، لذا يُفضَّل استخدام RestApiClient. في الاستدعاءات الممرَّرة عبر الوكيل، يتم إسقاط AbortSignal وخيارات RequestInit الأخرى، ولا يتم دعم سوى الأجسام من نوع string وURLSearchParams.
  • تغادر النطاقات (origins) الأخرى صندوق العزل مع Origin: null، لذلك يجيب طرف ثالث لواجهة برمجة التطبيقات فقط إذا أرسل Access-Control-Allow-Origin: *. استدعِها من دالة منطقية بدلًا من ذلك.
  • fetch('/rest/people') لا يتطابق أبدًا مع Twenty API، لأن صندوق العزل ليس لديه عنوان URL للصفحة لحساب مسار نسبي بناءً عليه.

اختيار الملف

تُرجع مدخلات الملفات كائنات File أصلية مع بيانات وصفية ومحتويات قابلة للقراءة. استخدم text() أو arrayBuffer() لقراءة ملف محدد، أو مرّره إلى دالة المضيف uploadFile. يتطلب فتح المُحدِّد الأصلي استدعاء input.click() خلال ثانية واحدة من نقرة موثوقة داخل مكوّنك. تم التحقق من تدفقات React وPreact المُنشأة بواسطة SDK في Chromium. تدعم عناوين URL للكائنات التي ينشئها العامل والمُعيّنة إلى img.src المعاينات. ألغِ عناوين URL الخاصة بك عند استبدال الصورة أو إلغاء تحميل مكوّنك. يتولى تطبيقك التحقق من صحة الملفات وتقدم التحميل والإلغاء.

التقاط الوسائط

صرّح بـ microphone أو camera أو كليهما في مصفوفة requestedCapabilities المُمرَّرة إلى defineApplication() لكي تظهر هذه الإمكانات أثناء التثبيت. تكون الموافقة على مستوى مساحة العمل بالكامل وتتطلب عضوًا يمكنه إدارة التطبيقات. إذا استدعى تطبيق مثبّت navigator.mediaDevices.getUserMedia() دون الوصول المطلوب، يوقف Twenty الالتقاط ويعرض مربع حوار للموافقة. يدعم هذا أيضًا التطبيقات الأقدم التي لا تحتوي على إقرارات بالإمكانات. يجب على الأعضاء الذين لا يمكنهم إدارة التطبيقات أن يطلبوا من مسؤول مساحة العمل الموافقة على الوصول. يؤدي إلغاء مربع الحوار إلى رفض الطلب قبل طلب أذونات المتصفح أو نظام التشغيل. تحافظ تحديثات الحزمة على أذونات مساحة العمل الحالية ولا تمنح تلقائيًا وصولًا إضافيًا إلى الوسائط. تتلقى تطبيقات التطوير المتزامنة محليًا الإمكانات المعلنة في ملف البيان الخاص بها. بمجرد الموافقة على الوصول، لا يطلب Twenty الإذن مرة أخرى لكل عملية التقاط، لكن أذونات المتصفح ونظام التشغيل تظل سارية. تشير نقطة برتقالية في رأس مساحة العمل إلى وجود التقاط نشط. مرر المؤشر فوق النقطة أو حددها لرؤية التطبيقات التي تستخدم الميكروفون أو الكاميرا وإيقاف التقاطها. تظهر الطلبات المعلقة كنقطة محددة بإطار مع عنصر تحكم للإلغاء. يعمل getUserMedia() وMediaRecorder عبر بدائل متعددة للساحة المعزولة، لذا يعمل رمز التسجيل القياسي وMediaRecorder.isTypeSupported() دون تغيير بعد الموافقة. يتم قبول كائنات القيود التفصيلية، ولكن لا تتم إعادة توجيهها. يلتقط المضيف باستخدام إعداداته الافتراضية للأنواع المطلوبة. يمكن لتطبيقات مختلفة الالتقاط بالتزامن، ولا يؤثر إيقاف تطبيق إلا في جلساته الخاصة. يدعم كل مكون أمامي عملية التقاط واحدة معلقة أو نشطة في كل مرة. خزِّن كائن Blob المسجَّل باستخدام دالة المضيف uploadFile.

مواقع ويب مضمنة مع إمكانية الوصول إلى الوسائط

لتضمين أداة هاتف أو فيديو، صرّح عن أصل HTTPS الخاص بها في requestedIframeMediaPermissions لتطبيقك، مع ضبط الوصول إلى الميكروفون والكاميرا بشكل منفصل. يمنح تثبيت التطبيق هذه المواقع صلاحية الوصول إلى مساحة العمل. تحافظ تحديثات التطبيقات المنشورة على الصلاحيات الحالية ولا تمنح تلقائيًا إمكانية وصول إضافية إلى الوسائط. تتلقى تطبيقات التطوير المتزامنة محليًا تلقائيًا المواقع المُصرّح بها في ملف البيان الخاص بها. يمكن لعضو لديه صلاحية إدارة التطبيقات الموافقة على الطلبات الجديدة من علامة التبويب عام للتطبيق المثبّت، ضمن الوصول إلى وسائط موقع الويب المضمن > مراجعة الوصول إلى الوسائط. تستخدم هذه الموافقة تأكيدًا أصليًا من المتصفح يعرض التطبيق وأصول مواقع الويب والوصول المطلوب إلى الميكروفون أو الكاميرا. يقرأ Twenty المواقع الممنوحة عند تحميل علامة تبويب، لذا أعد تحميل علامات تبويب Twenty المفتوحة بعد التثبيت أو الموافقة. اطلب الميزات المقابلة على iframe:
للفيديو، استخدم allow="microphone; camera" وصرّح بكليهما للأصل. يفوّض Twenty فقط الوسائط التي مُنح تطبيقك حق الوصول إليها لذلك الأصل. يجب على الأعضاء تحديد عنصر التحكم تمكين الخاص بالتضمين والموافقة على تأكيد أصلي من المتصفح يعرض التطبيق وأصل موقع الويب والوصول المطلوب إلى الميكروفون أو الكاميرا قبل أن يتلقى إمكانية الوصول إلى الوسائط. تظل أذونات المتصفح ونظام التشغيل سارية. لا يمكن لـ srcDoc والتضمينات الموجودة على أصل Twenty نفسه تلقي هذا التفويض. تستخدم عناصر واجهة تخطيط صفحة IFRAME المحددة من التطبيق الأصول الممنوحة للتطبيق المالك لها والتدفق نفسه للتأكيد. تظهر التضمينات الممكّنة في مؤشر الرأس نفسه المستخدم للالتقاط الأصلي، مع تم تمكين الوصول إلى الميكروفون أو تم تمكين الوصول إلى الكاميرا بدلًا من الادعاء بأن التسجيل نشط. يبقى المؤشر مرئيًا ما دام الوصول مُمكّنًا، حتى إذا كان التضمين مخفيًا. يؤدي تعطيل إلى إلغاء تحميل iframe وإعادة تحميله مع حظر الوسائط. يستمر التقاط التطبيق عبر getUserMedia() في الإبلاغ عن الالتقاط الفعلي ويحتفظ بعنصر التحكم إيقاف الخاص به. يبقى محتوى iframe في بيئة معزولة. تحتفظ تضمينات الوسائط الخارجية المعتمدة بأصلها الخاص بعد أن يفعّل العضو الوصول، مما يسمح لموقع الويب باستخدام جلسته وطلب أذونات وسائط المتصفح. تحتفظ إطارات iframe الأخرى لمكوّنات الواجهة بأصل بيئة معزولة معتم.

فجوات أخرى

  • حمولات السحب والإفلات. أحداث السحب تُطلق، لكن event.dataTransfer هي undefined.
  • الوحدات المدمجة في Node. fs وpath وnode:crypto تفشل في عملية البناء، لذا انقل ذلك العمل إلى دالة منطقية. تحل Web Crypto محل node:crypto، ويتوفر أيضًا كل من fetch وTextEncoder وURL، راجع النطاق العام — لكن لا شيء يحل محل fs: لا تحتوي البيئة المعزولة على نظام ملفات.