From ffea3ef3d2abf867adcf4318855780dfa05a9188 Mon Sep 17 00:00:00 2001 From: azc9673 Date: Sun, 26 Apr 2026 11:28:07 -0400 Subject: [PATCH] Enhance theme selection by rendering custom names in dropdown selectors. These changes allow quick-theme exports to use and preserve custom names, and theme selectors to display rendered custom names. Closes #47866 Signed-off-by: azc9673 --- .../info/ThemeInfoRepresentation.java | 9 +++++++++ docs/guides/ui-customization/themes.adoc | 3 +++ .../src/clients/add/LoginSettingsPanel.tsx | 5 ++++- .../src/realm-settings/themes/QuickTheme.tsx | 5 ++++- .../realm-settings/themes/ThemeSettings.tsx | 8 ++++---- .../src/defs/serverInfoRepesentation.ts | 1 + .../main/java/org/keycloak/theme/Theme.java | 4 ++++ .../admin/info/ServerInfoAdminResource.java | 1 + .../org/keycloak/theme/ClassLoaderTheme.java | 18 +++++++++++++++++- .../theme/ClasspathThemeProviderFactory.java | 11 ++++++++++- .../keycloak/theme/DefaultThemeManager.java | 5 +++++ .../keycloak/tests/admin/ServerInfoTest.java | 2 ++ 12 files changed, 64 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/org/keycloak/representations/info/ThemeInfoRepresentation.java b/core/src/main/java/org/keycloak/representations/info/ThemeInfoRepresentation.java index 82d8dde2f9e3..fae34167f5e4 100644 --- a/core/src/main/java/org/keycloak/representations/info/ThemeInfoRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/info/ThemeInfoRepresentation.java @@ -26,6 +26,7 @@ public class ThemeInfoRepresentation { private String name; + private String renderedName; private String[] locales; private String description; @@ -37,6 +38,14 @@ public void setName(String name) { this.name = name; } + public String getRenderedName() { + return renderedName; + } + + public void setRenderedName(String renderedName) { + this.renderedName = renderedName; + } + public String[] getLocales() { return locales; } diff --git a/docs/guides/ui-customization/themes.adoc b/docs/guides/ui-customization/themes.adoc index 267d725120e1..8b21b114a361 100644 --- a/docs/guides/ui-customization/themes.adoc +++ b/docs/guides/ui-customization/themes.adoc @@ -403,11 +403,14 @@ The contents of `META-INF/keycloak-themes.json` in this case would be: { "themes": [{ "name" : "mytheme", + "renderedName" : "My Theme", "types": [ "login", "email" ] }] } ---- + +`renderedName` is optional. If present, Keycloak uses it as the user-facing label in theme selection dropdowns, while `name` remains the canonical theme id used for resolution and resource loading. + A single archive can contain multiple themes and each theme can support one or more types. To deploy the archive to {project_name}, add it to the `providers/` directory of diff --git a/js/apps/admin-ui/src/clients/add/LoginSettingsPanel.tsx b/js/apps/admin-ui/src/clients/add/LoginSettingsPanel.tsx index 9cb28d92ed65..8e64bb632279 100644 --- a/js/apps/admin-ui/src/clients/add/LoginSettingsPanel.tsx +++ b/js/apps/admin-ui/src/clients/add/LoginSettingsPanel.tsx @@ -30,7 +30,10 @@ export const LoginSettingsPanel = ({ access }: { access?: boolean }) => { }} options={[ { key: "", value: t("choose") }, - ...loginThemes.map(({ name }) => ({ key: name, value: name })), + ...loginThemes.map(({ name, renderedName }) => ({ + key: name, + value: renderedName || name, + })), ]} /> { const zip = new JSZip(); const styles = JSON.parse(realm.attributes?.style ?? "{}"); - const { favicon, logo, bgimage, fileName } = realm; + const { favicon, logo, bgimage, fileName, themeName } = realm; + const themeId = "quick-theme"; + const renderedThemeName = themeName?.trim() || themeId; const logoName = "img/logo" + logo?.name?.substring(logo?.name?.lastIndexOf(".")); @@ -115,6 +117,7 @@ styles=css/login.css css/theme-styles.css `{ "themes": [{ "name" : "${themeNameClean}", + "renderedName" : ${JSON.stringify(renderedThemeName)}, "types": [ "login", "account", "admin", "common" ] }] }`, diff --git a/js/apps/admin-ui/src/realm-settings/themes/ThemeSettings.tsx b/js/apps/admin-ui/src/realm-settings/themes/ThemeSettings.tsx index 6b651c52d867..3f101cbad59b 100644 --- a/js/apps/admin-ui/src/realm-settings/themes/ThemeSettings.tsx +++ b/js/apps/admin-ui/src/realm-settings/themes/ThemeSettings.tsx @@ -56,7 +56,7 @@ export const ThemeSettingsTab = ({ realm, save }: ThemeSettingsTabProps) => { options={appendEmptyChoice( themeTypes.login.map((theme) => ({ key: theme.name, - value: theme.name, + value: theme.renderedName || theme.name, description: theme.description, })), )} @@ -71,7 +71,7 @@ export const ThemeSettingsTab = ({ realm, save }: ThemeSettingsTabProps) => { options={appendEmptyChoice( themeTypes.account.map((theme) => ({ key: theme.name, - value: theme.name, + value: theme.renderedName || theme.name, description: theme.description, })), )} @@ -86,7 +86,7 @@ export const ThemeSettingsTab = ({ realm, save }: ThemeSettingsTabProps) => { options={appendEmptyChoice( themeTypes.admin.map((theme) => ({ key: theme.name, - value: theme.name, + value: theme.renderedName || theme.name, description: theme.description, })), )} @@ -101,7 +101,7 @@ export const ThemeSettingsTab = ({ realm, save }: ThemeSettingsTabProps) => { options={appendEmptyChoice( themeTypes.email.map((theme) => ({ key: theme.name, - value: theme.name, + value: theme.renderedName || theme.name, description: theme.description, })), )} diff --git a/js/libs/keycloak-admin-client/src/defs/serverInfoRepesentation.ts b/js/libs/keycloak-admin-client/src/defs/serverInfoRepesentation.ts index 23bcb2c010f3..689f606552ec 100644 --- a/js/libs/keycloak-admin-client/src/defs/serverInfoRepesentation.ts +++ b/js/libs/keycloak-admin-client/src/defs/serverInfoRepesentation.ts @@ -31,6 +31,7 @@ export interface ServerInfoRepresentation { export interface ThemeInfoRepresentation { name: string; + renderedName?: string; locales?: string[]; description?: string; } diff --git a/server-spi/src/main/java/org/keycloak/theme/Theme.java b/server-spi/src/main/java/org/keycloak/theme/Theme.java index 516673039de0..3f5d7ec22dc0 100755 --- a/server-spi/src/main/java/org/keycloak/theme/Theme.java +++ b/server-spi/src/main/java/org/keycloak/theme/Theme.java @@ -38,6 +38,10 @@ enum Type { LOGIN, ACCOUNT, ADMIN, EMAIL, WELCOME, COMMON }; String getName(); + default String getRenderedName() { + return getName(); + } + String getParentName(); String getImportName(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java index e90e19164fa9..181b76e27538 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java @@ -246,6 +246,7 @@ private void setThemes(ServerInfoRepresentation info) { if (theme != null && name.equals(theme.getName()) && !theme.isAbstract()) { ThemeInfoRepresentation ti = new ThemeInfoRepresentation(); ti.setName(name); + ti.setRenderedName(theme.getRenderedName()); String locales = theme.getProperties().getProperty("locales"); if (locales != null) { diff --git a/services/src/main/java/org/keycloak/theme/ClassLoaderTheme.java b/services/src/main/java/org/keycloak/theme/ClassLoaderTheme.java index b9752db6fc1d..84b382d95eec 100644 --- a/services/src/main/java/org/keycloak/theme/ClassLoaderTheme.java +++ b/services/src/main/java/org/keycloak/theme/ClassLoaderTheme.java @@ -35,6 +35,8 @@ public class ClassLoaderTheme extends FileBasedTheme { private String name; + private String renderedName; + private String parentName; private String importName; @@ -52,11 +54,20 @@ public class ClassLoaderTheme extends FileBasedTheme { private Properties properties; public ClassLoaderTheme(String name, Type type, ClassLoader classLoader) throws IOException { - init(name, type, classLoader); + this(name, name, type, classLoader); + } + + public ClassLoaderTheme(String name, String renderedName, Type type, ClassLoader classLoader) throws IOException { + init(name, renderedName, type, classLoader); } public void init(String name, Type type, ClassLoader classLoader) throws IOException { + init(name, name, type, classLoader); + } + + public void init(String name, String renderedName, Type type, ClassLoader classLoader) throws IOException { this.name = name; + this.renderedName = renderedName == null ? name : renderedName; this.type = type; this.classLoader = classLoader; @@ -85,6 +96,11 @@ public String getName() { return name; } + @Override + public String getRenderedName() { + return renderedName; + } + @Override public String getParentName() { return parentName; diff --git a/services/src/main/java/org/keycloak/theme/ClasspathThemeProviderFactory.java b/services/src/main/java/org/keycloak/theme/ClasspathThemeProviderFactory.java index 0e262ccd36ac..3f39ba194047 100755 --- a/services/src/main/java/org/keycloak/theme/ClasspathThemeProviderFactory.java +++ b/services/src/main/java/org/keycloak/theme/ClasspathThemeProviderFactory.java @@ -47,6 +47,7 @@ public ClasspathThemeProviderFactory(String id, ClassLoader classLoader) { public static class ThemeRepresentation { private String name; + private String renderedName; private String[] types; public String getName() { @@ -57,6 +58,14 @@ public void setName(String name) { this.name = name; } + public String getRenderedName() { + return renderedName; + } + + public void setRenderedName(String renderedName) { + this.renderedName = renderedName; + } + public String[] getTypes() { return types; } @@ -116,7 +125,7 @@ protected void loadThemes(ClassLoader classLoader, ThemesRepresentation themesRe if (!themes.containsKey(type)) { themes.put(type, new HashMap<>()); } - themes.get(type).put(themeRep.getName(), new ClassLoaderTheme(themeRep.getName(), type, classLoader)); + themes.get(type).put(themeRep.getName(), new ClassLoaderTheme(themeRep.getName(), themeRep.getRenderedName(), type, classLoader)); } } } catch (Exception e) { diff --git a/services/src/main/java/org/keycloak/theme/DefaultThemeManager.java b/services/src/main/java/org/keycloak/theme/DefaultThemeManager.java index eb72ceb06bb3..27e974607156 100755 --- a/services/src/main/java/org/keycloak/theme/DefaultThemeManager.java +++ b/services/src/main/java/org/keycloak/theme/DefaultThemeManager.java @@ -193,6 +193,11 @@ public String getName() { return themes.get(0).getName(); } + @Override + public String getRenderedName() { + return themes.get(0).getRenderedName(); + } + @Override public String getParentName() { return themes.get(0).getParentName(); diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/ServerInfoTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/ServerInfoTest.java index 15cfabbd659c..30c31b78b51d 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/ServerInfoTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/ServerInfoTest.java @@ -70,6 +70,8 @@ public void testServerInfo() { Assert.assertNames(info.getThemes().get("email"), "keycloak"); Assert.assertNames(info.getThemes().get("login"), "keycloak", "keycloak.v2"); Assert.assertNames(info.getThemes().get("welcome"), "keycloak"); + info.getThemes().values().forEach(themes -> + themes.forEach(theme -> assertEquals(theme.getName(), theme.getRenderedName()))); assertNotNull(info.getEnums());