1715 lines
85 KiB
Plaintext
1715 lines
85 KiB
Plaintext
---
|
|
title: بناء التطبيقات
|
|
description: عرّف الكائنات، والدوال المنطقية، ومكوّنات الواجهة الأمامية، وغير ذلك باستخدام Twenty SDK.
|
|
---
|
|
|
|
<Warning>
|
|
التطبيقات حاليًا في مرحلة الألفا. الميزة تعمل لكنها لا تزال قيد التطور.
|
|
</Warning>
|
|
|
|
توفر حزمة `twenty-sdk` لبنات بناء مضبوطة الأنواع لإنشاء تطبيقك. تغطي هذه الصفحة كل نوع كيان وكل عميل واجهة برمجة تطبيقات متاح في SDK.
|
|
|
|
## دوال DefineEntity
|
|
|
|
يوفّر SDK دوالًا لتعريف كيانات تطبيقك. يجب عليك استخدام `export default defineEntity({...})` لكي يكتشف SDK الكيانات الخاصة بك. تتحقق هذه الدوال من تكوينك وقت البناء وتوفّر إكمالًا تلقائيًا في بيئة التطوير وأمان الأنواع.
|
|
|
|
<Note>
|
|
**تنظيم الملفات يعود إليك.**
|
|
يعتمد اكتشاف الكيانات على AST — حيث يعثر SDK على استدعاءات `export default defineEntity(...)` بغض النظر عن مكان وجود الملف. تجميع الملفات حسب النوع (مثلًا، `logic-functions/` و`roles/`) هو مجرّد عرف، وليس متطلبًا.
|
|
</Note>
|
|
|
|
<AccordionGroup>
|
|
<Accordion title="defineRole" description="تهيئة صلاحيات الدور والوصول إلى الكائنات">
|
|
|
|
تُغلّف الأدوار الصلاحيات على كائنات وإجراءات مساحة العمل لديك.
|
|
|
|
```ts restricted-company-role.ts
|
|
import {
|
|
defineRole,
|
|
PermissionFlag,
|
|
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
|
} from 'twenty-sdk';
|
|
|
|
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,
|
|
},
|
|
],
|
|
permissionFlags: [PermissionFlag.APPLICATIONS],
|
|
});
|
|
```
|
|
|
|
</Accordion>
|
|
<Accordion title="defineApplication" description="تهيئة بيانات التعريف للتطبيق (مطلوب، واحد لكل تطبيق)">
|
|
|
|
يجب أن يحتوي كل تطبيق على استدعاء واحد فقط لـ `defineApplication` يصف:
|
|
|
|
* **الهوية**: المعرّفات، اسم العرض، والوصف.
|
|
* **الأذونات**: أيُّ دورٍ تستخدمه وظائفه ومكوّناته الأمامية.
|
|
* **(اختياري) المتغيرات**: أزواج مفتاح-قيمة تُعرض لوظائفك كمتغيرات بيئة.
|
|
* **(اختياري) دوال ما قبل التثبيت/ما بعد التثبيت**: دوال منطقية تعمل قبل التثبيت أو بعده.
|
|
|
|
```ts src/application-config.ts
|
|
import { defineApplication } from 'twenty-sdk';
|
|
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
|
|
|
export default defineApplication({
|
|
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
|
displayName: 'My Twenty App',
|
|
description: 'My first Twenty app',
|
|
icon: 'IconWorld',
|
|
applicationVariables: {
|
|
DEFAULT_RECIPIENT_NAME: {
|
|
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
|
|
description: 'Default recipient name for postcards',
|
|
value: 'Jane Doe',
|
|
isSecret: false,
|
|
},
|
|
},
|
|
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
|
});
|
|
```
|
|
|
|
الملاحظات:
|
|
* حقول `universalIdentifier` هي معرّفات حتمية تملكها أنت. أنشِئها مرة واحدة واحتفظ بها ثابتة عبر عمليات المزامنة.
|
|
* `applicationVariables` تصبح متغيرات بيئة لوظائفك ومكوّناتك الأمامية (على سبيل المثال، `DEFAULT_RECIPIENT_NAME` متاح كـ `process.env.DEFAULT_RECIPIENT_NAME`).
|
|
* `defaultRoleUniversalIdentifier` يجب أن يُشير إلى دور مُعرَّف باستخدام `defineRole()` (انظر أعلاه).
|
|
* يتم اكتشاف دوال ما قبل التثبيت وما بعده تلقائيًا أثناء بناء البيان — لا حاجة للإشارة إليها في `defineApplication()`.
|
|
|
|
#### بيانات التعريف لسوق التطبيقات
|
|
|
|
إذا كنت تخطط لـ [نشر تطبيقك](/l/ar/developers/extend/apps/publishing)، فإن هذه الحقول الاختيارية تتحكّم في كيفية ظهوره في السوق:
|
|
|
|
| الحقل | الوصف |
|
|
| ------------------ | ------------------------------------------------------------------------------------------------------------ |
|
|
| `author` | اسم المؤلف أو الشركة |
|
|
| `category` | فئة التطبيق لتصفية سوق التطبيقات |
|
|
| `logoUrl` | مسار شعار تطبيقك (مثلًا، `public/logo.png`) |
|
|
| `screenshots` | مصفوفة لمسارات لقطات الشاشة (مثلًا، `public/screenshot-1.png`) |
|
|
| `aboutDescription` | وصف ماركداون أطول لعلامة التبويب "حول". إذا لم يتم تضمينه، يستخدم السوق ملف `README.md` الخاص بالحزمة من npm |
|
|
| `websiteUrl` | رابط إلى موقعك الإلكتروني |
|
|
| `termsUrl` | رابط إلى شروط الخدمة |
|
|
| `emailSupport` | عنوان البريد الإلكتروني للدعم |
|
|
| `issueReportUrl` | رابط إلى متتبّع المشاكل |
|
|
|
|
#### الأدوار والصلاحيات
|
|
|
|
يُحدّد الحقل `defaultRoleUniversalIdentifier` في `application-config.ts` الدور الافتراضي الذي تستخدمه وظائف المنطق والمكوّنات الأمامية في تطبيقك. راجع `defineRole` أعلاه للحصول على التفاصيل.
|
|
|
|
* رمز وقت التشغيل المحقون باسم `TWENTY_APP_ACCESS_TOKEN` مستمد من هذا الدور.
|
|
* العميل مضبوط الأنواع مقيَّد بالأذونات الممنوحة لذلك الدور.
|
|
* اتبع مبدأ أقل الامتياز: أنشئ دورًا مخصصًا يضم فقط الأذونات التي تحتاجها وظائفك.
|
|
|
|
##### الدور الافتراضي للوظيفة
|
|
|
|
عند توليد تطبيق جديد بالقالب، ينشئ CLI ملفّ دور افتراضي:
|
|
|
|
```ts src/roles/default-role.ts
|
|
import { defineRole, PermissionFlag } from 'twenty-sdk';
|
|
|
|
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
|
'b648f87b-1d26-4961-b974-0908fd991061';
|
|
|
|
export default defineRole({
|
|
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: [],
|
|
permissionFlags: [],
|
|
});
|
|
```
|
|
|
|
يُشار إلى `universalIdentifier` لهذا الدور في `application-config.ts` باسم `defaultRoleUniversalIdentifier`:
|
|
|
|
* **\*.role.ts** يحدد ما يمكن أن يفعله الدور.
|
|
* **application-config.ts** يشير إلى ذلك الدور بحيث ترث وظائفك أذوناته.
|
|
|
|
الملاحظات:
|
|
* ابدأ من الدور المُنشأ بالقالب، ثم قيّده تدريجيًا باتباع مبدأ أقل الامتياز.
|
|
* استبدل `objectPermissions` و`fieldPermissions` بالكائنات والحقول التي تحتاجها وظائفك فعليًا.
|
|
* `permissionFlags` تتحكم في الوصول إلى القدرات على مستوى المنصة. اجعلها في حدّها الأدنى.
|
|
* اطّلع على مثال عملي: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
|
|
|
|
</Accordion>
|
|
<Accordion title="defineObject" description="تعريف كائنات مخصصة مع حقول">
|
|
|
|
تصف الكائنات المخصصة كلًا من المخطط والسلوك للسجلات في مساحة عملك. استخدم `defineObject()` لتعريف كائنات مع تحقق مدمج:
|
|
|
|
```ts postCard.object.ts
|
|
import { defineObject, FieldType } from 'twenty-sdk';
|
|
|
|
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,
|
|
},
|
|
],
|
|
});
|
|
```
|
|
|
|
النقاط الرئيسية:
|
|
|
|
* استخدم `defineObject()` للحصول على تحقق مدمج ودعم أفضل من IDE.
|
|
* `universalIdentifier` يجب أن يكون فريدًا وثابتًا عبر عمليات النشر.
|
|
* يتطلب كل حقل `name` و`type` و`label` ومعرّف `universalIdentifier` ثابتًا خاصًا به.
|
|
* المصفوفة `fields` اختيارية — يمكنك تعريف كائنات بدون حقول مخصصة.
|
|
* يمكنك إنشاء كائنات جديدة باستخدام `yarn twenty add`، والذي يرشدك خلال التسمية والحقول والعلاقات.
|
|
|
|
<Note>
|
|
**يتم إنشاء الحقول الأساسية تلقائيًا.** عند تعريف كائن مخصص، يضيف Twenty تلقائيًا حقولًا قياسية
|
|
مثل `id` و`name` و`createdAt` و`updatedAt` و`createdBy` و`updatedBy` و`deletedAt`.
|
|
لا تحتاج إلى تعريف هذه في مصفوفة `fields` — أضف فقط حقولك المخصصة.
|
|
يمكنك تجاوز الحقول الافتراضية من خلال تعريف حقل بالاسم نفسه في مصفوفة `fields` الخاصة بك،
|
|
لكن هذا غير مستحسن.
|
|
</Note>
|
|
|
|
</Accordion>
|
|
<Accordion title="defineField — الحقول القياسية" description="وسّع الكائنات الموجودة بحقول إضافية">
|
|
|
|
استخدم `defineField()` لإضافة حقول إلى كائنات لا تملكها — مثل كائنات Twenty القياسية (Person, Company, etc.) أو كائنات من تطبيقات أخرى. على خلاف الحقول المضمّنة في `defineObject()`، تتطلّب الحقول المستقلة `objectUniversalIdentifier` لتحديد الكائن الذي تقوم بتوسيعه:
|
|
|
|
```ts src/fields/company-loyalty-tier.field.ts
|
|
import { defineField, FieldType } from 'twenty-sdk';
|
|
|
|
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` يحدّد الكائن الهدف. بالنسبة للكائنات القياسية، استخدم `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` المُصدَّر من `twenty-sdk`.
|
|
* عند تعريف الحقول بشكل مضمّن في `defineObject()`، **لا** تحتاج إلى `objectUniversalIdentifier` — إذ يُورَّث من الكائن الأب.
|
|
* `defineField()` هي الطريقة الوحيدة لإضافة حقول إلى كائنات لم تُنشئها باستخدام `defineObject()`.
|
|
|
|
</Accordion>
|
|
<Accordion title="defineField — حقول العلاقات" description="وصِل الكائنات معًا بعلاقات ثنائية الاتجاه">
|
|
|
|
تربط العلاقات الكائنات معًا. في 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';
|
|
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';
|
|
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
|
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
|
|
|
|
// Export so the other side can reference it
|
|
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
|
|
// Import from the other side
|
|
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
|
|
|
|
export default defineField({
|
|
universalIdentifier: POST_CARD_FIELD_ID,
|
|
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
|
|
type: FieldType.RELATION,
|
|
name: 'postCard',
|
|
label: 'Post Card',
|
|
icon: 'IconMail',
|
|
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
|
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
|
|
universalSettings: {
|
|
relationType: RelationType.MANY_TO_ONE,
|
|
onDelete: OnDeleteAction.CASCADE,
|
|
joinColumnName: 'postCardId',
|
|
},
|
|
});
|
|
```
|
|
|
|
<Note>
|
|
**الاستيرادات الدائرية:** كلا حقلي العلاقة يُحيل كلٌ منهما إلى `universalIdentifier` الخاص بالآخر. لتجنّب مشكلات الاستيراد الدائري، صدّر معرّفات الحقول كثوابت مسمّاة من كل ملف، واستوردها في الملف الآخر. يقوم نظام البناء بحلّها في وقت الترجمة.
|
|
</Note>
|
|
|
|
#### الربط مع الكائنات القياسية
|
|
|
|
لإنشاء علاقة مع كائن Twenty مضمّن (Person, Company, etc.)، استخدم `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`:
|
|
|
|
```ts src/fields/person-on-self-hosting-user.field.ts
|
|
import {
|
|
defineField,
|
|
FieldType,
|
|
RelationType,
|
|
OnDeleteAction,
|
|
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
|
} from 'twenty-sdk';
|
|
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
|
|
|
|
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
|
|
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
|
|
|
|
export default defineField({
|
|
universalIdentifier: PERSON_FIELD_ID,
|
|
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
|
|
type: FieldType.RELATION,
|
|
name: 'person',
|
|
label: 'Person',
|
|
description: 'Person matching with the self hosting user',
|
|
isNullable: true,
|
|
relationTargetObjectMetadataUniversalIdentifier:
|
|
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
|
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
|
|
universalSettings: {
|
|
relationType: RelationType.MANY_TO_ONE,
|
|
onDelete: OnDeleteAction.SET_NULL,
|
|
joinColumnName: 'personId',
|
|
},
|
|
});
|
|
```
|
|
|
|
#### خصائص حقل العلاقة
|
|
|
|
| الخاصية | مطلوب | الوصف |
|
|
| ------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------- |
|
|
| `type` | نعم | يجب أن يكون `FieldType.RELATION` |
|
|
| `relationTargetObjectMetadataUniversalIdentifier` | نعم | قيمة `universalIdentifier` للكائن الهدف |
|
|
| `relationTargetFieldMetadataUniversalIdentifier` | نعم | قيمة `universalIdentifier` للحقل المطابق على الكائن الهدف |
|
|
| `universalSettings.relationType` | نعم | `RelationType.MANY_TO_ONE` أو `RelationType.ONE_TO_MANY` |
|
|
| `universalSettings.onDelete` | MANY_TO_ONE فقط | ماذا يحدث عند حذف السجل المشار إليه: `CASCADE`، `SET_NULL`، `RESTRICT`، أو `NO_ACTION` |
|
|
| `universalSettings.joinColumnName` | MANY_TO_ONE فقط | اسم عمود قاعدة البيانات للمفتاح الخارجي (مثل `postCardId`) |
|
|
|
|
#### حقول العلاقات المضمّنة في defineObject
|
|
|
|
يمكنك أيضًا تعريف حقول العلاقات مباشرةً داخل `defineObject()`. في هذه الحالة، احذف `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
|
|
],
|
|
});
|
|
```
|
|
</Accordion>
|
|
<Accordion title="defineLogicFunction" description="عرّف الدوال المنطقية ومشغّلاتها">
|
|
|
|
كل ملف وظيفة يستخدم `defineLogicFunction()` لتصدير تكوين مع معالج ومشغّلات اختيارية.
|
|
|
|
```ts src/logic-functions/createPostCard.logic-function.ts
|
|
import { defineLogicFunction } from 'twenty-sdk';
|
|
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
|
|
import { CoreApiClient, type Person } from 'twenty-client-sdk/core';
|
|
|
|
const handler = async (params: RoutePayload) => {
|
|
const client = new CoreApiClient();
|
|
const name = 'name' in params.queryStringParameters
|
|
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
|
|
: '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: 'GET',
|
|
isAuthRequired: false,
|
|
},
|
|
/*databaseEventTriggerSettings: {
|
|
eventName: 'people.created',
|
|
},*/
|
|
/*cronTriggerSettings: {
|
|
pattern: '0 0 1 1 *',
|
|
},*/
|
|
});
|
|
```
|
|
|
|
أنواع المشغّلات المتاحة:
|
|
* **httpRoute**: يعرِض وظيفتك على مسار وطريقة HTTP **تحت نقطة النهاية `/s/`**:
|
|
> مثال: `path: '/post-card/create'` يمكن استدعاؤه عبر `https://your-twenty-server.com/s/post-card/create`
|
|
* **cron**: يشغّل وظيفتك على جدول باستخدام تعبير CRON.
|
|
* **databaseEvent**: يعمل على أحداث دورة حياة كائنات مساحة العمل. عندما تكون عملية الحدث هي `updated`، يمكن تحديد الحقول المحددة المراد الاستماع إليها في مصفوفة `updatedFields`. إذا تُركت غير معرّفة أو فارغة، فسيؤدي أي تحديث إلى تشغيل الدالة.
|
|
> مثال: `person.updated`، `*.created`، `company.*`
|
|
|
|
<Note>
|
|
يمكنك أيضًا تنفيذ دالة يدويًا باستخدام CLI:
|
|
|
|
```bash filename="Terminal"
|
|
yarn twenty exec -n create-new-post-card -p '{"key": "value"}'
|
|
```
|
|
|
|
```bash filename="Terminal"
|
|
yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
|
```
|
|
|
|
يمكنك متابعة السجلات باستخدام:
|
|
|
|
```bash filename="Terminal"
|
|
yarn twenty logs
|
|
```
|
|
</Note>
|
|
|
|
#### حمولة مشغل المسار
|
|
|
|
عندما يستدعي مُشغِّل المسار وظيفتك المنطقية، فإنها تتلقّى كائن `RoutePayload` الذي يتبع [صيغة AWS HTTP API v2](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html).
|
|
استورد نوع `RoutePayload` من `twenty-sdk`:
|
|
|
|
```ts
|
|
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
|
|
|
|
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' }` |
|
|
| `المحتوى` | `object \| null` | جسم الطلب المُحلَّل (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
|
| `isBase64Encoded` | `قيمة منطقية` | ما إذا كان جسم الطلب مُرمَّزًا بترميز base64 | |
|
|
| `requestContext.http.method` | `string` | طريقة HTTP (GET, POST, PUT, PATCH, DELETE) | |
|
|
| `requestContext.http.path` | `string` | المسار الخام للطلب | |
|
|
|
|
|
|
#### forwardedRequestHeaders
|
|
|
|
افتراضيًا، **لا** تُمرَّر رؤوس HTTP من الطلبات الواردة إلى دالتك المنطقية لأسباب أمنية.
|
|
للوصول إلى رؤوس محددة، أدرِجها في مصفوفة `forwardedRequestHeaders`:
|
|
|
|
```ts
|
|
export default defineLogicFunction({
|
|
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
|
name: 'webhook-handler',
|
|
handler,
|
|
httpRouteTriggerSettings: {
|
|
path: '/webhook',
|
|
httpMethod: 'POST',
|
|
isAuthRequired: false,
|
|
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
|
|
},
|
|
});
|
|
```
|
|
|
|
في معالجك، يمكنك الوصول إلى الرؤوس المُمرَّرة بهذه الطريقة:
|
|
|
|
```ts
|
|
const handler = async (event: RoutePayload) => {
|
|
const signature = event.headers['x-webhook-signature'];
|
|
const contentType = event.headers['content-type'];
|
|
|
|
// Validate webhook signature...
|
|
return { received: true };
|
|
};
|
|
```
|
|
|
|
<Note>
|
|
تُحوَّل أسماء الرؤوس إلى أحرف صغيرة. يمكنك الوصول إليها باستخدام مفاتيح بأحرف صغيرة (على سبيل المثال، `event.headers['content-type']`).
|
|
</Note>
|
|
|
|
#### إتاحة دالة كأداة
|
|
|
|
يمكن إتاحة الدوال المنطقية بوصفها **أدوات** لوكلاء الذكاء الاصطناعي وسير العمل. عند تمييز دالة كأداة، تصبح قابلة للاكتشاف بواسطة ميزات الذكاء الاصطناعي في Twenty ويمكن استخدامها في أتمتة سير العمل.
|
|
|
|
لتمييز دالة منطقية كأداة، عيِّن `isTool: true`:
|
|
|
|
```ts src/logic-functions/enrich-company.logic-function.ts
|
|
import { defineLogicFunction } from 'twenty-sdk';
|
|
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,
|
|
isTool: true,
|
|
});
|
|
```
|
|
|
|
النقاط الرئيسية:
|
|
|
|
* يمكنك دمج `isTool` مع المشغِّلات — إذ يمكن للدالة أن تكون أداة (قابلة للاستدعاء من قِبل وكلاء الذكاء الاصطناعي) وأن تُشغَّل بواسطة الأحداث في الوقت نفسه.
|
|
* **`toolInputSchema`** (اختياري): كائن JSON Schema يصف المعلمات التي تقبلها دالتك. يُحسَب المخطط تلقائيًا من خلال تحليل ساكن للشيفرة المصدرية، ولكن يمكنك تعيينه صراحةً:
|
|
|
|
```ts
|
|
export default defineLogicFunction({
|
|
...,
|
|
toolInputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
companyName: {
|
|
type: 'string',
|
|
description: 'The name of the company to enrich',
|
|
},
|
|
domain: {
|
|
type: 'string',
|
|
description: 'The company website domain (optional)',
|
|
},
|
|
},
|
|
required: ['companyName'],
|
|
},
|
|
});
|
|
```
|
|
|
|
<Note>
|
|
**اكتب `description` جيدًا.** يعتمد وكلاء الذكاء الاصطناعي على حقل `description` الخاص بالدالة لتحديد وقت استخدام الأداة. كن محددًا بشأن ما تفعله الأداة ومتى ينبغي استدعاؤها.
|
|
</Note>
|
|
|
|
</Accordion>
|
|
<Accordion title="definePreInstallLogicFunction" description="تعريف دالة منطقية لما قبل التثبيت (واحدة لكل تطبيق)">
|
|
|
|
دالة ما قبل التثبيت هي دالة منطقية تعمل تلقائيًا قبل تثبيت تطبيقك على مساحة عمل. يفيد ذلك في مهام التحقق، وفحص المتطلبات المسبقة، أو تجهيز حالة مساحة العمل قبل متابعة التثبيت الرئيسي.
|
|
|
|
```ts src/logic-functions/pre-install.ts
|
|
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
|
|
|
|
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
|
|
console.log('Pre install logic function executed successfully!', payload.previousVersion);
|
|
};
|
|
|
|
export default definePreInstallLogicFunction({
|
|
universalIdentifier: 'e0604b9e-e946-456b-886d-3f27d9a6b324',
|
|
name: 'pre-install',
|
|
description: 'Runs before installation to prepare the application.',
|
|
timeoutSeconds: 300,
|
|
handler,
|
|
});
|
|
```
|
|
|
|
يمكنك أيضًا تنفيذ دالة ما قبل التثبيت يدويًا في أي وقت باستخدام CLI:
|
|
|
|
```bash filename="Terminal"
|
|
yarn twenty exec --preInstall
|
|
```
|
|
|
|
النقاط الرئيسية:
|
|
* تستخدم دوال ما قبل التثبيت `definePreInstallLogicFunction()` — وهو إصدار متخصص يستبعد إعدادات المُشغِّل (`cronTriggerSettings` و`databaseEventTriggerSettings` و`httpRouteTriggerSettings` و`isTool`).
|
|
* يتلقى المُعالج `InstallLogicFunctionPayload` يحوي `{ previousVersion: string }` — إصدار التطبيق الذي كان مُثبّتًا سابقًا (أو سلسلة فارغة للتثبيتات الجديدة).
|
|
* يُسمح بدالة ما قبل التثبيت واحدة فقط لكل تطبيق. سيُنتج إنشاء ملف البيان خطأً إذا تم اكتشاف أكثر من واحدة.
|
|
* يتم تعيين `universalIdentifier` للدالة تلقائيًا كـ `preInstallLogicFunctionUniversalIdentifier` في بيان التطبيق أثناء الإنشاء — لست بحاجة إلى الإشارة إليه في `defineApplication()`.
|
|
* تم ضبط المهلة الافتراضية على 300 ثانية (5 دقائق) للسماح بمهام التحضير الأطول.
|
|
|
|
</Accordion>
|
|
<Accordion title="definePostInstallLogicFunction" description="تعريف دالة منطقية لما بعد التثبيت (واحدة لكل تطبيق)">
|
|
|
|
دالة ما بعد التثبيت هي دالة منطقية تعمل تلقائيًا بعد تثبيت تطبيقك على مساحة عمل. هذا مفيد لمهام الإعداد لمرة واحدة مثل تهيئة البيانات الافتراضية، وإنشاء السجلات الأولية، أو تكوين إعدادات مساحة العمل.
|
|
|
|
```ts src/logic-functions/post-install.ts
|
|
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
|
|
|
|
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
|
|
console.log('Post install logic function executed successfully!', payload.previousVersion);
|
|
};
|
|
|
|
export default definePostInstallLogicFunction({
|
|
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
|
|
name: 'post-install',
|
|
description: 'Runs after installation to set up the application.',
|
|
timeoutSeconds: 300,
|
|
handler,
|
|
});
|
|
```
|
|
|
|
يمكنك أيضًا تنفيذ دالة ما بعد التثبيت يدويًا في أي وقت باستخدام CLI:
|
|
|
|
```bash filename="Terminal"
|
|
yarn twenty exec --postInstall
|
|
```
|
|
|
|
النقاط الرئيسية:
|
|
* تستخدم دوال ما بعد التثبيت `definePostInstallLogicFunction()` — وهو إصدار متخصص يستبعد إعدادات المُشغِّل (`cronTriggerSettings` و`databaseEventTriggerSettings` و`httpRouteTriggerSettings` و`isTool`).
|
|
* يتلقى المُعالج `InstallLogicFunctionPayload` يحوي `{ previousVersion: string }` — إصدار التطبيق الذي كان مُثبّتًا سابقًا (أو سلسلة فارغة للتثبيتات الجديدة).
|
|
* يُسمح بدالة ما بعد التثبيت واحدة فقط لكل تطبيق. سيُنتج إنشاء ملف البيان خطأً إذا تم اكتشاف أكثر من واحدة.
|
|
* يتم تعيين `universalIdentifier` للدالة تلقائيًا كـ `postInstallLogicFunctionUniversalIdentifier` في بيان التطبيق أثناء الإنشاء — لست بحاجة إلى الإشارة إليه في `defineApplication()`.
|
|
* تم تعيين مهلة افتراضية إلى 300 ثانية (5 دقائق) للسماح بمهام الإعداد الأطول مثل تهيئة البيانات.
|
|
|
|
</Accordion>
|
|
<Accordion title="defineFrontComponent" description="عرِّف مكوّنات أمامية لواجهة مستخدم مخصّصة">
|
|
|
|
المكوّنات الأمامية هي مكوّنات React تُعرَض مباشرة داخل واجهة مستخدم Twenty. تعمل ضمن **Web Worker** معزول باستخدام Remote DOM — تكون شيفرتك في صندوق عزل لكنها تُعرَض أصيلًا داخل الصفحة، وليس ضمن iframe.
|
|
|
|
#### مثال أساسي
|
|
|
|
أسرع طريقة لرؤية مكوّن أمامي قيد العمل هي تسجيله كأمر. إضافة حقل `command` مع `isPinned: true` يجعلُه يظهر كزر إجراء سريع في الزاوية العلوية اليمنى من الصفحة — دون الحاجة إلى تخطيط صفحة:
|
|
|
|
```tsx src/front-components/hello-world.tsx
|
|
import { defineFrontComponent } from 'twenty-sdk';
|
|
|
|
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,
|
|
command: {
|
|
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
|
|
shortLabel: 'Hello',
|
|
label: 'Hello World',
|
|
icon: 'IconBolt',
|
|
isPinned: true,
|
|
availabilityType: 'GLOBAL',
|
|
},
|
|
});
|
|
```
|
|
|
|
بعد المزامنة باستخدام `yarn twenty dev`، يظهر الإجراء السريع في الزاوية العلوية اليمنى من الصفحة:
|
|
|
|
<div style={{textAlign: 'center'}}>
|
|
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="زر إجراء سريع في الزاوية العلوية اليمنى" />
|
|
</div>
|
|
|
|
انقره لعرض المكوّن مضمنًا داخل الصفحة.
|
|
|
|
{/* TODO: add screenshot of the rendered front component */}
|
|
|
|
#### حقول التكوين
|
|
|
|
| الحقل | مطلوب | الوصف |
|
|
| --------------------- | ----- | ----------------------------------------------------------------- |
|
|
| `universalIdentifier` | نعم | معرّف فريد ثابت لهذا المكوّن |
|
|
| `component` | نعم | دالة مكوّن React |
|
|
| `name` | لا | اسم العرض |
|
|
| `الوصف` | لا | وصف لما يفعله المكوّن |
|
|
| `isHeadless` | لا | عيِّنه إلى `true` إذا كان المكوّن بلا واجهة مرئية (انظر أدناه) |
|
|
| `أمر` | لا | سجّل المكوّن كأمر (انظر [خيارات الأوامر](#command-options) أدناه) |
|
|
|
|
#### وضع مكوّن أمامي على صفحة
|
|
|
|
إضافةً إلى الأوامر، يمكنك تضمين مكوّن أمامي مباشرةً في صفحة سجل عبر إضافته كودجت في **تخطيط صفحة**. راجع قسم [definePageLayout](#definepagelayout) للتفاصيل.
|
|
|
|
#### مكوّنات عديمة الواجهة (`isHeadless: true`)
|
|
|
|
المكوّنات عديمة الواجهة لا تُظهر واجهة مرئية لكنها ما تزال تُشغِّل منطق React. يكون هذا مفيدًا لـ **مكوّنات التأثير** — وهي مكوّنات تُجري تأثيرات جانبية عند التثبيت، مثل مزامنة البيانات، بدء مؤقّت، الاستماع إلى الأحداث، أو إطلاق إشعار.
|
|
|
|
```tsx src/front-components/sync-tracker.tsx
|
|
import { defineFrontComponent, useRecordId, enqueueSnackbar } from 'twenty-sdk';
|
|
import { useEffect } from 'react';
|
|
|
|
const SyncTracker = () => {
|
|
const recordId = useRecordId();
|
|
|
|
useEffect(() => {
|
|
enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
|
|
}, [recordId]);
|
|
|
|
return null;
|
|
};
|
|
|
|
export default defineFrontComponent({
|
|
universalIdentifier: '...',
|
|
name: 'sync-tracker',
|
|
description: 'Tracks record views silently',
|
|
isHeadless: true,
|
|
component: SyncTracker,
|
|
});
|
|
```
|
|
|
|
نظرًا لأن المكوّن يُرجع `null`، فإن Twenty يتخطّى عرض حاوية له — ولن تظهر مساحة فارغة في التخطيط. لا يزال لدى المكوّن إمكانية الوصول إلى جميع الخطافات وواجهة برمجة الاتصال مع المضيف.
|
|
|
|
#### الوصول إلى سياق وقت التشغيل
|
|
|
|
داخل مكوّنك، استخدم خطافات SDK للوصول إلى المستخدم الحالي، والسجل، ومثيل المكوّن:
|
|
|
|
```tsx src/front-components/record-info.tsx
|
|
import {
|
|
defineFrontComponent,
|
|
useUserId,
|
|
useRecordId,
|
|
useFrontComponentId,
|
|
} from 'twenty-sdk';
|
|
|
|
const RecordInfo = () => {
|
|
const userId = useUserId();
|
|
const recordId = useRecordId();
|
|
const componentId = useFrontComponentId();
|
|
|
|
return (
|
|
<div>
|
|
<p>User: {userId}</p>
|
|
<p>Record: {recordId ?? 'No record context'}</p>
|
|
<p>Component: {componentId}</p>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default defineFrontComponent({
|
|
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
|
|
name: 'record-info',
|
|
component: RecordInfo,
|
|
});
|
|
```
|
|
|
|
الخطافات المتاحة:
|
|
|
|
| الخطّاف | القيم المعادة | الوصف |
|
|
| --------------------------------------------- | ------------------ | ---------------------------------------------- |
|
|
| `useUserId()` | `string` أو `null` | معرّف المستخدم الحالي |
|
|
| `useRecordId()` | `string` أو `null` | معرّف السجل الحالي (عند وضعه على صفحة سجل) |
|
|
| `useFrontComponentId()` | `string` | معرّف مثيل هذا المكوّن |
|
|
| `useFrontComponentExecutionContext(selector)` | يختلف | الوصول إلى سياق التنفيذ الكامل عبر دالة محدِّد |
|
|
|
|
#### واجهة الاتصال مع المضيف
|
|
|
|
يمكن للمكوّنات الأمامية تشغيل التنقّل والنوافذ المنبثقة والإشعارات باستخدام دوال من `twenty-sdk`:
|
|
|
|
| دالة | الوصف |
|
|
| ----------------------------------------------- | ------------------------------ |
|
|
| `navigate(to, params?, queryParams?, options?)` | الانتقال إلى صفحة داخل التطبيق |
|
|
| `openSidePanelPage(params)` | فتح لوحة جانبية |
|
|
| `closeSidePanel()` | إغلاق اللوحة الجانبية |
|
|
| `openCommandConfirmationModal(params)` | عرض مربع حوار تأكيد |
|
|
| `enqueueSnackbar(params)` | عرض إشعار توست |
|
|
| `unmountFrontComponent()` | إلغاء تركيب المكوّن |
|
|
| `updateProgress(progress)` | تحديث مؤشّر التقدّم |
|
|
|
|
#### خيارات الأوامر
|
|
|
|
إضافة حقل `command` إلى `defineFrontComponent` تُسجِّل المكوّن في قائمة الأوامر (Cmd+K). إذا كانت قيمة `isPinned` هي `true`، فسيظهر أيضًا كزر إجراء سريع في الزاوية العلوية اليمنى من الصفحة.
|
|
|
|
| الحقل | مطلوب | الوصف |
|
|
| --------------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
| `universalIdentifier` | نعم | معرّف فريد ثابت للأمر |
|
|
| `التسمية` | نعم | التسمية الكاملة المعروضة في قائمة الأوامر (Cmd+K) |
|
|
| `shortLabel` | لا | تسمية أقصر تُعرَض على زر الإجراء السريع المثبّت |
|
|
| `أيقونة` | لا | اسم الأيقونة المعروض بجانب التسمية (مثل `'IconBolt'` و`'IconSend'`) |
|
|
| `isPinned` | لا | عند كونها `true`، يعرض الأمر كزر إجراء سريع في الزاوية العلوية اليمنى من الصفحة |
|
|
| `availabilityType` | لا | تتحكّم في مكان ظهور الأمر: `'GLOBAL'` (متاح دائمًا)، و`'RECORD_SELECTION'` (فقط عند تحديد سجلات)، أو `'FALLBACK'` (يُعرَض عند عدم تطابق أي أوامر أخرى) |
|
|
| `availabilityObjectUniversalIdentifier` | لا | تقييد الأمر بصفحات نوع كائن معيّن (مثل سجلات Company فقط) |
|
|
| `conditionalAvailabilityExpression` | لا | تعبير منطقي للتحكم ديناميكيًا في ما إذا كان الأمر مرئيًا (انظر أدناه) |
|
|
|
|
#### تعابير الإتاحة الشرطية
|
|
|
|
يتيح لك الحقل `conditionalAvailabilityExpression` التحكّم في وقت ظهور الأمر بناءً على سياق الصفحة الحالي. استورد متغيّرات ومشغّلات مضبوطة الأنواع من `twenty-sdk` لبناء التعابير:
|
|
|
|
```tsx
|
|
import {
|
|
defineFrontComponent,
|
|
pageType,
|
|
numberOfSelectedRecords,
|
|
objectPermissions,
|
|
everyEquals,
|
|
isDefined,
|
|
} from 'twenty-sdk';
|
|
|
|
export default defineFrontComponent({
|
|
universalIdentifier: '...',
|
|
name: 'bulk-action',
|
|
component: BulkAction,
|
|
command: {
|
|
universalIdentifier: '...',
|
|
label: 'Bulk Update',
|
|
availabilityType: 'RECORD_SELECTION',
|
|
conditionalAvailabilityExpression: everyEquals(
|
|
objectPermissions,
|
|
'canUpdateObjectRecords',
|
|
true,
|
|
),
|
|
},
|
|
});
|
|
```
|
|
|
|
**متغيّرات السياق** — تُمثّل الحالة الحالية للصفحة:
|
|
|
|
| المتغيّر | النوع | الوصف |
|
|
| ------------------------------ | ------------- | --------------------------------------------------------------- |
|
|
| `pageType` | `string` | نوع الصفحة الحالي (مثل `'RecordIndexPage'` و`'RecordShowPage'`) |
|
|
| `isInSidePanel` | `قيمة منطقية` | ما إذا كان المكوّن معروضًا في لوحة جانبية |
|
|
| `numberOfSelectedRecords` | `رقم` | عدد السجلات المحدّدة حاليًا |
|
|
| `isSelectAll` | `قيمة منطقية` | ما إذا كان "تحديد الكل" مفعّلًا |
|
|
| `selectedRecords` | `array` | كائنات السجلات المحدّدة |
|
|
| `favoriteRecordIds` | `array` | معرّفات السجلات المفضّلة |
|
|
| `objectPermissions` | `الكائن` | الأذونات الخاصة بنوع الكائن الحالي |
|
|
| `targetObjectReadPermissions` | `الكائن` | أذونات القراءة للكائن الهدف |
|
|
| `targetObjectWritePermissions` | `الكائن` | أذونات الكتابة للكائن الهدف |
|
|
| `featureFlags` | `الكائن` | أعلام الميزات المفعَّلة |
|
|
| `objectMetadataItem` | `الكائن` | بيانات التعريف لنوع الكائن الحالي |
|
|
| `hasAnySoftDeleteFilterOnView` | `قيمة منطقية` | ما إذا كان العرض الحالي يحتوي على مرشّح حذف منطقي |
|
|
|
|
**المُشغِّلات** — جمّع المتغيّرات في تعابير منطقية:
|
|
|
|
| المُشغِّل | الوصف |
|
|
| ----------------------------------- | -------------------------------------------------------------- |
|
|
| `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` إذا لم تكن الخاصية تساوي القيمة في أي عنصر |
|
|
|
|
#### الأصول العامة
|
|
|
|
يمكن للمكوّنات الأمامية الوصول إلى ملفات من دليل `public/` للتطبيق باستخدام `getPublicAssetUrl`:
|
|
|
|
```tsx
|
|
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk';
|
|
|
|
const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;
|
|
|
|
export default defineFrontComponent({
|
|
universalIdentifier: '...',
|
|
name: 'logo',
|
|
component: Logo,
|
|
});
|
|
```
|
|
|
|
راجع [قسم الأصول العامة](#accessing-public-assets-with-getpublicasseturl) للتفاصيل.
|
|
|
|
#### التنسيق
|
|
|
|
تدعم المكوّنات الأمامية عدة أساليب للتنسيق. يمكنك استخدام:
|
|
|
|
* **أنماط مضمنة** — `style={{ color: 'red' }}`
|
|
* **مكوّنات Twenty لواجهة المستخدم** — استورد من `twenty-sdk/ui` (Button وTag وStatus وChip وAvatar وغيرها)
|
|
* **Emotion** — CSS-in-JS مع `@emotion/react`
|
|
* **Styled-components** — أنماط `styled.div`
|
|
* **Tailwind CSS** — أصناف مساعدة
|
|
* **أي مكتبة CSS-in-JS** متوافقة مع React
|
|
|
|
```tsx
|
|
import { defineFrontComponent } from 'twenty-sdk';
|
|
import { Button, Tag, Status } from 'twenty-sdk/ui';
|
|
|
|
const StyledWidget = () => {
|
|
return (
|
|
<div style={{ padding: '16px', display: 'flex', gap: '8px' }}>
|
|
<Button title="Click me" onClick={() => alert('Clicked!')} />
|
|
<Tag text="Active" color="green" />
|
|
<Status color="green" text="Online" />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default defineFrontComponent({
|
|
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456',
|
|
name: 'styled-widget',
|
|
component: StyledWidget,
|
|
});
|
|
```
|
|
|
|
</Accordion>
|
|
|
|
<Accordion title="defineSkill" description="عرّف مهارات وكيل الذكاء الاصطناعي">
|
|
|
|
تُحدِّد المهارات تعليمات وإمكانات قابلة لإعادة الاستخدام يمكن لوكلاء الذكاء الاصطناعي استخدامها داخل مساحة العمل لديك. استخدم `defineSkill()` لتعريف مهارات مع تحقّق مدمج:
|
|
|
|
```ts src/skills/example-skill.ts
|
|
import { defineSkill } from 'twenty-sdk';
|
|
|
|
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';
|
|
|
|
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` (اختياري) يتجاوز نموذج الذكاء الاصطناعي الافتراضي الذي يستخدمه الوكيل.
|
|
|
|
</Accordion>
|
|
<Accordion title="defineView" description="تعريف العروض المحفوظة للكائنات">
|
|
|
|
العروض هي تكوينات محفوظة لكيفية عرض سجلات كائن ما — بما في ذلك الحقول المرئية وترتيبها وأي مرشّحات أو مجموعات مُطبَّقة. استخدم `defineView()` لتضمين عروض مُهيّأة مسبقًا مع تطبيقك:
|
|
|
|
```ts src/views/example-view.ts
|
|
import { defineView, ViewKey } from 'twenty-sdk';
|
|
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
|
import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
|
|
|
export default defineView({
|
|
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
|
name: 'All example items',
|
|
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
|
icon: 'IconList',
|
|
key: ViewKey.INDEX,
|
|
position: 0,
|
|
fields: [
|
|
{
|
|
universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
|
|
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
|
position: 0,
|
|
isVisible: true,
|
|
size: 200,
|
|
},
|
|
],
|
|
});
|
|
```
|
|
|
|
النقاط الرئيسية:
|
|
* `objectUniversalIdentifier` يحدّد الكائن الذي ينطبق عليه هذا العرض.
|
|
* `key` يحدّد نوع العرض (مثل `ViewKey.INDEX` لعرض القائمة الرئيسي).
|
|
* `fields` يتحكّم في الأعمدة الظاهرة وترتيبها. يشير كل حقل إلى `fieldMetadataUniversalIdentifier`.
|
|
* يمكنك أيضًا تعريف `filters` و`filterGroups` و`groups` و`fieldGroups` لمزيد من التكوينات المتقدمة.
|
|
* `position` يتحكّم في الترتيب عند وجود عدة عروض لنفس الكائن.
|
|
|
|
</Accordion>
|
|
<Accordion title="defineNavigationMenuItem" description="تعريف روابط التنقل في الشريط الجانبي">
|
|
|
|
تضيف عناصر قائمة التنقل إدخالات مخصّصة إلى الشريط الجانبي لمساحة العمل. استخدم `defineNavigationMenuItem()` للارتباط بالعروض أو عناوين URL خارجية أو الكائنات:
|
|
|
|
```ts src/navigation-menu-items/example-navigation-menu-item.ts
|
|
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk';
|
|
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` لعرض محفوظ، أو `NavigationMenuItemType.LINK` لعنوان URL خارجي.
|
|
* لروابط العروض، عيِّن `viewUniversalIdentifier`. لروابط خارجية، عيِّن `link`.
|
|
* `position` يتحكّم في الترتيب ضمن الشريط الجانبي.
|
|
* `icon` و`color` (اختياريان) يخصّصان المظهر.
|
|
|
|
</Accordion>
|
|
<Accordion title="definePageLayout" description="عرّف تخطيطات صفحات مخصّصة لعرض السجلات">
|
|
|
|
تتيح لك تخطيطات الصفحات تخصيص مظهر صفحة تفاصيل السجل — ما الألسنة التي تظهر، وما الويدجتات داخل كل لسان، وكيف يتم ترتيبها. استخدم `definePageLayout()` لتضمين تخطيطات مخصّصة مع تطبيقك:
|
|
|
|
```ts src/page-layouts/example-record-page-layout.ts
|
|
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk';
|
|
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
|
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
|
|
|
export default definePageLayout({
|
|
universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
|
|
name: 'Example Record Page',
|
|
type: 'RECORD_PAGE',
|
|
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
|
tabs: [
|
|
{
|
|
universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
|
|
title: 'Hello World',
|
|
position: 50,
|
|
icon: 'IconWorld',
|
|
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
|
widgets: [
|
|
{
|
|
universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
|
|
title: 'Hello World',
|
|
type: 'FRONT_COMPONENT',
|
|
configuration: {
|
|
configurationType: 'FRONT_COMPONENT',
|
|
frontComponentUniversalIdentifier:
|
|
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
```
|
|
|
|
النقاط الرئيسية:
|
|
* `type` يكون عادة `'RECORD_PAGE'` لتخصيص عرض التفاصيل لكائن محدّد.
|
|
* `objectUniversalIdentifier` يحدّد الكائن الذي ينطبق عليه هذا التخطيط.
|
|
* يُعرّف كل `tab` قسمًا من الصفحة مع `title` و`position` و`layoutMode` (`CANVAS` لتخطيط حرّ).
|
|
* يمكن لكل `widget` داخل لسان أن يعرض مكوّنًا أماميًا أو قائمة علاقات أو أنواع ويدجت مدمجة أخرى.
|
|
* `position` على الألسنة يتحكّم في ترتيبها. استخدم قيمًا أعلى (مثل 50) لوضع الألسنة المخصّصة بعد الألسنة المدمجة.
|
|
|
|
</Accordion>
|
|
</AccordionGroup>
|
|
|
|
## الأصول العامة (مجلد `public/`)
|
|
|
|
يحتوي مجلد `public/` في جذر تطبيقك على ملفات ثابتة — صور وأيقونات وخطوط وأي أصول أخرى يحتاجها تطبيقك وقت التشغيل. تُدرج هذه الملفات تلقائيًا في عمليات البناء، وتُزامَن أثناء وضع التطوير، وتُرفَع إلى الخادم.
|
|
|
|
الملفات الموضوعة في `public/` هي:
|
|
|
|
* **متاحة للعامة** — بمجرد مزامنتها إلى الخادم، تُقدَّم الأصول عبر عنوان URL عام. لا يلزم توثيق للوصول إليها.
|
|
* **متاحة في المكوّنات الأمامية** — استخدم عناوين الأصول لعرض الصور أو الأيقونات أو أي وسائط داخل مكوّنات React لديك.
|
|
* **متاحة في الدوال المنطقية** — أشِر إلى عناوين الأصول في رسائل البريد الإلكتروني أو استجابات واجهات البرمجة أو أي منطق على جهة الخادم.
|
|
* **مستخدمة لبيانات تعريف السوق** — يشير حقلا `logoUrl` و`screenshots` في `defineApplication()` إلى ملفات من هذا المجلد (مثل `public/logo.png`). تُعرَض هذه عند نشر تطبيقك في السوق.
|
|
* **تُزامَن تلقائيًا في وضع التطوير** — عند إضافة ملف في `public/` أو تحديثه أو حذفه، تتم مزامنته إلى الخادم تلقائيًا. لا حاجة لإعادة التشغيل.
|
|
* **مضمَّنة في عمليات البناء** — يقوم `yarn twenty build` بتجميع جميع الأصول العامة ضمن مخرجات التوزيع.
|
|
|
|
### الوصول إلى الأصول العامة باستخدام `getPublicAssetUrl`
|
|
|
|
استخدم المساعد `getPublicAssetUrl` من `twenty-sdk` للحصول على العنوان الكامل لملف في دليل `public/` لديك. يعمل ذلك في كلٍ من الدوال المنطقية والمكوّنات الأمامية.
|
|
|
|
**في دالة منطقية:**
|
|
|
|
```ts src/logic-functions/send-invoice.ts
|
|
import { defineLogicFunction, getPublicAssetUrl } from 'twenty-sdk';
|
|
|
|
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, getPublicAssetUrl } from 'twenty-sdk';
|
|
|
|
export default defineFrontComponent(() => {
|
|
const logoUrl = getPublicAssetUrl('logo.png');
|
|
|
|
return <img src={logoUrl} alt="App logo" />;
|
|
});
|
|
```
|
|
|
|
وسيطة `path` نسبية إلى مجلد `public/` الخاص بتطبيقك. كلٌّ من `getPublicAssetUrl('logo.png')` و`getPublicAssetUrl('public/logo.png')` يُحلاّن إلى العنوان نفسه — تتم إزالة بادئة `public/` تلقائيًا إن وُجدت.
|
|
|
|
## استخدام حِزَم 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';
|
|
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';
|
|
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,
|
|
});
|
|
```
|
|
|
|
### كيف يعمل التجميع
|
|
|
|
تستخدم خطوة البناء (`yarn twenty dev` أو `yarn twenty build`) أداة esbuild لإنتاج ملف واحد مستقل لكل دالة منطقية وكل مكوّن أمامي. تُضمَّن جميع الحزم المستوردة داخل الحزمة.
|
|
|
|
**الدوال المنطقية** تعمل في بيئة Node.js. الوحدات المدمجة في Node (`fs` و`path` و`crypto` و`http` وغيرها) متاحة ولا تحتاج إلى تثبيت.
|
|
|
|
**المكوّنات الأمامية** تعمل ضمن Web Worker. وحدات Node المدمجة غير متاحة — المتاح فقط واجهات برمجة المتصفّح وحِزَم npm التي تعمل في بيئة المتصفّح.
|
|
|
|
كلتا البيئتين تحتويان على `twenty-client-sdk/core` و`twenty-client-sdk/metadata` كوحدات متاحة مُسبقًا — لا تُضمَّن هذه ضمن الحزم بل تُحلّ وقت التشغيل بواسطة الخادم.
|
|
|
|
## توليد قوالب الكيانات باستخدام `yarn twenty add`
|
|
|
|
بدلًا من إنشاء ملفات الكيانات يدويًا، يمكنك استخدام أداة القوالب التفاعلية:
|
|
|
|
```bash filename="Terminal"
|
|
yarn twenty add
|
|
```
|
|
|
|
ستطالبك باختيار نوع الكيان وتُرشدك خلال الحقول المطلوبة. تُولّد ملفًا جاهزًا للاستخدام مع `universalIdentifier` ثابت واستدعاء `defineEntity()` الصحيح.
|
|
|
|
يمكنك أيضًا تمرير نوع الكيان مباشرة لتخطي المطالبة الأولى:
|
|
|
|
```bash filename="Terminal"
|
|
yarn twenty add object
|
|
yarn twenty add logicFunction
|
|
yarn twenty add frontComponent
|
|
```
|
|
|
|
### أنواع الكيانات المتاحة
|
|
|
|
| نوع الكيان | أمر | الملف المُولَّد |
|
|
| ------------------ | ------------------------------------ | ------------------------------------- |
|
|
| كائن | `yarn twenty add object` | `src/objects/<name>.ts` |
|
|
| الحقل | `yarn twenty add field` | `src/fields/<name>.ts` |
|
|
| دالة منطقية | `yarn twenty add logicFunction` | `src/logic-functions/<name>.ts` |
|
|
| مكوّن أمامي | `yarn twenty add frontComponent` | `src/front-components/<name>.tsx` |
|
|
| دور | `yarn twenty add role` | `src/roles/<name>.ts` |
|
|
| مهارة | `yarn twenty add skill` | `src/skills/<name>.ts` |
|
|
| وكيل | `yarn twenty add agent` | `src/agents/<name>.ts` |
|
|
| عرض | `yarn twenty add view` | `src/views/<name>.ts` |
|
|
| عنصر قائمة التنقّل | `yarn twenty add navigationMenuItem` | `src/navigation-menu-items/<name>.ts` |
|
|
| تخطيط الصفحة | `yarn twenty add pageLayout` | `src/page-layouts/<name>.ts` |
|
|
|
|
### ما الذي تُنشئه أداة القوالب
|
|
|
|
لكل نوع كيان قالب خاص به. على سبيل المثال، يسأل `yarn twenty add object` عن:
|
|
|
|
1. **الاسم (مفرد)** — مثل `invoice`
|
|
2. **الاسم (جمع)** — مثل `invoices`
|
|
3. **التسمية (مفرد)** — تُستمد تلقائيًا من الاسم (مثل `Invoice`)
|
|
4. **التسمية (جمع)** — تُملأ تلقائيًا (مثل `Invoices`)
|
|
5. **إنشاء عرض وعنصر تنقّل؟** — إذا أجبت بنعم، فستُنشئ أداة القوالب أيضًا عرضًا مطابقًا ورابط شريط جانبي للكائن الجديد.
|
|
|
|
أنواع الكيانات الأخرى لها مطالبات أبسط — فمعظمها يطلب اسمًا فقط.
|
|
|
|
نوع الكيان `field` أكثر تفصيلاً: يطلب اسم الحقل وتسمية الحقل ونوعه (من قائمة بكل أنواع الحقول المتاحة مثل `TEXT` و`NUMBER` و`SELECT` و`RELATION` وغيرها)، ومعرّف `universalIdentifier` للكائن الهدف.
|
|
|
|
### مسار خرج مخصّص
|
|
|
|
استخدم العلم `--path` لوضع الملف المُولَّد في موقع مخصّص:
|
|
|
|
```bash filename="Terminal"
|
|
yarn twenty add logicFunction --path src/custom-folder
|
|
```
|
|
|
|
## عملاء واجهة برمجة تطبيقات مضبوطة الأنواع (`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 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 build` أولًا، فسيؤدي ذلك إلى خطأ. تحدث عملية التوليد تلقائيًا — إذ يستطلع CLI مخطط GraphQL لمساحة عملك وينشئ عميلًا مضبوط الأنواع باستخدام `@genql/cli`.
|
|
</Note>
|
|
|
|
#### استخدام CoreSchema للتعليقات التوضيحية للأنواع
|
|
|
|
`CoreSchema` يوفّر أنواع TypeScript المطابقة لكائنات مساحة العمل لديك — مفيد لتعيين أنواع حالة المكوّن أو معاملات الدوال:
|
|
|
|
```ts
|
|
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
|
|
import { useState } from 'react';
|
|
|
|
const [company, setCompany] = useState<
|
|
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
|
|
>(undefined);
|
|
|
|
const client = new CoreApiClient();
|
|
const result = await client.query({
|
|
company: {
|
|
__args: { filter: { position: { eq: 1 } } },
|
|
id: true,
|
|
name: true,
|
|
},
|
|
});
|
|
setCompany(result.company);
|
|
```
|
|
|
|
</Accordion>
|
|
<Accordion title="MetadataApiClient" description="إعدادات مساحة العمل، والتطبيقات، ورفع الملفات">
|
|
|
|
يأتي `MetadataApiClient` مُجهّزًا مسبقًا مع SDK (لا حاجة للتوليد). يستعلم عن نقطة النهاية `/metadata` للحصول على تكوين مساحة العمل والتطبيقات ورفع الملفات.
|
|
|
|
```ts
|
|
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
|
|
|
const metadataClient = new MetadataApiClient();
|
|
|
|
// List first 10 objects in the workspace
|
|
const { objects } = await metadataClient.query({
|
|
objects: {
|
|
edges: {
|
|
node: {
|
|
id: true,
|
|
nameSingular: true,
|
|
namePlural: true,
|
|
labelSingular: true,
|
|
isCustom: true,
|
|
},
|
|
},
|
|
__args: {
|
|
filter: {},
|
|
paging: { first: 10 },
|
|
},
|
|
},
|
|
});
|
|
```
|
|
|
|
#### رفع الملفات
|
|
|
|
يتضمن `MetadataApiClient` طريقة `uploadFile` لإرفاق الملفات بالحقول من نوع الملف:
|
|
|
|
```ts
|
|
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
|
import * as fs from 'fs';
|
|
|
|
const metadataClient = new MetadataApiClient();
|
|
|
|
const fileBuffer = fs.readFileSync('./invoice.pdf');
|
|
|
|
const uploadedFile = await metadataClient.uploadFile(
|
|
fileBuffer, // file contents as a Buffer
|
|
'invoice.pdf', // filename
|
|
'application/pdf', // MIME type
|
|
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
|
|
);
|
|
|
|
console.log(uploadedFile);
|
|
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
|
|
```
|
|
|
|
| المعلمة | النوع | الوصف |
|
|
| ---------------------------------- | -------- | ---------------------------------------------------------------------- |
|
|
| `fileBuffer` | `Buffer` | المحتوى الخام للملف |
|
|
| `filename` | `string` | اسم الملف (يُستخدم للتخزين والعرض) |
|
|
| `contentType` | `string` | نوع MIME (القيمة الافتراضية `application/octet-stream` إذا لم يُحدَّد) |
|
|
| `fieldMetadataUniversalIdentifier` | `string` | قيمة `universalIdentifier` لحقل نوع الملف في كائنك |
|
|
|
|
النقاط الرئيسية:
|
|
* يستخدم `universalIdentifier` الخاص بالحقل (وليس معرّفه الخاص بمساحة العمل)، بحيث يعمل كود الرفع لديك عبر أي مساحة عمل مُثبَّت فيها تطبيقك.
|
|
* العنوان `url` المُعاد هو عنوان URL موقّع يمكنك استخدامه للوصول إلى الملف المرفوع.
|
|
|
|
</Accordion>
|
|
</AccordionGroup>
|
|
|
|
<Note>
|
|
عند تشغيل كودك على Twenty (وظائف منطقية أو مكوّنات أمامية)، يقوم النظام الأساسي بحقن بيانات الاعتماد كمتغيرات بيئية:
|
|
|
|
* `TWENTY_API_URL` — عنوان URL الأساسي لواجهة Twenty البرمجية
|
|
* `TWENTY_APP_ACCESS_TOKEN` — مفتاح قصير العمر ذو نطاق يقتصر على الدور الافتراضي لوظيفة تطبيقك
|
|
|
|
لست **بحاجة** إلى تمرير هذه القيم إلى العملاء — فهي تُقرأ تلقائيًا من `process.env`. تُحدَّد أذونات مفتاح واجهة برمجة التطبيقات بواسطة الدور المشار إليه في `defaultRoleUniversalIdentifier` ضمن `application-config.ts`.
|
|
</Note>
|
|
|
|
## اختبار تطبيقك
|
|
|
|
يوفّر SDK واجهات برمجة قابلة للتنفيذ برمجيًا تمكّنك من بناء تطبيقك ونشره وتثبيته وإلغاء تثبيته من شيفرة الاختبار. بالاقتران مع [Vitest](https://vitest.dev/) وعملاء واجهة البرمجة مضبوطي الأنواع، يمكنك كتابة اختبارات تكامل تتحقّق من أن تطبيقك يعمل من البداية إلى النهاية مقابل خادم Twenty حقيقي.
|
|
|
|
### إعداد
|
|
|
|
يتضمّن التطبيق المُولَّد بالقالب بالفعل Vitest. إذا أعددته يدويًا، فثبّت التبعيات:
|
|
|
|
```bash filename="Terminal"
|
|
yarn add -D vitest vite-tsconfig-paths
|
|
```
|
|
|
|
أنشئ `vitest.config.ts` في جذر تطبيقك:
|
|
|
|
```ts vitest.config.ts
|
|
import tsconfigPaths from 'vite-tsconfig-paths';
|
|
import { defineConfig } from 'vitest/config';
|
|
|
|
export default defineConfig({
|
|
plugins: [
|
|
tsconfigPaths({
|
|
projects: ['tsconfig.spec.json'],
|
|
ignoreConfigErrors: true,
|
|
}),
|
|
],
|
|
test: {
|
|
testTimeout: 120_000,
|
|
hookTimeout: 120_000,
|
|
include: ['src/**/*.integration-test.ts'],
|
|
setupFiles: ['src/__tests__/setup-test.ts'],
|
|
env: {
|
|
TWENTY_API_URL: 'http://localhost:2020',
|
|
TWENTY_API_KEY: 'your-api-key',
|
|
},
|
|
},
|
|
});
|
|
```
|
|
|
|
أنشئ ملف إعداد يتحقّق من إمكانية الوصول إلى الخادم قبل تشغيل الاختبارات:
|
|
|
|
```ts src/__tests__/setup-test.ts
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { beforeAll } from 'vitest';
|
|
|
|
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
|
|
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
|
|
|
|
beforeAll(async () => {
|
|
// Verify the server is running
|
|
const response = await fetch(`${TWENTY_API_URL}/healthz`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
|
|
'Start the server before running integration tests.',
|
|
);
|
|
}
|
|
|
|
// Write a temporary config for the SDK
|
|
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
|
|
|
fs.writeFileSync(
|
|
path.join(TEST_CONFIG_DIR, 'config.json'),
|
|
JSON.stringify({
|
|
remotes: {
|
|
local: {
|
|
apiUrl: process.env.TWENTY_API_URL,
|
|
apiKey: process.env.TWENTY_API_KEY,
|
|
},
|
|
},
|
|
defaultRemote: 'local',
|
|
}, null, 2),
|
|
);
|
|
});
|
|
```
|
|
|
|
### واجهات SDK البرمجية
|
|
|
|
يُصدِّر المسار الفرعي `twenty-sdk/cli` دوالًا يمكنك استدعاؤها مباشرةً من شيفرة الاختبار:
|
|
|
|
| دالة | الوصف |
|
|
| -------------- | ----------------------------------------- |
|
|
| `appBuild` | بناء التطبيق واختياريًا حزم ملف tarball |
|
|
| `appDeploy` | رفع ملف tarball إلى الخادم |
|
|
| `appInstall` | تثبيت التطبيق على مساحة العمل النشطة |
|
|
| `appUninstall` | إلغاء تثبيت التطبيق من مساحة العمل النشطة |
|
|
|
|
تُرجع كل دالة كائن نتيجة يحتوي على `success: boolean` وعلى إمّا `data` أو `error`.
|
|
|
|
### كتابة اختبار تكامل
|
|
|
|
إليك مثالًا كاملًا يبني التطبيق وينشره ويثبّته، ثم يتحقّق من ظهوره في مساحة العمل:
|
|
|
|
```ts src/__tests__/app-install.integration-test.ts
|
|
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
|
|
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
|
|
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
|
|
const APP_PATH = process.cwd();
|
|
|
|
describe('App installation', () => {
|
|
beforeAll(async () => {
|
|
const buildResult = await appBuild({
|
|
appPath: APP_PATH,
|
|
tarball: true,
|
|
onProgress: (message: string) => console.log(`[build] ${message}`),
|
|
});
|
|
|
|
if (!buildResult.success) {
|
|
throw new Error(`Build failed: ${buildResult.error?.message}`);
|
|
}
|
|
|
|
const deployResult = await appDeploy({
|
|
tarballPath: buildResult.data.tarballPath!,
|
|
onProgress: (message: string) => console.log(`[deploy] ${message}`),
|
|
});
|
|
|
|
if (!deployResult.success) {
|
|
throw new Error(`Deploy failed: ${deployResult.error?.message}`);
|
|
}
|
|
|
|
const installResult = await appInstall({ appPath: APP_PATH });
|
|
|
|
if (!installResult.success) {
|
|
throw new Error(`Install failed: ${installResult.error?.message}`);
|
|
}
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await appUninstall({ appPath: APP_PATH });
|
|
});
|
|
|
|
it('should find the installed app in the workspace', async () => {
|
|
const metadataClient = new MetadataApiClient();
|
|
|
|
const result = await metadataClient.query({
|
|
findManyApplications: {
|
|
id: true,
|
|
name: true,
|
|
universalIdentifier: true,
|
|
},
|
|
});
|
|
|
|
const installedApp = result.findManyApplications.find(
|
|
(app: { universalIdentifier: string }) =>
|
|
app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
|
|
);
|
|
|
|
expect(installedApp).toBeDefined();
|
|
});
|
|
});
|
|
```
|
|
|
|
### تشغيل الاختبارات
|
|
|
|
تأكّد من تشغيل خادم Twenty المحلي لديك، ثم:
|
|
|
|
```bash filename="Terminal"
|
|
yarn test
|
|
```
|
|
|
|
أو في وضع المراقبة أثناء التطوير:
|
|
|
|
```bash filename="Terminal"
|
|
yarn test:watch
|
|
```
|
|
|
|
### التحقق من الأنواع
|
|
|
|
يمكنك أيضًا تشغيل التحقق من الأنواع على تطبيقك دون تشغيل الاختبارات:
|
|
|
|
```bash filename="Terminal"
|
|
yarn twenty typecheck
|
|
```
|
|
|
|
يشغِّل هذا الأمر `tsc --noEmit` ويبلغ عن أي أخطاء في الأنواع.
|
|
|
|
## مرجع CLI
|
|
|
|
بالإضافة إلى `dev` و`build` و`add` و`typecheck`، يوفّر CLI أوامر لتنفيذ الدوال وعرض السجلات وإدارة تثبيتات التطبيقات.
|
|
|
|
### تنفيذ الدوال (`yarn twenty exec`)
|
|
|
|
تشغيل دالة منطقية يدويًا دون تشغيلها عبر HTTP أو cron أو حدث قاعدة بيانات:
|
|
|
|
```bash filename="Terminal"
|
|
# Execute by function name
|
|
yarn twenty exec -n create-new-post-card
|
|
|
|
# Execute by universalIdentifier
|
|
yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
|
|
|
# Pass a JSON payload
|
|
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
|
|
|
|
# Execute pre-install or post-install functions
|
|
yarn twenty exec --preInstall
|
|
yarn twenty exec --postInstall
|
|
```
|
|
|
|
### عرض سجلات الدوال (`yarn twenty logs`)
|
|
|
|
بثّ سجلات التنفيذ لدوال تطبيقك المنطقية:
|
|
|
|
```bash filename="Terminal"
|
|
# Stream all function logs
|
|
yarn twenty logs
|
|
|
|
# Filter by function name
|
|
yarn twenty logs -n create-new-post-card
|
|
|
|
# Filter by universalIdentifier
|
|
yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
|
```
|
|
|
|
<Note>
|
|
يختلف هذا عن `yarn twenty server logs`، الذي يعرض سجلات حاوية Docker. يعرض `yarn twenty logs` سجلات تنفيذ دوال تطبيقك من خادم Twenty.
|
|
</Note>
|
|
|
|
### إلغاء تثبيت تطبيق (`yarn twenty uninstall`)
|
|
|
|
أزل تطبيقك من مساحة العمل النشطة:
|
|
|
|
```bash filename="Terminal"
|
|
yarn twenty uninstall
|
|
|
|
# Skip the confirmation prompt
|
|
yarn twenty uninstall --yes
|
|
```
|