From aa2c5b3b604e086aa555644f10c06b5ec1616f1c Mon Sep 17 00:00:00 2001 From: Jay George Date: Wed, 19 Aug 2026 17:20:13 +0100 Subject: [PATCH 01/31] Add site grouping with new sectioned grid UI --- resources/css/components/fieldtypes/grid.css | 52 ++ .../components/fieldtypes/SlugFieldtype.vue | 27 +- .../js/components/fieldtypes/grid/Grid.vue | 24 +- .../js/components/fieldtypes/grid/Stacked.vue | 5 +- .../js/components/fieldtypes/grid/Table.vue | 21 +- .../js/components/fieldtypes/grid/View.vue | 46 +- .../js/components/ui/Publish/Sections.vue | 762 +++++++++++++++++- resources/js/pages/sites/Edit.vue | 114 ++- src/Facades/Site.php | 5 +- src/Fields/Section.php | 2 + .../Controllers/CP/Sites/SitesController.php | 85 +- src/Sites/Site.php | 8 + src/Sites/Sites.php | 244 +++++- tests/Fields/SectionTest.php | 36 + tests/Sites/SiteTest.php | 8 + tests/Sites/SitesConfigTest.php | 391 ++++++++- tests/Sites/SitesTest.php | 18 + 17 files changed, 1711 insertions(+), 137 deletions(-) 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/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..c46e7a0c97c 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; }, @@ -256,6 +277,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/Stacked.vue b/resources/js/components/fieldtypes/grid/Stacked.vue index fedceb19b71..383300e1457 100644 --- a/resources/js/components/fieldtypes/grid/Stacked.vue +++ b/resources/js/components/fieldtypes/grid/Stacked.vue @@ -1,10 +1,11 @@