Compare commits
8 Commits
v10.0.0-rc
...
v10.0.0-rc
Author | SHA1 | Date | |
---|---|---|---|
956b8260e7 | |||
b7954f87e0 | |||
540ce55dc2 | |||
d71a2c98d1 | |||
cb9cec676d | |||
9f2755bc7f | |||
fbe5a1f477 | |||
338642094d |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "keycloakify",
|
"name": "keycloakify",
|
||||||
"version": "10.0.0-rc.19",
|
"version": "10.0.0-rc.21",
|
||||||
"description": "Create Keycloak themes using React",
|
"description": "Create Keycloak themes using React",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -65,7 +65,6 @@
|
|||||||
"react": "*"
|
"react": "*"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"evt": "^2.5.7",
|
|
||||||
"minimal-polyfills": "^2.2.3",
|
"minimal-polyfills": "^2.2.3",
|
||||||
"react-markdown": "^5.0.3",
|
"react-markdown": "^5.0.3",
|
||||||
"tsafe": "^1.6.6"
|
"tsafe": "^1.6.6"
|
||||||
@ -121,6 +120,7 @@
|
|||||||
"vite": "^5.2.11",
|
"vite": "^5.2.11",
|
||||||
"vitest": "^0.29.8",
|
"vitest": "^0.29.8",
|
||||||
"yauzl": "^2.10.0",
|
"yauzl": "^2.10.0",
|
||||||
"zod": "^3.17.10"
|
"zod": "^3.17.10",
|
||||||
|
"evt": "^2.5.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,17 +29,7 @@ out["messagesPerField"]= {
|
|||||||
<#recover>
|
<#recover>
|
||||||
<#assign doExistErrorOnUsernameOrPassword = true>
|
<#assign doExistErrorOnUsernameOrPassword = true>
|
||||||
</#attempt>
|
</#attempt>
|
||||||
<#if doExistErrorOnUsernameOrPassword>
|
return <#if doExistErrorOnUsernameOrPassword>text<#else>undefined</#if>
|
||||||
return text;
|
|
||||||
<#else>
|
|
||||||
<#assign doExistMessageForField = "">
|
|
||||||
<#attempt>
|
|
||||||
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
|
|
||||||
<#recover>
|
|
||||||
<#assign doExistMessageForField = true>
|
|
||||||
</#attempt>
|
|
||||||
return <#if doExistMessageForField>text<#else>undefined</#if>;
|
|
||||||
</#if>
|
|
||||||
<#else>
|
<#else>
|
||||||
<#assign doExistMessageForField = "">
|
<#assign doExistMessageForField = "">
|
||||||
<#attempt>
|
<#attempt>
|
||||||
@ -107,22 +97,18 @@ out["messagesPerField"]= {
|
|||||||
</#attempt>
|
</#attempt>
|
||||||
<#if doExistErrorOnUsernameOrPassword>
|
<#if doExistErrorOnUsernameOrPassword>
|
||||||
<#attempt>
|
<#attempt>
|
||||||
return "${kcSanitize(msg('invalidUserMessage'))?no_esc}";
|
return decodeHtmlEntities("${msg('invalidUserMessage')?js_string}");
|
||||||
<#recover>
|
<#recover>
|
||||||
return "Invalid username or password.";
|
return "Invalid username or password.";
|
||||||
</#attempt>
|
</#attempt>
|
||||||
<#else>
|
<#else>
|
||||||
<#attempt>
|
return "";
|
||||||
return "${messagesPerField.get('${fieldName}')?no_esc}";
|
|
||||||
<#recover>
|
|
||||||
return "";
|
|
||||||
</#attempt>
|
|
||||||
</#if>
|
</#if>
|
||||||
<#else>
|
<#else>
|
||||||
<#attempt>
|
<#attempt>
|
||||||
return "${messagesPerField.get('${fieldName}')?no_esc}";
|
return decodeHtmlEntities("${messagesPerField.get('${fieldName}')?js_string}");
|
||||||
<#recover>
|
<#recover>
|
||||||
return "invalid field";
|
return "Invalid field";
|
||||||
</#attempt>
|
</#attempt>
|
||||||
</#if>
|
</#if>
|
||||||
}
|
}
|
||||||
@ -180,27 +166,36 @@ try {
|
|||||||
} catch(error) { }
|
} catch(error) { }
|
||||||
|
|
||||||
<#if profile?? && profile.attributes??>
|
<#if profile?? && profile.attributes??>
|
||||||
out["__localizationReamlOverrides_userProfile"] = {
|
out["lOCALIZATION_REALM_OVERRIDES_USER_PROFILE_PROPERTY_KEY_aaGLsPgGIdeeX"] = {
|
||||||
<#list profile.attributes as attribute>
|
<#list profile.attributes as attribute>
|
||||||
<#if attribute.annotations?? && attribute.displayName??>
|
<#if attribute.annotations?? && attribute.displayName??>
|
||||||
"${attribute.displayName}xx": "${advancedMsg(attribute.displayName)?no_esc}",
|
"${attribute.displayName}": decodeHtmlEntities("${advancedMsg(attribute.displayName)?js_string}"),
|
||||||
</#if>
|
</#if>
|
||||||
<#if attribute.annotations.inputHelperTextBefore??>
|
<#if attribute.annotations.inputHelperTextBefore??>
|
||||||
"${attribute.annotations.inputHelperTextBefore}": "${advancedMsg(attribute.annotations.inputHelperTextBefore)?no_esc}",
|
"${attribute.annotations.inputHelperTextBefore}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputHelperTextBefore)?js_string}"),
|
||||||
</#if>
|
</#if>
|
||||||
<#if attribute.annotations.inputHelperTextAfter??>
|
<#if attribute.annotations.inputHelperTextAfter??>
|
||||||
"${attribute.annotations.inputHelperTextAfter}": "${advancedMsg(attribute.annotations.inputHelperTextAfter)?no_esc}",
|
"${attribute.annotations.inputHelperTextAfter}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputHelperTextAfter)?js_string}"),
|
||||||
</#if>
|
</#if>
|
||||||
<#if attribute.annotations.inputTypePlaceholder??>
|
<#if attribute.annotations.inputTypePlaceholder??>
|
||||||
"${attribute.annotations.inputTypePlaceholder}": "${advancedMsg(attribute.annotations.inputTypePlaceholder)?no_esc}",
|
"${attribute.annotations.inputTypePlaceholder}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputTypePlaceholder)?js_string}"),
|
||||||
</#if>
|
</#if>
|
||||||
</#list>
|
</#list>
|
||||||
};
|
};
|
||||||
</#if>
|
</#if>
|
||||||
|
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
|
|
||||||
|
function decodeHtmlEntities(htmlStr){
|
||||||
|
var element = decodeHtmlEntities.element;
|
||||||
|
if (!element) {
|
||||||
|
element = document.createElement("textarea");
|
||||||
|
decodeHtmlEntities.element = element;
|
||||||
|
}
|
||||||
|
element.innerHTML = htmlStr;
|
||||||
|
return textarea.value;
|
||||||
|
}
|
||||||
|
|
||||||
})();
|
})();
|
||||||
<#function ftl_object_to_js_code_declaring_an_object object path>
|
<#function ftl_object_to_js_code_declaring_an_object object path>
|
||||||
|
|
||||||
|
@ -10,7 +10,8 @@ import {
|
|||||||
type ThemeType,
|
type ThemeType,
|
||||||
nameOfTheGlobal,
|
nameOfTheGlobal,
|
||||||
basenameOfTheKeycloakifyResourcesDir,
|
basenameOfTheKeycloakifyResourcesDir,
|
||||||
resources_common
|
resources_common,
|
||||||
|
nameOfTheLocalizationRealmOverridesUserProfileProperty
|
||||||
} from "../../shared/constants";
|
} from "../../shared/constants";
|
||||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||||
|
|
||||||
@ -135,8 +136,11 @@ export function generateFtlFilesCodeFactory(params: {
|
|||||||
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion)
|
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion)
|
||||||
.replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType)
|
.replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType)
|
||||||
.replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", themeName)
|
.replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", themeName)
|
||||||
.replace("RESOURCES_COMMON_cLsLsMrtDkpVv", resources_common);
|
.replace("RESOURCES_COMMON_cLsLsMrtDkpVv", resources_common)
|
||||||
|
.replace(
|
||||||
|
"lOCALIZATION_REALM_OVERRIDES_USER_PROFILE_PROPERTY_KEY_aaGLsPgGIdeeX",
|
||||||
|
nameOfTheLocalizationRealmOverridesUserProfileProperty
|
||||||
|
);
|
||||||
const ftlObjectToJsCodeDeclaringAnObjectPlaceholder =
|
const ftlObjectToJsCodeDeclaringAnObjectPlaceholder =
|
||||||
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }';
|
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }';
|
||||||
|
|
||||||
|
@ -184,18 +184,47 @@ function toUTF16(codePoint: number): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Escapes special characters and converts unicode to UTF-16 encoding
|
// Escapes special characters for use in a .properties file
|
||||||
function escapeString(str: string): string {
|
function escapeString(str: string): string {
|
||||||
let escapedStr = "";
|
let escapedStr = "";
|
||||||
for (const char of [...str]) {
|
for (const char of [...str]) {
|
||||||
const codePoint = char.codePointAt(0);
|
const codePoint = char.codePointAt(0);
|
||||||
if (!codePoint) continue;
|
if (!codePoint) continue;
|
||||||
if (char === "'") {
|
|
||||||
escapedStr += "''"; // double single quotes
|
switch (char) {
|
||||||
} else if (codePoint > 0x7f) {
|
case "\n":
|
||||||
escapedStr += toUTF16(codePoint); // non-ascii characters
|
escapedStr += "\\n";
|
||||||
} else {
|
break;
|
||||||
escapedStr += char;
|
case "\r":
|
||||||
|
escapedStr += "\\r";
|
||||||
|
break;
|
||||||
|
case "\t":
|
||||||
|
escapedStr += "\\t";
|
||||||
|
break;
|
||||||
|
case "\\":
|
||||||
|
escapedStr += "\\\\";
|
||||||
|
break;
|
||||||
|
case ":":
|
||||||
|
escapedStr += "\\:";
|
||||||
|
break;
|
||||||
|
case "=":
|
||||||
|
escapedStr += "\\=";
|
||||||
|
break;
|
||||||
|
case "#":
|
||||||
|
escapedStr += "\\#";
|
||||||
|
break;
|
||||||
|
case "!":
|
||||||
|
escapedStr += "\\!";
|
||||||
|
break;
|
||||||
|
case "'":
|
||||||
|
escapedStr += "''";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (codePoint > 0x7f) {
|
||||||
|
escapedStr += toUTF16(codePoint); // Non-ASCII characters
|
||||||
|
} else {
|
||||||
|
escapedStr += char; // ASCII character needs no escape
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return escapedStr;
|
return escapedStr;
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
export const nameOfTheGlobal = "kcContext";
|
export const nameOfTheGlobal = "kcContext";
|
||||||
|
export const nameOfTheLocalizationRealmOverridesUserProfileProperty =
|
||||||
|
"__localizationRealmOverridesUserProfile";
|
||||||
export const keycloak_resources = "keycloak-resources";
|
export const keycloak_resources = "keycloak-resources";
|
||||||
export const resources_common = "resources-common";
|
export const resources_common = "resources-common";
|
||||||
export const lastKeycloakVersionWithAccountV1 = "21.1.2";
|
export const lastKeycloakVersionWithAccountV1 = "21.1.2";
|
||||||
|
@ -17,17 +17,9 @@ export type BuildOptionsLike = {
|
|||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
|
||||||
export async function appBuild(params: {
|
export async function appBuild(params: {
|
||||||
doSkipIfReactAppBuildDirExists: boolean;
|
|
||||||
buildOptions: BuildOptionsLike;
|
buildOptions: BuildOptionsLike;
|
||||||
}): Promise<{ isAppBuildSuccess: boolean }> {
|
}): Promise<{ isAppBuildSuccess: boolean }> {
|
||||||
const { doSkipIfReactAppBuildDirExists, buildOptions } = params;
|
const { buildOptions } = params;
|
||||||
|
|
||||||
if (
|
|
||||||
doSkipIfReactAppBuildDirExists &&
|
|
||||||
fs.existsSync(buildOptions.reactAppBuildDirPath)
|
|
||||||
) {
|
|
||||||
return { isAppBuildSuccess: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
const { bundler } = buildOptions;
|
const { bundler } = buildOptions;
|
||||||
|
|
||||||
@ -82,6 +74,15 @@ export async function appBuild(params: {
|
|||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
scriptValue.includes("ng") &&
|
||||||
|
scriptValue.includes("build")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
) ?? [];
|
) ?? [];
|
||||||
|
|
||||||
|
@ -84,7 +84,6 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
|
|
||||||
{
|
{
|
||||||
const { isAppBuildSuccess } = await appBuild({
|
const { isAppBuildSuccess } = await appBuild({
|
||||||
doSkipIfReactAppBuildDirExists: true,
|
|
||||||
buildOptions
|
buildOptions
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -432,7 +431,6 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
console.log(chalk.cyan("Detected changes in the theme. Rebuilding ..."));
|
console.log(chalk.cyan("Detected changes in the theme. Rebuilding ..."));
|
||||||
|
|
||||||
const { isAppBuildSuccess } = await appBuild({
|
const { isAppBuildSuccess } = await appBuild({
|
||||||
doSkipIfReactAppBuildDirExists: false,
|
|
||||||
buildOptions
|
buildOptions
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -4,12 +4,14 @@ import yauzl from "yauzl";
|
|||||||
import stream from "stream";
|
import stream from "stream";
|
||||||
import { Deferred } from "evt/tools/Deferred";
|
import { Deferred } from "evt/tools/Deferred";
|
||||||
import { dirname as pathDirname, sep as pathSep } from "path";
|
import { dirname as pathDirname, sep as pathSep } from "path";
|
||||||
|
import { existsAsync } from "./fs.existsAsync";
|
||||||
|
|
||||||
export async function extractArchive(params: {
|
export async function extractArchive(params: {
|
||||||
archiveFilePath: string;
|
archiveFilePath: string;
|
||||||
onArchiveFile: (params: {
|
onArchiveFile: (params: {
|
||||||
relativeFilePathInArchive: string;
|
relativeFilePathInArchive: string;
|
||||||
readFile: () => Promise<Buffer>;
|
readFile: () => Promise<Buffer>;
|
||||||
|
/** NOTE: Will create the directory if it does not exist */
|
||||||
writeFile: (params: { filePath: string; modifiedData?: Buffer }) => Promise<void>;
|
writeFile: (params: { filePath: string; modifiedData?: Buffer }) => Promise<void>;
|
||||||
earlyExit: () => void;
|
earlyExit: () => void;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
@ -42,7 +44,13 @@ export async function extractArchive(params: {
|
|||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const { filePath, modifiedData } = params;
|
const { filePath, modifiedData } = params;
|
||||||
|
|
||||||
await fs.mkdir(pathDirname(filePath), { recursive: true });
|
{
|
||||||
|
const dirPath = pathDirname(filePath);
|
||||||
|
|
||||||
|
if (!(await existsAsync(dirPath))) {
|
||||||
|
await fs.mkdir(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (modifiedData !== undefined) {
|
if (modifiedData !== undefined) {
|
||||||
await fs.writeFile(filePath, modifiedData);
|
await fs.writeFile(filePath, modifiedData);
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import "minimal-polyfills/Object.fromEntries";
|
import "minimal-polyfills/Object.fromEntries";
|
||||||
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
|
|
||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import fallbackMessages from "./baseMessages/en";
|
import fallbackMessages from "./baseMessages/en";
|
||||||
import { getMessages } from "./baseMessages";
|
import { getMessages } from "./baseMessages";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import type { KcContext } from "../kcContext/KcContext";
|
import type { KcContext } from "../kcContext/KcContext";
|
||||||
import { Markdown } from "keycloakify/tools/Markdown";
|
|
||||||
|
|
||||||
export const fallbackLanguageTag = "en";
|
export const fallbackLanguageTag = "en";
|
||||||
|
|
||||||
@ -14,6 +12,7 @@ export type KcContextLike = {
|
|||||||
currentLanguageTag: string;
|
currentLanguageTag: string;
|
||||||
supported: { languageTag: string; url: string; label: string }[];
|
supported: { languageTag: string; url: string; label: string }[];
|
||||||
};
|
};
|
||||||
|
__localizationRealmOverridesUserProfile: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<KcContext extends KcContextLike ? true : false>();
|
assert<KcContext extends KcContextLike ? true : false>();
|
||||||
@ -62,7 +61,7 @@ export type GenericI18n<MessageKey extends string> = {
|
|||||||
/**
|
/**
|
||||||
* Examples assuming currentLanguageTag === "en"
|
* Examples assuming currentLanguageTag === "en"
|
||||||
* advancedMsg("${access-denied} foo bar") === msg("access-denied") + " foo bar" === "Access denied foo bar"
|
* advancedMsg("${access-denied} foo bar") === msg("access-denied") + " foo bar" === "Access denied foo bar"
|
||||||
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === "not-a-message-key"
|
* advancedMsg("${not-a-message-key}") === advancedMsg("not-a-message-key") === "not-a-message-key"
|
||||||
*/
|
*/
|
||||||
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
|
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
|
||||||
};
|
};
|
||||||
@ -100,7 +99,8 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
|
|||||||
...(await getMessages(currentLanguageTag)),
|
...(await getMessages(currentLanguageTag)),
|
||||||
...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}),
|
...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}),
|
||||||
...(extraMessages[currentLanguageTag] ?? {})
|
...(extraMessages[currentLanguageTag] ?? {})
|
||||||
} as any
|
} as any,
|
||||||
|
__localizationRealmOverridesUserProfile: kcContext.__localizationRealmOverridesUserProfile
|
||||||
}),
|
}),
|
||||||
currentLanguageTag,
|
currentLanguageTag,
|
||||||
getChangeLocalUrl: newLanguageTag => {
|
getChangeLocalUrl: newLanguageTag => {
|
||||||
@ -130,11 +130,12 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
|
|||||||
function createI18nTranslationFunctions<MessageKey extends string>(params: {
|
function createI18nTranslationFunctions<MessageKey extends string>(params: {
|
||||||
fallbackMessages: Record<MessageKey, string>;
|
fallbackMessages: Record<MessageKey, string>;
|
||||||
messages: Record<MessageKey, string>;
|
messages: Record<MessageKey, string>;
|
||||||
|
__localizationRealmOverridesUserProfile: Record<string, string>;
|
||||||
}): Pick<GenericI18n<MessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
|
}): Pick<GenericI18n<MessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
|
||||||
const { fallbackMessages, messages } = params;
|
const { fallbackMessages, messages /*__localizationRealmOverridesUserProfile*/ } = params;
|
||||||
|
|
||||||
function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderMarkdown: boolean }): string | JSX.Element | undefined {
|
function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): string | JSX.Element | undefined {
|
||||||
const { key, args, doRenderMarkdown } = props;
|
const { key, args, doRenderAsHtml } = props;
|
||||||
|
|
||||||
const messageOrUndefined: string | undefined = (messages as any)[key] ?? (fallbackMessages as any)[key];
|
const messageOrUndefined: string | undefined = (messages as any)[key] ?? (fallbackMessages as any)[key];
|
||||||
|
|
||||||
@ -163,23 +164,38 @@ function createI18nTranslationFunctions<MessageKey extends string>(params: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
messageWithArgsInjected = messageWithArgsInjected.replace(new RegExp(`\\{${i + startIndex}\\}`, "g"), arg);
|
messageWithArgsInjected = messageWithArgsInjected.replace(
|
||||||
|
new RegExp(`\\{${i + startIndex}\\}`, "g"),
|
||||||
|
arg.replace(/</g, "<").replace(/>/g, ">")
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return messageWithArgsInjected;
|
return messageWithArgsInjected;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return doRenderMarkdown ? (
|
return doRenderAsHtml ? (
|
||||||
<Markdown allowDangerousHtml renderers={{ paragraph: "span" }}>
|
<span
|
||||||
{messageWithArgsInjectedIfAny}
|
// NOTE: The message is trusted. The arguments are not but are escaped.
|
||||||
</Markdown>
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: messageWithArgsInjectedIfAny
|
||||||
|
}}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
messageWithArgsInjectedIfAny
|
messageWithArgsInjectedIfAny
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderMarkdown: boolean }): JSX.Element | string {
|
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): JSX.Element | string {
|
||||||
const { key, args, doRenderMarkdown } = props;
|
const { key, args, doRenderAsHtml } = props;
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
/*
|
||||||
|
if( key in __localizationRealmOverridesUserProfile ){
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
const match = key.match(/^\$\{([^{]+)\}$/);
|
const match = key.match(/^\$\{([^{]+)\}$/);
|
||||||
|
|
||||||
@ -188,26 +204,26 @@ function createI18nTranslationFunctions<MessageKey extends string>(params: {
|
|||||||
const out = resolveMsg({
|
const out = resolveMsg({
|
||||||
key: keyUnwrappedFromCurlyBraces,
|
key: keyUnwrappedFromCurlyBraces,
|
||||||
args,
|
args,
|
||||||
doRenderMarkdown
|
doRenderAsHtml
|
||||||
});
|
});
|
||||||
|
|
||||||
return (out !== undefined ? out : doRenderMarkdown ? <span>{keyUnwrappedFromCurlyBraces}</span> : keyUnwrappedFromCurlyBraces) as any;
|
return (out !== undefined ? out : doRenderAsHtml ? <span>{keyUnwrappedFromCurlyBraces}</span> : keyUnwrappedFromCurlyBraces) as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
msgStr: (key, ...args) => resolveMsg({ key, args, doRenderMarkdown: false }) as string,
|
msgStr: (key, ...args) => resolveMsg({ key, args, doRenderAsHtml: false }) as string,
|
||||||
msg: (key, ...args) => resolveMsg({ key, args, doRenderMarkdown: true }) as JSX.Element,
|
msg: (key, ...args) => resolveMsg({ key, args, doRenderAsHtml: true }) as JSX.Element,
|
||||||
advancedMsg: (key, ...args) =>
|
advancedMsg: (key, ...args) =>
|
||||||
resolveMsgAdvanced({
|
resolveMsgAdvanced({
|
||||||
key,
|
key,
|
||||||
args,
|
args,
|
||||||
doRenderMarkdown: true
|
doRenderAsHtml: true
|
||||||
}) as JSX.Element,
|
}) as JSX.Element,
|
||||||
advancedMsgStr: (key, ...args) =>
|
advancedMsgStr: (key, ...args) =>
|
||||||
resolveMsgAdvanced({
|
resolveMsgAdvanced({
|
||||||
key,
|
key,
|
||||||
args,
|
args,
|
||||||
doRenderMarkdown: false
|
doRenderAsHtml: false
|
||||||
}) as string
|
}) as string
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import type { ThemeType, LoginThemePageId } from "keycloakify/bin/shared/constants";
|
import type {
|
||||||
|
ThemeType,
|
||||||
|
LoginThemePageId,
|
||||||
|
nameOfTheLocalizationRealmOverridesUserProfileProperty
|
||||||
|
} from "keycloakify/bin/shared/constants";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import type { Equals } from "tsafe";
|
import type { Equals } from "tsafe";
|
||||||
import type { MessageKey } from "../i18n/i18n";
|
import type { MessageKey } from "../i18n/i18n";
|
||||||
@ -140,6 +144,7 @@ export declare namespace KcContext {
|
|||||||
tabId: string;
|
tabId: string;
|
||||||
ssoLoginInOtherTabsUrl: string;
|
ssoLoginInOtherTabsUrl: string;
|
||||||
};
|
};
|
||||||
|
__localizationRealmOverridesUserProfile: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SamlPostForm = Common & {
|
export type SamlPostForm = Common & {
|
||||||
@ -750,3 +755,12 @@ export type PasswordPolicies = {
|
|||||||
/** Whether the password can be the email address */
|
/** Whether the password can be the email address */
|
||||||
notEmail?: boolean;
|
notEmail?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
assert<
|
||||||
|
KcContext.Common extends Record<
|
||||||
|
typeof nameOfTheLocalizationRealmOverridesUserProfileProperty,
|
||||||
|
unknown
|
||||||
|
>
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
>();
|
||||||
|
@ -221,7 +221,8 @@ export const kcContextCommonMock: KcContext.Common = {
|
|||||||
},
|
},
|
||||||
scripts: [],
|
scripts: [],
|
||||||
isAppInitiatedAction: false,
|
isAppInitiatedAction: false,
|
||||||
properties: {}
|
properties: {},
|
||||||
|
__localizationRealmOverridesUserProfile: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loginUrl = {
|
const loginUrl = {
|
||||||
|
@ -4,11 +4,13 @@ import { fallbackLanguageTag } from "keycloakify/login/i18n/i18n";
|
|||||||
import { useConst } from "keycloakify/tools/useConst";
|
import { useConst } from "keycloakify/tools/useConst";
|
||||||
import { useConstCallback } from "keycloakify/tools/useConstCallback";
|
import { useConstCallback } from "keycloakify/tools/useConstCallback";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import { Evt } from "evt";
|
import {
|
||||||
import { useRerenderOnStateChange } from "evt/hooks/useRerenderOnStateChange";
|
createStatefulObservable,
|
||||||
|
useRerenderOnChange
|
||||||
|
} from "keycloakify/tools/StatefulObservable";
|
||||||
import { KcContext } from "../kcContext";
|
import { KcContext } from "../kcContext";
|
||||||
|
|
||||||
const evtTermsMarkdown = Evt.create<string | undefined>(undefined);
|
const obsTermsMarkdown = createStatefulObservable<string | undefined>(() => undefined);
|
||||||
|
|
||||||
export type KcContextLike = {
|
export type KcContextLike = {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
@ -45,15 +47,15 @@ export function useDownloadTerms(params: {
|
|||||||
if (kcContext.pageId === "terms.ftl" || kcContext.termsAcceptanceRequired) {
|
if (kcContext.pageId === "terms.ftl" || kcContext.termsAcceptanceRequired) {
|
||||||
downloadTermMarkdownMemoized(
|
downloadTermMarkdownMemoized(
|
||||||
kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag
|
kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag
|
||||||
).then(thermMarkdown => (evtTermsMarkdown.state = thermMarkdown));
|
).then(thermMarkdown => (obsTermsMarkdown.current = thermMarkdown));
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTermsMarkdown() {
|
export function useTermsMarkdown() {
|
||||||
useRerenderOnStateChange(evtTermsMarkdown);
|
useRerenderOnChange(obsTermsMarkdown);
|
||||||
|
|
||||||
const termsMarkdown = evtTermsMarkdown.state;
|
const termsMarkdown = obsTermsMarkdown.current;
|
||||||
|
|
||||||
return { termsMarkdown };
|
return { termsMarkdown };
|
||||||
}
|
}
|
||||||
|
16
src/tools/StatefulObservable/README.md
Normal file
16
src/tools/StatefulObservable/README.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
`StatefulObservable` is a construct that allow to avoid having to depend on [EVT](https://evt.land).
|
||||||
|
|
||||||
|
A `StatefulObservable` can be converted to an evt with:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { statefulObservableToStatefulEvt } from "powerhooks/tools/StatefulObservable/statefulObservableToStatefulEvt";
|
||||||
|
|
||||||
|
const evtXyz = statefulObservableToStatefulEvt({
|
||||||
|
statefulObservable: $xyz
|
||||||
|
//Optionally you can pass a Ctx
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
WARNING: Unlike `StatefulEvt`, `StatefulObservable` do not post when we first attach.
|
||||||
|
If the current value was not yet evaluated `next()` is called on the initial value returned by the function that
|
||||||
|
returns it.
|
58
src/tools/StatefulObservable/StatefulObservable.ts
Normal file
58
src/tools/StatefulObservable/StatefulObservable.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { is } from "tsafe/is";
|
||||||
|
|
||||||
|
export type StatefulObservable<T> = {
|
||||||
|
current: T;
|
||||||
|
subscribe: (next: (data: T) => void) => Subscription;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Subscription = {
|
||||||
|
unsubscribe(): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createStatefulObservable<T>(
|
||||||
|
getInitialValue: () => T
|
||||||
|
): StatefulObservable<T> {
|
||||||
|
const nextFunctions: ((data: T) => void)[] = [];
|
||||||
|
|
||||||
|
const { get, set } = (() => {
|
||||||
|
let wrappedState: [T] | undefined = undefined;
|
||||||
|
|
||||||
|
function set(data: T) {
|
||||||
|
wrappedState = [data];
|
||||||
|
|
||||||
|
nextFunctions.forEach(next => next(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
get: () => {
|
||||||
|
if (wrappedState === undefined) {
|
||||||
|
set(getInitialValue());
|
||||||
|
assert(!is<undefined>(wrappedState));
|
||||||
|
}
|
||||||
|
return wrappedState[0];
|
||||||
|
},
|
||||||
|
set
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
return Object.defineProperty(
|
||||||
|
{
|
||||||
|
current: null as any as T,
|
||||||
|
subscribe: (next: (data: T) => void) => {
|
||||||
|
nextFunctions.push(next);
|
||||||
|
|
||||||
|
return {
|
||||||
|
unsubscribe: () =>
|
||||||
|
nextFunctions.splice(nextFunctions.indexOf(next), 1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"current",
|
||||||
|
{
|
||||||
|
enumerable: true,
|
||||||
|
get,
|
||||||
|
set
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
2
src/tools/StatefulObservable/hooks/index.ts
Normal file
2
src/tools/StatefulObservable/hooks/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./useObservable";
|
||||||
|
export * from "./useRerenderOnChange";
|
25
src/tools/StatefulObservable/hooks/useObservable.ts
Normal file
25
src/tools/StatefulObservable/hooks/useObservable.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import type { Subscription } from "../StatefulObservable";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equivalent of https://docs.evt.land/api/react-hooks
|
||||||
|
*/
|
||||||
|
export function useObservable(
|
||||||
|
effect: (params: {
|
||||||
|
registerSubscription: (subscription: Subscription) => void;
|
||||||
|
}) => void,
|
||||||
|
deps: React.DependencyList
|
||||||
|
): void {
|
||||||
|
useEffect(() => {
|
||||||
|
const subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
effect({
|
||||||
|
registerSubscription: subscription => subscriptions.push(subscription)
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||||
|
subscriptions.length = 0;
|
||||||
|
};
|
||||||
|
}, deps);
|
||||||
|
}
|
19
src/tools/StatefulObservable/hooks/useRerenderOnChange.ts
Normal file
19
src/tools/StatefulObservable/hooks/useRerenderOnChange.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { useObservable } from "./useObservable";
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { StatefulObservable } from "../StatefulObservable";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equivalent of https://docs.evt.land/api/react-hooks
|
||||||
|
* */
|
||||||
|
export function useRerenderOnChange($: StatefulObservable<unknown>): void {
|
||||||
|
//NOTE: We use function in case the state is a function
|
||||||
|
const [, setCurrent] = useState(() => $.current);
|
||||||
|
|
||||||
|
useObservable(
|
||||||
|
({ registerSubscription }) => {
|
||||||
|
const subscription = $.subscribe(current => setCurrent(() => current));
|
||||||
|
registerSubscription(subscription);
|
||||||
|
},
|
||||||
|
[$]
|
||||||
|
);
|
||||||
|
}
|
2
src/tools/StatefulObservable/index.ts
Normal file
2
src/tools/StatefulObservable/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./StatefulObservable";
|
||||||
|
export * from "./hooks";
|
Reference in New Issue
Block a user