Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
448fb87
Add site grouping with new sectioned grid UI
jaygeorge Aug 19, 2026
5f87480
Opus review
jaygeorge Aug 19, 2026
ec6b8b0
When we initially "add groups" and no groups exist, we should go stra…
jaygeorge Aug 19, 2026
85fc406
when you click Add site it should focus on the new row
jaygeorge Aug 19, 2026
aa704d1
Also do that for the "other" group
jaygeorge Aug 19, 2026
c89eed7
Closing the Add Group stack without saving now removes the leftover N…
jaygeorge Aug 19, 2026
c7aa8d6
Draft
jaygeorge Aug 20, 2026
ba1ef5a
Improve editing positions
jaygeorge Aug 20, 2026
93befd1
Put other at the bottom
jaygeorge Aug 20, 2026
fd3189b
Fix entry grouping dropdown width
jaygeorge Aug 20, 2026
9f9d208
Replace / with >
jaygeorge Aug 20, 2026
ba12f7a
Fix grouped Working In Combobox layout and search
jaygeorge Aug 20, 2026
85a64cc
Fiddle with aesthetics
jaygeorge Aug 20, 2026
96fd5db
Fix subheading also being highlighted with the active item
jaygeorge Aug 20, 2026
eb818fc
Balance separator spacing
jaygeorge Aug 20, 2026
23b02af
Smooth Working In selection when switching groups
jaygeorge Aug 20, 2026
bb2ac69
Add a custom scrollbar to the combobox so there's no confusion about …
jaygeorge Aug 20, 2026
20d8a5a
Change the border radius to compensate for custom scrollbar
jaygeorge Aug 20, 2026
90dbb83
Add group to global site headr
jaygeorge Aug 20, 2026
e5fba99
Add the group structure to the combobox when the "Create Localization…
jaygeorge Aug 20, 2026
f33f9be
"Create Localization" > Add the status dot to make things clearer
jaygeorge Aug 20, 2026
a6faf04
Prefer a group origin when creating a localization
jaygeorge Aug 21, 2026
f9f299f
Follow the group origin when it moves out of the group
jaygeorge Aug 21, 2026
5a0614d
Show group headers in the collection configuration too
jaygeorge Aug 21, 2026
b752f4d
Collection Configuration > show headers in selected items too
jaygeorge Aug 21, 2026
2f9249a
Show groups in the global site header
jaygeorge Aug 21, 2026
4181b1f
Tweak spacing of groups
jaygeorge Aug 21, 2026
05df376
Add grouping to globals
jaygeorge Aug 21, 2026
7641600
Show group in the selected global origin combobox
jaygeorge Aug 21, 2026
9cf0f8f
For "Add sites" grid button, Only auto-focus on Configure Sites secti…
jaygeorge Aug 21, 2026
f6fa7fa
Fix multisite grouping review issues
jaygeorge Aug 21, 2026
1aaabea
Add grouping to the globals selector too
jaygeorge Aug 24, 2026
16491de
Make the globals selector wider to accommodate grouping
jaygeorge Aug 24, 2026
d378557
Escape literal dots in slug field path regex
jaygeorge Aug 24, 2026
369cb6d
Import Statamic trans helper in SitesController
jaygeorge Aug 24, 2026
e5177f2
Fix site grouping PHPUnit failures
jaygeorge Aug 24, 2026
5221182
Do not translate user-defined site group names
jaygeorge Aug 24, 2026
cc8d320
Only use active site origin when origin behavior is active
jaygeorge Aug 26, 2026
5ebe697
Accept legacy sites array when updating Configure Sites
jaygeorge Aug 26, 2026
1b36352
Show site groups in listing Site columns
jaygeorge Aug 26, 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
2 changes: 2 additions & 0 deletions resources/js/bootstrap/fieldtypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import FieldsFieldtype from '../components/fieldtypes/grid/FieldsFieldtype.vue';
import FilesFieldtype from '../components/fieldtypes/FilesFieldtype.vue';
import FloatFieldtype from '../components/fieldtypes/FloatFieldtype.vue';
import Sites from '../components/globals/Sites.vue';
import SitesIndexFieldtype from '../components/fieldtypes/SitesIndexFieldtype.vue';
import Grid from '../components/fieldtypes/grid/Grid.vue';
import GridIndex from '../components/fieldtypes/grid/GridIndex.vue';
import GroupFieldtype from '../components/fieldtypes/GroupFieldtype.vue';
Expand Down Expand Up @@ -102,6 +103,7 @@ export default function registerFieldtypes(app) {
app.component('files-fieldtype', FilesFieldtype);
app.component('float-fieldtype', FloatFieldtype);
app.component('global_set_sites-fieldtype', Sites);
app.component('sites-fieldtype-index', SitesIndexFieldtype);
app.component('grid-fieldtype', Grid);
app.component('grid-fieldtype-index', GridIndex);
app.component('group-fieldtype', GroupFieldtype);
Expand Down
56 changes: 51 additions & 5 deletions resources/js/components/SiteSelector.vue
Original file line number Diff line number Diff line change
@@ -1,23 +1,69 @@
<script setup>
import { Select } from '@/components/ui';
import { computed } from 'vue';
import { Icon, Select, Subheading } from '@/components/ui';
import {
flatOptionsFromSiteGroups,
groupItemsBySiteGroup,
hasNamedSiteGroups,
selectedSiteGroupLabel,
} from '@/util/site-groups.js';

defineProps({
const props = defineProps({
sites: { type: Array, required: true },
modelValue: { type: String, required: true },
});

defineEmits(['update:modelValue']);

const hasNamedGroups = computed(() => hasNamedSiteGroups(props.sites));

const options = computed(() => {
if (!hasNamedGroups.value) {
return props.sites;
}

return flatOptionsFromSiteGroups(groupItemsBySiteGroup(props.sites));
});

function groupLabel(option) {
return selectedSiteGroupLabel(option, hasNamedGroups.value);
}
</script>

<template>
<Select
class="w-36"
:options="sites"
class="w-60"
:options="options"
option-label="name"
option-value="handle"
align="end"
:adaptive-width="true"
:virtualize="!hasNamedGroups"
:model-value="modelValue"
@update:model-value="$emit('update:modelValue', $event)"
/>
>
<template #selected-option="{ option }">
<span v-if="option" class="flex min-w-0 items-center gap-1.5">
<template v-if="groupLabel(option)">
<span class="truncate">{{ groupLabel(option) }}</span>
<Icon name="chevron-right" class="size-3.5! shrink-0 text-gray-700 dark:text-white/70" aria-hidden="true" />
</template>
<span class="truncate">{{ __(option.name) }}</span>
</span>
</template>

<template v-if="hasNamedGroups" #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>
</Select>
</template>
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
47 changes: 47 additions & 0 deletions resources/js/components/fieldtypes/SitesIndexFieldtype.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<template>
<div class="flex flex-wrap gap-x-3 gap-y-1">
<ItemLabel
v-for="(item, index) in items"
:key="index"
:item="item"
:group-label="groupLabel(item)"
/>
</div>
</template>

<script>
import IndexFieldtype from './IndexFieldtype.vue';
import ItemLabel from '../inputs/relationship/ItemLabel.vue';
import {
hasNamedSiteGroups,
selectedSiteGroupLabel,
} from '@/util/site-groups.js';

export default {
mixins: [IndexFieldtype],

components: {
ItemLabel,
},

computed: {
items() {
if (!this.value) {
return [];
}

return Array.isArray(this.value) ? this.value : [this.value];
},

namedGroupsExist() {
return hasNamedSiteGroups(Statamic.$config.get('sites') || []);
},
},

methods: {
groupLabel(item) {
return selectedSiteGroupLabel(item, this.namedGroupsExist);
},
},
};
</script>
Loading
Loading