From b6ebf04bd5517eb2e8364198370b873e07fcd252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 4 Oct 2024 15:33:58 +0200 Subject: [PATCH 01/14] clean up --- .../block/workspace/block-element-manager.ts | 1 - .../property-type-based-property.element.ts | 1 - .../property/property/property.context.ts | 28 +++++++++---------- .../property/property/property.element.ts | 18 ++++++------ 4 files changed, 22 insertions(+), 26 deletions(-) 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/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/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 => { From 5024fff539449701b6c693b36b29398a1c744dac Mon Sep 17 00:00:00 2001 From: Ibrahim Muhammad Nada Date: Mon, 7 Oct 2024 12:26:33 +0300 Subject: [PATCH 02/14] fix: Tweak Arabic translations (#2414) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * tweak translations * commit * fix الأطفال --- .../src/assets/lang/ar.ts | 104 +++++++++--------- 1 file changed, 52 insertions(+), 52 deletions(-) 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..7e82bc78ce 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: { @@ -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: 'مستشهد به من قبل', From 6131d739ed54b172dc28e1c20a55bcb33fb9dd40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 7 Oct 2024 11:26:49 +0200 Subject: [PATCH 03/14] improve generation feedback --- src/Umbraco.Web.UI.Client/devops/icons/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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!'); }); }; From cde3b2ad67ddf40c69eb6a05c665564e31ffa681 Mon Sep 17 00:00:00 2001 From: Sean Thorne <29239704+Bakersbakebread@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:29:48 +0100 Subject: [PATCH 04/14] style: increase z-index of element in installer layout (#2413) --- .../src/apps/installer/shared/layout/installer-layout.element.ts | 1 + 1 file changed, 1 insertion(+) 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 { From bd30a2e047bcd753ba7769526eb451a74694f9fa Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:42:01 +0200 Subject: [PATCH 05/14] Feature: Dictionary Searchbar (#2415) --- .../components/collection-toolbar.element.ts | 4 ++ .../dictionary-collection.element.ts | 54 +++++++++++++++++++ .../dictionary/collection/manifests.ts | 1 + 3 files changed, 59 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/dictionary/collection/dictionary-collection.element.ts 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/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, From 63f855764fa6f4aede6f06e2093014ef6feec63a Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 7 Oct 2024 13:14:49 +0200 Subject: [PATCH 06/14] Merge commit from fork * fix: remove unwanted HTML from translation values * feat: add a general sanitizeHTML function * fix: use the `sanitizeHTML` function where values are showed in the Backoffice --- src/Umbraco.Web.UI.Client/src/assets/lang/ar.ts | 4 ++-- src/Umbraco.Web.UI.Client/src/assets/lang/bs.ts | 4 ++-- .../src/assets/lang/cs-cz.ts | 4 ++-- .../src/assets/lang/cy-gb.ts | 4 ++-- .../src/assets/lang/da-dk.ts | 4 ++-- .../src/assets/lang/de-de.ts | 4 ++-- .../src/assets/lang/en-us.ts | 4 ++-- src/Umbraco.Web.UI.Client/src/assets/lang/en.ts | 4 ++-- .../src/assets/lang/es-es.ts | 6 +++--- .../src/assets/lang/fr-fr.ts | 5 ++--- .../src/assets/lang/he-il.ts | 2 +- .../src/assets/lang/hr-hr.ts | 4 ++-- .../src/assets/lang/it-it.ts | 2 +- .../src/assets/lang/ja-jp.ts | 2 +- .../src/assets/lang/ko-kr.ts | 2 +- .../src/assets/lang/nb-no.ts | 4 +++- .../src/assets/lang/nl-nl.ts | 4 ++-- .../src/assets/lang/pl-pl.ts | 4 ++-- .../src/assets/lang/pt-br.ts | 2 +- .../src/assets/lang/ru-ru.ts | 4 ++-- .../src/assets/lang/tr-tr.ts | 4 ++-- .../src/assets/lang/uk-ua.ts | 4 ++-- .../src/assets/lang/zh-cn.ts | 4 ++-- .../src/assets/lang/zh-tw.ts | 3 +-- .../src/packages/core/utils/index.ts | 1 + .../utils/sanitize/sanitize-html.function.ts | 10 ++++++++++ .../workspace-view-dictionary-editor.element.ts | 17 +++++++++++------ .../input-markdown.element.ts | 4 ++-- 28 files changed, 68 insertions(+), 52 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/utils/sanitize/sanitize-html.function.ts 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 7e82bc78ce..64bb555510 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/ar.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/ar.ts @@ -547,9 +547,9 @@ export default { createNew: 'إنشاء عنصر قاموس جديد', }, dictionaryItem: { - description: "\n تحرير الإصدارات اللغوية المختلفة لعنصر القاموس '%0%' أدناه\n ", + description: "تحرير الإصدارات اللغوية المختلفة لعنصر القاموس '%0%' أدناه", displayName: 'اسم الثقافة', - changeKeyError: "\n المفتاح '%0%' موجود بالفعل.\n ", + changeKeyError: "المفتاح '%0%' موجود بالفعل.", overviewTitle: 'نظرة عامة على القاموس', }, examineManagement: { 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 08ac94fae7..5190da61f0 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 3a8de51a46..17d2f3271e 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 cd803070bb..26fe42eea5 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 @@ -597,9 +597,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 88e2129900..d714c4675e 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -606,9 +606,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/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/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/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..56fb515af0 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 @@ -6,6 +6,7 @@ import { css, html, customElement, state, repeat, ifDefined, unsafeHTML } from ' 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/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)}`; } From b9bf22481fa736ba78c01bc45835668a79c71c5c Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 7 Oct 2024 13:22:59 +0200 Subject: [PATCH 07/14] chore: fix linting --- .../workspace/views/workspace-view-dictionary-editor.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 56fb515af0..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,7 +2,7 @@ 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'; From 2a58e432165cf75f7374e56f8c1df3b3ae0dd423 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Mon, 7 Oct 2024 13:56:00 +0200 Subject: [PATCH 08/14] Feature: Adds the missing dictionary to search (#2419) * Feature: Adds the missing dictionary to search * Tweaks import sorting --------- Co-authored-by: leekelleher --- .../src/packages/dictionary/manifests.ts | 2 + .../packages/dictionary/search/constants.ts | 1 + .../search/dictionary-search.repository.ts | 23 ++++++++ .../dictionary-search.server.data-source.ts | 54 +++++++++++++++++++ .../search/dictionary.search-provider.ts | 25 +++++++++ .../src/packages/dictionary/search/index.ts | 1 + .../packages/dictionary/search/manifests.ts | 21 ++++++++ 7 files changed, 127 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/dictionary/search/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/dictionary/search/dictionary-search.repository.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/dictionary/search/dictionary-search.server.data-source.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/dictionary/search/dictionary.search-provider.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/dictionary/search/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/dictionary/search/manifests.ts 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], + }, +]; From 69f01c7a59257c99169db5b761fe133ddcd7d70f Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:45:04 +0200 Subject: [PATCH 09/14] chore: eslint --- .../src/packages/core/localization/manifests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'), - } + }, ]; From f4fb118b9a820f98839b3bec01565d29a04c47f2 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:45:47 +0200 Subject: [PATCH 10/14] fix: ensure the fallback language `en.ts` has all of the same variables as the other languages, namely en-us had a few new variables --- .../src/assets/lang/en-us.ts | 20 ++++++++++++++----- .../src/assets/lang/en.ts | 20 ++++++++++++++++--- 2 files changed, 32 insertions(+), 8 deletions(-) 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 26fe42eea5..f14e5b19fc 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', 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 d714c4675e..816ef955ea 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%'", }, @@ -1901,6 +1904,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', @@ -1910,6 +1921,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.', @@ -2043,6 +2055,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:', From 77cb1e39a5ec3ec28656ba6700e4fb3c72bc704e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 8 Oct 2024 13:36:56 +0200 Subject: [PATCH 11/14] update comment and example --- .../examples/block-custom-view/block-custom-view.ts | 3 ++- src/Umbraco.Web.UI.Client/examples/block-custom-view/index.ts | 2 +- .../components/block-grid-entry/block-grid-entry.element.ts | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) 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/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!) From b65993c97aeadf975a17f8ff27d3fa476ee6f3ad Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:57:06 +0200 Subject: [PATCH 12/14] V15: Upload folders in dropzone (#2116) * handling of folders in dropzone * consolelog * load bar * request folders, skip OS files, cleanup * cleanup * deprecated * remove comments dropzone * remove unused interface * mime util * setup mime and constructor * update * setup types and saving * remove observe * rename method in repo * destroy first when total is completed * ordering and cleanup * comment * move logic back to dropzone manager * document and media type imports * scaffold * update util * update types * update progress, cleanup in dropzone manager * from switch to if statement * sonarcloud pratice * css disabled * Fixed @sonarcloud issues * Added deprecation notices on renamed methods * file extension --------- Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Co-authored-by: leekelleher --- .../document-type-import-modal.element.ts | 16 +- .../modal/media-type-import-modal.element.ts | 24 +- .../media-type-structure.repository.ts | 4 + ...media-type-structure.server.data-source.ts | 18 +- .../media/media-types/utils.ts/index.ts | 9 +- .../collection/media-collection.element.ts | 13 +- .../input-rich-media.element.ts | 6 +- .../media/dropzone/dropzone-manager.class.ts | 502 ++++++++++-------- .../media/media/dropzone/dropzone.element.ts | 93 ++-- .../packages/media/media/dropzone/types.ts | 47 ++ .../media-picker-modal.element.ts | 2 +- 11 files changed, 442 insertions(+), 292 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/types.ts 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/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..091d168faa 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() { @@ -54,7 +55,7 @@ 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..b0a1e49e2e 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``; } #renderItems() { 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..7975f36562 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,9 +1,8 @@ -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 { @@ -16,24 +15,47 @@ export class UmbDropzoneElement extends UmbLitElement { @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 }) + disabled = false; - #files: Array = []; + @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..c29baf210f 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')}

` From 5173ff4de94e8e8f968f823a25f46c94c7285d9b Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:56:53 +0200 Subject: [PATCH 13/14] Bugfix: Uploadfield file preview shows file extension (#2426) --- .../input-upload-field-file.element.ts | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) 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. From 0e07decfd8431002cf3c2d5b5408aeca80581122 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Wed, 9 Oct 2024 08:31:33 +0200 Subject: [PATCH 14/14] Bugfix: Allow/disallow multiple files for dropzone (#2429) * Bugfix: Allow/disallow multiple files for dropzone --------- Co-authored-by: Nikolaj Brask-Nielsen --- .../media/media/collection/media-collection.element.ts | 1 + .../components/input-rich-media/input-rich-media.element.ts | 2 +- .../src/packages/media/media/dropzone/dropzone.element.ts | 6 +++--- .../media/modals/media-picker/media-picker-modal.element.ts | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) 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 091d168faa..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 @@ -54,6 +54,7 @@ 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 b0a1e49e2e..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 @@ -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/dropzone/dropzone.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts index 7975f36562..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 @@ -9,15 +9,15 @@ 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: String }) accept?: string; + @property({ type: Boolean, reflect: true }) + multiple: boolean = false; + @property({ type: Boolean, reflect: true }) disabled = false; 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 c29baf210f..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')}

`