Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
aa2c5b3
Add site grouping with new sectioned grid UI
jaygeorge Aug 19, 2026
ab32844
Opus review
jaygeorge Aug 19, 2026
2a3cf0a
When we initially "add groups" and no groups exist, we should go stra…
jaygeorge Aug 19, 2026
a0f9301
when you click Add site it should focus on the new row
jaygeorge Aug 19, 2026
57adc40
Also do that for the "other" group
jaygeorge Aug 19, 2026
9e4530d
Closing the Add Group stack without saving now removes the leftover N…
jaygeorge Aug 19, 2026
9a53917
Draft
jaygeorge Aug 20, 2026
26b85d9
Improve editing positions
jaygeorge Aug 20, 2026
cb66b09
Merge branch 'multisite-site-grouping-configuration' into multisite-e…
jaygeorge Aug 20, 2026
a90e3ba
Put other at the bottom
jaygeorge Aug 20, 2026
19f1bd6
Fix entry grouping dropdown width
jaygeorge Aug 20, 2026
2643032
Replace / with >
jaygeorge Aug 20, 2026
da214cb
Fix grouped Working In Combobox layout and search
jaygeorge Aug 20, 2026
c0c391e
Fiddle with aesthetics
jaygeorge Aug 20, 2026
324f86b
Fix subheading also being highlighted with the active item
jaygeorge Aug 20, 2026
f785992
Balance separator spacing
jaygeorge Aug 20, 2026
f2669c8
Smooth Working In selection when switching groups
jaygeorge Aug 20, 2026
d6dbc08
Add a custom scrollbar to the combobox so there's no confusion about …
jaygeorge Aug 20, 2026
fa56d9b
Change the border radius to compensate for custom scrollbar
jaygeorge Aug 20, 2026
e05acae
Add group to global site headr
jaygeorge Aug 20, 2026
a80af87
Merge branch '6.x' into multisite-entry-grouping
jaygeorge Aug 20, 2026
167e9f3
Add the group structure to the combobox when the "Create Localization…
jaygeorge Aug 20, 2026
07aa1fe
"Create Localization" > Add the status dot to make things clearer
jaygeorge Aug 20, 2026
79c54bb
Prefer a group origin when creating a localization
jaygeorge Aug 21, 2026
ff05248
Follow the group origin when it moves out of the group
jaygeorge Aug 21, 2026
7c5e0c3
Show group headers in the collection configuration too
jaygeorge Aug 21, 2026
74143cd
Collection Configuration > show headers in selected items too
jaygeorge Aug 21, 2026
2f2920e
Show groups in the global site header
jaygeorge Aug 21, 2026
f69d511
Tweak spacing of groups
jaygeorge Aug 21, 2026
85e2355
Add grouping to globals
jaygeorge Aug 21, 2026
d2c1903
Show group in the selected global origin combobox
jaygeorge Aug 21, 2026
d383a7e
For "Add sites" grid button, Only auto-focus on Configure Sites secti…
jaygeorge Aug 21, 2026
6bff4eb
Fix multisite grouping review issues
jaygeorge Aug 21, 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
52 changes: 52 additions & 0 deletions resources/css/components/fieldtypes/grid.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
107 changes: 101 additions & 6 deletions resources/js/components/entries/PublishForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@
v-if="showLocalizationSelector"
:localizations
:localizing="localizing !== null"
:confirming-switch="!!pendingLocalization"
@selected="localizationSelected"
/>
</div>
Expand Down Expand Up @@ -234,7 +235,63 @@
>
<div class="publish-fields">
<ui-field class="form-group field-w-100" :label="__('Origin')" :instructions="__('messages.entry_origin_instructions')">
<Select class="w-full" v-model="selectedOrigin" :options="originOptions" placeholder="" />
<Select
class="w-full"
v-model="selectedOrigin"
:options="originOptions"
:virtualize="!originHasNamedGroups"
placeholder=""
>
<template #selected-option="{ option }">
<span class="flex min-w-0 items-center gap-1.5">
<span
class="little-dot shrink-0"
:class="{
'bg-green-600': option.published,
'bg-gray-500': !option.published,
'bg-red-500': !option.exists,
}"
/>
<template v-if="selectedOriginGroupLabel(option)">
<span class="truncate">{{ selectedOriginGroupLabel(option) }}</span>
<Icon name="chevron-right" class="size-3.5! text-gray-700 dark:text-white/70" aria-hidden="true" />
</template>
<span class="truncate">{{ __(option.label) }}</span>
<Badge v-if="option.origin" class="ms-1.5" size="sm" color="orange" :text="__('Origin')" />
</span>
</template>

<template #before-option="option">
<div
v-if="option._showGroupSeparator"
class="mx-2 mb-2.25 mt-0.75 border-t border-gray-200 dark:border-gray-700"
role="separator"
/>
<Subheading
v-if="option._groupLabel"
size="sm"
class="px-2.5 pb-1 pt-1.5 font-semibold uppercase tracking-wide text-gray-950 text-2xs dark:text-gray-300"
:text="__(option._groupLabel)"
/>
</template>

<template #option="option">
<div class="flex min-w-0 items-center gap-x-2">
<div class="flex min-w-0 items-center">
<span
class="little-dot me-2 shrink-0"
:class="{
'bg-green-600': option.published,
'bg-gray-500': !option.published,
'bg-red-500': !option.exists,
}"
/>
<span class="truncate">{{ __(option.label) }}</span>
</div>
<Badge v-if="option.origin" class="ms-2" size="sm" color="orange" :text="__('Origin')" />
</div>
</template>
</Select>
</ui-field>
</div>
</confirmation-modal>
Expand All @@ -261,6 +318,7 @@ import HasActions from '../publish/HasActions';
import striptags from 'striptags';
import clone from '@/util/clone.js';
import {
Badge,
Button,
Card,
CardPanel,
Expand All @@ -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';
Expand All @@ -293,6 +358,7 @@ export default {
mixins: [HasPreferences, HasActions],

components: {
Badge,
Button,
Card,
CardPanel,
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.';

Expand Down Expand Up @@ -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() {
Expand Down
27 changes: 20 additions & 7 deletions resources/js/components/fieldtypes/SlugFieldtype.vue
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,17 @@
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() {
Expand Down Expand Up @@ -123,6 +125,17 @@
}
},

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();
},
Expand Down
46 changes: 45 additions & 1 deletion resources/js/components/fieldtypes/grid/Grid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,14 @@
@blur="blurred"
/>

<ui-button size="sm" v-if="canAddRows" v-text="__(addRowButtonLabel)" @click.prevent="addRow" />
<Teleport
v-if="canAddRows"
:disabled="!usesExternalAddRow"
defer
:to="addRowTeleportTarget"
>
<ui-button size="sm" v-text="__(addRowButtonLabel)" @click.prevent="addRow" />
</Teleport>
</section>
</div>
</element-container>
Expand Down Expand Up @@ -84,6 +91,10 @@ export default {
isInGridField: true,
},

inject: {
sectionAddRowTarget: { default: null },
},

computed: {
component() {
const stackAt = this.config.stack_at ?? 550;
Expand Down Expand Up @@ -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;
},
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)) {
Expand All @@ -256,6 +299,7 @@ export default {
metaPathPrefix: { get: () => this.metaPathPrefix },
fullScreenMode: { get: () => this.fullScreenMode },
toggleFullScreen: { get: () => this.toggleFullScreen },
usesExternalAddRow: { get: () => this.usesExternalAddRow },
});
return grid;
},
Expand Down
5 changes: 4 additions & 1 deletion resources/js/components/fieldtypes/grid/Row.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<template>
<tr :class="[sortableItemClass, { 'opacity-50': isExcessive, 'inset-ring-1 inset-ring-red': hasError }]">
<tr
:data-grid-row-id="values._id"
:class="[sortableItemClass, { 'opacity-50': isExcessive, 'inset-ring-1 inset-ring-red': hasError }]"
>
<td v-if="grid.isReorderable" class="drag-handle" :class="sortableHandleClass"></td>

<FieldsProvider
Expand Down
5 changes: 4 additions & 1 deletion resources/js/components/fieldtypes/grid/Stacked.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<template>
<div :class="{ 'mb-4': rows.length > 0 }">
<div :class="{ 'mb-4': (rows.length > 0 || usesSectionRowSortable) && !grid.usesExternalAddRow }">
<sortable-list
:model-value="rows"
:vertical="true"
:item-class="sortableItemClass"
:handle-class="sortableHandleClass"
:disabled="usesSectionRowSortable"
append-to="body"
constrain-dimensions
@dragstart="$emit('focus')"
Expand All @@ -13,8 +14,10 @@
v-slot="{}"
>
<div
ref="zone"
class="grid-stacked space-y-8"
:class="{
'publish-section-row-zone min-h-16': usesSectionRowSortable,
// 'mt-0': !allowFullscreen && hideDisplay,
// 'mt-4': !hideDisplay,
}"
Expand Down
Loading
Loading