diff --git a/src/bin/build-keycloak-theme/generateFtl/index.ts b/src/bin/build-keycloak-theme/generateFtl/index.ts index 98842d28..3f906039 100644 --- a/src/bin/build-keycloak-theme/generateFtl/index.ts +++ b/src/bin/build-keycloak-theme/generateFtl/index.ts @@ -45,7 +45,7 @@ export function generateFtlFilesCodeFactory( return; } - $(element).attr(attrName, "${url.resourcesPath}" + href); + $(element).attr(attrName, "${url.resourcesPath}/build" + href); }) ); diff --git a/src/bin/build-keycloak-theme/generateKeycloakThemeResources.ts b/src/bin/build-keycloak-theme/generateKeycloakThemeResources.ts index 1340d215..7760b6dc 100644 --- a/src/bin/build-keycloak-theme/generateKeycloakThemeResources.ts +++ b/src/bin/build-keycloak-theme/generateKeycloakThemeResources.ts @@ -7,6 +7,9 @@ import { replaceImportFromStaticInJsCode } from "./replaceImportFromStatic"; import { generateFtlFilesCodeFactory } from "./generateFtl"; +import { keycloakBuiltinThemesAndThirdPartyExamplesThemsUrl } from "../download-sample-keycloak-themes"; +import { downloadAndUnzip } from "../tools/downloadAndUnzip"; +import * as child_process from "child_process"; export const ftlValuesGlobalName = "keycloakPagesContext"; @@ -25,7 +28,7 @@ export function generateKeycloakThemeResources( let allCssGlobalsToDefine: Record = {}; transformCodebase({ - "destDirPath": pathJoin(themeDirPath, "resources"), + "destDirPath": pathJoin(themeDirPath, "resources", "build"), "srcDirPath": reactAppBuildDirPath, "transformSourceCodeString": ({ filePath, sourceCode }) => { @@ -79,9 +82,31 @@ export function generateKeycloakThemeResources( }); + { + + const destDirPath = pathJoin(themeDirPath, "..", "tmp_xxKdLpdIdLd"); + + downloadAndUnzip({ + "url": keycloakBuiltinThemesAndThirdPartyExamplesThemsUrl, + destDirPath + }); + + child_process.execSync( + [ + "mv", + pathJoin("keycloak", "common"), + pathJoin("..", "common") + ].join(" "), + { "cwd": destDirPath } + ); + + child_process.execSync(`rm -r ${destDirPath}`); + + } + fs.writeFileSync( pathJoin(themeDirPath, "theme.properties"), - Buffer.from("parent=base\n", "utf8") + Buffer.from(`import=common/${themeName}\n`, "utf8") ); } diff --git a/src/bin/build-keycloak-theme/replaceImportFromStatic.ts b/src/bin/build-keycloak-theme/replaceImportFromStatic.ts index c9e56898..765f9c38 100644 --- a/src/bin/build-keycloak-theme/replaceImportFromStatic.ts +++ b/src/bin/build-keycloak-theme/replaceImportFromStatic.ts @@ -12,7 +12,7 @@ export function replaceImportFromStaticInJsCode( const fixedJsCode = jsCode!.replace( /"static\//g, - `window.${ftlValuesGlobalName}.url.resourcesPath.replace(/^\\//,"") + "/" + "static/` + `window.${ftlValuesGlobalName}.url.resourcesPath.replace(/^\\//,"") + "/build/static/` ); return { fixedJsCode }; @@ -75,7 +75,7 @@ export function generateCssCodeToDefineGlobals( `--${cssVariableName}:`, [ "url(", - "${url.resourcesPath}" + + "${url.resourcesPath}/build" + cssGlobalsToDefine[cssVariableName].match(/^url\(([^)]+)\)$/)![1], ")" ].join("") diff --git a/src/lib/Template.tsx b/src/lib/Template.tsx new file mode 100644 index 00000000..f909d789 --- /dev/null +++ b/src/lib/Template.tsx @@ -0,0 +1,288 @@ + +import type { ReactNode } from "react"; +import { useState, useEffect } 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"; +import { appendLinkInHead } from "./tools/appendLinkInHead"; +import { appendScriptInHead } from "./tools/appendScriptInHead"; +import { join as pathJoin } from "path"; +import { useConstCallback } from "powerhooks"; + +type KcClasses = { [key in T]?: string[] | string }; + + +export type Props = { + displayInfo?: boolean; + displayMessage: boolean; + displayRequiredFields: boolean; + displayWide: boolean; + showAnotherWayIfPresent: boolean; + properties?: { + stylesCommon?: string[]; + styles?: string[]; + scripts?: string[]; + } & KcClasses< + "kcLoginClass" | + "kcHeaderClass" | + "kcHeaderWrapperClass" | + "kcFormCardClass" | + "kcFormCardAccountClass" | + "kcFormHeaderClass" | + "kcLocaleWrapperClass" | + "kcContentWrapperClass" | + "kcLabelWrapperClass" | + "kcContentWrapperClass" | + "kcLabelWrapperClass" | + "kcFormGroupClass" | + "kcResetFlowIcon" | + "kcResetFlowIcon" | + "kcFeedbackSuccessIcon" | + "kcFeedbackWarningIcon" | + "kcFeedbackErrorIcon" | + "kcFeedbackInfoIcon" | + "kcContentWrapperClass" | + "kcFormSocialAccountContentClass" | + "kcFormSocialAccountClass" | + "kcSignUpClass" | + "kcInfoAreaWrapperClass" + >; + headerNode: ReactNode; + showUsernameNode: ReactNode; + formNode: ReactNode; + displayInfoNode: ReactNode; +}; + +export function Template(props: Props) { + + const { + displayInfo = false, + displayMessage = true, + displayRequiredFields = false, + displayWide = false, + showAnotherWayIfPresent = true, + properties = {}, + headerNode, + showUsernameNode, + formNode, + displayInfoNode + } = props; + + const { t } = useKeycloakThemeTranslation(); + + const { keycloakLanguage, setKeycloakLanguage } = useKeycloakLanguage(); + + const onChangeLanguageClickFactory = useCallbackFactory( + ([languageTag]: [AvailableLanguages]) => + setKeycloakLanguage(languageTag) + ); + + const onTryAnotherWayClick = useConstCallback(() => { + + document.forms["kc-select-try-another-way-form" as never].submit(); + + return false; + + }); + + const [{ realm, locale, auth, url, message, isAppInitiatedAction }] = useState(() => { + + assert(keycloakPagesContext !== undefined); + + return keycloakPagesContext; + + }); + + useEffect(() => { + + properties.stylesCommon?.forEach( + relativePath => + appendLinkInHead( + { "href": pathJoin(url.resourcesCommonPath, relativePath) } + ) + ); + + properties.styles?.forEach( + relativePath => + appendLinkInHead( + { "href": pathJoin(url.resourcesPath, relativePath) } + ) + ); + + properties.scripts?.forEach( + relativePath => + appendScriptInHead( + { "src": pathJoin(url.resourcesPath, relativePath) } + ) + ); + + + }, []); + + return ( +
+ +
+
+ {t("loginTitleHtml", realm.displayNameHtml)} +
+
+ +
+
+ { + ( + realm.internationalizationEnabled && + (assert(locale !== undefined), true) && + locale.supported.length > 1 + ) && +
+
+
+ + {getLanguageLabel(keycloakLanguage)} + + +
+
+
+ + } + { + ( + auth !== undefined && + auth.showUsername && + !auth.showResetCredentials + ) ? + ( + displayRequiredFields ? + ( + +
+
+ + * + {t("requiredFields")} + +
+
+

{headerNode}

+
+
+ + ) + : + ( + +

{headerNode}

+ + ) + ) : ( + displayRequiredFields ? ( +
+
+ * {t("requiredFields")} +
+
+ {showUsernameNode} +
+
+ + +
+ + {t("restartLoginTooltip")} +
+
+
+
+
+
+ ) : ( + <> + {showUsernameNode} +
+
+ + +
+ + {t("restartLoginTooltip")} +
+
+
+
+ + ) + ) + } +
+
+
+ {/* App-initiated actions should not see warning messages about the need to complete the action during login. */} + { + ( + displayMessage && + message !== undefined && + ( + message.type !== "warning" || + !isAppInitiatedAction + ) + ) && +
+ {message.type === "success" && } + {message.type === "warning" && } + {message.type === "error" && } + {message.type === "info" && } + {message.summary} +
+ } + {formNode} + { + ( + auth !== undefined && + auth.showTryAnotherWayLink && + showAnotherWayIfPresent + ) && + +
+ +
+ } + { + displayInfo && + +
+
+ {displayInfoNode} +
+
+ } +
+
+
+
+ ); +} diff --git a/src/lib/Template.tsx.disabled b/src/lib/Template.tsx.disabled deleted file mode 100644 index df2d3d14..00000000 --- a/src/lib/Template.tsx.disabled +++ /dev/null @@ -1,211 +0,0 @@ - -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; - - }); - //
- - return ( - -
-
-
- {t("loginTitleHtml", realm.displayNameHtml)} -
-
-
-
- - { - ( - realm.internationalizationEnabled && - (assert(locale !== undefined), true) && - locale.supported.length > 1 - ) && ( -
-
-
- - {getLanguageLabel(keycloakLanguage)} - - -
-
-
- ) - } - - { - ( - auth !== undefined && - auth.showUsername && - !auth.showResetCredentials - ) ? - ( - displayRequiredFields ? - ( - -
-
- - * - {t("requiredFields")} - -
-
-

<#nested "header">

-
-
- - ) - : - ( - -

<#nested "header">

- - ) - ) - : - ( - displayRequiredFields ? ( -
-
- * ${msg("requiredFields")} -
-
- <#nested "show-username"> -
-
- - - - -
-
-
-
- ) : ( - - <#nested "show-username"> -
-
- - - - -
-
- ) - ) - - -} - - -
-
-
- - <#-- 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??)> -
- <#if message. - <#if message. - <#if message. - <#if message. - -
- - - <#nested "form" > - - <#if auth?has_content && auth.showTryAnotherWayLink() && showAnotherWayIfPresent > -
class="${properties.kcContentWrapperClass!}" > -
class="${properties.kcFormSocialAccountContentClass!} ${properties.kcFormSocialAccountClass!}" > - -
-
- - - <#if displayInfo> -
-
- <#nested "info"> -
-
- -
-
- -
-
- - - - - - ); - -} diff --git a/src/lib/tools/appendLinkInHead.ts b/src/lib/tools/appendLinkInHead.ts new file mode 100644 index 00000000..c51afa20 --- /dev/null +++ b/src/lib/tools/appendLinkInHead.ts @@ -0,0 +1,24 @@ + +export function appendLinkInHead( + props: { + href: string; + } +) { + + const { href } = props; + + var link = document.createElement("link"); + + Object.assign( + link, + { + href, + "type": "text/css", + "rel": "stylesheet", + "media": "screen,print" + } + ); + + document.getElementsByTagName("head")[0].appendChild(link); + +} diff --git a/src/lib/tools/appendScriptInHead.ts b/src/lib/tools/appendScriptInHead.ts new file mode 100644 index 00000000..596b4c00 --- /dev/null +++ b/src/lib/tools/appendScriptInHead.ts @@ -0,0 +1,22 @@ + +export function appendScriptInHead( + props: { + src: string; + } +) { + + const { src } = props; + + var script = document.createElement("script"); + + Object.assign( + script, + { + src, + "type": "text/javascript", + } + ); + + document.getElementsByTagName("head")[0].appendChild(script); + +}