كيفية تطوير مكون إضافي لـ”DocSpace” – دليل شامل خطوة بخطوة
في عالم يُقاس فيه النجاح بسرعة التعاون، لم تعد أدوات الإنتاجية خيارًا بل ضرورة. وتأتي المكونات الإضافية في “DocSpace” لتقدّم أسلوبًا عصريًا لتحسين طريقة إدارة المستندات والتفاعل معها. في هذا الدليل، سنأخذكم في جولة عملية لإنشاء مكون إضافي مخصّص لـ”DocSpace” باستخدام أمثلة واقعية من مستودع النماذج الرسمية.
حول حزمة “SDK” لمكونات “DocSpace” الإضافية من “ONLYOFFICE”
حزمة “ONLYOFFICE DocSpace Plugins SDK” هي حزمة “npm” مبنية على محركات “TypeScript”، وتوفّر واجهات برمجية تساعدكم على تطوير مكوناتكم الإضافية الخاصة ودمجها داخل بوابة “DocSpace”. تتضمن دورة تطوير المكون الإضافي مراحل التخطيط والتطوير والاختبار ثم الاستخدام.
ولتوضيح طريقة استخدام الحزمة، أنشأنا مستودعًا يحتوي على مجموعة من النماذج الجاهزة. دعونا نستعرض العملية الكاملة لتطوير المكونات الإضافية استنادًا إلى هذه النماذج.
التخطيط
ابدؤوا بتحديد الهدف من المكون الإضافي، وفكّروا في المكان الذي سيتم الوصول إليه منه، والخدمات الخارجية التي يحتاج إلى الاتصال بها.
الخطوة 1. تثبيت جميع الحزم والبرامج المطلوبة
- “ONLYOFFICE DocSpace” على الخادم المحلي.
- حزمة @onlyoffice/docspace-plugin-sdk من “npm”
لتثبيت الحزمة بشكل عام على الجهاز، يمكنكم تنفيذ الأمر التالي:
npm i -g @onlyoffice/docspace-plugin-sdk
الخطوة 2. تصميم آلية عمل المكون الإضافي
اختاروا الخدمة التي ستضيفون من خلالها الوظائف المطلوبة إلى “DocSpace”.
على سبيل المثال، في نماذجنا، استخدمنا الخدمات التالية:
- “AssemblyAI”: لتحويل الكلام من ملفات الصوت والفيديو إلى نص.
- “ConvertAPI”: لتحويل المستندات والجداول والعروض التقديمية والنماذج إلى صيغة “PDF”.
- “Draw.io”: لإنشاء الرسوم البيانية وتعديلها وإدراجها بطريقة احترافية.
تنويه مهم: تأكدوا من توفر الوثائق الرسمية للخدمة التي تستخدمونها، وراجعوا تراخيص الاستخدام، وتوفر واجهات “API” المناسبة. بعض الخدمات تتطلب الحصول على مفتاح “API” لاستخدامها في المكون الإضافي.
فكّروا في أين سيتم تنفيذ المكون الإضافي، وما هي بنيته، وكيف سيتفاعل المستخدمون مع مكوّناته. ضعوا قائمة بأنواع المكونات الإضافية والعناصر المطلوبة بناءً على هذه المعلومات. لمزيد من التفاصيل، يمكنكم الرجوع إلى قسمي “أنواع المكونات الإضافية” و“عناصر المكون الإضافي” في وثائق “Plugins SDK”.
على سبيل المثال، في مكون التحويل من صوت إلى نص، استخدمنا الواجهات التالية:
- “IPlugin”: الواجهة الأساسية المطلوبة لكل مكون، تحتوي على متغيّر “PluginStatus” الذي يدمج المكون في DocSpace.
- “ApiPlugin”: مطلوبة لتكامل المكون مع خدمة خارجية.
- “ISettingsPlugin” و“ISettings”: لإنشاء إعدادات قابلة للتعديل من قبل المستخدمين عبر المسار: الإعدادات -> التكامل -> المكونات الإضافية.
- “IContextMenuPlugin” و“IContextMenuItem”: تُستخدم لتنفيذ إجراء في قائمة السياق. في مكون التحويل من صوت إلى نص، ستكون متاحة لملفات الفيديو والصوت، مما يتيح للمستخدمين تحويل المحتوى إلى نص.
وقد تشمل القائمة واجهات إضافية حسب نوع المكون. على سبيل المثال، في مكون “draw.io”:
- “IMainButtonPlugin” و“IMainButtonItem”: تُستخدم لتنفيذ إجراء الزر الرئيسي. في مكون “draw.io”، نستخدم زر “الإجراء” -> عناصر قائمة “المزيد” في قسم “مستنداتي” أو في الغرفة المحددة لإنشاء رسومات “draw.io”.
- “IFilePlugin” و“IFileItem”: تُستخدم للتعامل مع أنواع الملفات المحددة. في هذه الحالة، مع ملفات “.“.drawio
ضعوا تصورًا واضحًا لبنية المكون. كل الملفات المطلوبة مذكورة في الوثائق، أما الباقي فيمكن تنظيمه كما ترونه مناسبًا. على سبيل المثال، في مكون “draw.io” تم تقسيم الكود إلى مجلدات حسب نوع المكون، بينما تم وضعه في مجلد “src” فقط في مكون التحويل من صوت إلى نص.
أخيرًا، اختاروا اسمًا ووصفًا واضحًا للمكون.
مرحلة التطوير
بعد الانتهاء من جميع خطوات التحضير، يمكنكم البدء في تطوير المكون الإضافي الخاص بكم.
الخطوة 1. إنشاء قالب للمكون الإضافي
أنشئوا قالبًا للمكون الإضافي وقوموا بضبط إعداداته التي ستُعرض في إعدادات مكونات “DocSpace” الإضافية:
لإنشاء قالب مكون إضافي، افتحوا الطرفية (“Terminal”) وشغّلوا الأمر التالي:
npx create-docspace-plugin
إذا لم يكن أمر “npx” متاحًا، يمكنكم تثبيت الحزمة @onlyoffice/docspace-plugin-sdk عالميًا باستخدام الأمر التالي:
npm i -g @onlyoffice/docspace-plugin-sdk
قوموا بضبط إعدادات المكون الإضافي من خلال الطرفية عبر تحديد الخيارات المطلوبة في الحوار. جميع الإعدادات مشروحة هنا.
بالنسبة لمكون التحويل من صوت إلى نص، يمكنكم استخدام القيم التالية:
مع العلم أن بإمكانكم تعديل جميع المعلمات المحددة لاحقًا داخل ملف “package.json”.
كما يمكنكم أيضًا إنشاء مكون إضافي ضمن أي مشروع عن طريق إضافة الحزمة @onlyoffice/docspace-plugin-sdk كاعتمادية (“dependency”) وتحديد جميع الحقول المطلوبة داخل ملف “package.json”.
الخطوة 2. ضبط نقطة الدخول للمكون الإضافي
سيتم إنشاء ملف “index.ts” كنقطة دخول للمكون الإضافي تلقائيًا داخل مجلد “src” خلال خطوة إنشاء القالب. هذا الملف ضروري.
يحتوي هذا الملف على جميع الوظائف الأساسية لأنواع المكونات الإضافية التي قمتم بتحديدها مسبقًا. يمكنكم تعديله في أي وقت.
إذا قمتم بإنشاء المكون الإضافي يدويًا دون استخدام القالب، يمكنكم استخدام الكود من عينات المكونات الجاهزة لدينا كنقطة دخول، وسيعمل بكفاءة.
الخطوة 3. إضافة أيقونات المكون الإضافي
أنشئوا مجلدًا باسم “assets” داخل المجلد الجذري للمكون الإضافي وأضيفوا فيه جميع أيقونات المكون. عدد الأيقونات وأحجامها يعتمد على أنواع المكونات التي قمتم بتنفيذها. بالنسبة لمكون التحويل من صوت إلى نص، نحتاج إلى الأيقونات التالية:
- النوع الافتراضي للمكون الإضافي يتطلب صورة شعار. وهي تُمثل قيمة الحقل “logo” في ملف “package.json”. سيُعرض هذا الشعار في إعدادات مكونات “DocSpace”. الحجم المطلوب للأيقونة هو 48×48 بكسل، وإذا كان غير ذلك فسيتم ضغطها لهذا الحجم.
- مكون قائمة السياق يستخدم أيقونة في زر “التحويل إلى نص”. الحجم المطلوب هو 16×16 بكسل، وإلا فسيتم ضغطها إلى هذا الحجم.
يمكن أيضًا استخدام هذه الأيقونة كأيقونة للزر الرئيسي. على سبيل المثال، في مكون “draw.io”، نستخدم نفس الأيقونة لقائمة السياق وزر القائمة الرئيسي.
يستخدم مكون “draw.io” أيضًا أيقونة خاصة تظهر بجانب ملفات “”.drawio التي يتم إنشاؤها باستخدام نوع مكون الملفات. الحجم المفضل للأيقونة في هذا السياق هو 32×32 بكسل.
الخطوة 4. ضبط عناصر واجهة الاستخدام في المكون
على سبيل المثال، يحتوي مكون “draw.io” على عنصرين رئيسيين في واجهة المستخدم: نافذة منبثقة ومحرر المخططات. أنشئوا الملفات اللازمة لضبط إعدادات كل عنصر. ولتسهيل التنظيم، يمكن وضع هذه الملفات داخل مجلد منفصل باسم “DrawIO”.
في ملف “Dialog.ts”، قوموا بضبط إعدادات النافذة المنبثقة. حدّدوا مكون “IFrame” لعرض موقع “draw.io” داخل النافذة:
export const frameProps: IFrame = {
width: "100%",
height: "100%",
name: "test-drawio",
src: "",
}
أنشئوا حاوية “IBox” لإضافة “iframe” بداخلها:
const body: IBox = {
widthProp: "100%",
heightProp: "100%",
children: [
{
component: Components.iFrame,
props: frameProps,
},
],
}
قوموا بضبط خصائص النافذة المنبثقة.
export const drawIoModalDialogProps: IModalDialog = {
dialogHeader: "",
dialogBody: body,
displayType: ModalDisplayType.modal,
fullScreen: true,
onClose: () => {
const message: IMessage = {
actions: [Actions.closeModal],
}
return message
},
onLoad: async () => {
return {
newDialogHeader: drawIoModalDialogProps.dialogHeader || "",
newDialogBody: drawIoModalDialogProps.dialogBody,
}
},
autoMaxHeight: true,
autoMaxWidth: true,
في ملف “Editor.ts”، قوموا بضبط إعدادات محرر المخططات. أنشئوا دالة باسم “DiagramEditor” تتضمن المعلمات التالية:
المعامل | النوع | المثال | الوصف |
ui | string | “default” | يحدد نمط واجهة المستخدم (“UI”) للمحرر. |
dark | string | “auto” | يحدد النمط الداكن (“Dark Theme”) للمحرر. |
off | boolean | false | يحدد ما إذا كان وضع عدم الاتصال مفعلًا أم لا. |
lib | boolean | false | يحدد ما إذا كانت المكتبات مفعلة أم لا. |
lang | string | “auto” | يحدد لغة المحرر. |
url | string | `https://embed.diagrams.net` | يحدد عنوان “URL” الخاص بالمحرر. |
showSaveButton | boolean | true | يحدد ما إذا كان سيتم عرض زر حفظ داخل المحرر. |
ثم قوموا بتحديد الدوال اللازمة للتعامل مع المخططات:
الدالة | الوصف |
startEditing | يبدأ تشغيل المحرر باستخدام البيانات المحددة. |
getData | يُرجع بيانات المخطط. |
getTitle | يُرجع عنوان المخطط. |
getFormat | يُرجع تنسيق (صيغة) المخطط. |
getFrameId | يُرجع معرّف إطار المحرر. |
getFrameUrl | يُرجع عنوان “URL” الخاص بـ”iframe”. |
handleMessage | يتعامل مع الرسالة المُعطاة. |
initializeEditor | يرسل رسالة “load” إلى المحرر. |
save | يحفظ البيانات المحددة. |
يمكنكم العثور على الكود الكامل لدالة “DiagramEditor” من خلال هذا الرابط.
الخطوة 5. إنشاء أنواع المكونات الإضافية
بعد الانتهاء من إعداد المكون الإضافي الأساسي، يمكنكم البدء في برمجة أنواع المكونات الإضافية الأخرى.
لكل نوع من المكونات الإضافية عناصر خاصة به. قوموا بتعريف عنصر قائمة السياق الذي سيظهر عند النقر بزر الفأرة الأيمن على ملفات الصوت أو الفيديو:
export const contextMenuItem: IContextMenuItem = {
key: "speech-to-text-context-menu-item",
label: "Convert to text",
icon: "speech-to-text-16.png",
onClick: assemblyAI.speechToText,
fileType: [FilesType.video],
withActiveItem: true,
}
يمكنكم إضافة أنواع مكونات إضافية أخرى. على سبيل المثال، يمكن الوصول إلى مكون “draw.io” من قائمة الزر الرئيسي، لذا يجب تحديد عنصر الزر الرئيسي:
const mainButtonItem: IMainButtonItem = {
key: "draw-io-main-button-item",
label: "Draw.io",
icon: "drawio.png",
onClick: (id: number) => {
drawIo.setCurrentFolderId(id)
const message: IMessage = {
actions: [Actions.showCreateDialogModal],
createDialogProps: {
title: "Create diagram",
startValue: "New diagram",
visible: true,
isCreateDialog: true,
extension: ".drawio",
onSave: async (e: any, value: string) => {
await drawIo.createNewFile(value)
},
onCancel: (e: any) => {
drawIo.setCurrentFolderId(null)
},
onClose: (e: any) => {
drawIo.setCurrentFolderId(null)
},
},
}
return message
},
// items: [createItem],
}
عند النقر على عنصر الزر الرئيسي، تظهر نافذة منبثقة يمكنكم من خلالها إدخال اسم المخطط وفتح ملف “.drawio” فارغ.
وبالنسبة لمكون “draw.io”، يجب أيضًا تكوين نوع مكون الملفات الذي يعمل عند فتح المستخدم لملف “.drawio” محدد:
قوموا بتحديد عنصر الملف الذي يُمثَّل كملف بامتداد (“.drawio”) ورمز مخصص:
export const drawIoItem: IFileItem = {
extension: ".drawio",
fileTypeName: "Diagram",
fileRowIcon: "drawio-32.svg",
fileTileIcon: "drawio-32.svg",
devices: [Devices.desktop],
onClick,
}
حددوا حدث “onClick” الذي سينفذ دالة “editDiagram” في كل مرة يتم فيها فتح ملف “.drawio”:
const onClick = async (item: File) => {
return await drawIo.editDiagram(item.id)
}
الخطوة 6. إنشاء نوع مكون الإعدادات
قوموا بتكوين نوع مكون الإعدادات لتوفير إعدادات المسؤول للمستخدمين.
أنشئوا حاوية تحتوي على إعدادات المكون الإضافي:
const descriptionText: TextGroup = {
component: Components.text,
props: {
text: "To generate API token visit https://www.assemblyai.com",
color: "#A3A9AE",
fontSize: "12px",
fontWeight: 400,
lineHeight: "16px",
},
}
const descGroup: BoxGroup = {
component: Components.box,
props: {children: [descriptionText]},
}
const parentBox: IBox = {
displayProp: "flex",
flexDirection: "column",
// marginProp: "16px 0 0 0",
children: [tokenGroup, descGroup],
}
في وصف الإعدادات، بيّنوا ضرورة توليد رمز “API” ليتمكن المستخدم من استخدام المكون الإضافي.
قوموا بتكوين إعدادات المسؤول باستخدام الكائن “ISettings”:
const adminSettings: ISettings = {
settings: parentBox,
saveButton: userButtonComponent,
onLoad: async () => {
assemblyAI.fetchAPIToken()
tokenInput.value = assemblyAI.apiToken
if (!assemblyAI.apiToken) {
return {
settings: parentBox,
}
}
plugin.setAdminPluginSettings(adminSettings)
return {settings: parentBox}
}
حددوا حدث “onLoad” الذي يُعرّف إعدادات المكون الإضافي التي ستُعرض عند تحميل قسم الإعدادات.
كل إعداد يتم تحديده في ملفات منفصلة (مثل الأزرار، الرمز).
الخطوة 7. إنشاء الملف الأساسي للمكون الإضافي
أنشئوا ملفًا في مجلد “src” يحتوي على الكود الأساسي للمكون الإضافي. هذا الملف إلزامي. قوموا بالرجوع إلى وثائق الخدمة الخارجية عند كتابة هذا الكود.
لنلقِ نظرة على كيفية تنظيم ملف “AssemblyAI.ts” بالتفصيل:
قوموا بتعريف الصنف “AssemblyAI” بجميع المتغيرات والدوال اللازمة:
- المتغيرات ووصفها:
“apiURL”
يحدد عنوان واجهة برمجة التطبيقات
apiURL = ""
“currentFileId”
يحدد مُعرّف الملف الحالي.
const currentFileId: numbernull | number = null
“apiToken”
يحدد رمز واجهة برمجة التطبيقات
apiToken = ""
- الدوال ووصفها:
“createAPIUrl”
ينشئ عنوان واجهة برمجة التطبيقات.
createAPIUrl = () => {
const api = plugin.getAPI()
this.apiURL = api.origin.replace(/\/+$/, "")
const params = [api.proxy, api.prefix]
if (this.apiURL) {
for (const part of params) {
if (!part) {
continue
}
const newPart = part.trim().replace(/^\/+/, "")
if (newPart) {
if (this.apiURL.length !== 0 && this.apiURL[this.apiURL.length - 1] === "/") {
this.apiURL += newPart
} else {
this.apiURL += `/${newPart}`
}
}
}
}
}
“setAPIUrl”
يضبط عنوان واجهة برمجة التطبيقات.
setAPIUrl = (url: string) => {
this.apiURL = url
}
“getAPIUrl”
يُرجع عنوان واجهة برمجة التطبيقات.
getAPIUrl = () => {
return this.apiURL
}
“setAPIToken”
يضبط رمز “API”.
setAPIToken = (apiToken: string) => {
this.apiToken = apiToken
}
“getAPIToken”
يُرجع رمز “API”.
getAPIToken = () => {
return this.apiToken
}
“fetchAPIToken”
يسترجع رمز “API”.
fetchAPIToken = async () => {
const apiToken = localStorage.getItem("speech-to-text-api-token")
if (!apiToken) {
return
}
this.setAPIToken(apiToken)
plugin.updateStatus(PluginStatus.active)
}
“saveAPIToken”
يحفظ رمز “API”.
saveAPIToken = (apiToken: string) => {
localStorage.setItem("speech-to-text-api-token", apiToken)
let status
if (apiToken) {
status = PluginStatus.active
} else {
status = PluginStatus.hide
}
plugin.updateStatus(status)
}
“setCurrentFileId”
يضبط مُعرّف الملف الحالي.
setCurrentFileId = (id: number | null) => {
this.currentFileId = id
}
“uploadFile”
يرفع الملف ليتم تفريغه.
uploadFile = async (apiToken: string, path: string, data: Blob) => {
console.log(`Uploading file: ${path}`)
const url = "https://api.assemblyai.com/v2/upload"
try {
const response = await fetch(url, {
method: "POST",
body: data,
headers: {
"Content-Type": "application/octet-stream",
"Authorization": apiToken,
},
})
if (response.status === 200) {
const responseData = await response.json()
return responseData["upload_url"]
}
console.error(`Error: ${response.status} - ${response.statusText}`)
return null
} catch (error) {
console.error(`Error: ${error}`)
return null
}
}
“transcribeAudio”
يُفرغ ملف الصوت إلى نص.
transcribeAudio = async (apiToken: string, audioUrl: string) => {
console.log("Transcribing audio... This might take a moment.")
const headers = {
"authorization": apiToken,
"content-type": "application/json",
}
const response = await fetch("https://api.assemblyai.com/v2/transcript", {
method: "POST",
body: JSON.stringify({audioUrl}),
headers,
})
const responseData = await response.json()
const transcriptId = responseData.id
const pollingEndpoint = `https://api.assemblyai.com/v2/transcript/${transcriptId}`
while (true) {
const pollingResponse = await fetch(pollingEndpoint, {headers})
const transcriptionResult = await pollingResponse.json()
if (transcriptionResult.status === "completed") {
return transcriptionResult
} else if (transcriptionResult.status === "error") {
throw new Error(`Transcription failed: ${transcriptionResult.error}`)
} else {
await new Promise((resolve) => {
setTimeout(resolve, 3000)
})
}
}
}
“speechToText”
يطبق الوظيفة الكاملة للمكون الإضافي.
speechToText = async (id: number) => {
if (!this.apiToken) {
return
}
this.setCurrentFileId(null)
if (!this.apiURL) {
this.createAPIUrl()
}
const response = await fetch(`${this.apiURL}/files/file/${id}`)
const data = await response.json()
const {viewUrl, title, folderId, fileExst} = data.response
const file = await fetch(viewUrl)
const fileBlob = await file.blob()
const uploadUrl = await this.uploadFile(this.apiToken, viewUrl, fileBlob)
const transcript = await this.transcribeAudio(this.apiToken, uploadUrl)
const blob = new Blob([transcript.text], {
type: "text/plain;charset=UTF-8",
})
const newFile = new File([blob], "blob", {
type: "",
lastModified: Date.now(),
})
const formData = new FormData()
formData.append("file", newFile)
const newTitle = `${title.replaceAll(fileExst, "")} text`
try {
const sessionRes = await fetch(
`${this.apiURL}/files/${folderId}/upload/create_session`,
{
method: "POST",
headers: {
"Content-Type": "application/json;charset=utf-8",
},
body: JSON.stringify({
createOn: new Date(),
fileName: `${newTitle}.txt`,
fileSize: newFile.size,
relativePath: "",
}),
},
)
const response = await sessionRes.json()
const sessionData = response.response.data
const data = await fetch(`${sessionData.location}`, {
method: "POST",
body: formData,
})
const jsonData = await data.json()
const {id: fileId} = jsonData.data
return fileId
} catch (e) {
console.log(e)
}
return {
actions: [Actions.showToast],
toastProps: [{type: ToastType.success, title: ""}],
} as IMessage
}
أعلنوا عن نسخة من صنف “AssemblyAI”:
const assemblyAI = new AssemblyAI()
ثم قوموا بتصدير النسخة المُنشأة من المكون الإضافي:
export default assemblyAI
الاختبار
للتحقق من وظيفة المكون الإضافي وإصلاح أي أخطاء محتملة، قوموا باختبار المكون الإضافي كما يلي:
- قوموا ببناء المكون الإضافي المُعد مسبقًا وفقًا للإرشادات المذكورة هنا.
سيتم إنشاء مجلد “dist” في المجلد الجذري للمكون الإضافي، وسيتم وضع أرشيف المكون الإضافي النهائي بداخله. هذا الأرشيف يمثل المكون الإضافي الجاهز للرفع إلى بوابة “DocSpace”.
- قوموا برفع المكون الإضافي إلى بوابة “DocSpace” واختبروا شكله ووظائفه بدقة.
ملاحظة مهمة: يمكنكم رفع المكونات الإضافية الخاصة بكم فقط في إصدار “DocSpace” الخادمي.
إذا ظهرت أية أخطاء، قوموا بإصلاح كود المصدر للمكون الإضافي، ثم أعيدوا بناءه واختباره من جديد.
والآن بعد أن تم اختبار المكون الإضافي ويعمل بشكل سليم، يمكنكم إضافته إلى إصدار “DocSpace” الخادمي والبدء باستخدامه.
يُعد مكون “DocSpace” الإضافي أداة قوية وسهلة الاستخدام لإدارة المستندات والتعاون. ومن خلال تكامله مع المنصات المفضلة للمستخدمين، يُسهم في إزالة العقبات الشائعة وتحسين الإنتاجية في مختلف سير العمل. إذا كانت لديكم أي استفسارات حول مكونات “DocSpace” الإضافية، لا تترددوا في التواصل مع مطورينا على منتدى “ONLYOFFICE” (التسجيل مطلوب). يمكنكم أيضًا طلب ميزة جديدة أو الإبلاغ عن مشكلة عبر نشر قضية على “GitHub”، أو مشاركة مكوناتكم الإضافية مع فريقنا والمجتمع.
نتمنى لكم التوفيق في رحلتكم الاستكشافية!
ONLYOFFICE ١. أنشئ حسابك المجاني من
،٢. قم بعرض و تحرير أو التعاون على المستندات، الجداول ، العروض التقديمية