Merge branch 'main' into v15/feature/select-all-option-in-publishing-dialogs

This commit is contained in:
Mads Rasmussen
2024-10-09 10:32:00 +02:00
committed by GitHub
60 changed files with 820 additions and 453 deletions

View File

@@ -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!');
});
};

View File

@@ -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;
}

View File

@@ -5,6 +5,6 @@ export const manifests: Array<UmbExtensionManifest> = [
name: 'Block Editor Custom View Test',
element: () => import('./block-custom-view.js'),
forContentTypeAlias: 'headlineUmbracoDemoBlock',
forBlockEditor: 'block-list',
forBlockEditor: ['block-list', 'block-grid'],
},
];

View File

@@ -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 {

View File

@@ -2,18 +2,18 @@ import type { UmbLocalizationDictionary } from '@umbraco-cms/backoffice/localiza
export default {
actions: {
assigndomain: 'الثقافة وأسماء المضيفين',
assigndomain: 'الثقافة وأسماء النطاقات',
auditTrail: 'سجل التدقيق',
browse: 'تصفح العقدة',
browse: 'تصفح ',
changeDataType: 'تغيير نوع البيانات',
changeDocType: 'تغيير نوع المستند',
changeDocType: 'تغيير نوع الوثيقة',
chooseWhereToCopy: 'اختر مكان النسخ',
chooseWhereToImport: 'اختر مكان الاستيراد',
chooseWhereToMove: 'اختر مكان النقل',
copy: 'نسخ',
copyTo: 'نسخ إلى',
create: 'إنشاء',
createblueprint: 'إنشاء مخطط المستند',
createblueprint: 'إنشاء مخطط انشائي للمستند',
createGroup: 'إنشاء مجموعة',
createPackage: 'إنشاء حزمة',
delete: 'حذف',
@@ -23,17 +23,17 @@ export default {
emptyrecyclebin: 'إفراغ سلة المحذوفات',
enable: 'تمكين',
export: 'تصدير',
exportDocumentType: 'تصدير نوع المستند',
exportDocumentType: 'تصدير نوع الوثيقة',
folderCreate: 'إنشاء مجلد',
folderDelete: 'حذف مجلد',
folderRename: 'إعادة تسمية مجلد',
import: 'استيراد',
importdocumenttype: 'استيراد نوع المستند',
importdocumenttype: 'استيراد نوع وثيقة',
importPackage: 'استيراد حزمة',
infiniteEditorChooseWhereToCopy: 'اختر مكان نسخ العنصر (العناصر) المحددة',
infiniteEditorChooseWhereToMove: 'اختر مكان نقل العنصر (العناصر) المحددة',
liveEdit: 'تحرير في القماش',
logout: 'خروج',
liveEdit: 'تحرير مباشر',
logout: 'تسجيل خروج',
move: 'نقل إلى',
notify: 'الإشعارات',
protect: 'الوصول العام',
@@ -50,11 +50,11 @@ export default {
sendToTranslate: 'إرسال للترجمة',
setGroup: 'تعيين مجموعة',
setPermissions: 'تعيين الأذونات',
sort: 'فرز الأطفال',
toInTheTreeStructureBelow: 'في هيكل الشجرة أدناه',
sort: 'ترتيب الابناء',
toInTheTreeStructureBelow: 'في هيكل الشجري أدناه',
translate: 'ترجمة',
trash: 'سلة المهملات',
unlock: 'فتح',
unlock: 'فتح القفل',
unpublish: 'إلغاء النشر',
update: 'تحديث',
wasCopiedTo: 'تم نسخه إلى',
@@ -68,10 +68,10 @@ export default {
other: 'أخرى',
},
actionDescriptions: {
assignDomain: 'السماح بالوصول لتعيين الثقافة وأسماء المضيفين',
assignDomain: 'السماح بالوصول لتعيين الثقافة وأسماء النطاقات',
auditTrail: 'السماح بالوصول لعرض سجل تاريخ العقدة',
browse: 'السماح بالوصول لعرض العقدة',
changeDocType: 'السماح بالوصول لتغيير نوع المستند للعقدة',
changeDocType: 'السماح بالوصول لتغيير نوع وثيقة للعقدة',
copy: 'السماح بالوصول لنسخ العقدة',
create: 'السماح بالوصول لإنشاء العقد',
delete: 'السماح بالوصول لحذف العقد',
@@ -86,7 +86,7 @@ export default {
sort: 'السماح بالوصول لتغيير ترتيب العقد',
translate: 'السماح بالوصول لترجمة العقدة',
update: 'السماح بالوصول لحفظ العقدة',
createblueprint: 'السماح بالوصول لإنشاء مخطط المستند',
createblueprint: 'السماح بالوصول لإنشاء مخطط الوثيقة',
notify: 'السماح بالوصول لإعداد الإشعارات لعقد المحتوى',
},
apps: {
@@ -547,9 +547,9 @@ export default {
createNew: 'إنشاء عنصر قاموس جديد',
},
dictionaryItem: {
description: "\n تحرير الإصدارات اللغوية المختلفة لعنصر القاموس '<em>%0%</em>' أدناه\n ",
description: "تحرير الإصدارات اللغوية المختلفة لعنصر القاموس '%0%' أدناه",
displayName: 'اسم الثقافة',
changeKeyError: "\n المفتاح '%0%' موجود بالفعل.\n ",
changeKeyError: "المفتاح '%0%' موجود بالفعل.",
overviewTitle: 'نظرة عامة على القاموس',
},
examineManagement: {
@@ -578,7 +578,7 @@ export default {
processIsTakingLonger:
'تستغرق العملية وقتًا أطول من المتوقع، تحقق من سجل Umbraco لمعرفة ما إذا كانت هناك أي أخطاء خلال هذه العملية',
indexCannotRebuild: 'لا يمكن إعادة بناء هذا الفهرس لأنه ليس لديه تعيين',
iIndexPopulator: 'IIndexPopulator',
iIndexPopulator: 'معبئ الفهارس',
},
placeholders: {
username: 'أدخل اسم المستخدم الخاص بك',
@@ -633,7 +633,7 @@ export default {
hasReferencesDeleteConsequence:
'حذف <strong>%0%</strong> سيؤدي إلى حذف الخصائص وبياناتها من العناصر التالية',
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: "موقع الويب الخاص بك فارغ تمامًا في الوقت الحالي، وهذا مثالي إذا كنت ترغب في البدء من الصفر وإنشاء الأنواع المستندة والقوالب الخاصة بك. (<a href=\"https://umbraco.tv/documentation/videos/for-site-builders/foundation/document-types\">تعرف على الكيفية</a>) لا يزال بإمكانك اختيار تثبيت Runway لاحقًا. يرجى الانتقال إلى قسم المطور واختيار الحزم.",
runwayFromScratchText: "موقع الويب الخاص بك فارغ تمامًا في الوقت الحالي، وهذا مثالي إذا كنت ترغب في البدء من الصفر وإنشاء الأنواع الوثائق والقوالب الخاصة بك. (<a href=\"https://umbraco.tv/documentation/videos/for-site-builders/foundation/document-types\">تعرف على الكيفية</a>) لا يزال بإمكانك اختيار تثبيت Runway لاحقًا. يرجى الانتقال إلى قسم المطور واختيار الحزم.",
runwayHeader: "لقد قمت بإعداد منصة Umbraco نظيفة. ماذا تريد أن تفعل بعد ذلك؟",
runwayInstalled: "تم تثبيت Runway",
runwayInstalledText: "لديك الأساس في مكانه. اختر الوحدات التي ترغب في تثبيتها فوقه.<br />\n هذه هي قائمتنا الموصى بها من الوحدات، قم بتحديد الوحدات التي ترغب في تثبيتها، أو عرض <a href=\"#\" onclick=\"toggleModules(); return false;\" id=\"toggleModuleList\">القائمة الكاملة للوحدات</a>",
runwayOnlyProUsers: "يوصى بها فقط للمستخدمين ذوي الخبرة",
runwaySimpleSite: "أريد البدء بموقع ويب بسيط",
runwaySimpleSiteText: "<p>\"Runway\" هو موقع ويب بسيط يوفر بعض الأنواع المستندة والقوالب الأساسية. يمكن للمثبت إعداد Runway لك تلقائيًا، لكن يمكنك بسهولة تحريره أو توسيعه أو إزالته. ليس ضروريًا ويمكنك استخدام Umbraco بشكل مثالي بدونها. ومع ذلك، يوفر Runway أساسًا سهلًا يعتمد على أفضل الممارسات لبدء التشغيل بسرعة أكبر من أي وقت مضى. إذا اخترت تثبيت Runway، يمكنك اختيار الوحدات الأساسية الاختيارية المعروفة باسم وحدات Runway لتعزيز صفحات Runway الخاصة بك.</p>\n <small><em>متضمن مع Runway:</em> الصفحة الرئيسية، صفحة البدء، صفحة تثبيت الوحدات.<br />\n <em>الوحدات الاختيارية:</em> التنقل العلوي، خريطة الموقع، الاتصال، المعرض.</small>",
runwaySimpleSiteText: "<p>\"Runway\" هو موقع ويب بسيط يوفر بعض الأنواع الوثائق والقوالب الأساسية. يمكن للمثبت إعداد Runway لك تلقائيًا، لكن يمكنك بسهولة تحريره أو توسيعه أو إزالته. ليس ضروريًا ويمكنك استخدام Umbraco بشكل مثالي بدونها. ومع ذلك، يوفر Runway أساسًا سهلًا يعتمد على أفضل الممارسات لبدء التشغيل بسرعة أكبر من أي وقت مضى. إذا اخترت تثبيت Runway، يمكنك اختيار الوحدات الأساسية الاختيارية المعروفة باسم وحدات Runway لتعزيز صفحات Runway الخاصة بك.</p>\n <small><em>متضمن مع Runway:</em> الصفحة الرئيسية، صفحة البدء، صفحة تثبيت الوحدات.<br />\n <em>الوحدات الاختيارية:</em> التنقل العلوي، خريطة الموقع، الاتصال، المعرض.</small>",
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: 'يظهر هذا الاختلافات بين الإصدار الحالي (المسودة) والإصدار المحدد<br /><del>النص الأحمر</del> سيتم حذفه في الإصدار المحدد، <ins>النص الأخضر</ins> سيتم إضافته',
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% <br /> نوع البيانات: %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: '<strong>ملاحظة!</strong> تنظيف إصدارات المحتوى التاريخية معطل عالميًا. لن تكون هذه الإعدادات فعالة حتى يتم تمكينها.',
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: 'مستشهد به من قبل',

View File

@@ -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 '<em>%0%</em>' 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: {

View File

@@ -498,9 +498,9 @@ export default {
},
dictionaryItem: {
description:
"\n Editujte různé jazykové verze pro položku slovníku '<em>%0%</em>' níže.<br/>Můžete přidat další jazyky v nabídce 'jazyky' nalevo.",
"Editujte různé jazykové verze pro položku slovníku '%0%' níže.<br/>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: {

View File

@@ -591,9 +591,9 @@ export default {
},
dictionaryItem: {
description:
"\n Golygwch y fersiynau iaith gwahanol ar gyfer yr eitem geiriadur '<em>%0%</em>' islaw<br/>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<br/>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: {

View File

@@ -588,9 +588,9 @@ export default {
},
dictionaryItem: {
description:
"\n Rediger de forskellige sprogversioner for ordbogselementet '%0%' herunder.<br />Du tilføjer flere sprog under 'sprog' i menuen til venstre </key>\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: {

View File

@@ -590,9 +590,9 @@ export default {
},
dictionaryItem: {
description:
"\n Bearbeiten Sie nachfolgend die verschiedenen Sprachversionen für den Wörterbucheintrag '<em>%0%</em>'.\n <br/>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%'.<br/>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: {

View File

@@ -171,7 +171,14 @@ export default {
morePublishingOptions: 'More publishing options',
submitChanges: 'Submit',
},
auditTrailsMedia: {
delete: 'Media deleted',
move: 'Media moved',
copy: 'Media copied',
save: 'Media saved',
},
auditTrails: {
assigndomain: 'Domain assigned: %0%',
atViewingFor: 'Viewing for',
delete: 'Content deleted',
unpublish: 'Content unpublished',
@@ -189,6 +196,7 @@ export default {
custom: '%0%',
contentversionpreventcleanup: 'Cleanup disabled for version: %0%',
contentversionenablecleanup: 'Cleanup enabled for version: %0%',
smallAssignDomain: 'Assign Domain',
smallCopy: 'Copy',
smallPublish: 'Publish',
smallPublishVariant: 'Publish',
@@ -333,6 +341,7 @@ export default {
variantSendForApprovalNotAllowed: 'Send for approval is not allowed',
variantScheduleNotAllowed: 'Schedule is not allowed',
variantUnpublishNotAllowed: 'Unpublish is not allowed',
selectAllVariants: 'Select all variants',
},
blueprints: {
createBlueprintFrom: "Create a new Document Blueprint from '%0%'",
@@ -354,25 +363,26 @@ export default {
invalidFileName: 'Cannot upload this file, it does not have a valid file name',
maxFileSize: 'Max file size is',
mediaRoot: 'Media root',
moveToSameFolderFailed: 'Parent and destination folders cannot be the same',
createFolderFailed: 'Failed to create a folder under parent id %0%',
renameFolderFailed: 'Failed to rename the folder with id %0%',
dragAndDropYourFilesIntoTheArea: 'Drag and drop your file(s) into the area',
fileSecurityValidationFailure: 'One or more file security validations have failed',
moveToSameFolderFailed: 'Parent and destination folders cannot be the same',
uploadNotAllowed: 'Upload is not allowed in this location.',
},
member: {
createNewMember: 'Create a new member',
'2fa': 'Two-Factor Authentication',
allMembers: 'All Members',
createNewMember: 'Create a new member',
duplicateMemberLogin: 'A member with this login already exists',
kind: 'Kind',
memberGroupNoProperties: 'Member groups have no additional properties for editing.',
memberHasGroup: "The member is already in group '%0%'",
memberHasPassword: 'The member already has a password set',
memberLockoutNotEnabled: 'Lockout is not enabled for this member',
memberNotInGroup: "The member is not in group '%0%'",
'2fa': 'Two-Factor Authentication',
memberKindDefault: 'Member',
memberKindApi: 'API Member',
memberLockoutNotEnabled: 'Lockout is not enabled for this member',
memberNotInGroup: "The member is not in group '%0%'",
},
contentType: {
copyFailed: 'Failed to copy content type',
@@ -597,9 +607,9 @@ export default {
createNew: 'Create dictionary item',
},
dictionaryItem: {
description: "\n Edit the different language versions for the dictionary item '<em>%0%</em>' 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: {

View File

@@ -370,13 +370,16 @@ export default {
uploadNotAllowed: 'Upload is not allowed in this location.',
},
member: {
createNewMember: 'Create a new member',
allMembers: 'All Members',
memberGroupNoProperties: 'Member groups have no additional properties for editing.',
'2fa': 'Two-Factor Authentication',
allMembers: 'All Members',
createNewMember: 'Create a new member',
duplicateMemberLogin: 'A member with this login already exists',
kind: 'Kind',
memberGroupNoProperties: 'Member groups have no additional properties for editing.',
memberHasGroup: "The member is already in group '%0%'",
memberHasPassword: 'The member already has a password set',
memberKindDefault: 'Member',
memberKindApi: 'API Member',
memberLockoutNotEnabled: 'Lockout is not enabled for this member',
memberNotInGroup: "The member is not in group '%0%'",
},
@@ -606,9 +609,9 @@ export default {
createNew: 'Create dictionary item',
},
dictionaryItem: {
description: "\n Edit the different language versions for the dictionary item '<em>%0%</em>' below\n ",
description: "Edit the different language versions for the dictionary item '%0%' below",
displayName: 'Culture Name',
changeKeyError: "\n The key '%0%' already exists.\n ",
changeKeyError: "The key '%0%' already exists.",
overviewTitle: 'Dictionary overview',
},
examineManagement: {
@@ -1902,6 +1905,14 @@ export default {
administrators: 'Administrator',
categoryField: 'Category field',
createDate: 'User created',
createUserHeadline: (kind: string) => {
return kind === 'Api' ? 'Create API user' : 'Create user';
},
createUserDescription: (kind: string) => {
const defaultUserText = `Create a user to give them access to Umbraco. When a user is created a password will be generated that you can share with them.`;
const apiUserText = `Create an Api User to allow external services to authenticate with the Umbraco Management API.`;
return kind === 'Api' ? apiUserText : defaultUserText;
},
changePassword: 'Change your password',
changePhoto: 'Change photo',
configureMfa: 'Configure MFA',
@@ -1911,6 +1922,7 @@ export default {
? 'The email address is used for notifications, password recovery, and as the username for logging in'
: 'The email address is used for notifications and password recovery';
},
kind: 'Kind',
newPassword: 'New password',
newPasswordFormatLengthTip: 'Minimum %0% character(s) to go!',
newPasswordFormatNonAlphaTip: 'There should be at least %0% special character(s) in there.',
@@ -2044,6 +2056,8 @@ export default {
sortCreateDateDescending: 'Newest',
sortCreateDateAscending: 'Oldest',
sortLastLoginDateDescending: 'Last login',
userKindDefault: 'User',
userKindApi: 'API User',
noUserGroupsAdded: 'No user groups have been added',
'2faDisableText':
'If you wish to disable this two-factor provider, then you must enter the code shown on your authentication device:',

View File

@@ -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',

View File

@@ -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 '<em>%0%</em>' 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: {

View File

@@ -204,7 +204,7 @@ export default {
},
dictionaryItem: {
description:
'\n ערוך את גרסאות השפות השונות לפריט המילון \'<em>%0%</em>\' למטה<br/>ניתן להוסיף שפות נוספות תחת "שפות" בתפריט בצד שמאל\n ',
'ערוך את גרסאות השפות השונות לפריט המילון \'%0%\' למטה ניתן להוסיף שפות נוספות תחת "שפות" בתפריט בצד שמאל',
displayName: 'שם התצוגה לשפה',
},
editdatatype: {

View File

@@ -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 '<em>%0%</em>' 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: {

View File

@@ -569,7 +569,7 @@ export default {
noItems: 'Non ci sono oggetti nel Dizionario.',
},
dictionaryItem: {
description: "Modifica le lingue per l'elemento '<em>%0%</em>' 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',

View File

@@ -275,7 +275,7 @@ export default {
},
dictionaryItem: {
description:
"\n ディクショナリのアイテム '<em>%0%</em>' の別の言語版を編集するには、左側のメニューの'言語'でその言語を追加します\n ",
"ディクショナリのアイテム '%0%' の別の言語版を編集するには、左側のメニューの'言語'でその言語を追加します",
displayName: 'カルチャ名',
},
placeholders: {

View File

@@ -204,7 +204,7 @@ export default {
},
dictionaryItem: {
description:
"\n '<em>%0%</em>'사전 항목 아래에 다른 언어버전들을 편집하세요<br/>왼쪽 '언어'메뉴를 사용하여 추가 언어들을 설정할 수 있습니다.\n ",
"'%0%'사전 항목 아래에 다른 언어버전들을 편집하세요<br/>왼쪽 '언어'메뉴를 사용하여 추가 언어들을 설정할 수 있습니다.",
displayName: '국가명',
},
editdatatype: {

View File

@@ -250,8 +250,10 @@ export default {
},
dictionaryItem: {
description:
"Rediger de forskjellige språkversjonene for ordbokelementet '<em>%0%</em>' under.<br/>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',

View File

@@ -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: {

View File

@@ -364,9 +364,9 @@ export default {
},
dictionaryItem: {
description:
'\n Edytuj różne wersje językowe dla elementu słownika \'<em>%0%</em>\' poniżej.<br/>\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',

View File

@@ -206,7 +206,7 @@ export default {
},
dictionaryItem: {
description:
"Editar as diferente versões de linguagem para o item de dicionário '<em>%0%</em>' abaixo <br /> 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: {

View File

@@ -423,9 +423,9 @@ export default {
},
dictionaryItem: {
description:
"\n\t\tНиже Вы можете указать различные переводы данной статьи словаря '<em>%0%</em>'<br/>Добавить другие языки можно, воспользовавшись пунктом 'Языки' в меню слева\n\t\t",
"Ниже Вы можете указать различные переводы данной статьи словаря '%0%'. Добавить другие языки можно, воспользовавшись пунктом 'Языки' в меню слева.",
displayName: 'Название языка (культуры)',
changeKeyError: "\n Ключ '%0%' уже существует в словаре.\n ",
changeKeyError: "Ключ '%0%' уже существует в словаре.",
overviewTitle: 'Обзор словаря',
},
editcontenttype: {

View File

@@ -510,9 +510,9 @@ export default {
noItems: 'Sözlük öğesi yok.',
},
dictionaryItem: {
description: "\n Aşağıdaki sözlük öğesi '<em>%0%</em>' 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: {

View File

@@ -422,9 +422,9 @@ export default {
},
dictionaryItem: {
description:
"\n\t\tНиже Ви можете вказати різні переклади даної статті словника '<em>%0%</em>'<br/>Додати інші мови можна, скориставшись пунктом 'Мови' в меню зліва\n\t\t",
"Ниже Ви можете вказати різні переклади даної статті словника '%0%'. Додати інші мови можна, скориставшись пунктом 'Мови' в меню зліва.",
displayName: 'Назва мови (культури)',
changeKeyError: "\n Ключ '%0%' вже існує у словнику.\n ",
changeKeyError: "Ключ '%0%' вже існує у словнику.",
overviewTitle: 'Огляд словника',
},
editcontenttype: {

View File

@@ -280,9 +280,9 @@ export default {
selectEditor: '选择编辑器',
},
dictionaryItem: {
description: '\n 为字典项编辑不同语言的版本‘<em>%0%</em><br/>您可以在左侧的“语言”中添加一种语言\n ',
description: '为字典项编辑不同语言的版本‘%0%, 您可以在左侧的“语言”中添加一种语言',
displayName: '语言名称',
changeKeyError: "\n 关键字 '%0%' 已经存在。\n ",
changeKeyError: "关键字 '%0%' 已经存在。",
},
placeholders: {
username: '输入您的用户名',

View File

@@ -278,8 +278,7 @@ export default {
selectEditor: '選擇編輯器',
},
dictionaryItem: {
description:
"\n 為此字典項目 '<em>%0%</em>' 編輯不同語言版本,<br />您可以在左方選單「語言」中增添新的語言\n ",
description: "為此字典項目 '%0%' 編輯不同語言版本,您可以在左方選單「語言」中增添新的語言",
displayName: '語言名稱',
},
placeholders: {

View File

@@ -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!)

View File

@@ -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<Observable<ReturnType | undefined> | undefined>} - Promise which resolves to an Observable
* @description Get an Observable for the value of this property.
*/

View File

@@ -7,6 +7,7 @@ export class UmbCollectionToolbarElement extends UmbLitElement {
override render() {
return html`
<umb-collection-action-bundle></umb-collection-action-bundle>
<div id="slot"><slot></slot></div>
<umb-collection-view-bundle></umb-collection-view-bundle>
`;
}
@@ -20,6 +21,9 @@ export class UmbCollectionToolbarElement extends UmbLitElement {
justify-content: space-between;
width: 100%;
}
#slot {
flex: 1;
}
`,
];
}

View File

@@ -109,7 +109,6 @@ export class UmbPropertyTypeBasedPropertyElement extends UmbLitElement {
if (this._isUnsupported) {
return html`<umb-unsupported-property
.alias=${this._property.alias}
.ownerEntityType=${this._ownerEntityType}
.schema=${this._propertyEditorSchemaAlias!}></umb-unsupported-property>`;
}
return html`

View File

@@ -260,5 +260,5 @@ export const manifests: Array<ManifestLocalization> = [
culture: 'zh-tw',
},
js: () => import('../../../assets/lang/zh-tw.js'),
}
},
];

View File

@@ -156,7 +156,7 @@ export class UmbPropertyContext<ValueType = any> extends UmbContextBase<UmbPrope
/**
* Set the alias of this property.
* @param {(string | undefined)} alias
* @param {string | undefined} alias - The alias of the property
* @memberof UmbPropertyContext
*/
public setAlias(alias: string | undefined): void {
@@ -174,7 +174,7 @@ export class UmbPropertyContext<ValueType = any> extends UmbContextBase<UmbPrope
/**
* Set the label of this property.
* @param {(string | undefined)} label
* @param {(string | undefined)} label - The label of the property
* @memberof UmbPropertyContext
*/
public setLabel(label: string | undefined): void {
@@ -183,7 +183,7 @@ export class UmbPropertyContext<ValueType = any> extends UmbContextBase<UmbPrope
/**
* Get the label of this property.
* @returns {*} {(string | undefined)}
* @returns {(string | undefined)} - the label
* @memberof UmbPropertyContext
*/
public getLabel(): string | undefined {
@@ -210,7 +210,7 @@ export class UmbPropertyContext<ValueType = any> extends UmbContextBase<UmbPrope
/**
* Set the appearance of this property.
* @param {(UmbPropertyTypeAppearanceModel | undefined)} appearance
* @param {UmbPropertyTypeAppearanceModel | undefined} appearance - the appearance properties of this property
* @memberof UmbPropertyContext
*/
public setAppearance(appearance: UmbPropertyTypeAppearanceModel | undefined): void {
@@ -219,7 +219,7 @@ export class UmbPropertyContext<ValueType = any> extends UmbContextBase<UmbPrope
/**
* Get the appearance of this property.
* @returns {*} {(UmbPropertyTypeAppearanceModel | undefined)}
* @returns {UmbPropertyTypeAppearanceModel | undefined}- the appearance properties of this property
* @memberof UmbPropertyContext
*/
public getAppearance(): UmbPropertyTypeAppearanceModel | undefined {
@@ -228,7 +228,7 @@ export class UmbPropertyContext<ValueType = any> extends UmbContextBase<UmbPrope
/**
* Set the value of this property.
* @param value {ValueType} the whole value to be set
* @param {unknown} value - the whole value to be set
*/
public setValue(value: ValueType | undefined): void {
const alias = this.#alias.getValue();
@@ -239,7 +239,7 @@ export class UmbPropertyContext<ValueType = any> extends UmbContextBase<UmbPrope
/**
* Gets the current value of this property.
* Notice this is not reactive, you should us the `value` observable for that.
* @returns {ValueType}
* @returns {unknown} - the current value of this property
*/
public getValue(): ValueType | undefined {
return this.#value.getValue();
@@ -247,7 +247,7 @@ export class UmbPropertyContext<ValueType = any> extends UmbContextBase<UmbPrope
/**
* Set the config of this property.
* @param {(Array<UmbPropertyEditorConfigProperty> | undefined)} config
* @param {Array<UmbPropertyEditorConfigProperty> | undefined} config - Array of configurations for this property
* @memberof UmbPropertyContext
*/
public setConfig(config: Array<UmbPropertyEditorConfigProperty> | undefined): void {
@@ -256,7 +256,7 @@ export class UmbPropertyContext<ValueType = any> extends UmbContextBase<UmbPrope
/**
* Get the config of this property.
* @returns {*} {(Array<UmbPropertyEditorConfigProperty> | undefined)}
* @returns {Array<UmbPropertyEditorConfigProperty> | undefined} - Array of configurations for this property
* @memberof UmbPropertyContext
*/
public getConfig(): Array<UmbPropertyEditorConfigProperty> | undefined {
@@ -265,7 +265,7 @@ export class UmbPropertyContext<ValueType = any> extends UmbContextBase<UmbPrope
/**
* Set the variant ID of this property.
* @param {(UmbVariantId | undefined)} variantId
* @param {UmbVariantId | undefined} variantId - The property Variant ID, not necessary the same as the Property Dataset Context VariantId.
* @memberof UmbPropertyContext
*/
public setVariantId(variantId: UmbVariantId | undefined): void {
@@ -274,7 +274,7 @@ export class UmbPropertyContext<ValueType = any> extends UmbContextBase<UmbPrope
/**
* Get the variant ID of this property.
* @returns {*} {(UmbVariantId | undefined)}
* @returns {UmbVariantId | undefined} - The property Variant ID, not necessary the same as the Property Dataset Context VariantId.
* @memberof UmbPropertyContext
*/
public getVariantId(): UmbVariantId | undefined {
@@ -283,7 +283,7 @@ export class UmbPropertyContext<ValueType = any> extends UmbContextBase<UmbPrope
/**
* Set the validation of this property.
* @param {(UmbPropertyTypeValidationModel | undefined)} validation
* @param {UmbPropertyTypeValidationModel | undefined} validation - Object holding the Validation Properties.
* @memberof UmbPropertyContext
*/
public setValidation(validation: UmbPropertyTypeValidationModel | undefined): void {
@@ -292,7 +292,7 @@ export class UmbPropertyContext<ValueType = any> extends UmbContextBase<UmbPrope
/**
* Get the validation of this property.
* @returns {*} {(UmbPropertyTypeValidationModel | undefined)}
* @returns {UmbPropertyTypeValidationModel | undefined} - Object holding the Validation Properties.
* @memberof UmbPropertyContext
*/
public getValidation(): UmbPropertyTypeValidationModel | undefined {
@@ -301,7 +301,7 @@ export class UmbPropertyContext<ValueType = any> extends UmbContextBase<UmbPrope
/**
* Get the read only state of this property
* @returns {*} {boolean}
* @returns {boolean} - If property is in read-only mode.
* @memberof UmbPropertyContext
*/
public getIsReadOnly(): boolean {

View File

@@ -32,7 +32,6 @@ export class UmbPropertyElement extends UmbLitElement {
/**
* Label. Name of the property
* @type {string}
* @attr
* @default
*/
@property({ type: String })
@@ -46,7 +45,6 @@ export class UmbPropertyElement extends UmbLitElement {
/**
* Description: render a description underneath the label.
* @type {string}
* @attr
* @default
*/
@property({ type: String })
@@ -72,7 +70,6 @@ export class UmbPropertyElement extends UmbLitElement {
* Alias
* @public
* @type {string}
* @attr
* @default
*/
@property({ type: String })
@@ -87,7 +84,6 @@ export class UmbPropertyElement extends UmbLitElement {
* Property Editor UI Alias. Render the Property Editor UI registered for this alias.
* @public
* @type {string}
* @attr
* @default
*/
@property({ type: String, attribute: 'property-editor-ui-alias' })
@@ -104,7 +100,6 @@ export class UmbPropertyElement extends UmbLitElement {
* Config. Configuration to pass to the Property Editor UI. This is also the configuration data stored on the Data Type.
* @public
* @type {string}
* @attr
* @default
*/
@property({ type: Array, attribute: false })
@@ -130,7 +125,6 @@ export class UmbPropertyElement extends UmbLitElement {
* DataPath, declare the path to the value of the data that this property represents.
* @public
* @type {string}
* @attr
* @default
*/
@property({ type: String, attribute: 'data-path' })
@@ -235,10 +229,14 @@ export class UmbPropertyElement extends UmbLitElement {
null,
);
this.observe(this.#propertyContext.isReadOnly, (value) => {
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 => {

View File

@@ -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';

View File

@@ -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);
}

View File

@@ -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`<umb-collection-toolbar slot="header">${this.#renderSearch()}</umb-collection-toolbar>`;
}
#renderSearch() {
return html`<uui-input
id="input-search"
@input=${this.#updateSearch}
placeholder=${this.localize.term('placeholders_search')}></uui-input>`;
}
static override styles = [
css`
#input-search {
width: 100%;
}
`,
];
}
export default UmbDictionaryCollectionElement;
declare global {
interface HTMLElementTagNameMap {
'umb-dictionary-collection': UmbDictionaryCollectionElement;
}
}

View File

@@ -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,

View File

@@ -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<UmbExtensionManifest> = [
...entityActionManifests,
...menuItemManifests,
...repositoryManifests,
...searchManifests,
...treeManifests,
...workspaceManifests,
];

View File

@@ -0,0 +1 @@
export const UMB_DICTIONARY_SEARCH_PROVIDER_ALIAS = 'Umb.SearchProvider.Dictionary';

View File

@@ -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<UmbDictionarySearchItemModel>, UmbApi
{
#dataSource: UmbDictionarySearchServerDataSource;
constructor(host: UmbControllerHost) {
super(host);
this.#dataSource = new UmbDictionarySearchServerDataSource(this);
}
search(args: UmbSearchRequestArgs) {
return this.#dataSource.search(args);
}
}

View File

@@ -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<UmbDictionarySearchItemModel> {
#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<UmbDictionarySearchItemModel> = 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 };
}
}

View File

@@ -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<UmbDictionarySearchItemModel>
{
#repository = new UmbDictionarySearchRepository(this);
async search(args: UmbSearchRequestArgs) {
return this.#repository.search(args);
}
override destroy(): void {
this.#repository.destroy();
}
}
export { UmbDictionarySearchProvider as api };

View File

@@ -0,0 +1 @@
export * from './constants.js';

View File

@@ -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<UmbExtensionManifest> = [
{
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],
},
];

View File

@@ -2,10 +2,11 @@ import { UMB_DICTIONARY_WORKSPACE_CONTEXT } from '../dictionary-workspace.contex
import type { UmbDictionaryDetailModel } from '../../types.js';
import type { UUITextareaElement } from '@umbraco-cms/backoffice/external/uui';
import { UUITextareaEvent } from '@umbraco-cms/backoffice/external/uui';
import { css, html, customElement, state, repeat, ifDefined, unsafeHTML } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbLanguageCollectionRepository, type UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language';
import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user';
import { sanitizeHTML } from '@umbraco-cms/backoffice/utils';
@customElement('umb-workspace-view-dictionary-editor')
export class UmbWorkspaceViewDictionaryEditorElement extends UmbLitElement {
@@ -21,8 +22,12 @@ export class UmbWorkspaceViewDictionaryEditorElement extends UmbLitElement {
@state()
private _currentUserHasAccessToAllLanguages?: boolean = false;
#languageCollectionRepository = new UmbLanguageCollectionRepository(this);
#workspaceContext!: typeof UMB_DICTIONARY_WORKSPACE_CONTEXT.TYPE;
get #dictionaryName() {
return typeof this._dictionary?.name !== 'undefined' ? sanitizeHTML(this._dictionary.name) : '...';
}
readonly #languageCollectionRepository = new UmbLanguageCollectionRepository(this);
#workspaceContext?: typeof UMB_DICTIONARY_WORKSPACE_CONTEXT.TYPE;
#currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE;
constructor() {
@@ -59,7 +64,7 @@ export class UmbWorkspaceViewDictionaryEditorElement extends UmbLitElement {
}
#observeDictionary() {
this.observe(this.#workspaceContext.dictionary, (dictionary) => {
this.observe(this.#workspaceContext?.dictionary, (dictionary) => {
this._dictionary = dictionary;
});
}
@@ -77,14 +82,14 @@ export class UmbWorkspaceViewDictionaryEditorElement extends UmbLitElement {
const translation = (target.value as string).toString();
const isoCode = target.getAttribute('name')!;
this.#workspaceContext.setPropertyValue(isoCode, translation);
this.#workspaceContext?.setPropertyValue(isoCode, translation);
}
}
override render() {
return html`
<uui-box>
${unsafeHTML(this.localize.term('dictionaryItem_description', this._dictionary?.name || '&#8203;'))}
${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)}></uui-textarea>
</umb-property-layout>`;
}

View File

@@ -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`<div id="wrapper">
Drag and drop your file here
<uui-button look="primary" label="or click here to choose a file" @click=${this.#onBrowse}></uui-button>
<umb-dropzone id="dropzone" createAsTemporary @change=${this.#onFileDropped}> </umb-dropzone>
<umb-dropzone
id="dropzone"
accept=".udt"
@complete=${this.#onUploadComplete}
createAsTemporary></umb-dropzone>
</div>`,
)}
`;

View File

@@ -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`<uui-scroll-container id="preview">${unsafeHTML(sanitizedHtml)}</uui-scroll-container>`;
}

View File

@@ -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<Element>) {
#mediaTypePreviewItemBuilder(elements: Array<Element>) {
const mediaTypes: Array<UmbMediaTypePreview> = [];
elements.forEach((MediaType) => {
const info = MediaType.getElementsByTagName('Info')[0];
@@ -129,7 +129,11 @@ export class UmbMediaTypeImportModalLayout extends UmbModalBaseElement<
html`<div id="wrapper">
Drag and drop your file here
<uui-button look="primary" label="or click here to choose a file" @click=${this.#onBrowse}></uui-button>
<umb-dropzone id="dropzone" createAsTemporary @change=${this.#onFileDropped}> </umb-dropzone>
<umb-dropzone
id="dropzone"
accept=".udt"
@complete=${this.#onUploadCompleted}
createAsTemporary></umb-dropzone>
</div>`,
)}
`;

View File

@@ -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;

View File

@@ -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,

View File

@@ -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();

View File

@@ -4,8 +4,6 @@ import type { UmbMediaCollectionContext } from './media-collection.context.js';
import { UMB_MEDIA_COLLECTION_CONTEXT } from './media-collection.context-token.js';
import { customElement, html, state, when } from '@umbraco-cms/backoffice/external/lit';
import { UmbCollectionDefaultElement } from '@umbraco-cms/backoffice/collection';
import type { UmbProgressEvent } from '@umbraco-cms/backoffice/event';
import './media-collection-toolbar.element.js';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action';
@@ -32,7 +30,7 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement {
});
}
async #onChange() {
async #onComplete() {
this._progress = -1;
this.#mediaCollection?.requestCollection();
@@ -44,8 +42,11 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement {
eventContext.dispatchEvent(event);
}
#onProgress(event: UmbProgressEvent) {
this._progress = event.progress;
#onProgress(event: ProgressEvent) {
this._progress = (event.loaded / event.total) * 100;
if (this._progress >= 100) {
this._progress = -1;
}
}
protected override renderToolbar() {
@@ -53,8 +54,9 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement {
<umb-media-collection-toolbar slot="header"></umb-media-collection-toolbar>
${when(this._progress >= 0, () => html`<uui-loader-bar progress=${this._progress}></uui-loader-bar>`)}
<umb-dropzone
multiple
.parentUnique=${this._unique}
@change=${this.#onChange}
@complete=${this.#onComplete}
@progress=${this.#onProgress}></umb-dropzone>
`;
}

View File

@@ -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<UmbUploadableFileModel>;
const completed = e.detail as Array<UmbUploadableItem>;
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`<umb-dropzone @change=${this.#onUploadCompleted}></umb-dropzone>`;
return html`<umb-dropzone ?multiple=${this.max > 1} @complete=${this.#onUploadCompleted}></umb-dropzone>`;
}
#renderItems() {

View File

@@ -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<any> | Map<PropertyKey, unknown>): 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.

View File

@@ -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<UmbAllowedMediaTypeModel>;
}
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<UmbUploadableFileModel | UmbTemporaryFileModel>(
[],
(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<UmbAllowedMediaTypesOfExtension>([], (x) => x.extension);
// The media types that the parent will allow to be created under it.
readonly #allowedChildrenOf = new UmbArrayState<UmbAllowedChildrenOfMediaType>([], (x) => x.mediaTypeUnique);
readonly #progress = new UmbObjectState<UmbFileDropzoneProgress>({ total: 0, completed: 0 });
public readonly progress = this.#progress.asObservable();
readonly #progressItems = new UmbArrayState<UmbUploadableItem>([], (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<Array<UmbUploadableFileModel>>
* @param { File[] } files - The files to upload.
* @returns {Promise<Array<UmbUploadableFileModel>>} - Files as temporary files.
*/
public async createFilesAsTemporary(files: Array<File>): Promise<Array<UmbTemporaryFileModel>> {
this.#completed.setValue([]);
const temporaryFiles: Array<UmbTemporaryFileModel> = [];
public async createTemporaryFiles(files: Array<File>) {
const uploadableItems = (await this.#setupProgress({ files, folders: [] }, null)) as Array<UmbUploadableFile>;
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<UmbTemporaryFileModel> = [];
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<void>
*/
public async createFilesAsMedia(files: Array<File>, 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<string>((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<UmbUploadableFileModel> = [];
const notAllowedFiles: Array<File> = [];
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<string>,
parentUnique: string | null,
): Promise<Array<UmbUploadableExtensionModel>> {
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<UmbUploadableExtensionModel> = [];
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<UmbAllowedMediaTypeModel>) {
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<UmbUploadableFileModel>, 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<UmbMediaDetailModel> = {
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<UmbUploadableItem>) {
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<Array<UmbAllowedMediaTypeModel>> {
// 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<UmbMediaDetailModel> {
// 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<UmbMediaDetailModel> = {
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<UmbUploadableItem> => {
const items: Array<UmbUploadableItem> = [];
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();
}
}

View File

@@ -1,39 +1,61 @@
import { UmbDropzoneManager, type UmbUploadableFileModel } from './dropzone-manager.class.js';
import { UmbProgressEvent } from '@umbraco-cms/backoffice/event';
import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui';
import { UmbDropzoneManager } from './dropzone-manager.class.js';
import { UmbFileDropzoneItemStatus, type UmbUploadableItem } from './types.js';
import { css, customElement, html, ifDefined, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbTemporaryFileModel } from '@umbraco-cms/backoffice/temporary-file';
import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui';
@customElement('umb-dropzone')
export class UmbDropzoneElement extends UmbLitElement {
@property({ attribute: false })
parentUnique: string | null = null;
@property({ type: Boolean })
multiple: boolean = true;
@property({ type: Boolean })
createAsTemporary: boolean = false;
@property({ type: Array, attribute: false })
accept: Array<string> = [];
@property({ type: String })
accept?: string;
//TODO: logic to disable the dropzone?
@property({ type: Boolean, reflect: true })
multiple: boolean = false;
#files: Array<UmbUploadableFileModel | UmbTemporaryFileModel> = [];
@property({ type: Boolean, reflect: true })
disabled = false;
@property({ type: Boolean, attribute: 'disable-folder-upload', reflect: true })
public get disableFolderUpload() {
return this._disableFolderUpload;
}
public set disableFolderUpload(isAllowed: boolean) {
this.dropzoneManager.setIsFoldersAllowed(!isAllowed);
}
private readonly _disableFolderUpload = false;
@state()
private _progressItems: Array<UmbUploadableItem> = [];
public dropzoneManager: UmbDropzoneManager;
/**
* @deprecated Please use `getItems()` instead; this method will be removed in Umbraco 17.
* @returns {Array<UmbUploadableItem>} 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<File> = 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<UmbUploadableItem>) => {
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`<uui-file-dropzone
id="dropzone"
.accept=${this.accept?.join(',')}
accept=${ifDefined(this.accept)}
?multiple=${this.multiple}
@change=${this.#onDropFiles}
label="${this.localize.term('media_dragAndDropYourFilesIntoTheArea')}"></uui-file-dropzone>`;
label=${this.localize.term('media_dragAndDropYourFilesIntoTheArea')}></uui-file-dropzone>`;
}
static override styles = [
css`
:host([dragging]) #dropzone {
:host(:not([disabled])[dragging]) #dropzone {
opacity: 1;
pointer-events: all;
}

View File

@@ -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<File>;
folders: Array<UUIFileFolder>;
}
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<UmbAllowedMediaTypeModel>;
}
export interface UmbAllowedChildrenOfMediaType {
mediaTypeUnique: string | null;
allowedChildren: Array<UmbAllowedMediaTypeModel>;
}
export interface UmbFileDropzoneProgress {
total: number;
completed: number;
}
export enum UmbFileDropzoneItemStatus {
WAITING = 'waiting',
COMPLETE = 'complete',
NOT_ALLOWED = 'not allowed',
CANCELLED = 'cancelled',
ERROR = 'error',
}

View File

@@ -168,7 +168,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<
#renderBody() {
return html`${this.#renderToolbar()}
<umb-dropzone id="dropzone" @change=${() => this.#loadMediaFolder()} .parentUnique=${this._currentMediaEntity.unique}></umb-dropzone>
<umb-dropzone id="dropzone" multiple @complete=${() => this.#loadMediaFolder()} .parentUnique=${this._currentMediaEntity.unique}></umb-dropzone>
${
!this._mediaFilteredList.length
? html`<div class="container"><p>${this.localize.term('content_listViewNoItems')}</p></div>`