Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
53ea2b7
PoC
jackmcdade Feb 13, 2026
7713aca
Confirm discarding changes when switching locales
jackmcdade Feb 13, 2026
4f5695c
Fix field syncing
jackmcdade Feb 13, 2026
07446a8
DB migration now aware of container and localizable scope
jackmcdade Feb 13, 2026
5ffdfa8
Ensure axios responses are in order
jackmcdade Feb 13, 2026
ecf2cff
prevent infinite loop
jackmcdade Feb 13, 2026
9a5f823
avoid infinite loops
jackmcdade Feb 13, 2026
6f40e85
Fix eloquent meta migration types
jackmcdade Feb 13, 2026
59b8241
Dont't assume migration's updated_at column exists
jackmcdade Feb 13, 2026
c33f8d1
Ensure valid site handle
jackmcdade Feb 13, 2026
c181368
Ensure legacy metadata doesn't get dropped during normalization
jackmcdade Feb 13, 2026
3ee6661
fix another infinite loop issue
jackmcdade Feb 13, 2026
a7ee572
use public accessor, not private method
jackmcdade Feb 14, 2026
bd824f3
Fix asset metadata persisting inherited values on save
jackmcdade Feb 14, 2026
627e342
Don't navigate left/right between assets if focused in a field
jackmcdade Feb 14, 2026
497f2ac
Keep focus on the default locale
jackmcdade Feb 14, 2026
0fa7c1e
Fix cursorbot suggestions
jackmcdade Feb 14, 2026
89cb022
Fix reupload meta data
jackmcdade Feb 14, 2026
3230806
Fix missing asset existence guard before localization
jackmcdade Feb 14, 2026
211c084
Fix code style in FieldtypeController
jackmcdade Feb 14, 2026
c0c83dc
add uncommitted test
jackmcdade Feb 14, 2026
007d581
Fix asset editor crash when load fails
jackmcdade Feb 16, 2026
3f03e19
Merge branch '6.x' into localizable-asset-meta-data
jackmcdade Feb 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 123 additions & 17 deletions resources/js/components/assets/Editor/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@
<Icon name="loading" />
</div>

<template v-if="!loading">
<template v-else-if="!loading && !asset">
<div class="flex flex-1 flex-col items-center justify-center gap-4 p-8">
<p class="text-sm text-gray-600 dark:text-gray-400">{{ loadError || __('Unable to load asset') }}</p>
<ui-button variant="primary" @click="load(activeSite)" :text="__('Retry')" />
</div>
</template>

<template v-if="!loading && asset">
<!-- Header -->
<header id="asset-editor-header" class="relative flex w-full justify-between px-2">
<button
Expand All @@ -31,6 +38,7 @@
<!-- Toolbar -->
<div v-if="isToolbarVisible" class="@container/toolbar dark flex flex-wrap items-center justify-center gap-2 px-2 py-4">
<ItemActions
v-if="Array.isArray(actions)"
:item="id"
:url="actionUrl"
:actions="actions"
Expand Down Expand Up @@ -116,6 +124,7 @@
</div>
</div>


<!-- Fields Area -->
<PublishContainer
v-if="fields"
Expand All @@ -127,13 +136,27 @@
:model-value="values"
:extra-values="extraValues"
:meta="meta"
:origin-values="originValues"
:origin-meta="originMeta"
:site="activeSite"
v-model:modified-fields="localizedFields"
:sync-field-confirmation-text="syncFieldConfirmationText"
:errors="errors"
@update:model-value="updateValues"
>
<div class="h-1/2 w-full overflow-scroll sm:p-4 md:h-full md:w-1/3 md:grow md:pt-px">
<div v-if="saving" class="loading">
<Icon name="loading" />
</div>
<ui-panel class="flex items-center justify-between">
<ui-heading size="lg" :text="__('Localizations')" class="ps-2" />
<SiteSelector
v-if="showLocalizationSelector"
:sites="localizations"
:model-value="activeSite"
@update:modelValue="localizationSelected"
/>
</ui-panel>

<PublishTabs />
</div>
Expand All @@ -160,7 +183,7 @@
</template>

<focal-point-editor
v-if="showFocalPointEditor && isFocalPointEditorEnabled"
v-if="asset && showFocalPointEditor && isFocalPointEditorEnabled"
:data="values.focus"
:image="asset.preview"
@selected="selectFocalPoint"
Expand All @@ -176,13 +199,24 @@
@confirm="confirmCloseWithChanges"
@cancel="closingWithChanges = false"
/>

<confirmation-modal
:open="!!pendingSiteSwitch"
:title="__('Unsaved Changes')"
:body-text="__('Are you sure? Unsaved changes will be lost.')"
:button-text="__('Continue')"
:danger="true"
@confirm="confirmSwitchSite"
@cancel="pendingSiteSwitch = null"
/>
</div>
</Stack>
</template>

<script>
import FocalPointEditor from './FocalPointEditor.vue';
import PdfViewer from './PdfViewer.vue';
import SiteSelector from '../../SiteSelector.vue';
import { pick, flatten } from 'lodash-es';
import {
Dropdown,
Expand All @@ -192,6 +226,7 @@ import {
PublishTabs,
Icon,
Stack,
Panel,
} from '@ui';
import ItemActions from '@/components/actions/ItemActions.vue';

Expand All @@ -205,6 +240,7 @@ export default {
ItemActions,
FocalPointEditor,
PdfViewer,
SiteSelector,
PublishContainer,
PublishTabs,
Icon,
Expand All @@ -225,6 +261,10 @@ export default {
return true;
},
},
site: {
type: String,
default: null,
},
},

data() {
Expand All @@ -244,12 +284,22 @@ export default {
errors: {},
actions: [],
closingWithChanges: false,
pendingSiteSwitch: null,
activeSite: this.site,
localizations: [],
localizedFields: [],
hasOrigin: false,
originValues: {},
originMeta: {},
syncFieldConfirmationText: __('messages.sync_entry_field_confirmation_text'),
loadId: 0,
loadError: null,
};
},

computed: {
readOnly() {
return !this.asset.isEditable;
return this.asset ? !this.asset.isEditable : true;
},

isImage() {
Expand All @@ -273,6 +323,10 @@ export default {
isToolbarVisible() {
return !this.readOnly && this.showToolbar;
},

showLocalizationSelector() {
return this.localizations.length > 1;
},
},

mounted() {
Expand Down Expand Up @@ -300,22 +354,35 @@ export default {
* This component is given an asset ID.
* It needs to get the corresponding data from the server.
*/
load() {
load(site = null) {
this.loading = true;
this.loadError = null;
const loadId = ++this.loadId;

const url = cp_url(`assets/${utf8btoa(this.id)}`);
const requestedSite = site ?? this.activeSite ?? this.site;

this.$axios.get(url, {
params: requestedSite ? { site: requestedSite } : {},
}).then((response) => {
if (loadId !== this.loadId) return;

this.$axios.get(url).then((response) => {
const data = response.data.data;
this.asset = data;

// If there are no fields, it will be an empty array when PHP encodes
// it into JSON on the server. We'll ensure it's always an object.
this.values = Array.isArray(data.values) ? {} : data.values;
this.values = Array.isArray(data.values) ? {} : (data.values || {});

this.meta = data.meta;
this.meta = data.meta || {};
this.actionUrl = data.actionUrl;
this.actions = data.actions;
this.actions = Array.isArray(data.actions) ? data.actions : [];
this.activeSite = data.locale || requestedSite;
this.localizations = data.localizations || [];
this.localizedFields = data.localizedFields || [];
this.hasOrigin = data.hasOrigin || false;
this.originValues = data.originValues || {};
this.originMeta = data.originMeta || {};

this.fieldset = data.blueprint;

Expand All @@ -338,10 +405,24 @@ export default {
]);

this.loading = false;
}).catch((err) => {
if (loadId === this.loadId) {
this.loading = false;
this.loadError = err?.response?.data?.message || __('Unable to load asset');
}
});
},

keydown(event) {
const target = event.target;
const isFormField = target instanceof HTMLElement && (
target.matches('input, textarea, select') || target.isContentEditable
);

if (isFormField) {
return;
}

if ((event.metaKey || event.ctrlKey) && event.key === 'ArrowLeft') {
this.navigateToPreviousAsset();
}
Expand Down Expand Up @@ -382,21 +463,23 @@ export default {
},

updateValues(values) {
let updated = { ...event, focus: values.focus };

if (JSON.stringify(values) === JSON.stringify(updated)) {
return
}

values = updated;
this.values = values;
},

save() {
this.saving = true;
const url = cp_url(`assets/${utf8btoa(this.id)}`);
const payload = {
...this.$refs.container.visibleValues,
site: this.activeSite,
};

if (this.hasOrigin) {
payload._localized = this.localizedFields;
}

return this.$axios
.patch(url, this.$refs.container.visibleValues)
.patch(url, payload)
.then((response) => {
this.$emit('saved', response.data.asset);
this.$toast.success(__('Saved'));
Expand All @@ -422,6 +505,29 @@ export default {
});
},

localizationSelected(site) {
if (site === this.activeSite) {
return;
}

if (this.$dirty.has(this.publishContainer)) {
this.pendingSiteSwitch = site;
return;
}

this.activeSite = site;
this.load(site);
},
Copy link

Choose a reason for hiding this comment

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

Failed locale load keeps stale editor state

Medium Severity

localizationSelected sets activeSite before load(site), but on load failure the catch block only sets loadError and leaves existing asset data intact. The UI can show prior-site data while activeSite points to a different locale, causing edits to be saved under the wrong site context.

Additional Locations (1)

Fix in Cursor Fix in Web


confirmSwitchSite() {
const site = this.pendingSiteSwitch;
this.pendingSiteSwitch = null;
this.$dirty.remove(this.publishContainer);
this.$refs.container?.clearDirtyState?.();
this.activeSite = site;
this.load(site);
},

saveAndClose() {
this.save().then(() => this.$emit('closed'));
},
Expand Down Expand Up @@ -459,7 +565,7 @@ export default {
},

canRunAction(handle) {
return this.actions.find((action) => action.handle == handle);
return (this.actions || []).find((action) => action.handle == handle);
},

runAction(actions, handle) {
Expand Down
4 changes: 4 additions & 0 deletions resources/js/components/fieldtypes/assets/Asset.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export default {
type: Boolean,
default: true,
},
site: {
type: String,
default: null,
},
},

data() {
Expand Down
1 change: 1 addition & 0 deletions resources/js/components/fieldtypes/assets/AssetRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
<asset-editor
v-if="editing"
:id="asset.id"
:site="site"
:allow-deleting="false"
@closed="closeEditor"
@saved="assetSaved"
Expand Down
1 change: 1 addition & 0 deletions resources/js/components/fieldtypes/assets/AssetTile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<asset-editor
v-if="editing"
:id="asset.id"
:site="site"
:allow-deleting="false"
@closed="closeEditor"
@saved="assetSaved"
Expand Down
12 changes: 12 additions & 0 deletions resources/js/components/fieldtypes/assets/AssetsFieldtype.vue
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
v-for="asset in assets"
:key="asset.id"
:asset="asset"
:site="currentSite"
:read-only="isReadOnly"
:show-filename="config.show_filename"
:show-set-alt="showSetAlt"
Expand Down Expand Up @@ -149,6 +150,7 @@
v-for="asset in assets"
:key="asset.id"
:asset="asset"
:site="currentSite"
:read-only="isReadOnly"
:show-filename="config.show_filename"
:show-set-alt="showSetAlt"
Expand Down Expand Up @@ -367,6 +369,10 @@ export default {
return this.config.query_scopes || [];
},

currentSite() {
return this.publishContainer.site || this.publishContainer.locale || null;
},

replicatorPreview() {
if (!this.showFieldPreviews) return;

Expand Down Expand Up @@ -473,6 +479,7 @@ export default {
this.$axios
.post(cp_url('assets-fieldtype'), {
assets,
site: this.currentSite,
})
.then((response) => {
this.assets = response.data;
Expand Down Expand Up @@ -647,6 +654,11 @@ export default {
showSelector(selecting) {
this.$emit(selecting ? 'focus' : 'blur');
},

currentSite(site, previous) {
if (!site || site === previous || !this.assets.length) return;
this.loadAssets(this.assetIds);
},
},

mounted() {
Expand Down
Loading
Loading