codeconsole commented on code in PR #15987:
URL: https://github.com/apache/grails-core/pull/15987#discussion_r3608976206
##########
grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/FormTagLib.groovy:
##########
@@ -963,39 +965,145 @@ class FormTagLib implements ApplicationContextAware,
InitializingBean, TagLibrar
}
/**
- * A helper tag for creating locale selects.<br/>
+ * A helper tag for locale selection.<br/>
*
- * eg. <g:localeSelect name="myLocale" value="${locale}" />
+ * <p>With no body it renders a control: a native {@code <select>} by
default, or a plain list of
+ * {@code <a>} links with {@code type="links"}. With a body it becomes an
iterating tag — it
+ * resolves the locales once and renders the body for each, exposing a
per-locale model under the
+ * {@code var} attribute so the caller supplies its own markup (a
Bootstrap dropdown, a footer
+ * list, etc.). The model exposes: {@code locale}, {@code tag}
(BCP‑47), {@code code}
+ * ({@code language_COUNTRY}), {@code autonym} (the name in its own
language), {@code name} (the
+ * name in the display locale), {@code label}, {@code active} (matches the
current locale),
+ * {@code default} (matches the configured default) and {@code index}.
*
- * @emptyTag
+ * eg. <g:localeSelect name="myLocale" value="${locale}"
labelType="autonym" />
*
- * @attr name REQUIRED The name of the select
- * @attr value The set locale, defaults to the current request locale if
not specified
- * @attr locale The locale to use for formatting the locale names.
Defaults to the current request locale and then the system default locale if
not specified
+ * @attr name The name of the select (select mode)
+ * @attr value The selected locale, defaults to the current request locale
if not specified
* @attr available If <code>true</code>, list only the locales the
application is translated into
* (those with a <code>messages_*.properties</code> bundle, as published
to the servlet context by
* the i18n plugin) instead of every locale the JVM knows about. Defaults
to <code>false</code>.
+ * @attr type <code>select</code> (default) or <code>links</code>. Ignored
when a body is supplied.
+ * @attr labelType The option/link label: <code>autonym</code> (each
locale in its own language),
+ * <code>name</code> (in the display locale), <code>both</code>, or
omitted for the legacy
+ * <code>"language, [COUNTRY,] name"</code> label.
+ * @attr sort If <code>true</code>, order the locales by their label using
a locale-independent collator.
+ * @attr tags If <code>true</code>, option/link keys are BCP‑47
language tags (<code>en-US</code>)
+ * rather than the legacy <code>en_US</code> form.
+ * @attr pinDefault If <code>true</code> (body mode), the configured
default locale is emitted first.
+ * @attr param The request parameter name used for <code>links</code>-mode
hrefs. Defaults to <code>lang</code>.
+ * @attr var Enables body mode: the name of the per-locale model variable
exposed to the body.
*/
- def localeSelect(Map attrs) {
- def availableAttr = attrs.remove('available')
- boolean availableOnly = availableAttr != null &&
Boolean.valueOf(availableAttr.toString())
+ def localeSelect(Map attrs, Closure body) {
+ boolean availableOnly =
Boolean.valueOf(attrs.remove('available')?.toString())
+ List locales
if (availableOnly) {
- def availableLocales =
request.servletContext?.getAttribute('availableLocales')
- attrs.from = availableLocales ?: [RCU.getLocale(request)]
+ def published =
request.servletContext?.getAttribute('availableLocales')
+ locales = published ? new ArrayList(published) :
[RCU.getLocale(request)]
}
else {
- attrs.from = Locale.getAvailableLocales()
+ locales = Locale.getAvailableLocales() as List
}
- attrs.value = (attrs.value ?: RCU.getLocale(request))?.toString()
- // set the key as a closure that formats the locale
- attrs.optionKey = { it.country ? "${it.language}_${it.country}" :
it.language }
- // set the option value as a closure that formats the locale for
display
- attrs.optionValue = { it.country ? "${it.language}, ${it.country},
${it.displayName}" : "${it.language}, ${it.displayName}" }
- // use generic select
+ Locale current = RCU.getLocale(request)
+ def valueAttr = attrs.value
+ if (valueAttr instanceof Locale) {
+ current = valueAttr
+ }
+ else if (valueAttr) {
+ current = StringUtils.parseLocale(valueAttr.toString()) ?: current
+ }
+
+ boolean useTags = Boolean.valueOf(attrs.remove('tags')?.toString())
+ Closure label = localeLabel(attrs.remove('labelType'), current)
+
+ if (Boolean.valueOf(attrs.remove('sort')?.toString())) {
+ Collator collator = Collator.getInstance(Locale.ROOT)
+ locales = locales.sort(false) { a, b ->
collator.compare(label(a).toString(), label(b).toString()) }
+ }
+
+ String varName = attrs.remove('var')
+ if (varName) {
+ boolean pinDefault =
Boolean.valueOf(attrs.remove('pinDefault')?.toString())
+ Locale defaultLocale = configuredDefaultLocale()
+ if (pinDefault) {
+ Locale pinned = locales.find { it.language ==
defaultLocale.language }
+ if (pinned) {
+ locales = [pinned] + locales.findAll { it.language !=
defaultLocale.language }
Review Comment:
Fixed in 47e8ebe933 exactly as suggested: the pin resolves the exact
configured variant (with a language-only fallback) and reorders with `[pinned]
+ remainder`, so `grails.i18n.default.locale=pt_BR` pins `pt_BR` while `pt_PT`
stays in the selector. The `default` flag elects the same single entry. Covered
by the new `LocaleSelectPinnedVariantSpec`.
##########
grails-forge/grails-forge-core/src/main/resources/gsp/index.gsp:
##########
@@ -281,26 +371,784 @@
</div>
</div>
</g:link>
+ </g:if>
+ <g:else>
+ <form
action="${controllerUrl}" method="post" class="m-0 d-flex align-items-center
gap-3 min-w-0 flex-grow-1">
+ <button type="submit"
class="btn p-0 border-0 d-flex align-items-center gap-3 text-start min-w-0
w-100">
+ <span class="min-w-0">
+ <span
class="fw-semibold text-body text-truncate d-block">
+ ${simpleName}
+ </span>
+ </span>
+ </button>
+ </form>
+ </g:else>
- <a href="${controllerUrl}"
- class="small link-primary
link-offset-2 link-underline-opacity-0 link-underline-opacity-75-hover
flex-shrink-0">
- ${controllerUrl}
- </a>
+ <div class="d-flex
align-items-center gap-2 flex-shrink-0 ms-auto">
+ <g:if test="${hasShow}">
+ <%-- Falls back to GET
/controller/show?id=… when JS is
+ off; welcome.js
upgrades it to the /show/{id} path. --%>
+ <form class="show-jump
d-lg-none" action="${showBase}" method="get" data-show-base="${showBase}">
+ <%-- No inputmode:
ids may be Long, String, or ObjectId, and a
+ numeric
keypad would lock out non-digit ids on mobile. --%>
+ <input type="text"
name="id" autocomplete="off"
+
class="form-control form-control-sm show-jump-input"
+
placeholder="${message(code: 'welcome.show.placeholder')}"
+
data-focus-placeholder="${message(code: 'welcome.show.hint')}"
+
aria-label="${message(code: 'welcome.show.aria', args: [simpleName])}">
+ <button
type="submit" class="btn btn-sm btn-primary show-jump-go">
+ <g:message
code="welcome.show.label"/>
+ </button>
+ </form>
+ </g:if>
+
+ <g:if test="${ctrlGetOk}">
+ <a href="${controllerUrl}"
+ class="small
link-primary link-offset-2 link-underline-opacity-0
link-underline-opacity-75-hover">
+ ${controllerUrl}
+ </a>
+ </g:if>
+ <g:else>
+ <span class="badge
bg-body-tertiary text-body-secondary border">
+
${[ctrlMethods].flatten()*.toString()*.toUpperCase().join(' / ')}
+ </span>
+ <span class="small
text-body-secondary">${controllerUrl}</span>
+ </g:else>
+ </div>
</div>
</li>
</g:each>
</ul>
</div>
</g:each>
</div>
- <p id="controllers-empty" class="small
text-body-secondary d-none mb-0"><g:message code="welcome.filter.none"/></p>
+ <g:if test="${numControllers != 0}">
+ <p id="controllers-empty" class="small
text-body-secondary d-none mb-0"><g:message code="welcome.filter.none"/></p>
+ </g:if>
+ <g:else>
+ <p class="small text-body-secondary
mb-0"><g:message code="welcome.artefacts.none"/></p>
+ </g:else>
+ </div>
+
+ <div data-switch-for="domains" class="d-none">
+ <div id="domains-list">
+ <ul class="list-group list-group-flush">
+ <g:each var="d"
in="${grailsApplication.domainClasses.toList().sort { it.shortName }}">
+ <li class="list-group-item px-2 d-flex
align-items-center justify-content-between gap-2" data-name="${d.shortName}">
+ <span class="fw-semibold text-body
text-truncate">${d.shortName}</span>
+ <span class="small text-body-secondary
text-truncate">${d.packageName}</span>
+ </li>
+ </g:each>
+ </ul>
+ </div>
+ <g:if test="${numDomains != 0}">
+ <p id="domains-empty" class="small
text-body-secondary d-none mb-0"><g:message code="welcome.filter.none"/></p>
+ </g:if>
+ <g:else>
+ <p class="small text-body-secondary
mb-0"><g:message code="welcome.artefacts.none"/></p>
+ </g:else>
+ </div>
+
+ <div data-switch-for="services" class="d-none">
+ <div id="services-list">
+ <ul class="list-group list-group-flush">
+ <g:each var="s"
in="${grailsApplication.serviceClasses.toList().sort { it.shortName }}">
+ <li class="list-group-item px-2 d-flex
align-items-center justify-content-between gap-2" data-name="${s.shortName}">
+ <span class="fw-semibold text-body
text-truncate">${s.shortName}</span>
+ <span class="small text-body-secondary
text-truncate">${s.packageName}</span>
+ </li>
+ </g:each>
+ </ul>
+ </div>
+ <g:if test="${numServices != 0}">
+ <p id="services-empty" class="small
text-body-secondary d-none mb-0"><g:message code="welcome.filter.none"/></p>
+ </g:if>
+ <g:else>
+ <p class="small text-body-secondary
mb-0"><g:message code="welcome.artefacts.none"/></p>
+ </g:else>
+ </div>
+
+ <div data-switch-for="taglibs" class="d-none">
+ <div id="taglibs-list">
+ <ul class="list-group list-group-flush">
+ <g:each var="t"
in="${grailsApplication.tagLibClasses.toList().sort { it.shortName }}">
+ <li class="list-group-item px-2 d-flex
align-items-center justify-content-between gap-2" data-name="${t.shortName}">
+ <span class="fw-semibold text-body
text-truncate">${t.shortName}</span>
+ <span class="small
text-truncate"><code>${t.namespace}</code> <span
class="text-body-secondary">${t.packageName}</span></span>
+ </li>
+ </g:each>
+ </ul>
+ </div>
+ <g:if test="${numTagLibs != 0}">
+ <p id="taglibs-empty" class="small
text-body-secondary d-none mb-0"><g:message code="welcome.filter.none"/></p>
+ </g:if>
+ <g:else>
+ <p class="small text-body-secondary
mb-0"><g:message code="welcome.artefacts.none"/></p>
+ </g:else>
+ </div>
+ </div>
+ </div>
+
+ <%-- RUNTIME INTERNALS: listeners, data binding, mime handling
and
+ the request filter pipeline resolved from the running
application
+ context; reuses the artefacts card's data-switch-scope
pattern. --%>
+ <g:set var="appListeners"
+
value="${applicationContext.applicationListeners.toList()
+ .collect { l -> [name: (l.getClass().simpleName
?: l.getClass().name.tokenize('.').last()),
+ packageName:
(l.getClass().package?.name ?: ''),
+ detail: l.toString()] }
+ .sort { a, b -> (a.name.toLowerCase() <=>
b.name.toLowerCase()) ?: (a.detail <=> b.detail) }}"/>
+ <g:set var="bindingGroups"
+ value="${[[code: 'welcome.binding.value', beans:
applicationContext.getBeansOfType(grails.databinding.converters.ValueConverter)],
+ [code: 'welcome.binding.formatted', beans:
applicationContext.getBeansOfType(grails.databinding.converters.FormattedValueConverter)],
+ [code: 'welcome.binding.structured', beans:
applicationContext.getBeansOfType(grails.databinding.TypedStructuredBindingEditor)],
+ [code: 'welcome.binding.listeners', beans:
applicationContext.getBeansOfType(grails.databinding.events.DataBindingListener)]]}"/>
+ <g:set var="numBindingBeans" value="${bindingGroups.sum { g ->
g.beans.size() } ?: 0}"/>
+ <g:set var="mimeTypeProviders"
+
value="${applicationContext.getBeansOfType(grails.web.mime.MimeTypeProvider)
+ .entrySet().toList().sort {
it.key.toLowerCase() }}"/>
+ <%-- The filters still on the call stack ARE this request's
pipeline, in
+ execution order: walk the reversed stack, keep Filter
classes, collapse
+ the extra frames a filter contributes through its
abstract bases, and
+ number what remains. No registry can report this actual
order. --%>
+ <g:set var="requestFilters"
+
value="${Thread.currentThread().stackTrace.toList().reverse()
+ .findResults { ste ->
+ def cls = null
+ try { cls = Class.forName(ste.className,
false, Thread.currentThread().contextClassLoader) } catch (Throwable ignored) {
}
+ (cls != null &&
jakarta.servlet.Filter.isAssignableFrom(cls)) ? cls : null
+ }
+ .inject([]) { acc, cls ->
+ def prev = acc ? acc[-1] : null
+ if (prev == cls) { return acc }
+ if (prev != null &&
prev.isAssignableFrom(cls)) { acc[-1] = cls; return acc }
+ if (prev != null &&
cls.isAssignableFrom(prev)) { return acc }
+ acc << cls
+ }
+ .unique()}"/>
+ <g:set var="filterRegistrations"
+
value="${applicationContext.getBeansOfType(org.springframework.boot.web.servlet.FilterRegistrationBean)
+ .entrySet().toList().sort { it.value.order }}"/>
+ <g:set var="filterChainProxyType"
+
value="${ClassUtils.isPresent('org.springframework.security.web.FilterChainProxy',
null) ?
ClassUtils.forName('org.springframework.security.web.FilterChainProxy', null) :
null}"/>
+ <g:set var="securityFilterChains"
Review Comment:
Agreed — as of 47e8ebe933 the whole diagnostic block renders only when
`Environment.current == Environment.DEVELOPMENT`: the runtime-internals card
(listeners, binding, mime providers, the filter pipeline, registrations,
security chains and the URL-mappings resolver) plus the Mime Types and
Datastores cards, with their `g:set` computations inside the gate so none of
the introspection even executes in production. Applied to both template copies;
the artefact/plugins/actuator cards that the previous welcome page also showed
remain ungated.
##########
grails-spring-security/ui/plugin/grails-app/taglib/grails/plugin/springsecurity/ui/SecurityUiTagLib.groovy:
##########
@@ -705,59 +700,59 @@ class SecurityUiTagLib {
}
def out = getOut()
- out << """<a id="$elementId" """
+ out << """<button type="submit" id="$elementId" class="btn
btn-primary" """
Review Comment:
Fixed in 47e8ebe933 — `submitButton`/`linkButton` now encode the element id
and text, and `writeRemainingAttributes` encodes every passed-through attribute
key and value (the stale `TODO encodeAsHTML` is gone). Added
`SecurityUiTagLibSpec` feeding markup-sensitive values through both tags to pin
the encoding.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]