feat(ui): adds edit many option for bulk uploads (#10646)
### What? This PR introduces the ability to bulk edit multiple uploads simultaneously within the `Edit all` option for bulk uploads. Users can now select fields to update across all selected uploads in a single operation. ### Why? Managing multiple uploads individually can be time-consuming and inefficient, especially when updating common fields. This feature streamlines the process, improving user experience and productivity when handling bulk uploads. ### How? * Added an `Edit Many` drawer component specific to bulk uploads that allows users to select fields for bulk editing. * Enhanced the FormsManager and related logic to ensure updates are applied consistently across all selected uploads. 
This commit is contained in:
@@ -182,6 +182,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
|
||||
'general:duplicate',
|
||||
'general:duplicateWithoutSaving',
|
||||
'general:edit',
|
||||
'general:editAll',
|
||||
'general:editing',
|
||||
'general:editingLabel',
|
||||
'general:editingTakenOver',
|
||||
|
||||
@@ -235,6 +235,7 @@ export const arTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'استنساخ',
|
||||
duplicateWithoutSaving: 'استنساخ بدون حفظ التغييرات',
|
||||
edit: 'تعديل',
|
||||
editAll: 'تحرير الكل',
|
||||
editedSince: 'تم التحرير منذ',
|
||||
editing: 'جاري التعديل',
|
||||
editingLabel_many: 'تعديل {{count}} {{label}}',
|
||||
|
||||
@@ -239,6 +239,7 @@ export const azTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Dublikat',
|
||||
duplicateWithoutSaving: 'Dəyişiklikləri saxlamadan dublikatla',
|
||||
edit: 'Redaktə et',
|
||||
editAll: 'Hamısını redaktə et',
|
||||
editedSince: 'Redaktə edilib',
|
||||
editing: 'Redaktə olunur',
|
||||
editingLabel_many: '{{count}} {{label}} redaktə olunur',
|
||||
|
||||
@@ -238,6 +238,7 @@ export const bgTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Дупликирай',
|
||||
duplicateWithoutSaving: 'Дупликирай без да запазваш промените',
|
||||
edit: 'Редактирай',
|
||||
editAll: 'Редактирай всички',
|
||||
editedSince: 'Редактирано от',
|
||||
editing: 'Редактиране',
|
||||
editingLabel_many: 'Редактиране на {{count}} {{label}}',
|
||||
|
||||
@@ -239,6 +239,7 @@ export const caTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Duplicar',
|
||||
duplicateWithoutSaving: 'Duplica sense desar',
|
||||
edit: 'Edita',
|
||||
editAll: 'Edita-ho tot',
|
||||
editedSince: 'Editat des de',
|
||||
editing: 'Editant',
|
||||
editingLabel_many: 'Editent {{count}} {{label}}',
|
||||
|
||||
@@ -237,6 +237,7 @@ export const csTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Duplikovat',
|
||||
duplicateWithoutSaving: 'Duplikovat bez uložení změn',
|
||||
edit: 'Upravit',
|
||||
editAll: 'Upravit vše',
|
||||
editedSince: 'Upraveno od',
|
||||
editing: 'Úprava',
|
||||
editingLabel_many: 'Úprava {{count}} {{label}}',
|
||||
|
||||
@@ -237,6 +237,7 @@ export const daTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Duplikér',
|
||||
duplicateWithoutSaving: 'Dupliker uden at gemme ændringer',
|
||||
edit: 'Redigere',
|
||||
editAll: 'Rediger alle',
|
||||
editedSince: 'Dette dokument er blevet redigeret siden du startede',
|
||||
editing: 'Rediger',
|
||||
editingLabel_many: 'Rediger {{count}} {{label}}',
|
||||
|
||||
@@ -243,6 +243,7 @@ export const deTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Duplizieren',
|
||||
duplicateWithoutSaving: 'Dupliziere ohne Änderungen zu speichern',
|
||||
edit: 'Bearbeiten',
|
||||
editAll: 'Bearbeite alle',
|
||||
editedSince: 'Bearbeitet seit',
|
||||
editing: 'Bearbeite',
|
||||
editingLabel_many: 'Bearbeiten von {{count}} {{label}}',
|
||||
|
||||
@@ -239,6 +239,7 @@ export const enTranslations = {
|
||||
duplicate: 'Duplicate',
|
||||
duplicateWithoutSaving: 'Duplicate without saving changes',
|
||||
edit: 'Edit',
|
||||
editAll: 'Edit all',
|
||||
editedSince: 'Edited since',
|
||||
editing: 'Editing',
|
||||
editingLabel_many: 'Editing {{count}} {{label}}',
|
||||
@@ -375,8 +376,8 @@ export const enTranslations = {
|
||||
within: 'within',
|
||||
},
|
||||
upload: {
|
||||
addFile: 'Add File',
|
||||
addFiles: 'Add Files',
|
||||
addFile: 'Add file',
|
||||
addFiles: 'Add files',
|
||||
bulkUpload: 'Bulk Upload',
|
||||
crop: 'Crop',
|
||||
cropToolDescription:
|
||||
|
||||
@@ -243,6 +243,7 @@ export const esTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Duplicar',
|
||||
duplicateWithoutSaving: 'Duplicar sin guardar cambios',
|
||||
edit: 'Editar',
|
||||
editAll: 'Editar Todo',
|
||||
editedSince: 'Editado desde',
|
||||
editing: 'Editando',
|
||||
editingLabel_many: 'Edición de {{count}} {{label}}',
|
||||
|
||||
@@ -236,6 +236,7 @@ export const etTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Dubleeri',
|
||||
duplicateWithoutSaving: 'Dubleeri ilma muudatusi salvestamata',
|
||||
edit: 'Muuda',
|
||||
editAll: 'Muuda kõiki',
|
||||
editedSince: 'Muudetud alates',
|
||||
editing: 'Muutmine',
|
||||
editingLabel_many: 'Muudan {{count}} {{label}}',
|
||||
|
||||
@@ -237,6 +237,7 @@ export const faTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'تکراری',
|
||||
duplicateWithoutSaving: 'رونوشت بدون ذخیره کردن تغییرات',
|
||||
edit: 'نگارش',
|
||||
editAll: 'ویرایش همه',
|
||||
editedSince: 'ویرایش شده از',
|
||||
editing: 'در حال نگارش',
|
||||
editingLabel_many: 'در حال نگارش {{count}} از {{label}}',
|
||||
|
||||
@@ -246,6 +246,7 @@ export const frTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Dupliquer',
|
||||
duplicateWithoutSaving: 'Dupliquer sans enregistrer les modifications',
|
||||
edit: 'Éditer',
|
||||
editAll: 'Modifier tout',
|
||||
editedSince: 'Modifié depuis',
|
||||
editing: 'Modification en cours',
|
||||
editingLabel_many: 'Modification des {{count}} {{label}}',
|
||||
|
||||
@@ -233,6 +233,7 @@ export const heTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'שכפול',
|
||||
duplicateWithoutSaving: 'שכפול ללא שמירת שינויים',
|
||||
edit: 'עריכה',
|
||||
editAll: 'עריכה הכל',
|
||||
editedSince: 'נערך מאז',
|
||||
editing: 'עריכה',
|
||||
editingLabel_many: 'עריכת {{count}} {{label}}',
|
||||
|
||||
@@ -239,6 +239,7 @@ export const hrTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Duplikat',
|
||||
duplicateWithoutSaving: 'Dupliciraj bez spremanja promjena',
|
||||
edit: 'Uredi',
|
||||
editAll: 'Uredi sve',
|
||||
editedSince: 'Uređeno od',
|
||||
editing: 'Uređivanje',
|
||||
editingLabel_many: 'Uređivanje {{count}} {{label}}',
|
||||
|
||||
@@ -241,6 +241,7 @@ export const huTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Duplikálás',
|
||||
duplicateWithoutSaving: 'Duplikálás a módosítások mentése nélkül',
|
||||
edit: 'Szerkesztés',
|
||||
editAll: 'Összes szerkesztése',
|
||||
editedSince: 'Szerkesztve',
|
||||
editing: 'Szerkesztés',
|
||||
editingLabel_many: '{{count}} {{label}} szerkesztése',
|
||||
|
||||
@@ -242,6 +242,7 @@ export const itTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Duplica',
|
||||
duplicateWithoutSaving: 'Duplica senza salvare le modifiche',
|
||||
edit: 'Modificare',
|
||||
editAll: 'Modifica Tutto',
|
||||
editedSince: 'Modificato da',
|
||||
editing: 'Modifica',
|
||||
editingLabel_many: 'Modificare {{count}} {{label}}',
|
||||
|
||||
@@ -239,6 +239,7 @@ export const jaTranslations: DefaultTranslationsObject = {
|
||||
duplicate: '複製',
|
||||
duplicateWithoutSaving: '変更を保存せずに複製',
|
||||
edit: '編集',
|
||||
editAll: 'すべてを編集',
|
||||
editedSince: 'から編集',
|
||||
editing: '編集',
|
||||
editingLabel_many: '{{count}}つの{{label}}を編集しています',
|
||||
|
||||
@@ -237,6 +237,7 @@ export const koTranslations: DefaultTranslationsObject = {
|
||||
duplicate: '복제',
|
||||
duplicateWithoutSaving: '변경 사항 저장 없이 복제',
|
||||
edit: '수정',
|
||||
editAll: '모두 수정',
|
||||
editedSince: '편집됨',
|
||||
editing: '수정 중',
|
||||
editingLabel_many: '{{count}}개의 {{label}} 수정 중',
|
||||
|
||||
@@ -241,6 +241,7 @@ export const myTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'ပုံတူပွားမည်။',
|
||||
duplicateWithoutSaving: 'သေချာပါပြီ။',
|
||||
edit: 'တည်းဖြတ်ပါ။',
|
||||
editAll: 'အားလုံးကို တည်းဖြတ်ပါ။',
|
||||
editedSince: 'ကစပြီးတည်းဖြတ်ခဲ့သည်',
|
||||
editing: 'ပြင်ဆင်နေသည်။',
|
||||
editingLabel_many: 'တည်းဖြတ်ခြင်း {{count}} {{label}}',
|
||||
|
||||
@@ -239,6 +239,7 @@ export const nbTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Dupliser',
|
||||
duplicateWithoutSaving: 'Dupliser uten å lagre endringer',
|
||||
edit: 'Redigere',
|
||||
editAll: 'Rediger alle',
|
||||
editedSince: 'Redigert siden',
|
||||
editing: 'Redigerer',
|
||||
editingLabel_many: 'Redigerer {{count}} {{label}}',
|
||||
|
||||
@@ -242,6 +242,7 @@ export const nlTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Dupliceren',
|
||||
duplicateWithoutSaving: 'Dupliceren zonder wijzigingen te bewaren',
|
||||
edit: 'Bewerk',
|
||||
editAll: 'Bewerk alles',
|
||||
editedSince: 'Bewerkt sinds',
|
||||
editing: 'Bewerken',
|
||||
editingLabel_many: 'Bewerken {{count}} {{label}}',
|
||||
|
||||
@@ -239,6 +239,7 @@ export const plTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Zduplikuj',
|
||||
duplicateWithoutSaving: 'Zduplikuj bez zapisywania zmian',
|
||||
edit: 'Edytuj',
|
||||
editAll: 'Edytuj wszystko',
|
||||
editedSince: 'Edytowano od',
|
||||
editing: 'Edycja',
|
||||
editingLabel_many: 'Edytowanie {{count}} {{label}}',
|
||||
|
||||
@@ -239,6 +239,7 @@ export const ptTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Duplicar',
|
||||
duplicateWithoutSaving: 'Duplicar sem salvar alterações',
|
||||
edit: 'Editar',
|
||||
editAll: 'Editar todos',
|
||||
editedSince: 'Editado desde',
|
||||
editing: 'Editando',
|
||||
editingLabel_many: 'Editando {{count}} {{label}}',
|
||||
|
||||
@@ -243,6 +243,7 @@ export const roTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Duplicați',
|
||||
duplicateWithoutSaving: 'Duplicați fără salvarea modificărilor',
|
||||
edit: 'Editează',
|
||||
editAll: 'Editează toate',
|
||||
editedSince: 'Editat din',
|
||||
editing: 'Editare',
|
||||
editingLabel_many: 'Editare {{count}} {{label}}',
|
||||
|
||||
@@ -239,6 +239,7 @@ export const rsTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Дупликат',
|
||||
duplicateWithoutSaving: 'Понови без чувања промена',
|
||||
edit: 'Уреди',
|
||||
editAll: 'Уреди све',
|
||||
editedSince: 'Измењено од',
|
||||
editing: 'Уређивање',
|
||||
editingLabel_many: 'Уређивање {{count}} {{label}}',
|
||||
|
||||
@@ -239,6 +239,7 @@ export const rsLatinTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Duplikat',
|
||||
duplicateWithoutSaving: 'Ponovi bez čuvanja promena',
|
||||
edit: 'Uredi',
|
||||
editAll: 'Uredi sve',
|
||||
editedSince: 'Izmenjeno od',
|
||||
editing: 'Uređivanje',
|
||||
editingLabel_many: 'Uređivanje {{count}} {{label}}',
|
||||
|
||||
@@ -241,6 +241,7 @@ export const ruTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Дублировать',
|
||||
duplicateWithoutSaving: 'Дублирование без сохранения изменений',
|
||||
edit: 'Редактировать',
|
||||
editAll: 'Редактировать все',
|
||||
editedSince: 'Отредактировано с',
|
||||
editing: 'Редактирование',
|
||||
editingLabel_many: 'Редактирование {{count}} {{label}}',
|
||||
|
||||
@@ -240,6 +240,7 @@ export const skTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Duplikovať',
|
||||
duplicateWithoutSaving: 'Duplikovať bez uloženia zmien',
|
||||
edit: 'Upraviť',
|
||||
editAll: 'Upraviť všetko',
|
||||
editedSince: 'Upravené od',
|
||||
editing: 'Úpravy',
|
||||
editingLabel_many: 'Úprava {{count}} {{label}}',
|
||||
|
||||
@@ -238,6 +238,7 @@ export const slTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Podvoji',
|
||||
duplicateWithoutSaving: 'Podvoji brez shranjevanja sprememb',
|
||||
edit: 'Uredi',
|
||||
editAll: 'Uredi vse',
|
||||
editedSince: 'Urejeno od',
|
||||
editing: 'Urejanje',
|
||||
editingLabel_many: 'Urejanje {{count}} {{label}}',
|
||||
|
||||
@@ -239,6 +239,7 @@ export const svTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Duplicera',
|
||||
duplicateWithoutSaving: 'Duplicera utan att spara ändringar',
|
||||
edit: 'Redigera',
|
||||
editAll: 'Redigera alla',
|
||||
editedSince: 'Redigerad sedan',
|
||||
editing: 'Redigerar',
|
||||
editingLabel_many: 'Redigerar {{count}} {{label}}',
|
||||
|
||||
@@ -235,6 +235,7 @@ export const thTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'สำเนา',
|
||||
duplicateWithoutSaving: 'สำเนาโดยไม่บันทึกการแก้ไข',
|
||||
edit: 'แก้ไข',
|
||||
editAll: 'แก้ไขทั้งหมด',
|
||||
editedSince: 'แก้ไขตั้งแต่',
|
||||
editing: 'แก้ไข',
|
||||
editingLabel_many: 'กำลังแก้ไข {{count}} {{label}}',
|
||||
|
||||
@@ -242,6 +242,7 @@ export const trTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Çoğalt',
|
||||
duplicateWithoutSaving: 'Ayarları kaydetmeden çoğalt',
|
||||
edit: 'Düzenle',
|
||||
editAll: 'Hepsini düzenle',
|
||||
editedSince: 'O tarihten itibaren düzenlendi',
|
||||
editing: 'Düzenleniyor',
|
||||
editingLabel_many: '{{count}} {{label}} düzenleniyor',
|
||||
|
||||
@@ -238,6 +238,7 @@ export const ukTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Дублювати',
|
||||
duplicateWithoutSaving: 'Дублювання без збереження змін',
|
||||
edit: 'Редагувати',
|
||||
editAll: 'Редагувати все',
|
||||
editedSince: 'Відредаговано з',
|
||||
editing: 'Редагування',
|
||||
editingLabel_many: 'Редагування {{count}} {{label}}',
|
||||
|
||||
@@ -238,6 +238,7 @@ export const viTranslations: DefaultTranslationsObject = {
|
||||
duplicate: 'Tạo bản sao',
|
||||
duplicateWithoutSaving: 'Không lưu dữ liệu và tạo bản sao',
|
||||
edit: 'Chỉnh sửa',
|
||||
editAll: 'Chỉnh sửa tất cả',
|
||||
editedSince: 'Được chỉnh sửa từ',
|
||||
editing: 'Đang chỉnh sửa',
|
||||
editingLabel_many: 'Đang chỉnh sửa {{count}} {{label}}',
|
||||
|
||||
@@ -229,6 +229,7 @@ export const zhTranslations: DefaultTranslationsObject = {
|
||||
duplicate: '重复',
|
||||
duplicateWithoutSaving: '重复而不保存更改。',
|
||||
edit: '编辑',
|
||||
editAll: '编辑全部',
|
||||
editedSince: '自...以来编辑',
|
||||
editing: '编辑中',
|
||||
editingLabel_many: '编辑 {{count}} {{label}}',
|
||||
|
||||
@@ -229,6 +229,7 @@ export const zhTwTranslations: DefaultTranslationsObject = {
|
||||
duplicate: '複製',
|
||||
duplicateWithoutSaving: '複製而不儲存變更。',
|
||||
edit: '編輯',
|
||||
editAll: '編輯全部',
|
||||
editedSince: '自...以來編輯',
|
||||
editing: '編輯中',
|
||||
editingLabel_many: '編輯 {{count}} 個 {{label}}',
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
'use client'
|
||||
|
||||
import type { ClientCollectionConfig } from 'payload'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import { ChevronIcon } from '../../../icons/Chevron/index.js'
|
||||
import { useConfig } from '../../../providers/Config/index.js'
|
||||
import { useTranslation } from '../../../providers/Translation/index.js'
|
||||
import { Button } from '../../Button/index.js'
|
||||
import { EditManyBulkUploads } from '../EditMany/index.js'
|
||||
import { useFormsManager } from '../FormsManager/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'bulk-upload--actions-bar'
|
||||
|
||||
export function ActionsBar() {
|
||||
type Props = {
|
||||
readonly collectionConfig: ClientCollectionConfig
|
||||
}
|
||||
|
||||
export function ActionsBar({ collectionConfig }: Props) {
|
||||
const { activeIndex, forms, setActiveIndex } = useFormsManager()
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -56,6 +63,7 @@ export function ActionsBar() {
|
||||
<ChevronIcon direction="right" />
|
||||
</Button>
|
||||
</div>
|
||||
<EditManyBulkUploads collection={collectionConfig} />
|
||||
</div>
|
||||
|
||||
<Actions className={`${baseClass}__saveButtons`} />
|
||||
|
||||
@@ -36,7 +36,7 @@ export function AddingFilesView() {
|
||||
const { user } = useAuth()
|
||||
const { openModal } = useModal()
|
||||
|
||||
const collection = getEntityConfig({ collectionSlug })
|
||||
const collectionConfig = getEntityConfig({ collectionSlug })
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
@@ -45,7 +45,7 @@ export function AddingFilesView() {
|
||||
<div className={`${baseClass}__editView`}>
|
||||
<DrawerHeader
|
||||
onClose={() => openModal(discardBulkUploadModalSlug)}
|
||||
title={getTranslation(collection.labels.singular, i18n)}
|
||||
title={getTranslation(collectionConfig.labels.singular, i18n)}
|
||||
/>
|
||||
{activeForm ? (
|
||||
<DocumentInfoProvider
|
||||
@@ -66,7 +66,7 @@ export function AddingFilesView() {
|
||||
Upload={documentSlots.Upload}
|
||||
versionCount={0}
|
||||
>
|
||||
<ActionsBar />
|
||||
<ActionsBar collectionConfig={collectionConfig} />
|
||||
<EditForm submitted={hasSubmitted} />
|
||||
</DocumentInfoProvider>
|
||||
) : null}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import type { ClientCollectionConfig } from 'payload'
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation.js'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
'use client'
|
||||
|
||||
import type { ClientCollectionConfig, FieldWithPathClient } from 'payload'
|
||||
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
|
||||
import type { FormProps } from '../../../forms/Form/index.js'
|
||||
import type { State } from '../FormsManager/reducer.js'
|
||||
|
||||
import { Button } from '../../../elements/Button/index.js'
|
||||
import { Form } from '../../../forms/Form/index.js'
|
||||
import { RenderFields } from '../../../forms/RenderFields/index.js'
|
||||
import { XIcon } from '../../../icons/X/index.js'
|
||||
import { useAuth } from '../../../providers/Auth/index.js'
|
||||
import { useTranslation } from '../../../providers/Translation/index.js'
|
||||
import { FieldSelect } from '../../FieldSelect/index.js'
|
||||
import { useFormsManager } from '../FormsManager/index.js'
|
||||
import { baseClass, type EditManyBulkUploadsProps } from './index.js'
|
||||
import './index.scss'
|
||||
|
||||
export const EditManyBulkUploadsDrawerContent: React.FC<
|
||||
{
|
||||
collection: ClientCollectionConfig
|
||||
drawerSlug: string
|
||||
forms: State['forms']
|
||||
} & EditManyBulkUploadsProps
|
||||
> = (props) => {
|
||||
const { collection: { slug, fields, labels: { plural } } = {}, drawerSlug, forms } = props
|
||||
|
||||
const { permissions } = useAuth()
|
||||
const { i18n, t } = useTranslation()
|
||||
const { closeModal } = useModal()
|
||||
const { bulkUpdateForm } = useFormsManager()
|
||||
|
||||
const [selectedFields, setSelectedFields] = useState<FieldWithPathClient[]>([])
|
||||
const collectionPermissions = permissions?.collections?.[slug]
|
||||
|
||||
const handleSubmit: FormProps['onSubmit'] = useCallback(
|
||||
(formState) => {
|
||||
const pairedData = selectedFields.reduce((acc, field) => {
|
||||
const { path } = field
|
||||
if (formState[path]) {
|
||||
acc[path] = formState[path].value
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
void bulkUpdateForm(pairedData, () => closeModal(drawerSlug))
|
||||
},
|
||||
[closeModal, drawerSlug, bulkUpdateForm, selectedFields],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__main`}>
|
||||
<div className={`${baseClass}__header`}>
|
||||
<h2 className={`${baseClass}__header__title`}>
|
||||
{t('general:editingLabel', {
|
||||
count: forms.length,
|
||||
label: getTranslation(plural, i18n),
|
||||
})}
|
||||
</h2>
|
||||
<button
|
||||
aria-label={t('general:close')}
|
||||
className={`${baseClass}__header__close`}
|
||||
id={`close-drawer__${drawerSlug}`}
|
||||
onClick={() => closeModal(drawerSlug)}
|
||||
type="button"
|
||||
>
|
||||
<XIcon />
|
||||
</button>
|
||||
</div>
|
||||
<Form className={`${baseClass}__form`} initialState={{}} onSubmit={handleSubmit}>
|
||||
<FieldSelect fields={fields} setSelected={setSelectedFields} />
|
||||
{selectedFields.length === 0 ? null : (
|
||||
<RenderFields
|
||||
fields={selectedFields}
|
||||
parentIndexPath=""
|
||||
parentPath=""
|
||||
parentSchemaPath={slug}
|
||||
permissions={collectionPermissions?.fields}
|
||||
readOnly={false}
|
||||
/>
|
||||
)}
|
||||
<div className={`${baseClass}__sidebar-wrap`}>
|
||||
<div className={`${baseClass}__sidebar`}>
|
||||
<div className={`${baseClass}__sidebar-sticky-wrap`}>
|
||||
<div className={`${baseClass}__document-actions`}>
|
||||
<Button type="submit">{t('general:applyChanges')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
201
packages/ui/src/elements/BulkUpload/EditMany/index.scss
Normal file
201
packages/ui/src/elements/BulkUpload/EditMany/index.scss
Normal file
@@ -0,0 +1,201 @@
|
||||
@import '../../../scss/styles.scss';
|
||||
|
||||
@layer payload-default {
|
||||
.edit-many-bulk-uploads {
|
||||
&__toggle {
|
||||
font-size: 1rem;
|
||||
line-height: base(1.2);
|
||||
display: inline-flex;
|
||||
background: var(--theme-elevation-150);
|
||||
color: var(--theme-elevation-800);
|
||||
border-radius: $style-radius-s;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
border: 0;
|
||||
padding: 0 base(0.4);
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--theme-elevation-100);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--theme-elevation-100);
|
||||
}
|
||||
}
|
||||
|
||||
&__form {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__main {
|
||||
width: calc(100% - #{base(15)});
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
margin-top: base(2.5);
|
||||
margin-bottom: base(1);
|
||||
width: 100%;
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&__close {
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
width: base(1);
|
||||
height: base(1);
|
||||
|
||||
svg {
|
||||
width: base(2);
|
||||
height: base(2);
|
||||
position: relative;
|
||||
inset-inline-start: base(-0.5);
|
||||
top: base(-0.5);
|
||||
|
||||
.stroke {
|
||||
stroke-width: 2px;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__edit {
|
||||
padding-top: base(1);
|
||||
padding-bottom: base(2);
|
||||
flex-grow: 1;
|
||||
}
|
||||
[dir='rtl'] &__sidebar-wrap {
|
||||
left: 0;
|
||||
border-right: 1px solid var(--theme-elevation-100);
|
||||
right: auto;
|
||||
}
|
||||
|
||||
&__sidebar-wrap {
|
||||
position: fixed;
|
||||
width: base(15);
|
||||
height: 100%;
|
||||
top: 0;
|
||||
right: 0;
|
||||
overflow: visible;
|
||||
border-left: 1px solid var(--theme-elevation-100);
|
||||
}
|
||||
|
||||
&__sidebar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&__sidebar-sticky-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
&__collection-actions,
|
||||
&__meta,
|
||||
&__sidebar-fields {
|
||||
[dir='ltr'] & {
|
||||
padding-left: base(1.5);
|
||||
}
|
||||
[dir='rtl'] & {
|
||||
padding-right: base(1.5);
|
||||
}
|
||||
}
|
||||
|
||||
&__document-actions {
|
||||
padding-right: $baseline;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: var(--z-nav);
|
||||
|
||||
> * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
@include blur-bg;
|
||||
}
|
||||
}
|
||||
|
||||
&__document-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: base(1);
|
||||
gap: base(0.5);
|
||||
|
||||
.form-submit {
|
||||
width: calc(50% - #{base(1)});
|
||||
|
||||
@include mid-break {
|
||||
width: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding-left: base(0.5);
|
||||
padding-right: base(0.5);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
&__main {
|
||||
width: 100%;
|
||||
min-height: initial;
|
||||
}
|
||||
|
||||
&__sidebar-wrap {
|
||||
position: static;
|
||||
width: 100%;
|
||||
height: initial;
|
||||
}
|
||||
|
||||
&__form {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__edit {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&__document-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: auto;
|
||||
z-index: var(--z-nav);
|
||||
}
|
||||
|
||||
&__document-actions,
|
||||
&__sidebar-fields {
|
||||
padding-left: var(--gutter-h);
|
||||
padding-right: var(--gutter-h);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
58
packages/ui/src/elements/BulkUpload/EditMany/index.tsx
Normal file
58
packages/ui/src/elements/BulkUpload/EditMany/index.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
'use client'
|
||||
|
||||
import type { ClientCollectionConfig } from 'payload'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import { useAuth } from '../../../providers/Auth/index.js'
|
||||
import { EditDepthProvider } from '../../../providers/EditDepth/index.js'
|
||||
import { useTranslation } from '../../../providers/Translation/index.js'
|
||||
import { Drawer, DrawerToggler } from '../../Drawer/index.js'
|
||||
import { useFormsManager } from '../FormsManager/index.js'
|
||||
import './index.scss'
|
||||
import { EditManyBulkUploadsDrawerContent } from './DrawerContent.js'
|
||||
|
||||
export const baseClass = 'edit-many-bulk-uploads'
|
||||
|
||||
export type EditManyBulkUploadsProps = {
|
||||
readonly collection: ClientCollectionConfig
|
||||
}
|
||||
|
||||
export const EditManyBulkUploads: React.FC<EditManyBulkUploadsProps> = (props) => {
|
||||
const { collection: { slug } = {}, collection } = props
|
||||
|
||||
const { permissions } = useAuth()
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { forms } = useFormsManager() // Access forms managed in bulk uploads
|
||||
|
||||
const collectionPermissions = permissions?.collections?.[slug]
|
||||
const hasUpdatePermission = collectionPermissions?.update
|
||||
|
||||
const drawerSlug = `edit-${slug}-bulk-uploads`
|
||||
|
||||
if (!hasUpdatePermission) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<DrawerToggler
|
||||
aria-label={t('general:editAll')}
|
||||
className={`${baseClass}__toggle`}
|
||||
slug={drawerSlug}
|
||||
>
|
||||
{t('general:editAll')}
|
||||
</DrawerToggler>
|
||||
<EditDepthProvider>
|
||||
<Drawer Header={null} slug={drawerSlug}>
|
||||
<EditManyBulkUploadsDrawerContent
|
||||
collection={collection}
|
||||
drawerSlug={drawerSlug}
|
||||
forms={forms}
|
||||
/>
|
||||
</Drawer>
|
||||
</EditDepthProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -185,9 +185,14 @@
|
||||
|
||||
&__header__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--base);
|
||||
}
|
||||
|
||||
&__header__addFile {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
&__toggler {
|
||||
display: none;
|
||||
margin: 0;
|
||||
|
||||
@@ -16,9 +16,9 @@ import { Pill } from '../../Pill/index.js'
|
||||
import { ShimmerEffect } from '../../ShimmerEffect/index.js'
|
||||
import { Thumbnail } from '../../Thumbnail/index.js'
|
||||
import { Actions } from '../ActionsBar/index.js'
|
||||
import './index.scss'
|
||||
import { AddFilesView } from '../AddFilesView/index.js'
|
||||
import { useFormsManager } from '../FormsManager/index.js'
|
||||
import './index.scss'
|
||||
import { useBulkUpload } from '../index.js'
|
||||
|
||||
const addMoreFilesDrawerSlug = 'bulk-upload-drawer--add-more-files'
|
||||
@@ -88,8 +88,13 @@ export function FileSidebar() {
|
||||
</div>
|
||||
|
||||
<div className={`${baseClass}__header__actions`}>
|
||||
{typeof maxFiles === 'number' && totalFileCount < maxFiles ? (
|
||||
<Pill onClick={() => openModal(addMoreFilesDrawerSlug)}>{t('upload:addFile')}</Pill>
|
||||
{(typeof maxFiles === 'number' ? totalFileCount < maxFiles : true) ? (
|
||||
<Pill
|
||||
className={`${baseClass}__header__addFile`}
|
||||
onClick={() => openModal(addMoreFilesDrawerSlug)}
|
||||
>
|
||||
{t('upload:addFile')}
|
||||
</Pill>
|
||||
) : null}
|
||||
<Button
|
||||
buttonStyle="transparent"
|
||||
|
||||
@@ -25,6 +25,10 @@ import { formsManagementReducer } from './reducer.js'
|
||||
type FormsManagerContext = {
|
||||
readonly activeIndex: State['activeIndex']
|
||||
readonly addFiles: (filelist: FileList) => Promise<void>
|
||||
readonly bulkUpdateForm: (
|
||||
updatedFields: Record<string, unknown>,
|
||||
afterStateUpdate?: () => void,
|
||||
) => Promise<void>
|
||||
readonly collectionSlug: string
|
||||
readonly docPermissions?: SanitizedDocumentPermissions
|
||||
readonly documentSlots: DocumentSlots
|
||||
@@ -51,6 +55,7 @@ type FormsManagerContext = {
|
||||
const Context = React.createContext<FormsManagerContext>({
|
||||
activeIndex: 0,
|
||||
addFiles: () => Promise.resolve(),
|
||||
bulkUpdateForm: () => null,
|
||||
collectionSlug: '',
|
||||
docPermissions: undefined,
|
||||
documentSlots: {},
|
||||
@@ -300,51 +305,49 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
|
||||
}
|
||||
|
||||
// should expose some sort of helper for this
|
||||
if (json?.errors?.length) {
|
||||
const [fieldErrors, nonFieldErrors] = json.errors.reduce(
|
||||
([fieldErrs, nonFieldErrs], err) => {
|
||||
const newFieldErrs: any[] = []
|
||||
const newNonFieldErrs: any[] = []
|
||||
const [fieldErrors, nonFieldErrors] = (json?.errors || []).reduce(
|
||||
([fieldErrs, nonFieldErrs], err) => {
|
||||
const newFieldErrs: any[] = []
|
||||
const newNonFieldErrs: any[] = []
|
||||
|
||||
if (err?.message) {
|
||||
newNonFieldErrs.push(err)
|
||||
}
|
||||
|
||||
if (Array.isArray(err?.data?.errors)) {
|
||||
err.data?.errors.forEach((dataError) => {
|
||||
if (dataError?.path) {
|
||||
newFieldErrs.push(dataError)
|
||||
} else {
|
||||
newNonFieldErrs.push(dataError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return [
|
||||
[...fieldErrs, ...newFieldErrs],
|
||||
[...nonFieldErrs, ...newNonFieldErrs],
|
||||
]
|
||||
},
|
||||
[[], []],
|
||||
)
|
||||
|
||||
currentForms[i] = {
|
||||
errorCount: fieldErrors.length,
|
||||
formState: fieldReducer(currentForms[i].formState, {
|
||||
type: 'ADD_SERVER_ERRORS',
|
||||
errors: fieldErrors,
|
||||
}),
|
||||
}
|
||||
|
||||
if (req.status === 413) {
|
||||
// file too large
|
||||
currentForms[i] = {
|
||||
...currentForms[i],
|
||||
errorCount: currentForms[i].errorCount + 1,
|
||||
if (err?.message) {
|
||||
newNonFieldErrs.push(err)
|
||||
}
|
||||
|
||||
toast.error(nonFieldErrors[0]?.message)
|
||||
if (Array.isArray(err?.data?.errors)) {
|
||||
err.data?.errors.forEach((dataError) => {
|
||||
if (dataError?.path) {
|
||||
newFieldErrs.push(dataError)
|
||||
} else {
|
||||
newNonFieldErrs.push(dataError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return [
|
||||
[...fieldErrs, ...newFieldErrs],
|
||||
[...nonFieldErrs, ...newNonFieldErrs],
|
||||
]
|
||||
},
|
||||
[[], []],
|
||||
)
|
||||
|
||||
currentForms[i] = {
|
||||
errorCount: fieldErrors.length,
|
||||
formState: fieldReducer(currentForms[i].formState, {
|
||||
type: 'ADD_SERVER_ERRORS',
|
||||
errors: fieldErrors,
|
||||
}),
|
||||
}
|
||||
|
||||
if (req.status === 413) {
|
||||
// file too large
|
||||
currentForms[i] = {
|
||||
...currentForms[i],
|
||||
errorCount: currentForms[i].errorCount + 1,
|
||||
}
|
||||
|
||||
toast.error(nonFieldErrors[0]?.message)
|
||||
}
|
||||
} catch (_) {
|
||||
// swallow
|
||||
@@ -385,6 +388,53 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
|
||||
[actionURL, activeIndex, forms, onSuccess, t, closeModal, drawerSlug],
|
||||
)
|
||||
|
||||
const bulkUpdateForm = React.useCallback(
|
||||
async (updatedFields: Record<string, unknown>, afterStateUpdate?: () => void) => {
|
||||
for (let i = 0; i < forms.length; i++) {
|
||||
Object.entries(updatedFields).forEach(([path, value]) => {
|
||||
if (forms[i].formState[path]) {
|
||||
forms[i].formState[path].value = value
|
||||
|
||||
dispatch({
|
||||
type: 'UPDATE_FORM',
|
||||
errorCount: forms[i].errorCount,
|
||||
formState: forms[i].formState,
|
||||
index: i,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (typeof afterStateUpdate === 'function') {
|
||||
afterStateUpdate()
|
||||
}
|
||||
|
||||
if (hasSubmitted) {
|
||||
const { state } = await getFormState({
|
||||
collectionSlug,
|
||||
docPermissions,
|
||||
docPreferences: null,
|
||||
formState: forms[i].formState,
|
||||
operation: 'create',
|
||||
schemaPath: collectionSlug,
|
||||
})
|
||||
|
||||
const newFormErrorCount = Object.values(state).reduce(
|
||||
(acc, value) => (value?.valid === false ? acc + 1 : acc),
|
||||
0,
|
||||
)
|
||||
|
||||
dispatch({
|
||||
type: 'UPDATE_FORM',
|
||||
errorCount: newFormErrorCount,
|
||||
formState: state,
|
||||
index: i,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
[collectionSlug, docPermissions, forms, getFormState, hasSubmitted],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!collectionSlug) {
|
||||
return
|
||||
@@ -425,6 +475,7 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
|
||||
value={{
|
||||
activeIndex: state.activeIndex,
|
||||
addFiles,
|
||||
bulkUpdateForm,
|
||||
collectionSlug,
|
||||
docPermissions,
|
||||
documentSlots,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FormFieldWithoutComponents, FormState } from 'payload'
|
||||
import type { FormState } from 'payload'
|
||||
|
||||
export type State = {
|
||||
activeIndex: number
|
||||
@@ -15,6 +15,13 @@ type Action =
|
||||
index: number
|
||||
type: 'UPDATE_ERROR_COUNT'
|
||||
}
|
||||
| {
|
||||
errorCount: number
|
||||
formState: FormState
|
||||
index: number
|
||||
type: 'UPDATE_FORM'
|
||||
updatedFields?: Record<string, unknown>
|
||||
}
|
||||
| {
|
||||
files: FileList
|
||||
initialState: FormState | null
|
||||
@@ -99,6 +106,25 @@ export function formsManagementReducer(state: State, action: Action): State {
|
||||
totalErrorCount: state.forms.reduce((acc, form) => acc + form.errorCount, 0),
|
||||
}
|
||||
}
|
||||
case 'UPDATE_FORM': {
|
||||
const updatedForms = [...state.forms]
|
||||
updatedForms[action.index].errorCount = action.errorCount
|
||||
|
||||
// Merge the existing formState with the new formState
|
||||
updatedForms[action.index] = {
|
||||
...updatedForms[action.index],
|
||||
formState: {
|
||||
...updatedForms[action.index].formState,
|
||||
...action.formState,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
forms: updatedForms,
|
||||
totalErrorCount: state.forms.reduce((acc, form) => acc + form.errorCount, 0),
|
||||
}
|
||||
}
|
||||
default: {
|
||||
return state
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import '../../../scss/styles.scss';
|
||||
|
||||
@layer payload-default {
|
||||
.bulk-upload--drawer-header {
|
||||
display: flex;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import { DrawerCloseButton } from '../DrawerCloseButton/index.js'
|
||||
|
||||
@@ -36,7 +36,7 @@ import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
|
||||
import { upsertPrefs } from 'helpers/e2e/upsertPrefs.js'
|
||||
import { RESTClient } from 'helpers/rest.js'
|
||||
import { GeneratedTypes } from 'helpers/sdk/types.js'
|
||||
import { wait } from 'payload/shared'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
@@ -49,7 +49,7 @@ const dirname = path.dirname(filename)
|
||||
* Repeat above for Globals
|
||||
*/
|
||||
|
||||
const { beforeAll, beforeEach, describe, afterEach } = test
|
||||
const { beforeAll, beforeEach, describe } = test
|
||||
let url: AdminUrlUtil
|
||||
let urlWithRequiredLocalizedFields: AdminUrlUtil
|
||||
let urlRelationshipLocalized: AdminUrlUtil
|
||||
|
||||
@@ -25,6 +25,11 @@ export const Uploads2: CollectionConfig = {
|
||||
enableRichTextRelationship: false,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'prefix',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'title',
|
||||
|
||||
@@ -413,7 +413,7 @@ describe('Uploads', () => {
|
||||
// save the document and expect an error
|
||||
await page.locator('button#action-save').click()
|
||||
await expect(page.locator('.payload-toast-container .toast-error')).toContainText(
|
||||
'The following field is invalid: audio',
|
||||
'The following field is invalid: Audio',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -689,6 +689,171 @@ describe('Uploads', () => {
|
||||
await expect(clientText).toHaveText('This text was rendered on the client')
|
||||
})
|
||||
|
||||
describe('bulk uploads', () => {
|
||||
test('should bulk upload multiple files', async () => {
|
||||
// Navigate to the upload creation page
|
||||
await page.goto(uploadsOne.create)
|
||||
await page.waitForURL(uploadsOne.create)
|
||||
|
||||
// Upload single file
|
||||
await page.setInputFiles(
|
||||
'.file-field input[type="file"]',
|
||||
path.resolve(dirname, './image.png'),
|
||||
)
|
||||
const filename = page.locator('.file-field__filename')
|
||||
await expect(filename).toHaveValue('image.png')
|
||||
|
||||
const bulkUploadButton = page.locator('#field-hasManyUpload button', {
|
||||
hasText: exactText('Create New'),
|
||||
})
|
||||
await bulkUploadButton.click()
|
||||
|
||||
const bulkUploadModal = page.locator('#bulk-upload-drawer-slug-1')
|
||||
await expect(bulkUploadModal).toBeVisible()
|
||||
|
||||
// Bulk upload multiple files at once
|
||||
await page.setInputFiles('#bulk-upload-drawer-slug-1 .dropzone input[type="file"]', [
|
||||
path.resolve(dirname, './image.png'),
|
||||
path.resolve(dirname, './test-image.png'),
|
||||
])
|
||||
|
||||
await page
|
||||
.locator('.bulk-upload--file-manager .render-fields #field-prefix')
|
||||
.fill('prefix-one')
|
||||
|
||||
const nextImageChevronButton = page.locator(
|
||||
'.bulk-upload--actions-bar__controls button:nth-of-type(2)',
|
||||
)
|
||||
await nextImageChevronButton.click()
|
||||
|
||||
await page
|
||||
.locator('.bulk-upload--file-manager .render-fields #field-prefix')
|
||||
.fill('prefix-two')
|
||||
|
||||
const saveButton = page.locator('.bulk-upload--actions-bar__saveButtons button')
|
||||
await saveButton.click()
|
||||
|
||||
await page.waitForSelector('#field-hasManyUpload .upload--has-many__dragItem')
|
||||
const itemCount = await page
|
||||
.locator('#field-hasManyUpload .upload--has-many__dragItem')
|
||||
.count()
|
||||
expect(itemCount).toEqual(2)
|
||||
|
||||
await saveDocAndAssert(page)
|
||||
})
|
||||
|
||||
test('should apply field value to all bulk upload files after edit many', async () => {
|
||||
// Navigate to the upload creation page
|
||||
await page.goto(uploadsOne.create)
|
||||
await page.waitForURL(uploadsOne.create)
|
||||
|
||||
// Upload single file
|
||||
await page.setInputFiles(
|
||||
'.file-field input[type="file"]',
|
||||
path.resolve(dirname, './image.png'),
|
||||
)
|
||||
const filename = page.locator('.file-field__filename')
|
||||
await expect(filename).toHaveValue('image.png')
|
||||
|
||||
const bulkUploadButton = page.locator('#field-hasManyUpload button', {
|
||||
hasText: exactText('Create New'),
|
||||
})
|
||||
await bulkUploadButton.click()
|
||||
|
||||
const bulkUploadModal = page.locator('#bulk-upload-drawer-slug-1')
|
||||
await expect(bulkUploadModal).toBeVisible()
|
||||
|
||||
// Bulk upload multiple files at once
|
||||
await page.setInputFiles('#bulk-upload-drawer-slug-1 .dropzone input[type="file"]', [
|
||||
path.resolve(dirname, './image.png'),
|
||||
path.resolve(dirname, './test-image.png'),
|
||||
])
|
||||
|
||||
await page.locator('#bulk-upload-drawer-slug-1 .edit-many-bulk-uploads__toggle').click()
|
||||
const editManyBulkUploadModal = page.locator('#edit-uploads-2-bulk-uploads')
|
||||
await expect(editManyBulkUploadModal).toBeVisible()
|
||||
|
||||
const fieldSelector = page.locator('.edit-many-bulk-uploads__form .react-select')
|
||||
await fieldSelector.click({ delay: 100 })
|
||||
const options = page.locator('.rs__option')
|
||||
// Select an option
|
||||
await options.locator('text=Prefix').click()
|
||||
|
||||
await page.locator('#edit-uploads-2-bulk-uploads #field-prefix').fill('some prefix')
|
||||
|
||||
await page.locator('.edit-many-bulk-uploads__sidebar-wrap button').click()
|
||||
|
||||
const saveButton = page.locator('.bulk-upload--actions-bar__saveButtons button')
|
||||
await saveButton.click()
|
||||
|
||||
await page.waitForSelector('#field-hasManyUpload .upload--has-many__dragItem')
|
||||
const itemCount = await page
|
||||
.locator('#field-hasManyUpload .upload--has-many__dragItem')
|
||||
.count()
|
||||
expect(itemCount).toEqual(2)
|
||||
|
||||
await saveDocAndAssert(page)
|
||||
})
|
||||
|
||||
test('should remove validation errors from bulk upload files after correction in edit many drawer', async () => {
|
||||
// Navigate to the upload creation page
|
||||
await page.goto(uploadsOne.create)
|
||||
await page.waitForURL(uploadsOne.create)
|
||||
|
||||
// Upload single file
|
||||
await page.setInputFiles(
|
||||
'.file-field input[type="file"]',
|
||||
path.resolve(dirname, './image.png'),
|
||||
)
|
||||
const filename = page.locator('.file-field__filename')
|
||||
await expect(filename).toHaveValue('image.png')
|
||||
|
||||
const bulkUploadButton = page.locator('#field-hasManyUpload button', {
|
||||
hasText: exactText('Create New'),
|
||||
})
|
||||
await bulkUploadButton.click()
|
||||
|
||||
const bulkUploadModal = page.locator('#bulk-upload-drawer-slug-1')
|
||||
await expect(bulkUploadModal).toBeVisible()
|
||||
|
||||
// Bulk upload multiple files at once
|
||||
await page.setInputFiles('#bulk-upload-drawer-slug-1 .dropzone input[type="file"]', [
|
||||
path.resolve(dirname, './image.png'),
|
||||
path.resolve(dirname, './test-image.png'),
|
||||
])
|
||||
|
||||
const saveButton = page.locator('.bulk-upload--actions-bar__saveButtons button')
|
||||
await saveButton.click()
|
||||
await expect(page.locator('.payload-toast-container')).toContainText('Failed to save 2 files')
|
||||
|
||||
const errorCount = page
|
||||
.locator('#bulk-upload-drawer-slug-1 .file-selections .error-pill__count')
|
||||
.first()
|
||||
await expect(errorCount).toHaveText('2')
|
||||
|
||||
await page.locator('#bulk-upload-drawer-slug-1 .edit-many-bulk-uploads__toggle').click()
|
||||
const editManyBulkUploadModal = page.locator('#edit-uploads-2-bulk-uploads')
|
||||
await expect(editManyBulkUploadModal).toBeVisible()
|
||||
|
||||
const fieldSelector = page.locator('.edit-many-bulk-uploads__form .react-select')
|
||||
await fieldSelector.click({ delay: 100 })
|
||||
const options = page.locator('.rs__option')
|
||||
// Select an option
|
||||
await options.locator('text=Prefix').click()
|
||||
|
||||
await page.locator('#edit-uploads-2-bulk-uploads #field-prefix').fill('some prefix')
|
||||
|
||||
await page.locator('.edit-many-bulk-uploads__sidebar-wrap button').click()
|
||||
|
||||
await saveButton.click()
|
||||
await expect(page.locator('.payload-toast-container')).toContainText(
|
||||
'Successfully saved 2 files',
|
||||
)
|
||||
|
||||
await saveDocAndAssert(page)
|
||||
})
|
||||
})
|
||||
|
||||
describe('remote url fetching', () => {
|
||||
beforeAll(async () => {
|
||||
mockCorsServer = startMockCorsServer()
|
||||
|
||||
@@ -959,6 +959,7 @@ export interface Uploads1 {
|
||||
*/
|
||||
export interface Uploads2 {
|
||||
id: string;
|
||||
prefix: string;
|
||||
title?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -2280,6 +2281,7 @@ export interface Uploads1Select<T extends boolean = true> {
|
||||
* via the `definition` "uploads-2_select".
|
||||
*/
|
||||
export interface Uploads2Select<T extends boolean = true> {
|
||||
prefix?: T;
|
||||
title?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
|
||||
BIN
test/uploads/test-image.png
Normal file
BIN
test/uploads/test-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Reference in New Issue
Block a user