Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -15,41 +15,67 @@ import { ButtonGroup } from '@Pimcore/components/button-group/button-group'
import { IconButton } from '@Pimcore/components/icon-button/icon-button'
import { NoContent } from '@Pimcore/components/no-content/no-content'
import { t } from 'i18next'
import { LanguageSelection } from '@Pimcore/components/language-selection/language-selection'
import { transformLanguage } from '@Pimcore/components/language-selection/helpers'
import { useSettings } from '@Pimcore/modules/app/settings/hooks/use-settings'
import { PermissionBasedLanguageSelectionControl } from '@Pimcore/modules/element/components/language-selection/permission-based-language-selection-control'
import { Form } from '@Pimcore/components/form/form'
import { useBatchEdit } from './hooks/use-batch-edit'
import { type BatchEdit } from './batch-edit-provider'
import { areGroupsEqual } from './utils/dropdown-filter'
import { DefaultBatchEdit } from './default-batch-edit'
import { hasFieldDefinition } from '@Pimcore/modules/element/listing/decorators/utils/column-configuration/has-field-definition'

export const BatchEditListContainer = (): React.JSX.Element => {
const { batchEdits, removeBatchEdit } = useBatchEdit()
const { updateLocale } = useBatchEdit()
const settings = useSettings()
const { batchEdits, removeBatchEdit, updateLocale } = useBatchEdit()
const form = Form.useFormInstance()

const languages = settings.requiredLanguages
// Localized values are stored in the form under ['localizedfields', key, locale] and the
// whole form tree is submitted. Changing a row's locale remounts the field at a new path, so
// carry the value over to the new locale (and clear the old one). Only regular localizable
// fields use this path; classification-store / object-brick build their own name paths.
const migrateLocaleValue = (batchEdit: BatchEdit, nextLocale: string | null): void => {
const isRegularLocalizable = batchEdit.localizable &&
batchEdit.type !== 'dataobject.classificationstore' &&
batchEdit.type !== 'dataobject.objectbrick'
Comment on lines +35 to +37
Comment on lines +35 to +37

if (!isRegularLocalizable || batchEdit.locale == null || nextLocale == null || nextLocale === batchEdit.locale) {
return
}

const oldPath = ['localizedfields', batchEdit.key, batchEdit.locale]
const newPath = ['localizedfields', batchEdit.key, nextLocale]
form.setFieldValue(newPath, form.getFieldValue(oldPath))
form.setFieldValue(oldPath, undefined)
Comment on lines +43 to +46
}

const items: StackListProps['items'] = batchEdits.map((batchEdit) => {
// @todo infer selected language from grid config when available
const selectedLanguage = batchEdit.locale ?? settings.requiredLanguages[0]
// A localizable field can have one row per locale. Exclude locales already used by the
// field's other rows so the same locale can't be picked twice.
const usedByOtherRows = batchEdit.type === 'dataobject.classificationstore'
? []
: batchEdits
.filter(edit => edit.key === batchEdit.key && areGroupsEqual(edit.group, batchEdit.group) && edit.locale !== batchEdit.locale)
.map(edit => edit.locale)
.filter((locale): locale is string => locale !== null)

const batchEditTitle = hasFieldDefinition(batchEdit.config) ? (batchEdit.config.fieldDefinition as { title: string }).title : batchEdit.key
const key = batchEdit.type === 'dataobject.classificationstore' ? `${batchEdit.key}-${(batchEdit.config as { keyId: number }).keyId}-${(batchEdit.config as { groupId: number }).groupId}` : batchEdit.key
const baseKey = batchEdit.type === 'dataobject.classificationstore' ? `${batchEdit.key}-${(batchEdit.config as { keyId: number }).keyId}-${(batchEdit.config as { groupId: number }).groupId}` : batchEdit.key
const key = batchEdit.localizable ? `${baseKey}-${batchEdit.locale ?? ''}` : baseKey
Comment on lines +60 to +61

return ({
id: `${batchEdit.key}`,
id: key,
key,
children: <Tag>{t(`${batchEditTitle}`)}</Tag>,
renderRightToolbar: <ButtonGroup items={
[...(batchEdit.localizable
? [
<LanguageSelection
<PermissionBasedLanguageSelectionControl
customKeys={ batchEdit.type === 'dataobject.classificationstore' ? ['default'] : [] }
excludeLocales={ usedByOtherRows }
key="language-selection"
languages={ languages }
onSelectLanguage={ (language) => {
updateLocale(batchEdit, transformLanguage(language))
onChange={ (language) => {
migrateLocaleValue(batchEdit, language)
updateLocale(batchEdit, language)
Comment on lines +74 to +76
} }
selectedLanguage={ selectedLanguage }
value={ batchEdit.locale }
/>
]
: []),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { type AvailableColumn, buildColumnPickerGroups } from '@Pimcore/modules/
import { useRowSelection } from '@Pimcore/modules/element/listing/decorators/row-selection/context-layer/provider/use-row-selection'
import { api, useDataObjectPatchByIdMutation, useDataObjectPatchFolderByIdMutation } from '@Pimcore/modules/data-object/data-object-api-slice-enhanced'
import { useSettings } from '@Pimcore/modules/element/listing/abstract/settings/use-settings'
import { useUser } from '@Pimcore/modules/auth/hooks/use-user'
import { useElementContext } from '@Pimcore/modules/element/hooks/use-element-context'
import trackError, { ApiError } from '@Pimcore/modules/app/error-handler'
import { invalidatingTags } from '@Pimcore/app/api/pimcore/tags'
Expand Down Expand Up @@ -57,6 +58,8 @@ export const BatchEditModal = ({ batchEditModalOpen, setBatchEditModalOpen }: Ba
const [patchObjectsInFolder, { error: folderPatchError, isError: isFolderPatchError, isSuccess: isFolderPatchSuccess }] = useDataObjectPatchFolderByIdMutation()
const [patchObjectsByIds, { error: idPatchError, isError: isIdPatchError, isSuccess: isIdPatchSuccess }] = useDataObjectPatchByIdMutation()
const { useDataQueryHelper } = useSettings()
const user = useUser()
const contentLanguages = Array.isArray(user.contentLanguages) ? user.contentLanguages as string[] : []
const { getArgs } = useDataQueryHelper()
const { id, elementType } = useElementContext()
const dispatch = useAppDispatch()
Expand Down Expand Up @@ -204,11 +207,11 @@ export const BatchEditModal = ({ batchEditModalOpen, setBatchEditModalOpen }: Ba

const columnGroups = useMemo(() => {
const includableColumns = availableColumns.filter((column) =>
shouldIncludeColumnItem({ ...column, mainType: column.type }, batchEdits, hasType, getType)
shouldIncludeColumnItem({ ...column, mainType: column.type }, batchEdits, hasType, getType, contentLanguages)
)

return buildColumnPickerGroups(includableColumns, t)
}, [availableColumns, batchEdits, hasType, getType])
}, [availableColumns, batchEdits, hasType, getType, contentLanguages])

return (
<FieldCollectionProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
import { useContext } from 'react'
import { type BatchContext, type BatchEdit, BatchEditContext } from '../batch-edit-provider'
import { type AvailableColumn } from '@Pimcore/modules/element/listing/decorators/utils/column-configuration/context-layer/provider/available-columns/available-columns-provider'
import { useSettings } from '@Pimcore/modules/app/settings/hooks/use-settings'
import { useUser } from '@Pimcore/modules/auth/hooks/use-user'
import { areGroupsEqual } from '../utils/dropdown-filter'

interface UseBatchEditHookReturn extends BatchContext {
addOrUpdateBatchEdit: (column: AvailableColumn, value: BatchEdit['value']) => void
Expand All @@ -21,41 +22,58 @@ interface UseBatchEditHookReturn extends BatchContext {
removeBatchEdit: (batchEdit: BatchEdit) => void
}

const isSameEntry = (a: BatchEdit, b: BatchEdit): boolean => {
if (a.key !== b.key || (a.locale ?? null) !== (b.locale ?? null) || !areGroupsEqual(a.group, b.group)) {
return false
}

if (a.type === 'dataobject.classificationstore' || b.type === 'dataobject.classificationstore') {
const aConfig = a.config as { keyId?: unknown, groupId?: unknown }
const bConfig = b.config as { keyId?: unknown, groupId?: unknown }

return aConfig.keyId === bConfig.keyId && aConfig.groupId === bConfig.groupId
}

return true
}

export const useBatchEdit = (): UseBatchEditHookReturn => {
const { batchEdits, setBatchEdits } = useContext(BatchEditContext)
const settings = useSettings()
const user = useUser()
const contentLanguages = Array.isArray(user.contentLanguages) ? user.contentLanguages as string[] : []

const resetBatchEdits = (): void => {
setBatchEdits([])
}

const updateLocale = (batchEdit: BatchEdit, locale: string | null): void => {
const columnKey = batchEdit.key

const updatedEdits = batchEdits.map(edit => {
if (edit.key === columnKey) {
return {
...edit,
locale
}
}

return edit
})
const updatedEdits = batchEdits.map(edit =>
isSameEntry(edit, batchEdit) ? { ...edit, locale } : edit
)
setBatchEdits(updatedEdits)
}

const addOrUpdateBatchEdit = (column: AvailableColumn, value: BatchEdit['value']): void => {
const newEdit: BatchEdit = {
...column,
// @todo infer selected language from grid config when available
locale: column.localizable ? column.locale ?? settings.requiredLanguages[0] : null,
value
if (column.localizable) {
const usedLocales = new Set(
batchEdits
.filter(edit => edit.key === column.key && areGroupsEqual(edit.group, column.group))
.map(edit => edit.locale)
)
const nextLocale = contentLanguages.find(language => !usedLocales.has(language))

if (nextLocale === undefined) {
return
}

setBatchEdits([...batchEdits, { ...column, locale: nextLocale, value }])
return
Comment on lines +58 to +70
}

const updatedEdits: BatchEdit[] = [...batchEdits]
const newEdit: BatchEdit = { ...column, locale: null, value }

const existingIndex = batchEdits.findIndex(edit => edit.key === newEdit.key)
const updatedEdits: BatchEdit[] = [...batchEdits]
const existingIndex = batchEdits.findIndex(edit => edit.key === newEdit.key && areGroupsEqual(edit.group, newEdit.group))

if (existingIndex !== -1) {
updatedEdits[existingIndex] = newEdit
Expand All @@ -72,8 +90,7 @@ export const useBatchEdit = (): UseBatchEditHookReturn => {
columns.forEach(column => {
const newEdit: BatchEdit = {
...column,
// @todo infer selected language from grid config when available
locale: column.localizable ? column.locale ?? settings.requiredLanguages[0] : null,
locale: column.localizable ? column.locale ?? contentLanguages[0] : null,
value: undefined
}
Comment on lines 91 to 95

Expand All @@ -90,17 +107,7 @@ export const useBatchEdit = (): UseBatchEditHookReturn => {
}

const removeBatchEdit = (batchEdit: BatchEdit): void => {
const updatedEdits = batchEdits.filter(edit => {
if (batchEdit.type === 'dataobject.classificationstore') {
if (!('keyId' in edit.config) || !('groupId' in edit.config) || !('keyId' in batchEdit.config) || !('groupId' in batchEdit.config)) {
throw new Error('keyId or groupId is missing in config')
}

return !(edit.key === batchEdit.key && edit.config.keyId === batchEdit.config.keyId && edit.config.groupId === batchEdit.config.groupId)
}

return edit.key !== batchEdit.key
})
const updatedEdits = batchEdits.filter(edit => !isSameEntry(edit, batchEdit))
setBatchEdits(updatedEdits)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,18 @@ export const shouldIncludeColumnItem = (
item: any,
batchEdits: BatchEdit[],
hasType: (props: { target: string, dynamicTypeIds: string[] }) => boolean,
getType: UseDynamicTypeResolverReturnType['getType']
getType: UseDynamicTypeResolverReturnType['getType'],
contentLanguages: string[] = []
): boolean => {
const isEditable: boolean = item.editable === true
const isAlreadyInBatchEditList = batchEdits.some(batchItem =>
item.key === batchItem.key && areGroupsEqual(item.group, batchItem.group) && item.mainType !== 'dataobject.classificationstore'
const existingEntries = batchEdits.filter(batchItem =>
item.key === batchItem.key && areGroupsEqual(item.group, batchItem.group)
Comment on lines +48 to +52
)

const isAlreadyInBatchEditList = item.mainType !== 'dataobject.classificationstore' && (
item.localizable === true
? contentLanguages.length > 0 && existingEntries.length >= contentLanguages.length
: existingEntries.length > 0
)
Comment on lines +55 to 59
Comment on lines +55 to 59
Comment on lines +55 to 59

const hasDynamicType = hasType({
Expand Down
Binary file not shown.