From 1d6ffcb80e4c3fa4220f8a40798a98cea8b95842 Mon Sep 17 00:00:00 2001 From: Said Akhrarov <36972061+akhrarovsaid@users.noreply.github.com> Date: Wed, 9 Jul 2025 09:59:22 -0400 Subject: [PATCH] feat(ui): adds support for copy pasting complex fields (#11513) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What? This PR introduces support for copy + pasting complex fields such as Arrays and Blocks. These changes introduce a new `ClipboardAction` component that houses logic for copy + pasting to and from the clipboard to supported fields. I've scoped this PR to include only Blocks & Arrays, however the structure of the components introduced lend themselves to be easily extended to other field types. I've limited the scope because there may be design & functional blockers that make it unclear how to add actions to particular fields. Supported fields: - Arrays ([Demo](https://github.com/user-attachments/assets/523916f6-77d0-43e2-9a11-a6a9d8c1b71c)) - Array Rows ([Demo](https://github.com/user-attachments/assets/0cd01a1f-3e5e-4fea-ac83-8c0bba8d1aac)) - Blocks ([Demo](https://github.com/user-attachments/assets/4c55ac2b-55f4-4793-9b53-309b2e090dd9)) - Block Rows ([Demo](https://github.com/user-attachments/assets/1b4d2bea-981a-485b-a6c4-c59a77a50567)) Fields that may be supported in the future with minimal effort by adopting the changes introduced here: - Tabs - Groups - Collapsible - Relationships This PR also encompasses e2e tests that check both field and row-level copy/pasting. ### Why? To make it simpler and faster to copy complex fields over between documents and rows within those docs. ### How? Introduces a new `ClipboardAction` component with helper utilities to aid in copy/pasting and validating field data. Addresses #2977 & #10703 Notes: - There seems to be an issue with Blocks & Arrays that contain RichText fields where the RichText field dissappears from the dom upon replacing form state. These fields are resurfaced after either saving the data or dragging/dropping the row containing them. - Copying a Row and then pasting it at the field-level will overwrite the field to include only that one row. This is intended however can be changed if requested. - Clipboard permissions are required to use this feature. [See Clipboard API caniuse](https://caniuse.com/async-clipboard). #### TODO - [x] ~~I forgot BlockReferences~~ - [x] ~~Fix tests failing due to new buttons causing locator conflicts~~ - [x] ~~Ensure deeply nested structures work~~ - [x] ~~Add missing translations~~ - [x] ~~Implement local storage instead of clipboard api~~ - [x] ~~Improve tests~~ --------- Co-authored-by: Germán Jabloñski <43938777+GermanJablo@users.noreply.github.com> --- packages/translations/src/clientKeys.ts | 7 + packages/translations/src/languages/ar.ts | 7 + packages/translations/src/languages/az.ts | 8 + packages/translations/src/languages/bg.ts | 8 + packages/translations/src/languages/bnBd.ts | 8 + packages/translations/src/languages/bnIn.ts | 8 + packages/translations/src/languages/ca.ts | 8 + packages/translations/src/languages/cs.ts | 8 + packages/translations/src/languages/da.ts | 8 + packages/translations/src/languages/de.ts | 12 +- packages/translations/src/languages/en.ts | 8 + packages/translations/src/languages/es.ts | 8 + packages/translations/src/languages/et.ts | 8 + packages/translations/src/languages/fa.ts | 8 + packages/translations/src/languages/fr.ts | 8 + packages/translations/src/languages/he.ts | 7 + packages/translations/src/languages/hr.ts | 8 + packages/translations/src/languages/hu.ts | 8 + packages/translations/src/languages/hy.ts | 8 + packages/translations/src/languages/it.ts | 8 + packages/translations/src/languages/ja.ts | 8 + packages/translations/src/languages/ko.ts | 8 + packages/translations/src/languages/lt.ts | 8 + packages/translations/src/languages/lv.ts | 8 + packages/translations/src/languages/my.ts | 8 + packages/translations/src/languages/nb.ts | 8 + packages/translations/src/languages/nl.ts | 8 + packages/translations/src/languages/pl.ts | 7 + packages/translations/src/languages/pt.ts | 8 + packages/translations/src/languages/ro.ts | 8 + packages/translations/src/languages/rs.ts | 8 + .../translations/src/languages/rsLatin.ts | 8 + packages/translations/src/languages/ru.ts | 8 + packages/translations/src/languages/sk.ts | 8 + packages/translations/src/languages/sl.ts | 8 + packages/translations/src/languages/sv.ts | 8 + packages/translations/src/languages/th.ts | 8 + packages/translations/src/languages/tr.ts | 8 + packages/translations/src/languages/uk.ts | 8 + packages/translations/src/languages/vi.ts | 8 + packages/translations/src/languages/zh.ts | 7 + packages/translations/src/languages/zhTw.ts | 7 + .../ui/src/elements/ArrayAction/index.tsx | 25 +- .../ClipboardAction/ClipboardActionLabel.tsx | 32 +++ .../ClipboardAction/clipboardUtilities.ts | 67 +++++ .../ui/src/elements/ClipboardAction/index.tsx | 117 +++++++++ .../ClipboardAction/isClipboardDataValid.ts | 109 ++++++++ .../mergeFormStateFromClipboard.ts | 131 ++++++++++ .../ui/src/elements/ClipboardAction/types.ts | 58 +++++ packages/ui/src/fields/Array/ArrayRow.tsx | 6 + packages/ui/src/fields/Array/index.tsx | 161 ++++++++++-- packages/ui/src/fields/Blocks/BlockRow.tsx | 6 + packages/ui/src/fields/Blocks/RowActions.tsx | 6 + packages/ui/src/fields/Blocks/index.tsx | 173 +++++++++++-- test/access-control/e2e.spec.ts | 2 +- test/fields/collections/Array/e2e.spec.ts | 233 ++++++++++++++++++ test/fields/collections/Blocks/e2e.spec.ts | 220 +++++++++++++++++ test/fields/collections/Blocks/index.ts | 25 ++ test/helpers/e2e/copyPasteField.ts | 44 ++++ test/joins/e2e.spec.ts | 2 +- test/localization/e2e.spec.ts | 4 +- 61 files changed, 1699 insertions(+), 56 deletions(-) create mode 100644 packages/ui/src/elements/ClipboardAction/ClipboardActionLabel.tsx create mode 100644 packages/ui/src/elements/ClipboardAction/clipboardUtilities.ts create mode 100644 packages/ui/src/elements/ClipboardAction/index.tsx create mode 100644 packages/ui/src/elements/ClipboardAction/isClipboardDataValid.ts create mode 100644 packages/ui/src/elements/ClipboardAction/mergeFormStateFromClipboard.ts create mode 100644 packages/ui/src/elements/ClipboardAction/types.ts create mode 100644 test/helpers/e2e/copyPasteField.ts diff --git a/packages/translations/src/clientKeys.ts b/packages/translations/src/clientKeys.ts index ddfa28401..c586ea55b 100644 --- a/packages/translations/src/clientKeys.ts +++ b/packages/translations/src/clientKeys.ts @@ -68,12 +68,15 @@ export const clientTranslationKeys = createClientTranslationKeys([ 'error:emailOrPasswordIncorrect', 'error:usernameOrPasswordIncorrect', 'error:loadingDocument', + 'error:insufficientClipboardPermissions', + 'error:invalidClipboardData', 'error:invalidRequestArgs', 'error:invalidFileType', 'error:logoutFailed', 'error:noMatchedField', 'error:notAllowedToAccessPage', 'error:previewing', + 'error:unableToCopy', 'error:unableToDeleteCount', 'error:unableToReindexCollection', 'error:unableToUpdateCount', @@ -182,6 +185,8 @@ export const clientTranslationKeys = createClientTranslationKeys([ 'general:copied', 'general:clearAll', 'general:copy', + 'general:copyField', + 'general:copyRow', 'general:copyWarning', 'general:copying', 'general:create', @@ -267,6 +272,8 @@ export const clientTranslationKeys = createClientTranslationKeys([ 'general:overwriteExistingData', 'general:pageNotFound', 'general:password', + 'general:pasteField', + 'general:pasteRow', 'general:payloadSettings', 'general:perPage', 'general:previous', diff --git a/packages/translations/src/languages/ar.ts b/packages/translations/src/languages/ar.ts index 790fbbef2..e8382ab45 100644 --- a/packages/translations/src/languages/ar.ts +++ b/packages/translations/src/languages/ar.ts @@ -90,6 +90,8 @@ export const arTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'الحقل التالي غير صالح:', followingFieldsInvalid_other: 'الحقول التالية غير صالحة:', incorrectCollection: 'مجموعة غير صحيحة', + insufficientClipboardPermissions: 'تم رفض الوصول إلى الحافظة. يرجى التحقق من أذونات الحافظة.', + invalidClipboardData: 'بيانات الحافظة غير صالحة.', invalidFileType: 'نوع ملف غير صالح', invalidFileTypeValue: 'نوع ملف غير صالح: {{value}}', invalidRequestArgs: 'تم تمرير وسيطات غير صالحة في الطلب: {{args}}', @@ -111,6 +113,7 @@ export const arTranslations: DefaultTranslationsObject = { problemUploadingFile: 'حدث خطأ اثناء رفع الملفّ.', tokenInvalidOrExpired: 'الرّمز إمّا غير صالح أو منتهي الصّلاحيّة.', tokenNotProvided: 'لم يتم تقديم الرمز.', + unableToCopy: 'تعذر النسخ.', unableToDeleteCount: 'يتعذّر حذف {{count}} من {{total}} {{label}}.', unableToReindexCollection: 'خطأ في إعادة فهرسة المجموعة {{collection}}. تم إيقاف العملية.', unableToUpdateCount: 'يتعذّر تحديث {{count}} من {{total}} {{label}}.', @@ -237,7 +240,9 @@ export const arTranslations: DefaultTranslationsObject = { 'سيؤدي هذا إلى إزالة الفهارس الحالية وإعادة فهرسة المستندات في جميع المجموعات.', copied: 'تمّ النّسخ', copy: 'نسخ', + copyField: 'نسخ الحقل', copying: 'نسخ', + copyRow: 'نسخ الصف', copyWarning: 'أنت على وشك الكتابة فوق {{to}} بـ {{from}} لـ {{label}} {{title}}. هل أنت متأكد؟', create: 'إنشاء', created: 'تمّ الإنشاء', @@ -330,6 +335,8 @@ export const arTranslations: DefaultTranslationsObject = { overwriteExistingData: 'استبدل بيانات الحقل الموجودة', pageNotFound: 'الصّفحة غير موجودة', password: 'كلمة المرور', + pasteField: 'لصق الحقل', + pasteRow: 'لصق الصف', payloadSettings: 'الإعدادات', perPage: 'لكلّ صفحة: {{limit}}', previous: 'سابق', diff --git a/packages/translations/src/languages/az.ts b/packages/translations/src/languages/az.ts index 95807d643..ff7f352ee 100644 --- a/packages/translations/src/languages/az.ts +++ b/packages/translations/src/languages/az.ts @@ -90,6 +90,9 @@ export const azTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Aşağıdakı sahə yanlışdır:', followingFieldsInvalid_other: 'Aşağıdaki sahələr yanlışdır:', incorrectCollection: 'Yanlış Kolleksiya', + insufficientClipboardPermissions: + 'Mübadilə buferinə giriş rədd edildi. Zəhmət olmasa, icazələri yoxlayın.', + invalidClipboardData: 'Yanlış mübadilə buferi məlumatı.', invalidFileType: 'Yanlış fayl növü', invalidFileTypeValue: 'Yanlış fayl növü: {{value}}', invalidRequestArgs: 'Sorguda etibarsız arqumentlər təqdim edildi: {{args}}', @@ -111,6 +114,7 @@ export const azTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Faylın yüklənməsi zamanı problem yarandı.', tokenInvalidOrExpired: 'Token ya yanlışdır və ya müddəti bitib.', tokenNotProvided: 'Token təqdim edilməyib.', + unableToCopy: 'Kopyalama mümkün deyil.', unableToDeleteCount: '{{count}} dən {{total}} {{label}} silinə bilmir.', unableToReindexCollection: '{{collection}} kolleksiyasının yenidən indekslənməsi zamanı səhv baş verdi. Əməliyyat dayandırıldı.', @@ -241,7 +245,9 @@ export const azTranslations: DefaultTranslationsObject = { 'Bu, mövcud indeksləri siləcək və bütün kolleksiyalardakı sənədləri yenidən indeksləyəcək.', copied: 'Kopyalandı', copy: 'Kopyala', + copyField: 'Sahəni kopyala', copying: 'Kopyalama', + copyRow: 'Sətiri kopyala', copyWarning: 'Siz {{label}} {{title}} üçün {{from}} ilə {{to}} -nu üzərindən yazmaq ətrafındasınız. Eminsiniz?', create: 'Yarat', @@ -336,6 +342,8 @@ export const azTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Mövcud sahə məlumatlarını yenidən yazın', pageNotFound: 'Səhifə tapılmadı', password: 'Şifrə', + pasteField: 'Sahəni yapışdır', + pasteRow: 'Sətiri yapışdır', payloadSettings: 'Payload Parametrləri', perPage: 'Hər səhifədə: {{limit}}', previous: 'Əvvəlki', diff --git a/packages/translations/src/languages/bg.ts b/packages/translations/src/languages/bg.ts index b1327aa19..4ac64a7b6 100644 --- a/packages/translations/src/languages/bg.ts +++ b/packages/translations/src/languages/bg.ts @@ -90,6 +90,9 @@ export const bgTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Следното поле е некоректно:', followingFieldsInvalid_other: 'Следните полета са некоректни:', incorrectCollection: 'Грешна колекция', + insufficientClipboardPermissions: + 'Достъпът до клипборда е отказан. Моля, проверете вашите разрешения за клипборда.', + invalidClipboardData: 'Невалидни данни в клипборда.', invalidFileType: 'Невалиден тип на файл', invalidFileTypeValue: 'Невалиден тип на файл: {{value}}', invalidRequestArgs: 'Невалидни аргументи в заявката: {{args}}', @@ -111,6 +114,7 @@ export const bgTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Имаше проблем при качването на файла.', tokenInvalidOrExpired: 'Ключът е невалиден или изтекъл.', tokenNotProvided: 'Токенът не е предоставен.', + unableToCopy: 'Неуспешно копиране.', unableToDeleteCount: 'Не беше възможно да се изтрият {{count}} от {{total}} {{label}}.', unableToReindexCollection: 'Грешка при преиндексиране на колекцията {{collection}}. Операцията е прекратена.', @@ -240,7 +244,9 @@ export const bgTranslations: DefaultTranslationsObject = { 'Това ще премахне съществуващите индекси и ще преиндексира документите във всички колекции.', copied: 'Копирано', copy: 'Копирай', + copyField: 'Копирай поле', copying: 'Копиране', + copyRow: 'Копирай ред', copyWarning: 'Предстои да презапишете {{to}} с {{from}} за {{label}} {{title}}. Сигурни ли сте?', create: 'Създай', @@ -335,6 +341,8 @@ export const bgTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Презапишете съществуващите данни в полето', pageNotFound: 'Страницата не беше открита', password: 'Парола', + pasteField: 'Постави поле', + pasteRow: 'Постави ред', payloadSettings: 'Настройки на Payload', perPage: 'На страница: {{limit}}', previous: 'Предишен', diff --git a/packages/translations/src/languages/bnBd.ts b/packages/translations/src/languages/bnBd.ts index 2afab6ad1..3119dc1c1 100644 --- a/packages/translations/src/languages/bnBd.ts +++ b/packages/translations/src/languages/bnBd.ts @@ -90,6 +90,9 @@ export const bnBdTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'নিম্নলিখিত ক্ষেত্রটি অবৈধ:', followingFieldsInvalid_other: 'নিম্নলিখিত ক্ষেত্রগুলি অবৈধ:', incorrectCollection: 'ভুল সংগ্রহ', + insufficientClipboardPermissions: + 'ক্লিপবোর্ড অ্যাক্সেস প্রত্যাখ্যান করা হয়েছে। দয়া করে আপনার ক্লিপবোর্ড অনুমতিগুলি পরীক্ষা করুন।', + invalidClipboardData: 'অবৈধ ক্লিপবোর্ড ডেটা।', invalidFileType: 'অবৈধ ফাইল প্রকার', invalidFileTypeValue: 'অবৈধ ফাইল প্রকার: {{value}}', invalidRequestArgs: 'অনুরোধে অবৈধ আর্গুমেন্ট পাস করা হয়েছে: {{args}}', @@ -111,6 +114,7 @@ export const bnBdTranslations: DefaultTranslationsObject = { problemUploadingFile: 'ফাইল আপলোড করতে একটি সমস্যা হয়েছে।', tokenInvalidOrExpired: 'টোকেন অবৈধ বা মেয়াদ শেষ হয়ে গেছে।', tokenNotProvided: 'টোকেন প্রদান করা হয়নি।', + unableToCopy: 'কপি করা সম্ভব নয়।', unableToDeleteCount: '{{total}} {{label}} এর মধ্যে {{count}} টি মুছতে অক্ষম।', unableToReindexCollection: '{{collection}} সংগ্রহ পুনরায় সূচিবদ্ধ করতে ত্রুটি হয়েছে। অপারেশন বাতিল করা হয়েছে।', @@ -242,7 +246,9 @@ export const bnBdTranslations: DefaultTranslationsObject = { 'এটি বিদ্যমান সূচিগুলি সরিয়ে দেবে এবং সমস্ত সংগ্রহগুলির ডকুমেন্টগুলি পুনরায় সূচিবদ্ধ করবে।', copied: 'কপি করা হয়েছে', copy: 'কপি করুন', + copyField: 'ফিল্ড কপি করুন', copying: 'কপি করা হচ্ছে', + copyRow: 'সারি কপি করুন', copyWarning: 'আপনি {{label}} {{title}} এর জন্য {{to}} কে {{from}} দ্বারা ওভাররাইট করতে চলেছেন। আপনি কি নিশ্চিত?', create: 'তৈরি করুন', @@ -337,6 +343,8 @@ export const bnBdTranslations: DefaultTranslationsObject = { overwriteExistingData: 'বিদ্যমান ফিল্ড ডেটা ওভাররাইট করুন', pageNotFound: 'পৃষ্ঠা পাওয়া যায়নি', password: 'পাসওয়ার্ড', + pasteField: 'ফিল্ড পেস্ট করুন', + pasteRow: 'সারি পেস্ট করুন', payloadSettings: 'পেলোড সেটিংস', perPage: 'প্রতি পৃষ্ঠায়: {{limit}}', previous: 'পূর্ববর্তী', diff --git a/packages/translations/src/languages/bnIn.ts b/packages/translations/src/languages/bnIn.ts index 6ee79296d..58e4d8e56 100644 --- a/packages/translations/src/languages/bnIn.ts +++ b/packages/translations/src/languages/bnIn.ts @@ -90,6 +90,9 @@ export const bnInTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'নিম্নলিখিত ক্ষেত্রটি অবৈধ:', followingFieldsInvalid_other: 'নিম্নলিখিত ক্ষেত্রগুলি অবৈধ:', incorrectCollection: 'ভুল সংগ্রহ', + insufficientClipboardPermissions: + 'ক্লিপবোর্ড অ্যাক্সেস অস্বীকৃত হয়েছে। অনুগ্রহ করে আপনার ক্লিপবোর্ড অনুমতিগুলি পরীক্ষা করুন।', + invalidClipboardData: 'অবৈধ ক্লিপবোর্ড ডেটা।', invalidFileType: 'অবৈধ ফাইল প্রকার', invalidFileTypeValue: 'অবৈধ ফাইল প্রকার: {{value}}', invalidRequestArgs: 'অনুরোধে অবৈধ আর্গুমেন্ট পাস করা হয়েছে: {{args}}', @@ -111,6 +114,7 @@ export const bnInTranslations: DefaultTranslationsObject = { problemUploadingFile: 'ফাইল আপলোড করতে একটি সমস্যা হয়েছে।', tokenInvalidOrExpired: 'টোকেন অবৈধ বা মেয়াদ শেষ হয়ে গেছে।', tokenNotProvided: 'টোকেন প্রদান করা হয়নি।', + unableToCopy: 'কপি করতে অক্ষম।', unableToDeleteCount: '{{total}} {{label}} এর মধ্যে {{count}} টি মুছতে অক্ষম।', unableToReindexCollection: '{{collection}} সংগ্রহ পুনরায় সূচিবদ্ধ করতে ত্রুটি হয়েছে। অপারেশন বাতিল করা হয়েছে।', @@ -242,7 +246,9 @@ export const bnInTranslations: DefaultTranslationsObject = { 'এটি বিদ্যমান সূচিগুলি সরিয়ে দেবে এবং সমস্ত সংগ্রহগুলির ডকুমেন্টগুলি পুনরায় সূচিবদ্ধ করবে।', copied: 'কপি করা হয়েছে', copy: 'কপি করুন', + copyField: 'ফিল্ড কপি করুন', copying: 'কপি করা হচ্ছে', + copyRow: 'সারি কপি করুন', copyWarning: 'আপনি {{label}} {{title}} এর জন্য {{to}} কে {{from}} দ্বারা ওভাররাইট করতে চলেছেন। আপনি কি নিশ্চিত?', create: 'তৈরি করুন', @@ -337,6 +343,8 @@ export const bnInTranslations: DefaultTranslationsObject = { overwriteExistingData: 'বিদ্যমান ফিল্ড ডেটা ওভাররাইট করুন', pageNotFound: 'পৃষ্ঠা পাওয়া যায়নি', password: 'পাসওয়ার্ড', + pasteField: 'ফিল্ড পেস্ট করুন', + pasteRow: 'সারি পেস্ট করুন', payloadSettings: 'পেলোড সেটিংস', perPage: 'প্রতি পৃষ্ঠায়: {{limit}}', previous: 'পূর্ববর্তী', diff --git a/packages/translations/src/languages/ca.ts b/packages/translations/src/languages/ca.ts index f724b604d..adb643836 100644 --- a/packages/translations/src/languages/ca.ts +++ b/packages/translations/src/languages/ca.ts @@ -91,6 +91,9 @@ export const caTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'El següent camp no és vàlid:', followingFieldsInvalid_other: 'Els següents camps no són vàlids:', incorrectCollection: 'Col·lecció incorrecta', + insufficientClipboardPermissions: + 'Accés al porta-retalls denegat. Comproveu els permisos del porta-retalls.', + invalidClipboardData: 'Dades del porta-retalls no vàlides.', invalidFileType: "Tipus d'arxiu no vàlid", invalidFileTypeValue: "Tipus d'arxiu no vàlid: {{value}}", invalidRequestArgs: 'Arguments no vàlids en la sol·licitud: {{args}}', @@ -112,6 +115,7 @@ export const caTranslations: DefaultTranslationsObject = { problemUploadingFile: "Hi ha hagut un problema mentre es carregava l'arxiu.", tokenInvalidOrExpired: 'El token és invàlid o ha caducat.', tokenNotProvided: "No s'ha proporcionat cap token.", + unableToCopy: 'No es pot copiar.', unableToDeleteCount: "No s'han pogut eliminar {{count}} de {{total}} {{label}}.", unableToReindexCollection: 'Error al reindexar la col·lecció {{collection}}. Operació cancel·lada.', @@ -241,7 +245,9 @@ export const caTranslations: DefaultTranslationsObject = { 'Aixo eliminarà els índexs existents i reindexarà els documents de totes les col·leccions.', copied: 'Copiat', copy: 'Copiar', + copyField: 'Copiar camp', copying: 'Copiant', + copyRow: 'Copiar fila', copyWarning: 'Estas a punt de sobreescriure {{to}} amb {{from}} per {{label}} {{title}}. Estas segur?', create: 'Crear', @@ -336,6 +342,8 @@ export const caTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Sobreescriu les dades existents', pageNotFound: 'Pàgina no trobada', password: 'Contrasenya', + pasteField: 'Enganxar camp', + pasteRow: 'Enganxar fila', payloadSettings: 'configuracio Payload', perPage: 'Per pagian: {{limit}}', previous: 'Previ', diff --git a/packages/translations/src/languages/cs.ts b/packages/translations/src/languages/cs.ts index 5cdd9a101..da916e1aa 100644 --- a/packages/translations/src/languages/cs.ts +++ b/packages/translations/src/languages/cs.ts @@ -90,6 +90,9 @@ export const csTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Následující pole je neplatné:', followingFieldsInvalid_other: 'Následující pole jsou neplatná:', incorrectCollection: 'Nesprávná kolekce', + insufficientClipboardPermissions: + 'Přístup ke schránce byl odepřen. Zkontrolujte oprávnění ke schránce.', + invalidClipboardData: 'Neplatná data ve schránce.', invalidFileType: 'Neplatný typ souboru', invalidFileTypeValue: 'Neplatný typ souboru: {{value}}', invalidRequestArgs: 'Neplatné argumenty v požadavku: {{args}}', @@ -111,6 +114,7 @@ export const csTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Při nahrávání souboru došlo k chybě.', tokenInvalidOrExpired: 'Token je neplatný nebo vypršel.', tokenNotProvided: 'Token není poskytnut.', + unableToCopy: 'Nelze zkopírovat.', unableToDeleteCount: 'Nelze smazat {{count}} z {{total}} {{label}}', unableToReindexCollection: 'Chyba při přeindexování kolekce {{collection}}. Operace byla přerušena.', @@ -240,7 +244,9 @@ export const csTranslations: DefaultTranslationsObject = { 'Tímto budou odstraněny stávající indexy a dokumenty ve všech kolekcích budou znovu zaindexovány.', copied: 'Zkopírováno', copy: 'Kopírovat', + copyField: 'Kopírovat pole', copying: 'Kopírování', + copyRow: 'Kopírovat řádek', copyWarning: 'Chystáte se přepsat {{to}} s {{from}} pro {{label}} {{title}}. Jste si jistý?', create: 'Vytvořit', created: 'Vytvořeno', @@ -334,6 +340,8 @@ export const csTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Přepsat existující data pole', pageNotFound: 'Stránka nenalezena', password: 'Heslo', + pasteField: 'Vložit pole', + pasteRow: 'Vložit řádek', payloadSettings: 'Payload nastavení', perPage: 'Na stránku: {{limit}}', previous: 'Předchozí', diff --git a/packages/translations/src/languages/da.ts b/packages/translations/src/languages/da.ts index a7d4d81af..5ed7783b1 100644 --- a/packages/translations/src/languages/da.ts +++ b/packages/translations/src/languages/da.ts @@ -89,6 +89,9 @@ export const daTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Feltet er ugyldigt:', followingFieldsInvalid_other: 'Felterne er ugyldige:', incorrectCollection: 'Forkert samling', + insufficientClipboardPermissions: + 'Adgang til udklipsholder nægtet. Kontroller dine udklipsholderrettigheder.', + invalidClipboardData: 'Ugyldige data i udklipsholderen.', invalidFileType: 'Ugyldig filtype', invalidFileTypeValue: 'Ugyldig filtype: {{value}}', invalidRequestArgs: 'Ugyldige argumenter i anmodningen: {{args}}', @@ -110,6 +113,7 @@ export const daTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Der opstod et problem under uploadingen af filen.', tokenInvalidOrExpired: 'Token er enten ugyldig eller udløbet.', tokenNotProvided: 'Token ikke angivet.', + unableToCopy: 'Kan ikke kopiere.', unableToDeleteCount: 'Kunne ikke slette {{count}} mangler {{total}} {{label}}.', unableToReindexCollection: 'Fejl ved genindeksering af samling {{collection}}. Operationen blev afbrudt.', @@ -239,7 +243,9 @@ export const daTranslations: DefaultTranslationsObject = { 'Dette vil fjerne eksisterende indekser og genindeksere dokumenter i alle samlinger.', copied: 'Kopieret', copy: 'Kopier', + copyField: 'Kopiér felt', copying: 'Kopiering', + copyRow: 'Kopiér række', copyWarning: 'Du er lige ved at overskrive {{to}} med {{from}} for {{label}} {{title}}. Er du sikker?', create: 'Opret', @@ -333,6 +339,8 @@ export const daTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Overskriv eksisterende feltdata', pageNotFound: 'Siden blev ikke fundet', password: 'Adgangskode', + pasteField: 'Indsæt felt', + pasteRow: 'Indsæt række', payloadSettings: 'Payload-indstillinger', perPage: 'Per side: {{limit}}', previous: 'Tidligere', diff --git a/packages/translations/src/languages/de.ts b/packages/translations/src/languages/de.ts index 5ca319cc8..876b44584 100644 --- a/packages/translations/src/languages/de.ts +++ b/packages/translations/src/languages/de.ts @@ -92,6 +92,9 @@ export const deTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Das folgende Feld ist nicht korrekt:', followingFieldsInvalid_other: 'Die folgenden Felder sind nicht korrekt:', incorrectCollection: 'Falsche Sammlung', + insufficientClipboardPermissions: + 'Zugriff auf die Zwischenablage verweigert. Bitte überprüfen Sie die Berechtigungen.', + invalidClipboardData: 'Ungültige Zwischenablagedaten.', invalidFileType: 'Ungültiger Datei-Typ', invalidFileTypeValue: 'Ungültiger Datei-Typ: {{value}}', invalidRequestArgs: 'Ungültige Argumente in der Anfrage: {{args}}', @@ -112,8 +115,9 @@ export const deTranslations: DefaultTranslationsObject = { previewing: 'Bei der Vorschau dieses Dokuments ist ein Fehler aufgetreten.', problemUploadingFile: 'Beim Hochladen der Datei ist ein Fehler aufgetreten.', tokenInvalidOrExpired: 'Token ist entweder ungültig oder abgelaufen.', - tokenNotProvided: 'Kein Token vorhanden.', - unableToDeleteCount: '{{count}} von {{total}} {{label}} konnten nicht gelöscht werden.', + tokenNotProvided: 'Token nicht bereitgestellt.', + unableToCopy: 'Kopieren nicht möglich.', + unableToDeleteCount: '{{count}} von {{total}} {{label}} konnte nicht gelöscht werden.', unableToReindexCollection: 'Fehler beim Neuindizieren der Sammlung {{collection}}. Vorgang abgebrochen.', unableToUpdateCount: '{{count}} von {{total}} {{label}} konnten nicht aktualisiert werden.', @@ -246,7 +250,9 @@ export const deTranslations: DefaultTranslationsObject = { 'Dies entfernt bestehende Indizes und indiziert die Dokumente in allen Sammlungen neu.', copied: 'Kopiert', copy: 'Kopieren', + copyField: 'Feld kopieren', copying: 'Kopieren', + copyRow: 'Zeile kopieren', copyWarning: 'Du bist dabei, {{to}} mit {{from}} für {{label}} {{title}} zu überschreiben. Bist du dir sicher?', create: 'Erstellen', @@ -341,6 +347,8 @@ export const deTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Vorhandene Eingaben überschreiben', pageNotFound: 'Seite nicht gefunden', password: 'Passwort', + pasteField: 'Feld einfügen', + pasteRow: 'Zeile einfügen', payloadSettings: 'Payload-Einstellungen', perPage: 'Pro Seite: {{limit}}', previous: 'Vorherige', diff --git a/packages/translations/src/languages/en.ts b/packages/translations/src/languages/en.ts index a00695959..9563bfe3a 100644 --- a/packages/translations/src/languages/en.ts +++ b/packages/translations/src/languages/en.ts @@ -91,6 +91,9 @@ export const enTranslations = { followingFieldsInvalid_one: 'The following field is invalid:', followingFieldsInvalid_other: 'The following fields are invalid:', incorrectCollection: 'Incorrect Collection', + insufficientClipboardPermissions: + 'Clipboard access denied. Please check your clipboard permissions.', + invalidClipboardData: 'Invalid clipboard data.', invalidFileType: 'Invalid file type', invalidFileTypeValue: 'Invalid file type: {{value}}', invalidRequestArgs: 'Invalid arguments passed in request: {{args}}', @@ -112,6 +115,7 @@ export const enTranslations = { problemUploadingFile: 'There was a problem while uploading the file.', tokenInvalidOrExpired: 'Token is either invalid or has expired.', tokenNotProvided: 'Token not provided.', + unableToCopy: 'Unable to copy.', unableToDeleteCount: 'Unable to delete {{count}} out of {{total}} {{label}}.', unableToReindexCollection: 'Error reindexing collection {{collection}}. Operation aborted.', unableToUpdateCount: 'Unable to update {{count}} out of {{total}} {{label}}.', @@ -241,7 +245,9 @@ export const enTranslations = { 'This will remove existing indexes and reindex documents in all collections.', copied: 'Copied', copy: 'Copy', + copyField: 'Copy Field', copying: 'Copying', + copyRow: 'Copy Row', copyWarning: 'You are about to overwrite {{to}} with {{from}} for {{label}} {{title}}. Are you sure?', create: 'Create', @@ -336,6 +342,8 @@ export const enTranslations = { overwriteExistingData: 'Overwrite existing field data', pageNotFound: 'Page not found', password: 'Password', + pasteField: 'Paste Field', + pasteRow: 'Paste Row', payloadSettings: 'Payload Settings', perPage: 'Per Page: {{limit}}', previous: 'Previous', diff --git a/packages/translations/src/languages/es.ts b/packages/translations/src/languages/es.ts index 41186f09f..362fa355a 100644 --- a/packages/translations/src/languages/es.ts +++ b/packages/translations/src/languages/es.ts @@ -90,6 +90,9 @@ export const esTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'El siguiente campo es inválido:', followingFieldsInvalid_other: 'Los siguientes campos son inválidos:', incorrectCollection: 'Colección Incorrecta', + insufficientClipboardPermissions: + 'Acceso al portapapeles denegado. Verifique los permisos del portapapeles.', + invalidClipboardData: 'Datos del portapapeles no válidos.', invalidFileType: 'Tipo de archivo inválido', invalidFileTypeValue: 'Tipo de archivo inválido: {{value}}', invalidRequestArgs: 'Argumentos inválidos en la solicitud: {{args}}', @@ -111,6 +114,7 @@ export const esTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Ocurrió un problema al subir el archivo.', tokenInvalidOrExpired: 'El token es inválido o ya expiró.', tokenNotProvided: 'Token no proporcionado.', + unableToCopy: 'No se puede copiar.', unableToDeleteCount: 'No se pudo eliminar {{count}} de {{total}} {{label}}.', unableToReindexCollection: 'Error al reindexar la colección {{collection}}. Operación abortada.', @@ -245,7 +249,9 @@ export const esTranslations: DefaultTranslationsObject = { 'Esto eliminará los índices existentes y volverá a indexar los documentos en todas las colecciones.', copied: 'Copiado', copy: 'Copiar', + copyField: 'Copiar campo', copying: 'Copiando', + copyRow: 'Copiar fila', copyWarning: 'Estás a punto de sobrescribir {{to}} con {{from}} para {{label}} {{title}}. ¿Estás seguro?', create: 'Crear', @@ -340,6 +346,8 @@ export const esTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Sobrescribir los datos existentes del campo', pageNotFound: 'Página no encontrada', password: 'Contraseña', + pasteField: 'Pegar campo', + pasteRow: 'Pegar fila', payloadSettings: 'Configuración de Payload', perPage: 'Por página: {{limit}}', previous: 'Anterior', diff --git a/packages/translations/src/languages/et.ts b/packages/translations/src/languages/et.ts index 25004901c..4263e0c2a 100644 --- a/packages/translations/src/languages/et.ts +++ b/packages/translations/src/languages/et.ts @@ -89,6 +89,9 @@ export const etTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Järgmine väli on vigane:', followingFieldsInvalid_other: 'Järgmised väljad on vigased:', incorrectCollection: 'Vale kollektsioon', + insufficientClipboardPermissions: + 'Lõikelaua juurdepääs keelatud. Palun kontrollige oma lõikelaua õigusi.', + invalidClipboardData: 'Kehtetu lõikelaua andmed.', invalidFileType: 'Vale failitüüp', invalidFileTypeValue: 'Vale failitüüp: {{value}}', invalidRequestArgs: 'Päringule edastati vigased argumendid: {{args}}', @@ -110,6 +113,7 @@ export const etTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Faili üleslaadimisel tekkis probleem.', tokenInvalidOrExpired: 'Võti on kas vigane või aegunud.', tokenNotProvided: 'Võtit ei esitatud.', + unableToCopy: 'Kopeerimine ebaõnnestus.', unableToDeleteCount: 'Ei õnnestunud kustutada {{count}} {{total}}-st {{label}}.', unableToReindexCollection: 'Viga kollektsiooni {{collection}} taasindekseerimisel. Toiming katkestatud.', @@ -239,7 +243,9 @@ export const etTranslations: DefaultTranslationsObject = { 'See eemaldab olemasolevad indeksid ja indekseerib uuesti dokumendid kõigis kollektsioonides.', copied: 'Kopeeritud', copy: 'Kopeeri', + copyField: 'Kopeeri väli', copying: 'Kopeerimine', + copyRow: 'Kopeeri rida', copyWarning: 'Olete üle kirjutamas {{to}} {{from}}-ga {{label}} {{title}} jaoks. Olete kindel?', create: 'Loo', created: 'Loodud', @@ -332,6 +338,8 @@ export const etTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Kirjuta olemasolevad välja andmed üle', pageNotFound: 'Lehte ei leitud', password: 'Parool', + pasteField: 'Kleebi väli', + pasteRow: 'Kleebi rida', payloadSettings: 'Payload seaded', perPage: 'Lehel: {{limit}}', previous: 'Eelmine', diff --git a/packages/translations/src/languages/fa.ts b/packages/translations/src/languages/fa.ts index 0b7aaf46c..d2141cb6d 100644 --- a/packages/translations/src/languages/fa.ts +++ b/packages/translations/src/languages/fa.ts @@ -89,6 +89,9 @@ export const faTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'کادر زیر نامعتبر است:', followingFieldsInvalid_other: 'کادرهای زیر نامعتبر هستند:', incorrectCollection: 'مجموعه نادرست', + insufficientClipboardPermissions: + 'دسترسی به کلیپ‌بورد رد شد. لطفاً دسترسی‌های کلیپ‌بورد خود را بررسی کنید.', + invalidClipboardData: 'داده‌های نامعتبر در کلیپ‌بورد.', invalidFileType: 'نوع رسانه نامعتبر است', invalidFileTypeValue: 'نوع رسانه نامعتبر: {{value}}', invalidRequestArgs: 'آرگومان‌های نامعتبر در درخواست ارسال شدند: {{args}}', @@ -110,6 +113,7 @@ export const faTranslations: DefaultTranslationsObject = { problemUploadingFile: 'هنگام بارگذاری سند خطایی رخ داد.', tokenInvalidOrExpired: 'ژتون شما نامعتبر یا منقضی شده است.', tokenNotProvided: 'توکن ارائه نشده است.', + unableToCopy: 'کپی امکان‌پذیر نیست.', unableToDeleteCount: 'نمی‌توان {{count}} از {{total}} {{label}} را حذف کرد.', unableToReindexCollection: 'خطا در بازنمایه‌سازی مجموعه {{collection}}. عملیات متوقف شد.', unableToUpdateCount: 'امکان به روز رسانی {{count}} خارج از {{total}} {{label}} وجود ندارد.', @@ -239,7 +243,9 @@ export const faTranslations: DefaultTranslationsObject = { 'این کار ایندکس‌های موجود را حذف کرده و اسناد را در همه مجموعه‌ها بازایندکس می‌کند.', copied: 'رونوشت شده', copy: 'رونوشت', + copyField: 'کپی فیلد', copying: 'کپی کردن', + copyRow: 'کپی ردیف', copyWarning: 'شما در حال استفاده از {{from}} به جای {{to}} برای {{label}} {{title}} هستید. آیا مطمئن هستید؟', create: 'ساختن', @@ -334,6 +340,8 @@ export const faTranslations: DefaultTranslationsObject = { overwriteExistingData: 'بازنویسی داده‌های فیلد موجود', pageNotFound: 'برگه یافت نشد', password: 'گذرواژه', + pasteField: 'چسباندن فیلد', + pasteRow: 'چسباندن ردیف', payloadSettings: 'تنظیمات پی‌لود', perPage: 'هر برگه: {{limit}}', previous: 'قبلی', diff --git a/packages/translations/src/languages/fr.ts b/packages/translations/src/languages/fr.ts index 9b3cd34d7..142a3c8ac 100644 --- a/packages/translations/src/languages/fr.ts +++ b/packages/translations/src/languages/fr.ts @@ -92,6 +92,9 @@ export const frTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Le champ suivant n’est pas valide :', followingFieldsInvalid_other: 'Les champs suivants ne sont pas valides :', incorrectCollection: 'Collection incorrecte', + insufficientClipboardPermissions: + 'Accès au presse-papiers refusé. Veuillez vérifier vos autorisations pour le presse-papiers.', + invalidClipboardData: 'Données invalides dans le presse-papiers.', invalidFileType: 'Type de fichier invalide', invalidFileTypeValue: 'Type de fichier invalide : {{value}}', invalidRequestArgs: 'Arguments non valides dans la requête : {{args}}', @@ -114,6 +117,7 @@ export const frTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Il y a eu un problème lors du téléversement du fichier.', tokenInvalidOrExpired: 'Le jeton n’est soit pas valide ou a expiré.', tokenNotProvided: 'Jeton non fourni.', + unableToCopy: 'Impossible de copier.', unableToDeleteCount: 'Impossible de supprimer {{count}} sur {{total}} {{label}}.', unableToReindexCollection: 'Erreur lors de la réindexation de la collection {{collection}}. Opération annulée.', @@ -248,7 +252,9 @@ export const frTranslations: DefaultTranslationsObject = { 'Cela supprimera les index existants et réindexera les documents dans toutes les collections.', copied: 'Copié', copy: 'Copie', + copyField: 'Copier le champ', copying: 'Copie', + copyRow: 'Copier la ligne', copyWarning: "Vous êtes sur le point d'écraser {{to}} avec {{from}} pour {{label}} {{title}}. Êtes-vous sûr ?", create: 'Créer', @@ -343,6 +349,8 @@ export const frTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Écraser les données existantes du champ', pageNotFound: 'Page non trouvée', password: 'Mot de passe', + pasteField: 'Coller le champ', + pasteRow: 'Coller la ligne', payloadSettings: 'Paramètres de Payload', perPage: 'Par Page: {{limit}}', previous: 'Précédent', diff --git a/packages/translations/src/languages/he.ts b/packages/translations/src/languages/he.ts index 5db6d8be3..595c64ade 100644 --- a/packages/translations/src/languages/he.ts +++ b/packages/translations/src/languages/he.ts @@ -88,6 +88,8 @@ export const heTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'השדה הבא אינו תקין:', followingFieldsInvalid_other: 'השדות הבאים אינם תקינים:', incorrectCollection: 'אוסף שגוי', + insufficientClipboardPermissions: 'הגישה ללוח הרחב נדחתה. אנא בדוק את הרשאות הלוח הרחב שלך.', + invalidClipboardData: 'נתוני לוח רחב לא חוקיים.', invalidFileType: 'סוג קובץ לא תקין', invalidFileTypeValue: 'סוג קובץ לא תקין: {{value}}', invalidRequestArgs: 'ארגומנטים לא חוקיים הועברו בבקשה: {{args}}', @@ -109,6 +111,7 @@ export const heTranslations: DefaultTranslationsObject = { problemUploadingFile: 'אירעה בעיה בזמן העלאת הקובץ.', tokenInvalidOrExpired: 'הטוקן אינו תקין או שפג תוקפו.', tokenNotProvided: 'טוקן לא סופק.', + unableToCopy: 'לא ניתן להעתיק.', unableToDeleteCount: 'לא ניתן למחוק {{count}} מתוך {{total}} {{label}}.', unableToReindexCollection: 'שגיאה בהחזרת אינדקס של אוסף {{collection}}. הפעולה בוטלה.', unableToUpdateCount: 'לא ניתן לעדכן {{count}} מתוך {{total}} {{label}}.', @@ -234,7 +237,9 @@ export const heTranslations: DefaultTranslationsObject = { confirmReindexDescriptionAll: 'זה יסיר את האינדקסים הקיימים ויחזיר אינדקס למסמכים בכל האוספים.', copied: 'הועתק', copy: 'העתק', + copyField: 'העתק שדה', copying: 'העתקה', + copyRow: 'העתק שורה', copyWarning: 'אתה עומד לדרוס את {{to}} באמצעות {{from}} עבור {{label}} {{title}}. האם אתה בטוח?', create: 'יצירה', @@ -327,6 +332,8 @@ export const heTranslations: DefaultTranslationsObject = { overwriteExistingData: 'דרוס את נתוני השדה הקיימים', pageNotFound: 'הדף לא נמצא', password: 'סיסמה', + pasteField: 'הדבק שדה', + pasteRow: 'הדבק שורה', payloadSettings: 'הגדרות מערכת Payload', perPage: '{{limit}} בכל עמוד', previous: 'קודם', diff --git a/packages/translations/src/languages/hr.ts b/packages/translations/src/languages/hr.ts index f9460d15e..8e38eeccf 100644 --- a/packages/translations/src/languages/hr.ts +++ b/packages/translations/src/languages/hr.ts @@ -91,6 +91,9 @@ export const hrTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Ovo polje je neispravno:', followingFieldsInvalid_other: 'Ova polja su neispravna:', incorrectCollection: 'Neispravna kolekcija', + insufficientClipboardPermissions: + 'Pristup međuspremniku odbijen. Provjerite svoja dopuštenja za međuspremnik.', + invalidClipboardData: 'Nevažeći podaci u međuspremniku.', invalidFileType: 'Neispravan tip datoteke', invalidFileTypeValue: 'Neispravan tip datoteke: {{value}}', invalidRequestArgs: 'Nevažeći argumenti u zahtjevu: {{args}}', @@ -112,6 +115,7 @@ export const hrTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Došlo je do problema pri učitavanju datoteke.', tokenInvalidOrExpired: 'Token je neispravan ili je istekao.', tokenNotProvided: 'Token nije pružen.', + unableToCopy: 'Nije moguće kopirati.', unableToDeleteCount: 'Nije moguće izbrisati {{count}} od {{total}} {{label}}.', unableToReindexCollection: 'Pogreška pri ponovnom indeksiranju kolekcije {{collection}}. Operacija je prekinuta.', @@ -241,7 +245,9 @@ export const hrTranslations: DefaultTranslationsObject = { 'Ovo će ukloniti postojeće indekse i ponovno indeksirati dokumente u svim kolekcijama.', copied: 'Kopirano', copy: 'Kopiraj', + copyField: 'Kopiraj polje', copying: 'Kopiranje', + copyRow: 'Kopiraj redak', copyWarning: 'Na rubu ste prepisivanja {{to}} s {{from}} za {{label}} {{title}}. Jeste li sigurni?', create: 'Izradi', @@ -336,6 +342,8 @@ export const hrTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Prepišite postojeće podatke u polju', pageNotFound: 'Stranica nije pronađena', password: 'Lozinka', + pasteField: 'Zalijepi polje', + pasteRow: 'Zalijepi redak', payloadSettings: 'Payload postavke', perPage: 'Po stranici: {{limit}}', previous: 'Prethodni', diff --git a/packages/translations/src/languages/hu.ts b/packages/translations/src/languages/hu.ts index f39831692..1bb4386db 100644 --- a/packages/translations/src/languages/hu.ts +++ b/packages/translations/src/languages/hu.ts @@ -92,6 +92,9 @@ export const huTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'A következő mező érvénytelen:', followingFieldsInvalid_other: 'A következő mezők érvénytelenek:', incorrectCollection: 'Helytelen gyűjtemény', + insufficientClipboardPermissions: + 'A vágólaphoz való hozzáférés elutasítva. Kérjük, ellenőrizze a vágólap engedélyeit.', + invalidClipboardData: 'Érvénytelen vágólap adat.', invalidFileType: 'Érvénytelen fájltípus', invalidFileTypeValue: 'Érvénytelen fájltípus: {{value}}', invalidRequestArgs: 'Érvénytelen argumentumok a kérésben: {{args}}', @@ -113,6 +116,7 @@ export const huTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Hiba történt a fájl feltöltése közben.', tokenInvalidOrExpired: 'A token érvénytelen vagy lejárt.', tokenNotProvided: 'Token nem biztosított.', + unableToCopy: 'Másolás nem lehetséges.', unableToDeleteCount: 'Nem sikerült törölni {{count}}/{{total}} {{label}}.', unableToReindexCollection: 'Hiba a(z) {{collection}} gyűjtemény újraindexelésekor. A művelet megszakítva.', @@ -243,7 +247,9 @@ export const huTranslations: DefaultTranslationsObject = { 'Ez eltávolítja a meglévő indexeket, és újraindexálja a dokumentumokat az összes gyűjteményben.', copied: 'Másolva', copy: 'Másolás', + copyField: 'Mező másolása', copying: 'Másolás', + copyRow: 'Sor másolása', copyWarning: 'Ön azzal készül felülírni {{to}} -t {{from}} -mal a {{label}} {{title}} számára. Biztos benne?', create: 'Létrehozás', @@ -337,6 +343,8 @@ export const huTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Írja felül a meglévő mezőadatokat', pageNotFound: 'Az oldal nem található', password: 'Jelszó', + pasteField: 'Mező beillesztése', + pasteRow: 'Sor beillesztése', payloadSettings: 'Payload beállítások', perPage: 'Oldalanként: {{limit}}', previous: 'Előző', diff --git a/packages/translations/src/languages/hy.ts b/packages/translations/src/languages/hy.ts index 149d84490..17a97ca9d 100644 --- a/packages/translations/src/languages/hy.ts +++ b/packages/translations/src/languages/hy.ts @@ -90,6 +90,9 @@ export const hyTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Հետևյալ դաշտն անվավեր է։', followingFieldsInvalid_other: 'Հետևյալ դաշտերն անվավեր են։', incorrectCollection: 'Սխալ հավաքածու', + insufficientClipboardPermissions: + 'Սեղմատախտակին հասանելիությունը մերժվել է։ Խնդրում ենք ստուգել ձեր սեղմատախտակի թույլտվությունները։', + invalidClipboardData: 'Անվավեր սեղմատախտակի տվյալներ։', invalidFileType: 'Անվավեր ֆայլի տեսակ', invalidFileTypeValue: 'Անվավեր ֆայլի տեսակ՝ {{value}}', invalidRequestArgs: 'Հայտում փոխանցված անվավեր արգումենտներ՝ {{args}}', @@ -111,6 +114,7 @@ export const hyTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Ֆայլը վերբեռնելու ժամանակ խնդիր է առաջացել։', tokenInvalidOrExpired: 'Թոքենն անվավեր է կամ ժամկետանց։', tokenNotProvided: 'Թոքենը տրամադրված չէ։', + unableToCopy: 'Չհաջողվեց պատճենել։', unableToDeleteCount: 'Հնարավոր չէ ջնջել {{count}}-ը {{total}} {{label}}-ից։', unableToReindexCollection: 'Հավաքածու {{collection}}-ը վերաինդեքսավորելու սխալ։ Գործողությունն ընդհատվել է։', @@ -241,7 +245,9 @@ export const hyTranslations: DefaultTranslationsObject = { 'Սա կհեռացնի գոյություն ունեցող ինդեքսները և կվերաինդեքսավորի փաստաթղթերը բոլոր հավաքածուներում։', copied: 'Պատճենված', copy: 'Պատճենել', + copyField: 'Պատճենել դաշտը', copying: 'Պատճենվում է', + copyRow: 'Պատճենել տողը', copyWarning: 'Դուք պատրաստվում եք վերագրել {{to}}-ը {{from}}-ով {{label}} {{title}}-ի համար։ Համոզվա՞ծ եք։', create: 'Ստեղծել', @@ -336,6 +342,8 @@ export const hyTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Վերագրել գոյություն ունեցող դաշտի տվյալները', pageNotFound: 'Էջը չի գտնվել', password: 'Գաղտնաբառ', + pasteField: 'Տեղադրել դաշտը', + pasteRow: 'Տեղադրել տողը', payloadSettings: 'Payload-ի կարգավորումներ', perPage: 'Էջում՝ {{limit}}', previous: 'Նախորդ', diff --git a/packages/translations/src/languages/it.ts b/packages/translations/src/languages/it.ts index e8f65f1b2..4edcbd5ff 100644 --- a/packages/translations/src/languages/it.ts +++ b/packages/translations/src/languages/it.ts @@ -91,6 +91,9 @@ export const itTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Il seguente campo non è valido:', followingFieldsInvalid_other: 'I seguenti campi non sono validi:', incorrectCollection: 'Collezione non corretta', + insufficientClipboardPermissions: + 'Accesso alla clipboard negato. Verifica i permessi della clipboard.', + invalidClipboardData: 'Dati della clipboard non validi.', invalidFileType: 'Tipo di file non valido', invalidFileTypeValue: 'Tipo di file non valido: {{value}}', invalidRequestArgs: 'Argomenti non validi nella richiesta: {{args}}', @@ -113,6 +116,7 @@ export const itTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Si è verificato un problema durante il caricamento del file.', tokenInvalidOrExpired: 'Il token non è valido o è scaduto.', tokenNotProvided: 'Token non fornito.', + unableToCopy: 'Impossibile copiare.', unableToDeleteCount: 'Impossibile eliminare {{count}} su {{total}} {{label}}.', unableToReindexCollection: 'Errore durante la reindicizzazione della collezione {{collection}}. Operazione annullata.', @@ -245,7 +249,9 @@ export const itTranslations: DefaultTranslationsObject = { "Questo rimuoverà gli indici esistenti e rifarà l'indice dei documenti in tutte le collezioni.", copied: 'Copiato', copy: 'Copia', + copyField: 'Copia campo', copying: 'Copia', + copyRow: 'Copia riga', copyWarning: 'Stai per sovrascrivere {{to}} con {{from}} per {{label}} {{title}}. Sei sicuro?', create: 'Crea', created: 'Data di creazione', @@ -338,6 +344,8 @@ export const itTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Sovrascrivi i dati del campo esistente', pageNotFound: 'Pagina non trovata', password: 'Password', + pasteField: 'Incolla campo', + pasteRow: 'Incolla riga', payloadSettings: 'Impostazioni di Payload', perPage: 'Per Pagina: {{limit}}', previous: 'Precedente', diff --git a/packages/translations/src/languages/ja.ts b/packages/translations/src/languages/ja.ts index 9de2d07a1..30f97016f 100644 --- a/packages/translations/src/languages/ja.ts +++ b/packages/translations/src/languages/ja.ts @@ -91,6 +91,9 @@ export const jaTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: '次のフィールドは無効です:', followingFieldsInvalid_other: '次のフィールドは無効です:', incorrectCollection: '不正なコレクション', + insufficientClipboardPermissions: + 'クリップボードへのアクセスが拒否されました。クリップボードの権限を確認してください。', + invalidClipboardData: '無効なクリップボードデータ。', invalidFileType: '無効なファイル形式', invalidFileTypeValue: '無効なファイル形式: {{value}}', invalidRequestArgs: 'リクエストに無効な引数が渡されました: {{args}}', @@ -112,6 +115,7 @@ export const jaTranslations: DefaultTranslationsObject = { problemUploadingFile: 'ファイルのアップロード中に問題が発生しました。', tokenInvalidOrExpired: 'トークンが無効、または、有効期限が切れています。', tokenNotProvided: 'トークンが提供されていません。', + unableToCopy: 'コピーできません。', unableToDeleteCount: '{{total}} {{label}} から {{count}} を削除できません。', unableToReindexCollection: 'コレクション {{collection}} の再インデックス中にエラーが発生しました。操作は中止されました。', @@ -241,7 +245,9 @@ export const jaTranslations: DefaultTranslationsObject = { 'これにより既存のインデックスが削除され、すべてのコレクション内のドキュメントが再インデックスされます。', copied: 'コピーしました', copy: 'コピー', + copyField: 'フィールドをコピー', copying: 'コピーする', + copyRow: '行をコピー', copyWarning: 'あなたは{{label}} {{title}}の{{to}}を{{from}}で上書きしようとしています。よろしいですか?', create: '作成', @@ -336,6 +342,8 @@ export const jaTranslations: DefaultTranslationsObject = { overwriteExistingData: '既存のフィールドデータを上書きする', pageNotFound: 'ページが見つかりません', password: 'パスワード', + pasteField: 'フィールドを貼り付け', + pasteRow: '行を貼り付け', payloadSettings: 'Payload 設定', perPage: '表示件数: {{limit}}', previous: '前の', diff --git a/packages/translations/src/languages/ko.ts b/packages/translations/src/languages/ko.ts index 8d33c629f..39f967fc0 100644 --- a/packages/translations/src/languages/ko.ts +++ b/packages/translations/src/languages/ko.ts @@ -90,6 +90,9 @@ export const koTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: '다음 입력란이 유효하지 않습니다:', followingFieldsInvalid_other: '다음 입력란이 유효하지 않습니다:', incorrectCollection: '잘못된 컬렉션', + insufficientClipboardPermissions: + '클립보드 접근이 거부되었습니다. 클립보드 권한을 확인하십시오.', + invalidClipboardData: '유효하지 않은 클립보드 데이터입니다.', invalidFileType: '잘못된 파일 형식', invalidFileTypeValue: '잘못된 파일 형식: {{value}}', invalidRequestArgs: '요청에 잘못된 인수가 전달되었습니다: {{args}}', @@ -111,6 +114,7 @@ export const koTranslations: DefaultTranslationsObject = { problemUploadingFile: '파일 업로드 중에 문제가 발생했습니다.', tokenInvalidOrExpired: '토큰이 유효하지 않거나 만료되었습니다.', tokenNotProvided: '토큰이 제공되지 않았습니다.', + unableToCopy: '복사할 수 없습니다.', unableToDeleteCount: '총 {{total}}개 중 {{count}}개의 {{label}}을(를) 삭제할 수 없습니다.', unableToReindexCollection: '{{collection}} 컬렉션의 재인덱싱 중 오류가 발생했습니다. 작업이 중단되었습니다.', @@ -239,7 +243,9 @@ export const koTranslations: DefaultTranslationsObject = { '이 작업은 기존 인덱스를 삭제하고 모든 컬렉션 내의 문서를 다시 인덱싱합니다.', copied: '복사됨', copy: '복사', + copyField: '필드 복사', copying: '복사하기', + copyRow: '행 복사', copyWarning: '{{label}} {{title}}에 대해 {{from}}으로 {{to}}를 덮어쓰려고 합니다. 확실합니까?', create: '생성', created: '생성됨', @@ -333,6 +339,8 @@ export const koTranslations: DefaultTranslationsObject = { overwriteExistingData: '기존 필드 데이터 덮어쓰기', pageNotFound: '페이지를 찾을 수 없음', password: '비밀번호', + pasteField: '필드 붙여넣기', + pasteRow: '행 붙여넣기', payloadSettings: 'Payload 설정', perPage: '페이지당 개수: {{limit}}', previous: '이전', diff --git a/packages/translations/src/languages/lt.ts b/packages/translations/src/languages/lt.ts index 1669dcad5..68310295f 100644 --- a/packages/translations/src/languages/lt.ts +++ b/packages/translations/src/languages/lt.ts @@ -91,6 +91,9 @@ export const ltTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Šis laukas yra netinkamas:', followingFieldsInvalid_other: 'Šie laukai yra neteisingi:', incorrectCollection: 'Neteisinga kolekcija', + insufficientClipboardPermissions: + 'Prieiga prie iškarpinės atmesta. Patikrinkite savo iškarpinės teises.', + invalidClipboardData: 'Neteisingi iškarpinės duomenys.', invalidFileType: 'Netinkamas failo tipas', invalidFileTypeValue: 'Neteisingas failo tipas: {{value}}', invalidRequestArgs: 'Netinkami argumentai perduoti užklausoje: {{args}}', @@ -112,6 +115,7 @@ export const ltTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Failo įkelti nepavyko dėl problemos.', tokenInvalidOrExpired: 'Žetonas yra neteisingas arba jo galiojimas pasibaigė.', tokenNotProvided: 'Žetonas nesuteiktas.', + unableToCopy: 'Nepavyko nukopijuoti.', unableToDeleteCount: 'Negalima ištrinti {{count}} iš {{total}} {{label}}.', unableToReindexCollection: 'Klaida perindeksuojant rinkinį {{collection}}. Operacija nutraukta.', @@ -243,7 +247,9 @@ export const ltTranslations: DefaultTranslationsObject = { 'Tai pašalins esamas indeksus ir perindeksuos dokumentus visose kolekcijose.', copied: 'Nukopijuota', copy: 'Kopijuoti', + copyField: 'Kopijuoti lauką', copying: 'Kopijavimas', + copyRow: 'Kopijuoti eilutę', copyWarning: 'Jūs ketinate perrašyti {{to}} į {{from}} šildymui {{label}} {{title}}. Ar esate tikri?', create: 'Sukurti', @@ -338,6 +344,8 @@ export const ltTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Perrašyti esamus lauko duomenis', pageNotFound: 'Puslapis nerastas', password: 'Slaptažodis', + pasteField: 'Įklijuoti lauką', + pasteRow: 'Įklijuoti eilutę', payloadSettings: 'Payload nustatymai', perPage: 'Puslapyje: {{limit}}', previous: 'Ankstesnis', diff --git a/packages/translations/src/languages/lv.ts b/packages/translations/src/languages/lv.ts index 4cf52aad6..051e2d34a 100644 --- a/packages/translations/src/languages/lv.ts +++ b/packages/translations/src/languages/lv.ts @@ -90,6 +90,9 @@ export const lvTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Šis lauks nav derīgs:', followingFieldsInvalid_other: 'Šie lauki nav derīgi:', incorrectCollection: 'Nepareiza kolekcija', + insufficientClipboardPermissions: + 'Piekļuve starpliktuvei liegta. Lūdzu, pārbaudiet savas starpliktuves atļaujas.', + invalidClipboardData: 'Nederīgi starpliktuves dati.', invalidFileType: 'Nederīgs faila tips', invalidFileTypeValue: 'Nederīgs faila tips: {{value}}', invalidRequestArgs: 'Pieprasījumā nodoti nederīgi argumenti: {{args}}', @@ -111,6 +114,7 @@ export const lvTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Radās problēma, augšupielādējot failu.', tokenInvalidOrExpired: 'Tokens ir nederīgs vai beidzies.', tokenNotProvided: 'Tokens nav norādīts.', + unableToCopy: 'Neizdevās kopēt.', unableToDeleteCount: 'Neizdevās izdzēst {{count}} no {{total}} {{label}}.', unableToReindexCollection: 'Radās kļūda, pārindeksējot kolekciju {{collection}}. Operācija pārtraukta.', @@ -240,7 +244,9 @@ export const lvTranslations: DefaultTranslationsObject = { 'Tas noņems esošos indeksus un pārindeksēs dokumentus visās kolekcijās.', copied: 'Nokopēts', copy: 'Kopēt', + copyField: 'Kopēt lauku', copying: 'Kopē...', + copyRow: 'Kopēt rindu', copyWarning: 'Jūs grasāties pārrakstīt {{to}} ar {{from}} priekš {{label}} {{title}}. Vai esat pārliecināts?', create: 'Izveidot', @@ -335,6 +341,8 @@ export const lvTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Pārrakstīt esošos datus', pageNotFound: 'Lapa nav atrasta', password: 'Parole', + pasteField: 'Ielīmēt lauku', + pasteRow: 'Ielīmēt rindu', payloadSettings: 'Payload iestatījumi', perPage: 'Lapas ieraksti: {{limit}}', previous: 'Iepriekšējais', diff --git a/packages/translations/src/languages/my.ts b/packages/translations/src/languages/my.ts index 857cd3f2c..e4575be8c 100644 --- a/packages/translations/src/languages/my.ts +++ b/packages/translations/src/languages/my.ts @@ -90,6 +90,9 @@ export const myTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'ထည့်သွင်းထားသော အချက်အလက်သည် မမှန်ကန်ပါ။', followingFieldsInvalid_other: 'ထည့်သွင်းထားသော အချက်အလက်များသည် မမှန်ကန်ပါ။', incorrectCollection: 'မှားယွင်းသော စုစည်းမှု', + insufficientClipboardPermissions: + 'ကလစ်ဘုတ်ဝင်ရောက်ခွင့်ပြုချက်မရှိပါ။ ကလစ်ဘုတ်ပြုချက်များကိုစစ်ဆေးပါ။', + invalidClipboardData: 'မမှန်ကန်သောကလစ်ဘုတ်ဒေတာ။', invalidFileType: 'မမှန်ကန်သော ဖိုင်အမျိုးအစား', invalidFileTypeValue: 'မမှန်ကန်သော ဖိုင်အမျိုးအစား: {{value}}', invalidRequestArgs: 'တောင်းဆိုမှုတွင် မှားယွင်းသော အကြောင်းပြချက်များ ပေးပို့ထားသည်: {{args}}', @@ -111,6 +114,7 @@ export const myTranslations: DefaultTranslationsObject = { problemUploadingFile: 'ဖိုင်ကို အပ်လုဒ်တင်ရာတွင် ပြဿနာရှိနေသည်။', tokenInvalidOrExpired: 'တိုကင်သည် မမှန်ကန်ပါ သို့မဟုတ် သက်တမ်းကုန်သွားပါပြီ။', tokenNotProvided: 'Token မပေးထားပါ။', + unableToCopy: 'ကူးရန်မဖြစ်နိုင်ပါ။', unableToDeleteCount: '{{total}} {{label}} မှ {{count}} ကို ဖျက်၍မရပါ။', unableToReindexCollection: '{{collection}} စုစည်းမှုကို ပြန်လည်အညွှန်းပြုလုပ်ခြင်း အမှားရှိနေသည်။ လုပ်ဆောင်မှုကို ဖျက်သိမ်းခဲ့သည်။', @@ -243,7 +247,9 @@ export const myTranslations: DefaultTranslationsObject = { 'ဤသည်သည် ရှိပြီးသား အညွှန်းများကို ဖျက်ပစ်ပြီး အားလုံးသော ကော်လက်ရှင်းများတွင် စာရွက်များကို ထပ်လိပ်ပါလိမ့်မည်။', copied: 'ကူးယူပြီးပြီ။', copy: 'ကူးယူမည်။', + copyField: 'ကွက်လပ်ကိုကူးပါ', copying: 'ကူးယူခြင်း', + copyRow: 'တန်းကိုကူးပါ', copyWarning: 'Anda akan menulis ganti {{to}} dengan {{from}} untuk {{label}} {{title}}. Adakah anda pasti?', create: 'ဖန်တီးမည်။', @@ -338,6 +344,8 @@ export const myTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Menulis semula data bidang yang sedia ada', pageNotFound: 'ရောက်ရှိနေသော စာမျက်နှာသည် မရှိပါ။', password: 'စကားဝှက်', + pasteField: 'ကွက်လပ်ကိုတင်ပါ', + pasteRow: 'တန်းကိုတင်ပါ', payloadSettings: 'ရွေးချယ်စရာများ', perPage: 'စာမျက်နှာ အလိုက်: {{limit}}', previous: 'ယခင်', diff --git a/packages/translations/src/languages/nb.ts b/packages/translations/src/languages/nb.ts index 413514e28..f2f5a2b43 100644 --- a/packages/translations/src/languages/nb.ts +++ b/packages/translations/src/languages/nb.ts @@ -90,6 +90,9 @@ export const nbTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Følgende felt er ugyldig:', followingFieldsInvalid_other: 'Følgende felter er ugyldige:', incorrectCollection: 'Ugyldig samling', + insufficientClipboardPermissions: + 'Tilgang til utklippstavlen ble nektet. Sjekk utklippstavle-tillatelsene dine.', + invalidClipboardData: 'Ugyldige utklippstavldata.', invalidFileType: 'Ugyldig filtype', invalidFileTypeValue: 'Ugyldig filtype: {{value}}', invalidRequestArgs: 'Ugyldige argumenter i forespørselen: {{args}}', @@ -111,6 +114,7 @@ export const nbTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Det oppstod et problem under opplasting av filen.', tokenInvalidOrExpired: 'Token er enten ugyldig eller har utløpt.', tokenNotProvided: 'Token ikke angitt.', + unableToCopy: 'Kan ikke kopiere.', unableToDeleteCount: 'Kan ikke slette {{count}} av {{total}} {{label}}.', unableToReindexCollection: 'Feil ved reindeksering av samlingen {{collection}}. Operasjonen ble avbrutt.', @@ -241,7 +245,9 @@ export const nbTranslations: DefaultTranslationsObject = { 'Dette vil fjerne eksisterende indekser og reindeksere dokumentene i alle samlinger.', copied: 'Kopiert', copy: 'Kopiér', + copyField: 'Kopier felt', copying: 'Kopiering', + copyRow: 'Kopier rad', copyWarning: 'Du er i ferd med å overskrive {{to}} med {{from}} for {{label}} {{title}}. Er du sikker?', create: 'Opprett', @@ -336,6 +342,8 @@ export const nbTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Overskriv eksisterende feltdata', pageNotFound: 'Siden ble ikke funnet', password: 'Passord', + pasteField: 'Lim inn felt', + pasteRow: 'Lim inn rad', payloadSettings: 'Payload-innstillinger', perPage: 'Per side: {{limit}}', previous: 'Forrige', diff --git a/packages/translations/src/languages/nl.ts b/packages/translations/src/languages/nl.ts index f629798e0..dee4ae388 100644 --- a/packages/translations/src/languages/nl.ts +++ b/packages/translations/src/languages/nl.ts @@ -91,6 +91,9 @@ export const nlTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Het volgende veld is ongeldig:', followingFieldsInvalid_other: 'De volgende velden zijn ongeldig:', incorrectCollection: 'Ongeldige collectie', + insufficientClipboardPermissions: + 'Toegang tot het klembord geweigerd. Controleer je klembordmachtigingen.', + invalidClipboardData: 'Ongeldige klembordgegevens.', invalidFileType: 'Ongeldig bestandstype', invalidFileTypeValue: 'Ongeldig bestandstype: {{value}}', invalidRequestArgs: 'Ongeldige argumenten in verzoek: {{args}}', @@ -112,6 +115,7 @@ export const nlTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Er was een probleem bij het uploaden van het bestand.', tokenInvalidOrExpired: 'Token is ongeldig of verlopen.', tokenNotProvided: 'Token niet verstrekt.', + unableToCopy: 'Kan niet kopiëren.', unableToDeleteCount: 'Kan {{count}} van {{total}} {{label}} niet verwijderen.', unableToReindexCollection: 'Fout bij het herindexeren van de collectie {{collection}}. De operatie is afgebroken.', @@ -244,7 +248,9 @@ export const nlTranslations: DefaultTranslationsObject = { 'Dit verwijdert bestaande indexen en indexeert de documenten in alle collecties opnieuw.', copied: 'Gekopieerd', copy: 'Kopiëren', + copyField: 'Veld kopiëren', copying: 'Kopiëren', + copyRow: 'Rij kopiëren', copyWarning: 'U staat op het punt om {{to}} te overschrijven met {{from}} voor {{label}} {{title}}. Bent u zeker?', create: 'Aanmaken', @@ -339,6 +345,8 @@ export const nlTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Overschrijf bestaande veldgegevens', pageNotFound: 'Pagina niet gevonden', password: 'Wachtwoord', + pasteField: 'Veld plakken', + pasteRow: 'Rij plakken', payloadSettings: 'Payload Instellingen', perPage: 'Per pagina: {{limit}}', previous: 'Vorige', diff --git a/packages/translations/src/languages/pl.ts b/packages/translations/src/languages/pl.ts index 2fe46c666..a4ff89c11 100644 --- a/packages/translations/src/languages/pl.ts +++ b/packages/translations/src/languages/pl.ts @@ -90,6 +90,8 @@ export const plTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'To pole jest nieprawidłowe:', followingFieldsInvalid_other: 'Następujące pola są nieprawidłowe:', incorrectCollection: 'Nieprawidłowa kolekcja', + insufficientClipboardPermissions: 'Odmowa dostępu do schowka. Sprawdź uprawnienia schowka.', + invalidClipboardData: 'Nieprawidłowe dane schowka.', invalidFileType: 'Nieprawidłowy typ pliku', invalidFileTypeValue: 'Nieprawidłowy typ pliku: {{value}}', invalidRequestArgs: 'Nieprawidłowe argumenty w żądaniu: {{args}}', @@ -111,6 +113,7 @@ export const plTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Wystąpił problem podczas przesyłania pliku.', tokenInvalidOrExpired: 'Token jest nieprawidłowy lub wygasł.', tokenNotProvided: 'Token nie został dostarczony.', + unableToCopy: 'Nie można skopiować.', unableToDeleteCount: 'Nie można usunąć {{count}} z {{total}} {{label}}.', unableToReindexCollection: 'Błąd podczas ponownego indeksowania kolekcji {{collection}}. Operacja została przerwana.', @@ -241,7 +244,9 @@ export const plTranslations: DefaultTranslationsObject = { 'Spowoduje to usunięcie istniejących indeksów i ponowne zaindeksowanie dokumentów we wszystkich kolekcjach.', copied: 'Skopiowano', copy: 'Skopiuj', + copyField: 'Kopiuj pole', copying: 'Kopiowanie', + copyRow: 'Kopiuj wiersz', copyWarning: 'Zamierzasz nadpisać {{to}} na {{from}} dla {{label}} {{title}}. Czy jesteś pewny?', create: 'Stwórz', @@ -336,6 +341,8 @@ export const plTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Nadpisz istniejące dane pola', pageNotFound: 'Strona nie znaleziona', password: 'Hasło', + pasteField: 'Wklej pole', + pasteRow: 'Wklej wiersz', payloadSettings: 'Ustawienia Payload', perPage: 'Na stronę: {{limit}}', previous: 'Poprzedni', diff --git a/packages/translations/src/languages/pt.ts b/packages/translations/src/languages/pt.ts index eeb889a21..b3f1f366b 100644 --- a/packages/translations/src/languages/pt.ts +++ b/packages/translations/src/languages/pt.ts @@ -91,6 +91,9 @@ export const ptTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'O campo a seguir está inválido:', followingFieldsInvalid_other: 'Os campos a seguir estão inválidos:', incorrectCollection: 'Coleção Incorreta', + insufficientClipboardPermissions: + 'Acesso à área de transferência negado. Verifique suas permissões da área de transferência.', + invalidClipboardData: 'Dados inválidos na área de transferência.', invalidFileType: 'Tipo de arquivo inválido', invalidFileTypeValue: 'Tipo de arquivo inválido: {{value}}', invalidRequestArgs: 'Argumentos inválidos passados na solicitação: {{args}}', @@ -112,6 +115,7 @@ export const ptTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Ocorreu um problema ao carregar o arquivo.', tokenInvalidOrExpired: 'Token expirado ou inválido.', tokenNotProvided: 'Token não fornecido.', + unableToCopy: 'Não é possível copiar.', unableToDeleteCount: 'Não é possível excluir {{count}} de {{total}} {{label}}.', unableToReindexCollection: 'Erro ao reindexar a coleção {{collection}}. Operação abortada.', unableToUpdateCount: 'Não foi possível atualizar {{count}} de {{total}} {{label}}.', @@ -241,7 +245,9 @@ export const ptTranslations: DefaultTranslationsObject = { 'Isso removerá os índices existentes e reindexará os documentos em todas as coleções.', copied: 'Copiado', copy: 'Copiar', + copyField: 'Copiar campo', copying: 'Copiando', + copyRow: 'Copiar linha', copyWarning: 'Você está prestes a sobrescrever {{to}} com {{from}} para {{label}} {{title}}. Tem certeza?', create: 'Criar', @@ -336,6 +342,8 @@ export const ptTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Sobrescrever dados de campo existentes', pageNotFound: 'Página não encontrada', password: 'Senha', + pasteField: 'Colar campo', + pasteRow: 'Colar linha', payloadSettings: 'Configurações do Payload', perPage: 'Itens por Página: {{limit}}', previous: 'Anterior', diff --git a/packages/translations/src/languages/ro.ts b/packages/translations/src/languages/ro.ts index 85fbf7539..64e557c85 100644 --- a/packages/translations/src/languages/ro.ts +++ b/packages/translations/src/languages/ro.ts @@ -92,6 +92,9 @@ export const roTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Următorul câmp nu este valid:', followingFieldsInvalid_other: 'Următoarele câmpuri nu sunt valabile:', incorrectCollection: 'Colecție incorectă', + insufficientClipboardPermissions: + 'Accesul la clipboard a fost refuzat. Verificați permisiunile clipboard-ului.', + invalidClipboardData: 'Date invalide în clipboard.', invalidFileType: 'Tip de fișier invalid', invalidFileTypeValue: 'Tip de fișier invalid: {{value}}', invalidRequestArgs: 'Argumente invalide transmise în cerere: {{args}}', @@ -113,6 +116,7 @@ export const roTranslations: DefaultTranslationsObject = { problemUploadingFile: 'A existat o problemă în timpul încărcării fișierului.', tokenInvalidOrExpired: 'Tokenul este invalid sau a expirat.', tokenNotProvided: 'Tokenul nu a fost furnizat.', + unableToCopy: 'Imposibil de copiat.', unableToDeleteCount: 'Nu se poate șterge {{count}} din {{total}} {{label}}.', unableToReindexCollection: 'Eroare la reindexarea colecției {{collection}}. Operațiune anulată.', @@ -245,7 +249,9 @@ export const roTranslations: DefaultTranslationsObject = { 'Aceasta va elimina indexurile existente și va reindexa documentele din toate colecțiile.', copied: 'Copiat', copy: 'Copiați', + copyField: 'Copiază câmpul', copying: 'Copiere', + copyRow: 'Copiază rândul', copyWarning: 'Sunteți pe cale să suprascrieți {{to}} cu {{from}} pentru {{label}} {{title}}. Sunteți sigur?', create: 'Creează', @@ -340,6 +346,8 @@ export const roTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Suprascrieți datele existente din câmp', pageNotFound: 'Pagina nu a fost găsită', password: 'Parola', + pasteField: 'Lipește câmpul', + pasteRow: 'Lipește rândul', payloadSettings: 'Setări de Payload', perPage: 'Pe pagină: {{limit}}', previous: 'Anterior', diff --git a/packages/translations/src/languages/rs.ts b/packages/translations/src/languages/rs.ts index d90726c8f..7fa6b100e 100644 --- a/packages/translations/src/languages/rs.ts +++ b/packages/translations/src/languages/rs.ts @@ -91,6 +91,9 @@ export const rsTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Ово поље је невалидно:', followingFieldsInvalid_other: 'Ова поља су невалидна:', incorrectCollection: 'Невалидна колекција', + insufficientClipboardPermissions: + 'Приступ к клипборду је одбијен. Провјерите своја овлашћења за клипборд.', + invalidClipboardData: 'Неважећи подаци у клипборду.', invalidFileType: 'Невалидан тип датотеке', invalidFileTypeValue: 'Невалидан тип датотеке: {{value}}', invalidRequestArgs: 'Неважећи аргументи прослеђени у захтеву: {{args}}', @@ -112,6 +115,7 @@ export const rsTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Постоји проблем при учитавању датотеке.', tokenInvalidOrExpired: 'Токен је невалидан или је истекао.', tokenNotProvided: 'Token nije dostavljen.', + unableToCopy: 'Није могуће копирати.', unableToDeleteCount: 'Није могуће избрисати {{count}} од {{total}} {{label}}.', unableToReindexCollection: 'Грешка при реиндексирању колекције {{collection}}. Операција је прекинута.', @@ -241,7 +245,9 @@ export const rsTranslations: DefaultTranslationsObject = { 'Ovo će ukloniti postojeće indekse i ponovo indeksirati dokumente u svim kolekcijama.', copied: 'Копирано', copy: 'Копирај', + copyField: 'Копирај поље', copying: 'Kopiranje', + copyRow: 'Копирај ред', copyWarning: 'На путу сте да препишете {{to}} са {{from}} за {{label}} {{title}}. Да ли сте сигурни?', create: 'Креирај', @@ -336,6 +342,8 @@ export const rsTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Prepišite postojeće podatke u polju', pageNotFound: 'Страница није пронађена', password: 'Лозинка', + pasteField: 'Залепи поље', + pasteRow: 'Залепи ред', payloadSettings: 'Payload поставке', perPage: 'По страници: {{limit}}', previous: 'Prethodni', diff --git a/packages/translations/src/languages/rsLatin.ts b/packages/translations/src/languages/rsLatin.ts index 096fdf31a..beee30395 100644 --- a/packages/translations/src/languages/rsLatin.ts +++ b/packages/translations/src/languages/rsLatin.ts @@ -91,6 +91,9 @@ export const rsLatinTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Ovo polje je nevalidno:', followingFieldsInvalid_other: 'Ova polja su nevalidna:', incorrectCollection: 'Nevalidna kolekcija', + insufficientClipboardPermissions: + 'Pristup clipboard-u odbijen. Proverite svoja dopuštenja za clipboard.', + invalidClipboardData: 'Nevažeći podaci u clipboard-u.', invalidFileType: 'Nevalidan tip datoteke', invalidFileTypeValue: 'Nevalidan tip datoteke: {{value}}', invalidRequestArgs: 'Nevažeći argumenti prosleđeni u zahtevu: {{args}}', @@ -112,6 +115,7 @@ export const rsLatinTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Postoji problem pri učitavanju datoteke.', tokenInvalidOrExpired: 'Token je nevalidan ili je istekao.', tokenNotProvided: 'Token nije obezbeđen.', + unableToCopy: 'Kopiranje nije moguće.', unableToDeleteCount: 'Nije moguće izbrisati {{count}} od {{total}} {{label}}.', unableToReindexCollection: 'Greška pri reindeksiranju kolekcije {{collection}}. Operacija je prekinuta.', @@ -241,7 +245,9 @@ export const rsLatinTranslations: DefaultTranslationsObject = { 'Ovo će ukloniti postojeće indekse i ponovo indeksirati dokumente u svim kolekcijama.', copied: 'Kopirano', copy: 'Kopiraj', + copyField: 'Kopiraj polje', copying: 'Kopiranje', + copyRow: 'Kopiraj red', copyWarning: 'Na korak ste da prepišete {{to}} sa {{from}} za {{label}} {{title}}. Da li ste sigurni?', create: 'Kreiraj', @@ -336,6 +342,8 @@ export const rsLatinTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Prepiši postojeće podatke iz polja', pageNotFound: 'Stranica nije pronađena', password: 'Lozinka', + pasteField: 'Zalepi polje', + pasteRow: 'Zalepi red', payloadSettings: 'Payload postavke', perPage: 'Po stranici: {{limit}}', previous: 'Prethodni', diff --git a/packages/translations/src/languages/ru.ts b/packages/translations/src/languages/ru.ts index 53aa502e1..ee29c5d40 100644 --- a/packages/translations/src/languages/ru.ts +++ b/packages/translations/src/languages/ru.ts @@ -91,6 +91,9 @@ export const ruTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Следующее поле недействительно:', followingFieldsInvalid_other: 'Следующие поля недействительны:', incorrectCollection: 'Неправильная Коллекция', + insufficientClipboardPermissions: + 'Доступ к буферу обмена отклонен. Проверьте разрешения буфера обмена.', + invalidClipboardData: 'Неверные данные в буфере обмена.', invalidFileType: 'Недопустимый тип файла', invalidFileTypeValue: 'Недопустимый тип файла: {{value}}', invalidRequestArgs: 'В запрос переданы недопустимые аргументы: {{args}}', @@ -112,6 +115,7 @@ export const ruTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Возникла проблема при загрузке файла.', tokenInvalidOrExpired: 'Токен либо недействителен, либо срок его действия истек.', tokenNotProvided: 'Токен не предоставлен.', + unableToCopy: 'Не удалось скопировать.', unableToDeleteCount: 'Не удалось удалить {{count}} из {{total}} {{label}}.', unableToReindexCollection: 'Ошибка при переиндексации коллекции {{collection}}. Операция прервана.', @@ -243,7 +247,9 @@ export const ruTranslations: DefaultTranslationsObject = { 'Это удалит существующие индексы и переиндексирует документы во всех коллекциях.', copied: 'Скопировано', copy: 'Скопировать', + copyField: 'Копировать поле', copying: 'Копирование', + copyRow: 'Копировать строку', copyWarning: 'Вы собираетесь перезаписать {{to}} на {{from}} для {{label}} {{title}}. Вы уверены?', create: 'Создать', @@ -338,6 +344,8 @@ export const ruTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Перезаписать существующие данные поля', pageNotFound: 'Страница не найдена', password: 'Пароль', + pasteField: 'Вставить поле', + pasteRow: 'Вставить строку', payloadSettings: 'Настройки Payload', perPage: 'На странице: {{limit}}', previous: 'Предыдущий', diff --git a/packages/translations/src/languages/sk.ts b/packages/translations/src/languages/sk.ts index f0dd04d0d..f4479b174 100644 --- a/packages/translations/src/languages/sk.ts +++ b/packages/translations/src/languages/sk.ts @@ -91,6 +91,9 @@ export const skTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Nasledujúce pole je neplatné:', followingFieldsInvalid_other: 'Nasledujúce polia sú neplatné:', incorrectCollection: 'Nesprávna kolekcia', + insufficientClipboardPermissions: + 'Prístup do schránky bol zamietnutý. Skontrolujte svoje oprávnenia pre schránku.', + invalidClipboardData: 'Neplatné dáta v schránke.', invalidFileType: 'Neplatný typ súboru', invalidFileTypeValue: 'Neplatný typ súboru: {{value}}', invalidRequestArgs: 'Neplatné argumenty odoslané v požiadavke: {{args}}', @@ -112,6 +115,7 @@ export const skTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Pri nahrávaní súboru došlo k chybe.', tokenInvalidOrExpired: 'Token je neplatný alebo vypršal.', tokenNotProvided: 'Token nie je poskytnutý.', + unableToCopy: 'Kopírovanie nie je možné.', unableToDeleteCount: 'Nie je možné zmazať {{count}} z {{total}} {{label}}.', unableToReindexCollection: 'Chyba pri reindexácii kolekcie {{collection}}. Operácia bola prerušená.', @@ -243,7 +247,9 @@ export const skTranslations: DefaultTranslationsObject = { 'Týmto sa odstránia existujúce indexy a znova sa zaindexujú dokumenty vo všetkých kolekciách.', copied: 'Skopírované', copy: 'Kopírovať', + copyField: 'Kopírovať pole', copying: 'Kopírovanie', + copyRow: 'Kopírovať riadok', copyWarning: 'Chystáte sa prepísať {{to}} na {{from}} pre {{label}} {{title}}. Ste si istý?', create: 'Vytvoriť', created: 'Vytvořeno', @@ -336,6 +342,8 @@ export const skTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Prepísať existujúce pole dát', pageNotFound: 'Stránka nenájdená', password: 'Heslo', + pasteField: 'Prilepiť pole', + pasteRow: 'Prilepiť riadok', payloadSettings: 'Nastavenia dátového záznamu', perPage: 'Na stránku: {{limit}}', previous: 'Predchádzajúci', diff --git a/packages/translations/src/languages/sl.ts b/packages/translations/src/languages/sl.ts index 5f388f0c1..f424587ff 100644 --- a/packages/translations/src/languages/sl.ts +++ b/packages/translations/src/languages/sl.ts @@ -90,6 +90,9 @@ export const slTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Naslednje polje je neveljavno:', followingFieldsInvalid_other: 'Naslednja polja so neveljavna:', incorrectCollection: 'Napačna zbirka', + insufficientClipboardPermissions: + 'Dostop do odložišča je bil zavrnjen. Preverite dovoljenja za odložišče.', + invalidClipboardData: 'Neveljavni podatki v odložišču.', invalidFileType: 'Neveljaven tip datoteke', invalidFileTypeValue: 'Neveljaven tip datoteke: {{value}}', invalidRequestArgs: 'V zahtevi so bili poslani neveljavni argumenti: {{args}}', @@ -111,6 +114,7 @@ export const slTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Pri nalaganju datoteke je prišlo do težave.', tokenInvalidOrExpired: 'Žeton je neveljaven ali je potekel.', tokenNotProvided: 'Žeton ni bil posredovan.', + unableToCopy: 'Kopiranje ni mogoče.', unableToDeleteCount: 'Ni bilo mogoče izbrisati {{count}} od {{total}} {{label}}.', unableToReindexCollection: 'Napaka pri reindeksiranju zbirke {{collection}}. Operacija je bila prekinjena.', @@ -241,7 +245,9 @@ export const slTranslations: DefaultTranslationsObject = { 'To bo odstranilo obstoječe indekse in ponovno indeksiralo dokumente v vseh zbirkah.', copied: 'Kopirano', copy: 'Kopiraj', + copyField: 'Kopiraj polje', copying: 'Kopiranje', + copyRow: 'Kopiraj vrstico', copyWarning: 'Prepisali boste {{to}} z {{from}} za {{label}} {{title}}. Ste prepričani?', create: 'Ustvari', created: 'Ustvarjeno', @@ -335,6 +341,8 @@ export const slTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Prepišite obstoječe podatke polja', pageNotFound: 'Stran ni najdena', password: 'Geslo', + pasteField: 'Prilepi polje', + pasteRow: 'Prilepi vrstico', payloadSettings: 'Nastavitve Payloada', perPage: 'Na stran: {{limit}}', previous: 'Prejšnji', diff --git a/packages/translations/src/languages/sv.ts b/packages/translations/src/languages/sv.ts index 1eec3056a..5fcdcb719 100644 --- a/packages/translations/src/languages/sv.ts +++ b/packages/translations/src/languages/sv.ts @@ -90,6 +90,9 @@ export const svTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Följande fält är ogiltigt:', followingFieldsInvalid_other: 'Följande fält är ogiltiga:', incorrectCollection: 'Felaktig samling', + insufficientClipboardPermissions: + 'Åtkomst till urklipp nekades. Kontrollera dina behörigheter för urklipp.', + invalidClipboardData: 'Ogiltiga urklippsdata.', invalidFileType: 'Ogiltig filtyp', invalidFileTypeValue: 'Ogiltig filtyp: {{value}}', invalidRequestArgs: 'Ogiltiga argument har skickats i begäran: {{args}}', @@ -111,6 +114,7 @@ export const svTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Det uppstod ett problem när filen laddades upp.', tokenInvalidOrExpired: 'Token är antingen ogiltig eller har löpt ut.', tokenNotProvided: 'Token inte tillhandahållet.', + unableToCopy: 'Kan inte kopiera.', unableToDeleteCount: 'Det gick inte att ta bort {{count}} av {{total}} {{label}}.', unableToReindexCollection: 'Fel vid omindexering av samlingen {{collection}}. Operationen avbröts.', @@ -241,7 +245,9 @@ export const svTranslations: DefaultTranslationsObject = { 'Detta kommer att ta bort befintliga index och omindexera dokumenten i alla samlingar.', copied: 'Kopierad', copy: 'Kopiera', + copyField: 'Kopiera fält', copying: 'Kopierar...', + copyRow: 'Kopiera rad', copyWarning: 'Du håller på att skriva över {{to}} med {{from}} för {{label}} {{title}}. Är du säker?', create: 'Skapa', @@ -336,6 +342,8 @@ export const svTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Skriv över befintlig fältdatabas', pageNotFound: 'Sidan hittas inte', password: 'Lösenord', + pasteField: 'Klistra in fält', + pasteRow: 'Klistra in rad', payloadSettings: 'Programinställningar', perPage: 'Per Sida: {{limit}}', previous: 'Föregående', diff --git a/packages/translations/src/languages/th.ts b/packages/translations/src/languages/th.ts index a1855b951..9a39e8f1e 100644 --- a/packages/translations/src/languages/th.ts +++ b/packages/translations/src/languages/th.ts @@ -88,6 +88,9 @@ export const thTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'ช่องต่อไปนี้ไม่ถูกต้อง:', followingFieldsInvalid_other: 'ช่องต่อไปนี้ไม่ถูกต้อง:', incorrectCollection: 'Collection ไม่ถูกต้อง', + insufficientClipboardPermissions: + 'การเข้าถึงคลิปบอร์ดถูกปฏิเสธ กรุณาตรวจสอบสิทธิ์การเข้าถึงคลิปบอร์ดของคุณ', + invalidClipboardData: 'ข้อมูลคลิปบอร์ดไม่ถูกต้อง', invalidFileType: 'ประเภทของไฟล์ไม่ถูกต้อง', invalidFileTypeValue: 'ประเภทของไฟล์ไม่ถูกต้อง: {{value}}', invalidRequestArgs: 'มีการส่งอาร์กิวเมนต์ที่ไม่ถูกต้องในคำขอ: {{args}}', @@ -109,6 +112,7 @@ export const thTranslations: DefaultTranslationsObject = { problemUploadingFile: 'เกิดปัญหาระหว่างการอัปโหลดไฟล์', tokenInvalidOrExpired: 'Token ไม่ถูกต้องหรือหมดอายุ', tokenNotProvided: 'ไม่ได้รับโทเค็น', + unableToCopy: 'ไม่สามารถคัดลอกได้', unableToDeleteCount: 'ไม่สามารถลบ {{count}} จาก {{total}} {{label}}', unableToReindexCollection: 'เกิดข้อผิดพลาดในการจัดทำดัชนีใหม่ของคอลเลกชัน {{collection}}. การดำเนินการถูกยกเลิก', @@ -236,7 +240,9 @@ export const thTranslations: DefaultTranslationsObject = { 'การดำเนินการนี้จะลบดัชนีที่มีอยู่และทำการจัดทำดัชนีใหม่ในเอกสารของทุกคอลเลกชัน.', copied: 'คัดลอกแล้ว', copy: 'คัดลอก', + copyField: 'คัดลอกฟิลด์', copying: 'การคัดลอก', + copyRow: 'คัดลอกแถว', copyWarning: 'คุณกำลังจะเขียนทับ {{to}} ด้วย {{from}} สำหรับ {{label}} {{title}}. คุณแน่ใจหรือไม่?', create: 'สร้าง', @@ -330,6 +336,8 @@ export const thTranslations: DefaultTranslationsObject = { overwriteExistingData: 'เขียนทับข้อมูลในฟิลด์ที่มีอยู่แล้ว', pageNotFound: 'ไม่พบหน้าที่ต้องการ', password: 'รหัสผ่าน', + pasteField: 'วางฟิลด์', + pasteRow: 'วางแถว', payloadSettings: 'การตั้งค่า Payload', perPage: 'จำนวนต่อหน้า: {{limit}}', previous: 'ก่อนหน้านี้', diff --git a/packages/translations/src/languages/tr.ts b/packages/translations/src/languages/tr.ts index b05c8b212..57375f000 100644 --- a/packages/translations/src/languages/tr.ts +++ b/packages/translations/src/languages/tr.ts @@ -91,6 +91,9 @@ export const trTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Lütfen geçersiz alanı düzeltin:', followingFieldsInvalid_other: 'Lütfen geçersiz alanları düzeltin:', incorrectCollection: 'Hatalı koleksiyon', + insufficientClipboardPermissions: + 'Pano erişim reddedildi. Lütfen pano izinlerinizi kontrol edin.', + invalidClipboardData: 'Geçersiz pano verisi.', invalidFileType: 'Geçersiz dosya türü', invalidFileTypeValue: 'Geçersiz dosya türü: {{value}}', invalidRequestArgs: 'İstek içerisinde geçersiz argümanlar iletildi: {{args}}', @@ -112,6 +115,7 @@ export const trTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Dosya yüklenirken bir sorun oluştu.', tokenInvalidOrExpired: 'Geçersiz veya süresi dolmuş token.', tokenNotProvided: 'Jeton sağlanmadı.', + unableToCopy: 'Kopyalanamıyor.', unableToDeleteCount: '{{total}} {{label}} içinden {{count}} silinemiyor.', unableToReindexCollection: '{{collection}} koleksiyonunun yeniden indekslenmesinde hata oluştu. İşlem durduruldu.', @@ -244,7 +248,9 @@ export const trTranslations: DefaultTranslationsObject = { 'Bu işlem mevcut dizinleri kaldıracak ve tüm koleksiyonlardaki belgeleri yeniden dizine alacaktır.', copied: 'Kopyalandı', copy: 'Kopyala', + copyField: 'Alanı kopyala', copying: 'Kopyalama', + copyRow: 'Satırı kopyala', copyWarning: "{{to}}'yu {{from}} ile {{label}} {{title}} için üstüne yazmak üzeresiniz. Emin misiniz?", create: 'Oluştur', @@ -339,6 +345,8 @@ export const trTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Mevcut alan verilerinin üzerine yazın', pageNotFound: 'Sayfa bulunamadı', password: 'Parola', + pasteField: 'Alanı yapıştır', + pasteRow: 'Satırı yapıştır', payloadSettings: 'Ayarlar', perPage: 'Sayfa başına: {{limit}}', previous: 'Önceki', diff --git a/packages/translations/src/languages/uk.ts b/packages/translations/src/languages/uk.ts index 9864931d4..c1f96e477 100644 --- a/packages/translations/src/languages/uk.ts +++ b/packages/translations/src/languages/uk.ts @@ -91,6 +91,9 @@ export const ukTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Наступне поле невірне:', followingFieldsInvalid_other: 'Наступні поля невірні', incorrectCollection: 'Неправильна колекція', + insufficientClipboardPermissions: + 'Доступ до буфера обміну відхилено. Перевірте свої дозволи на буфер обміну.', + invalidClipboardData: 'Невірні дані в буфері обміну.', invalidFileType: 'Невірний тип файлу', invalidFileTypeValue: 'Невірний тип файлу: {{value}}', invalidRequestArgs: 'Неправильні аргументи передано в запиті: {{args}}', @@ -112,6 +115,7 @@ export const ukTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Виникла помилка під час завантаження файлу.', tokenInvalidOrExpired: 'Токен недійсний, або його строк дії закінчився.', tokenNotProvided: 'Токен не надано.', + unableToCopy: 'Неможливо скопіювати.', unableToDeleteCount: 'Не вдалося видалити {{count}} із {{total}} {{label}}.', unableToReindexCollection: 'Помилка при повторному індексуванні колекції {{collection}}. Операцію скасовано.', @@ -241,7 +245,9 @@ export const ukTranslations: DefaultTranslationsObject = { 'Це видалить наявні індекси та перебудує індекси документів у всіх колекціях.', copied: 'Скопійовано', copy: 'Скопіювати', + copyField: 'Копіювати поле', copying: 'Копіювання', + copyRow: 'Копіювати рядок', copyWarning: 'Ви збираєтесь замінити {{to}} на {{from}} для {{label}} {{title}}. Ви впевнені?', create: 'Створити', created: 'Створено', @@ -335,6 +341,8 @@ export const ukTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Перезаписати існуючі дані поля', pageNotFound: 'Сторінка не знайдена', password: 'Пароль', + pasteField: 'Вставити поле', + pasteRow: 'Вставити рядок', payloadSettings: 'Налаштування Payload', perPage: 'На сторінці: {{limit}}', previous: 'Попередній', diff --git a/packages/translations/src/languages/vi.ts b/packages/translations/src/languages/vi.ts index 2c59447e7..d235a7de0 100644 --- a/packages/translations/src/languages/vi.ts +++ b/packages/translations/src/languages/vi.ts @@ -90,6 +90,9 @@ export const viTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: 'Lỗi - Field sau không hợp lệ:', followingFieldsInvalid_other: 'Lỗi - Những fields sau không hợp lệ:', incorrectCollection: 'Lỗi - Collection không hợp lệ.', + insufficientClipboardPermissions: + 'Truy cập vào bộ nhớ tạm bị từ chối. Vui lòng kiểm tra quyền truy cập bộ nhớ tạm của bạn.', + invalidClipboardData: 'Dữ liệu bộ nhớ tạm không hợp lệ.', invalidFileType: 'Lỗi - Định dạng tệp không hợp lệ.', invalidFileTypeValue: 'Lỗi - Định dạng tệp không hợp lệ: {{value}}.', invalidRequestArgs: 'Các đối số không hợp lệ đã được truyền trong yêu cầu: {{args}}', @@ -111,6 +114,7 @@ export const viTranslations: DefaultTranslationsObject = { problemUploadingFile: 'Lỗi - Đã xảy ra vấn để khi tải lên file sau.', tokenInvalidOrExpired: 'Lỗi - Token không hợp lệ hoặc đã hết hạn.', tokenNotProvided: 'Không cung cấp mã thông báo.', + unableToCopy: 'Không thể sao chép.', unableToDeleteCount: 'Không thể xóa {{count}} trong số {{total}} {{label}}.', unableToReindexCollection: 'Lỗi khi tái lập chỉ mục bộ sưu tập {{collection}}. Quá trình bị hủy.', @@ -240,7 +244,9 @@ export const viTranslations: DefaultTranslationsObject = { 'Điều này sẽ xóa các chỉ mục hiện tại và tái lập chỉ mục các tài liệu trong tất cả các bộ sưu tập.', copied: 'Đâ sao chép', copy: 'Sao chép', + copyField: 'Sao chép trường', copying: 'Sao chép', + copyRow: 'Sao chép dòng', copyWarning: 'Bạn đang chuẩn bị ghi đè {{to}} bằng {{from}} cho {{label}} {{title}}. Bạn có chắc chắn không?', create: 'Tạo', @@ -335,6 +341,8 @@ export const viTranslations: DefaultTranslationsObject = { overwriteExistingData: 'Ghi đè dữ liệu trường hiện tại', pageNotFound: 'Không tìm thấy trang', password: 'Mật khẩu', + pasteField: 'Dán trường', + pasteRow: 'Dán dòng', payloadSettings: 'Cài đặt', perPage: 'Hiển thị mỗi trang: {{limit}}', previous: 'Trước đó', diff --git a/packages/translations/src/languages/zh.ts b/packages/translations/src/languages/zh.ts index a654befe0..c8a4b04e1 100644 --- a/packages/translations/src/languages/zh.ts +++ b/packages/translations/src/languages/zh.ts @@ -86,6 +86,8 @@ export const zhTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: '下面的字段是无效的:', followingFieldsInvalid_other: '以下字段是无效的:', incorrectCollection: '不正确的集合', + insufficientClipboardPermissions: '剪贴板访问被拒绝。请检查您的剪贴板权限。', + invalidClipboardData: '剪贴板数据无效。', invalidFileType: '无效的文件类型', invalidFileTypeValue: '无效的文件类型: {{value}}', invalidRequestArgs: '请求中传递了无效的参数:{{args}}', @@ -107,6 +109,7 @@ export const zhTranslations: DefaultTranslationsObject = { problemUploadingFile: '上传文件时出现了问题。', tokenInvalidOrExpired: '令牌无效或已过期。', tokenNotProvided: '未提供令牌。', + unableToCopy: '无法复制。', unableToDeleteCount: '无法从 {{total}} {{label}} 中删除 {{count}}。', unableToReindexCollection: '重新索引集合 {{collection}} 时出错。操作已中止。', unableToUpdateCount: '无法更新 {{count}} 个,共 {{total}} 个 {{label}}。', @@ -229,7 +232,9 @@ export const zhTranslations: DefaultTranslationsObject = { confirmReindexDescriptionAll: '此操作将删除现有索引,并重新索引所有集合中的文档。', copied: '已复制', copy: '复制', + copyField: '复制字段', copying: '复制中', + copyRow: '复制行', copyWarning: '您即将用{{from}}覆盖{{to}},用于{{label}} {{title}}。您确定吗?', create: '创建', created: '已创建', @@ -321,6 +326,8 @@ export const zhTranslations: DefaultTranslationsObject = { overwriteExistingData: '覆盖现有字段数据', pageNotFound: '未找到页面', password: '密码', + pasteField: '粘贴字段', + pasteRow: '粘贴行', payloadSettings: 'Payload设置', perPage: '每一页: {{limit}}', previous: '前一个', diff --git a/packages/translations/src/languages/zhTw.ts b/packages/translations/src/languages/zhTw.ts index f23ff13a1..2512bb091 100644 --- a/packages/translations/src/languages/zhTw.ts +++ b/packages/translations/src/languages/zhTw.ts @@ -86,6 +86,8 @@ export const zhTwTranslations: DefaultTranslationsObject = { followingFieldsInvalid_one: '下面的字串是無效的:', followingFieldsInvalid_other: '以下字串是無效的:', incorrectCollection: '不正確的集合', + insufficientClipboardPermissions: '剪貼簿訪問被拒絕。請檢查您的剪貼簿權限。', + invalidClipboardData: '剪貼簿資料無效。', invalidFileType: '無效的文件類型', invalidFileTypeValue: '無效的文件類型: {{value}}', invalidRequestArgs: '請求中傳遞了無效的參數:{{args}}', @@ -107,6 +109,7 @@ export const zhTwTranslations: DefaultTranslationsObject = { problemUploadingFile: '上傳文件時出現了問題。', tokenInvalidOrExpired: '令牌無效或已過期。', tokenNotProvided: '未提供令牌。', + unableToCopy: '無法複製。', unableToDeleteCount: '無法從 {{total}} 個中刪除 {{count}} 個 {{label}}。', unableToReindexCollection: '重新索引集合 {{collection}} 時出現錯誤。操作已中止。', unableToUpdateCount: '無法從 {{total}} 個中更新 {{count}} 個 {{label}}。', @@ -229,7 +232,9 @@ export const zhTwTranslations: DefaultTranslationsObject = { confirmReindexDescriptionAll: '此操作將刪除現有索引並重新索引所有集合中的文件。', copied: '已複製', copy: '複製', + copyField: '複製欄位', copying: '複製', + copyRow: '複製列', copyWarning: '您即將以{{from}}覆蓋{{to}},這將影響{{label}} {{title}}。您確定要這麼做嗎?', create: '建立', created: '已建立', @@ -321,6 +326,8 @@ export const zhTwTranslations: DefaultTranslationsObject = { overwriteExistingData: '覆蓋現有欄位資料', pageNotFound: '未找到頁面', password: '密碼', + pasteField: '貼上欄位', + pasteRow: '貼上列', payloadSettings: 'Payload設定', perPage: '每一頁: {{limit}} 個', previous: '先前的', diff --git a/packages/ui/src/elements/ArrayAction/index.tsx b/packages/ui/src/elements/ArrayAction/index.tsx index 60d5986eb..81e778bb0 100644 --- a/packages/ui/src/elements/ArrayAction/index.tsx +++ b/packages/ui/src/elements/ArrayAction/index.tsx @@ -7,29 +7,34 @@ import { MoreIcon } from '../../icons/More/index.js' import { PlusIcon } from '../../icons/Plus/index.js' import { XIcon } from '../../icons/X/index.js' import { useTranslation } from '../../providers/Translation/index.js' -import { Popup, PopupList } from '../Popup/index.js' +import { ClipboardActionLabel } from '../ClipboardAction/ClipboardActionLabel.js' import './index.scss' +import { Popup, PopupList } from '../Popup/index.js' const baseClass = 'array-actions' export type Props = { addRow: (current: number, blockType?: string) => Promise | void + copyRow: (index: number) => void duplicateRow: (current: number) => void hasMaxRows: boolean index: number isSortable?: boolean moveRow: (from: number, to: number) => void + pasteRow: (index: number) => void removeRow: (index: number) => void rowCount: number } export const ArrayAction: React.FC = ({ addRow, + copyRow, duplicateRow, hasMaxRows, index, isSortable, moveRow, + pasteRow, removeRow, rowCount, }) => { @@ -96,6 +101,24 @@ export const ArrayAction: React.FC = ({ )} + { + copyRow(index) + close() + }} + > + + + { + pasteRow(index) + close() + }} + > + + { diff --git a/packages/ui/src/elements/ClipboardAction/ClipboardActionLabel.tsx b/packages/ui/src/elements/ClipboardAction/ClipboardActionLabel.tsx new file mode 100644 index 000000000..5a01f4ca6 --- /dev/null +++ b/packages/ui/src/elements/ClipboardAction/ClipboardActionLabel.tsx @@ -0,0 +1,32 @@ +'use client' + +import { Fragment } from 'react' + +import { CopyIcon } from '../../icons/Copy/index.js' +import { EditIcon } from '../../icons/Edit/index.js' +import { useTranslation } from '../../providers/Translation/index.js' + +export const ClipboardActionLabel = ({ + isPaste, + isRow, +}: { + isPaste?: boolean + isRow?: boolean +}) => { + const { t } = useTranslation() + + let label = t('general:copyField') + if (!isRow && isPaste) { + label = t('general:pasteField') + } else if (isRow && !isPaste) { + label = t('general:copyRow') + } else if (isRow && isPaste) { + label = t('general:pasteRow') + } + + return ( + + {isPaste ? : } {label} + + ) +} diff --git a/packages/ui/src/elements/ClipboardAction/clipboardUtilities.ts b/packages/ui/src/elements/ClipboardAction/clipboardUtilities.ts new file mode 100644 index 000000000..3d9915af3 --- /dev/null +++ b/packages/ui/src/elements/ClipboardAction/clipboardUtilities.ts @@ -0,0 +1,67 @@ +import type { + ClipboardCopyActionArgs, + ClipboardPasteActionArgs, + ClipboardPasteActionValidateArgs, + ClipboardPasteData, +} from './types.js' + +import { isClipboardDataValid } from './isClipboardDataValid.js' + +const localStorageClipboardKey = '_payloadClipboard' + +/** + * @note This function doesn't use the Clipboard API, but localStorage. See rationale in #11513 + */ +export function clipboardCopy(args: ClipboardCopyActionArgs): string | true { + const { getDataToCopy, t, ...rest } = args + + const dataToWrite = { + data: getDataToCopy(), + ...rest, + } + + try { + localStorage.setItem(localStorageClipboardKey, JSON.stringify(dataToWrite)) + return true + } catch (_err) { + return t('error:unableToCopy') + } +} + +/** + * @note This function doesn't use the Clipboard API, but localStorage. See rationale in #11513 + */ +export function clipboardPaste({ + onPaste, + path: fieldPath, + t, + ...args +}: ClipboardPasteActionArgs): string | true { + let dataToPaste: ClipboardPasteData + + try { + const jsonFromClipboard = localStorage.getItem(localStorageClipboardKey) + + if (!jsonFromClipboard) { + return t('error:invalidClipboardData') + } + + dataToPaste = JSON.parse(jsonFromClipboard) + } catch (_err) { + return t('error:invalidClipboardData') + } + + const dataToValidate = { + ...dataToPaste, + ...args, + fieldPath, + } as ClipboardPasteActionValidateArgs + + if (!isClipboardDataValid(dataToValidate)) { + return t('error:invalidClipboardData') + } + + onPaste(dataToPaste) + + return true +} diff --git a/packages/ui/src/elements/ClipboardAction/index.tsx b/packages/ui/src/elements/ClipboardAction/index.tsx new file mode 100644 index 000000000..8a1d531d8 --- /dev/null +++ b/packages/ui/src/elements/ClipboardAction/index.tsx @@ -0,0 +1,117 @@ +'use client' + +import type { FormStateWithoutComponents } from 'payload' + +import { type FC, useCallback } from 'react' +import { toast } from 'sonner' + +import type { ClipboardCopyData, OnPasteFn } from './types.js' + +import { MoreIcon } from '../../icons/More/index.js' +import { useTranslation } from '../../providers/Translation/index.js' +import { Popup, PopupList } from '../Popup/index.js' +import { ClipboardActionLabel } from './ClipboardActionLabel.js' +import { clipboardCopy, clipboardPaste } from './clipboardUtilities.js' + +const baseClass = 'clipboard-action' + +type Props = { + className?: string + copyClassName?: string + disableCopy?: boolean + disablePaste?: boolean + getDataToCopy: () => FormStateWithoutComponents + isRow?: boolean + onPaste: OnPasteFn + pasteClassName?: string +} & ClipboardCopyData + +/** + * Menu actions for copying and pasting fields. Currently, this is only used in Arrays and Blocks. + * @note This component doesn't use the Clipboard API, but localStorage. See rationale in #11513 + */ +export const ClipboardAction: FC = ({ + className, + copyClassName, + disableCopy, + disablePaste, + isRow, + onPaste, + pasteClassName, + path, + ...rest +}) => { + const { t } = useTranslation() + + const classes = [`${baseClass}__popup`, className].filter(Boolean).join(' ') + + const handleCopy = useCallback(() => { + const clipboardResult = clipboardCopy({ + path, + t, + ...rest, + }) + + if (typeof clipboardResult === 'string') { + toast.error(clipboardResult) + } else { + toast.success(t('general:copied')) + } + }, [t, rest, path]) + + const handlePaste = useCallback(() => { + const clipboardResult = clipboardPaste( + rest.type === 'array' + ? { + onPaste, + path, + schemaFields: rest.fields, + t, + } + : { + onPaste, + path, + schemaBlocks: rest.blocks, + t, + }, + ) + + if (typeof clipboardResult === 'string') { + toast.error(clipboardResult) + } + }, [onPaste, rest, path, t]) + + return ( + } + className={classes} + horizontalAlign="center" + render={({ close }) => ( + + { + void handleCopy() + close() + }} + > + + + { + void handlePaste() + close() + }} + > + + + + )} + size="large" + verticalAlign="bottom" + /> + ) +} diff --git a/packages/ui/src/elements/ClipboardAction/isClipboardDataValid.ts b/packages/ui/src/elements/ClipboardAction/isClipboardDataValid.ts new file mode 100644 index 000000000..1ad948943 --- /dev/null +++ b/packages/ui/src/elements/ClipboardAction/isClipboardDataValid.ts @@ -0,0 +1,109 @@ +import type { ClientBlock, ClientField } from 'payload' + +import { fieldAffectsData, fieldHasSubFields } from 'payload/shared' + +import type { ClipboardPasteActionValidateArgs } from './types.js' + +/** + * Validates whether clipboard data is compatible with the target schema. + * For this to be true, the copied field and the target to be pasted must + * be structurally equivalent (same schema) + * + * @returns True if the clipboard data is valid and can be pasted, false otherwise + */ +export function isClipboardDataValid({ data, path, ...args }: ClipboardPasteActionValidateArgs) { + if (typeof data === 'undefined' || !path || !args.type) { + return false + } + + if (args.type === 'blocks') { + return isClipboardBlocksValid({ + blocksFromClipboard: args.blocks, + blocksFromConfig: args.schemaBlocks, + }) + } else { + return isClipboardFieldsValid({ + fieldsFromClipboard: args.fields, + fieldsFromConfig: args.schemaFields, + }) + } +} + +function isClipboardFieldsValid({ + fieldsFromClipboard, + fieldsFromConfig, +}: { + fieldsFromClipboard: ClientField[] + fieldsFromConfig?: ClientField[] +}): boolean { + if (!fieldsFromConfig || fieldsFromClipboard.length !== fieldsFromConfig?.length) { + return false + } + + return fieldsFromClipboard.every((clipboardField, i) => { + const configField = fieldsFromConfig[i] + + if (clipboardField.type !== configField.type) { + return false + } + + const affectsData = fieldAffectsData(clipboardField) && fieldAffectsData(configField) + if (affectsData && clipboardField.name !== configField.name) { + return false + } + + const hasNestedFieldsConfig = fieldHasSubFields(configField) + const hasNestedFieldsClipboard = fieldHasSubFields(clipboardField) + if (hasNestedFieldsClipboard !== hasNestedFieldsConfig) { + return false + } + + if (hasNestedFieldsClipboard && hasNestedFieldsConfig) { + return isClipboardFieldsValid({ + fieldsFromClipboard: clipboardField.fields, + fieldsFromConfig: configField.fields, + }) + } + + return true + }) +} + +function isClipboardBlocksValid({ + blocksFromClipboard, + blocksFromConfig, +}: { + blocksFromClipboard: ClientBlock[] + blocksFromConfig?: ClientBlock[] +}) { + const configBlockMap = new Map(blocksFromConfig?.map((block) => [block.slug, block])) + + if (!configBlockMap.size) { + return false + } + + const checkedSlugs = new Set() + + for (const currBlock of blocksFromClipboard) { + const currSlug = currBlock.slug + + if (!checkedSlugs.has(currSlug)) { + const configBlock = configBlockMap.get(currSlug) + if (!configBlock) { + return false + } + + if ( + !isClipboardFieldsValid({ + fieldsFromClipboard: currBlock.fields, + fieldsFromConfig: configBlock.fields, + }) + ) { + return false + } + + checkedSlugs.add(currSlug) + } + } + return true +} diff --git a/packages/ui/src/elements/ClipboardAction/mergeFormStateFromClipboard.ts b/packages/ui/src/elements/ClipboardAction/mergeFormStateFromClipboard.ts new file mode 100644 index 000000000..5a8d01c1f --- /dev/null +++ b/packages/ui/src/elements/ClipboardAction/mergeFormStateFromClipboard.ts @@ -0,0 +1,131 @@ +import type { FieldState, FormState } from 'payload' + +import type { ClipboardPasteData } from './types.js' + +export function reduceFormStateByPath({ + formState, + path, + rowIndex, +}: { + formState: FormState + path: string + rowIndex?: number +}) { + const filteredState: Record = {} + const prefix = typeof rowIndex !== 'number' ? path : `${path}.${rowIndex}` + + for (const key in formState) { + if (!key.startsWith(prefix)) { + continue + } + + const { customComponents: _, validate: __, ...field } = formState[key] + + if (Array.isArray(field.rows)) { + field.rows = field.rows.map((row) => { + if (!row || typeof row !== 'object') { + return row + } + const { customComponents: _, ...serializableRow } = row + return serializableRow + }) + } + + filteredState[key] = field + } + + return filteredState +} + +export function mergeFormStateFromClipboard({ + dataFromClipboard: clipboardData, + formState, + path, + rowIndex, +}: { + dataFromClipboard: ClipboardPasteData + formState: FormState + path: string + rowIndex?: number +}) { + const { + type: typeFromClipboard, + data: dataFromClipboard, + path: pathFromClipboard, + rowIndex: rowIndexFromClipboard, + } = clipboardData + + const copyFromField = typeof rowIndexFromClipboard !== 'number' + const pasteIntoField = typeof rowIndex !== 'number' + const fromRowToField = !copyFromField && pasteIntoField + const isArray = typeFromClipboard === 'array' + + let pathToReplace: string + if (copyFromField && pasteIntoField) { + pathToReplace = pathFromClipboard + } else if (copyFromField) { + pathToReplace = `${pathFromClipboard}.${rowIndex}` + } else { + pathToReplace = `${pathFromClipboard}.${rowIndexFromClipboard}` + } + + let targetSegment: string + if (!pasteIntoField) { + targetSegment = `${path}.${rowIndex}` + } else if (fromRowToField) { + targetSegment = `${path}.0` + } else { + targetSegment = path + } + + if (fromRowToField) { + const lastRenderedPath = `${path}.0` + const rowIDFromClipboard = dataFromClipboard[`${pathToReplace}.id`].value as string + const hasRows = formState[path].rows?.length + + formState[path].rows = [ + { + ...(hasRows && isArray ? formState[path].rows[0] : {}), + id: rowIDFromClipboard, + isLoading: false, + lastRenderedPath, + }, + ] + formState[path].value = 1 + formState[path].initialValue = 1 + formState[path].disableFormData = true + + for (const fieldPath in formState) { + if ( + fieldPath !== path && + !fieldPath.startsWith(lastRenderedPath) && + fieldPath.startsWith(path) + ) { + delete formState[fieldPath] + } + } + } + + for (const clipboardPath in dataFromClipboard) { + // Pasting a row id, skip overwriting + if ( + (!pasteIntoField && clipboardPath.endsWith('.id')) || + !clipboardPath.startsWith(pathToReplace) + ) { + continue + } + + const newPath = clipboardPath.replace(pathToReplace, targetSegment) + + const customComponents = isArray ? formState[newPath]?.customComponents : undefined + const validate = isArray ? formState[newPath]?.validate : undefined + + formState[newPath] = { + customComponents, + validate, + ...dataFromClipboard[clipboardPath], + } + } + + return formState +} diff --git a/packages/ui/src/elements/ClipboardAction/types.ts b/packages/ui/src/elements/ClipboardAction/types.ts new file mode 100644 index 000000000..e0e6a0367 --- /dev/null +++ b/packages/ui/src/elements/ClipboardAction/types.ts @@ -0,0 +1,58 @@ +import type { TFunction } from '@payloadcms/translations' +import type { ClientBlock, ClientField, FormStateWithoutComponents } from 'payload' + +export type ClipboardCopyBlocksSchema = { + schemaBlocks: ClientBlock[] +} + +export type ClipboardCopyBlocksData = { + blocks: ClientBlock[] + type: 'blocks' +} + +export type ClipboardCopyFieldsSchema = { + schemaFields: ClientField[] +} + +export type ClipboardCopyFieldsData = { + fields: ClientField[] + type: 'array' +} + +export type ClipboardCopyData = { + path: string + rowIndex?: number +} & (ClipboardCopyBlocksData | ClipboardCopyFieldsData) + +export type ClipboardCopyActionArgs = { + getDataToCopy: () => FormStateWithoutComponents + t: TFunction +} & ClipboardCopyData + +export type ClipboardPasteData = { + data: FormStateWithoutComponents + path: string + rowIndex?: number +} & (ClipboardCopyBlocksData | ClipboardCopyFieldsData) + +export type OnPasteFn = (data: ClipboardPasteData) => void + +export type ClipboardPasteActionArgs = { + onPaste: OnPasteFn + path: string + t: TFunction +} & (ClipboardCopyBlocksSchema | ClipboardCopyFieldsSchema) + +export type ClipboardPasteActionValidateArgs = { + fieldPath: string +} & ( + | { + schemaBlocks: ClientBlock[] + type: 'blocks' + } + | { + schemaFields: ClientField[] + type: 'array' + } +) & + ClipboardPasteData diff --git a/packages/ui/src/fields/Array/ArrayRow.tsx b/packages/ui/src/fields/Array/ArrayRow.tsx index d3c495d16..1cc5fe11f 100644 --- a/packages/ui/src/fields/Array/ArrayRow.tsx +++ b/packages/ui/src/fields/Array/ArrayRow.tsx @@ -21,6 +21,7 @@ const baseClass = 'array-field' type ArrayRowProps = { readonly addRow: (rowIndex: number) => Promise | void + readonly copyRow: (rowIndex: number) => void readonly CustomRowLabel?: React.ReactNode readonly duplicateRow: (rowIndex: number) => void readonly errorCount: number @@ -32,6 +33,7 @@ type ArrayRowProps = { readonly labels: Partial readonly moveRow: (fromIndex: number, toIndex: number) => void readonly parentPath: string + readonly pasteRow: (rowIndex: number) => void readonly path: string readonly permissions: SanitizedFieldPermissions readonly readOnly?: boolean @@ -46,6 +48,7 @@ type ArrayRowProps = { export const ArrayRow: React.FC = ({ addRow, attributes, + copyRow, CustomRowLabel, duplicateRow, errorCount, @@ -59,6 +62,7 @@ export const ArrayRow: React.FC = ({ listeners, moveRow, parentPath, + pasteRow, path, permissions, readOnly, @@ -107,11 +111,13 @@ export const ArrayRow: React.FC = ({ !readOnly ? ( diff --git a/packages/ui/src/fields/Array/index.tsx b/packages/ui/src/fields/Array/index.tsx index 954937599..fe6cd8cfa 100644 --- a/packages/ui/src/fields/Array/index.tsx +++ b/packages/ui/src/fields/Array/index.tsx @@ -6,10 +6,19 @@ import type { } from 'payload' import { getTranslation } from '@payloadcms/translations' -import React, { useCallback } from 'react' +import React, { Fragment, useCallback } from 'react' +import { toast } from 'sonner' + +import type { ClipboardPasteData } from '../../elements/ClipboardAction/types.js' import { Banner } from '../../elements/Banner/index.js' import { Button } from '../../elements/Button/index.js' +import { clipboardCopy, clipboardPaste } from '../../elements/ClipboardAction/clipboardUtilities.js' +import { ClipboardAction } from '../../elements/ClipboardAction/index.js' +import { + mergeFormStateFromClipboard, + reduceFormStateByPath, +} from '../../elements/ClipboardAction/mergeFormStateFromClipboard.js' import { DraggableSortableItem } from '../../elements/DraggableSortable/DraggableSortableItem/index.js' import { DraggableSortable } from '../../elements/DraggableSortable/index.js' import { ErrorPill } from '../../elements/ErrorPill/index.js' @@ -21,6 +30,7 @@ import { useForm, useFormSubmitted } from '../../forms/Form/context.js' import { extractRowsAndCollapsedIDs, toggleAllRows } from '../../forms/Form/rowHelpers.js' import { NullifyLocaleField } from '../../forms/NullifyField/index.js' import { useField } from '../../forms/useField/index.js' +import './index.scss' import { withCondition } from '../../forms/withCondition/index.js' import { useConfig } from '../../providers/Config/index.js' import { useDocumentInfo } from '../../providers/DocumentInfo/index.js' @@ -29,7 +39,6 @@ import { useTranslation } from '../../providers/Translation/index.js' import { scrollToID } from '../../utilities/scrollToID.js' import { fieldBaseClass } from '../shared/index.js' import { ArrayRow } from './ArrayRow.js' -import './index.scss' const baseClass = 'array-field' @@ -37,6 +46,7 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => { const { field: { name, + type, admin: { className, description, isSortable = true } = {}, fields, label, @@ -58,7 +68,15 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => { const minRows = (minRowsProp ?? required) ? 1 : 0 const { setDocFieldPreferences } = useDocumentInfo() - const { addFieldRow, dispatchFields, moveFieldRow, removeFieldRow, setModified } = useForm() + const { + addFieldRow, + dispatchFields, + getFields, + moveFieldRow, + removeFieldRow, + replaceState, + setModified, + } = useForm() const submitted = useFormSubmitted() const { code: locale } = useLocale() const { i18n, t } = useTranslation() @@ -196,6 +214,83 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => { [dispatchFields, path, rows, setDocFieldPreferences], ) + const copyRow = useCallback( + (rowIndex: number) => { + const formState = { ...getFields() } + const clipboardResult = clipboardCopy({ + type, + fields, + getDataToCopy: () => + reduceFormStateByPath({ + formState, + path, + rowIndex, + }), + path, + rowIndex, + t, + }) + + if (typeof clipboardResult === 'string') { + toast.error(clipboardResult) + } else { + toast.success(t('general:copied')) + } + }, + [fields, getFields, path, t, type], + ) + + const pasteRow = useCallback( + (rowIndex: number) => { + const formState = { ...getFields() } + const pasteArgs = { + onPaste: (dataFromClipboard: ClipboardPasteData) => { + const newState = mergeFormStateFromClipboard({ + dataFromClipboard, + formState, + path, + rowIndex, + }) + replaceState(newState) + setModified(true) + }, + path, + schemaFields: fields, + t, + } + + const clipboardResult = clipboardPaste(pasteArgs) + + if (typeof clipboardResult === 'string') { + toast.error(clipboardResult) + } + }, + [fields, getFields, path, replaceState, setModified, t], + ) + + const pasteField = useCallback( + (dataFromClipboard: ClipboardPasteData) => { + const formState = { ...getFields() } + const newState = mergeFormStateFromClipboard({ + dataFromClipboard, + formState, + path, + }) + replaceState(newState) + setModified(true) + }, + [getFields, path, replaceState, setModified], + ) + + const getDataToCopy = useCallback( + () => + reduceFormStateByPath({ + formState: { ...getFields() }, + path, + }), + [getFields, path], + ) + const hasMaxRows = maxRows && rows.length >= maxRows const fieldErrorCount = errorPaths.length @@ -243,28 +338,42 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => { )} - {rows?.length > 0 && ( -
    -
  • - -
  • -
  • - -
  • -
- )} +
    + {rows?.length > 0 && ( + +
  • + +
  • +
  • + +
  • +
    + )} +
  • + 0)} + disablePaste={readOnly} + fields={fields} + getDataToCopy={getDataToCopy} + onPaste={pasteField} + path={path} + type={type} + /> +
  • +
{ { labels={labels} moveRow={moveRow} parentPath={path} + pasteRow={pasteRow} path={rowPath} permissions={permissions} readOnly={readOnly || disabled} diff --git a/packages/ui/src/fields/Blocks/BlockRow.tsx b/packages/ui/src/fields/Blocks/BlockRow.tsx index 14b01a45e..257399b44 100644 --- a/packages/ui/src/fields/Blocks/BlockRow.tsx +++ b/packages/ui/src/fields/Blocks/BlockRow.tsx @@ -25,6 +25,7 @@ type BlocksFieldProps = { addRow: (rowIndex: number, blockType: string) => Promise | void block: ClientBlock blocks: (ClientBlock | string)[] | ClientBlock[] + copyRow: (rowIndex: number) => void duplicateRow: (rowIndex: number) => void errorCount: number fields: ClientField[] @@ -35,6 +36,7 @@ type BlocksFieldProps = { labels: Labels moveRow: (fromIndex: number, toIndex: number) => void parentPath: string + pasteRow: (rowIndex: number) => void path: string permissions: SanitizedFieldPermissions readOnly: boolean @@ -51,6 +53,7 @@ export const BlockRow: React.FC = ({ attributes, block, blocks, + copyRow, duplicateRow, errorCount, fields, @@ -62,6 +65,7 @@ export const BlockRow: React.FC = ({ listeners, moveRow, parentPath, + pasteRow, path, permissions, readOnly, @@ -119,12 +123,14 @@ export const BlockRow: React.FC = ({ addRow={addRow} blocks={blocks} blockType={row.blockType} + copyRow={copyRow} duplicateRow={duplicateRow} fields={block.fields} hasMaxRows={hasMaxRows} isSortable={isSortable} labels={labels} moveRow={moveRow} + pasteRow={pasteRow} removeRow={removeRow} rowCount={rowCount} rowIndex={rowIndex} diff --git a/packages/ui/src/fields/Blocks/RowActions.tsx b/packages/ui/src/fields/Blocks/RowActions.tsx index 9e996b7b3..5b810e3a0 100644 --- a/packages/ui/src/fields/Blocks/RowActions.tsx +++ b/packages/ui/src/fields/Blocks/RowActions.tsx @@ -12,12 +12,14 @@ export const RowActions: React.FC<{ readonly addRow: (rowIndex: number, blockType: string) => Promise | void readonly blocks: (ClientBlock | string)[] readonly blockType: string + readonly copyRow: (rowIndex: number) => void readonly duplicateRow: (rowIndex: number, blockType: string) => void readonly fields: ClientField[] readonly hasMaxRows?: boolean readonly isSortable?: boolean readonly labels: Labels readonly moveRow: (fromIndex: number, toIndex: number) => void + readonly pasteRow: (rowIndex: number) => void readonly removeRow: (rowIndex: number) => void readonly rowCount: number readonly rowIndex: number @@ -26,11 +28,13 @@ export const RowActions: React.FC<{ addRow, blocks, blockType, + copyRow, duplicateRow, hasMaxRows, isSortable, labels, moveRow, + pasteRow, removeRow, rowCount, rowIndex, @@ -60,11 +64,13 @@ export const RowActions: React.FC<{ setIndexToAdd(index) openModal(drawerSlug) }} + copyRow={copyRow} duplicateRow={() => duplicateRow(rowIndex, blockType)} hasMaxRows={hasMaxRows} index={rowIndex} isSortable={isSortable} moveRow={moveRow} + pasteRow={pasteRow} removeRow={removeRow} rowCount={rowCount} /> diff --git a/packages/ui/src/fields/Blocks/index.tsx b/packages/ui/src/fields/Blocks/index.tsx index 99a857808..39cf410e9 100644 --- a/packages/ui/src/fields/Blocks/index.tsx +++ b/packages/ui/src/fields/Blocks/index.tsx @@ -2,10 +2,19 @@ import type { BlocksFieldClientComponent, ClientBlock } from 'payload' import { getTranslation } from '@payloadcms/translations' -import React, { Fragment, useCallback } from 'react' +import React, { Fragment, useCallback, useMemo } from 'react' +import { toast } from 'sonner' + +import type { ClipboardPasteData } from '../../elements/ClipboardAction/types.js' import { Banner } from '../../elements/Banner/index.js' import { Button } from '../../elements/Button/index.js' +import { clipboardCopy, clipboardPaste } from '../../elements/ClipboardAction/clipboardUtilities.js' +import { ClipboardAction } from '../../elements/ClipboardAction/index.js' +import { + mergeFormStateFromClipboard, + reduceFormStateByPath, +} from '../../elements/ClipboardAction/mergeFormStateFromClipboard.js' import { DraggableSortableItem } from '../../elements/DraggableSortable/DraggableSortableItem/index.js' import { DraggableSortable } from '../../elements/DraggableSortable/index.js' import { DrawerToggler } from '../../elements/Drawer/index.js' @@ -22,13 +31,13 @@ import { useDocumentInfo } from '../../providers/DocumentInfo/index.js' import { useLocale } from '../../providers/Locale/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { scrollToID } from '../../utilities/scrollToID.js' +import './index.scss' import { FieldDescription } from '../FieldDescription/index.js' import { FieldError } from '../FieldError/index.js' import { FieldLabel } from '../FieldLabel/index.js' import { fieldBaseClass } from '../shared/index.js' import { BlockRow } from './BlockRow.js' import { BlocksDrawer } from './BlocksDrawer/index.js' -import './index.scss' const baseClass = 'blocks-field' @@ -38,6 +47,7 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => { const { field: { name, + type, admin: { className, description, isSortable = true } = {}, blockReferences, blocks, @@ -60,7 +70,15 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => { const minRows = (minRowsProp ?? required) ? 1 : 0 const { setDocFieldPreferences } = useDocumentInfo() - const { addFieldRow, dispatchFields, moveFieldRow, removeFieldRow, setModified } = useForm() + const { + addFieldRow, + dispatchFields, + getFields, + moveFieldRow, + removeFieldRow, + replaceState, + setModified, + } = useForm() const { code: locale } = useLocale() const { config: { localization }, @@ -84,6 +102,23 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => { return true })() + const clientBlocks = useMemo(() => { + if (!blockReferences) { + return blocks + } + + const resolvedBlocks: ClientBlock[] = [] + for (const blockReference of blockReferences) { + const block = + typeof blockReference === 'string' ? config.blocksMap[blockReference] : blockReference + if (block) { + resolvedBlocks.push(block) + } + } + + return resolvedBlocks + }, [blockReferences, blocks, config.blocksMap]) + const memoizedValidate = useCallback( (value, options) => { // alternative locales can be null @@ -184,6 +219,73 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => { [dispatchFields, path, rows, setDocFieldPreferences], ) + const copyRow = useCallback( + (rowIndex: number) => { + const clipboardResult = clipboardCopy({ + type, + blocks: clientBlocks, + getDataToCopy: () => + reduceFormStateByPath({ + formState: { ...getFields() }, + path, + rowIndex, + }), + path, + rowIndex, + t, + }) + + if (typeof clipboardResult === 'string') { + toast.error(clipboardResult) + } else { + toast.success(t('general:copied')) + } + }, + [clientBlocks, path, t, type, getFields], + ) + + const pasteRow = useCallback( + (rowIndex: number) => { + const pasteArgs = { + onPaste: (dataFromClipboard: ClipboardPasteData) => { + const formState = { ...getFields() } + const newState = mergeFormStateFromClipboard({ + dataFromClipboard, + formState, + path, + rowIndex, + }) + replaceState(newState) + setModified(true) + }, + path, + schemaBlocks: clientBlocks, + t, + } + + const clipboardResult = clipboardPaste(pasteArgs) + + if (typeof clipboardResult === 'string') { + toast.error(clipboardResult) + } + }, + [clientBlocks, getFields, path, replaceState, setModified, t], + ) + + const pasteBlocks = useCallback( + (dataFromClipboard: ClipboardPasteData) => { + const formState = { ...getFields() } + const newState = mergeFormStateFromClipboard({ + dataFromClipboard, + formState, + path, + }) + replaceState(newState) + setModified(true) + }, + [getFields, path, replaceState, setModified], + ) + const hasMaxRows = maxRows && rows.length >= maxRows const fieldErrorCount = errorPaths.length @@ -225,28 +327,47 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => { )} - {rows.length > 0 && ( -
    -
  • - -
  • -
  • - -
  • -
- )} +
    + {rows.length > 0 && ( + +
  • + +
  • +
  • + +
  • +
    + )} +
  • + 0)} + disablePaste={readOnly} + getDataToCopy={() => + reduceFormStateByPath({ + formState: { ...getFields() }, + path, + }) + } + onPaste={pasteBlocks} + path={path} + type={type} + /> +
  • +
{ addRow={addRow} block={blockConfig} blocks={blockReferences ?? blocks} + copyRow={copyRow} duplicateRow={duplicateRow} errorCount={rowErrorCount} fields={blockConfig.fields} @@ -298,6 +420,7 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => { labels={labels} moveRow={moveRow} parentPath={path} + pasteRow={pasteRow} path={rowPath} permissions={permissions} readOnly={readOnly || disabled} diff --git a/test/access-control/e2e.spec.ts b/test/access-control/e2e.spec.ts index 41a9469b2..f9d05c195 100644 --- a/test/access-control/e2e.spec.ts +++ b/test/access-control/e2e.spec.ts @@ -729,7 +729,7 @@ describe('Access Control', () => { await page.locator('#field-unnamedTab').fill('unnamed tab') // array field - await page.locator('#field-array button').click() + await page.locator('#field-array > button').click() await page.locator('#field-array__0__text').fill('array row 0') await saveDocAndAssert(page) diff --git a/test/fields/collections/Array/e2e.spec.ts b/test/fields/collections/Array/e2e.spec.ts index 29bf43ce8..c5f1c27b1 100644 --- a/test/fields/collections/Array/e2e.spec.ts +++ b/test/fields/collections/Array/e2e.spec.ts @@ -3,6 +3,7 @@ import type { Page } from '@playwright/test' import { expect, test } from '@playwright/test' import { assertToastErrors } from 'helpers/assertToastErrors.js' +import { copyPasteField } from 'helpers/e2e/copyPasteField.js' import { toggleBlockOrArrayRow } from 'helpers/e2e/toggleCollapsible.js' import path from 'path' import { wait } from 'payload/shared' @@ -439,4 +440,236 @@ describe('Array', () => { expect(await field.count()).toEqual(0) }) }) + + describe('copy paste', () => { + test('should prevent copying an empty array field', async () => { + await page.goto(url.create) + const arrayFieldPopupBtn = page.locator( + '#field-collapsedArray .popup.clipboard-action__popup button.popup-button', + ) + await arrayFieldPopupBtn.click() + const disabledCopyBtn = page.locator( + '#field-collapsedArray .popup.clipboard-action__popup .popup__content div.popup-button-list__disabled:has-text("Copy Field")', + ) + await expect(disabledCopyBtn).toBeVisible() + }) + + test('should prevent pasting into readonly array field', async () => { + await page.goto(url.create) + await copyPasteField({ + fieldName: 'readOnly', + page, + }) + const popupBtn = page.locator( + '#field-readOnly .popup.clipboard-action__popup button.popup-button', + ) + await expect(popupBtn).toBeVisible() + await popupBtn.click() + const disabledPasteBtn = page.locator( + '#field-readOnly .popup.clipboard-action__popup .popup__content div.popup-button-list__disabled:has-text("Paste Field")', + ) + await expect(disabledPasteBtn).toBeVisible() + }) + + test('should prevent pasting into array field with different schema', async () => { + await page.goto(url.create) + await copyPasteField({ + fieldName: 'readOnly', + page, + }) + await copyPasteField({ + fieldName: 'items', + page, + action: 'paste', + }) + const pasteErrorToast = page + .locator('.payload-toast-item.toast-error') + .filter({ hasText: 'Invalid clipboard data.' }) + await expect(pasteErrorToast).toBeVisible() + }) + + test('should copy and paste array fields', async () => { + await page.goto(url.create) + const arrayField = page.locator('#field-items') + const row = arrayField.locator('#items-row-0') + const rowTextInput = row.locator('#field-items__0__text') + + const textVal = 'row one copy' + await rowTextInput.fill(textVal) + + await copyPasteField({ + page, + fieldName: 'items', + }) + + await page.reload() + + await expect(rowTextInput).toHaveValue('row one') + + await copyPasteField({ + page, + action: 'paste', + fieldName: 'items', + }) + + await expect(rowTextInput).toHaveValue(textVal) + }) + + test('should copy and paste array rows', async () => { + await page.goto(url.create) + const arrayField = page.locator('#field-items') + const row = arrayField.locator('#items-row-0') + const rowTextInput = row.locator('#field-items__0__text') + + const textVal = 'row one copy' + await rowTextInput.fill(textVal) + + await copyPasteField({ + page, + fieldName: 'items', + rowIndex: 0, + }) + + await page.reload() + + await expect(rowTextInput).toHaveValue('row one') + + await copyPasteField({ + page, + action: 'paste', + fieldName: 'items', + rowIndex: 0, + }) + + await expect(rowTextInput).toHaveValue(textVal) + }) + + test('should copy an array row and paste into a field with the same schema', async () => { + await page.goto(url.create) + + await copyPasteField({ + page, + fieldName: 'localized', + rowIndex: 0, + }) + + await copyPasteField({ + page, + fieldName: 'disableSort', + action: 'paste', + }) + + const rowsContainer = page + .locator('#field-disableSort > div.array-field__draggable-rows') + .first() + await expect(rowsContainer).toBeVisible() + const rowTextInput = rowsContainer.locator('#field-disableSort__0__text') + await expect(rowTextInput).toHaveValue('row one') + }) + + test('should copy an array field and paste into a row with the same schema', async () => { + await page.goto(url.create) + + await copyPasteField({ + page, + fieldName: 'localized', + }) + + const field = page.locator('#field-disableSort') + const addArrayBtn = field + .locator('button.array-field__add-row') + .filter({ hasText: 'Add Disable Sort' }) + await expect(addArrayBtn).toBeVisible() + await addArrayBtn.click() + + const row = field.locator('#disableSort-row-0') + await expect(row).toBeVisible() + + await copyPasteField({ page, action: 'paste', fieldName: 'disableSort' }) + + const rowsContainer = page + .locator('#field-disableSort > div.array-field__draggable-rows') + .first() + await expect(rowsContainer).toBeVisible() + const rowTextInput = rowsContainer.locator('#field-disableSort__0__text') + await expect(rowTextInput).toHaveValue('row one') + }) + + test('should correctly paste a row with nested arrays into a row with no children', async () => { + await page.goto(url.create) + + const field = page.locator('#field-items') + const addSubArrayBtn = field.locator( + '#field-items__0__subArray > button.array-field__add-row', + ) + await addSubArrayBtn.click() + + const textInputRowOne = field.locator('#field-items__0__subArray__0__text') + await expect(textInputRowOne).toBeVisible() + + const textInputRowOneValue = 'sub array row one' + await textInputRowOne.fill(textInputRowOneValue) + + await copyPasteField({ + page, + fieldName: 'items', + rowIndex: 0, + }) + + await copyPasteField({ + page, + fieldName: 'items', + rowIndex: 1, + action: 'paste', + }) + + const textInputRowTwo = field.locator('#field-items__1__subArray__0__text') + await expect(textInputRowTwo).toBeVisible() + await expect(textInputRowTwo).toHaveValue(textInputRowOneValue) + }) + + test('should replace the rows of a nested array field with those of its paste counterpart', async () => { + await page.goto(url.create) + + const field = page.locator('#field-items') + + const addSubArrayBtn = field.locator( + '#field-items__0__subArray > button.array-field__add-row', + ) + await expect(addSubArrayBtn).toBeVisible() + await addSubArrayBtn.click() + await addSubArrayBtn.click() + + const addSubArrayBtn2 = field.locator( + '#field-items__1__subArray > button.array-field__add-row', + ) + await expect(addSubArrayBtn2).toBeVisible() + await addSubArrayBtn2.click() + + const subArrayContainer = field.locator( + '#field-items__0__subArray > div.array-field__draggable-rows > div', + ) + const subArrayContainer2 = field.locator( + '#field-items__1__subArray > div.array-field__draggable-rows > div', + ) + await expect(subArrayContainer).toHaveCount(2) + await expect(subArrayContainer2).toHaveCount(1) + + await copyPasteField({ + page, + fieldName: 'items', + rowIndex: 1, + }) + + await copyPasteField({ + page, + fieldName: 'items', + rowIndex: 0, + action: 'paste', + }) + + await expect(subArrayContainer).toHaveCount(1) + await expect(subArrayContainer2).toHaveCount(1) + }) + }) }) diff --git a/test/fields/collections/Blocks/e2e.spec.ts b/test/fields/collections/Blocks/e2e.spec.ts index 5a2da4cd3..16fc3e14f 100644 --- a/test/fields/collections/Blocks/e2e.spec.ts +++ b/test/fields/collections/Blocks/e2e.spec.ts @@ -2,6 +2,7 @@ import type { BrowserContext, Page } from '@playwright/test' import { expect, test } from '@playwright/test' import { addBlock } from 'helpers/e2e/addBlock.js' +import { copyPasteField } from 'helpers/e2e/copyPasteField.js' import { openBlocksDrawer } from 'helpers/e2e/openBlocksDrawer.js' import { reorderBlocks } from 'helpers/e2e/reorderBlocks.js' import { scrollEntirePage } from 'helpers/e2e/scrollEntirePage.js' @@ -502,4 +503,223 @@ describe('Block fields', () => { await expect(groupLabel).toHaveText('Group in en') }) }) + + describe('copy paste', () => { + test('should prevent copying an empty block field', async () => { + await page.goto(url.create) + const popupBtn = page.locator( + '#field-i18nBlocks .popup.clipboard-action__popup button.popup-button', + ) + await popupBtn.click() + const disabledCopyBtn = page.locator( + '#field-i18nBlocks .popup.clipboard-action__popup .popup__content div.popup-button-list__disabled:has-text("Copy Field")', + ) + await expect(disabledCopyBtn).toBeVisible() + }) + + test('should prevent pasting into readonly block field', async () => { + await page.goto(url.create) + await copyPasteField({ + fieldName: 'readOnly', + page, + }) + const popupBtn = page.locator( + '#field-readOnly .popup.clipboard-action__popup button.popup-button', + ) + await expect(popupBtn).toBeVisible() + await popupBtn.click() + const disabledPasteBtn = page.locator( + '#field-readOnly .popup.clipboard-action__popup .popup__content div.popup-button-list__disabled:has-text("Paste Field")', + ) + await expect(disabledPasteBtn).toBeVisible() + }) + + test('should prevent pasting into block field with different schema', async () => { + await page.goto(url.create) + await copyPasteField({ + fieldName: 'readOnly', + page, + }) + await copyPasteField({ + fieldName: 'groupedBlocks', + page, + action: 'paste', + }) + const pasteErrorToast = page + .locator('.payload-toast-item.toast-error') + .filter({ hasText: 'Invalid clipboard data.' }) + await expect(pasteErrorToast).toBeVisible() + }) + + test('should copy and paste block fields', async () => { + await page.goto(url.create) + const field = page.locator('#field-blocks') + const row = field.locator('#blocks-row-0') + const rowTextInput = row.locator('#field-blocks__0__text') + + const textVal = 'row one copy' + await rowTextInput.fill(textVal) + + await copyPasteField({ + page, + fieldName: 'blocks', + }) + + await page.reload() + + await expect(rowTextInput).toHaveValue('first block') + + await copyPasteField({ + page, + action: 'paste', + fieldName: 'blocks', + }) + + await expect(rowTextInput).toHaveValue(textVal) + }) + + test('should copy and paste block rows', async () => { + await page.goto(url.create) + const field = page.locator('#field-blocks') + const row = field.locator('#blocks-row-0') + const rowTextInput = row.locator('#field-blocks__0__text') + + const textVal = 'row one copy' + await rowTextInput.fill(textVal) + + await copyPasteField({ + page, + fieldName: 'blocks', + rowIndex: 0, + }) + + await page.reload() + + await expect(rowTextInput).toHaveValue('first block') + + await copyPasteField({ + page, + action: 'paste', + fieldName: 'blocks', + rowIndex: 0, + }) + + await expect(rowTextInput).toHaveValue(textVal) + }) + + test('should copy a block row and paste into a field with the same schema', async () => { + await page.goto(url.create) + + await copyPasteField({ + page, + fieldName: 'blocks', + rowIndex: 1, + }) + + await copyPasteField({ + page, + fieldName: 'duplicate', + action: 'paste', + }) + + const rowsContainer = page.locator('#field-duplicate > div.blocks-field__rows').first() + await expect(rowsContainer).toBeVisible() + const rowTextInput = rowsContainer.locator('#field-duplicate__0__number') + await expect(rowTextInput).toHaveValue('342') + }) + + test('should copy a block field and paste into a row with the same schema', async () => { + await page.goto(url.create) + + const originalField = page.locator('#field-blocks') + const originalRow = originalField.locator('#blocks-row-0') + const originalInput = originalRow.locator('#field-blocks__0__text') + + const textVal = 'row one copy' + await originalInput.fill(textVal) + + await copyPasteField({ + page, + fieldName: 'blocks', + }) + + const field = page.locator('#field-duplicate') + const fieldInput = field.locator('#field-duplicate__0__text') + await expect(fieldInput).toHaveValue('first block') + + await copyPasteField({ page, action: 'paste', fieldName: 'duplicate', rowIndex: 0 }) + + const rowsContainer = page.locator('#field-duplicate > div.blocks-field__rows').first() + await expect(rowsContainer).toBeVisible() + const rowTextInput = rowsContainer.locator('#field-duplicate__0__text') + await expect(rowTextInput).toHaveValue('row one copy') + }) + + test('should correctly paste a row with nested blocks into a row with no children', async () => { + await page.goto(url.create) + + const field = page.locator('#field-blocks') + await addBlock({ page, fieldName: 'blocks', blockToSelect: 'Sub Block' }) + + const textInputRowOne = field.locator('#field-blocks__2__subBlocks__1__text') + await expect(textInputRowOne).toBeVisible() + + const textInputRowOneValue = 'copied second sub block' + await textInputRowOne.fill(textInputRowOneValue) + + await copyPasteField({ + page, + fieldName: 'blocks', + rowIndex: 2, + }) + + await copyPasteField({ + page, + fieldName: 'blocks', + rowIndex: 4, + action: 'paste', + }) + + const textInputRowTwo = field.locator('#field-blocks__4__subBlocks__1__text') + await expect(textInputRowTwo).toBeVisible() + await expect(textInputRowTwo).toHaveValue(textInputRowOneValue) + }) + + test('should replace the rows of a nested block field with those of its paste counterpart', async () => { + await page.goto(url.create) + + await addBlock({ + page, + fieldName: 'blocks', + blockToSelect: 'Sub Block', + }) + + const field = page.locator('#field-blocks') + + const subArrayContainer = field.locator( + '#field-blocks__2__subBlocks > div.blocks-field__rows > div', + ) + const subArrayContainer2 = field.locator( + '#field-blocks__4__subBlocks > div.blocks-field__rows > div', + ) + await expect(subArrayContainer).toHaveCount(2) + await expect(subArrayContainer2).toHaveCount(0) + + await copyPasteField({ + page, + fieldName: 'blocks', + rowIndex: 4, + }) + + await copyPasteField({ + page, + fieldName: 'blocks', + rowIndex: 2, + action: 'paste', + }) + + await expect(subArrayContainer).toHaveCount(0) + await expect(subArrayContainer2).toHaveCount(0) + }) + }) }) diff --git a/test/fields/collections/Blocks/index.ts b/test/fields/collections/Blocks/index.ts index 491d2d7bc..f058aa75c 100644 --- a/test/fields/collections/Blocks/index.ts +++ b/test/fields/collections/Blocks/index.ts @@ -495,6 +495,31 @@ const BlockFields: CollectionConfig = { }, ], }, + { + name: 'readOnly', + type: 'blocks', + admin: { + readOnly: true, + }, + defaultValue: [ + { + blockType: 'readOnlyBlock', + title: 'readOnly', + }, + ], + blocks: [ + { + slug: 'readOnlyBlock', + fields: [ + { + type: 'text', + name: 'title', + defaultValue: 'readOnly', + }, + ], + }, + ], + }, ], } diff --git a/test/helpers/e2e/copyPasteField.ts b/test/helpers/e2e/copyPasteField.ts new file mode 100644 index 000000000..b633b75e9 --- /dev/null +++ b/test/helpers/e2e/copyPasteField.ts @@ -0,0 +1,44 @@ +import type { Page } from '@playwright/test' + +import { expect } from '@playwright/test' +import { wait } from 'payload/shared' + +export async function copyPasteField({ + fieldName, + rowIndex, + page, + action = 'copy', +}: { + action?: 'copy' | 'paste' + fieldName: string + page: Page + rowIndex?: number +}) { + const isCopy = action === 'copy' + const field = page.locator(`#field-${fieldName}`) + const rowAction = typeof rowIndex === 'number' + await expect(field).toBeVisible() + + if (rowAction) { + await wait(1000) + } + + const popupBtnSelector = rowAction + ? `#${fieldName}-row-${rowIndex} .collapsible__actions button.array-actions__button` + : 'header .clipboard-action__popup button.popup-button' + const popupBtn = field.locator(popupBtnSelector).first() + await expect(popupBtn).toBeVisible() + await popupBtn.click() + + const actionBtnSelector = rowAction + ? `#${fieldName}-row-${rowIndex} .popup__content .popup-button-list button.array-actions__${action}` + : `.popup.clipboard-action__popup .popup__content .popup-button-list button:has-text("${isCopy ? 'Copy' : 'Paste'} Field")` + const actionBtn = field.locator(actionBtnSelector).first() + await expect(actionBtn).toBeVisible() + await actionBtn.click() + + if (isCopy) { + const copySuccessToast = page.locator('.payload-toast-item.toast-success') + await expect(copySuccessToast).toBeVisible() + } +} diff --git a/test/joins/e2e.spec.ts b/test/joins/e2e.spec.ts index 3928426a3..9aaf8edd6 100644 --- a/test/joins/e2e.spec.ts +++ b/test/joins/e2e.spec.ts @@ -349,7 +349,7 @@ describe('Join Field', () => { await editButton.click() const drawer = page.locator('[id^=doc-drawer_posts_1_]') await expect(drawer).toBeVisible() - const popupButton = drawer.locator('button.popup-button') + const popupButton = drawer.locator('.doc-controls__popup button.popup-button') await expect(popupButton).toBeVisible() await popupButton.click() const deleteButton = drawer.locator('#action-delete') diff --git a/test/localization/e2e.spec.ts b/test/localization/e2e.spec.ts index fc34f0002..ddb0dbf17 100644 --- a/test/localization/e2e.spec.ts +++ b/test/localization/e2e.spec.ts @@ -440,7 +440,9 @@ describe('Localization', () => { await addBlock.click() const selectBlock = page.locator('.blocks-drawer__block button') await selectBlock.click() - const addContentButton = page.locator('#field-content__0__content button') + const addContentButton = page + .locator('#field-content__0__content') + .getByRole('button', { name: 'Add Content' }) await addContentButton.click() await selectBlock.click() const textField = page.locator('#field-content__0__content__0__text')