Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 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
e62ea36
Merge branch '6.x' into localizable-asset-meta-data
jackmcdade Mar 29, 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
138 changes: 121 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="showToolbar" 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 @@ -121,6 +129,7 @@
</div>
</div>


<!-- Fields Area -->
<PublishContainer
v-if="fields"
Expand All @@ -132,13 +141,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 @@ -165,7 +188,7 @@
</template>

<focal-point-editor
v-if="showFocalPointEditor && isFocalPointEditorEnabled"
v-if="asset && showFocalPointEditor && isFocalPointEditorEnabled"
:data="values.focus"
:image="asset.preview"
@selected="selectFocalPoint"
Expand All @@ -190,6 +213,16 @@
@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>
Expand All @@ -198,6 +231,7 @@
import FocalPointEditor from './FocalPointEditor.vue';
import CropEditor from './CropEditor.vue';
import PdfViewer from './PdfViewer.vue';
import SiteSelector from '../../SiteSelector.vue';
import { pick, flatten } from 'lodash-es';
import { router } from '@inertiajs/vue3';
import {
Expand Down Expand Up @@ -225,6 +259,7 @@ export default {
FocalPointEditor,
CropEditor,
PdfViewer,
SiteSelector,
PublishContainer,
PublishTabs,
Icon,
Expand All @@ -245,6 +280,10 @@ export default {
return true;
},
},
site: {
type: String,
default: null,
},
},

data() {
Expand All @@ -264,12 +303,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 Down Expand Up @@ -297,6 +346,10 @@ export default {
isFocalPointEditorEnabled() {
return Statamic.$config.get('focalPointEditorEnabled');
},

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

setup() {
Expand Down Expand Up @@ -335,22 +388,34 @@ 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;

return this.$axios.get(url).then((response) => {
this.$axios.get(url, {
params: requestedSite ? { site: requestedSite } : {},
}).then((response) => {
if (loadId !== this.loadId) return;
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 @@ -373,10 +438,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 @@ -438,21 +517,23 @@ export default {
},

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

load() no longer returns promise, breaking await

Medium Severity

The load() method no longer returns the axios promise (the return keyword was removed), but handleCropReplaced() calls await this.load(). Since load() now returns undefined, the await resolves immediately, causing bustAndReloadImageCaches to fire before the asset data has actually reloaded.

Additional Locations (1)
Fix in Cursor Fix in Web

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 @@ -478,6 +559,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);
},

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 @@ -515,7 +619,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 @@ -112,6 +112,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 @@ -152,6 +153,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 @@ -380,6 +382,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 @@ -493,6 +499,7 @@ export default {
this.$axios
.post(cp_url('assets-fieldtype'), {
assets,
site: this.currentSite,
})
.then((response) => {
this.assets = response.data;
Expand Down Expand Up @@ -667,6 +674,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