diff --git a/src/Umbraco.Web.UI.Client/devops/icons/index.js b/src/Umbraco.Web.UI.Client/devops/icons/index.js index 7829a4d89e..a37ebf36db 100644 --- a/src/Umbraco.Web.UI.Client/devops/icons/index.js +++ b/src/Umbraco.Web.UI.Client/devops/icons/index.js @@ -163,7 +163,7 @@ const writeIconsToDisk = (icons) => { } // eslint-disable-next-line no-undef - console.log(`icon: ${icon.name} generated`); + //console.log(`icon: ${icon.name} generated`); }); }); }; @@ -188,7 +188,7 @@ const generateJS = (icons) => { } // eslint-disable-next-line no-undef - console.log('icon manifests generated'); + console.log('Icons outputted and Icon Manifests generated!'); }); }; diff --git a/src/Umbraco.Web.UI.Client/examples/block-custom-view/block-custom-view.ts b/src/Umbraco.Web.UI.Client/examples/block-custom-view/block-custom-view.ts index 78de43087d..8f0e4a11e7 100644 --- a/src/Umbraco.Web.UI.Client/examples/block-custom-view/block-custom-view.ts +++ b/src/Umbraco.Web.UI.Client/examples/block-custom-view/block-custom-view.ts @@ -32,7 +32,8 @@ export class ExampleBlockCustomView extends UmbElementMixin(LitElement) implemen display: block; height: 100%; box-sizing: border-box; - background-color: #dddddd; + background-color: red; + color: white; border-radius: 9px; padding: 12px; } diff --git a/src/Umbraco.Web.UI.Client/examples/block-custom-view/index.ts b/src/Umbraco.Web.UI.Client/examples/block-custom-view/index.ts index 9bf95dc267..b015618ffe 100644 --- a/src/Umbraco.Web.UI.Client/examples/block-custom-view/index.ts +++ b/src/Umbraco.Web.UI.Client/examples/block-custom-view/index.ts @@ -5,6 +5,6 @@ export const manifests: Array = [ name: 'Block Editor Custom View Test', element: () => import('./block-custom-view.js'), forContentTypeAlias: 'headlineUmbracoDemoBlock', - forBlockEditor: 'block-list', + forBlockEditor: ['block-list', 'block-grid'], }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/apps/installer/shared/layout/installer-layout.element.ts b/src/Umbraco.Web.UI.Client/src/apps/installer/shared/layout/installer-layout.element.ts index 8e6805d877..f10b4fcc35 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/installer/shared/layout/installer-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/installer/shared/layout/installer-layout.element.ts @@ -49,6 +49,7 @@ export class UmbInstallerLayoutElement extends LitElement { top: var(--uui-size-space-5); left: var(--uui-size-space-5); height: 30px; + z-index: 10; } #logo img { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/ar.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/ar.ts index 0951ffd59e..64bb555510 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/ar.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/ar.ts @@ -2,18 +2,18 @@ import type { UmbLocalizationDictionary } from '@umbraco-cms/backoffice/localiza export default { actions: { - assigndomain: 'الثقافة وأسماء المضيفين', + assigndomain: 'الثقافة وأسماء النطاقات', auditTrail: 'سجل التدقيق', - browse: 'تصفح العقدة', + browse: 'تصفح ', changeDataType: 'تغيير نوع البيانات', - changeDocType: 'تغيير نوع المستند', + changeDocType: 'تغيير نوع الوثيقة', chooseWhereToCopy: 'اختر مكان النسخ', chooseWhereToImport: 'اختر مكان الاستيراد', chooseWhereToMove: 'اختر مكان النقل', copy: 'نسخ', copyTo: 'نسخ إلى', create: 'إنشاء', - createblueprint: 'إنشاء مخطط المستند', + createblueprint: 'إنشاء مخطط انشائي للمستند', createGroup: 'إنشاء مجموعة', createPackage: 'إنشاء حزمة', delete: 'حذف', @@ -23,17 +23,17 @@ export default { emptyrecyclebin: 'إفراغ سلة المحذوفات', enable: 'تمكين', export: 'تصدير', - exportDocumentType: 'تصدير نوع المستند', + exportDocumentType: 'تصدير نوع الوثيقة', folderCreate: 'إنشاء مجلد', folderDelete: 'حذف مجلد', folderRename: 'إعادة تسمية مجلد', import: 'استيراد', - importdocumenttype: 'استيراد نوع المستند', + importdocumenttype: 'استيراد نوع وثيقة', importPackage: 'استيراد حزمة', infiniteEditorChooseWhereToCopy: 'اختر مكان نسخ العنصر (العناصر) المحددة', infiniteEditorChooseWhereToMove: 'اختر مكان نقل العنصر (العناصر) المحددة', - liveEdit: 'تحرير في القماش', - logout: 'خروج', + liveEdit: 'تحرير مباشر', + logout: 'تسجيل خروج', move: 'نقل إلى', notify: 'الإشعارات', protect: 'الوصول العام', @@ -50,11 +50,11 @@ export default { sendToTranslate: 'إرسال للترجمة', setGroup: 'تعيين مجموعة', setPermissions: 'تعيين الأذونات', - sort: 'فرز الأطفال', - toInTheTreeStructureBelow: 'في هيكل الشجرة أدناه', + sort: 'ترتيب الابناء', + toInTheTreeStructureBelow: 'في هيكل الشجري أدناه', translate: 'ترجمة', trash: 'سلة المهملات', - unlock: 'فتح', + unlock: 'فتح القفل', unpublish: 'إلغاء النشر', update: 'تحديث', wasCopiedTo: 'تم نسخه إلى', @@ -68,10 +68,10 @@ export default { other: 'أخرى', }, actionDescriptions: { - assignDomain: 'السماح بالوصول لتعيين الثقافة وأسماء المضيفين', + assignDomain: 'السماح بالوصول لتعيين الثقافة وأسماء النطاقات', auditTrail: 'السماح بالوصول لعرض سجل تاريخ العقدة', browse: 'السماح بالوصول لعرض العقدة', - changeDocType: 'السماح بالوصول لتغيير نوع المستند للعقدة', + changeDocType: 'السماح بالوصول لتغيير نوع وثيقة للعقدة', copy: 'السماح بالوصول لنسخ العقدة', create: 'السماح بالوصول لإنشاء العقد', delete: 'السماح بالوصول لحذف العقد', @@ -86,7 +86,7 @@ export default { sort: 'السماح بالوصول لتغيير ترتيب العقد', translate: 'السماح بالوصول لترجمة العقدة', update: 'السماح بالوصول لحفظ العقدة', - createblueprint: 'السماح بالوصول لإنشاء مخطط المستند', + createblueprint: 'السماح بالوصول لإنشاء مخطط الوثيقة', notify: 'السماح بالوصول لإعداد الإشعارات لعقد المحتوى', }, apps: { @@ -547,9 +547,9 @@ export default { createNew: 'إنشاء عنصر قاموس جديد', }, dictionaryItem: { - description: "\n تحرير الإصدارات اللغوية المختلفة لعنصر القاموس '%0%' أدناه\n ", + description: "تحرير الإصدارات اللغوية المختلفة لعنصر القاموس '%0%' أدناه", displayName: 'اسم الثقافة', - changeKeyError: "\n المفتاح '%0%' موجود بالفعل.\n ", + changeKeyError: "المفتاح '%0%' موجود بالفعل.", overviewTitle: 'نظرة عامة على القاموس', }, examineManagement: { @@ -578,7 +578,7 @@ export default { processIsTakingLonger: 'تستغرق العملية وقتًا أطول من المتوقع، تحقق من سجل Umbraco لمعرفة ما إذا كانت هناك أي أخطاء خلال هذه العملية', indexCannotRebuild: 'لا يمكن إعادة بناء هذا الفهرس لأنه ليس لديه تعيين', - iIndexPopulator: 'IIndexPopulator', + iIndexPopulator: 'معبئ الفهارس', }, placeholders: { username: 'أدخل اسم المستخدم الخاص بك', @@ -633,7 +633,7 @@ export default { hasReferencesDeleteConsequence: 'حذف %0% سيؤدي إلى حذف الخصائص وبياناتها من العناصر التالية', acceptDeleteConsequence: - 'أفهم أن هذا الإجراء سيؤدي إلى حذف الخصائص والبيانات المستندة إلى هذا النوع من البيانات\n \n ', + 'أفهم أن هذا الإجراء سيؤدي إلى حذف الخصائص والبيانات الوثائق إلى هذا النوع من البيانات\n \n ', }, errorHandling: { errorButDataWasSaved: @@ -652,7 +652,7 @@ export default { errorRegExpWithoutTab: '%0% ليس بالتنسيق الصحيح', }, errors: { - defaultError: 'حدث فشل غير معروف', + defaultError: 'حدث خطاء غير معروف', concurrencyError: 'فشل التزامن المتفائل، تم تعديل الكائن', receivedErrorFromServer: 'تم استلام خطأ من الخادم', dissallowedMediaType: 'تم رفض نوع الملف المحدد من قبل المسؤول', @@ -881,7 +881,7 @@ export default { addEditor: 'إضافة محرر', addTemplate: 'إضافة قالب', addChildNode: 'إضافة عقدة فرعية', - addChild: 'إضافة طفل', + addChild: 'إضافة ابن', editDataType: 'تحرير نوع البيانات', navigateSections: 'التنقل بين الأقسام', shortcut: 'اختصارات', @@ -942,13 +942,13 @@ export default { permissionsSettingUpPermissions: "إعداد أذونات المجلدات", permissionsText: "يحتاج Umbraco إلى أذونات الكتابة/التعديل لبعض الأدلة لتخزين الملفات مثل الصور وملفات PDF. كما أنه يخزن بيانات مؤقتة (المعروفة باسم: ذاكرة التخزين المؤقت) لتحسين أداء موقع الويب الخاص بك.", runwayFromScratch: "أريد البدء من الصفر", - runwayFromScratchText: "موقع الويب الخاص بك فارغ تمامًا في الوقت الحالي، وهذا مثالي إذا كنت ترغب في البدء من الصفر وإنشاء الأنواع المستندة والقوالب الخاصة بك. (تعرف على الكيفية) لا يزال بإمكانك اختيار تثبيت Runway لاحقًا. يرجى الانتقال إلى قسم المطور واختيار الحزم.", + runwayFromScratchText: "موقع الويب الخاص بك فارغ تمامًا في الوقت الحالي، وهذا مثالي إذا كنت ترغب في البدء من الصفر وإنشاء الأنواع الوثائق والقوالب الخاصة بك. (تعرف على الكيفية) لا يزال بإمكانك اختيار تثبيت Runway لاحقًا. يرجى الانتقال إلى قسم المطور واختيار الحزم.", runwayHeader: "لقد قمت بإعداد منصة Umbraco نظيفة. ماذا تريد أن تفعل بعد ذلك؟", runwayInstalled: "تم تثبيت Runway", runwayInstalledText: "لديك الأساس في مكانه. اختر الوحدات التي ترغب في تثبيتها فوقه.
\n هذه هي قائمتنا الموصى بها من الوحدات، قم بتحديد الوحدات التي ترغب في تثبيتها، أو عرض القائمة الكاملة للوحدات", runwayOnlyProUsers: "يوصى بها فقط للمستخدمين ذوي الخبرة", runwaySimpleSite: "أريد البدء بموقع ويب بسيط", - runwaySimpleSiteText: "

\"Runway\" هو موقع ويب بسيط يوفر بعض الأنواع المستندة والقوالب الأساسية. يمكن للمثبت إعداد Runway لك تلقائيًا، لكن يمكنك بسهولة تحريره أو توسيعه أو إزالته. ليس ضروريًا ويمكنك استخدام Umbraco بشكل مثالي بدونها. ومع ذلك، يوفر Runway أساسًا سهلًا يعتمد على أفضل الممارسات لبدء التشغيل بسرعة أكبر من أي وقت مضى. إذا اخترت تثبيت Runway، يمكنك اختيار الوحدات الأساسية الاختيارية المعروفة باسم وحدات Runway لتعزيز صفحات Runway الخاصة بك.

\n متضمن مع Runway: الصفحة الرئيسية، صفحة البدء، صفحة تثبيت الوحدات.
\n الوحدات الاختيارية: التنقل العلوي، خريطة الموقع، الاتصال، المعرض.
", + runwaySimpleSiteText: "

\"Runway\" هو موقع ويب بسيط يوفر بعض الأنواع الوثائق والقوالب الأساسية. يمكن للمثبت إعداد Runway لك تلقائيًا، لكن يمكنك بسهولة تحريره أو توسيعه أو إزالته. ليس ضروريًا ويمكنك استخدام Umbraco بشكل مثالي بدونها. ومع ذلك، يوفر Runway أساسًا سهلًا يعتمد على أفضل الممارسات لبدء التشغيل بسرعة أكبر من أي وقت مضى. إذا اخترت تثبيت Runway، يمكنك اختيار الوحدات الأساسية الاختيارية المعروفة باسم وحدات Runway لتعزيز صفحات Runway الخاصة بك.

\n متضمن مع Runway: الصفحة الرئيسية، صفحة البدء، صفحة تثبيت الوحدات.
\n الوحدات الاختيارية: التنقل العلوي، خريطة الموقع، الاتصال، المعرض.
", runwayWhatIsRunway: "ما هو Runway", step1: "الخطوة 1/5 قبول الترخيص", step2: "الخطوة 2/5: تكوين قاعدة البيانات", @@ -978,13 +978,13 @@ export default { renewSession: 'جدد الآن لحفظ عملك', }, login: { - greeting0: 'مرحبًا', - greeting1: 'مرحبًا', - greeting2: 'مرحبًا', - greeting3: 'مرحبًا', - greeting4: 'مرحبًا', - greeting5: 'مرحبًا', - greeting6: 'مرحبًا', + greeting0: 'مرحبًا! بداية أسبوع مثمرة مع Umbraco!', + greeting1: 'مرحبًا! يوم ثلاثاء إبداعي في Umbraco!', + greeting2: 'مرحبًا! يوم أربعاء موفق في إدارة محتواك!', + greeting3: 'مرحبًا! يوم خميس مليء بالإنجازات مع Umbraco!', + greeting4: 'مرحبًا! إنه يوم جمعة رائع لإدارة محتوى موقعك!', + greeting5: 'مرحبًا! عطلة نهاية أسبوع سعيدة مع Umbraco!', + greeting6: 'مرحبًا! يوم أحد جديد، فرص جديدة في Umbraco!', instruction: 'سجل الدخول إلى Umbraco', signInWith: 'سجل الدخول باستخدام {0}', timeout: 'انتهت جلستك. يرجى تسجيل الدخول مرة أخرى أدناه.', @@ -997,15 +997,15 @@ export default { moveOrCopy: { choose: 'اختر الصفحة أعلاه...', copyDone: '%0% تم نسخه إلى %1%', - copyTo: 'حدد المكان الذي يجب نسخ المستند %0% إليه أدناه', + copyTo: 'حدد المكان الذي يجب نسخ الوثيقة %0% إليه أدناه', moveDone: '%0% تم نقله إلى %1%', - moveTo: 'حدد المكان الذي يجب نقل المستند %0% إليه أدناه', + moveTo: 'حدد المكان الذي يجب نقل الوثيقة %0% إليه أدناه', nodeSelected: "تم تحديده كجذر لمحتواك الجديد، انقر على 'موافق' أدناه.", noNodeSelected: "لم يتم تحديد أي عقدة بعد، يرجى تحديد عقدة في القائمة أعلاه قبل النقر على 'موافق'", notAllowedByContentType: 'العقدة الحالية غير مسموح بها تحت العقدة المحددة بسبب نوعها', notAllowedByPath: 'لا يمكن نقل العقدة الحالية إلى إحدى صفحاتها الفرعية، ولا يمكن أن تكون الوالد والوجهة هي نفسها', notAllowedAtRoot: 'لا يمكن أن توجد العقدة الحالية في الجذر', - notValid: "لا يُسمح بالإجراء نظرًا لأن لديك أذونات غير كافية على 1 أو أكثر من المستندات الفرعية.\n", + notValid: "لا يُسمح بالإجراء نظرًا لأن لديك أذونات غير كافية على 1 أو أكثر من الوثيقة الفرعية.\n", relateToOriginal: 'ربط العناصر المنسوخة بالأصل', }, notifications: { @@ -1095,7 +1095,7 @@ export default { paMembersHelp: 'إذا كنت ترغب في منح الوصول إلى أعضاء محددين', }, publish: { - invalidPublishBranchPermissions: 'أذونات المستخدم غير كافية لنشر جميع المستندات التابعة', + invalidPublishBranchPermissions: 'أذونات المستخدم غير كافية لنشر جميع الوثائق التابعة', contentPublishedFailedIsTrashed: '\n %0% لم يتم نشره لأن العنصر في سلة المهملات.\n ', contentPublishedFailedAwaitingRelease: '\n %0% لم يتم نشره لأن العنصر مجدول للإصدار.\n ', contentPublishedFailedExpired: '\n %0% لم يتم نشره لأن العنصر منتهي الصلاحية.\n ', @@ -1182,7 +1182,7 @@ export default { externalLinkPlaceholder: 'أدخل الرابط', }, imagecropper: { - reset: 'إعادة تعيين الاقتصاص', + reset: 'إعادة تعيين', updateEditCrop: 'تم', undoEditCrop: 'تراجع عن التعديلات', customCrop: 'مخصص', @@ -1194,7 +1194,7 @@ export default { currentVersion: 'الإصدار الحالي', diffHelp: 'يظهر هذا الاختلافات بين الإصدار الحالي (المسودة) والإصدار المحدد
النص الأحمر سيتم حذفه في الإصدار المحدد، النص الأخضر سيتم إضافته', noDiff: 'لا توجد اختلافات بين الإصدار الحالي (المسودة) والإصدار المحدد', - documentRolledBack: 'تمت استعادة المستند', + documentRolledBack: 'تمت استعادة الوثيقة', htmlHelp: 'يعرض هذا الإصدار المحدد كـ HTML، إذا كنت ترغب في رؤية الفرق بين إصدارين في نفس الوقت، استخدم عرض الفرق\n ', rollbackTo: 'استعادة إلى', selectVersion: 'اختر الإصدار', @@ -1212,7 +1212,7 @@ export default { media: 'الوسائط', member: 'الأعضاء', packages: 'الحزم', - marketplace: 'سوق', + marketplace: 'السوق', settings: 'الإعدادات', translation: 'القاموس', users: 'المستخدمون', @@ -1227,7 +1227,7 @@ export default { }, settings: { defaulttemplate: 'القالب الافتراضي', - importDocumentTypeHelp: 'للاستيراد نوع المستند، ابحث عن ملف ".udt" على جهاز الكمبيوتر الخاص بك بالنقر على زر "استيراد" (سيُطلب منك تأكيد ذلك في الشاشة التالية)', + importDocumentTypeHelp: 'للاستيراد نوع الوثيقة ابحث عن ملف ".udt" على جهاز الكمبيوتر الخاص بك بالنقر على زر "استيراد" (سيُطلب منك تأكيد ذلك في الشاشة التالية)', newtabname: 'عنوان التبويب الجديد', nodetype: 'نوع العقدة', objecttype: 'النوع', @@ -1265,7 +1265,7 @@ export default { contentTypePropertyTypeCreated: 'تم إنشاء نوع الخاصية', contentTypePropertyTypeCreatedText: 'الاسم: %0%
نوع البيانات: %1%', contentTypePropertyTypeDeleted: 'تم حذف نوع الخاصية', - contentTypeSavedHeader: 'تم حفظ نوع المستند', + contentTypeSavedHeader: 'تم حفظ نوع الوثيقة', contentTypeTabCreated: 'تم إنشاء التبويب', contentTypeTabDeleted: 'تم حذف التبويب', contentTypeTabDeletedText: 'تم حذف التبويب بالمعرف: %0%', @@ -1279,7 +1279,7 @@ export default { editMultiContentPublishedText: 'تم نشر %0% مستندات وهي مرئية على الموقع', editVariantPublishedText: '%0% تم نشره وهو مرئي على الموقع', editMultiVariantPublishedText: '%0% مستندات تم نشرها للغات %1% وهي مرئية على الموقع', - editBlueprintSavedHeader: 'تم حفظ مخطط المستند', + editBlueprintSavedHeader: 'تم حفظ مخطط الوثيقة', editBlueprintSavedText: 'تم حفظ التغييرات بنجاح', editContentSavedHeader: 'تم حفظ المحتوى', editContentSavedText: 'تذكر النشر لجعل التغييرات مرئية', @@ -1335,19 +1335,19 @@ export default { deleteUserSuccess: 'تم حذف المستخدم %0%', resendInviteHeader: 'دعوة المستخدم', resendInviteSuccess: 'تم إعادة إرسال الدعوة إلى %0%', - contentReqCulturePublishError: "لا يمكن نشر المستند لأن '%0%' المطلوب غير منشور", + contentReqCulturePublishError: "لا يمكن نشر الوثيقة لأن '%0%' المطلوب غير منشور", contentCultureValidationError: "فشل التحقق من اللغة '%0%'", - documentTypeExportedSuccess: 'تم تصدير نوع المستند إلى ملف', - documentTypeExportedError: 'حدث خطأ أثناء تصدير نوع المستند', + documentTypeExportedSuccess: 'تم تصدير نوع الوثيقة إلى ملف', + documentTypeExportedError: 'حدث خطأ أثناء تصدير نوع الوثيقة', dictionaryItemExportedSuccess: 'تم تصدير عنصر (عناصر) القاموس إلى ملف', dictionaryItemExportedError: 'حدث خطأ أثناء تصدير عنصر (عناصر) القاموس', dictionaryItemImported: 'تم استيراد عنصر (عناصر) القاموس التالية!', scheduleErrReleaseDate1: 'تاريخ الإصدار لا يمكن أن يكون في الماضي', - scheduleErrReleaseDate2: "لا يمكن جدولة المستند للنشر لأن '%0%' المطلوب غير منشور", - scheduleErrReleaseDate3: "لا يمكن جدولة المستند للنشر لأن '%0%' المطلوب له تاريخ نشر لاحق من لغة غير إلزامية", + scheduleErrReleaseDate2: "لا يمكن جدولة الوثيقة للنشر لأن '%0%' المطلوب غير منشور", + scheduleErrReleaseDate3: "لا يمكن جدولة الوثيقة للنشر لأن '%0%' المطلوب له تاريخ نشر لاحق من لغة غير إلزامية", scheduleErrExpireDate1: 'تاريخ انتهاء الصلاحية لا يمكن أن يكون في الماضي', scheduleErrExpireDate2: 'تاريخ انتهاء الصلاحية لا يمكن أن يكون قبل تاريخ الإصدار', - publishWithNoDomains: 'لم يتم تكوين المجالات لموقع متعدد اللغات، يرجى الاتصال بالمسؤول، راجع السجل لمزيد من المعلومات', + publishWithNoDomains: 'لم يتم تكوين النطاقات لموقع متعدد اللغات، يرجى الاتصال بالمسؤول، راجع السجل لمزيد من المعلومات', publishWithMissingDomain: 'لا يوجد مجال مكون لـ %0%، يرجى الاتصال بالمسؤول، راجع السجل لمزيد من المعلومات', preventCleanupEnableError: 'حدث خطأ أثناء تمكين تنظيف الإصدارات لـ %0%', preventCleanupDisableError: 'حدث خطأ أثناء تعطيل تنظيف الإصدارات لـ %0%', @@ -1489,7 +1489,7 @@ export default { editProperty: 'تعديل الخاصية', requiredLabel: 'التسمية المطلوبة', enableListViewHeading: 'تمكين عرض القائمة', - enableListViewDescription: 'تكوين العنصر لعرض قائمة قابلة للفرز والبحث من أطفاله.', + enableListViewDescription: 'تكوين العنصر لعرض قائمة قابلة للفرز والبحث من أبنائه.', allowedTemplatesHeading: 'القوالب المسموح بها', allowedTemplatesDescription: 'اختر القوالب التي يُسمح للمحررين باستخدامها على محتوى من هذا النوع', allowAtRootHeading: 'السماح في الجذر', @@ -1497,10 +1497,10 @@ export default { childNodesHeading: 'أنواع العقد الفرعية المسموح بها', childNodesDescription: 'السماح بإنشاء محتوى من الأنواع المحددة أسفل محتوى من هذا النوع.', chooseChildNode: 'اختر العقدة الفرعية', - compositionsDescription: 'ارث التبويبات والخصائص من نوع مستند موجود. سيتم إضافة التبويبات الجديدة إلى نوع المستند الحالي أو دمجها إذا كان هناك تبويب بنفس الاسم.', + compositionsDescription: 'ارث التبويبات والخصائص من نوع مستند موجود. سيتم إضافة التبويبات الجديدة إلى نوع الوثيقة الحالي أو دمجها إذا كان هناك تبويب بنفس الاسم.', compositionInUse: 'هذا النوع من المحتوى قيد الاستخدام في تركيب، وبالتالي لا يمكن تركيبه بنفسه.\n ', noAvailableCompositions: 'لا توجد أنواع محتوى متاحة لاستخدامها كتركيب.', - compositionRemoveWarning: 'إزالة التركيب ستؤدي إلى حذف جميع بيانات الخصائص المرتبطة. بمجرد حفظ نوع المستند، لا يوجد طريق للعودة.', + compositionRemoveWarning: 'إزالة التركيب ستؤدي إلى حذف جميع بيانات الخصائص المرتبطة. بمجرد حفظ نوع الوثيقة لا يوجد طريق للعودة.', availableEditors: 'إنشاء جديد', reuse: 'استخدام موجود', editorSettings: 'إعدادات المحرر', @@ -1513,13 +1513,13 @@ export default { folderToMove: 'اختر المجلد للتحريك', folderToCopy: 'اختر المجلد للنسخ', structureBelow: 'إلى هيكل الشجرة أدناه', - allDocumentTypes: 'جميع أنواع المستندات', - allDocuments: 'جميع المستندات', + allDocumentTypes: 'جميع أنواع الوثائق', + allDocuments: 'جميع الوثائق', allMediaItems: 'جميع العناصر الإعلامية', - usingThisDocument: 'استخدام هذا النوع من المستندات سيتم حذفه نهائيًا، يرجى تأكيد أنك تريد حذف هذه أيضًا.\n ', + usingThisDocument: 'استخدام هذا النوع من الوثائق سيتم حذفه نهائيًا، يرجى تأكيد أنك تريد حذف هذه أيضًا.\n ', usingThisMedia: 'استخدام هذا النوع من الوسائط سيتم حذفه نهائيًا، يرجى تأكيد أنك تريد حذف هذه أيضًا.\n ', usingThisMember: 'استخدام هذا النوع من الأعضاء سيتم حذفه نهائيًا، يرجى تأكيد أنك تريد حذف هذه أيضًا\n ', - andAllDocuments: 'وجميع المستندات التي تستخدم هذا النوع', + andAllDocuments: 'وجميع الوثائق التي تستخدم هذا النوع', andAllMediaItems: 'وجميع العناصر الإعلامية التي تستخدم هذا النوع', andAllMembers: 'وجميع الأعضاء الذين يستخدمون هذا النوع', memberCanEdit: 'يمكن للعضو التعديل', @@ -1569,7 +1569,7 @@ export default { historyCleanupGloballyDisabled: 'ملاحظة! تنظيف إصدارات المحتوى التاريخية معطل عالميًا. لن تكون هذه الإعدادات فعالة حتى يتم تمكينها.', changeDataTypeHelpText: 'تغيير نوع البيانات مع القيم المخزنة معطل. للسماح بذلك، يمكنك تغيير إعداد Umbraco:CMS:DataTypes:CanBeChanged في appsettings.json.', collections: 'المجموعات', - collectionsDescription: 'تكوين العنصر لعرض قائمة بأطفاله.', + collectionsDescription: 'تكوين العنصر لعرض قائمة بأبنائه.', structure: 'الهيكل', presentation: 'العرض', }, @@ -2089,7 +2089,7 @@ export default { tabName: 'المراجع', DataTypeNoReferences: 'لا توجد مراجع لهذا النوع من البيانات.', itemHasNoReferences: 'لا يحتوي هذا العنصر على مراجع.', - labelUsedByDocumentTypes: 'مستشهد به من قبل أنواع المستندات التالية', + labelUsedByDocumentTypes: 'مستشهد به من قبل أنواع الوثائق التالية', labelUsedByMediaTypes: 'مستشهد به من قبل أنواع الوسائط التالية', labelUsedByMemberTypes: 'مستشهد به من قبل أنواع الأعضاء التالية', usedByProperties: 'مستشهد به من قبل', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/bs.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/bs.ts index bfc3158628..d7b50553cf 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/bs.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/bs.ts @@ -566,9 +566,9 @@ export default { createNew: 'Kreirajte stavku iz rječnika', }, dictionaryItem: { - description: "\n Uredite različite jezičke verzije za stavku rječnika '%0%' ispod\n ", + description: "Uredite različite jezičke verzije za stavku rječnika '%0%' ispod", displayName: 'Kultura', - changeKeyError: "\n Ključ '%0%' već postoji.\n ", + changeKeyError: "Ključ '%0%' već postoji.", overviewTitle: 'Pregled riječnika', }, examineManagement: { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/cs-cz.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/cs-cz.ts index 6a2f47c6b9..af0a03f54a 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/cs-cz.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/cs-cz.ts @@ -498,9 +498,9 @@ export default { }, dictionaryItem: { description: - "\n Editujte různé jazykové verze pro položku slovníku '%0%' níže.
Můžete přidat další jazyky v nabídce 'jazyky' nalevo.", + "Editujte různé jazykové verze pro položku slovníku '%0%' níže.
Můžete přidat další jazyky v nabídce 'jazyky' nalevo.", displayName: 'Název jazyka', - changeKeyError: "\n Klíč '%0%' již existuje.\n ", + changeKeyError: "Klíč '%0%' již existuje.", overviewTitle: 'Přehled slovníku', }, examineManagement: { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/cy-gb.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/cy-gb.ts index 4d320ab231..afea7d0aff 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/cy-gb.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/cy-gb.ts @@ -591,9 +591,9 @@ export default { }, dictionaryItem: { description: - "\n Golygwch y fersiynau iaith gwahanol ar gyfer yr eitem geiriadur '%0%' islaw
Gallwch ychwanegu ieithoedd ychwanegol o dan 'ieithoedd' yn y ddewislen ar y chwith\n ", + "Golygwch y fersiynau iaith gwahanol ar gyfer yr eitem geiriadur '%0%' islaw
Gallwch ychwanegu ieithoedd ychwanegol o dan 'ieithoedd' yn y ddewislen ar y chwith", displayName: 'Enw Diwylliant', - changeKeyError: "\n Mae'r allwedd '%0%' yn bodoli eisoes.\n ", + changeKeyError: "Mae'r allwedd '%0%' yn bodoli eisoes.", overviewTitle: 'Trosolwg Geiriadur', }, examineManagement: { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts index 6f79bf2640..4c49b058aa 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts @@ -588,9 +588,9 @@ export default { }, dictionaryItem: { description: - "\n Rediger de forskellige sprogversioner for ordbogselementet '%0%' herunder.
Du tilføjer flere sprog under 'sprog' i menuen til venstre \n ", + "Rediger de forskellige sprogversioner for ordbogselementet '%0%' herunder. Du tilføjer flere sprog under 'sprog' i menuen til venstre.", displayName: 'Kulturnavn', - changeKeyError: "\n Navnet '%0%' eksisterer allerede.\n ", + changeKeyError: "Navnet '%0%' eksisterer allerede.", overviewTitle: 'Ordbogsoversigt', }, examineManagement: { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/de-de.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/de-de.ts index 3d4af17cd2..fed36b1c08 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/de-de.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/de-de.ts @@ -590,9 +590,9 @@ export default { }, dictionaryItem: { description: - "\n Bearbeiten Sie nachfolgend die verschiedenen Sprachversionen für den Wörterbucheintrag '%0%'.\n
Unter dem links angezeigten Menüpunkt 'Sprachen' können Sie weitere hinzufügen.", + "Bearbeiten Sie nachfolgend die verschiedenen Sprachversionen für den Wörterbucheintrag '%0%'.
Unter dem links angezeigten Menüpunkt 'Sprachen' können Sie weitere hinzufügen.", displayName: 'Name der Kultur', - changeKeyError: "\n Der Wert '%0%' ist bereits vorhanden.\n ", + changeKeyError: "Der Wert '%0%' ist bereits vorhanden.", overviewTitle: 'Wörterbuch Übersicht', }, examineManagement: { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts index 25d297e308..94cecc220c 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts @@ -171,7 +171,14 @@ export default { morePublishingOptions: 'More publishing options', submitChanges: 'Submit', }, + auditTrailsMedia: { + delete: 'Media deleted', + move: 'Media moved', + copy: 'Media copied', + save: 'Media saved', + }, auditTrails: { + assigndomain: 'Domain assigned: %0%', atViewingFor: 'Viewing for', delete: 'Content deleted', unpublish: 'Content unpublished', @@ -189,6 +196,7 @@ export default { custom: '%0%', contentversionpreventcleanup: 'Cleanup disabled for version: %0%', contentversionenablecleanup: 'Cleanup enabled for version: %0%', + smallAssignDomain: 'Assign Domain', smallCopy: 'Copy', smallPublish: 'Publish', smallPublishVariant: 'Publish', @@ -333,6 +341,7 @@ export default { variantSendForApprovalNotAllowed: 'Send for approval is not allowed', variantScheduleNotAllowed: 'Schedule is not allowed', variantUnpublishNotAllowed: 'Unpublish is not allowed', + selectAllVariants: 'Select all variants', }, blueprints: { createBlueprintFrom: "Create a new Document Blueprint from '%0%'", @@ -354,25 +363,26 @@ export default { invalidFileName: 'Cannot upload this file, it does not have a valid file name', maxFileSize: 'Max file size is', mediaRoot: 'Media root', - moveToSameFolderFailed: 'Parent and destination folders cannot be the same', createFolderFailed: 'Failed to create a folder under parent id %0%', renameFolderFailed: 'Failed to rename the folder with id %0%', dragAndDropYourFilesIntoTheArea: 'Drag and drop your file(s) into the area', + fileSecurityValidationFailure: 'One or more file security validations have failed', + moveToSameFolderFailed: 'Parent and destination folders cannot be the same', uploadNotAllowed: 'Upload is not allowed in this location.', }, member: { - createNewMember: 'Create a new member', + '2fa': 'Two-Factor Authentication', allMembers: 'All Members', + createNewMember: 'Create a new member', duplicateMemberLogin: 'A member with this login already exists', kind: 'Kind', memberGroupNoProperties: 'Member groups have no additional properties for editing.', memberHasGroup: "The member is already in group '%0%'", memberHasPassword: 'The member already has a password set', - memberLockoutNotEnabled: 'Lockout is not enabled for this member', - memberNotInGroup: "The member is not in group '%0%'", - '2fa': 'Two-Factor Authentication', memberKindDefault: 'Member', memberKindApi: 'API Member', + memberLockoutNotEnabled: 'Lockout is not enabled for this member', + memberNotInGroup: "The member is not in group '%0%'", }, contentType: { copyFailed: 'Failed to copy content type', @@ -597,9 +607,9 @@ export default { createNew: 'Create dictionary item', }, dictionaryItem: { - description: "\n Edit the different language versions for the dictionary item '%0%' below\n ", + description: "Edit the different language versions for the dictionary item '%0%' below", displayName: 'Culture Name', - changeKeyError: "\n The key '%0%' already exists.\n ", + changeKeyError: "The key '%0%' already exists.", overviewTitle: 'Dictionary overview', }, examineManagement: { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 8b31dc00d1..3127bcb0fc 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -370,13 +370,16 @@ export default { uploadNotAllowed: 'Upload is not allowed in this location.', }, member: { - createNewMember: 'Create a new member', - allMembers: 'All Members', - memberGroupNoProperties: 'Member groups have no additional properties for editing.', '2fa': 'Two-Factor Authentication', + allMembers: 'All Members', + createNewMember: 'Create a new member', duplicateMemberLogin: 'A member with this login already exists', + kind: 'Kind', + memberGroupNoProperties: 'Member groups have no additional properties for editing.', memberHasGroup: "The member is already in group '%0%'", memberHasPassword: 'The member already has a password set', + memberKindDefault: 'Member', + memberKindApi: 'API Member', memberLockoutNotEnabled: 'Lockout is not enabled for this member', memberNotInGroup: "The member is not in group '%0%'", }, @@ -606,9 +609,9 @@ export default { createNew: 'Create dictionary item', }, dictionaryItem: { - description: "\n Edit the different language versions for the dictionary item '%0%' below\n ", + description: "Edit the different language versions for the dictionary item '%0%' below", displayName: 'Culture Name', - changeKeyError: "\n The key '%0%' already exists.\n ", + changeKeyError: "The key '%0%' already exists.", overviewTitle: 'Dictionary overview', }, examineManagement: { @@ -1902,6 +1905,14 @@ export default { administrators: 'Administrator', categoryField: 'Category field', createDate: 'User created', + createUserHeadline: (kind: string) => { + return kind === 'Api' ? 'Create API user' : 'Create user'; + }, + createUserDescription: (kind: string) => { + const defaultUserText = `Create a user to give them access to Umbraco. When a user is created a password will be generated that you can share with them.`; + const apiUserText = `Create an Api User to allow external services to authenticate with the Umbraco Management API.`; + return kind === 'Api' ? apiUserText : defaultUserText; + }, changePassword: 'Change your password', changePhoto: 'Change photo', configureMfa: 'Configure MFA', @@ -1911,6 +1922,7 @@ export default { ? 'The email address is used for notifications, password recovery, and as the username for logging in' : 'The email address is used for notifications and password recovery'; }, + kind: 'Kind', newPassword: 'New password', newPasswordFormatLengthTip: 'Minimum %0% character(s) to go!', newPasswordFormatNonAlphaTip: 'There should be at least %0% special character(s) in there.', @@ -2044,6 +2056,8 @@ export default { sortCreateDateDescending: 'Newest', sortCreateDateAscending: 'Oldest', sortLastLoginDateDescending: 'Last login', + userKindDefault: 'User', + userKindApi: 'API User', noUserGroupsAdded: 'No user groups have been added', '2faDisableText': 'If you wish to disable this two-factor provider, then you must enter the code shown on your authentication device:', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/es-es.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/es-es.ts index 58c612db2c..1e815027ef 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/es-es.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/es-es.ts @@ -370,9 +370,9 @@ export default { createNew: 'Crear elemento de diccionario', }, dictionaryItem: { - description: "Editar las diferentes versiones lingüísticas para la entrada en el diccionario '% 0%' debajo", - displayName: 'nombre de la cultura\n', - changeKeyError: "\n La clave '%0%' ya existe.\n ", + description: "Editar las diferentes versiones lingüísticas para la entrada en el diccionario '%0%' debajo", + displayName: 'nombre de la cultura', + changeKeyError: "La clave '%0%' ya existe.", }, placeholders: { username: 'Escribe tu nombre de usuario', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/fr-fr.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/fr-fr.ts index 5413b2e03c..88ec99abfa 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/fr-fr.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/fr-fr.ts @@ -517,10 +517,9 @@ export default { createNew: 'Créer un élément de dictionnaire', }, dictionaryItem: { - description: - "\n Editez les différentes versions de langues pour l'élément de dictionnaire '%0%' ci-dessous.\n ", + description: "Editez les différentes versions de langues pour l'élément de dictionnaire '%0%' ci-dessous.", displayName: 'Nom de Culture', - changeKeyError: "\n La clé '%0%' existe déjà.\n ", + changeKeyError: "La clé '%0%' existe déjà.", overviewTitle: 'Aperçu du dictionaire', }, examineManagement: { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/he-il.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/he-il.ts index fd9ee14b19..146ec4f545 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/he-il.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/he-il.ts @@ -204,7 +204,7 @@ export default { }, dictionaryItem: { description: - '\n ערוך את גרסאות השפות השונות לפריט המילון \'%0%\' למטה
ניתן להוסיף שפות נוספות תחת "שפות" בתפריט בצד שמאל\n ', + 'ערוך את גרסאות השפות השונות לפריט המילון \'%0%\' למטה ניתן להוסיף שפות נוספות תחת "שפות" בתפריט בצד שמאל', displayName: 'שם התצוגה לשפה', }, editdatatype: { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/hr-hr.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/hr-hr.ts index 3597b95fc9..d08b4d332e 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/hr-hr.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/hr-hr.ts @@ -568,9 +568,9 @@ export default { createNew: 'Kreirajte stavku iz rječnika', }, dictionaryItem: { - description: "\n Uredite različite jezičke varijante za stavku rječnika '%0%' ispod\n ", + description: "Uredite različite jezičke varijante za stavku rječnika '%0%' ispod", displayName: 'Kultura', - changeKeyError: "\n Stavka '%0%' već postoji.\n ", + changeKeyError: "Stavka '%0%' već postoji.", overviewTitle: 'Pregled riječnika', }, examineManagement: { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/it-it.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/it-it.ts index 2f2d5f60b7..bef4e1f0eb 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/it-it.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/it-it.ts @@ -569,7 +569,7 @@ export default { noItems: 'Non ci sono oggetti nel Dizionario.', }, dictionaryItem: { - description: "Modifica le lingue per l'elemento '%0%' qui sotto.", + description: "Modifica le lingue per l'elemento '%0%' qui sotto.", displayName: 'Nome della cultura', changeKeyError: "La chiave '%0%' esiste già.", overviewTitle: 'Panoramica del Dizionario', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/ja-jp.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/ja-jp.ts index 2b812c42aa..d16087a107 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/ja-jp.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/ja-jp.ts @@ -275,7 +275,7 @@ export default { }, dictionaryItem: { description: - "\n ディクショナリのアイテム '%0%' の別の言語版を編集するには、左側のメニューの'言語'でその言語を追加します\n ", + "ディクショナリのアイテム '%0%' の別の言語版を編集するには、左側のメニューの'言語'でその言語を追加します", displayName: 'カルチャ名', }, placeholders: { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/ko-kr.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/ko-kr.ts index f2cfcab172..16ab5e6ba9 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/ko-kr.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/ko-kr.ts @@ -204,7 +204,7 @@ export default { }, dictionaryItem: { description: - "\n '%0%'사전 항목 아래에 다른 언어버전들을 편집하세요
왼쪽 '언어'메뉴를 사용하여 추가 언어들을 설정할 수 있습니다.\n ", + "'%0%'사전 항목 아래에 다른 언어버전들을 편집하세요
왼쪽 '언어'메뉴를 사용하여 추가 언어들을 설정할 수 있습니다.", displayName: '국가명', }, editdatatype: { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/nb-no.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/nb-no.ts index 14ff0ab87a..94c3cb5eb2 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/nb-no.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/nb-no.ts @@ -250,8 +250,10 @@ export default { }, dictionaryItem: { description: - "Rediger de forskjellige språkversjonene for ordbokelementet '%0%' under.
Du kan legge til flere språk under 'språk' i menyen til venstre.", + "Rediger de forskjellige språkversjonene for ordbokelementet '%0%' under. Du kan legge til flere språk under 'språk' i menyen til venstre.", displayName: 'Språk', + changeKeyError: "Kan ikke endre nøkkel for '%0%' fordi det allerede finnes en oversettelse for denne nøkkelen", + overviewTitle: 'Ordbok', }, placeholders: { username: 'Skriv inn ditt brukernavn', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/nl-nl.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/nl-nl.ts index 3f7bb995ae..8cb9748c6a 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/nl-nl.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/nl-nl.ts @@ -529,9 +529,9 @@ export default { }, dictionaryItem: { description: - "\n Wijzig de verschillende taalversies voor het woordenboek item '%0%'. Je kunt extra talen toevoegen bij 'talen' in het menu links\n ", + "Wijzig de verschillende taalversies voor het woordenboek item '%0%'. Je kunt extra talen toevoegen bij 'talen' in het menu links", displayName: 'Cultuurnaam', - changeKeyError: "\n De key '%0%' bestaat al.\n ", + changeKeyError: "De key '%0%' bestaat al.", overviewTitle: 'Woordenboek overzicht', }, examineManagement: { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/pl-pl.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/pl-pl.ts index 3dbfc8a21a..c91545622e 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/pl-pl.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/pl-pl.ts @@ -364,9 +364,9 @@ export default { }, dictionaryItem: { description: - '\n Edytuj różne wersje językowe dla elementu słownika \'%0%\' poniżej.
\n Możesz dodać dodatkowe języki w menu "Języki" po lewej stronie.', + 'Edytuj różne wersje językowe dla elementu słownika \'%0%\' poniżej. Możesz dodać dodatkowe języki w menu "Języki" po lewej stronie.', displayName: 'Nazwa języka', - changeKeyError: "\n Klucz '%0%' już istnieje.\n ", + changeKeyError: "Klucz '%0%' już istnieje.", }, placeholders: { username: 'Wpisz nazwę użytkownika', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/pt-br.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/pt-br.ts index 1253547fef..501692b6d4 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/pt-br.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/pt-br.ts @@ -206,7 +206,7 @@ export default { }, dictionaryItem: { description: - "Editar as diferente versões de linguagem para o item de dicionário '%0%' abaixo
Você pode adicionar mais linguagens sob 'linguagens' no menu à esquerda", + "Editar as diferente versões de linguagem para o item de dicionário '%0%' abaixo. Você pode adicionar mais linguagens sob 'linguagens' no menu à esquerda.", displayName: 'Nome da Cultura', }, editdatatype: { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/ru-ru.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/ru-ru.ts index 4e7a769eb4..042853eed7 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/ru-ru.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/ru-ru.ts @@ -423,9 +423,9 @@ export default { }, dictionaryItem: { description: - "\n\t\tНиже Вы можете указать различные переводы данной статьи словаря '%0%'
Добавить другие языки можно, воспользовавшись пунктом 'Языки' в меню слева\n\t\t", + "Ниже Вы можете указать различные переводы данной статьи словаря '%0%'. Добавить другие языки можно, воспользовавшись пунктом 'Языки' в меню слева.", displayName: 'Название языка (культуры)', - changeKeyError: "\n Ключ '%0%' уже существует в словаре.\n ", + changeKeyError: "Ключ '%0%' уже существует в словаре.", overviewTitle: 'Обзор словаря', }, editcontenttype: { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/tr-tr.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/tr-tr.ts index dfe98dd10e..a7a1c8186b 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/tr-tr.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/tr-tr.ts @@ -510,9 +510,9 @@ export default { noItems: 'Sözlük öğesi yok.', }, dictionaryItem: { - description: "\n Aşağıdaki sözlük öğesi '%0%' için farklı dil sürümlerini düzenleyin\n ", + description: "Aşağıdaki sözlük öğesi '%0%' için farklı dil sürümlerini düzenleyin", displayName: 'Kültür Adı', - changeKeyError: "\n '%0%' anahtarı zaten var.\n ", + changeKeyError: "'%0%' anahtarı zaten var.", overviewTitle: 'Sözlüğe genel bakış', }, examineManagement: { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/uk-ua.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/uk-ua.ts index eaa3f06a06..f54696ab06 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/uk-ua.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/uk-ua.ts @@ -422,9 +422,9 @@ export default { }, dictionaryItem: { description: - "\n\t\tНиже Ви можете вказати різні переклади даної статті словника '%0%'
Додати інші мови можна, скориставшись пунктом 'Мови' в меню зліва\n\t\t", + "Ниже Ви можете вказати різні переклади даної статті словника '%0%'. Додати інші мови можна, скориставшись пунктом 'Мови' в меню зліва.", displayName: 'Назва мови (культури)', - changeKeyError: "\n Ключ '%0%' вже існує у словнику.\n ", + changeKeyError: "Ключ '%0%' вже існує у словнику.", overviewTitle: 'Огляд словника', }, editcontenttype: { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/zh-cn.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/zh-cn.ts index 50715aa84e..b9038734bd 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/zh-cn.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/zh-cn.ts @@ -280,9 +280,9 @@ export default { selectEditor: '选择编辑器', }, dictionaryItem: { - description: '\n 为字典项编辑不同语言的版本‘%0%
您可以在左侧的“语言”中添加一种语言\n ', + description: '为字典项编辑不同语言的版本‘%0%’, 您可以在左侧的“语言”中添加一种语言', displayName: '语言名称', - changeKeyError: "\n 关键字 '%0%' 已经存在。\n ", + changeKeyError: "关键字 '%0%' 已经存在。", }, placeholders: { username: '输入您的用户名', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/zh-tw.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/zh-tw.ts index 62d39dae16..da9413e108 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/zh-tw.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/zh-tw.ts @@ -278,8 +278,7 @@ export default { selectEditor: '選擇編輯器', }, dictionaryItem: { - description: - "\n 為此字典項目 '%0%' 編輯不同語言版本,
您可以在左方選單「語言」中增添新的語言\n ", + description: "為此字典項目 '%0%' 編輯不同語言版本,您可以在左方選單「語言」中增添新的語言", displayName: '語言名稱', }, placeholders: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts index 1b5bc3ab44..2f1e8aab2d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts @@ -363,6 +363,7 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper }; #extensionSlotFilterMethod = (manifest: ManifestBlockEditorCustomView) => { + // We do have _contentTypeAlias at this stage, cause we do use the filter method in the extension slot which first gets rendered when we have the _contentTypeAlias. [NL] if ( manifest.forContentTypeAlias && !stringOrStringArrayContains(manifest.forContentTypeAlias, this._contentTypeAlias!) diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts index 868fbb81bb..d2c642d652 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts @@ -101,7 +101,6 @@ export class UmbBlockElementManager extends UmbControllerBase { /** * @function propertyValueByAlias * @param {string} propertyAlias - Property Alias to observe the value of. - * @param {UmbVariantId | undefined} variantId - Optional variantId to filter by. * @returns {Promise | undefined>} - Promise which resolves to an Observable * @description Get an Observable for the value of this property. */ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-toolbar.element.ts index 7121710110..3531f9de2a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-toolbar.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-toolbar.element.ts @@ -7,6 +7,7 @@ export class UmbCollectionToolbarElement extends UmbLitElement { override render() { return html` +
`; } @@ -20,6 +21,9 @@ export class UmbCollectionToolbarElement extends UmbLitElement { justify-content: space-between; width: 100%; } + #slot { + flex: 1; + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/components/property-type-based-property/property-type-based-property.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/components/property-type-based-property/property-type-based-property.element.ts index 8e9e1973fc..475188cd24 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/components/property-type-based-property/property-type-based-property.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/components/property-type-based-property/property-type-based-property.element.ts @@ -109,7 +109,6 @@ export class UmbPropertyTypeBasedPropertyElement extends UmbLitElement { if (this._isUnsupported) { return html``; } return html` diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/localization/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/localization/manifests.ts index cec662b335..576c677f65 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/localization/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/localization/manifests.ts @@ -260,5 +260,5 @@ export const manifests: Array = [ culture: 'zh-tw', }, js: () => import('../../../assets/lang/zh-tw.js'), - } + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property/property.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property/property.context.ts index 86e9e322eb..5b67188c4d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/property/property.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property/property.context.ts @@ -156,7 +156,7 @@ export class UmbPropertyContext extends UmbContextBase extends UmbContextBase extends UmbContextBase extends UmbContextBase extends UmbContextBase extends UmbContextBase extends UmbContextBase extends UmbContextBase | undefined)} config + * @param {Array | undefined} config - Array of configurations for this property * @memberof UmbPropertyContext */ public setConfig(config: Array | undefined): void { @@ -256,7 +256,7 @@ export class UmbPropertyContext extends UmbContextBase | undefined)} + * @returns {Array | undefined} - Array of configurations for this property * @memberof UmbPropertyContext */ public getConfig(): Array | undefined { @@ -265,7 +265,7 @@ export class UmbPropertyContext extends UmbContextBase extends UmbContextBase extends UmbContextBase extends UmbContextBase extends UmbContextBase { - this._isReadOnly = value; - this._element?.toggleAttribute('readonly', value); - }); + this.observe( + this.#propertyContext.isReadOnly, + (value) => { + this._isReadOnly = value; + this._element?.toggleAttribute('readonly', value); + }, + null, + ); } private _onPropertyEditorChange = (e: CustomEvent): void => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts index 8b19679abf..078596afd7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts @@ -19,6 +19,7 @@ export * from './path/stored-path.function.js'; export * from './path/transform-server-path-to-client-path.function.js'; export * from './path/umbraco-path.function.js'; export * from './path/url-pattern-to-string.function.js'; +export * from './sanitize/sanitize-html.function.js'; export * from './selection-manager/selection.manager.js'; export * from './state-manager/index.js'; export * from './string/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/sanitize/sanitize-html.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/sanitize/sanitize-html.function.ts new file mode 100644 index 0000000000..56c2902218 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/sanitize/sanitize-html.function.ts @@ -0,0 +1,10 @@ +import { DOMPurify } from '@umbraco-cms/backoffice/external/dompurify'; + +/** + * Sanitize a HTML string by removing any potentially harmful content such as scripts. + * @param {string} html The HTML string to sanitize. + * @returns The sanitized HTML string. + */ +export function sanitizeHTML(html: string): string { + return DOMPurify.sanitize(html); +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/collection/dictionary-collection.element.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/collection/dictionary-collection.element.ts new file mode 100644 index 0000000000..d4372f22ec --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/collection/dictionary-collection.element.ts @@ -0,0 +1,54 @@ +import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit'; +import { + UMB_COLLECTION_CONTEXT, + UmbCollectionDefaultElement, + type UmbDefaultCollectionContext, +} from '@umbraco-cms/backoffice/collection'; + +@customElement('umb-dictionary-collection') +export class UmbDictionaryCollectionElement extends UmbCollectionDefaultElement { + #collectionContext?: UmbDefaultCollectionContext; + #inputTimer?: NodeJS.Timeout; + #inputTimerAmount = 500; + + constructor() { + super(); + this.consumeContext(UMB_COLLECTION_CONTEXT, (context) => { + this.#collectionContext = context; + }); + } + + #updateSearch(event: InputEvent) { + const target = event.target as HTMLInputElement; + const filter = target.value || ''; + clearTimeout(this.#inputTimer); + this.#inputTimer = setTimeout(() => this.#collectionContext?.setFilter({ filter }), this.#inputTimerAmount); + } + + protected override renderToolbar() { + return html`${this.#renderSearch()}`; + } + + #renderSearch() { + return html``; + } + + static override styles = [ + css` + #input-search { + width: 100%; + } + `, + ]; +} + +export default UmbDictionaryCollectionElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-dictionary-collection': UmbDictionaryCollectionElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/collection/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/collection/manifests.ts index dbab8a757d..eeffe2cb95 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/collection/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/collection/manifests.ts @@ -9,6 +9,7 @@ const collectionManifest: ManifestCollection = { type: 'collection', kind: 'default', alias: UMB_DICTIONARY_COLLECTION_ALIAS, + element: () => import('./dictionary-collection.element.js'), name: 'Dictionary Collection', meta: { repositoryAlias: UMB_DICTIONARY_COLLECTION_REPOSITORY_ALIAS, diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/manifests.ts index 699256e02a..d6af6cab52 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/manifests.ts @@ -3,6 +3,7 @@ import { manifests as dashboardManifests } from './dashboard/manifests.js'; import { manifests as entityActionManifests } from './entity-action/manifests.js'; import { manifests as menuItemManifests } from './menu-item/manifests.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; +import { manifests as searchManifests } from './search/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; @@ -12,6 +13,7 @@ export const manifests: Array = [ ...entityActionManifests, ...menuItemManifests, ...repositoryManifests, + ...searchManifests, ...treeManifests, ...workspaceManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/constants.ts new file mode 100644 index 0000000000..b209cb74c4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/constants.ts @@ -0,0 +1 @@ +export const UMB_DICTIONARY_SEARCH_PROVIDER_ALIAS = 'Umb.SearchProvider.Dictionary'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/dictionary-search.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/dictionary-search.repository.ts new file mode 100644 index 0000000000..8bdc0bf948 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/dictionary-search.repository.ts @@ -0,0 +1,23 @@ +import { UmbDictionarySearchServerDataSource } from './dictionary-search.server.data-source.js'; +import type { UmbDictionarySearchItemModel } from './dictionary.search-provider.js'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbSearchRepository, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; + +export class UmbDictionarySearchRepository + extends UmbControllerBase + implements UmbSearchRepository, UmbApi +{ + #dataSource: UmbDictionarySearchServerDataSource; + + constructor(host: UmbControllerHost) { + super(host); + + this.#dataSource = new UmbDictionarySearchServerDataSource(this); + } + + search(args: UmbSearchRequestArgs) { + return this.#dataSource.search(args); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/dictionary-search.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/dictionary-search.server.data-source.ts new file mode 100644 index 0000000000..6233077a87 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/dictionary-search.server.data-source.ts @@ -0,0 +1,54 @@ +import { UMB_DICTIONARY_ENTITY_TYPE } from '../entity.js'; +import type { UmbDictionarySearchItemModel } from './dictionary.search-provider.js'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import { DictionaryService } from '@umbraco-cms/backoffice/external/backend-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbSearchDataSource, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; + +/** + * A data source for the Rollback that fetches data from the server + * @class UmbDictionarySearchServerDataSource + * @implements {RepositoryDetailDataSource} + */ +export class UmbDictionarySearchServerDataSource implements UmbSearchDataSource { + #host: UmbControllerHost; + + /** + * Creates an instance of UmbDictionarySearchServerDataSource. + * @param {UmbControllerHost} host - The controller host for this controller to be appended to + * @memberof UmbDictionarySearchServerDataSource + */ + constructor(host: UmbControllerHost) { + this.#host = host; + } + + /** + * Get a list of versions for a data + * @param args + * @returns {*} + * @memberof UmbDictionarySearchServerDataSource + */ + async search(args: UmbSearchRequestArgs) { + const { data, error } = await tryExecuteAndNotify( + this.#host, + DictionaryService.getDictionary({ + filter: args.query, + }), + ); + + if (data) { + const mappedItems: Array = data.items.map((item) => { + return { + href: '/section/translation/workspace/dictionary/edit/' + item.id, + entityType: UMB_DICTIONARY_ENTITY_TYPE, + unique: item.id, + name: item.name ?? '', + }; + }); + + return { data: { items: mappedItems, total: data.total } }; + } + + return { error }; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/dictionary.search-provider.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/dictionary.search-provider.ts new file mode 100644 index 0000000000..7cc3c13a59 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/dictionary.search-provider.ts @@ -0,0 +1,25 @@ +import type { UmbDictionaryItemModel } from '../index.js'; +import { UmbDictionarySearchRepository } from './dictionary-search.repository.js'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbSearchProvider, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; + +export interface UmbDictionarySearchItemModel extends UmbDictionaryItemModel { + href: string; +} + +export class UmbDictionarySearchProvider + extends UmbControllerBase + implements UmbSearchProvider +{ + #repository = new UmbDictionarySearchRepository(this); + + async search(args: UmbSearchRequestArgs) { + return this.#repository.search(args); + } + + override destroy(): void { + this.#repository.destroy(); + } +} + +export { UmbDictionarySearchProvider as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/index.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/index.ts new file mode 100644 index 0000000000..4f07201dcf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/index.ts @@ -0,0 +1 @@ +export * from './constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/manifests.ts new file mode 100644 index 0000000000..7c1175adff --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/manifests.ts @@ -0,0 +1,21 @@ +import { UMB_DICTIONARY_ENTITY_TYPE } from '../entity.js'; +import { UMB_DICTIONARY_SEARCH_PROVIDER_ALIAS } from './constants.js'; + +export const manifests: Array = [ + { + name: 'Dictionary Search Provider', + alias: UMB_DICTIONARY_SEARCH_PROVIDER_ALIAS, + type: 'searchProvider', + api: () => import('./dictionary.search-provider.js'), + weight: 600, + meta: { + label: 'Dictionary', + }, + }, + { + name: 'Dictionary Search Result Item ', + alias: 'Umb.SearchResultItem.Dictionary', + type: 'searchResultItem', + forEntityTypes: [UMB_DICTIONARY_ENTITY_TYPE], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/views/workspace-view-dictionary-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/views/workspace-view-dictionary-editor.element.ts index 2982f9a2dd..fdeaf2130b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/views/workspace-view-dictionary-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/views/workspace-view-dictionary-editor.element.ts @@ -2,10 +2,11 @@ import { UMB_DICTIONARY_WORKSPACE_CONTEXT } from '../dictionary-workspace.contex import type { UmbDictionaryDetailModel } from '../../types.js'; import type { UUITextareaElement } from '@umbraco-cms/backoffice/external/uui'; import { UUITextareaEvent } from '@umbraco-cms/backoffice/external/uui'; -import { css, html, customElement, state, repeat, ifDefined, unsafeHTML } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbLanguageCollectionRepository, type UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language'; import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user'; +import { sanitizeHTML } from '@umbraco-cms/backoffice/utils'; @customElement('umb-workspace-view-dictionary-editor') export class UmbWorkspaceViewDictionaryEditorElement extends UmbLitElement { @@ -21,8 +22,12 @@ export class UmbWorkspaceViewDictionaryEditorElement extends UmbLitElement { @state() private _currentUserHasAccessToAllLanguages?: boolean = false; - #languageCollectionRepository = new UmbLanguageCollectionRepository(this); - #workspaceContext!: typeof UMB_DICTIONARY_WORKSPACE_CONTEXT.TYPE; + get #dictionaryName() { + return typeof this._dictionary?.name !== 'undefined' ? sanitizeHTML(this._dictionary.name) : '...'; + } + + readonly #languageCollectionRepository = new UmbLanguageCollectionRepository(this); + #workspaceContext?: typeof UMB_DICTIONARY_WORKSPACE_CONTEXT.TYPE; #currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE; constructor() { @@ -59,7 +64,7 @@ export class UmbWorkspaceViewDictionaryEditorElement extends UmbLitElement { } #observeDictionary() { - this.observe(this.#workspaceContext.dictionary, (dictionary) => { + this.observe(this.#workspaceContext?.dictionary, (dictionary) => { this._dictionary = dictionary; }); } @@ -77,14 +82,14 @@ export class UmbWorkspaceViewDictionaryEditorElement extends UmbLitElement { const translation = (target.value as string).toString(); const isoCode = target.getAttribute('name')!; - this.#workspaceContext.setPropertyValue(isoCode, translation); + this.#workspaceContext?.setPropertyValue(isoCode, translation); } } override render() { return html` - ${unsafeHTML(this.localize.term('dictionaryItem_description', this._dictionary?.name || '​'))} + ${this.localize.term('dictionaryItem_description', this.#dictionaryName)} ${repeat( this._languages, (item) => item.unique, @@ -105,7 +110,7 @@ export class UmbWorkspaceViewDictionaryEditorElement extends UmbLitElement { name=${language.unique} label="translation" @change=${this.#onTextareaChange} - value=${ifDefined(translation?.translation)} + .value=${translation?.translation ?? ''} ?readonly=${this.#isReadOnly(language.unique)}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/import/modal/document-type-import-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/import/modal/document-type-import-modal.element.ts index 9bfef4ee36..7bd699489b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/import/modal/document-type-import-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/import/modal/document-type-import-modal.element.ts @@ -43,12 +43,12 @@ export class UmbDocumentTypeImportModalLayout extends UmbModalBaseElement< }; } - #onFileDropped() { - const data = this.dropzone?.getFiles()[0]; - if (!data) return; + #onUploadComplete() { + const data = this.dropzone?.getItems()[0]; + if (!data?.temporaryFile) return; - this.#temporaryUnique = data.temporaryUnique; - this.#fileReader.readAsText(data.file); + this.#temporaryUnique = data.temporaryFile.temporaryUnique; + this.#fileReader.readAsText(data.temporaryFile.file); } async #onFileImport() { @@ -136,7 +136,11 @@ export class UmbDocumentTypeImportModalLayout extends UmbModalBaseElement< html`
Drag and drop your file here - +
`, )} `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/input-markdown-editor/input-markdown.element.ts b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/input-markdown-editor/input-markdown.element.ts index 919665e66e..9921108fdc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/input-markdown-editor/input-markdown.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/input-markdown-editor/input-markdown.element.ts @@ -12,7 +12,6 @@ import { createExtensionApi } from '@umbraco-cms/backoffice/extension-api'; import { marked } from '@umbraco-cms/backoffice/external/marked'; import { monaco } from '@umbraco-cms/backoffice/external/monaco-editor'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import { DOMPurify } from '@umbraco-cms/backoffice/external/dompurify'; import { UmbChangeEvent, type UmbInputEvent } from '@umbraco-cms/backoffice/event'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; @@ -22,6 +21,7 @@ import { UmbCodeEditorLoadedEvent } from '@umbraco-cms/backoffice/code-editor'; import type { UmbCodeEditorController, UmbCodeEditorElement } from '@umbraco-cms/backoffice/code-editor'; import type { UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui'; import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import { sanitizeHTML } from '@umbraco-cms/backoffice/utils'; const elementName = 'umb-input-markdown'; @@ -560,7 +560,7 @@ export class UmbInputMarkdownElement extends UmbFormControlMixin(UmbLitElement, #renderPreview() { if (!this.preview || !this.value) return; const markdownAsHtml = marked.parse(this.value as string) as string; - const sanitizedHtml = markdownAsHtml ? DOMPurify.sanitize(markdownAsHtml) : ''; + const sanitizedHtml = markdownAsHtml ? sanitizeHTML(markdownAsHtml) : ''; return html`${unsafeHTML(sanitizedHtml)}`; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.element.ts index 76e40e994f..06973dd3a2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.element.ts @@ -33,19 +33,19 @@ export class UmbMediaTypeImportModalLayout extends UmbModalBaseElement< this.#fileReader.onload = (e) => { if (typeof e.target?.result === 'string') { const fileContent = e.target.result; - this.#MediaTypePreviewBuilder(fileContent); + this.#mediaTypePreviewBuilder(fileContent); } else { this.#requestReset(); } }; } - #onFileDropped() { - const data = this.dropzone?.getFiles()[0]; - if (!data) return; + #onUploadCompleted() { + const data = this.dropzone?.getItems()[0]; + if (!data?.temporaryFile) return; - this.#temporaryUnique = data.temporaryUnique; - this.#fileReader.readAsText(data.file); + this.#temporaryUnique = data.temporaryFile.temporaryUnique; + this.#fileReader.readAsText(data.temporaryFile.file); } async #onFileImport() { @@ -55,7 +55,7 @@ export class UmbMediaTypeImportModalLayout extends UmbModalBaseElement< this._submitModal(); } - #MediaTypePreviewBuilder(htmlString: string) { + #mediaTypePreviewBuilder(htmlString: string) { const parser = new DOMParser(); const doc = parser.parseFromString(htmlString, 'text/xml'); const childNodes = doc.childNodes; @@ -68,10 +68,10 @@ export class UmbMediaTypeImportModalLayout extends UmbModalBaseElement< } }); - this._fileContent = this.#MediaTypePreviewItemBuilder(elements); + this._fileContent = this.#mediaTypePreviewItemBuilder(elements); } - #MediaTypePreviewItemBuilder(elements: Array) { + #mediaTypePreviewItemBuilder(elements: Array) { const mediaTypes: Array = []; elements.forEach((MediaType) => { const info = MediaType.getElementsByTagName('Info')[0]; @@ -129,7 +129,11 @@ export class UmbMediaTypeImportModalLayout extends UmbModalBaseElement< html`
Drag and drop your file here - +
`, )} `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.repository.ts index 86733391e0..2d1f125c04 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.repository.ts @@ -21,6 +21,10 @@ export class UmbMediaTypeStructureRepository extends UmbContentTypeStructureRepo }) { return this.#dataSource.getMediaTypesOfFileExtension({ fileExtension, skip, take }); } + + async requestMediaTypesOfFolders({ skip = 0, take = 100 } = {}) { + return this.#dataSource.getMediaTypesOfFolders({ skip, take }); + } } export default UmbMediaTypeStructureRepository; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.server.data-source.ts index df88ed9ec2..8990417133 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.server.data-source.ts @@ -1,12 +1,10 @@ import type { UmbAllowedMediaTypeModel } from './types.js'; -import { UmbContentTypeStructureServerDataSourceBase } from '@umbraco-cms/backoffice/content-type'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import type { AllowedMediaTypeModel } from '@umbraco-cms/backoffice/external/backend-api'; import { MediaTypeService } from '@umbraco-cms/backoffice/external/backend-api'; +import { UmbContentTypeStructureServerDataSourceBase } from '@umbraco-cms/backoffice/content-type'; +import type { AllowedMediaTypeModel } from '@umbraco-cms/backoffice/external/backend-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; /** - * - * @class UmbMediaTypeStructureServerDataSource * @augments {UmbContentTypeStructureServerDataSourceBase} */ @@ -21,6 +19,10 @@ export class UmbMediaTypeStructureServerDataSource extends UmbContentTypeStructu getMediaTypesOfFileExtension({ fileExtension, skip, take }: { fileExtension: string; skip: number; take: number }) { return getAllowedMediaTypesOfExtension({ fileExtension, skip, take }); } + + getMediaTypesOfFolders({ skip, take }: { skip: number; take: number }) { + return getAllowedMediaTypesOfFolders({ skip, take }); + } } const getAllowedChildrenOf = (unique: string | null) => { @@ -42,6 +44,12 @@ const mapper = (item: AllowedMediaTypeModel): UmbAllowedMediaTypeModel => { }; }; +const getAllowedMediaTypesOfFolders = async ({ skip, take }: { skip: number; take: number }) => { + // eslint-disable-next-line local-rules/no-direct-api-import + const { items } = await MediaTypeService.getItemMediaTypeFolders({ skip, take }); + return items.map((item) => mapper(item)); +}; + const getAllowedMediaTypesOfExtension = async ({ fileExtension, skip, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils.ts/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils.ts/index.ts index ab82bad8c0..91b6008b49 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils.ts/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils.ts/index.ts @@ -1,13 +1,14 @@ -//TODO Can we trust this is the unique? This probably need a similar solution like the media collection repository method getDefaultConfiguration() +// TODO: Can we trust this is the unique? This probably need a similar solution like the media collection repository method getDefaultConfiguration() + /** - * + * @returns {string} The unique identifier for the Umbraco folder media-type. */ export function getUmbracoFolderUnique(): string { return 'f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d'; } /** - * - * @param unique + * @param {string} unique The unique identifier of the media-type to check. + * @returns {boolean} True if the unique identifier is the Umbraco folder media-type. */ export function isUmbracoFolder(unique?: string): boolean { return unique === getUmbracoFolderUnique(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts index d830224a7c..b6a7c9d677 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts @@ -4,8 +4,6 @@ import type { UmbMediaCollectionContext } from './media-collection.context.js'; import { UMB_MEDIA_COLLECTION_CONTEXT } from './media-collection.context-token.js'; import { customElement, html, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbCollectionDefaultElement } from '@umbraco-cms/backoffice/collection'; -import type { UmbProgressEvent } from '@umbraco-cms/backoffice/event'; - import './media-collection-toolbar.element.js'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action'; @@ -32,7 +30,7 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { }); } - async #onChange() { + async #onComplete() { this._progress = -1; this.#mediaCollection?.requestCollection(); @@ -44,8 +42,11 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { eventContext.dispatchEvent(event); } - #onProgress(event: UmbProgressEvent) { - this._progress = event.progress; + #onProgress(event: ProgressEvent) { + this._progress = (event.loaded / event.total) * 100; + if (this._progress >= 100) { + this._progress = -1; + } } protected override renderToolbar() { @@ -53,8 +54,9 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { ${when(this._progress >= 0, () => html``)} `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts index 4c54e71f50..24a92a2af8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts @@ -3,7 +3,7 @@ import { UmbMediaItemRepository } from '../../repository/index.js'; import { UMB_IMAGE_CROPPER_EDITOR_MODAL, UMB_MEDIA_PICKER_MODAL } from '../../modals/index.js'; import type { UmbCropModel, UmbMediaPickerPropertyValue } from '../../types.js'; import type { UmbMediaItemModel } from '../../repository/index.js'; -import type { UmbUploadableFileModel } from '../../dropzone/index.js'; +import type { UmbUploadableItem } from '../../dropzone/types.js'; import { customElement, html, nothing, property, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { umbConfirmModal, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; @@ -331,7 +331,7 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement, } async #onUploadCompleted(e: CustomEvent) { - const completed = e.detail?.completed as Array; + const completed = e.detail as Array; const uploaded = completed.map((file) => file.unique); this.#addItems(uploaded); } @@ -346,7 +346,7 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement, #renderDropzone() { if (this.readonly) return nothing; if (this._cards && this._cards.length >= this.max) return; - return html``; + return html` 1} @complete=${this.#onUploadCompleted}>`; } #renderItems() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-file.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-file.element.ts index a5dbf5ca44..6f7412d1a1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-file.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/preview/input-upload-field-file.element.ts @@ -24,6 +24,8 @@ export default class UmbInputUploadFieldFileElement extends UmbLitElement { #serverUrl = ''; + #loadingText = `(${this.localize.term('general_loading')}...)`; + /** * */ @@ -37,8 +39,8 @@ export default class UmbInputUploadFieldFileElement extends UmbLitElement { protected override updated(_changedProperties: PropertyValueMap | Map): void { super.updated(_changedProperties); if (_changedProperties.has('file') && this.file) { - this.extension = this.#getExtensionFromMime(this.file.type) ?? ''; - this.label = this.file.name || 'loading...'; + this.extension = this.file.name.split('.').pop() ?? ''; + this.label = this.file.name || this.#loadingText; } if (_changedProperties.has('path')) { @@ -46,22 +48,11 @@ export default class UmbInputUploadFieldFileElement extends UmbLitElement { if (this.file) return; this.extension = this.path.split('.').pop() ?? ''; - this.label = this.#serverUrl ? this.path.substring(this.#serverUrl.length) : 'loading...'; + this.label = this.#serverUrl ? this.path.substring(this.#serverUrl.length) : this.#loadingText; } } } - #getExtensionFromMime(mime: string): string { - if (!mime) return ''; //folders - - const extension = mime.split('/')[1]; - if (extension === 'svg+xml') { - return 'svg'; - } else { - return extension; - } - } - #renderLabel() { if (this.path) { // Don't make it a link if it's a temp file upload. diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts index 2ee586305a..19a7896c1c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts @@ -1,272 +1,328 @@ -import type { UmbMediaDetailModel } from '../types.js'; import { UmbMediaDetailRepository } from '../repository/index.js'; -import { UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL } from './modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.js'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbMediaDetailModel, UmbMediaValueModel } from '../types.js'; +import { UmbFileDropzoneItemStatus } from './types.js'; +import { UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL } from './modals/index.js'; +import type { + UmbUploadableFile, + UmbUploadableFolder, + UmbFileDropzoneDroppedItems, + UmbFileDropzoneProgress, + UmbUploadableItem, + UmbAllowedMediaTypesOfExtension, + UmbAllowedChildrenOfMediaType, +} from './types.js'; +import { TemporaryFileStatus, UmbTemporaryFileManager } from '@umbraco-cms/backoffice/temporary-file'; +import { UmbArrayState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; -import { type UmbAllowedMediaTypeModel, UmbMediaTypeStructureRepository } from '@umbraco-cms/backoffice/media-type'; -import { - TemporaryFileStatus, - UmbTemporaryFileManager, - type UmbTemporaryFileModel, -} from '@umbraco-cms/backoffice/temporary-file'; import { UmbId } from '@umbraco-cms/backoffice/id'; +import { UmbMediaTypeStructureRepository } from '@umbraco-cms/backoffice/media-type'; import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; -import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; - -export interface UmbUploadableFileModel extends UmbTemporaryFileModel { - unique: string; - mediaTypeUnique: string; -} - -export interface UmbUploadableExtensionModel { - fileExtension: string; - mediaTypes: Array; -} +import type { UmbAllowedMediaTypeModel } from '@umbraco-cms/backoffice/media-type'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbTemporaryFileModel } from '@umbraco-cms/backoffice/temporary-file'; /** - * Manages the dropzone and uploads files to the server. - * @function createFilesAsMedia - Upload files to the server and creates the items using corresponding media type. - * @function createFilesAsTemporary - Upload the files as temporary files and returns the data. - * @observable completed - Emits an array of completed uploads. + * Manages the dropzone and uploads folders and files to the server. + * @function createMediaItems - Upload files and folders to the server and creates the items using corresponding media type. + * @function createTemporaryFiles - Upload the files as temporary files and returns the data. + * @observable progress - Emits the number of completed items and total items. + * @observable progressItems - Emits the items with their current status. */ export class UmbDropzoneManager extends UmbControllerBase { - #host; - - #tempFileManager = new UmbTemporaryFileManager(this); + readonly #host: UmbControllerHost; + #isFoldersAllowed = true; #mediaTypeStructure = new UmbMediaTypeStructureRepository(this); #mediaDetailRepository = new UmbMediaDetailRepository(this); - #completed = new UmbArrayState( - [], - (upload) => upload.temporaryUnique, - ); - public readonly completed = this.#completed.asObservable(); + #tempFileManager = new UmbTemporaryFileManager(this); + + // The available media types for a file extension. + readonly #availableMediaTypesOf = new UmbArrayState([], (x) => x.extension); + + // The media types that the parent will allow to be created under it. + readonly #allowedChildrenOf = new UmbArrayState([], (x) => x.mediaTypeUnique); + + readonly #progress = new UmbObjectState({ total: 0, completed: 0 }); + public readonly progress = this.#progress.asObservable(); + + readonly #progressItems = new UmbArrayState([], (x) => x.unique); + public readonly progressItems = this.#progressItems.asObservable(); constructor(host: UmbControllerHost) { super(host); this.#host = host; } + public setIsFoldersAllowed(isAllowed: boolean) { + this.#isFoldersAllowed = isAllowed; + } + + public getIsFoldersAllowed(): boolean { + return this.#isFoldersAllowed; + } + + /** @deprecated Please use `createMediaItems()` instead; this method will be removed in Umbraco 17. */ + public createFilesAsMedia = this.createMediaItems; + + /** + * Uploads files and folders to the server and creates the media items with corresponding media type.\ + * Allows the user to pick a media type option if multiple types are allowed. + * @param {UmbFileDropzoneDroppedItems} items - The files and folders to upload + * @param {string | null} parentUnique - Where the items should be uploaded + */ + public async createMediaItems(items: UmbFileDropzoneDroppedItems, parentUnique: string | null = null) { + const uploadableItems = await this.#setupProgress(items, parentUnique); + if (uploadableItems.length === 1) { + // When there is only one item being uploaded, allow the user to pick the media type, if more than one is allowed. + await this.#createOneMediaItem(uploadableItems[0]); + } else { + // When there are multiple items being uploaded, automatically pick the media types for each item. We probably want to allow the user to pick the media type in the future. + await this.#createMediaItems(uploadableItems); + } + } + + /** @deprecated Please use `createTemporaryFiles()` instead; this method will be removed in Umbraco 17. */ + public createFilesAsTemporary = this.createTemporaryFiles; + /** * Uploads the files as temporary files and returns the data. - * @param files - * @returns Promise> + * @param { File[] } files - The files to upload. + * @returns {Promise>} - Files as temporary files. */ - public async createFilesAsTemporary(files: Array): Promise> { - this.#completed.setValue([]); - const temporaryFiles: Array = []; + public async createTemporaryFiles(files: Array) { + const uploadableItems = (await this.#setupProgress({ files, folders: [] }, null)) as Array; - for (const file of files) { - const uploaded = await this.#tempFileManager.uploadOne({ temporaryUnique: UmbId.new(), file }); - this.#completed.setValue([...this.#completed.getValue(), uploaded]); - temporaryFiles.push(uploaded); - } + const uploadedItems: Array = []; - return temporaryFiles; - } + for (const item of uploadableItems) { + // Upload as temp file + const uploaded = await this.#tempFileManager.uploadOne({ + temporaryUnique: item.temporaryFile.temporaryUnique, + file: item.temporaryFile.file, + }); - /** - * Uploads files to the server and creates the items with corresponding media type. - * Allows the user to pick a media type option if multiple types are allowed. - * @param files - * @param parentUnique - * @returns Promise - */ - public async createFilesAsMedia(files: Array, parentUnique: string | null) { - if (!files.length) return; - if (files.length === 1) return this.#handleOneOneFile(files[0], parentUnique); + // Update progress + const progress = this.#progress.getValue(); + this.#progress.update({ completed: progress.completed + 1 }); - // Handler for multiple files dropped - - this.#completed.setValue([]); - // removes duplicate file types so we don't call endpoints unnecessarily when building options. - const mimeTypes = [...new Set(files.map((file) => file.type))]; - const optionsArray = await this.#buildOptionsArrayFrom( - mimeTypes.map((mimetype) => this.#getExtensionFromMime(mimetype)), - parentUnique, - ); - - if (!optionsArray.length) return; // None of the files are allowed in current dropzone. - - // Building an array of uploadable files. Do we want to build an array of failed files to let the user know which ones? - const uploadableFiles: Array = []; - const notAllowedFiles: Array = []; - - for (const file of files) { - const extension = this.#getExtensionFromMime(file.type); - if (!extension) { - // Folders have no extension on file drop. We assume it is a folder being uploaded. - continue; - } - const options = optionsArray.find((option) => option.fileExtension === extension)?.mediaTypes; - - if (!options || !options.length) { - // TODO Current dropped file not allowed in this area. Find a good way to show this to the user after we finish uploading the rest of the files. - notAllowedFiles.push(file); - continue; + if (uploaded.status === TemporaryFileStatus.SUCCESS) { + this.#progressItems.updateOne(item.unique, { status: UmbFileDropzoneItemStatus.COMPLETE }); + } else { + this.#progressItems.updateOne(item.unique, { status: UmbFileDropzoneItemStatus.ERROR }); } - // Since we are uploading multiple files, we will pick first allowed option. - // Consider a way we can handle this differently in the future to let the user choose. Maybe a list of all files with an allowed media type dropdown? - const mediaType = options[0]; - uploadableFiles.push({ - temporaryUnique: UmbId.new(), - file, - mediaTypeUnique: mediaType.unique, - unique: UmbId.new(), - }); + // Add to return value + uploadedItems.push(uploaded); } - notAllowedFiles.forEach((file) => { - // TODO: It seems like some implementation(user feedback) is missing here? [NL] - console.error(`File ${file.name} of type ${file.type} is not allowed here.`); - }); - - if (!uploadableFiles.length) return; - - await this.#handleUpload(uploadableFiles, parentUnique); - } - - async #handleOneOneFile(file: File, parentUnique: string | null) { - this.#completed.setValue([]); - const extension = this.#getExtensionFromMime(file.type); - - if (!extension) { - // TODO Folders have no extension on file drop. Assume it is a folder being uploaded. - return; - } - - const optionsArray = await this.#buildOptionsArrayFrom([extension], parentUnique); - if (!optionsArray.length || !optionsArray[0].mediaTypes.length) { - throw new Error(`File ${file.name} of type ${file.type} is not allowed here.`); // Parent does not allow this file type here. - } - - const mediaTypes = optionsArray[0].mediaTypes; - if (mediaTypes.length === 1) { - // Only one allowed option, upload file using that option. - const uploadableFile: UmbUploadableFileModel = { - unique: UmbId.new(), - temporaryUnique: UmbId.new(), - file, - mediaTypeUnique: mediaTypes[0].unique, - }; - - await this.#handleUpload([uploadableFile], parentUnique); - return; - } - - // Multiple options, show a dialog for the user to pick one. - const mediaType = await this.#showDialogMediaTypePicker(mediaTypes); - if (!mediaType) return; // Upload cancelled. - - const uploadableFile: UmbUploadableFileModel = { - unique: UmbId.new(), - temporaryUnique: UmbId.new(), - file, - mediaTypeUnique: mediaType.unique, - }; - await this.#handleUpload([uploadableFile], parentUnique); - } - - #getExtensionFromMime(mime: string): string { - //TODO Temporary solution. - if (!mime) return ''; //folders - const extension = mime.split('/')[1]; - switch (extension) { - case 'svg+xml': - return 'svg'; - default: - return extension; - } - } - - async #buildOptionsArrayFrom( - fileExtensions: Array, - parentUnique: string | null, - ): Promise> { - let parentMediaType: string | null = null; - if (parentUnique) { - const { data } = await this.#mediaDetailRepository.requestByUnique(parentUnique); - parentMediaType = data?.mediaType.unique ?? null; - } - - // Getting all media types allowed in our current position based on parent's media type. - - const { data: allAllowedMediaTypes } = await this.#mediaTypeStructure.requestAllowedChildrenOf(parentMediaType); - if (!allAllowedMediaTypes?.items.length) return []; - - const allowedByParent = allAllowedMediaTypes.items; - - // Building an array of options the files can be uploaded as. - const options: Array = []; - - for (const fileExtension of fileExtensions) { - const extensionOptions = await this.#mediaTypeStructure.requestMediaTypesOf({ fileExtension }); - const mediaTypes = extensionOptions.filter((option) => { - return allowedByParent.find((allowed) => option.unique === allowed.unique); - }); - options.push({ fileExtension, mediaTypes }); - } - return options; + return uploadedItems; } async #showDialogMediaTypePicker(options: Array) { const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); const modalContext = modalManager.open(this.#host, UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL, { data: { options } }); const value = await modalContext.onSubmit().catch(() => undefined); - return value ? { unique: value.mediaTypeUnique ?? options[0].unique } : null; + return value?.mediaTypeUnique; } - async #handleUpload(files: Array, parentUnique: string | null) { - for (const file of files) { - const upload = (await this.#tempFileManager.uploadOne(file)) as UmbUploadableFileModel; + async #createOneMediaItem(item: UmbUploadableItem) { + const options = await this.#getMediaTypeOptions(item); + if (!options.length) { + return this.#updateProgress(item, UmbFileDropzoneItemStatus.NOT_ALLOWED); + } - if (upload.status === TemporaryFileStatus.SUCCESS) { - // Upload successful. Create media item. - // TODO: Use a scaffolding feature to ensure consistency. [NL] - const preset: Partial = { - unique: file.unique, - mediaType: { - unique: upload.mediaTypeUnique, - collection: null, - }, - variants: [ - { - culture: null, - segment: null, - name: upload.file.name, - createDate: null, - updateDate: null, - }, - ], - values: [ - { - // We do not need to parse the right editorAlias here, because the server does not read it. If we need to parse it we would need to load the contentType to make this happen properly. [NL] - editorAlias: null as any, - alias: 'umbracoFile', - value: { temporaryFileId: upload.temporaryUnique }, - culture: null, - segment: null, - }, - ], - }; - const { data } = await this.#mediaDetailRepository.createScaffold(preset); - await this.#mediaDetailRepository.create(data!, parentUnique); - } - // TODO Find a good way to show files that ended up as TemporaryFileStatus.ERROR. Notice that they were allowed in current area + const mediaTypeUnique = options.length > 1 ? await this.#showDialogMediaTypePicker(options) : options[0].unique; - this.#completed.setValue([...this.#completed.getValue(), upload]); + if (!mediaTypeUnique) { + return this.#updateProgress(item, UmbFileDropzoneItemStatus.CANCELLED); + } + + if (item.temporaryFile) { + this.#handleFile(item as UmbUploadableFile, mediaTypeUnique); + } else if (item.folder) { + this.#handleFolder(item as UmbUploadableFolder, mediaTypeUnique); } } - private _reset() { - // + async #createMediaItems(uploadableItems: Array) { + for (const item of uploadableItems) { + const options = await this.#getMediaTypeOptions(item); + if (!options.length) { + this.#updateProgress(item, UmbFileDropzoneItemStatus.NOT_ALLOWED); + continue; + } + + const mediaTypeUnique = options[0].unique; + + // Handle files and folders differently: a file is uploaded as temp then created as a media item, and a folder is created as a media item directly + if (item.temporaryFile) { + await this.#handleFile(item as UmbUploadableFile, mediaTypeUnique); + } else if (item.folder) { + await this.#handleFolder(item as UmbUploadableFolder, mediaTypeUnique); + } + } } + async #handleFile(item: UmbUploadableFile, mediaTypeUnique: string) { + // Upload the file as a temporary file and update progress. + const temporaryFile = await this.#uploadAsTemporaryFile(item); + if (temporaryFile.status !== TemporaryFileStatus.SUCCESS) { + this.#updateProgress(item, UmbFileDropzoneItemStatus.ERROR); + return; + } + + // Create the media item. + const scaffold = await this.#getItemScaffold(item, mediaTypeUnique); + const { data } = await this.#mediaDetailRepository.create(scaffold, item.parentUnique); + + if (data) { + this.#updateProgress(item, UmbFileDropzoneItemStatus.COMPLETE); + } else { + this.#updateProgress(item, UmbFileDropzoneItemStatus.ERROR); + } + } + + async #handleFolder(item: UmbUploadableFolder, mediaTypeUnique: string) { + const scaffold = await this.#getItemScaffold(item, mediaTypeUnique); + const { data } = await this.#mediaDetailRepository.create(scaffold, item.parentUnique); + if (data) { + this.#updateProgress(item, UmbFileDropzoneItemStatus.COMPLETE); + } else { + this.#updateProgress(item, UmbFileDropzoneItemStatus.ERROR); + } + } + + async #uploadAsTemporaryFile(item: UmbUploadableFile) { + return await this.#tempFileManager.uploadOne({ + temporaryUnique: item.temporaryFile.temporaryUnique, + file: item.temporaryFile.file, + }); + } + + // Media types + async #getMediaTypeOptions(item: UmbUploadableItem): Promise> { + // Check the parent which children media types are allowed + const parent = item.parentUnique ? await this.#mediaDetailRepository.requestByUnique(item.parentUnique) : null; + const allowedChildren = await this.#getAllowedChildrenOf(parent?.data?.mediaType.unique ?? null); + + const extension = item.temporaryFile?.file.name.split('.').pop() ?? null; + + // Check which media types allow the file's extension + const availableMediaType = await this.#getAvailableMediaTypesOf(extension); + + if (!availableMediaType.length) return []; + + const options = allowedChildren.filter((x) => availableMediaType.find((y) => y.unique === x.unique)); + return options; + } + + async #getAvailableMediaTypesOf(extension: string | null) { + // Check if we already have information on this file extension. + const available = this.#availableMediaTypesOf + .getValue() + .find((x) => x.extension === extension)?.availableMediaTypes; + if (available) return available; + + // Request information on this file extension + const availableMediaTypes = extension + ? await this.#mediaTypeStructure.requestMediaTypesOf({ fileExtension: extension }) + : await this.#mediaTypeStructure.requestMediaTypesOfFolders(); + + this.#availableMediaTypesOf.appendOne({ extension, availableMediaTypes }); + return availableMediaTypes; + } + + async #getAllowedChildrenOf(mediaTypeUnique: string | null) { + //Check if we already got information on this media type. + const allowed = this.#allowedChildrenOf + .getValue() + .find((x) => x.mediaTypeUnique === mediaTypeUnique)?.allowedChildren; + if (allowed) return allowed; + + // Request information on this media type. + const { data } = await this.#mediaTypeStructure.requestAllowedChildrenOf(mediaTypeUnique); + if (!data) throw new Error('Parent media type does not exists'); + + this.#allowedChildrenOf.appendOne({ mediaTypeUnique, allowedChildren: data.items }); + return data.items; + } + + // Scaffold + async #getItemScaffold(item: UmbUploadableItem, mediaTypeUnique: string): Promise { + // TODO: Use a scaffolding feature to ensure consistency. [NL] + const name = item.temporaryFile ? item.temporaryFile.file.name : (item.folder?.name ?? ''); + const umbracoFile: UmbMediaValueModel = { + editorAlias: null as any, + alias: 'umbracoFile', + value: { temporaryFileId: item.temporaryFile?.temporaryUnique }, + culture: null, + segment: null, + }; + + const preset: Partial = { + unique: item.unique, + mediaType: { unique: mediaTypeUnique, collection: null }, + variants: [{ culture: null, segment: null, createDate: null, updateDate: null, name }], + values: item.temporaryFile ? [umbracoFile] : undefined, + }; + const { data } = await this.#mediaDetailRepository.createScaffold(preset); + return data!; + } + + // Progress handling + async #setupProgress(items: UmbFileDropzoneDroppedItems, parent: string | null) { + const current = this.#progress.getValue(); + const currentItems = this.#progressItems.getValue(); + + const uploadableItems = this.#prepareItemsAsUploadable({ folders: items.folders, files: items.files }, parent); + + this.#progressItems.setValue([...currentItems, ...uploadableItems]); + this.#progress.setValue({ total: current.total + uploadableItems.length, completed: current.completed }); + + return uploadableItems; + } + + #updateProgress(item: UmbUploadableItem, status: UmbFileDropzoneItemStatus) { + this.#progressItems.updateOne(item.unique, { status }); + const progress = this.#progress.getValue(); + this.#progress.update({ completed: progress.completed + 1 }); + } + + readonly #prepareItemsAsUploadable = ( + { folders, files }: UmbFileDropzoneDroppedItems, + parentUnique: string | null, + ): Array => { + const items: Array = []; + + for (const file of files) { + const unique = UmbId.new(); + if (file.type) { + items.push({ + unique, + parentUnique, + status: UmbFileDropzoneItemStatus.WAITING, + temporaryFile: { file, temporaryUnique: UmbId.new() }, + }); + } + } + + for (const subfolder of folders) { + const unique = UmbId.new(); + items.push({ + unique, + parentUnique, + status: UmbFileDropzoneItemStatus.WAITING, + folder: { name: subfolder.folderName }, + }); + + items.push(...this.#prepareItemsAsUploadable({ folders: subfolder.folders, files: subfolder.files }, unique)); + } + return items; + }; + public override destroy() { this.#tempFileManager.destroy(); - this.#completed.destroy(); super.destroy(); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts index f54bd1cbeb..247528d08a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts @@ -1,39 +1,61 @@ -import { UmbDropzoneManager, type UmbUploadableFileModel } from './dropzone-manager.class.js'; -import { UmbProgressEvent } from '@umbraco-cms/backoffice/event'; -import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; -import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UmbDropzoneManager } from './dropzone-manager.class.js'; +import { UmbFileDropzoneItemStatus, type UmbUploadableItem } from './types.js'; +import { css, customElement, html, ifDefined, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { UmbTemporaryFileModel } from '@umbraco-cms/backoffice/temporary-file'; +import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; @customElement('umb-dropzone') export class UmbDropzoneElement extends UmbLitElement { @property({ attribute: false }) parentUnique: string | null = null; - @property({ type: Boolean }) - multiple: boolean = true; - @property({ type: Boolean }) createAsTemporary: boolean = false; - @property({ type: Array, attribute: false }) - accept: Array = []; + @property({ type: String }) + accept?: string; - //TODO: logic to disable the dropzone? + @property({ type: Boolean, reflect: true }) + multiple: boolean = false; - #files: Array = []; + @property({ type: Boolean, reflect: true }) + disabled = false; + @property({ type: Boolean, attribute: 'disable-folder-upload', reflect: true }) + public get disableFolderUpload() { + return this._disableFolderUpload; + } + public set disableFolderUpload(isAllowed: boolean) { + this.dropzoneManager.setIsFoldersAllowed(!isAllowed); + } + private readonly _disableFolderUpload = false; + + @state() + private _progressItems: Array = []; + + public dropzoneManager: UmbDropzoneManager; + + /** + * @deprecated Please use `getItems()` instead; this method will be removed in Umbraco 17. + * @returns {Array} An array of uploadable items. + */ public getFiles() { - return this.#files; + return this.getItems(); + } + + public getItems() { + return this._progressItems; } public browse() { + if (this.disabled) return; const element = this.shadowRoot?.querySelector('#dropzone') as UUIFileDropzoneElement; return element.browse(); } constructor() { super(); + this.dropzoneManager = new UmbDropzoneManager(this); document.addEventListener('dragenter', this.#handleDragEnter.bind(this)); document.addEventListener('dragleave', this.#handleDragLeave.bind(this)); document.addEventListener('drop', this.#handleDrop.bind(this)); @@ -41,70 +63,73 @@ export class UmbDropzoneElement extends UmbLitElement { override disconnectedCallback(): void { super.disconnectedCallback(); + this.dropzoneManager.destroy(); document.removeEventListener('dragenter', this.#handleDragEnter.bind(this)); document.removeEventListener('dragleave', this.#handleDragLeave.bind(this)); document.removeEventListener('drop', this.#handleDrop.bind(this)); } #handleDragEnter(e: DragEvent) { + if (this.disabled) return; // Avoid collision with UmbSorterController const types = e.dataTransfer?.types; if (!types?.length || !types?.includes('Files')) return; + this.toggleAttribute('dragging', true); } #handleDragLeave() { + if (this.disabled) return; this.toggleAttribute('dragging', false); } #handleDrop(event: DragEvent) { event.preventDefault(); + if (this.disabled) return; this.toggleAttribute('dragging', false); } async #onDropFiles(event: UUIFileDropzoneEvent) { - // TODO Handle of folder uploads. + if (this.disabled) return; + if (!event.detail.files.length && !event.detail.folders.length) return; - const files: Array = event.detail.files; - if (!files.length) return; + // TODO Create some placeholder items while files are being uploaded? Could update them as they get completed. + // We can observe progressItems and check for any files that did not succeed, then show some kind of dialog to the user with the information. - const dropzoneManager = new UmbDropzoneManager(this); this.observe( - dropzoneManager.completed, - (completed) => { - if (!completed.length) return; - - const progress = Math.floor(completed.length / files.length); - this.dispatchEvent(new UmbProgressEvent(progress)); - - if (completed.length === files.length) { - this.#files = completed; - this.dispatchEvent(new CustomEvent('change', { detail: { completed } })); - dropzoneManager.destroy(); - } - }, - '_observeCompleted', + this.dropzoneManager.progress, + (progress) => + this.dispatchEvent(new ProgressEvent('progress', { loaded: progress.completed, total: progress.total })), + '_observeProgress', ); - //TODO Create some placeholder items while files are being uploaded? Could update them as they get completed. + + this.observe(this.dropzoneManager.progressItems, (progressItems: Array) => { + this._progressItems = progressItems; + const waiting = progressItems.find((item) => item.status === UmbFileDropzoneItemStatus.WAITING); + if (progressItems.length && !waiting) { + this.dispatchEvent(new CustomEvent('complete', { detail: progressItems })); + } + }); + if (this.createAsTemporary) { - await dropzoneManager.createFilesAsTemporary(files); + this.dropzoneManager.createTemporaryFiles(event.detail.files); } else { - await dropzoneManager.createFilesAsMedia(files, this.parentUnique); + this.dropzoneManager.createMediaItems(event.detail, this.parentUnique); } } override render() { return html``; + label=${this.localize.term('media_dragAndDropYourFilesIntoTheArea')}>`; } static override styles = [ css` - :host([dragging]) #dropzone { + :host(:not([disabled])[dragging]) #dropzone { opacity: 1; pointer-events: all; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/types.ts new file mode 100644 index 0000000000..0e99dbb2d7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/types.ts @@ -0,0 +1,47 @@ +import type { UUIFileFolder } from '@umbraco-cms/backoffice/external/uui'; +import type { UmbAllowedMediaTypeModel } from '@umbraco-cms/backoffice/media-type'; +import type { UmbTemporaryFileModel } from '@umbraco-cms/backoffice/temporary-file'; + +export interface UmbFileDropzoneDroppedItems { + files: Array; + folders: Array; +} + +export interface UmbUploadableItem { + unique: string; + parentUnique: string | null; + status: UmbFileDropzoneItemStatus; + folder?: { name: string }; + temporaryFile?: UmbTemporaryFileModel; +} + +export interface UmbUploadableFile extends UmbUploadableItem { + temporaryFile: UmbTemporaryFileModel; +} + +export interface UmbUploadableFolder extends UmbUploadableItem { + folder: { name: string }; +} + +export interface UmbAllowedMediaTypesOfExtension { + extension: string | null; // Null is considered a folder. + availableMediaTypes: Array; +} + +export interface UmbAllowedChildrenOfMediaType { + mediaTypeUnique: string | null; + allowedChildren: Array; +} + +export interface UmbFileDropzoneProgress { + total: number; + completed: number; +} + +export enum UmbFileDropzoneItemStatus { + WAITING = 'waiting', + COMPLETE = 'complete', + NOT_ALLOWED = 'not allowed', + CANCELLED = 'cancelled', + ERROR = 'error', +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts index b83579178f..eac3c2bfa3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts @@ -168,7 +168,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement< #renderBody() { return html`${this.#renderToolbar()} - this.#loadMediaFolder()} .parentUnique=${this._currentMediaEntity.unique}> + this.#loadMediaFolder()} .parentUnique=${this._currentMediaEntity.unique}> ${ !this._mediaFilteredList.length ? html`

${this.localize.term('content_listViewNoItems')}

`