Make SiteMesh 3 the default GSP layout#15713
Conversation
…s-layout Introduce a GspLayout one-of feature group so SiteMesh 3 (grails-sitemesh3) and the legacy SiteMesh 2 grails-layout are both selectable but never applied together (enforced by OneOfFeatureValidator): - GspLayout: abstract OneOfFeature parent (Category.VIEW), WEB/WEB_PLUGIN - Sitemesh3: default member; auto-applied unless another GspLayout is selected; adds grails-sitemesh3 - GrailsLayout: opt-in member; adds grails-layout (SiteMesh 2) The GspLayout features now own the layout dependency, so GrailsGsp's 'if (!isFeaturePresent(Sitemesh3)) add grails-layout' block is removed. Selecting both sitemesh3 and grails-layout now fails fast.
2fc6775 to
b0e0990
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #15713 +/- ##
=================================================
- Coverage 49.0042% 0 -49.0042%
=================================================
Files 2014 0 -2014
Lines 94747 0 -94747
Branches 16547 0 -16547
=================================================
- Hits 46430 0 -46430
+ Misses 41019 0 -41019
+ Partials 7298 0 -7298 🚀 New features to boost your workflow:
|
|
Gaps to review and potentially address before SM3 becomes default
Not gaps (verified parity)Resolution order (6 steps), Minor / cosmeticCache-interval key differs: SM3 |
jdaugherty
left a comment
There was a problem hiding this comment.
I forgot about the test apps pointing at grails-layout by default. We'll need to update that as part of this switch and ensure no regressions.
|
I wish github didn't scroll on tables like that in comments, but there are a number of key features that will need to be added to the sitemesh 3 plugin to match sitemesh 2's historical feature set. When the tests are all run against sitemesh 3 it will surface most of these, but the comment above should be close to comprehensive. |
…itch
Address review feedback on the SiteMesh 3 default change:
- Align the SiteMesh 3 feature title with SiteMesh 2 ("GSP SiteMesh 3
Layouts") and clean up GspLayoutSpec (single quotes, drop the
short-lived SNAPSHOT-repo assertions).
- Fix the UI glitch where replacing the default layout left both
sitemesh3 and grails-layout selected. The two decorators are now
driven by a single GspLayoutImpl option (SITEMESH3 default,
GRAILS_LAYOUT) instead of a visible feature card, mirroring the
servlet and reloading one-of groups. Both members are invisible
DefaultFeatures that apply based on the selected option, so the
default-features endpoint always resolves exactly one member.
Threads the option through Options, FeatureFilter, ApplicationController
and ContextFactory, exposes it via select-options (GspLayoutImplDTO /
GspLayoutImplSelectOptions) for the UI dropdown, and adds a --gsp-layout
CLI option. Updates BuildBuilder and GspLayoutSpec and adds
SelectOptionsControllerSpec.
The test apps already gate the layout dependency on the SITEMESH3_TESTING_ENABLED environment variable, but no CI lane set it, so the suite only ever exercised the legacy grails-layout. Set the flag at the workflow level in the CI and Groovy joint builds so the example apps and grails-test-suite-uber resolve grails-sitemesh3 by default. No build-script changes needed: the flag stays as-is, the SiteMesh 2 anchors keep their grails-layout coverage, and developers can still drop the flag to run against SiteMesh 2 locally.
f816246 to
021059d
Compare
In the filterless SiteMesh 3 pipeline the GSP capture taglib writes the full <head>/<body> markup to the response buffer and also captures the head/body into a Sitemesh3CapturedPage. When no decorator is selected (e.g. a view with no <meta name="layout"> and no matching convention or default layout), SiteMeshView writes content.getData() back. The captured page had neither renderedContent nor pageBuffer set, so getData() reconstructed only from (empty) properties and emitted an empty <html><head></head><body></body></html>. Attach the original response buffer as the captured page's rendered content in the no-merge branch so the original page is written back when nothing decorates it. Decorated (meta-layout) pages are unaffected: they take the decorate branch, which reads the head/body child properties. Verified against grails-test-examples/app1 integration tests under SITEMESH3_TESTING_ENABLED=true: ConfigTestControllerSpec, ControllerIncludesSpec, ControllerFromPluginSpec and ConditionalOnPropertyFromPluginYmlSpec now pass, with no regression to meta-layout pages (BookFunctionalSpec).
The grails-fields plugin renders embedded objects by wrapping the sub-fields in <g:applyLayout name="_fields/embedded" params="[...]">. SiteMesh 3's applyLayout built the body Content from the request-scoped Sitemesh3CapturedPage (the outer page being decorated) and ignored the tag's params, so the _fields/embedded layout's <g:layoutBody/> rendered empty and <g:pageProperty name="legend"/> / pageProperty(name:'type') had no values. The result was an empty <fieldset class="embedded "> with none of the address.* inputs. SiteMesh 2's GrailsLayoutTagLib pushed a fresh layout page and copied params to page properties, which is why the same grails-fields plugin worked under SiteMesh 2 but not SiteMesh 3. applyLayout now pushes a fresh Sitemesh3CapturedPage for the body render and restores the outer page in a finally block, wires the rendered body in via setBodyBuffer so <g:layoutBody/> works, applies each params entry as a page property for <g:pageProperty>, and emits the raw body verbatim when no decorator is resolved (matching SiteMesh 2's no-decorator fallback). Verified under SITEMESH3_TESTING_ENABLED=true: scaffolding-fields RelationshipsFunctionalSpec (embedded address fields) passes and the full scaffolding-fields integration suite is green, with no regression to meta-layout pages or the earlier undecorated-page fix.
database-cleanup, scaffolding-fields and test-phases declared grails-layout directly in addition to grails-dependencies-starter-web. starter-web already provides the layout plugin and flips it with SITEMESH3_TESTING_ENABLED, so with the flag on these apps ended up with both grails-sitemesh3 (via starter-web) and grails-layout (direct) on the classpath, and failed to boot with a jspViewResolver bean-type clash. Remove the redundant direct grails-layout dependency so each app gets exactly one layout plugin from starter-web: SiteMesh 2 when the flag is off, SiteMesh 3 when it is on. The dedicated SiteMesh 2 layout coverage remains on gsp-layout, which does not use starter-web.
A controller's "render template: 'x'" renders the GSP directly with renderView=false and must not be decorated with a layout. Under the filterless SiteMesh 3 integration the template view was still wrapped by GrailsSiteMeshView and Sitemesh3LayoutFinder picked the default layout, so partials came back wrapped in the application layout (e.g. the main layout's title instead of the partial's own). SiteMesh 2 suppressed this via GrailsLayoutSelector keyed on the renderView flag. Sitemesh3LayoutFinder.selectDecoratorPaths now returns no decorator when the current render has renderView=false and no explicit layout was requested. An explicit "render template: ..., layout: 'x'" still sets the LAYOUT_ATTRIBUTE and is honoured, and normal view renders (renderView=true) are unaffected, so decorated pages still decorate. Verified under SITEMESH3_TESTING_ENABLED=true: LayoutWithTemplateSpec passes; BookFunctionalSpec (meta-layout pages), ControllerIncludesSpec (incl. include-from-template), ConfigTestControllerSpec and scaffolding-fields RelationshipsFunctionalSpec remain green.
✅ All tests passed ✅🏷️ Commit: 8a1fbcb Learn more about TestLens at testlens.app. |

Makes SiteMesh 3 the default GSP layout in generated applications, mutually exclusive with the legacy SiteMesh 2
grails-layoutfeature.Introduces a
GspLayoutone-of feature group (enforced byOneOfFeatureValidator):GspLayout— abstract one-of parent (Category.VIEW, WEB/WEB_PLUGIN)Sitemesh3— default member; auto-applied unless anotherGspLayoutis selected; addsgrails-sitemesh3GrailsLayout— opt-in member; addsgrails-layout(SiteMesh 2)GrailsGspis intentionally not modified: its existingif (!isFeaturePresent(Sitemesh3))guard already skipsgrails-layoutwhen SiteMesh 3 applies, so SiteMesh 2 remains available viaGrailsLayout. Selecting bothsitemesh3andgrails-layoutnow fails fast.Depends on #15710
Split out from #15710 per review feedback ("making this the default & updating to sitemesh 3 should be separate PRs"). This branch is stacked on #15710, so until #15710 merges this PR's diff also shows the enable-SiteMesh-3 commits; it will reduce to just the make-default change once #15710 lands. Please merge #15710 first.