This commit is contained in:
Joseph Garrone
2021-03-01 20:45:37 +01:00
parent 57a0d9f10f
commit f599df2bb7
11 changed files with 617 additions and 158 deletions

View File

@ -2,17 +2,16 @@
import { keycloakThemeBuildingDirPath } from "./build-keycloak-theme";
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
import { join as pathJoin } from "path";
export const keycloakBuiltinThemesAndThirdPartyExamplesThemsUrl =
"https://github.com/garronej/keycloak-react-theming/releases/download/v0.0.1/other_keycloak_thems.zip";
if (require.main === module) {
console.log("execute!");
downloadAndUnzip({
"url": keycloakBuiltinThemesAndThirdPartyExamplesThemsUrl,
"destDirPath": keycloakThemeBuildingDirPath
"destDirPath": pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme")
});
}

View File

@ -14,6 +14,8 @@ export function downloadAndUnzip(
fs.mkdirSync(destDirPath, { "recursive": true });
console.log({ url, destDirPath });
[
`wget ${url}`,
...["unzip", "rm"].map(prg => `${prg} ${pathBasename(url)}`),

126
src/lib/Template.tsx vendored
View File

@ -1,126 +0,0 @@
/*
export function Template() {
return (
<div className="kcLoginClass">
<div id="kc-header" className="kcHeaderClass">
<div id="kc-header-wrapper" className="kcHeaderWrapperClass">
${kcSanitize(msg("loginTitleHtml", (realm.displayNameHtml!'')))?no_esc}
</div>
</div>
<div class="${properties.kcFormCardClass!} <#if displayWide>${properties.kcFormCardAccountClass!}</#if>">
<header class="${properties.kcFormHeaderClass!}">
<#if realm.internationalizationEnabled && locale.supported?size gt 1>
<div id="kc-locale">
<div id="kc-locale-wrapper" class="${properties.kcLocaleWrapperClass!}">
<div class="kc-dropdown" id="kc-locale-dropdown">
<a href="#" id="kc-current-locale-link">${locale.current}</a>
<ul>
<#list locale.supported as l>
<li class="kc-dropdown-item"><a href="${l.url}">${l.label}</a></li>
</#list>
</ul>
</div>
</div>
</div>
</#if>
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
<#if displayRequiredFields>
<div class="${properties.kcContentWrapperClass!}">
<div class="${properties.kcLabelWrapperClass!} subtitle">
<span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span>
</div>
<div class="col-md-10">
<h1 id="kc-page-title"><#nested "header"></h1>
</div>
</div>
<#else>
<h1 id="kc-page-title"><#nested "header"></h1>
</#if>
<#else>
<#if displayRequiredFields>
<div class="${properties.kcContentWrapperClass!}">
<div class="${properties.kcLabelWrapperClass!} subtitle">
<span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span>
</div>
<div class="col-md-10">
<#nested "show-username">
<div class="${properties.kcFormGroupClass!}">
<div id="kc-username">
<label id="kc-attempted-username">${auth.attemptedUsername}</label>
<a id="reset-login" href="${url.loginRestartFlowUrl}">
<div class="kc-login-tooltip">
<i class="${properties.kcResetFlowIcon!}"></i>
<span class="kc-tooltip-text">${msg("restartLoginTooltip")}</span>
</div>
</a>
</div>
</div>
</div>
</div>
<#else>
<#nested "show-username">
<div class="${properties.kcFormGroupClass!}">
<div id="kc-username">
<label id="kc-attempted-username">${auth.attemptedUsername}</label>
<a id="reset-login" href="${url.loginRestartFlowUrl}">
<div class="kc-login-tooltip">
<i class="${properties.kcResetFlowIcon!}"></i>
<span class="kc-tooltip-text">${msg("restartLoginTooltip")}</span>
</div>
</a>
</div>
</div>
</#if>
</#if>
</header>
<div id="kc-content">
<div id="kc-content-wrapper">
<#-- App-initiated actions should not see warning messages about the need to complete the action -->
<#-- during login. -->
<#if displayMessage && message?has_content && (message.type != 'warning' || !isAppInitiatedAction??)>
<div class="alert alert-${message.type}">
<#if message.<span class="${properties.kcFeedbackSuccessIcon!}"></span></#if>
<#if message.<span class="${properties.kcFeedbackWarningIcon!}"></span></#if>
<#if message.<span class="${properties.kcFeedbackErrorIcon!}"></span></#if>
<#if message.<span class="${properties.kcFeedbackInfoIcon!}"></span></#if>
<span class="kc-feedback-text">${kcSanitize(message.summary) ? no_esc}</span>
</div>
</#if >
<#nested "form" >
<#if auth?has_content && auth.showTryAnotherWayLink() && showAnotherWayIfPresent >
<form id="kc-select-try-another-way-form" action="${url.loginAction}" method="post" <#if displayWide>class="${properties.kcContentWrapperClass!}"</#if> >
<div <#if displayWide>class="${properties.kcFormSocialAccountContentClass!} ${properties.kcFormSocialAccountClass!}"</#if> >
<div class="${properties.kcFormGroupClass!}">
<input type="hidden" name="tryAnotherWay" value="on" />
<a href="#" id="try-another-way" onclick="document.forms['kc-select-try-another-way-form'].submit();return false;">${msg("doTryAnotherWay")}</a>
</div>
</div >
</form >
</#if >
<#if displayInfo>
<div id="kc-info" class="${properties.kcSignUpClass!}">
<div id="kc-info-wrapper" class="${properties.kcInfoAreaWrapperClass!}">
<#nested "info">
</div>
</div>
</#if>
</div >
</div >
</div >
</div >
);
}
*/
export {};

211
src/lib/Template.tsx.disabled vendored Normal file
View File

@ -0,0 +1,211 @@
import { useState } from "react";
import { useKeycloakThemeTranslation } from "./i18n/useKeycloakTranslation";
import { keycloakPagesContext } from "./keycloakFtlValues";
import { assert } from "evt/tools/typeSafety/assert";
import { cx } from "tss-react";
import { useKeycloakLanguage, AvailableLanguages } from "./i18n/useKeycloakLanguage";
import { getLanguageLabel } from "./i18n/getLanguageLabel";
import { useCallbackFactory } from "powerhooks";
export type Props = {
displayInfo?: boolean;
displayMessage: boolean;
displayRequiredFields: boolean;
displayWide: boolean;
showAnotherWayIfPresent: boolean;
};
export function Template(props: Props) {
const {
displayInfo = false,
displayMessage = true,
displayRequiredFields = false,
displayWide = false,
showAnotherWayIfPresent = true
} = props;
const { t } = useKeycloakThemeTranslation();
const { keycloakLanguage, setKeycloakLanguage } = useKeycloakLanguage();
const onChangeLanguageClickFactory = useCallbackFactory(
([languageTag]: [AvailableLanguages]) =>
setKeycloakLanguage(languageTag)
);
const [{ realm, locale, auth }] = useState(() => {
assert(keycloakPagesContext !== undefined);
return keycloakPagesContext;
});
//<div className="kcBodyClass"></div>
return (
<div className="kcLoginClass">
<div id="kc-header" className="kcHeaderClass">
<div id="kc-header-wrapper" className="kcHeaderWrapperClass">
{t("loginTitleHtml", realm.displayNameHtml)}
</div>
</div>
<div className={cx("kcFormCardClass", displayWide && "kcFormCardAccountClass")}>
<header className="kcFormHeaderClass">
{
(
realm.internationalizationEnabled &&
(assert(locale !== undefined), true) &&
locale.supported.length > 1
) && (
<div id="kc-locale">
<div id="kc-locale-wrapper" className="kcLocaleWrapperClass">
<div className="kc-dropdown" id="kc-locale-dropdown">
<a href="#" id="kc-current-locale-link">
{getLanguageLabel(keycloakLanguage)}
</a>
<ul>
{
locale.supported.map(
({ languageTag }) =>
<li className="kc-dropdown-item">
<a href="#" onClick={onChangeLanguageClickFactory(languageTag)}>
{getLanguageLabel(languageTag)}
</a>
</li>
)
}
</ul>
</div>
</div>
</div>
)
}
{
(
auth !== undefined &&
auth.showUsername &&
!auth.showResetCredentials
) ?
(
displayRequiredFields ?
(
<div className="kcContentWrapperClass">
<div className="kcLabelWrapperClass subtitle">
<span className="subtitle">
<span className="required">*</span>
{t("requiredFields")}
</span>
</div>
<div className="col-md-10">
<h1 id="kc-page-title"><#nested "header"></h1>
</div>
</div>
)
:
(
<h1 id="kc-page-title"><#nested "header"></h1>
)
)
:
(
displayRequiredFields ? (
<div className="kcContentWrapperClass">
<div class="${properties.kcLabelWrapperClass!} subtitle">
<span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span>
</div>
<div class="col-md-10">
<#nested "show-username">
<div class="${properties.kcFormGroupClass!}">
<div id="kc-username">
<label id="kc-attempted-username">${auth.attemptedUsername}</label>
<a id="reset-login" href="${url.loginRestartFlowUrl}">
<div class="kc-login-tooltip">
<i class="${properties.kcResetFlowIcon!}"></i>
<span class="kc-tooltip-text">${msg("restartLoginTooltip")}</span>
</div>
</a>
</div>
</div>
</div>
</div>
) : (
<#nested "show-username">
<div class="${properties.kcFormGroupClass!}">
<div id="kc-username">
<label id="kc-attempted-username">${auth.attemptedUsername}</label>
<a id="reset-login" href="${url.loginRestartFlowUrl}">
<div class="kc-login-tooltip">
<i class="${properties.kcResetFlowIcon!}"></i>
<span class="kc-tooltip-text">${msg("restartLoginTooltip")}</span>
</div>
</a>
</div>
</div>
)
)
}
</header>
<div id="kc-content">
<div id="kc-content-wrapper">
<#-- App-initiated actions should not see warning messages about the need to complete the action -->
<#-- during login. -->
<#if displayMessage && message?has_content && (message.type != 'warning' || !isAppInitiatedAction??)>
<div class="alert alert-${message.type}">
<#if message.<span class="${properties.kcFeedbackSuccessIcon!}"></span></#if>
<#if message.<span class="${properties.kcFeedbackWarningIcon!}"></span></#if>
<#if message.<span class="${properties.kcFeedbackErrorIcon!}"></span></#if>
<#if message.<span class="${properties.kcFeedbackInfoIcon!}"></span></#if>
<span class="kc-feedback-text">${kcSanitize(message.summary) ? no_esc}</span>
</div >
</#if >
<#nested "form" >
<#if auth?has_content && auth.showTryAnotherWayLink() && showAnotherWayIfPresent >
<form id="kc-select-try-another-way-form" action="${url.loginAction}" method="post" <#if displayWide>class="${properties.kcContentWrapperClass!}"</#if> >
<div <#if displayWide>class="${properties.kcFormSocialAccountContentClass!} ${properties.kcFormSocialAccountClass!}"</#if> >
<div class="${properties.kcFormGroupClass!}">
<input type="hidden" name="tryAnotherWay" value="on" />
<a href="#" id="try-another-way" onclick="document.forms['kc-select-try-another-way-form'].submit();return false;">${msg("doTryAnotherWay")}</a>
</div>
</div >
</form >
</#if >
<#if displayInfo>
<div id="kc-info" class="${properties.kcSignUpClass!}">
<div id="kc-info-wrapper" class="${properties.kcInfoAreaWrapperClass!}">
<#nested "info">
</div>
</div>
</#if>
</div >
</div >
</div >
</div >
);
}

View File

@ -3,6 +3,7 @@ import { createUseGlobalState } from "powerhooks";
import { messages } from "./generated_messages/login";
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
import { getLanguageLabel } from "./getLanguageLabel";
import { keycloakPagesContext } from "../keycloakFtlValues";
const availableLanguages = objectKeys(messages);
@ -10,18 +11,21 @@ export type AvailableLanguages = typeof availableLanguages[number];
export const { useKeycloakLanguage } = createUseGlobalState(
"keycloakLanguage",
getKeycloakAvailableLanguageBestGuess,
() => getBestMatchAmongKeycloakAvailableLanguages(
keycloakPagesContext?.locale?.["current" as never] ??
navigator.language
),
{ "persistance": "cookies" }
);
/**
* Pass in "fr-FR" or "français" for example, it will return the AvailableLanguage
* it corresponds to.
* it corresponds to: "fr".
* If there is no reasonable match it's guessed from navigator.language.
* If still no matches en is returned.
* If still no matches "en" is returned.
*/
export function getKeycloakAvailableLanguageBestGuess(
languageLike: string = navigator.language
export function getBestMatchAmongKeycloakAvailableLanguages(
languageLike: string
): AvailableLanguages {
const iso2LanguageLike = languageLike.split("-")[0].toLowerCase();
@ -32,7 +36,7 @@ export function getKeycloakAvailableLanguageBestGuess(
);
if (language === undefined && languageLike !== navigator.language) {
return getKeycloakAvailableLanguageBestGuess(navigator.language);
return getBestMatchAmongKeycloakAvailableLanguages(navigator.language);
}
return "en";

View File

@ -2,7 +2,7 @@
import { useKeycloakLanguage } from "./useKeycloakLanguage";
import { messages } from "./generated_messages/login";
import { useConstCallback } from "powerhooks";
import type { ReactNode } from "react";
import type { ReactNode } from "react";
export type MessageKey = keyof typeof messages["en"]
@ -12,13 +12,19 @@ export function useKeycloakThemeTranslation() {
const { keycloakLanguage } = useKeycloakLanguage();
const t = useConstCallback(
(key: MessageKey, ...args: string[]): ReactNode => {
(key: MessageKey, ...args: (string | undefined)[]): ReactNode => {
let out: string = messages[keycloakLanguage as any as "en"][key] ?? messages["en"][key];
args.forEach((arg, i)=>
out= out.replace(new RegExp(`\\{${i}\\}`, "g"), arg)
);
args.forEach((arg, i) => {
if (arg === undefined) {
return;
}
out = out.replace(new RegExp(`\\{${i}\\}`, "g"), arg);
});
return <span className={key} dangerouslySetInnerHTML={{ "__html": out }} />;

View File

@ -2,6 +2,8 @@
import { ftlValuesGlobalName } from "../bin/build-keycloak-theme/generateKeycloakThemeResources";
import type { generateFtlFilesCodeFactory } from "../bin/build-keycloak-theme/generateFtl";
import { id } from "evt/tools/typeSafety/id";
//import type { LanguageLabel } from "./i18n/getLanguageLabel";
import type { AvailableLanguages } from "./i18n/useKeycloakLanguage";
export type KeycloakFtlValues = {
@ -17,19 +19,20 @@ export type KeycloakFtlValues = {
displayNameHtml?: string;
internationalizationEnabled: boolean;
},
//NOTE: Undefined if !realm.internationalizationEnabled
//We hide this since we provide a client side internationalization engine
/*
/** Undefined if !realm.internationalizationEnabled */
locale?: {
supported: {
url: string;
//url: string;
languageTag: AvailableLanguages;
//NOTE: Is determined by languageTag. Ex: languageTag === "en" => label === "English"
label: LanguageLabel;
},
current: LanguageLabel;
/** Is determined by languageTag. Ex: languageTag === "en" => label === "English"
* or getLanguageLabel(languageTag) === label
*/
//label: LanguageLabel;
}[];
//NOTE: We do not expose this because the language is managed
//client side. We use this value however to set the default.
//current: LanguageLabel;
},
*/
auth?: {
showUsername: boolean;
showResetCredentials: boolean;

View File

@ -2,13 +2,15 @@
import { sampleReactProjectDirPath } from "./setupSampleReactProject";
import * as st from "scripting-tools";
import { join as pathJoin } from "path";
import { getProjectRoot } from "../bin/tools/getProjectRoot";
import { getProjectRoot } from "../bin/tools/getProjectRoot";
console.log(`Running main in ${sampleReactProjectDirPath}`);
st.execSync(
`node ${pathJoin(getProjectRoot(), "dist","bin","download-sample-keycloak-themes")}`,
{ "cwd": sampleReactProjectDirPath }
console.log(
st.execSync(
`node ${pathJoin(getProjectRoot(), "dist", "bin", "download-sample-keycloak-themes")}`,
{ "cwd": sampleReactProjectDirPath }
)
);