-
-
Notifications
You must be signed in to change notification settings - Fork 613
[6.x] Localizable Asset Meta Data #13928
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 6.x
Are you sure you want to change the base?
Changes from all commits
53ea2b7
7713aca
4f5695c
07446a8
5ffdfa8
ecf2cff
9a5f823
6f40e85
59b8241
c33f8d1
c181368
3ee6661
a7ee572
bd824f3
627e342
497f2ac
0fa7c1e
89cb022
3230806
211c084
c0c83dc
007d581
3f03e19
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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" | ||
|
|
@@ -116,6 +124,7 @@ | |
| </div> | ||
| </div> | ||
|
|
||
|
|
||
| <!-- Fields Area --> | ||
| <PublishContainer | ||
| v-if="fields" | ||
|
|
@@ -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> | ||
|
|
@@ -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" | ||
|
|
@@ -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, | ||
|
|
@@ -192,6 +226,7 @@ import { | |
| PublishTabs, | ||
| Icon, | ||
| Stack, | ||
| Panel, | ||
| } from '@ui'; | ||
| import ItemActions from '@/components/actions/ItemActions.vue'; | ||
|
|
||
|
|
@@ -205,6 +240,7 @@ export default { | |
| ItemActions, | ||
| FocalPointEditor, | ||
| PdfViewer, | ||
| SiteSelector, | ||
| PublishContainer, | ||
| PublishTabs, | ||
| Icon, | ||
|
|
@@ -225,6 +261,10 @@ export default { | |
| return true; | ||
| }, | ||
| }, | ||
| site: { | ||
| type: String, | ||
| default: null, | ||
| }, | ||
| }, | ||
|
|
||
| data() { | ||
|
|
@@ -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() { | ||
|
|
@@ -273,6 +323,10 @@ export default { | |
| isToolbarVisible() { | ||
| return !this.readOnly && this.showToolbar; | ||
| }, | ||
|
|
||
| showLocalizationSelector() { | ||
| return this.localizations.length > 1; | ||
| }, | ||
| }, | ||
|
|
||
| mounted() { | ||
|
|
@@ -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; | ||
|
|
||
|
|
@@ -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'); | ||
| } | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }); | ||
| }, | ||
|
|
||
| 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(); | ||
| } | ||
|
|
@@ -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')); | ||
|
|
@@ -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); | ||
| }, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Failed locale load keeps stale editor stateMedium Severity
Additional Locations (1) |
||
|
|
||
| confirmSwitchSite() { | ||
| const site = this.pendingSiteSwitch; | ||
| this.pendingSiteSwitch = null; | ||
| this.$dirty.remove(this.publishContainer); | ||
| this.$refs.container?.clearDirtyState?.(); | ||
| this.activeSite = site; | ||
| this.load(site); | ||
| }, | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| saveAndClose() { | ||
| this.save().then(() => this.$emit('closed')); | ||
| }, | ||
|
|
@@ -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) { | ||
|
|
||


Uh oh!
There was an error while loading. Please reload this page.