Skip to content

[Batch Edit] Edit multiple languages of a localizable asset metadata field#3899

Open
xIrusux wants to merge 11 commits into
2026.xfrom
feature/batch-edit-multi-locale-assets-documents
Open

[Batch Edit] Edit multiple languages of a localizable asset metadata field#3899
xIrusux wants to merge 11 commits into
2026.xfrom
feature/batch-edit-multi-locale-assets-documents

Conversation

@xIrusux

@xIrusux xIrusux commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

What

Extends the multi-language batch-edit to assets: a localizable metadata field can now be edited in several locales at once, one input row per locale.

UX

  • Selecting a localizable metadata field adds an input row; the field stays in the "fields to add" panel so more locales can be added.
  • Each click adds a row for the next unused locale (the first row defaults to "no language", matching current asset behaviour).
  • Each row has its own locale picker that excludes locales already used by the field's other rows.
  • The field leaves the panel once every locale has a row (each content language + the "no language" row).

How

  • Batch-edit entries are identified by (key + locale) instead of key alone, so one field can hold multiple locale rows (hooks/use-batch-edit.ts).
  • Locale candidates come from user.contentLanguages via PermissionBasedLanguageSelectionControl, matching the grid language switcher and column config.
  • Each localizable row's fields render under a Form.Group keyed by locale, so rows sharing a field key don't collide on the flat metadata name — no shared field components changed.
  • onFinish rebuilds one PatchCustomMetadata { name, language, data, type } per row; language: null (language-neutral) is kept as a valid value.

Scope

Assets only. Documents were evaluated and excluded — they have no editable/localizable listing (no batch-edit substrate), so the feature does not apply there. Sibling of the data-object multi-locale change.

Verification

tsc + eslint clean. Manual QA recommended: add a localizable metadata field, add multiple locale rows, confirm used-locale exclusion, and that saving writes one metadata patch per (name, language).

🤖 Generated with Claude Code

xIrusux and others added 2 commits July 16, 2026 13:01
…field (#2492)

Assets could only batch-edit a single locale of a localizable metadata field. Mirror the
data-object multi-row model: entries are identified by (key + locale), each field click adds
a row for the next unused locale, each row has its own content-language picker that excludes
locales used by the field's other rows, and the field leaves the "fields to add" panel once
every locale has a row.

Asset metadata differs from object localizedfields: values are submitted as flat
PatchCustomMetadata { name, language, data, type } and language: null (language-neutral) is a
valid value. Each localizable row's fields are namespaced under a Form.Group keyed by locale
so rows don't collide on the flat metadata name, and the null "no language" row is kept.
Locale candidates come from user.contentLanguages via PermissionBasedLanguageSelectionControl,
matching the grid language switcher and column config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove verbose and issue-reference comments, keeping only minimal notes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 16, 2026 14:48
@xIrusux xIrusux self-assigned this Jul 16, 2026
@xIrusux xIrusux added this to the 2026.3.0 milestone Jul 16, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extends asset batch editing to support multiple locale rows per localizable metadata field.

Changes:

  • Identifies batch edits by field and locale.
  • Adds locale-aware form grouping and filtering.
  • Builds one metadata patch per locale row.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
dropdown-filter.ts Keeps fields available until all locales are added.
use-batch-edit.ts Manages locale-specific batch-edit rows.
batch-edit-provider.tsx Adds the language-neutral form key.
batch-edit-modal.tsx Extracts locale-grouped values into patches.
batch-edit-list-container.tsx Renders locale pickers and grouped editors.

Comment on lines +41 to +42
const localeFormKey = batchEdit.locale ?? NO_LOCALE_FORM_KEY
const rowKey = batchEdit.localizable ? `${batchEdit.key}-${localeFormKey}` : batchEdit.key
Comment on lines +44 to +48
const body = batchEdit.localizable
? (
<Form.Group name={ [localeFormKey] }>
<DefaultBatchEdit batchEdit={ batchEdit } />
</Form.Group>
// Namespace each localizable row's fields under a Form.Group keyed by locale so rows sharing
// a field key don't collide on the flat metadata name.
const localeFormKey = batchEdit.locale ?? NO_LOCALE_FORM_KEY
const rowKey = batchEdit.localizable ? `${batchEdit.key}-${localeFormKey}` : batchEdit.key
Copilot AI review requested due to automatic review settings July 16, 2026 14:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 2 comments.

Comment on lines +63 to +64
onChange={ (language) => {
updateLocale(batchEdit, language)
Comment on lines +49 to +50
const candidateLocales: Array<string | null> = [null, ...contentLanguages]
const nextLocale = candidateLocales.find(locale => !usedLocales.includes(locale))
Use !== instead of negated ===; use a Set for used-locale lookup. Drop a redundant comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 16, 2026 15:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.

Comment on lines +39 to +44
const localeFormKey = batchEdit.locale ?? NO_LOCALE_FORM_KEY
const rowKey = batchEdit.localizable ? `${batchEdit.key}-${localeFormKey}` : batchEdit.key

const body = batchEdit.localizable
? (
<Form.Group name={ [localeFormKey] }>
Copilot AI review requested due to automatic review settings July 16, 2026 15:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.

Comment on lines +57 to 65
<PermissionBasedLanguageSelectionControl
excludeLocales={ usedByOtherRows }
isNullable={ !isNullUsedByOtherRows }
key="language-selection"
languages={ languages }
onSelectLanguage={ (language) => {
updateLocale(batchEdit.key, transformLanguage(language))
onChange={ (language) => {
updateLocale(batchEdit, language)
} }
selectedLanguage={ selectedLanguage }
value={ batchEdit.locale ?? null }
/>
Copilot AI review requested due to automatic review settings July 16, 2026 16:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.

const addOrUpdateBatchEdit = (column: AvailableColumn): void => {
const newEdit: BatchEdit = column
// Each click on a localizable field adds a row for the next unused locale (null first).
if (column.localizable) {
Copilot AI review requested due to automatic review settings July 16, 2026 16:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.

Comment on lines +12 to +36
import { useElementContext } from '@Pimcore/modules/element/hooks/use-element-context'
import { useElementDraft } from '@Pimcore/modules/element/hooks/use-element-draft'

/**
* The content languages the current user may view/edit for the current element, i.e. the user's
* content languages narrowed by the element's `localizedView` permission. Requires an element
* context. This is the same set PermissionBasedLanguageSelectionControl offers, so callers that
* need to stay in sync with that picker (e.g. deriving available locales) should use it too.
*/
export const usePermittedContentLanguages = (): string[] => {
const user = useUser()
const elementContext = useElementContext()
const elementDraft = useElementDraft(elementContext.id, elementContext.elementType)
const contentLanguages = Array.isArray(user.contentLanguages) ? user.contentLanguages as string[] : []

if (!('permissions' in elementDraft)) {
return contentLanguages
}

const permissions = elementDraft.permissions as Record<string, any>
const viewableLanguages: string[] = permissions?.localizedView?.split(',') ?? []

if (viewableLanguages.length === 0 || (viewableLanguages.length === 1 && viewableLanguages[0] === 'default')) {
return contentLanguages
}
Comment on lines +42 to +44
if (column.localizable) {
const usedLocales = new Set(
batchEdits.filter(edit => edit.key === column.key).map(edit => edit.locale ?? null)
Comment on lines +28 to +31
const siblingLocales = batchEdit.localizable
? batchEdits
.filter(edit => edit.key === batchEdit.key && (edit.locale ?? null) !== (batchEdit.locale ?? null))
.map(edit => edit.locale ?? null)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but we use uuids they are always uniqe

Comment on lines +57 to +61
const updatedEdits: BatchEdit[] = [...batchEdits]
const existingIndex = batchEdits.findIndex(edit => edit.key === column.key)

if (existingIndex !== -1) {
updatedEdits[existingIndex] = newEdit
updatedEdits[existingIndex] = { ...column, rowId: batchEdits[existingIndex].rowId, locale: null }
…tity

Revert the usePermittedContentLanguages hook and shared with-element-context change
(the localizedView filter is a no-op — tracked separately); use user.contentLanguages
directly. Match batch-edit entry identity by key + group (and rowId for self-exclusion),
consistent with the fields-to-add filter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 16, 2026 18:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 2 comments.

Copilot AI review requested due to automatic review settings July 16, 2026 18:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 3 comments.


export const useBatchEdit = (): UseBatchEditHookReturn => {
const { batchEdits, setBatchEdits } = useContext(BatchEditContext)
const contentLanguages = (useUser().contentLanguages ?? []) as string[]
const { getArgs } = useDataQueryHelper()
const { hasType } = useDynamicTypeResolver()
const { refreshGrid } = useRefreshGrid(elementType)
const contentLanguages = (useUser().contentLanguages ?? []) as string[]
user.contentLanguages is typed as object; guard with Array.isArray before spreading
so a non-array value can't break adding rows or the completion check. Also correct the
error useEffect deps so a folder-patch failure is always reported.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 16, 2026 18:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings July 16, 2026 18:50
@sonarqubecloud

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.

.filter(edit => edit.key === column.key && areGroupsEqual(edit.group, column.group))
.map(edit => edit.locale ?? null)
)
const candidateLocales: Array<string | null> = [null, ...contentLanguages]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants