diff --git a/resources/css/components/fieldtypes/grid.css b/resources/css/components/fieldtypes/grid.css index c1c5a1ea33d..eb579c97501 100644 --- a/resources/css/components/fieldtypes/grid.css +++ b/resources/css/components/fieldtypes/grid.css @@ -101,6 +101,58 @@ } } + /* GRID FIELDTYPE / SECTIONED MODIFIER + =================================================== */ + /* Grid whose column headers double as the enclosing publish section's header. + - e.g. A site group in `cp/sites` + - the table and its headers go transparent so the headers sit on the section background + - the rows take over the card background, border and corners + - headers stay real table headers, so they keep aligning with their columns + */ + .grid-table--sectioned { + @apply rounded-none border-0 bg-transparent shadow-none dark:bg-transparent; + + thead th { + @apply static border-b-0 bg-transparent dark:bg-transparent; + &:first-child { + @apply rounded-ss-none; + } + &:last-child { + @apply rounded-se-none; + } + } + + > tbody > tr { + > td { + @apply bg-white dark:bg-gray-900; + + &:first-child { + @apply border-s dark:border-s-gray-700; + } + + &:last-child { + @apply border-e dark:border-e-gray-700; + } + } + + &:first-child > td { + @apply border-t dark:border-t-gray-700; + + &:first-child { + @apply rounded-ss-[7px]; + } + + &:last-child { + @apply rounded-se-[7px]; + } + } + + &:last-child > td { + @apply border-b dark:border-b-gray-700; + } + } + } + .grid-item-header { @apply flex cursor-move items-center justify-between border-b bg-gray-200 px-4 py-2 text-sm outline-hidden; } diff --git a/resources/js/components/entries/PublishForm.vue b/resources/js/components/entries/PublishForm.vue index 2c888064b35..c4539d657d0 100644 --- a/resources/js/components/entries/PublishForm.vue +++ b/resources/js/components/entries/PublishForm.vue @@ -169,6 +169,7 @@ v-if="showLocalizationSelector" :localizations :localizing="localizing !== null" + :confirming-switch="!!pendingLocalization" @selected="localizationSelected" /> @@ -234,7 +235,63 @@ >
- + + + + + +
@@ -261,6 +318,7 @@ import HasActions from '../publish/HasActions'; import striptags from 'striptags'; import clone from '@/util/clone.js'; import { + Badge, Button, Card, CardPanel, @@ -285,6 +343,13 @@ import { Stack, } from '@ui'; import resetValuesFromResponse from '@/util/resetValuesFromResponse.js'; +import { + flatOptionsFromSiteGroups, + groupItemsBySiteGroup, + hasNamedSiteGroups, + preferredOriginHandle, + selectedSiteGroupLabel, +} from '@/util/site-groups.js'; import { computed, ref } from 'vue'; import { Pipeline, Request, BeforeSaveHooks, AfterSaveHooks, PipelineStopped } from '@ui/Publish/SavePipeline.js'; import { router } from '@inertiajs/vue3'; @@ -293,6 +358,7 @@ export default { mixins: [HasPreferences, HasActions], components: { + Badge, Button, Card, CardPanel, @@ -513,13 +579,32 @@ export default { return this.getPreference('after_save') ?? 'listing'; }, + defaultOriginHandle() { + return preferredOriginHandle( + this.localizations, + this.localizing || null, + this.originBehavior, + ); + }, + originOptions() { - return this.localizations + const existing = this.localizations .filter((localization) => localization.exists) .map((localization) => ({ value: localization.handle, label: localization.name, + group: localization.group, + group_handle: localization.group_handle, + published: localization.published, + exists: localization.exists, + origin: localization.handle === this.defaultOriginHandle, })); + + return flatOptionsFromSiteGroups(groupItemsBySiteGroup(existing)); + }, + + originHasNamedGroups() { + return hasNamedSiteGroups(this.originOptions); }, direction() { @@ -654,6 +739,11 @@ export default { this.$dirty.remove(this.publishContainer); this.localizing = localization; + this.selectedOrigin = preferredOriginHandle( + this.localizations, + localization, + this.originBehavior, + ); if (localization.exists) { this.editLocalization(localization); @@ -724,6 +814,10 @@ export default { this.localizing = false; }, + selectedOriginGroupLabel(option) { + return selectedSiteGroupLabel(option, this.originHasNamedGroups); + }, + localizationStatusText(localization) { if (!localization.exists) return 'This entry does not exist for this site.'; @@ -877,10 +971,11 @@ export default { created() { window.history.replaceState({}, document.title, document.location.href.replace('created=true', '')); - this.selectedOrigin = - this.originBehavior === 'active' - ? this.localizations.find((l) => l.active)?.handle - : this.localizations.find((l) => l.root)?.handle; + this.selectedOrigin = preferredOriginHandle( + this.localizations, + this.localizations.find((localization) => localization.active), + this.originBehavior, + ); }, beforeUnmount() { diff --git a/resources/js/components/fieldtypes/SlugFieldtype.vue b/resources/js/components/fieldtypes/SlugFieldtype.vue index d9dda4158e7..03b4dbcf12b 100644 --- a/resources/js/components/fieldtypes/SlugFieldtype.vue +++ b/resources/js/components/fieldtypes/SlugFieldtype.vue @@ -74,15 +74,17 @@ export default { source() { if (!this.generate) return; - const field = this.config.from || 'title'; - let key = field; + const from = this.valueFrom(this.config.from || 'title', { relative: true }); - if (this.fieldPathPrefix) { - let dottedPrefix = this.fieldPathPrefix.replace(new RegExp('\.' + this.handle + '$'), ''); - key = dottedPrefix + '.' + field; - } + if (!from) return from; - return data_get(this.publishContainer?.values, key); + const prefix = this.config.prefix_from + ? this.valueFrom(this.config.prefix_from, { relative: false }) + : null; + + if (!prefix) return from; + + return `${prefix} ${from}`; }, language() { @@ -123,6 +125,17 @@ export default { } }, + valueFrom(field, { relative }) { + let key = field; + + if (relative && this.fieldPathPrefix) { + let dottedPrefix = this.fieldPathPrefix.replace(new RegExp('\.' + this.handle + '$'), ''); + key = dottedPrefix + '.' + field; + } + + return data_get(this.publishContainer?.values, key); + }, + sync() { this.$refs.slugify.reset(); }, diff --git a/resources/js/components/fieldtypes/grid/Grid.vue b/resources/js/components/fieldtypes/grid/Grid.vue index 1b1a8de1622..c8922f2e31c 100644 --- a/resources/js/components/fieldtypes/grid/Grid.vue +++ b/resources/js/components/fieldtypes/grid/Grid.vue @@ -36,7 +36,14 @@ @blur="blurred" /> - + + + @@ -84,6 +91,10 @@ export default { isInGridField: true, }, + inject: { + sectionAddRowTarget: { default: null }, + }, + computed: { component() { const stackAt = this.config.stack_at ?? 550; @@ -116,6 +127,16 @@ export default { return __(this.config.add_row) || __('Add Row'); }, + usesExternalAddRow() { + return !!this.sectionAddRowTarget && !!this.config.headers_in_section && !this.fullScreenMode; + }, + + addRowTeleportTarget() { + if (!this.usesExternalAddRow) return 'body'; + + return `[${this.sectionAddRowTarget}="${CSS.escape(this.handle)}"]`; + }, + hasMaxRows() { return this.maxRows != null; }, @@ -186,6 +207,11 @@ export default { this.updateRowMeta(id, this.meta.new); this.update([...this.value, row]); + + // Only auto-focus on Configure Sites sectioned grids, not every Grid in the CP. + if (this.config.headers_in_section) { + this.focusNewRow(id); + } }, updated(index, row) { @@ -233,6 +259,23 @@ export default { // TODO }, + focusNewRow(id, attempts = 0) { + const row = document.querySelector(`[data-grid-row-id="${CSS.escape(id)}"]`); + const focusable = row?.querySelector( + 'input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled])', + ); + + if (focusable) { + focusable.focus(); + row.scrollIntoView({ block: 'nearest' }); + return; + } + + if (attempts < 20) { + requestAnimationFrame(() => this.focusNewRow(id, attempts + 1)); + } + }, + blurred() { setTimeout(() => { if (!this.$el.contains(document.activeElement)) { @@ -256,6 +299,7 @@ export default { metaPathPrefix: { get: () => this.metaPathPrefix }, fullScreenMode: { get: () => this.fullScreenMode }, toggleFullScreen: { get: () => this.toggleFullScreen }, + usesExternalAddRow: { get: () => this.usesExternalAddRow }, }); return grid; }, diff --git a/resources/js/components/fieldtypes/grid/Row.vue b/resources/js/components/fieldtypes/grid/Row.vue index b28b4f6f4c6..c19a5edf337 100644 --- a/resources/js/components/fieldtypes/grid/Row.vue +++ b/resources/js/components/fieldtypes/grid/Row.vue @@ -1,5 +1,8 @@