Compare commits
68 Commits
Author | SHA1 | Date | |
---|---|---|---|
e48459762e | |||
235ebeae97 | |||
dfe909606e | |||
1ba780598d | |||
88923838c5 | |||
98e46d6ac9 | |||
daff614fb4 | |||
5ea324c7f2 | |||
23fedbf94a | |||
27fdaeff46 | |||
53c0079656 | |||
93780b77e0 | |||
b712ed0421 | |||
ee96f1b345 | |||
d13464df3d | |||
6bde2e4d96 | |||
0a4953c020 | |||
96c488880c | |||
7e0adf3f66 | |||
09f716440a | |||
2251c84171 | |||
5cfe78dcd1 | |||
6a48325132 | |||
294be0a79a | |||
c94b264b44 | |||
7220c4e3e3 | |||
5aadeba2ec | |||
0f47a5b6ba | |||
36f32d28f2 | |||
6d69ccf229 | |||
37073b42be | |||
837501c948 | |||
b300966fa8 | |||
730eb06c84 | |||
aca8d3f4b7 | |||
b5b3af4659 | |||
6cd231426d | |||
0c7cd1cd75 | |||
2425704ead | |||
4e22159206 | |||
52cf1ba02c | |||
516e84182f | |||
a3a9853e18 | |||
08e26600fd | |||
7793c2c6ba | |||
9e826d16dd | |||
80618bbd9c | |||
38ad47ea75 | |||
45ed359bef | |||
fcc26c3e7a | |||
d4ff6b1f40 | |||
557de34eea | |||
e034dc4d90 | |||
cfbd1e5e4b | |||
0df661819f | |||
1a9f6d10d4 | |||
a787215c95 | |||
64ab400af5 | |||
a463878bf2 | |||
9f72024c61 | |||
243fbd4dc9 | |||
4e6a290693 | |||
ac05d529ca | |||
b38d79004a | |||
f4a547df11 | |||
2b87c35058 | |||
b11833e450 | |||
fa8e119514 |
@ -6,4 +6,5 @@ node_modules/
|
|||||||
/src/test/apps/
|
/src/test/apps/
|
||||||
/src/tools/types/
|
/src/tools/types/
|
||||||
/sample_react_project
|
/sample_react_project
|
||||||
/build_keycloak/
|
/build_keycloak/
|
||||||
|
/src/lib/i18n/generated_messages/
|
26
README.md
26
README.md
@ -8,9 +8,6 @@
|
|||||||
<a href="https://github.com/garronej/keycloakify/actions">
|
<a href="https://github.com/garronej/keycloakify/actions">
|
||||||
<img src="https://github.com/garronej/keycloakify/workflows/ci/badge.svg?branch=main">
|
<img src="https://github.com/garronej/keycloakify/workflows/ci/badge.svg?branch=main">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://bundlephobia.com/package/keycloakify">
|
|
||||||
<img src="https://img.shields.io/bundlephobia/minzip/keycloakify">
|
|
||||||
</a>
|
|
||||||
<a href="https://www.npmjs.com/package/keycloakify">
|
<a href="https://www.npmjs.com/package/keycloakify">
|
||||||
<img src="https://img.shields.io/npm/dm/keycloakify">
|
<img src="https://img.shields.io/npm/dm/keycloakify">
|
||||||
</a>
|
</a>
|
||||||
@ -27,15 +24,9 @@
|
|||||||
<a href="https://www.keycloakify.dev">Home</a>
|
<a href="https://www.keycloakify.dev">Home</a>
|
||||||
-
|
-
|
||||||
<a href="https://docs.keycloakify.dev">Documentation</a>
|
<a href="https://docs.keycloakify.dev">Documentation</a>
|
||||||
</p>
|
|
||||||
<p align="center"> ---- Project starter / Demo setup ---- </p>
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://github.com/garronej/keycloakify-starter">CSS Level customization</a>
|
|
||||||
-
|
-
|
||||||
<a href="https://github.com/garronej/keycloakify-advanced-starter">Component Level customization</a>
|
<a href="https://github.com/codegouvfr/keycloakify-starter">Starter project</a>
|
||||||
</p>
|
</p>
|
||||||
<p align="center"> ---- </p>
|
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@ -49,6 +40,21 @@
|
|||||||
|
|
||||||
# Changelog highlights
|
# Changelog highlights
|
||||||
|
|
||||||
|
## 6.12
|
||||||
|
|
||||||
|
Massive improvement in the developer experience:
|
||||||
|
|
||||||
|
- There is now only one starter repo: https://github.com/codegouvfr/keycloakify-starter
|
||||||
|
- A lot of comments have been added in the code of the starter to make it easier to get started.
|
||||||
|
- The doc has been updated: https://docs.keycloakify.dev
|
||||||
|
- A lot of improvements in the type system.
|
||||||
|
|
||||||
|
## 6.11.4
|
||||||
|
|
||||||
|
- You no longer need to have Maven installed to build the theme. Thanks to @lordvlad, [see PR](https://github.com/InseeFrLab/keycloakify/pull/239).
|
||||||
|
- Feature new build options: [`bundler`](https://docs.keycloakify.dev/build-options#keycloakify.bundler), [`groupId`](https://docs.keycloakify.dev/build-options#keycloakify.groupid), [`artifactId`](https://docs.keycloakify.dev/build-options#keycloakify.artifactid), [`version`](https://docs.keycloakify.dev/build-options#version).
|
||||||
|
Theses options can be user to customize the output name of the .jar. You can use environnement variables to overrides the values read in the package.json. Thanks to @lordvlad.
|
||||||
|
|
||||||
## 6.10.0
|
## 6.10.0
|
||||||
|
|
||||||
- Widows compat (thanks to @lordvlad, [see PR](https://github.com/InseeFrLab/keycloakify/pull/226)). WSL is no longer required 🎉
|
- Widows compat (thanks to @lordvlad, [see PR](https://github.com/InseeFrLab/keycloakify/pull/226)). WSL is no longer required 🎉
|
||||||
|
26
package.json
26
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "keycloakify",
|
"name": "keycloakify",
|
||||||
"version": "6.10.0",
|
"version": "6.12.7",
|
||||||
"description": "Keycloak theme generator for Reacts app",
|
"description": "Keycloak theme generator for Reacts app",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -15,11 +15,13 @@
|
|||||||
"copy-files": "copyfiles -u 1 src/**/*.ftl",
|
"copy-files": "copyfiles -u 1 src/**/*.ftl",
|
||||||
"pretest": "yarn build:test",
|
"pretest": "yarn build:test",
|
||||||
"test": "node dist_test/test/bin && node dist_test/test/lib",
|
"test": "node dist_test/test/bin && node dist_test/test/lib",
|
||||||
"generate-messages": "node dist/bin/generate-i18n-messages.js",
|
|
||||||
"link_in_test_app": "node dist/bin/link_in_test_app.js",
|
|
||||||
"_format": "prettier '**/*.{ts,tsx,json,md}'",
|
"_format": "prettier '**/*.{ts,tsx,json,md}'",
|
||||||
"format": "yarn _format --write",
|
"format": "yarn _format --write",
|
||||||
"format:check": "yarn _format --list-different"
|
"format:check": "yarn _format --list-different",
|
||||||
|
"generate-messages": "ts-node --skipProject src/scripts/generate-i18n-messages.ts",
|
||||||
|
"link-in-app": "ts-node --skipProject src/scripts/link-in-app.ts",
|
||||||
|
"link-in-starter": "yarn link-in-app keycloakify-advanced-starter",
|
||||||
|
"tsc-watch": "tsc -p src/bin -w & tsc -p src/lib -w "
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"keycloakify": "dist/bin/keycloakify/index.js",
|
"keycloakify": "dist/bin/keycloakify/index.js",
|
||||||
@ -40,6 +42,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"files": [
|
"files": [
|
||||||
"src/",
|
"src/",
|
||||||
|
"!src/scripts",
|
||||||
"dist/",
|
"dist/",
|
||||||
"!dist/tsconfig.tsbuildinfo"
|
"!dist/tsconfig.tsbuildinfo"
|
||||||
],
|
],
|
||||||
@ -61,7 +64,7 @@
|
|||||||
"@babel/core": "^7.0.0",
|
"@babel/core": "^7.0.0",
|
||||||
"@types/memoizee": "^0.4.7",
|
"@types/memoizee": "^0.4.7",
|
||||||
"@types/minimist": "^1.2.2",
|
"@types/minimist": "^1.2.2",
|
||||||
"@types/node": "^17.0.25",
|
"@types/node": "^18.14.1",
|
||||||
"@types/react": "18.0.9",
|
"@types/react": "18.0.9",
|
||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
"husky": "^4.3.8",
|
"husky": "^4.3.8",
|
||||||
@ -70,24 +73,21 @@
|
|||||||
"properties-parser": "^0.3.1",
|
"properties-parser": "^0.3.1",
|
||||||
"react": "18.1.0",
|
"react": "18.1.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"@emotion/react": "^11.10.4",
|
"typescript": "^4.9.5",
|
||||||
"typescript": "^4.2.3"
|
"ts-node": "^10.9.1",
|
||||||
|
"scripting-tools": "^0.19.13"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/rest": "^18.12.0",
|
"@octokit/rest": "^18.12.0",
|
||||||
"cheerio": "^1.0.0-rc.5",
|
"cheerio": "^1.0.0-rc.5",
|
||||||
"cli-select": "^1.1.2",
|
"cli-select": "^1.1.2",
|
||||||
"evt": "^2.4.13",
|
"evt": "^2.4.15",
|
||||||
"memoizee": "^0.4.15",
|
|
||||||
"minimal-polyfills": "^2.2.2",
|
"minimal-polyfills": "^2.2.2",
|
||||||
"minimist": "^1.2.6",
|
"minimist": "^1.2.6",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"powerhooks": "^0.22.0",
|
|
||||||
"react-markdown": "^5.0.3",
|
"react-markdown": "^5.0.3",
|
||||||
"rfc4648": "^1.5.2",
|
"rfc4648": "^1.5.2",
|
||||||
"scripting-tools": "^0.19.13",
|
"tsafe": "^1.4.3",
|
||||||
"tsafe": "^1.4.1",
|
|
||||||
"tss-react": "4.4.1-rc.0",
|
|
||||||
"zod": "^3.17.10"
|
"zod": "^3.17.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,11 +13,11 @@
|
|||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"packagePatterns": ["*"],
|
"packagePatterns": ["*"],
|
||||||
"excludePackagePatterns": ["powerhooks", "tsafe", "evt"],
|
"excludePackagePatterns": ["tsafe", "evt"],
|
||||||
"enabled": false
|
"enabled": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"packagePatterns": ["powerhooks", "tsafe", "evt"],
|
"packagePatterns": ["tsafe", "evt"],
|
||||||
"matchUpdateTypes": ["minor", "patch"],
|
"matchUpdateTypes": ["minor", "patch"],
|
||||||
"automerge": true,
|
"automerge": true,
|
||||||
"automergeType": "branch",
|
"automergeType": "branch",
|
||||||
|
@ -3,7 +3,11 @@ import { assert } from "tsafe/assert";
|
|||||||
import type { Equals } from "tsafe";
|
import type { Equals } from "tsafe";
|
||||||
import { id } from "tsafe/id";
|
import { id } from "tsafe/id";
|
||||||
import { parse as urlParse } from "url";
|
import { parse as urlParse } from "url";
|
||||||
|
import { typeGuard } from "tsafe/typeGuard";
|
||||||
|
import { symToStr } from "tsafe/symToStr";
|
||||||
|
|
||||||
|
const bundlers = ["mvn", "keycloakify", "none"] as const;
|
||||||
|
type Bundler = (typeof bundlers)[number];
|
||||||
type ParsedPackageJson = {
|
type ParsedPackageJson = {
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
@ -12,6 +16,9 @@ type ParsedPackageJson = {
|
|||||||
extraPages?: string[];
|
extraPages?: string[];
|
||||||
extraThemeProperties?: string[];
|
extraThemeProperties?: string[];
|
||||||
areAppAndKeycloakServerSharingSameDomain?: boolean;
|
areAppAndKeycloakServerSharingSameDomain?: boolean;
|
||||||
|
artifactId?: string;
|
||||||
|
groupId?: string;
|
||||||
|
bundler?: Bundler;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -23,12 +30,15 @@ const zParsedPackageJson = z.object({
|
|||||||
.object({
|
.object({
|
||||||
"extraPages": z.array(z.string()).optional(),
|
"extraPages": z.array(z.string()).optional(),
|
||||||
"extraThemeProperties": z.array(z.string()).optional(),
|
"extraThemeProperties": z.array(z.string()).optional(),
|
||||||
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional()
|
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional(),
|
||||||
|
"artifactId": z.string().optional(),
|
||||||
|
"groupId": z.string().optional(),
|
||||||
|
"bundler": z.enum(bundlers).optional()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
assert<Equals<ReturnType<typeof zParsedPackageJson["parse"]>, ParsedPackageJson>>();
|
assert<Equals<ReturnType<(typeof zParsedPackageJson)["parse"]>, ParsedPackageJson>>();
|
||||||
|
|
||||||
/** Consolidated build option gathered form CLI arguments and config in package.json */
|
/** Consolidated build option gathered form CLI arguments and config in package.json */
|
||||||
export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets;
|
export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets;
|
||||||
@ -40,8 +50,9 @@ export namespace BuildOptions {
|
|||||||
themeName: string;
|
themeName: string;
|
||||||
extraPages?: string[];
|
extraPages?: string[];
|
||||||
extraThemeProperties?: string[];
|
extraThemeProperties?: string[];
|
||||||
//NOTE: Only for the pom.xml file, questionable utility...
|
|
||||||
groupId: string;
|
groupId: string;
|
||||||
|
artifactId: string;
|
||||||
|
bundler: Bundler;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Standalone = Common & {
|
export type Standalone = Common & {
|
||||||
@ -108,7 +119,7 @@ export function readBuildOptions(params: {
|
|||||||
const common: BuildOptions.Common = (() => {
|
const common: BuildOptions.Common = (() => {
|
||||||
const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
|
const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
|
||||||
|
|
||||||
const { extraPages, extraThemeProperties } = keycloakify ?? {};
|
const { extraPages, extraThemeProperties, groupId, artifactId, bundler } = keycloakify ?? {};
|
||||||
|
|
||||||
const themeName = name
|
const themeName = name
|
||||||
.replace(/^@(.*)/, "$1")
|
.replace(/^@(.*)/, "$1")
|
||||||
@ -117,10 +128,26 @@ export function readBuildOptions(params: {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
themeName,
|
themeName,
|
||||||
|
"bundler": (() => {
|
||||||
|
const { KEYCLOAKIFY_BUNDLER } = process.env;
|
||||||
|
|
||||||
|
assert(
|
||||||
|
typeGuard<Bundler | undefined>(
|
||||||
|
KEYCLOAKIFY_BUNDLER,
|
||||||
|
[undefined, ...id<readonly string[]>(bundlers)].includes(KEYCLOAKIFY_BUNDLER)
|
||||||
|
),
|
||||||
|
`${symToStr({ KEYCLOAKIFY_BUNDLER })} should be one of ${bundlers.join(", ")}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return KEYCLOAKIFY_BUNDLER ?? bundler ?? "keycloakify";
|
||||||
|
})(),
|
||||||
|
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? artifactId ?? `${themeName}-keycloak-theme`,
|
||||||
"groupId": (() => {
|
"groupId": (() => {
|
||||||
const fallbackGroupId = `${themeName}.keycloak`;
|
const fallbackGroupId = `${themeName}.keycloak`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
process.env.KEYCLOAKIFY_GROUP_ID ??
|
||||||
|
groupId ??
|
||||||
(!homepage
|
(!homepage
|
||||||
? fallbackGroupId
|
? fallbackGroupId
|
||||||
: urlParse(homepage)
|
: urlParse(homepage)
|
||||||
@ -130,7 +157,7 @@ export function readBuildOptions(params: {
|
|||||||
.join(".") ?? fallbackGroupId) + ".keycloak"
|
.join(".") ?? fallbackGroupId) + ".keycloak"
|
||||||
);
|
);
|
||||||
})(),
|
})(),
|
||||||
"version": version,
|
"version": process.env.KEYCLOAKIFY_VERSION ?? version,
|
||||||
extraPages,
|
extraPages,
|
||||||
extraThemeProperties,
|
extraThemeProperties,
|
||||||
isSilent
|
isSilent
|
||||||
|
@ -174,6 +174,10 @@ ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
|
|||||||
) || (
|
) || (
|
||||||
["masterAdminClient", "delegateForUpdate", "defaultRole"]?seq_contains(key) &&
|
["masterAdminClient", "delegateForUpdate", "defaultRole"]?seq_contains(key) &&
|
||||||
are_same_path(path, ["realm"])
|
are_same_path(path, ["realm"])
|
||||||
|
) || (
|
||||||
|
"error.ftl" == pageId &&
|
||||||
|
are_same_path(path, ["realm"]) &&
|
||||||
|
!["name", "displayName", "displayNameHtml", "internationalizationEnabled", "registrationEmailAsUsername" ]?seq_contains(key)
|
||||||
)
|
)
|
||||||
>
|
>
|
||||||
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
|
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
|
||||||
|
@ -68,7 +68,7 @@ export namespace BuildOptionsLike {
|
|||||||
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PageId = typeof pageIds[number];
|
export type PageId = (typeof pageIds)[number];
|
||||||
|
|
||||||
export function generateFtlFilesCodeFactory(params: {
|
export function generateFtlFilesCodeFactory(params: {
|
||||||
indexHtmlCode: string;
|
indexHtmlCode: string;
|
||||||
|
@ -7,6 +7,8 @@ import type { BuildOptions } from "./BuildOptions";
|
|||||||
export type BuildOptionsLike = {
|
export type BuildOptionsLike = {
|
||||||
themeName: string;
|
themeName: string;
|
||||||
groupId: string;
|
groupId: string;
|
||||||
|
artifactId?: string;
|
||||||
|
version: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -16,7 +18,6 @@ export type BuildOptionsLike = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function generateJavaStackFiles(params: {
|
export function generateJavaStackFiles(params: {
|
||||||
version: string;
|
|
||||||
keycloakThemeBuildingDirPath: string;
|
keycloakThemeBuildingDirPath: string;
|
||||||
doBundlesEmailTemplate: boolean;
|
doBundlesEmailTemplate: boolean;
|
||||||
buildOptions: BuildOptionsLike;
|
buildOptions: BuildOptionsLike;
|
||||||
@ -24,14 +25,11 @@ export function generateJavaStackFiles(params: {
|
|||||||
jarFilePath: string;
|
jarFilePath: string;
|
||||||
} {
|
} {
|
||||||
const {
|
const {
|
||||||
version,
|
buildOptions: { groupId, themeName, version, artifactId },
|
||||||
buildOptions: { groupId, themeName },
|
|
||||||
keycloakThemeBuildingDirPath,
|
keycloakThemeBuildingDirPath,
|
||||||
doBundlesEmailTemplate
|
doBundlesEmailTemplate
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
const artefactId = `${themeName}-keycloak-theme`;
|
|
||||||
|
|
||||||
{
|
{
|
||||||
const { pomFileCode } = (function generatePomFileCode(): {
|
const { pomFileCode } = (function generatePomFileCode(): {
|
||||||
pomFileCode: string;
|
pomFileCode: string;
|
||||||
@ -43,9 +41,9 @@ export function generateJavaStackFiles(params: {
|
|||||||
` xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">`,
|
` xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">`,
|
||||||
` <modelVersion>4.0.0</modelVersion>`,
|
` <modelVersion>4.0.0</modelVersion>`,
|
||||||
` <groupId>${groupId}</groupId>`,
|
` <groupId>${groupId}</groupId>`,
|
||||||
` <artifactId>${artefactId}</artifactId>`,
|
` <artifactId>${artifactId}</artifactId>`,
|
||||||
` <version>${version}</version>`,
|
` <version>${version}</version>`,
|
||||||
` <name>${artefactId}</name>`,
|
` <name>${artifactId}</name>`,
|
||||||
` <description />`,
|
` <description />`,
|
||||||
`</project>`
|
`</project>`
|
||||||
].join("\n");
|
].join("\n");
|
||||||
@ -84,6 +82,6 @@ export function generateJavaStackFiles(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${artefactId}-${version}.jar`)
|
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${artifactId}-${version}.jar`)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -4,5 +4,5 @@ export * from "./keycloakify";
|
|||||||
import { main } from "./keycloakify";
|
import { main } from "./keycloakify";
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
main();
|
main().catch(e => console.error(e));
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,9 @@ import * as fs from "fs";
|
|||||||
import { readBuildOptions } from "./BuildOptions";
|
import { readBuildOptions } from "./BuildOptions";
|
||||||
import { getLogger } from "../tools/logger";
|
import { getLogger } from "../tools/logger";
|
||||||
import { getCliOptions } from "../tools/cliOptions";
|
import { getCliOptions } from "../tools/cliOptions";
|
||||||
|
import jar from "../tools/jar";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { Equals } from "tsafe";
|
||||||
|
|
||||||
const reactProjectDirPath = process.cwd();
|
const reactProjectDirPath = process.cwd();
|
||||||
|
|
||||||
@ -45,17 +48,34 @@ export async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { jarFilePath } = generateJavaStackFiles({
|
const { jarFilePath } = generateJavaStackFiles({
|
||||||
"version": buildOptions.version,
|
|
||||||
keycloakThemeBuildingDirPath,
|
keycloakThemeBuildingDirPath,
|
||||||
doBundlesEmailTemplate,
|
doBundlesEmailTemplate,
|
||||||
buildOptions
|
buildOptions
|
||||||
});
|
});
|
||||||
|
|
||||||
child_process.execSync("mvn package", {
|
switch (buildOptions.bundler) {
|
||||||
"cwd": keycloakThemeBuildingDirPath
|
case "none":
|
||||||
});
|
logger.log("😱 Skipping bundling step, there will be no jar");
|
||||||
|
break;
|
||||||
|
case "keycloakify":
|
||||||
|
logger.log("🫶 Let keycloakify do its thang");
|
||||||
|
await jar({
|
||||||
|
"rootPath": pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources"),
|
||||||
|
"version": buildOptions.version,
|
||||||
|
"groupId": buildOptions.groupId,
|
||||||
|
"artifactId": buildOptions.artifactId,
|
||||||
|
"targetPath": jarFilePath
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "mvn":
|
||||||
|
logger.log("🫙 Run maven to deliver a jar");
|
||||||
|
child_process.execSync("mvn package", { "cwd": keycloakThemeBuildingDirPath });
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assert<Equals<typeof buildOptions.bundler, never>>(false);
|
||||||
|
}
|
||||||
|
|
||||||
//We want, however, to test in a container running the latest Keycloak version
|
// We want, however, to test in a container running the latest Keycloak version
|
||||||
const containerKeycloakVersion = "20.0.1";
|
const containerKeycloakVersion = "20.0.1";
|
||||||
|
|
||||||
generateStartKeycloakTestingContainer({
|
generateStartKeycloakTestingContainer({
|
||||||
|
@ -1,128 +0,0 @@
|
|||||||
import { execSync } from "child_process";
|
|
||||||
import { join as pathJoin, relative as pathRelative } from "path";
|
|
||||||
import { exclude } from "tsafe/exclude";
|
|
||||||
import * as fs from "fs";
|
|
||||||
|
|
||||||
const keycloakifyDirPath = pathJoin(__dirname, "..", "..");
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
pathJoin(keycloakifyDirPath, "dist", "package.json"),
|
|
||||||
Buffer.from(
|
|
||||||
JSON.stringify(
|
|
||||||
(() => {
|
|
||||||
const packageJsonParsed = JSON.parse(fs.readFileSync(pathJoin(keycloakifyDirPath, "package.json")).toString("utf8"));
|
|
||||||
|
|
||||||
return {
|
|
||||||
...packageJsonParsed,
|
|
||||||
"main": packageJsonParsed["main"].replace(/^dist\//, ""),
|
|
||||||
"types": packageJsonParsed["types"].replace(/^dist\//, ""),
|
|
||||||
"bin": Object.fromEntries(Object.entries<string>(packageJsonParsed["bin"]).map(([k, v]) => [k, v.replace(/^dist\//, "")]))
|
|
||||||
};
|
|
||||||
})(),
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
),
|
|
||||||
"utf8"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const commonThirdPartyDeps = (() => {
|
|
||||||
const namespaceModuleNames = ["@emotion"];
|
|
||||||
const standaloneModuleNames = ["react", "@types/react", "powerhooks", "tss-react", "evt"];
|
|
||||||
|
|
||||||
return [
|
|
||||||
...namespaceModuleNames
|
|
||||||
.map(namespaceModuleName =>
|
|
||||||
fs
|
|
||||||
.readdirSync(pathJoin(keycloakifyDirPath, "node_modules", namespaceModuleName))
|
|
||||||
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
|
|
||||||
)
|
|
||||||
.reduce((prev, curr) => [...prev, ...curr], []),
|
|
||||||
...standaloneModuleNames
|
|
||||||
];
|
|
||||||
})();
|
|
||||||
|
|
||||||
const yarnHomeDirPath = pathJoin(keycloakifyDirPath, ".yarn_home");
|
|
||||||
|
|
||||||
fs.rmSync(yarnHomeDirPath, { "recursive": true, "force": true });
|
|
||||||
fs.mkdirSync(yarnHomeDirPath);
|
|
||||||
|
|
||||||
const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
|
|
||||||
const { targetModuleName, cwd } = params;
|
|
||||||
|
|
||||||
const cmd = ["yarn", "link", ...(targetModuleName !== undefined ? [targetModuleName] : [])].join(" ");
|
|
||||||
|
|
||||||
console.log(`$ cd ${pathRelative(keycloakifyDirPath, cwd) || "."} && ${cmd}`);
|
|
||||||
|
|
||||||
execSync(cmd, {
|
|
||||||
cwd,
|
|
||||||
"env": {
|
|
||||||
...process.env,
|
|
||||||
"HOME": yarnHomeDirPath
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const testAppPaths = (() => {
|
|
||||||
const arg = process.argv[2];
|
|
||||||
|
|
||||||
const testAppNames = arg !== undefined ? [arg] : ["keycloakify-starter", "keycloakify-advanced-starter"];
|
|
||||||
|
|
||||||
return testAppNames
|
|
||||||
.map(testAppName => {
|
|
||||||
const testAppPath = pathJoin(keycloakifyDirPath, "..", testAppName);
|
|
||||||
|
|
||||||
if (fs.existsSync(testAppPath)) {
|
|
||||||
return testAppPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn(`Skipping ${testAppName} since it cant be found here: ${testAppPath}`);
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
})
|
|
||||||
.filter(exclude(undefined));
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (testAppPaths.length === 0) {
|
|
||||||
console.error("No test app to link into!");
|
|
||||||
process.exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
testAppPaths.forEach(testAppPath => execSync("yarn install", { "cwd": testAppPath }));
|
|
||||||
|
|
||||||
console.log("=== Linking common dependencies ===");
|
|
||||||
|
|
||||||
const total = commonThirdPartyDeps.length;
|
|
||||||
let current = 0;
|
|
||||||
|
|
||||||
commonThirdPartyDeps.forEach(commonThirdPartyDep => {
|
|
||||||
current++;
|
|
||||||
|
|
||||||
console.log(`${current}/${total} ${commonThirdPartyDep}`);
|
|
||||||
|
|
||||||
const localInstallPath = pathJoin(
|
|
||||||
...[keycloakifyDirPath, "node_modules", ...(commonThirdPartyDep.startsWith("@") ? commonThirdPartyDep.split("/") : [commonThirdPartyDep])]
|
|
||||||
);
|
|
||||||
|
|
||||||
execYarnLink({ "cwd": localInstallPath });
|
|
||||||
});
|
|
||||||
|
|
||||||
commonThirdPartyDeps.forEach(commonThirdPartyDep =>
|
|
||||||
testAppPaths.forEach(testAppPath =>
|
|
||||||
execYarnLink({
|
|
||||||
"cwd": testAppPath,
|
|
||||||
"targetModuleName": commonThirdPartyDep
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("=== Linking in house dependencies ===");
|
|
||||||
|
|
||||||
execYarnLink({ "cwd": pathJoin(keycloakifyDirPath, "dist") });
|
|
||||||
|
|
||||||
testAppPaths.forEach(testAppPath =>
|
|
||||||
execYarnLink({
|
|
||||||
"cwd": testAppPath,
|
|
||||||
"targetModuleName": "keycloakify"
|
|
||||||
})
|
|
||||||
);
|
|
54
src/bin/tools/crc32.ts
Normal file
54
src/bin/tools/crc32.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { Readable } from "stream";
|
||||||
|
|
||||||
|
const crc32tab = [
|
||||||
|
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
|
||||||
|
0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
|
||||||
|
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
|
||||||
|
0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
|
||||||
|
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
|
||||||
|
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
|
||||||
|
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
|
||||||
|
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
|
||||||
|
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
|
||||||
|
0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
|
||||||
|
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
|
||||||
|
0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
|
||||||
|
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
|
||||||
|
0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
|
||||||
|
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
|
||||||
|
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
|
||||||
|
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
|
||||||
|
0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
|
||||||
|
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
|
||||||
|
0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
|
||||||
|
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
|
||||||
|
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param input either a byte stream, a string or a buffer, you want to have the checksum for
|
||||||
|
* @returns a promise for a checksum (uint32)
|
||||||
|
*/
|
||||||
|
export function crc32(input: Readable | String | Buffer): Promise<number> {
|
||||||
|
if (typeof input === "string") {
|
||||||
|
let crc = ~0;
|
||||||
|
for (let i = 0; i < input.length; i++) crc = (crc >>> 8) ^ crc32tab[(crc ^ input.charCodeAt(i)) & 0xff];
|
||||||
|
return Promise.resolve((crc ^ -1) >>> 0);
|
||||||
|
} else if (input instanceof Buffer) {
|
||||||
|
let crc = ~0;
|
||||||
|
for (let i = 0; i < input.length; i++) crc = (crc >>> 8) ^ crc32tab[(crc ^ input[i]) & 0xff];
|
||||||
|
return Promise.resolve((crc ^ -1) >>> 0);
|
||||||
|
} else if (input instanceof Readable) {
|
||||||
|
return new Promise<number>((resolve, reject) => {
|
||||||
|
let crc = ~0;
|
||||||
|
input.on("end", () => resolve((crc ^ -1) >>> 0));
|
||||||
|
input.on("error", e => reject(e));
|
||||||
|
input.on("data", (chunk: Buffer) => {
|
||||||
|
for (let i = 0; i < chunk.length; i++) crc = (crc >>> 8) ^ crc32tab[(crc ^ chunk[i]) & 0xff];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("Unsupported input " + typeof input);
|
||||||
|
}
|
||||||
|
}
|
61
src/bin/tools/deflate.ts
Normal file
61
src/bin/tools/deflate.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { PassThrough, Readable, TransformCallback, Writable } from "stream";
|
||||||
|
import { pipeline } from "stream/promises";
|
||||||
|
import { deflateRaw as deflateRawCb, createDeflateRaw } from "zlib";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
|
import { crc32 } from "./crc32";
|
||||||
|
import tee from "./tee";
|
||||||
|
|
||||||
|
const deflateRaw = promisify(deflateRawCb);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A stream transformer that records the number of bytes
|
||||||
|
* passed in its `size` property.
|
||||||
|
*/
|
||||||
|
class ByteCounter extends PassThrough {
|
||||||
|
size: number = 0;
|
||||||
|
_transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback) {
|
||||||
|
if ("length" in chunk) this.size += chunk.length;
|
||||||
|
super._transform(chunk, encoding, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param data buffer containing the data to be compressed
|
||||||
|
* @returns a buffer containing the compressed/deflated data and the crc32 checksum
|
||||||
|
* of the source data
|
||||||
|
*/
|
||||||
|
export async function deflateBuffer(data: Buffer) {
|
||||||
|
const [deflated, checksum] = await Promise.all([deflateRaw(data), crc32(data)]);
|
||||||
|
return { deflated, crc32: checksum };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param input a byte stream, containing data to be compressed
|
||||||
|
* @param sink a method that will accept chunks of compressed data; We don't pass
|
||||||
|
* a writable here, since we don't want the writablestream to be closed after
|
||||||
|
* a single file
|
||||||
|
* @returns a promise, which will resolve with the crc32 checksum and the
|
||||||
|
* compressed size
|
||||||
|
*/
|
||||||
|
export async function deflateStream(input: Readable, sink: (chunk: Buffer) => void) {
|
||||||
|
const deflateWriter = new Writable({
|
||||||
|
write(chunk, _, callback) {
|
||||||
|
sink(chunk);
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// tee the input stream, so we can compress and calc crc32 in parallel
|
||||||
|
const [rs1, rs2] = tee(input);
|
||||||
|
const byteCounter = new ByteCounter();
|
||||||
|
const [_, crc] = await Promise.all([
|
||||||
|
// pipe input into zip compressor, count the bytes
|
||||||
|
// returned and pass compressed data to the sink
|
||||||
|
pipeline(rs1, createDeflateRaw(), byteCounter, deflateWriter),
|
||||||
|
// calc checksum
|
||||||
|
crc32(rs2)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { crc32: crc, compressedSize: byteCounter.size };
|
||||||
|
}
|
@ -243,7 +243,7 @@ async function* iterateZipArchive(zipFile: string): ZipRecordGenerator {
|
|||||||
const filenameLength = chunk.readUint16LE(i + 28);
|
const filenameLength = chunk.readUint16LE(i + 28);
|
||||||
const extraLength = chunk.readUint16LE(i + 30);
|
const extraLength = chunk.readUint16LE(i + 30);
|
||||||
const commentLength = chunk.readUint16LE(i + 32);
|
const commentLength = chunk.readUint16LE(i + 32);
|
||||||
// Start of thea actual content byte stream is after the 'local' record header,
|
// Start of the actual content byte stream is after the 'local' record header,
|
||||||
// which is 30 bytes long plus filename and extra field
|
// which is 30 bytes long plus filename and extra field
|
||||||
const start = chunk.readUint32LE(i + 42) + 30 + filenameLength + extraLength;
|
const start = chunk.readUint32LE(i + 42) + 30 + filenameLength + extraLength;
|
||||||
const end = start + compressedFileSize;
|
const end = start + compressedFileSize;
|
||||||
|
@ -3,10 +3,10 @@ import { join as pathJoin } from "path";
|
|||||||
import { constants } from "fs";
|
import { constants } from "fs";
|
||||||
import { chmod, stat } from "fs/promises";
|
import { chmod, stat } from "fs/promises";
|
||||||
|
|
||||||
async () => {
|
(async () => {
|
||||||
var { bin } = await import(pathJoin(getProjectRoot(), "package.json"));
|
const { bin } = await import(pathJoin(getProjectRoot(), "package.json"));
|
||||||
|
|
||||||
var promises = Object.values<string>(bin).map(async scriptPath => {
|
const promises = Object.values<string>(bin).map(async scriptPath => {
|
||||||
const fullPath = pathJoin(getProjectRoot(), scriptPath);
|
const fullPath = pathJoin(getProjectRoot(), scriptPath);
|
||||||
const oldMode = (await stat(fullPath)).mode;
|
const oldMode = (await stat(fullPath)).mode;
|
||||||
const newMode = oldMode | constants.S_IXUSR | constants.S_IXGRP | constants.S_IXOTH;
|
const newMode = oldMode | constants.S_IXUSR | constants.S_IXGRP | constants.S_IXOTH;
|
||||||
@ -14,4 +14,4 @@ async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
};
|
})();
|
||||||
|
102
src/bin/tools/jar.ts
Normal file
102
src/bin/tools/jar.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { Readable, Transform } from "stream";
|
||||||
|
import { dirname, relative, sep } from "path";
|
||||||
|
import { createWriteStream } from "fs";
|
||||||
|
|
||||||
|
import walk from "./walk";
|
||||||
|
import type { ZipSource } from "./zip";
|
||||||
|
import zip from "./zip";
|
||||||
|
import { mkdir } from "fs/promises";
|
||||||
|
|
||||||
|
/** Trim leading whitespace from every line */
|
||||||
|
const trimIndent = (s: string) => s.replace(/(\n)\s+/g, "$1");
|
||||||
|
|
||||||
|
type JarArgs = {
|
||||||
|
rootPath: string;
|
||||||
|
targetPath: string;
|
||||||
|
groupId: string;
|
||||||
|
artifactId: string;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a jar archive, using the resources found at `rootPath` (a directory) and write the
|
||||||
|
* archive to `targetPath` (a file). Use `groupId`, `artifactId` and `version` to define
|
||||||
|
* the contents of the pom.properties file which is going to be added to the archive.
|
||||||
|
*/
|
||||||
|
export default async function jar({ groupId, artifactId, version, rootPath, targetPath }: JarArgs) {
|
||||||
|
const manifest: ZipSource = {
|
||||||
|
path: "META-INF/MANIFEST.MF",
|
||||||
|
data: Buffer.from(
|
||||||
|
trimIndent(
|
||||||
|
`Manifest-Version: 1.0
|
||||||
|
Archiver-Version: Plexus Archiver
|
||||||
|
Created-By: Keycloakify
|
||||||
|
Built-By: unknown
|
||||||
|
Build-Jdk: 19.0.0`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
const pomProps: ZipSource = {
|
||||||
|
path: `META-INF/maven/${groupId}/${artifactId}/pom.properties`,
|
||||||
|
data: Buffer.from(
|
||||||
|
trimIndent(
|
||||||
|
`# Generated by keycloakify
|
||||||
|
# ${new Date()}
|
||||||
|
artifactId=${artifactId}
|
||||||
|
groupId=${groupId}
|
||||||
|
version=${version}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert every path entry to a ZipSource record, and when all records are
|
||||||
|
* processed, append records for MANIFEST.mf and pom.properties
|
||||||
|
*/
|
||||||
|
const pathToRecord = () =>
|
||||||
|
new Transform({
|
||||||
|
objectMode: true,
|
||||||
|
transform: function (fsPath, _, cb) {
|
||||||
|
const path = relative(rootPath, fsPath).split(sep).join("/");
|
||||||
|
this.push({ path, fsPath });
|
||||||
|
cb();
|
||||||
|
},
|
||||||
|
final: function () {
|
||||||
|
this.push(manifest);
|
||||||
|
this.push(pomProps);
|
||||||
|
this.push(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await mkdir(dirname(targetPath), { recursive: true });
|
||||||
|
|
||||||
|
// Create an async pipeline, wait until everything is fully processed
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
// walk all files in `rootPath` recursively
|
||||||
|
Readable.from(walk(rootPath))
|
||||||
|
// transform every path into a ZipSource object
|
||||||
|
.pipe(pathToRecord())
|
||||||
|
// let the zip lib convert all ZipSource objects into a byte stream
|
||||||
|
.pipe(zip())
|
||||||
|
// write that byte stream to targetPath
|
||||||
|
.pipe(createWriteStream(targetPath, { encoding: "binary" }))
|
||||||
|
.on("finish", () => resolve())
|
||||||
|
.on("error", e => reject(e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standalone usage, call e.g. `ts-node jar.ts dirWithSources some-jar.jar`
|
||||||
|
*/
|
||||||
|
if (require.main === module) {
|
||||||
|
const main = () =>
|
||||||
|
jar({
|
||||||
|
rootPath: process.argv[2],
|
||||||
|
targetPath: process.argv[3],
|
||||||
|
artifactId: process.env.ARTIFACT_ID ?? "artifact",
|
||||||
|
groupId: process.env.GROUP_ID ?? "group",
|
||||||
|
version: process.env.VERSION ?? "1.0.0"
|
||||||
|
});
|
||||||
|
main().catch(e => console.error(e));
|
||||||
|
}
|
37
src/bin/tools/tee.ts
Normal file
37
src/bin/tools/tee.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { PassThrough, Readable } from "stream";
|
||||||
|
|
||||||
|
export default function tee(input: Readable) {
|
||||||
|
const a = new PassThrough();
|
||||||
|
const b = new PassThrough();
|
||||||
|
|
||||||
|
let aFull = false;
|
||||||
|
let bFull = false;
|
||||||
|
|
||||||
|
a.on("drain", () => {
|
||||||
|
aFull = false;
|
||||||
|
if (!aFull && !bFull) input.resume();
|
||||||
|
});
|
||||||
|
b.on("drain", () => {
|
||||||
|
bFull = false;
|
||||||
|
if (!aFull && !bFull) input.resume();
|
||||||
|
});
|
||||||
|
|
||||||
|
input.on("error", e => {
|
||||||
|
a.emit("error", e);
|
||||||
|
b.emit("error", e);
|
||||||
|
});
|
||||||
|
|
||||||
|
input.on("data", chunk => {
|
||||||
|
aFull = !a.write(chunk);
|
||||||
|
bFull = !b.write(chunk);
|
||||||
|
|
||||||
|
if (aFull || bFull) input.pause();
|
||||||
|
});
|
||||||
|
|
||||||
|
input.on("end", () => {
|
||||||
|
a.end();
|
||||||
|
b.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
return [a, b] as const;
|
||||||
|
}
|
19
src/bin/tools/walk.ts
Normal file
19
src/bin/tools/walk.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { readdir } from "fs/promises";
|
||||||
|
import { resolve } from "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously and recursively walk a directory tree, yielding every file and directory
|
||||||
|
* found
|
||||||
|
*
|
||||||
|
* @param root the starting directory
|
||||||
|
* @returns AsyncGenerator
|
||||||
|
*/
|
||||||
|
export default async function* walk(root: string): AsyncGenerator<string, void, void> {
|
||||||
|
for (const entry of await readdir(root, { withFileTypes: true })) {
|
||||||
|
const absolutePath = resolve(root, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
yield absolutePath;
|
||||||
|
yield* walk(absolutePath);
|
||||||
|
} else yield absolutePath;
|
||||||
|
}
|
||||||
|
}
|
246
src/bin/tools/zip.ts
Normal file
246
src/bin/tools/zip.ts
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import { Transform, TransformOptions } from "stream";
|
||||||
|
import { createReadStream } from "fs";
|
||||||
|
import { stat } from "fs/promises";
|
||||||
|
import { Blob } from "buffer";
|
||||||
|
|
||||||
|
import { deflateBuffer, deflateStream } from "./deflate";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zip source
|
||||||
|
* @property filename the name of the entry in the archie
|
||||||
|
* @property path of the source file, if the source is an actual file
|
||||||
|
* @property data the actual data buffer, if the source is constructed in-memory
|
||||||
|
*/
|
||||||
|
export type ZipSource = { path: string } & ({ fsPath: string } | { data: Buffer });
|
||||||
|
|
||||||
|
export type ZipRecord = {
|
||||||
|
path: string;
|
||||||
|
compression: "deflate" | undefined;
|
||||||
|
uncompressedSize: number;
|
||||||
|
compressedSize?: number;
|
||||||
|
crc32?: number;
|
||||||
|
offset?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns the actual byte size of an string
|
||||||
|
*/
|
||||||
|
function utf8size(s: string) {
|
||||||
|
return new Blob([s]).size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param record
|
||||||
|
* @returns a buffer representing a Zip local header
|
||||||
|
* @link https://en.wikipedia.org/wiki/ZIP_(file_format)#Local_file_header
|
||||||
|
*/
|
||||||
|
function localHeader(record: ZipRecord) {
|
||||||
|
const { path, compression, uncompressedSize } = record;
|
||||||
|
const filenameSize = utf8size(path);
|
||||||
|
const buf = Buffer.alloc(30 + filenameSize);
|
||||||
|
|
||||||
|
buf.writeUInt32LE(0x04_03_4b_50, 0); // local header signature
|
||||||
|
buf.writeUInt16LE(10, 4); // min version
|
||||||
|
// we write 0x08 because crc and compressed size are unknown at
|
||||||
|
buf.writeUInt16LE(0x08, 6); // general purpose bit flag
|
||||||
|
buf.writeUInt16LE(compression ? ({ "deflate": 8 } as const)[compression] : 0, 8);
|
||||||
|
buf.writeUInt16LE(0, 10); // modified time
|
||||||
|
buf.writeUInt16LE(0, 12); // modified date
|
||||||
|
buf.writeUInt32LE(0, 14); // crc unknown
|
||||||
|
buf.writeUInt32LE(0, 18); // compressed size unknown
|
||||||
|
buf.writeUInt32LE(uncompressedSize, 22);
|
||||||
|
buf.writeUInt16LE(filenameSize, 26);
|
||||||
|
buf.writeUInt16LE(0, 28); // extra field length
|
||||||
|
buf.write(path, 30, "utf-8");
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param record
|
||||||
|
* @returns a buffer representing a Zip central header
|
||||||
|
* @link https://en.wikipedia.org/wiki/ZIP_(file_format)#Central_directory_file_header
|
||||||
|
*/
|
||||||
|
function centralHeader(record: ZipRecord) {
|
||||||
|
const { path, compression, crc32, compressedSize, uncompressedSize, offset } = record;
|
||||||
|
const filenameSize = utf8size(path);
|
||||||
|
const buf = Buffer.alloc(46 + filenameSize);
|
||||||
|
const isFile = !path.endsWith("/");
|
||||||
|
|
||||||
|
if (typeof offset === "undefined") throw new Error("Illegal argument");
|
||||||
|
|
||||||
|
// we don't want to deal with possibly messed up file or directory
|
||||||
|
// permissions, so we ignore the original permissions
|
||||||
|
const externalAttr = isFile ? 0x81a40000 : 0x41ed0000;
|
||||||
|
|
||||||
|
buf.writeUInt32LE(0x0201_4b50, 0); // central header signature
|
||||||
|
buf.writeUInt16LE(10, 4); // version
|
||||||
|
buf.writeUInt16LE(10, 6); // min version
|
||||||
|
buf.writeUInt16LE(0, 8); // general purpose bit flag
|
||||||
|
buf.writeUInt16LE(compression ? ({ "deflate": 8 } as const)[compression] : 0, 10);
|
||||||
|
buf.writeUInt16LE(0, 12); // modified time
|
||||||
|
buf.writeUInt16LE(0, 14); // modified date
|
||||||
|
buf.writeUInt32LE(crc32 || 0, 16);
|
||||||
|
buf.writeUInt32LE(compressedSize || 0, 20);
|
||||||
|
buf.writeUInt32LE(uncompressedSize, 24);
|
||||||
|
buf.writeUInt16LE(filenameSize, 28);
|
||||||
|
buf.writeUInt16LE(0, 30); // extra field length
|
||||||
|
buf.writeUInt16LE(0, 32); // comment field length
|
||||||
|
buf.writeUInt16LE(0, 34); // disk number
|
||||||
|
buf.writeUInt16LE(0, 36); // internal
|
||||||
|
buf.writeUInt32LE(externalAttr, 38); // external
|
||||||
|
buf.writeUInt32LE(offset, 42); // offset where file starts
|
||||||
|
buf.write(path, 46, "utf-8");
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns a buffer representing an Zip End-Of-Central-Directory block
|
||||||
|
* @link https://en.wikipedia.org/wiki/ZIP_(file_format)#End_of_central_directory_record_(EOCD)
|
||||||
|
*/
|
||||||
|
function eocd({ offset, cdSize, nRecords }: { offset: number; cdSize: number; nRecords: number }) {
|
||||||
|
const buf = Buffer.alloc(22);
|
||||||
|
buf.writeUint32LE(0x06054b50, 0); // eocd signature
|
||||||
|
buf.writeUInt16LE(0, 4); // disc number
|
||||||
|
buf.writeUint16LE(0, 6); // disc where central directory starts
|
||||||
|
buf.writeUint16LE(nRecords, 8); // records on this disc
|
||||||
|
buf.writeUInt16LE(nRecords, 10); // records total
|
||||||
|
buf.writeUInt32LE(cdSize, 12); // byte size of cd
|
||||||
|
buf.writeUInt32LE(offset, 16); // cd offset
|
||||||
|
buf.writeUint16LE(0, 20); // comment length
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns a stream Transform, which reads a stream of ZipRecords and
|
||||||
|
* writes a bytestream
|
||||||
|
*/
|
||||||
|
export default function zip() {
|
||||||
|
/**
|
||||||
|
* This is called when the input stream of ZipSource items is finished.
|
||||||
|
* Will write central directory and end-of-central-direcotry blocks.
|
||||||
|
*/
|
||||||
|
const final = () => {
|
||||||
|
// write central directory
|
||||||
|
let cdSize = 0;
|
||||||
|
for (const record of records) {
|
||||||
|
const head = centralHeader(record);
|
||||||
|
zipTransform.push(head);
|
||||||
|
cdSize += head.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// write end-of-central-directory
|
||||||
|
zipTransform.push(eocd({ offset, cdSize, nRecords: records.length }));
|
||||||
|
// signal stream end
|
||||||
|
zipTransform.push(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a directory entry to the archive
|
||||||
|
* @param path
|
||||||
|
*/
|
||||||
|
const writeDir = async (path: string) => {
|
||||||
|
const record: ZipRecord = {
|
||||||
|
path: path + "/",
|
||||||
|
offset,
|
||||||
|
compression: undefined,
|
||||||
|
uncompressedSize: 0
|
||||||
|
};
|
||||||
|
const head = localHeader(record);
|
||||||
|
zipTransform.push(head);
|
||||||
|
records.push(record);
|
||||||
|
offset += head.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a file entry to the archive
|
||||||
|
* @param archivePath path of the file in archive
|
||||||
|
* @param fsPath path to file on filesystem
|
||||||
|
* @param size of the actual, uncompressed, file
|
||||||
|
*/
|
||||||
|
const writeFile = async (archivePath: string, fsPath: string, size: number) => {
|
||||||
|
const record: ZipRecord = {
|
||||||
|
path: archivePath,
|
||||||
|
offset,
|
||||||
|
compression: "deflate",
|
||||||
|
uncompressedSize: size
|
||||||
|
};
|
||||||
|
const head = localHeader(record);
|
||||||
|
zipTransform.push(head);
|
||||||
|
|
||||||
|
const { crc32, compressedSize } = await deflateStream(createReadStream(fsPath), chunk => zipTransform.push(chunk));
|
||||||
|
|
||||||
|
record.crc32 = crc32;
|
||||||
|
record.compressedSize = compressedSize;
|
||||||
|
records.push(record);
|
||||||
|
offset += head.length + compressedSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write archive record based on filesystem file or directory
|
||||||
|
* @param archivePath path of item in archive
|
||||||
|
* @param fsPath path to item on filesystem
|
||||||
|
*/
|
||||||
|
const writeFromPath = async (archivePath: string, fsPath: string) => {
|
||||||
|
const fileStats = await stat(fsPath);
|
||||||
|
fileStats.isDirectory() ? await writeDir(archivePath) /**/ : await writeFile(archivePath, fsPath, fileStats.size) /**/;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write archive record based on data in a buffer
|
||||||
|
* @param path
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
const writeFromBuffer = async (path: string, data: Buffer) => {
|
||||||
|
const { deflated, crc32 } = await deflateBuffer(data);
|
||||||
|
const record: ZipRecord = {
|
||||||
|
path,
|
||||||
|
compression: "deflate",
|
||||||
|
crc32,
|
||||||
|
uncompressedSize: data.length,
|
||||||
|
compressedSize: deflated.length,
|
||||||
|
offset
|
||||||
|
};
|
||||||
|
const head = localHeader(record);
|
||||||
|
zipTransform.push(head);
|
||||||
|
zipTransform.push(deflated);
|
||||||
|
records.push(record);
|
||||||
|
offset += head.length + deflated.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write an archive record
|
||||||
|
* @param source
|
||||||
|
*/
|
||||||
|
const writeRecord = async (source: ZipSource) => {
|
||||||
|
if ("fsPath" in source) await writeFromPath(source.path, source.fsPath);
|
||||||
|
else if ("data" in source) await writeFromBuffer(source.path, source.data);
|
||||||
|
else throw new Error("Illegal argument " + typeof source + " " + JSON.stringify(source));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The actual stream transform function
|
||||||
|
* @param source
|
||||||
|
* @param _ encoding, ignored
|
||||||
|
* @param cb
|
||||||
|
*/
|
||||||
|
const transform: TransformOptions["transform"] = async (source: ZipSource, _, cb) => {
|
||||||
|
await writeRecord(source);
|
||||||
|
cb();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** offset and records keep local state during processing */
|
||||||
|
let offset = 0;
|
||||||
|
const records: ZipRecord[] = [];
|
||||||
|
|
||||||
|
const zipTransform = new Transform({
|
||||||
|
readableObjectMode: false,
|
||||||
|
writableObjectMode: true,
|
||||||
|
transform,
|
||||||
|
final
|
||||||
|
});
|
||||||
|
|
||||||
|
return zipTransform;
|
||||||
|
}
|
@ -1,41 +1,34 @@
|
|||||||
import React, { lazy, memo, Suspense } from "react";
|
import React, { lazy, Suspense } from "react";
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
import { __unsafe_useI18n as useI18n } from "./i18n";
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import { __unsafe_useI18n as useI18n } from "../i18n";
|
|
||||||
import type { I18n } from "../i18n";
|
|
||||||
import DefaultTemplate from "./Template";
|
import DefaultTemplate from "./Template";
|
||||||
import type { TemplateProps } from "./Template";
|
import type { KcContextBase } from "./getKcContext/KcContextBase";
|
||||||
|
import type { PageProps } from "./KcProps";
|
||||||
|
import type { I18nBase } from "./i18n";
|
||||||
|
import type { SetOptional } from "./tools/SetOptional";
|
||||||
|
|
||||||
const Login = lazy(() => import("./Login"));
|
const Login = lazy(() => import("./pages/Login"));
|
||||||
const Register = lazy(() => import("./Register"));
|
const Register = lazy(() => import("./pages/Register"));
|
||||||
const RegisterUserProfile = lazy(() => import("./RegisterUserProfile"));
|
const RegisterUserProfile = lazy(() => import("./pages/RegisterUserProfile"));
|
||||||
const Info = lazy(() => import("./Info"));
|
const Info = lazy(() => import("./pages/Info"));
|
||||||
const Error = lazy(() => import("./Error"));
|
const Error = lazy(() => import("./pages/Error"));
|
||||||
const LoginResetPassword = lazy(() => import("./LoginResetPassword"));
|
const LoginResetPassword = lazy(() => import("./pages/LoginResetPassword"));
|
||||||
const LoginVerifyEmail = lazy(() => import("./LoginVerifyEmail"));
|
const LoginVerifyEmail = lazy(() => import("./pages/LoginVerifyEmail"));
|
||||||
const Terms = lazy(() => import("./Terms"));
|
const Terms = lazy(() => import("./pages/Terms"));
|
||||||
const LoginOtp = lazy(() => import("./LoginOtp"));
|
const LoginOtp = lazy(() => import("./pages/LoginOtp"));
|
||||||
const LoginPassword = lazy(() => import("./LoginPassword"));
|
const LoginPassword = lazy(() => import("./pages/LoginPassword"));
|
||||||
const LoginUsername = lazy(() => import("./LoginUsername"));
|
const LoginUsername = lazy(() => import("./pages/LoginUsername"));
|
||||||
const WebauthnAuthenticate = lazy(() => import("./WebauthnAuthenticate"));
|
const WebauthnAuthenticate = lazy(() => import("./pages/WebauthnAuthenticate"));
|
||||||
const LoginUpdatePassword = lazy(() => import("./LoginUpdatePassword"));
|
const LoginUpdatePassword = lazy(() => import("./pages/LoginUpdatePassword"));
|
||||||
const LoginUpdateProfile = lazy(() => import("./LoginUpdateProfile"));
|
const LoginUpdateProfile = lazy(() => import("./pages/LoginUpdateProfile"));
|
||||||
const LoginIdpLinkConfirm = lazy(() => import("./LoginIdpLinkConfirm"));
|
const LoginIdpLinkConfirm = lazy(() => import("./pages/LoginIdpLinkConfirm"));
|
||||||
const LoginPageExpired = lazy(() => import("./LoginPageExpired"));
|
const LoginPageExpired = lazy(() => import("./pages/LoginPageExpired"));
|
||||||
const LoginIdpLinkEmail = lazy(() => import("./LoginIdpLinkEmail"));
|
const LoginIdpLinkEmail = lazy(() => import("./pages/LoginIdpLinkEmail"));
|
||||||
const LoginConfigTotp = lazy(() => import("./LoginConfigTotp"));
|
const LoginConfigTotp = lazy(() => import("./pages/LoginConfigTotp"));
|
||||||
const LogoutConfirm = lazy(() => import("./LogoutConfirm"));
|
const LogoutConfirm = lazy(() => import("./pages/LogoutConfirm"));
|
||||||
const UpdateUserProfile = lazy(() => import("./UpdateUserProfile"));
|
const UpdateUserProfile = lazy(() => import("./pages/UpdateUserProfile"));
|
||||||
const IdpReviewUserProfile = lazy(() => import("./IdpReviewUserProfile"));
|
const IdpReviewUserProfile = lazy(() => import("./pages/IdpReviewUserProfile"));
|
||||||
|
|
||||||
export type KcAppProps = KcProps & {
|
export default function KcApp(props_: SetOptional<PageProps<KcContextBase, I18nBase>, "Template">) {
|
||||||
kcContext: KcContextBase;
|
|
||||||
i18n?: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const KcApp = memo((props_: KcAppProps) => {
|
|
||||||
const { kcContext, i18n: userProvidedI18n, Template = DefaultTemplate, ...kcProps } = props_;
|
const { kcContext, i18n: userProvidedI18n, Template = DefaultTemplate, ...kcProps } = props_;
|
||||||
|
|
||||||
const i18n = (function useClosure() {
|
const i18n = (function useClosure() {
|
||||||
@ -104,6 +97,4 @@ const KcApp = memo((props_: KcAppProps) => {
|
|||||||
})()}
|
})()}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default KcApp;
|
|
@ -1,5 +1,8 @@
|
|||||||
import { allPropertiesValuesToUndefined } from "../tools/allPropertiesValuesToUndefined";
|
import { allPropertiesValuesToUndefined } from "./tools/allPropertiesValuesToUndefined";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { KcContextBase } from "./getKcContext";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { I18nBase } from "./i18n";
|
||||||
|
|
||||||
/** Class names can be provided as an array or separated by whitespace */
|
/** Class names can be provided as an array or separated by whitespace */
|
||||||
export type KcPropsGeneric<CssClasses extends string> = {
|
export type KcPropsGeneric<CssClasses extends string> = {
|
||||||
@ -205,6 +208,29 @@ export const defaultKcProps = {
|
|||||||
"kcFormOptionsWrapperClass": []
|
"kcFormOptionsWrapperClass": []
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export type TemplateProps<KcContext extends KcContextBase.Common, I18n extends I18nBase> = {
|
||||||
|
kcContext: KcContext;
|
||||||
|
i18n: I18n;
|
||||||
|
doFetchDefaultThemeResources: boolean;
|
||||||
|
} & {
|
||||||
|
displayInfo?: boolean;
|
||||||
|
displayMessage?: boolean;
|
||||||
|
displayRequiredFields?: boolean;
|
||||||
|
displayWide?: boolean;
|
||||||
|
showAnotherWayIfPresent?: boolean;
|
||||||
|
headerNode: ReactNode;
|
||||||
|
showUsernameNode?: ReactNode;
|
||||||
|
formNode: ReactNode;
|
||||||
|
infoNode?: ReactNode;
|
||||||
|
} & KcTemplateProps;
|
||||||
|
|
||||||
|
export type PageProps<KcContext, I18n extends I18nBase> = {
|
||||||
|
kcContext: KcContext;
|
||||||
|
i18n: I18n;
|
||||||
|
doFetchDefaultThemeResources?: boolean;
|
||||||
|
Template: (props: TemplateProps<any, any>) => JSX.Element | null;
|
||||||
|
} & KcProps;
|
||||||
|
|
||||||
assert<typeof defaultKcProps extends KcProps ? true : false>();
|
assert<typeof defaultKcProps extends KcProps ? true : false>();
|
||||||
|
|
||||||
/** Tu use if you don't want any default */
|
/** Tu use if you don't want any default */
|
@ -1,32 +1,13 @@
|
|||||||
import React, { useReducer, useEffect, memo } from "react";
|
import React, { useReducer, useEffect } from "react";
|
||||||
import type { ReactNode } from "react";
|
import { assert } from "./tools/assert";
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
import { headInsert } from "./tools/headInsert";
|
||||||
import { assert } from "../tools/assert";
|
import { pathJoin } from "../bin/tools/pathJoin";
|
||||||
import { useCallbackFactory } from "powerhooks/useCallbackFactory";
|
import { clsx } from "./tools/clsx";
|
||||||
import { headInsert } from "../tools/headInsert";
|
import type { TemplateProps } from "./KcProps";
|
||||||
import { pathJoin } from "../../bin/tools/pathJoin";
|
import type { KcContextBase } from "./getKcContext/KcContextBase";
|
||||||
import { useConstCallback } from "powerhooks/useConstCallback";
|
import type { I18nBase } from "./i18n";
|
||||||
import type { KcTemplateProps } from "./KcProps";
|
|
||||||
import { clsx } from "../tools/clsx";
|
|
||||||
import type { I18n } from "../i18n";
|
|
||||||
|
|
||||||
export type TemplateProps = {
|
export default function Template(props: TemplateProps<KcContextBase.Common, I18nBase>) {
|
||||||
displayInfo?: boolean;
|
|
||||||
displayMessage?: boolean;
|
|
||||||
displayRequiredFields?: boolean;
|
|
||||||
displayWide?: boolean;
|
|
||||||
showAnotherWayIfPresent?: boolean;
|
|
||||||
headerNode: ReactNode;
|
|
||||||
showUsernameNode?: ReactNode;
|
|
||||||
formNode: ReactNode;
|
|
||||||
infoNode?: ReactNode;
|
|
||||||
/** If you write your own page you probably want
|
|
||||||
* to avoid pulling the default theme assets.
|
|
||||||
*/
|
|
||||||
doFetchDefaultThemeResources: boolean;
|
|
||||||
} & { kcContext: KcContextBase; i18n: I18n } & KcTemplateProps;
|
|
||||||
|
|
||||||
const Template = memo((props: TemplateProps) => {
|
|
||||||
const {
|
const {
|
||||||
displayInfo = false,
|
displayInfo = false,
|
||||||
displayMessage = true,
|
displayMessage = true,
|
||||||
@ -39,76 +20,27 @@ const Template = memo((props: TemplateProps) => {
|
|||||||
infoNode = null,
|
infoNode = null,
|
||||||
kcContext,
|
kcContext,
|
||||||
i18n,
|
i18n,
|
||||||
doFetchDefaultThemeResources
|
doFetchDefaultThemeResources,
|
||||||
|
stylesCommon,
|
||||||
|
styles,
|
||||||
|
scripts,
|
||||||
|
kcHtmlClass
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
|
const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
|
||||||
|
|
||||||
const onChangeLanguageClickFactory = useCallbackFactory(([kcLanguageTag]: [string]) => changeLocale(kcLanguageTag));
|
|
||||||
|
|
||||||
const onTryAnotherWayClick = useConstCallback(() => (document.forms["kc-select-try-another-way-form" as never].submit(), false));
|
|
||||||
|
|
||||||
const { realm, locale, auth, url, message, isAppInitiatedAction } = kcContext;
|
const { realm, locale, auth, url, message, isAppInitiatedAction } = kcContext;
|
||||||
|
|
||||||
const [isExtraCssLoaded, setExtraCssLoaded] = useReducer(() => true, false);
|
const { isReady } = usePrepareTemplate({
|
||||||
|
doFetchDefaultThemeResources,
|
||||||
|
stylesCommon,
|
||||||
|
styles,
|
||||||
|
scripts,
|
||||||
|
url,
|
||||||
|
kcHtmlClass
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
if (!isReady) {
|
||||||
if (!doFetchDefaultThemeResources) {
|
|
||||||
setExtraCssLoaded();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let isUnmounted = false;
|
|
||||||
const cleanups: (() => void)[] = [];
|
|
||||||
|
|
||||||
const toArr = (x: string | readonly string[] | undefined) => (typeof x === "string" ? x.split(" ") : x ?? []);
|
|
||||||
|
|
||||||
Promise.all(
|
|
||||||
[
|
|
||||||
...toArr(props.stylesCommon).map(relativePath => pathJoin(url.resourcesCommonPath, relativePath)),
|
|
||||||
...toArr(props.styles).map(relativePath => pathJoin(url.resourcesPath, relativePath))
|
|
||||||
]
|
|
||||||
.reverse()
|
|
||||||
.map(href =>
|
|
||||||
headInsert({
|
|
||||||
"type": "css",
|
|
||||||
href,
|
|
||||||
"position": "prepend"
|
|
||||||
})
|
|
||||||
)
|
|
||||||
).then(() => {
|
|
||||||
if (isUnmounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setExtraCssLoaded();
|
|
||||||
});
|
|
||||||
|
|
||||||
toArr(props.scripts).forEach(relativePath =>
|
|
||||||
headInsert({
|
|
||||||
"type": "javascript",
|
|
||||||
"src": pathJoin(url.resourcesPath, relativePath)
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (props.kcHtmlClass !== undefined) {
|
|
||||||
const htmlClassList = document.getElementsByTagName("html")[0].classList;
|
|
||||||
|
|
||||||
const tokens = clsx(props.kcHtmlClass).split(" ");
|
|
||||||
|
|
||||||
htmlClassList.add(...tokens);
|
|
||||||
|
|
||||||
cleanups.push(() => htmlClassList.remove(...tokens));
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isUnmounted = true;
|
|
||||||
|
|
||||||
cleanups.forEach(f => f());
|
|
||||||
};
|
|
||||||
}, [props.kcHtmlClass]);
|
|
||||||
|
|
||||||
if (!isExtraCssLoaded) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,13 +58,15 @@ const Template = memo((props: TemplateProps) => {
|
|||||||
<div id="kc-locale">
|
<div id="kc-locale">
|
||||||
<div id="kc-locale-wrapper" className={clsx(props.kcLocaleWrapperClass)}>
|
<div id="kc-locale-wrapper" className={clsx(props.kcLocaleWrapperClass)}>
|
||||||
<div className="kc-dropdown" id="kc-locale-dropdown">
|
<div className="kc-dropdown" id="kc-locale-dropdown">
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||||
<a href="#" id="kc-current-locale-link">
|
<a href="#" id="kc-current-locale-link">
|
||||||
{labelBySupportedLanguageTag[currentLanguageTag]}
|
{labelBySupportedLanguageTag[currentLanguageTag]}
|
||||||
</a>
|
</a>
|
||||||
<ul>
|
<ul>
|
||||||
{locale.supported.map(({ languageTag }) => (
|
{locale.supported.map(({ languageTag }) => (
|
||||||
<li key={languageTag} className="kc-dropdown-item">
|
<li key={languageTag} className="kc-dropdown-item">
|
||||||
<a href="#" onClick={onChangeLanguageClickFactory(languageTag)}>
|
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||||
|
<a href="#" onClick={() => changeLocale(languageTag)}>
|
||||||
{labelBySupportedLanguageTag[languageTag]}
|
{labelBySupportedLanguageTag[languageTag]}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -225,7 +159,15 @@ const Template = memo((props: TemplateProps) => {
|
|||||||
<div className={clsx(displayWide && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass])}>
|
<div className={clsx(displayWide && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass])}>
|
||||||
<div className={clsx(props.kcFormGroupClass)}>
|
<div className={clsx(props.kcFormGroupClass)}>
|
||||||
<input type="hidden" name="tryAnotherWay" value="on" />
|
<input type="hidden" name="tryAnotherWay" value="on" />
|
||||||
<a href="#" id="try-another-way" onClick={onTryAnotherWayClick}>
|
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
id="try-another-way"
|
||||||
|
onClick={() => {
|
||||||
|
document.forms["kc-select-try-another-way-form" as never].submit();
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
{msg("doTryAnotherWay")}
|
{msg("doTryAnotherWay")}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -244,6 +186,80 @@ const Template = memo((props: TemplateProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default Template;
|
export function usePrepareTemplate(params: {
|
||||||
|
doFetchDefaultThemeResources: boolean;
|
||||||
|
stylesCommon: string | readonly string[] | undefined;
|
||||||
|
styles: string | readonly string[] | undefined;
|
||||||
|
scripts: string | readonly string[] | undefined;
|
||||||
|
url: {
|
||||||
|
resourcesCommonPath: string;
|
||||||
|
resourcesPath: string;
|
||||||
|
};
|
||||||
|
kcHtmlClass: string | readonly string[] | undefined;
|
||||||
|
}) {
|
||||||
|
const { doFetchDefaultThemeResources, stylesCommon, styles, url, scripts, kcHtmlClass } = params;
|
||||||
|
|
||||||
|
const [isReady, setReady] = useReducer(() => true, !doFetchDefaultThemeResources);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!doFetchDefaultThemeResources) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isUnmounted = false;
|
||||||
|
|
||||||
|
const toArr = (x: string | readonly string[] | undefined) => (typeof x === "string" ? x.split(" ") : x ?? []);
|
||||||
|
|
||||||
|
Promise.all(
|
||||||
|
[
|
||||||
|
...toArr(stylesCommon).map(relativePath => pathJoin(url.resourcesCommonPath, relativePath)),
|
||||||
|
...toArr(styles).map(relativePath => pathJoin(url.resourcesPath, relativePath))
|
||||||
|
]
|
||||||
|
.reverse()
|
||||||
|
.map(href =>
|
||||||
|
headInsert({
|
||||||
|
"type": "css",
|
||||||
|
href,
|
||||||
|
"position": "prepend"
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).then(() => {
|
||||||
|
if (isUnmounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setReady();
|
||||||
|
});
|
||||||
|
|
||||||
|
toArr(scripts).forEach(relativePath =>
|
||||||
|
headInsert({
|
||||||
|
"type": "javascript",
|
||||||
|
"src": pathJoin(url.resourcesPath, relativePath)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isUnmounted = true;
|
||||||
|
};
|
||||||
|
}, [kcHtmlClass]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (kcHtmlClass === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlClassList = document.getElementsByTagName("html")[0].classList;
|
||||||
|
|
||||||
|
const tokens = clsx(kcHtmlClass).split(" ");
|
||||||
|
|
||||||
|
htmlClassList.add(...tokens);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
htmlClassList.remove(...tokens);
|
||||||
|
};
|
||||||
|
}, [kcHtmlClass]);
|
||||||
|
|
||||||
|
return { isReady };
|
||||||
|
}
|
@ -1,173 +0,0 @@
|
|||||||
import React, { memo, useEffect, Fragment } from "react";
|
|
||||||
import type { KcProps } from "../KcProps";
|
|
||||||
import type { Attribute } from "../../getKcContext/KcContextBase";
|
|
||||||
import { clsx } from "../../tools/clsx";
|
|
||||||
import type { ReactComponent } from "../../tools/ReactComponent";
|
|
||||||
import { useCallbackFactory } from "powerhooks/useCallbackFactory";
|
|
||||||
import { useFormValidationSlice } from "../../useFormValidationSlice";
|
|
||||||
import type { I18n } from "../../i18n";
|
|
||||||
import type { Param0 } from "tsafe/Param0";
|
|
||||||
|
|
||||||
export type UserProfileFormFieldsProps = {
|
|
||||||
kcContext: Param0<typeof useFormValidationSlice>["kcContext"];
|
|
||||||
i18n: I18n;
|
|
||||||
} & KcProps &
|
|
||||||
Partial<Record<"BeforeField" | "AfterField", ReactComponent<{ attribute: Attribute }>>> & {
|
|
||||||
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UserProfileFormFields = memo(
|
|
||||||
({ kcContext, onIsFormSubmittableValueChange, i18n, BeforeField, AfterField, ...props }: UserProfileFormFieldsProps) => {
|
|
||||||
const { advancedMsg } = i18n;
|
|
||||||
|
|
||||||
const {
|
|
||||||
formValidationState: { fieldStateByAttributeName, isFormSubmittable },
|
|
||||||
formValidationReducer,
|
|
||||||
attributesWithPassword
|
|
||||||
} = useFormValidationSlice({
|
|
||||||
kcContext,
|
|
||||||
i18n
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onIsFormSubmittableValueChange(isFormSubmittable);
|
|
||||||
}, [isFormSubmittable]);
|
|
||||||
|
|
||||||
const onChangeFactory = useCallbackFactory(
|
|
||||||
(
|
|
||||||
[name]: [string],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
target: { value }
|
|
||||||
}
|
|
||||||
]: [React.ChangeEvent<HTMLInputElement | HTMLSelectElement>]
|
|
||||||
) =>
|
|
||||||
formValidationReducer({
|
|
||||||
"action": "update value",
|
|
||||||
name,
|
|
||||||
"newValue": value
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const onBlurFactory = useCallbackFactory(([name]: [string]) =>
|
|
||||||
formValidationReducer({
|
|
||||||
"action": "focus lost",
|
|
||||||
name
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
let currentGroup = "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{attributesWithPassword.map((attribute, i) => {
|
|
||||||
const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute;
|
|
||||||
|
|
||||||
const { value, displayableErrors } = fieldStateByAttributeName[attribute.name];
|
|
||||||
|
|
||||||
const formGroupClassName = clsx(props.kcFormGroupClass, displayableErrors.length !== 0 && props.kcFormGroupErrorClass);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment key={i}>
|
|
||||||
{group !== currentGroup && (currentGroup = group) !== "" && (
|
|
||||||
<div className={formGroupClassName}>
|
|
||||||
<div className={clsx(props.kcContentWrapperClass)}>
|
|
||||||
<label id={`header-${group}`} className={clsx(props.kcFormGroupHeader)}>
|
|
||||||
{advancedMsg(groupDisplayHeader) || currentGroup}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{groupDisplayDescription !== "" && (
|
|
||||||
<div className={clsx(props.kcLabelWrapperClass)}>
|
|
||||||
<label id={`description-${group}`} className={`${clsx(props.kcLabelClass)}`}>
|
|
||||||
{advancedMsg(groupDisplayDescription)}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{BeforeField && <BeforeField attribute={attribute} />}
|
|
||||||
|
|
||||||
<div className={formGroupClassName}>
|
|
||||||
<div className={clsx(props.kcLabelWrapperClass)}>
|
|
||||||
<label htmlFor={attribute.name} className={clsx(props.kcLabelClass)}>
|
|
||||||
{advancedMsg(attribute.displayName ?? "")}
|
|
||||||
</label>
|
|
||||||
{attribute.required && <>*</>}
|
|
||||||
</div>
|
|
||||||
<div className={clsx(props.kcInputWrapperClass)}>
|
|
||||||
{(() => {
|
|
||||||
const { options } = attribute.validators;
|
|
||||||
|
|
||||||
if (options !== undefined) {
|
|
||||||
return (
|
|
||||||
<select
|
|
||||||
id={attribute.name}
|
|
||||||
name={attribute.name}
|
|
||||||
onChange={onChangeFactory(attribute.name)}
|
|
||||||
onBlur={onBlurFactory(attribute.name)}
|
|
||||||
value={value}
|
|
||||||
>
|
|
||||||
{options.options.map(option => (
|
|
||||||
<option key={option} value={option}>
|
|
||||||
{option}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type={(() => {
|
|
||||||
switch (attribute.name) {
|
|
||||||
case "password-confirm":
|
|
||||||
case "password":
|
|
||||||
return "password";
|
|
||||||
default:
|
|
||||||
return "text";
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
id={attribute.name}
|
|
||||||
name={attribute.name}
|
|
||||||
value={value}
|
|
||||||
onChange={onChangeFactory(attribute.name)}
|
|
||||||
className={clsx(props.kcInputClass)}
|
|
||||||
aria-invalid={displayableErrors.length !== 0}
|
|
||||||
disabled={attribute.readOnly}
|
|
||||||
autoComplete={attribute.autocomplete}
|
|
||||||
onBlur={onBlurFactory(attribute.name)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
{displayableErrors.length !== 0 &&
|
|
||||||
(() => {
|
|
||||||
const divId = `input-error-${attribute.name}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<style>{`#${divId} > span: { display: block; }`}</style>
|
|
||||||
<span
|
|
||||||
id={divId}
|
|
||||||
className={clsx(props.kcInputErrorMessageClass)}
|
|
||||||
style={{
|
|
||||||
"position": displayableErrors.length === 1 ? "absolute" : undefined
|
|
||||||
}}
|
|
||||||
aria-live="polite"
|
|
||||||
>
|
|
||||||
{displayableErrors.map(({ errorMessage }) => errorMessage)}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{AfterField && <AfterField attribute={attribute} />}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
@ -2,7 +2,7 @@ import type { PageId } from "../../bin/keycloakify/generateFtl";
|
|||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import type { Equals } from "tsafe";
|
import type { Equals } from "tsafe";
|
||||||
import type { MessageKeyBase } from "../i18n";
|
import type { MessageKeyBase } from "../i18n";
|
||||||
import type { KcTemplateClassKey } from "../components/KcProps";
|
import type { KcTemplateClassKey } from "../KcProps";
|
||||||
|
|
||||||
type ExtractAfterStartingWith<Prefix extends string, StrEnum> = StrEnum extends `${Prefix}${infer U}` ? U : never;
|
type ExtractAfterStartingWith<Prefix extends string, StrEnum> = StrEnum extends `${Prefix}${infer U}` ? U : never;
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import "minimal-polyfills/Object.fromEntries";
|
import "minimal-polyfills/Object.fromEntries";
|
||||||
import type { KcContextBase, Attribute } from "../KcContextBase";
|
import type { KcContextBase, Attribute } from "./KcContextBase";
|
||||||
//NOTE: Aside because we want to be able to import them from node
|
//NOTE: Aside because we want to be able to import them from node
|
||||||
import { mockTestingResourcesCommonPath, mockTestingResourcesPath } from "../../../bin/mockTestingResourcesPath";
|
import { mockTestingResourcesCommonPath, mockTestingResourcesPath } from "../../bin/mockTestingResourcesPath";
|
||||||
import { id } from "tsafe/id";
|
import { id } from "tsafe/id";
|
||||||
import { pathJoin } from "../../../bin/tools/pathJoin";
|
import { pathJoin } from "../../bin/tools/pathJoin";
|
||||||
|
|
||||||
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
|
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
|
||||||
|
|
||||||
@ -117,10 +117,13 @@ export const kcContextCommonMock: KcContextBase.Common = {
|
|||||||
"registrationEmailAsUsername": false
|
"registrationEmailAsUsername": false
|
||||||
},
|
},
|
||||||
"messagesPerField": {
|
"messagesPerField": {
|
||||||
"printIfExists": (...[, x]) => x,
|
"printIfExists": () => {
|
||||||
"existsError": () => true,
|
console.log("coucou");
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
"existsError": () => false,
|
||||||
"get": key => `Fake error for ${key}`,
|
"get": key => `Fake error for ${key}`,
|
||||||
"exists": () => true
|
"exists": () => false
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"supported": [
|
"supported": [
|
@ -1 +0,0 @@
|
|||||||
export * from "./kcContextMocks";
|
|
292
src/lib/i18n/i18n.tsx
Normal file
292
src/lib/i18n/i18n.tsx
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
import "minimal-polyfills/Object.fromEntries";
|
||||||
|
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
|
||||||
|
import React, { useEffect, useState, useRef } from "react";
|
||||||
|
import type baseMessages from "./generated_messages/18.0.1/login/en";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||||
|
import { Markdown } from "../tools/Markdown";
|
||||||
|
|
||||||
|
export const fallbackLanguageTag = "en";
|
||||||
|
|
||||||
|
export type KcContextLike = {
|
||||||
|
locale?: {
|
||||||
|
currentLanguageTag: string;
|
||||||
|
supported: { languageTag: string; url: string; label: string }[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<KcContextBase extends KcContextLike ? true : false>();
|
||||||
|
|
||||||
|
export type MessageKeyBase = keyof typeof baseMessages | keyof (typeof keycloakifyExtraMessages)[typeof fallbackLanguageTag];
|
||||||
|
|
||||||
|
export type I18n<MessageKey extends string> = {
|
||||||
|
/**
|
||||||
|
* e.g: "en", "fr", "zh-CN"
|
||||||
|
*
|
||||||
|
* The current language
|
||||||
|
*/
|
||||||
|
currentLanguageTag: string;
|
||||||
|
/**
|
||||||
|
* To call when the user switch language.
|
||||||
|
* This will cause the page to be reloaded,
|
||||||
|
* on next load currentLanguageTag === newLanguageTag
|
||||||
|
*/
|
||||||
|
changeLocale: (newLanguageTag: string) => never;
|
||||||
|
/**
|
||||||
|
* e.g. "en" => "English", "fr" => "Français", ...
|
||||||
|
*
|
||||||
|
* Used to render a select that enable user to switch language.
|
||||||
|
* ex: https://user-images.githubusercontent.com/6702424/186044799-38801eec-4e89-483b-81dd-8e9233e8c0eb.png
|
||||||
|
* */
|
||||||
|
labelBySupportedLanguageTag: Record<string, string>;
|
||||||
|
/**
|
||||||
|
* Examples assuming currentLanguageTag === "en"
|
||||||
|
*
|
||||||
|
* msg("access-denied") === <span>Access denied</span>
|
||||||
|
* msg("impersonateTitleHtml", "Foo") === <span><strong>Foo</strong> Impersonate User</span>
|
||||||
|
*/
|
||||||
|
msg: (key: MessageKey, ...args: (string | undefined)[]) => JSX.Element;
|
||||||
|
/**
|
||||||
|
* It's the same thing as msg() but instead of returning a JSX.Element it returns a string.
|
||||||
|
* It can be more convenient to manipulate strings but if there are HTML tags it wont render.
|
||||||
|
* msgStr("impersonateTitleHtml", "Foo") === "<strong>Foo</strong> Impersonate User"
|
||||||
|
*/
|
||||||
|
msgStr: (key: MessageKey, ...args: (string | undefined)[]) => string;
|
||||||
|
/**
|
||||||
|
* Examples assuming currentLanguageTag === "en"
|
||||||
|
* advancedMsg("${access-denied} foo bar") === <span>${msgStr("access-denied")} foo bar<span> === <span>Access denied foo bar</span>
|
||||||
|
* advancedMsg("${access-denied}") === advancedMsg("access-denied") === msg("access-denied") === <span>Access denied</span>
|
||||||
|
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === <span>not-a-message-key</span>
|
||||||
|
*/
|
||||||
|
advancedMsg: (key: string, ...args: (string | undefined)[]) => JSX.Element;
|
||||||
|
/**
|
||||||
|
* Examples assuming currentLanguageTag === "en"
|
||||||
|
* 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"
|
||||||
|
*/
|
||||||
|
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type I18nBase = I18n<MessageKeyBase>;
|
||||||
|
|
||||||
|
export function __unsafe_useI18n<ExtraMessageKey extends string = never>(params: {
|
||||||
|
kcContext: KcContextLike;
|
||||||
|
extraMessages: { [languageTag: string]: { [key in ExtraMessageKey]: string } };
|
||||||
|
doSkip: boolean;
|
||||||
|
}): I18n<MessageKeyBase | ExtraMessageKey> | null {
|
||||||
|
const { kcContext, extraMessages, doSkip } = params;
|
||||||
|
|
||||||
|
const [i18n, setI18n] = useState<I18n<ExtraMessageKey | MessageKeyBase> | undefined>(undefined);
|
||||||
|
|
||||||
|
const refHasStartedFetching = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (doSkip || refHasStartedFetching.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refHasStartedFetching.current = true;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const { currentLanguageTag = fallbackLanguageTag } = kcContext.locale ?? {};
|
||||||
|
|
||||||
|
const [fallbackMessages, messages] = await Promise.all([
|
||||||
|
import("./generated_messages/18.0.1/login/en"),
|
||||||
|
(() => {
|
||||||
|
switch (currentLanguageTag) {
|
||||||
|
case "ca":
|
||||||
|
return import("./generated_messages/18.0.1/login/ca");
|
||||||
|
case "cs":
|
||||||
|
return import("./generated_messages/18.0.1/login/cs");
|
||||||
|
case "da":
|
||||||
|
return import("./generated_messages/18.0.1/login/da");
|
||||||
|
case "de":
|
||||||
|
return import("./generated_messages/18.0.1/login/de");
|
||||||
|
case "en":
|
||||||
|
return import("./generated_messages/18.0.1/login/en");
|
||||||
|
case "es":
|
||||||
|
return import("./generated_messages/18.0.1/login/es");
|
||||||
|
case "fi":
|
||||||
|
return import("./generated_messages/18.0.1/login/fi");
|
||||||
|
case "fr":
|
||||||
|
return import("./generated_messages/18.0.1/login/fr");
|
||||||
|
case "hu":
|
||||||
|
return import("./generated_messages/18.0.1/login/hu");
|
||||||
|
case "it":
|
||||||
|
return import("./generated_messages/18.0.1/login/it");
|
||||||
|
case "ja":
|
||||||
|
return import("./generated_messages/18.0.1/login/ja");
|
||||||
|
case "lt":
|
||||||
|
return import("./generated_messages/18.0.1/login/lt");
|
||||||
|
case "lv":
|
||||||
|
return import("./generated_messages/18.0.1/login/lv");
|
||||||
|
case "nl":
|
||||||
|
return import("./generated_messages/18.0.1/login/nl");
|
||||||
|
case "no":
|
||||||
|
return import("./generated_messages/18.0.1/login/no");
|
||||||
|
case "pl":
|
||||||
|
return import("./generated_messages/18.0.1/login/pl");
|
||||||
|
case "pt-BR":
|
||||||
|
return import("./generated_messages/18.0.1/login/pt-BR");
|
||||||
|
case "ru":
|
||||||
|
return import("./generated_messages/18.0.1/login/ru");
|
||||||
|
case "sk":
|
||||||
|
return import("./generated_messages/18.0.1/login/sk");
|
||||||
|
case "sv":
|
||||||
|
return import("./generated_messages/18.0.1/login/sv");
|
||||||
|
case "tr":
|
||||||
|
return import("./generated_messages/18.0.1/login/tr");
|
||||||
|
case "zh-CN":
|
||||||
|
return import("./generated_messages/18.0.1/login/zh-CN");
|
||||||
|
default:
|
||||||
|
return { "default": {} };
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
]).then(modules => modules.map(module => module.default));
|
||||||
|
|
||||||
|
setI18n({
|
||||||
|
...createI18nTranslationFunctions({
|
||||||
|
"fallbackMessages": {
|
||||||
|
...fallbackMessages,
|
||||||
|
...(keycloakifyExtraMessages[fallbackLanguageTag] ?? {}),
|
||||||
|
...(extraMessages[fallbackLanguageTag] ?? {})
|
||||||
|
} as any,
|
||||||
|
"messages": {
|
||||||
|
...messages,
|
||||||
|
...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}),
|
||||||
|
...(extraMessages[currentLanguageTag] ?? {})
|
||||||
|
} as any
|
||||||
|
}),
|
||||||
|
currentLanguageTag,
|
||||||
|
"changeLocale": newLanguageTag => {
|
||||||
|
const { locale } = kcContext;
|
||||||
|
|
||||||
|
assert(locale !== undefined, "Internationalization not enabled");
|
||||||
|
|
||||||
|
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === newLanguageTag);
|
||||||
|
|
||||||
|
assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`);
|
||||||
|
|
||||||
|
window.location.href = targetSupportedLocale.url;
|
||||||
|
|
||||||
|
assert(false, "never");
|
||||||
|
},
|
||||||
|
"labelBySupportedLanguageTag": Object.fromEntries(
|
||||||
|
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])
|
||||||
|
)
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return i18n ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useI18n_private = __unsafe_useI18n;
|
||||||
|
|
||||||
|
export function useI18n<ExtraMessageKey extends string = never>(params: {
|
||||||
|
kcContext: KcContextLike;
|
||||||
|
extraMessages: { [languageTag: string]: { [key in ExtraMessageKey]: string } };
|
||||||
|
}): I18n<MessageKeyBase | ExtraMessageKey> | null {
|
||||||
|
return useI18n_private({
|
||||||
|
...params,
|
||||||
|
"doSkip": false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createI18nTranslationFunctions<MessageKey extends string>(params: {
|
||||||
|
fallbackMessages: Record<MessageKey, string>;
|
||||||
|
messages: Record<MessageKey, string>;
|
||||||
|
}): Pick<I18n<MessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
|
||||||
|
const { fallbackMessages, messages } = params;
|
||||||
|
|
||||||
|
function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderMarkdown: boolean }): string | JSX.Element | undefined {
|
||||||
|
const { key, args, doRenderMarkdown } = props;
|
||||||
|
|
||||||
|
const messageOrUndefined: string | undefined = (messages as any)[key] ?? (fallbackMessages as any)[key];
|
||||||
|
|
||||||
|
if (messageOrUndefined === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = messageOrUndefined;
|
||||||
|
|
||||||
|
const messageWithArgsInjectedIfAny = (() => {
|
||||||
|
const startIndex = message
|
||||||
|
.match(/{[0-9]+}/g)
|
||||||
|
?.map(g => g.match(/{([0-9]+)}/)![1])
|
||||||
|
.map(indexStr => parseInt(indexStr))
|
||||||
|
.sort((a, b) => a - b)[0];
|
||||||
|
|
||||||
|
if (startIndex === undefined) {
|
||||||
|
// No {0} in message (no arguments expected)
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
let messageWithArgsInjected = message;
|
||||||
|
|
||||||
|
args.forEach((arg, i) => {
|
||||||
|
if (arg === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
messageWithArgsInjected = messageWithArgsInjected.replace(new RegExp(`\\{${i + startIndex}\\}`, "g"), arg);
|
||||||
|
});
|
||||||
|
|
||||||
|
return messageWithArgsInjected;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return doRenderMarkdown ? (
|
||||||
|
<Markdown allowDangerousHtml renderers={{ "paragraph": "span" }}>
|
||||||
|
{messageWithArgsInjectedIfAny}
|
||||||
|
</Markdown>
|
||||||
|
) : (
|
||||||
|
messageWithArgsInjectedIfAny
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderMarkdown: boolean }): JSX.Element | string {
|
||||||
|
const { key, args, doRenderMarkdown } = props;
|
||||||
|
|
||||||
|
const match = key.match(/^\$\{([^{]+)\}$/);
|
||||||
|
|
||||||
|
const keyUnwrappedFromCurlyBraces = match === null ? key : match[1];
|
||||||
|
|
||||||
|
const out = resolveMsg({
|
||||||
|
"key": keyUnwrappedFromCurlyBraces,
|
||||||
|
args,
|
||||||
|
doRenderMarkdown
|
||||||
|
});
|
||||||
|
|
||||||
|
return (out !== undefined ? out : doRenderMarkdown ? <span>{keyUnwrappedFromCurlyBraces}</span> : keyUnwrappedFromCurlyBraces) as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"msgStr": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": false }) as string,
|
||||||
|
"msg": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": true }) as JSX.Element,
|
||||||
|
"advancedMsg": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": true }) as JSX.Element,
|
||||||
|
"advancedMsgStr": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": false }) as string
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const keycloakifyExtraMessages = {
|
||||||
|
"en": {
|
||||||
|
"shouldBeEqual": "{0} should be equal to {1}",
|
||||||
|
"shouldBeDifferent": "{0} should be different to {1}",
|
||||||
|
"shouldMatchPattern": "Pattern should match: `/{0}/`",
|
||||||
|
"mustBeAnInteger": "Must be an integer",
|
||||||
|
"notAValidOption": "Not a valid option"
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
/* spell-checker: disable */
|
||||||
|
"shouldBeEqual": "{0} doit être égal à {1}",
|
||||||
|
"shouldBeDifferent": "{0} doit être différent de {1}",
|
||||||
|
"shouldMatchPattern": "Dois respecter le schéma: `/{0}/`",
|
||||||
|
"mustBeAnInteger": "Doit être un nombre entier",
|
||||||
|
"notAValidOption": "N'est pas une option valide",
|
||||||
|
|
||||||
|
"logoutConfirmTitle": "Déconnexion",
|
||||||
|
"logoutConfirmHeader": "Êtes-vous sûr(e) de vouloir vous déconnecter ?",
|
||||||
|
"doLogout": "Se déconnecter"
|
||||||
|
/* spell-checker: enable */
|
||||||
|
}
|
||||||
|
};
|
@ -1,290 +1 @@
|
|||||||
import "minimal-polyfills/Object.fromEntries";
|
export * from "./i18n";
|
||||||
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
|
|
||||||
import React, { useEffect, useState, useRef } from "react";
|
|
||||||
import type baseMessages from "./generated_messages/18.0.1/login/en";
|
|
||||||
import { assert } from "tsafe/assert";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { Markdown } from "../tools/Markdown";
|
|
||||||
|
|
||||||
export const fallbackLanguageTag = "en";
|
|
||||||
|
|
||||||
export type KcContextLike = {
|
|
||||||
locale?: {
|
|
||||||
currentLanguageTag: string;
|
|
||||||
supported: { languageTag: string; url: string; label: string }[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
assert<KcContextBase extends KcContextLike ? true : false>();
|
|
||||||
|
|
||||||
export type MessageKeyBase = keyof typeof baseMessages | keyof typeof keycloakifyExtraMessages[typeof fallbackLanguageTag];
|
|
||||||
|
|
||||||
export type I18n<MessageKey extends string = MessageKeyBase> = {
|
|
||||||
/**
|
|
||||||
* e.g: "en", "fr", "zh-CN"
|
|
||||||
*
|
|
||||||
* The current language
|
|
||||||
*/
|
|
||||||
currentLanguageTag: string;
|
|
||||||
/**
|
|
||||||
* To call when the user switch language.
|
|
||||||
* This will cause the page to be reloaded,
|
|
||||||
* on next load currentLanguageTag === newLanguageTag
|
|
||||||
*/
|
|
||||||
changeLocale: (newLanguageTag: string) => never;
|
|
||||||
/**
|
|
||||||
* e.g. "en" => "English", "fr" => "Français", ...
|
|
||||||
*
|
|
||||||
* Used to render a select that enable user to switch language.
|
|
||||||
* ex: https://user-images.githubusercontent.com/6702424/186044799-38801eec-4e89-483b-81dd-8e9233e8c0eb.png
|
|
||||||
* */
|
|
||||||
labelBySupportedLanguageTag: Record<string, string>;
|
|
||||||
/**
|
|
||||||
* Examples assuming currentLanguageTag === "en"
|
|
||||||
*
|
|
||||||
* msg("access-denied") === <span>Access denied</span>
|
|
||||||
* msg("impersonateTitleHtml", "Foo") === <span><strong>Foo</strong> Impersonate User</span>
|
|
||||||
*/
|
|
||||||
msg: (key: MessageKey, ...args: (string | undefined)[]) => JSX.Element;
|
|
||||||
/**
|
|
||||||
* It's the same thing as msg() but instead of returning a JSX.Element it returns a string.
|
|
||||||
* It can be more convenient to manipulate strings but if there are HTML tags it wont render.
|
|
||||||
* msgStr("impersonateTitleHtml", "Foo") === "<strong>Foo</strong> Impersonate User"
|
|
||||||
*/
|
|
||||||
msgStr: (key: MessageKey, ...args: (string | undefined)[]) => string;
|
|
||||||
/**
|
|
||||||
* Examples assuming currentLanguageTag === "en"
|
|
||||||
* advancedMsg("${access-denied} foo bar") === <span>${msgStr("access-denied")} foo bar<span> === <span>Access denied foo bar</span>
|
|
||||||
* advancedMsg("${access-denied}") === advancedMsg("access-denied") === msg("access-denied") === <span>Access denied</span>
|
|
||||||
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === <span>not-a-message-key</span>
|
|
||||||
*/
|
|
||||||
advancedMsg: (key: string, ...args: (string | undefined)[]) => JSX.Element;
|
|
||||||
/**
|
|
||||||
* Examples assuming currentLanguageTag === "en"
|
|
||||||
* 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"
|
|
||||||
*/
|
|
||||||
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function __unsafe_useI18n<ExtraMessageKey extends string = never>(params: {
|
|
||||||
kcContext: KcContextLike;
|
|
||||||
extraMessages: { [languageTag: string]: { [key in ExtraMessageKey]: string } };
|
|
||||||
doSkip: boolean;
|
|
||||||
}): I18n<MessageKeyBase | ExtraMessageKey> | null {
|
|
||||||
const { kcContext, extraMessages, doSkip } = params;
|
|
||||||
|
|
||||||
const [i18n, setI18n] = useState<I18n<ExtraMessageKey | MessageKeyBase> | undefined>(undefined);
|
|
||||||
|
|
||||||
const refHasStartedFetching = useRef(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (doSkip || refHasStartedFetching.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
refHasStartedFetching.current = true;
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const { currentLanguageTag = fallbackLanguageTag } = kcContext.locale ?? {};
|
|
||||||
|
|
||||||
const [fallbackMessages, messages] = await Promise.all([
|
|
||||||
import("./generated_messages/18.0.1/login/en"),
|
|
||||||
(() => {
|
|
||||||
switch (currentLanguageTag) {
|
|
||||||
case "ca":
|
|
||||||
return import("./generated_messages/18.0.1/login/ca");
|
|
||||||
case "cs":
|
|
||||||
return import("./generated_messages/18.0.1/login/cs");
|
|
||||||
case "da":
|
|
||||||
return import("./generated_messages/18.0.1/login/da");
|
|
||||||
case "de":
|
|
||||||
return import("./generated_messages/18.0.1/login/de");
|
|
||||||
case "en":
|
|
||||||
return import("./generated_messages/18.0.1/login/en");
|
|
||||||
case "es":
|
|
||||||
return import("./generated_messages/18.0.1/login/es");
|
|
||||||
case "fi":
|
|
||||||
return import("./generated_messages/18.0.1/login/fi");
|
|
||||||
case "fr":
|
|
||||||
return import("./generated_messages/18.0.1/login/fr");
|
|
||||||
case "hu":
|
|
||||||
return import("./generated_messages/18.0.1/login/hu");
|
|
||||||
case "it":
|
|
||||||
return import("./generated_messages/18.0.1/login/it");
|
|
||||||
case "ja":
|
|
||||||
return import("./generated_messages/18.0.1/login/ja");
|
|
||||||
case "lt":
|
|
||||||
return import("./generated_messages/18.0.1/login/lt");
|
|
||||||
case "lv":
|
|
||||||
return import("./generated_messages/18.0.1/login/lv");
|
|
||||||
case "nl":
|
|
||||||
return import("./generated_messages/18.0.1/login/nl");
|
|
||||||
case "no":
|
|
||||||
return import("./generated_messages/18.0.1/login/no");
|
|
||||||
case "pl":
|
|
||||||
return import("./generated_messages/18.0.1/login/pl");
|
|
||||||
case "pt-BR":
|
|
||||||
return import("./generated_messages/18.0.1/login/pt-BR");
|
|
||||||
case "ru":
|
|
||||||
return import("./generated_messages/18.0.1/login/ru");
|
|
||||||
case "sk":
|
|
||||||
return import("./generated_messages/18.0.1/login/sk");
|
|
||||||
case "sv":
|
|
||||||
return import("./generated_messages/18.0.1/login/sv");
|
|
||||||
case "tr":
|
|
||||||
return import("./generated_messages/18.0.1/login/tr");
|
|
||||||
case "zh-CN":
|
|
||||||
return import("./generated_messages/18.0.1/login/zh-CN");
|
|
||||||
default:
|
|
||||||
return { "default": {} };
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
]).then(modules => modules.map(module => module.default));
|
|
||||||
|
|
||||||
setI18n({
|
|
||||||
...createI18nTranslationFunctions({
|
|
||||||
"fallbackMessages": {
|
|
||||||
...fallbackMessages,
|
|
||||||
...(keycloakifyExtraMessages[fallbackLanguageTag] ?? {}),
|
|
||||||
...(extraMessages[fallbackLanguageTag] ?? {})
|
|
||||||
} as any,
|
|
||||||
"messages": {
|
|
||||||
...messages,
|
|
||||||
...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}),
|
|
||||||
...(extraMessages[currentLanguageTag] ?? {})
|
|
||||||
} as any
|
|
||||||
}),
|
|
||||||
currentLanguageTag,
|
|
||||||
"changeLocale": newLanguageTag => {
|
|
||||||
const { locale } = kcContext;
|
|
||||||
|
|
||||||
assert(locale !== undefined, "Internationalization not enabled");
|
|
||||||
|
|
||||||
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === newLanguageTag);
|
|
||||||
|
|
||||||
assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`);
|
|
||||||
|
|
||||||
window.location.href = targetSupportedLocale.url;
|
|
||||||
|
|
||||||
assert(false, "never");
|
|
||||||
},
|
|
||||||
"labelBySupportedLanguageTag": Object.fromEntries(
|
|
||||||
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])
|
|
||||||
)
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return i18n ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useI18n_private = __unsafe_useI18n;
|
|
||||||
|
|
||||||
export function useI18n<ExtraMessageKey extends string = never>(params: {
|
|
||||||
kcContext: KcContextLike;
|
|
||||||
extraMessages: { [languageTag: string]: { [key in ExtraMessageKey]: string } };
|
|
||||||
}): I18n<MessageKeyBase | ExtraMessageKey> | null {
|
|
||||||
return useI18n_private({
|
|
||||||
...params,
|
|
||||||
"doSkip": false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createI18nTranslationFunctions<MessageKey extends string>(params: {
|
|
||||||
fallbackMessages: Record<MessageKey, string>;
|
|
||||||
messages: Record<MessageKey, string>;
|
|
||||||
}): Pick<I18n<MessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
|
|
||||||
const { fallbackMessages, messages } = params;
|
|
||||||
|
|
||||||
function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderMarkdown: boolean }): string | JSX.Element | undefined {
|
|
||||||
const { key, args, doRenderMarkdown } = props;
|
|
||||||
|
|
||||||
const messageOrUndefined: string | undefined = (messages as any)[key] ?? (fallbackMessages as any)[key];
|
|
||||||
|
|
||||||
if (messageOrUndefined === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = messageOrUndefined;
|
|
||||||
|
|
||||||
const messageWithArgsInjectedIfAny = (() => {
|
|
||||||
const startIndex = message
|
|
||||||
.match(/{[0-9]+}/g)
|
|
||||||
?.map(g => g.match(/{([0-9]+)}/)![1])
|
|
||||||
.map(indexStr => parseInt(indexStr))
|
|
||||||
.sort((a, b) => a - b)[0];
|
|
||||||
|
|
||||||
if (startIndex === undefined) {
|
|
||||||
// No {0} in message (no arguments expected)
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
let messageWithArgsInjected = message;
|
|
||||||
|
|
||||||
args.forEach((arg, i) => {
|
|
||||||
if (arg === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
messageWithArgsInjected = messageWithArgsInjected.replace(new RegExp(`\\{${i + startIndex}\\}`, "g"), arg);
|
|
||||||
});
|
|
||||||
|
|
||||||
return messageWithArgsInjected;
|
|
||||||
})();
|
|
||||||
|
|
||||||
return doRenderMarkdown ? (
|
|
||||||
<Markdown allowDangerousHtml renderers={{ "paragraph": "span" }}>
|
|
||||||
{messageWithArgsInjectedIfAny}
|
|
||||||
</Markdown>
|
|
||||||
) : (
|
|
||||||
messageWithArgsInjectedIfAny
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderMarkdown: boolean }): JSX.Element | string {
|
|
||||||
const { key, args, doRenderMarkdown } = props;
|
|
||||||
|
|
||||||
const match = key.match(/^\$\{([^{]+)\}$/);
|
|
||||||
|
|
||||||
const keyUnwrappedFromCurlyBraces = match === null ? key : match[1];
|
|
||||||
|
|
||||||
const out = resolveMsg({
|
|
||||||
"key": keyUnwrappedFromCurlyBraces,
|
|
||||||
args,
|
|
||||||
doRenderMarkdown
|
|
||||||
});
|
|
||||||
|
|
||||||
return (out !== undefined ? out : doRenderMarkdown ? <span>{keyUnwrappedFromCurlyBraces}</span> : keyUnwrappedFromCurlyBraces) as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"msgStr": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": false }) as string,
|
|
||||||
"msg": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": true }) as JSX.Element,
|
|
||||||
"advancedMsg": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": true }) as JSX.Element,
|
|
||||||
"advancedMsgStr": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": false }) as string
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const keycloakifyExtraMessages = {
|
|
||||||
"en": {
|
|
||||||
"shouldBeEqual": "{0} should be equal to {1}",
|
|
||||||
"shouldBeDifferent": "{0} should be different to {1}",
|
|
||||||
"shouldMatchPattern": "Pattern should match: `/{0}/`",
|
|
||||||
"mustBeAnInteger": "Must be an integer",
|
|
||||||
"notAValidOption": "Not a valid option"
|
|
||||||
},
|
|
||||||
"fr": {
|
|
||||||
/* spell-checker: disable */
|
|
||||||
"shouldBeEqual": "{0} doit être égal à {1}",
|
|
||||||
"shouldBeDifferent": "{0} doit être différent de {1}",
|
|
||||||
"shouldMatchPattern": "Dois respecter le schéma: `/{0}/`",
|
|
||||||
"mustBeAnInteger": "Doit être un nombre entier",
|
|
||||||
"notAValidOption": "N'est pas une option valide",
|
|
||||||
|
|
||||||
"logoutConfirmTitle": "Déconnexion",
|
|
||||||
"logoutConfirmHeader": "Êtes-vous sûr(e) de vouloir vous déconnecter ?",
|
|
||||||
"doLogout": "Se déconnecter"
|
|
||||||
/* spell-checker: enable */
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
@ -2,12 +2,12 @@ export * from "./getKcContext";
|
|||||||
|
|
||||||
export * from "./i18n";
|
export * from "./i18n";
|
||||||
|
|
||||||
export { useDownloadTerms } from "./components/Terms";
|
export { useDownloadTerms } from "./pages/Terms";
|
||||||
|
|
||||||
export * from "./components/KcProps";
|
export * from "./KcProps";
|
||||||
export * from "./keycloakJsAdapter";
|
export * from "./keycloakJsAdapter";
|
||||||
export * from "./useFormValidationSlice";
|
export * from "./useFormValidationSlice";
|
||||||
|
|
||||||
import KcApp from "./components/KcApp";
|
import KcApp from "./KcApp";
|
||||||
|
|
||||||
export default KcApp;
|
export default KcApp;
|
||||||
|
@ -1,19 +1,10 @@
|
|||||||
import React, { memo } from "react";
|
import React from "react";
|
||||||
import DefaultTemplate from "./Template";
|
import type { PageProps } from "../KcProps";
|
||||||
import type { TemplateProps } from "./Template";
|
import type { KcContextBase } from "../getKcContext";
|
||||||
import type { KcProps } from "./KcProps";
|
import type { I18nBase } from "../i18n";
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import type { I18n } from "../i18n";
|
|
||||||
|
|
||||||
export type ErrorProps = KcProps & {
|
export default function Error(props: PageProps<Extract<KcContextBase, { pageId: "error.ftl" }>, I18nBase>) {
|
||||||
kcContext: KcContextBase.Error;
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
i18n: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Error = memo((props: ErrorProps) => {
|
|
||||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
|
||||||
|
|
||||||
const { message, client } = kcContext;
|
const { message, client } = kcContext;
|
||||||
|
|
||||||
@ -38,6 +29,4 @@ const Error = memo((props: ErrorProps) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default Error;
|
|
@ -1,21 +1,12 @@
|
|||||||
import React, { useState, memo } from "react";
|
import React, { useState } from "react";
|
||||||
import DefaultTemplate from "./Template";
|
|
||||||
import type { TemplateProps } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { clsx } from "../tools/clsx";
|
import { clsx } from "../tools/clsx";
|
||||||
import type { I18n } from "../i18n";
|
|
||||||
import { UserProfileFormFields } from "./shared/UserProfileCommons";
|
import { UserProfileFormFields } from "./shared/UserProfileCommons";
|
||||||
|
import type { PageProps } from "../KcProps";
|
||||||
|
import type { KcContextBase } from "../getKcContext";
|
||||||
|
import type { I18nBase } from "../i18n";
|
||||||
|
|
||||||
export type IdpReviewUserProfileProps = KcProps & {
|
export default function IdpReviewUserProfile(props: PageProps<Extract<KcContextBase, { pageId: "idp-review-user-profile.ftl" }>, I18nBase>) {
|
||||||
kcContext: KcContextBase.IdpReviewUserProfile;
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
i18n: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const IdpReviewUserProfile = memo((props: IdpReviewUserProfileProps) => {
|
|
||||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
|
||||||
|
|
||||||
const { msg, msgStr } = i18n;
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
@ -53,6 +44,4 @@ const IdpReviewUserProfile = memo((props: IdpReviewUserProfileProps) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default IdpReviewUserProfile;
|
|
@ -1,20 +1,11 @@
|
|||||||
import React, { memo } from "react";
|
import React from "react";
|
||||||
import DefaultTemplate from "./Template";
|
|
||||||
import type { TemplateProps } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import { assert } from "../tools/assert";
|
import { assert } from "../tools/assert";
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
import type { PageProps } from "../KcProps";
|
||||||
import type { I18n } from "../i18n";
|
import type { KcContextBase } from "../getKcContext";
|
||||||
|
import type { I18nBase } from "../i18n";
|
||||||
|
|
||||||
export type InfoProps = KcProps & {
|
export default function Info(props: PageProps<Extract<KcContextBase, { pageId: "info.ftl" }>, I18nBase>) {
|
||||||
kcContext: KcContextBase.Info;
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
i18n: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Info = memo((props: InfoProps) => {
|
|
||||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
|
||||||
|
|
||||||
const { msgStr, msg } = i18n;
|
const { msgStr, msg } = i18n;
|
||||||
|
|
||||||
@ -55,6 +46,4 @@ const Info = memo((props: InfoProps) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default Info;
|
|
@ -1,22 +1,12 @@
|
|||||||
import React, { useState, memo } from "react";
|
import React, { useState, type FormEventHandler } from "react";
|
||||||
import DefaultTemplate from "./Template";
|
|
||||||
import type { TemplateProps } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { clsx } from "../tools/clsx";
|
import { clsx } from "../tools/clsx";
|
||||||
import { useConstCallback } from "powerhooks/useConstCallback";
|
import { useConstCallback } from "../tools/useConstCallback";
|
||||||
import type { FormEventHandler } from "react";
|
import type { PageProps } from "../KcProps";
|
||||||
import type { I18n } from "../i18n";
|
import type { KcContextBase } from "../getKcContext";
|
||||||
|
import type { I18nBase } from "../i18n";
|
||||||
|
|
||||||
export type LoginProps = KcProps & {
|
export default function Login(props: PageProps<Extract<KcContextBase, { pageId: "login.ftl" }>, I18nBase>) {
|
||||||
kcContext: KcContextBase.Login;
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
i18n: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Login = memo((props: LoginProps) => {
|
|
||||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
|
||||||
|
|
||||||
const { social, realm, url, usernameEditDisabled, login, auth, registrationDisabled } = kcContext;
|
const { social, realm, url, usernameEditDisabled, login, auth, registrationDisabled } = kcContext;
|
||||||
|
|
||||||
@ -199,6 +189,4 @@ const Login = memo((props: LoginProps) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default Login;
|
|
@ -1,20 +1,11 @@
|
|||||||
import React, { memo } from "react";
|
import React from "react";
|
||||||
import DefaultTemplate from "./Template";
|
|
||||||
import type { TemplateProps } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { clsx } from "../tools/clsx";
|
import { clsx } from "../tools/clsx";
|
||||||
import type { I18n } from "../i18n";
|
import type { PageProps } from "../KcProps";
|
||||||
|
import type { KcContextBase } from "../getKcContext";
|
||||||
|
import type { I18nBase } from "../i18n";
|
||||||
|
|
||||||
export type LoginConfigTotpProps = KcProps & {
|
export default function LoginConfigTotp(props: PageProps<Extract<KcContextBase, { pageId: "login-config-totp.ftl" }>, I18nBase>) {
|
||||||
kcContext: KcContextBase.LoginConfigTotp;
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
i18n: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LoginConfigTotp = memo((props: LoginConfigTotpProps) => {
|
|
||||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
|
||||||
|
|
||||||
const { url, isAppInitiatedAction, totp, mode, messagesPerField } = kcContext;
|
const { url, isAppInitiatedAction, totp, mode, messagesPerField } = kcContext;
|
||||||
|
|
||||||
@ -188,6 +179,4 @@ const LoginConfigTotp = memo((props: LoginConfigTotpProps) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default LoginConfigTotp;
|
|
@ -1,20 +1,11 @@
|
|||||||
import React, { memo } from "react";
|
import React from "react";
|
||||||
import DefaultTemplate from "./Template";
|
|
||||||
import type { TemplateProps } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { clsx } from "../tools/clsx";
|
import { clsx } from "../tools/clsx";
|
||||||
import type { I18n } from "../i18n";
|
import type { PageProps } from "../KcProps";
|
||||||
|
import type { KcContextBase } from "../getKcContext";
|
||||||
|
import type { I18nBase } from "../i18n";
|
||||||
|
|
||||||
export type LoginIdpLinkConfirmProps = KcProps & {
|
export default function LoginIdpLinkConfirm(props: PageProps<Extract<KcContextBase, { pageId: "login-idp-link-confirm.ftl" }>, I18nBase>) {
|
||||||
kcContext: KcContextBase.LoginIdpLinkConfirm;
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
i18n: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LoginIdpLinkConfirm = memo((props: LoginIdpLinkConfirmProps) => {
|
|
||||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
|
||||||
|
|
||||||
const { url, idpAlias } = kcContext;
|
const { url, idpAlias } = kcContext;
|
||||||
|
|
||||||
@ -60,6 +51,4 @@ const LoginIdpLinkConfirm = memo((props: LoginIdpLinkConfirmProps) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default LoginIdpLinkConfirm;
|
|
@ -1,19 +1,10 @@
|
|||||||
import React, { memo } from "react";
|
import React from "react";
|
||||||
import DefaultTemplate from "./Template";
|
import type { KcContextBase } from "../getKcContext";
|
||||||
import type { TemplateProps } from "./Template";
|
import type { PageProps } from "../KcProps";
|
||||||
import type { KcProps } from "./KcProps";
|
import type { I18nBase } from "../i18n";
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import type { I18n } from "../i18n";
|
|
||||||
|
|
||||||
export type LoginIdpLinkEmailProps = KcProps & {
|
export default function LoginIdpLinkEmail(props: PageProps<Extract<KcContextBase, { pageId: "login-idp-link-email.ftl" }>, I18nBase>) {
|
||||||
kcContext: KcContextBase.LoginIdpLinkEmail;
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
i18n: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LoginIdpLinkEmail = memo((props: LoginIdpLinkEmailProps) => {
|
|
||||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
|
||||||
|
|
||||||
const { url, realm, brokerContext, idpAlias } = kcContext;
|
const { url, realm, brokerContext, idpAlias } = kcContext;
|
||||||
|
|
||||||
@ -38,6 +29,4 @@ const LoginIdpLinkEmail = memo((props: LoginIdpLinkEmailProps) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default LoginIdpLinkEmail;
|
|
@ -1,22 +1,13 @@
|
|||||||
import React, { useEffect, memo } from "react";
|
import React, { useEffect } from "react";
|
||||||
import DefaultTemplate from "./Template";
|
|
||||||
import type { TemplateProps } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { headInsert } from "../tools/headInsert";
|
import { headInsert } from "../tools/headInsert";
|
||||||
import { pathJoin } from "../../bin/tools/pathJoin";
|
import { pathJoin } from "../../bin/tools/pathJoin";
|
||||||
import { clsx } from "../tools/clsx";
|
import { clsx } from "../tools/clsx";
|
||||||
import type { I18n } from "../i18n";
|
import type { PageProps } from "../KcProps";
|
||||||
|
import type { KcContextBase } from "../getKcContext";
|
||||||
|
import type { I18nBase } from "../i18n";
|
||||||
|
|
||||||
export type LoginOtpProps = KcProps & {
|
export default function LoginOtp(props: PageProps<Extract<KcContextBase, { pageId: "login-otp.ftl" }>, I18nBase>) {
|
||||||
kcContext: KcContextBase.LoginOtp;
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
i18n: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LoginOtp = memo((props: LoginOtpProps) => {
|
|
||||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
|
||||||
|
|
||||||
const { otpLogin, url } = kcContext;
|
const { otpLogin, url } = kcContext;
|
||||||
|
|
||||||
@ -96,7 +87,7 @@ const LoginOtp = memo((props: LoginOtpProps) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
declare const $: any;
|
declare const $: any;
|
||||||
|
|
||||||
@ -121,5 +112,3 @@ function evaluateInlineScript() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LoginOtp;
|
|
@ -1,19 +1,10 @@
|
|||||||
import React, { memo } from "react";
|
import React from "react";
|
||||||
import DefaultTemplate from "./Template";
|
import type { PageProps } from "../KcProps";
|
||||||
import type { TemplateProps } from "./Template";
|
import type { KcContextBase } from "../getKcContext";
|
||||||
import type { KcProps } from "./KcProps";
|
import type { I18nBase } from "../i18n";
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import type { I18n } from "../i18n";
|
|
||||||
|
|
||||||
export type LoginPageExpired = KcProps & {
|
export default function LoginPageExpired(props: PageProps<Extract<KcContextBase, { pageId: "login-page-expired.ftl" }>, I18nBase>) {
|
||||||
kcContext: KcContextBase.LoginPageExpired;
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
i18n: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LoginPageExpired = memo((props: LoginPageExpired) => {
|
|
||||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
|
||||||
|
|
||||||
const { url } = kcContext;
|
const { url } = kcContext;
|
||||||
|
|
||||||
@ -42,6 +33,4 @@ const LoginPageExpired = memo((props: LoginPageExpired) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default LoginPageExpired;
|
|
@ -1,22 +1,13 @@
|
|||||||
import React, { useState, memo } from "react";
|
import React, { useState } from "react";
|
||||||
import DefaultTemplate from "./Template";
|
|
||||||
import type { TemplateProps } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { clsx } from "../tools/clsx";
|
import { clsx } from "../tools/clsx";
|
||||||
import { useConstCallback } from "powerhooks/useConstCallback";
|
import { useConstCallback } from "../tools/useConstCallback";
|
||||||
import type { FormEventHandler } from "react";
|
import type { FormEventHandler } from "react";
|
||||||
import type { I18n } from "../i18n";
|
import type { PageProps } from "../KcProps";
|
||||||
|
import type { KcContextBase } from "../getKcContext";
|
||||||
|
import type { I18nBase } from "../i18n";
|
||||||
|
|
||||||
export type LoginPasswordProps = KcProps & {
|
export default function LoginPassword(props: PageProps<Extract<KcContextBase, { "pageId": "login-password.ftl" }>, I18nBase>) {
|
||||||
kcContext: KcContextBase.LoginPassword;
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
i18n: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LoginPassword = memo((props: LoginPasswordProps) => {
|
|
||||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
|
||||||
|
|
||||||
const { realm, url, login } = kcContext;
|
const { realm, url, login } = kcContext;
|
||||||
|
|
||||||
@ -92,6 +83,4 @@ const LoginPassword = memo((props: LoginPasswordProps) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default LoginPassword;
|
|
@ -1,20 +1,11 @@
|
|||||||
import React, { memo } from "react";
|
import React from "react";
|
||||||
import DefaultTemplate from "./Template";
|
|
||||||
import type { TemplateProps } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { clsx } from "../tools/clsx";
|
import { clsx } from "../tools/clsx";
|
||||||
import type { I18n } from "../i18n";
|
import type { PageProps } from "../KcProps";
|
||||||
|
import type { KcContextBase } from "../getKcContext";
|
||||||
|
import type { I18nBase } from "../i18n";
|
||||||
|
|
||||||
export type LoginResetPasswordProps = KcProps & {
|
export default function LoginResetPassword(props: PageProps<Extract<KcContextBase, { pageId: "login-reset-password.ftl" }>, I18nBase>) {
|
||||||
kcContext: KcContextBase.LoginResetPassword;
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
i18n: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LoginResetPassword = memo((props: LoginResetPasswordProps) => {
|
|
||||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
|
||||||
|
|
||||||
const { url, realm, auth } = kcContext;
|
const { url, realm, auth } = kcContext;
|
||||||
|
|
||||||
@ -75,6 +66,4 @@ const LoginResetPassword = memo((props: LoginResetPasswordProps) => {
|
|||||||
infoNode={msg("emailInstruction")}
|
infoNode={msg("emailInstruction")}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default LoginResetPassword;
|
|
@ -1,20 +1,11 @@
|
|||||||
import React, { memo } from "react";
|
import React from "react";
|
||||||
import DefaultTemplate from "./Template";
|
|
||||||
import type { TemplateProps } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { clsx } from "../tools/clsx";
|
import { clsx } from "../tools/clsx";
|
||||||
import type { I18n } from "../i18n";
|
import type { PageProps } from "../KcProps";
|
||||||
|
import type { KcContextBase } from "../getKcContext";
|
||||||
|
import type { I18nBase } from "../i18n";
|
||||||
|
|
||||||
export type LoginUpdatePasswordProps = KcProps & {
|
export default function LoginUpdatePassword(props: PageProps<Extract<KcContextBase, { pageId: "login-update-password.ftl" }>, I18nBase>) {
|
||||||
kcContext: KcContextBase.LoginUpdatePassword;
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
i18n: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LoginUpdatePassword = memo((props: LoginUpdatePasswordProps) => {
|
|
||||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
|
||||||
|
|
||||||
const { msg, msgStr } = i18n;
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
@ -123,6 +114,4 @@ const LoginUpdatePassword = memo((props: LoginUpdatePasswordProps) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default LoginUpdatePassword;
|
|
@ -1,20 +1,11 @@
|
|||||||
import React, { memo } from "react";
|
import React from "react";
|
||||||
import DefaultTemplate from "./Template";
|
|
||||||
import type { TemplateProps } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { clsx } from "../tools/clsx";
|
import { clsx } from "../tools/clsx";
|
||||||
import type { I18n } from "../i18n";
|
import type { PageProps } from "../KcProps";
|
||||||
|
import type { KcContextBase } from "../getKcContext";
|
||||||
|
import type { I18nBase } from "../i18n";
|
||||||
|
|
||||||
export type LoginUpdateProfile = KcProps & {
|
export default function LoginUpdateProfile(props: PageProps<Extract<KcContextBase, { pageId: "login-update-profile.ftl" }>, I18nBase>) {
|
||||||
kcContext: KcContextBase.LoginUpdateProfile;
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
i18n: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LoginUpdateProfile = memo((props: LoginUpdateProfile) => {
|
|
||||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
|
||||||
|
|
||||||
const { msg, msgStr } = i18n;
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
@ -130,6 +121,4 @@ const LoginUpdateProfile = memo((props: LoginUpdateProfile) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default LoginUpdateProfile;
|
|
@ -1,22 +1,13 @@
|
|||||||
import React, { useState, memo } from "react";
|
import React, { useState } from "react";
|
||||||
import DefaultTemplate from "./Template";
|
|
||||||
import type { TemplateProps } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { clsx } from "../tools/clsx";
|
import { clsx } from "../tools/clsx";
|
||||||
import { useConstCallback } from "powerhooks/useConstCallback";
|
import { useConstCallback } from "../tools/useConstCallback";
|
||||||
import type { FormEventHandler } from "react";
|
import type { FormEventHandler } from "react";
|
||||||
import type { I18n } from "../i18n";
|
import type { PageProps } from "../KcProps";
|
||||||
|
import type { KcContextBase } from "../getKcContext";
|
||||||
|
import type { I18nBase } from "../i18n";
|
||||||
|
|
||||||
export type LoginUsernameProps = KcProps & {
|
export default function LoginUsername(props: PageProps<Extract<KcContextBase, { pageId: "login-username.ftl" }>, I18nBase>) {
|
||||||
kcContext: KcContextBase.LoginUsername;
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
i18n: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LoginUsername = memo((props: LoginUsernameProps) => {
|
|
||||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
|
||||||
|
|
||||||
const { social, realm, url, usernameHidden, login, registrationDisabled } = kcContext;
|
const { social, realm, url, usernameHidden, login, registrationDisabled } = kcContext;
|
||||||
|
|
||||||
@ -164,6 +155,4 @@ const LoginUsername = memo((props: LoginUsernameProps) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default LoginUsername;
|
|
@ -1,19 +1,10 @@
|
|||||||
import React, { memo } from "react";
|
import React from "react";
|
||||||
import DefaultTemplate from "./Template";
|
import type { PageProps } from "../KcProps";
|
||||||
import type { TemplateProps } from "./Template";
|
import type { KcContextBase } from "../getKcContext";
|
||||||
import type { KcProps } from "./KcProps";
|
import type { I18nBase } from "../i18n";
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import type { I18n } from "../i18n";
|
|
||||||
|
|
||||||
export type LoginVerifyEmailProps = KcProps & {
|
export default function LoginVerifyEmail(props: PageProps<Extract<KcContextBase, { pageId: "login-verify-email.ftl" }>, I18nBase>) {
|
||||||
kcContext: KcContextBase.LoginVerifyEmail;
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
i18n: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LoginVerifyEmail = memo((props: LoginVerifyEmailProps) => {
|
|
||||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
|
||||||
|
|
||||||
const { msg } = i18n;
|
const { msg } = i18n;
|
||||||
|
|
||||||
@ -38,6 +29,4 @@ const LoginVerifyEmail = memo((props: LoginVerifyEmailProps) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default LoginVerifyEmail;
|
|
@ -1,20 +1,11 @@
|
|||||||
import React, { memo } from "react";
|
import React from "react";
|
||||||
import { clsx } from "../tools/clsx";
|
import { clsx } from "../tools/clsx";
|
||||||
import DefaultTemplate from "./Template";
|
import type { PageProps } from "../KcProps";
|
||||||
import type { TemplateProps } from "./Template";
|
import type { KcContextBase } from "../getKcContext";
|
||||||
import type { KcProps } from "./KcProps";
|
import type { I18nBase } from "../i18n";
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import type { I18n } from "../i18n";
|
|
||||||
|
|
||||||
export type LogoutConfirmProps = KcProps & {
|
export default function LogoutConfirm(props: PageProps<Extract<KcContextBase, { pageId: "logout-confirm.ftl" }>, I18nBase>) {
|
||||||
kcContext: KcContextBase.LogoutConfirm;
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
i18n: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LogoutConfirm = memo((props: LogoutConfirmProps) => {
|
|
||||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
|
||||||
|
|
||||||
const { url, client, logoutConfirm } = kcContext;
|
const { url, client, logoutConfirm } = kcContext;
|
||||||
|
|
||||||
@ -64,6 +55,4 @@ const LogoutConfirm = memo((props: LogoutConfirmProps) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default LogoutConfirm;
|
|
@ -1,20 +1,11 @@
|
|||||||
import React, { memo } from "react";
|
import React from "react";
|
||||||
import DefaultTemplate from "./Template";
|
|
||||||
import type { TemplateProps } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { clsx } from "../tools/clsx";
|
import { clsx } from "../tools/clsx";
|
||||||
import type { I18n } from "../i18n";
|
import type { PageProps } from "../KcProps";
|
||||||
|
import type { KcContextBase } from "../getKcContext";
|
||||||
|
import type { I18nBase } from "../i18n";
|
||||||
|
|
||||||
export type RegisterProps = KcProps & {
|
export default function Register(props: PageProps<Extract<KcContextBase, { pageId: "register.ftl" }>, I18nBase>) {
|
||||||
kcContext: KcContextBase.Register;
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
i18n: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Register = memo((props: RegisterProps) => {
|
|
||||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
|
||||||
|
|
||||||
const { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
const { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
||||||
|
|
||||||
@ -167,6 +158,4 @@ const Register = memo((props: RegisterProps) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default Register;
|
|
@ -1,21 +1,12 @@
|
|||||||
import React, { memo, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import DefaultTemplate from "./Template";
|
|
||||||
import type { TemplateProps } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { clsx } from "../tools/clsx";
|
import { clsx } from "../tools/clsx";
|
||||||
import type { I18n } from "../i18n";
|
|
||||||
import { UserProfileFormFields } from "./shared/UserProfileCommons";
|
import { UserProfileFormFields } from "./shared/UserProfileCommons";
|
||||||
|
import type { PageProps } from "../KcProps";
|
||||||
|
import type { KcContextBase } from "../getKcContext";
|
||||||
|
import type { I18nBase } from "../i18n";
|
||||||
|
|
||||||
export type RegisterUserProfileProps = KcProps & {
|
export default function RegisterUserProfile(props: PageProps<Extract<KcContextBase, { pageId: "register-user-profile.ftl" }>, I18nBase>) {
|
||||||
kcContext: KcContextBase.RegisterUserProfile;
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
i18n: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const RegisterUserProfile = memo((props: RegisterUserProfileProps) => {
|
|
||||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
|
||||||
|
|
||||||
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
||||||
|
|
||||||
@ -66,6 +57,4 @@ const RegisterUserProfile = memo((props: RegisterUserProfileProps) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default RegisterUserProfile;
|
|
@ -1,70 +1,20 @@
|
|||||||
import React, { useEffect, memo } from "react";
|
import React, { useEffect } from "react";
|
||||||
import DefaultTemplate from "./Template";
|
import { memoize } from "../tools/memoize";
|
||||||
import type { TemplateProps } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { clsx } from "../tools/clsx";
|
import { clsx } from "../tools/clsx";
|
||||||
import { Evt } from "evt";
|
import { Evt } from "evt";
|
||||||
import { useRerenderOnStateChange } from "evt/hooks";
|
import { useRerenderOnStateChange } from "evt/hooks";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import { fallbackLanguageTag } from "../i18n";
|
import { fallbackLanguageTag } from "../i18n";
|
||||||
import type { I18n } from "../i18n";
|
import { useConst } from "../tools/useConst";
|
||||||
import memoize from "memoizee";
|
import { useConstCallback } from "../tools/useConstCallback";
|
||||||
import { useConst } from "powerhooks/useConst";
|
|
||||||
import { useConstCallback } from "powerhooks/useConstCallback";
|
|
||||||
import { Markdown } from "../tools/Markdown";
|
import { Markdown } from "../tools/Markdown";
|
||||||
import type { Extends } from "tsafe";
|
import type { Extends } from "tsafe";
|
||||||
|
import type { PageProps } from "../KcProps";
|
||||||
|
import type { KcContextBase } from "../getKcContext";
|
||||||
|
import type { I18nBase } from "../i18n";
|
||||||
|
|
||||||
export const evtTermMarkdown = Evt.create<string | undefined>(undefined);
|
export default function Terms(props: PageProps<Extract<KcContextBase, { pageId: "terms.ftl" }>, I18nBase>) {
|
||||||
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
export type KcContextLike = {
|
|
||||||
pageId: KcContextBase["pageId"];
|
|
||||||
locale?: {
|
|
||||||
currentLanguageTag: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
assert<Extends<KcContextBase, KcContextLike>>();
|
|
||||||
|
|
||||||
/** Allow to avoid bundling the terms and download it on demand*/
|
|
||||||
export function useDownloadTerms(params: {
|
|
||||||
kcContext: KcContextLike;
|
|
||||||
downloadTermMarkdown: (params: { currentLanguageTag: string }) => Promise<string>;
|
|
||||||
}) {
|
|
||||||
const { kcContext } = params;
|
|
||||||
|
|
||||||
const { downloadTermMarkdownMemoized } = (function useClosure() {
|
|
||||||
const { downloadTermMarkdown } = params;
|
|
||||||
|
|
||||||
const downloadTermMarkdownConst = useConstCallback(downloadTermMarkdown);
|
|
||||||
|
|
||||||
const downloadTermMarkdownMemoized = useConst(() =>
|
|
||||||
memoize((currentLanguageTag: string) => downloadTermMarkdownConst({ currentLanguageTag }), { "promise": true })
|
|
||||||
);
|
|
||||||
|
|
||||||
return { downloadTermMarkdownMemoized };
|
|
||||||
})();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (kcContext.pageId !== "terms.ftl") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadTermMarkdownMemoized(kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag).then(
|
|
||||||
thermMarkdown => (evtTermMarkdown.state = thermMarkdown)
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TermsProps = KcProps & {
|
|
||||||
kcContext: KcContextBase.Terms;
|
|
||||||
i18n: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Terms = memo((props: TermsProps) => {
|
|
||||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
|
||||||
|
|
||||||
const { msg, msgStr } = i18n;
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
@ -111,6 +61,45 @@ const Terms = memo((props: TermsProps) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default Terms;
|
export const evtTermMarkdown = Evt.create<string | undefined>(undefined);
|
||||||
|
|
||||||
|
export type KcContextLike = {
|
||||||
|
pageId: KcContextBase["pageId"];
|
||||||
|
locale?: {
|
||||||
|
currentLanguageTag: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<Extends<KcContextBase, KcContextLike>>();
|
||||||
|
|
||||||
|
/** Allow to avoid bundling the terms and download it on demand*/
|
||||||
|
export function useDownloadTerms(params: {
|
||||||
|
kcContext: KcContextLike;
|
||||||
|
downloadTermMarkdown: (params: { currentLanguageTag: string }) => Promise<string>;
|
||||||
|
}) {
|
||||||
|
const { kcContext } = params;
|
||||||
|
|
||||||
|
const { downloadTermMarkdownMemoized } = (function useClosure() {
|
||||||
|
const { downloadTermMarkdown } = params;
|
||||||
|
|
||||||
|
const downloadTermMarkdownConst = useConstCallback(downloadTermMarkdown);
|
||||||
|
|
||||||
|
const downloadTermMarkdownMemoized = useConst(() =>
|
||||||
|
memoize((currentLanguageTag: string) => downloadTermMarkdownConst({ currentLanguageTag }))
|
||||||
|
);
|
||||||
|
|
||||||
|
return { downloadTermMarkdownMemoized };
|
||||||
|
})();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (kcContext.pageId !== "terms.ftl") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadTermMarkdownMemoized(kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag).then(
|
||||||
|
thermMarkdown => (evtTermMarkdown.state = thermMarkdown)
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
}
|
@ -1,21 +1,12 @@
|
|||||||
import React, { useState, memo } from "react";
|
import React, { useState } from "react";
|
||||||
import DefaultTemplate from "./Template";
|
|
||||||
import type { TemplateProps } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { clsx } from "../tools/clsx";
|
import { clsx } from "../tools/clsx";
|
||||||
import type { I18n } from "../i18n";
|
|
||||||
import { UserProfileFormFields } from "./shared/UserProfileCommons";
|
import { UserProfileFormFields } from "./shared/UserProfileCommons";
|
||||||
|
import type { PageProps } from "../KcProps";
|
||||||
|
import type { KcContextBase } from "../getKcContext";
|
||||||
|
import type { I18nBase } from "../i18n";
|
||||||
|
|
||||||
export type UpdateUserProfileProps = KcProps & {
|
export default function UpdateUserProfile(props: PageProps<Extract<KcContextBase, { pageId: "update-user-profile.ftl" }>, I18nBase>) {
|
||||||
kcContext: KcContextBase.UpdateUserProfile;
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
i18n: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const UpdateUserProfile = memo((props: UpdateUserProfileProps) => {
|
|
||||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
|
||||||
|
|
||||||
const { msg, msgStr } = i18n;
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
@ -73,6 +64,4 @@ const UpdateUserProfile = memo((props: UpdateUserProfileProps) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default UpdateUserProfile;
|
|
@ -1,22 +1,14 @@
|
|||||||
import React, { useRef, useState, memo } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
import DefaultTemplate from "./Template";
|
|
||||||
import type { TemplateProps } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { clsx } from "../tools/clsx";
|
import { clsx } from "../tools/clsx";
|
||||||
import type { I18n, MessageKeyBase } from "../i18n";
|
import type { MessageKeyBase } from "../i18n";
|
||||||
import { base64url } from "rfc4648";
|
import { base64url } from "rfc4648";
|
||||||
import { useConstCallback } from "powerhooks/useConstCallback";
|
import { useConstCallback } from "../tools/useConstCallback";
|
||||||
|
import type { PageProps } from "../KcProps";
|
||||||
|
import type { KcContextBase } from "../getKcContext";
|
||||||
|
import type { I18nBase } from "../i18n";
|
||||||
|
|
||||||
export type WebauthnAuthenticateProps = KcProps & {
|
export default function WebauthnAuthenticate(props: PageProps<Extract<KcContextBase, { pageId: "webauthn-authenticate.ftl" }>, I18nBase>) {
|
||||||
kcContext: KcContextBase.WebauthnAuthenticate;
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
i18n: I18n;
|
|
||||||
doFetchDefaultThemeResources?: boolean;
|
|
||||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const WebauthnAuthenticate = memo((props: WebauthnAuthenticateProps) => {
|
|
||||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
|
||||||
|
|
||||||
const { url } = kcContext;
|
const { url } = kcContext;
|
||||||
|
|
||||||
@ -198,6 +190,4 @@ const WebauthnAuthenticate = memo((props: WebauthnAuthenticateProps) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default WebauthnAuthenticate;
|
|
650
src/lib/pages/shared/UserProfileCommons.tsx
Normal file
650
src/lib/pages/shared/UserProfileCommons.tsx
Normal file
@ -0,0 +1,650 @@
|
|||||||
|
import "../../tools/Array.prototype.every";
|
||||||
|
import React, { useEffect, useMemo, useReducer, Fragment } from "react";
|
||||||
|
import type { KcProps } from "../../KcProps";
|
||||||
|
import type { KcContextBase, Validators, Attribute } from "../../getKcContext";
|
||||||
|
import { clsx } from "../../tools/clsx";
|
||||||
|
import { useConstCallback } from "../../tools/useConstCallback";
|
||||||
|
import { emailRegexp } from "../../tools/emailRegExp";
|
||||||
|
import type { I18nBase, MessageKeyBase } from "../../i18n";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
|
||||||
|
export type UserProfileFormFieldsProps = {
|
||||||
|
kcContext: Parameters<typeof useFormValidation>[0]["kcContext"];
|
||||||
|
i18n: I18nBase;
|
||||||
|
} & KcProps &
|
||||||
|
Partial<Record<"BeforeField" | "AfterField", (props: { attribute: Attribute }) => JSX.Element | null>> & {
|
||||||
|
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function UserProfileFormFields({
|
||||||
|
kcContext,
|
||||||
|
onIsFormSubmittableValueChange,
|
||||||
|
i18n,
|
||||||
|
BeforeField,
|
||||||
|
AfterField,
|
||||||
|
...props
|
||||||
|
}: UserProfileFormFieldsProps) {
|
||||||
|
const { advancedMsg } = i18n;
|
||||||
|
|
||||||
|
const {
|
||||||
|
formValidationState: { fieldStateByAttributeName, isFormSubmittable },
|
||||||
|
formValidationDispatch,
|
||||||
|
attributesWithPassword
|
||||||
|
} = useFormValidation({
|
||||||
|
kcContext,
|
||||||
|
i18n
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onIsFormSubmittableValueChange(isFormSubmittable);
|
||||||
|
}, [isFormSubmittable]);
|
||||||
|
|
||||||
|
let currentGroup = "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{attributesWithPassword.map((attribute, i) => {
|
||||||
|
const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute;
|
||||||
|
|
||||||
|
const { value, displayableErrors } = fieldStateByAttributeName[attribute.name];
|
||||||
|
|
||||||
|
const formGroupClassName = clsx(props.kcFormGroupClass, displayableErrors.length !== 0 && props.kcFormGroupErrorClass);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment key={i}>
|
||||||
|
{group !== currentGroup && (currentGroup = group) !== "" && (
|
||||||
|
<div className={formGroupClassName}>
|
||||||
|
<div className={clsx(props.kcContentWrapperClass)}>
|
||||||
|
<label id={`header-${group}`} className={clsx(props.kcFormGroupHeader)}>
|
||||||
|
{advancedMsg(groupDisplayHeader) || currentGroup}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{groupDisplayDescription !== "" && (
|
||||||
|
<div className={clsx(props.kcLabelWrapperClass)}>
|
||||||
|
<label id={`description-${group}`} className={`${clsx(props.kcLabelClass)}`}>
|
||||||
|
{advancedMsg(groupDisplayDescription)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{BeforeField && <BeforeField attribute={attribute} />}
|
||||||
|
|
||||||
|
<div className={formGroupClassName}>
|
||||||
|
<div className={clsx(props.kcLabelWrapperClass)}>
|
||||||
|
<label htmlFor={attribute.name} className={clsx(props.kcLabelClass)}>
|
||||||
|
{advancedMsg(attribute.displayName ?? "")}
|
||||||
|
</label>
|
||||||
|
{attribute.required && <>*</>}
|
||||||
|
</div>
|
||||||
|
<div className={clsx(props.kcInputWrapperClass)}>
|
||||||
|
{(() => {
|
||||||
|
const { options } = attribute.validators;
|
||||||
|
|
||||||
|
if (options !== undefined) {
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
id={attribute.name}
|
||||||
|
name={attribute.name}
|
||||||
|
onChange={event =>
|
||||||
|
formValidationDispatch({
|
||||||
|
"action": "update value",
|
||||||
|
"name": attribute.name,
|
||||||
|
"newValue": event.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onBlur={() =>
|
||||||
|
formValidationDispatch({
|
||||||
|
"action": "focus lost",
|
||||||
|
"name": attribute.name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
value={value}
|
||||||
|
>
|
||||||
|
{options.options.map(option => (
|
||||||
|
<option key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={(() => {
|
||||||
|
switch (attribute.name) {
|
||||||
|
case "password-confirm":
|
||||||
|
case "password":
|
||||||
|
return "password";
|
||||||
|
default:
|
||||||
|
return "text";
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
id={attribute.name}
|
||||||
|
name={attribute.name}
|
||||||
|
value={value}
|
||||||
|
onChange={event =>
|
||||||
|
formValidationDispatch({
|
||||||
|
"action": "update value",
|
||||||
|
"name": attribute.name,
|
||||||
|
"newValue": event.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onBlur={() =>
|
||||||
|
formValidationDispatch({
|
||||||
|
"action": "focus lost",
|
||||||
|
"name": attribute.name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className={clsx(props.kcInputClass)}
|
||||||
|
aria-invalid={displayableErrors.length !== 0}
|
||||||
|
disabled={attribute.readOnly}
|
||||||
|
autoComplete={attribute.autocomplete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
{displayableErrors.length !== 0 &&
|
||||||
|
(() => {
|
||||||
|
const divId = `input-error-${attribute.name}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style>{`#${divId} > span: { display: block; }`}</style>
|
||||||
|
<span
|
||||||
|
id={divId}
|
||||||
|
className={clsx(props.kcInputErrorMessageClass)}
|
||||||
|
style={{
|
||||||
|
"position": displayableErrors.length === 1 ? "absolute" : undefined
|
||||||
|
}}
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
{displayableErrors.map(({ errorMessage }) => errorMessage)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{AfterField && <AfterField attribute={attribute} />}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOTE: The attributesWithPassword returned is actually augmented with
|
||||||
|
* artificial password related attributes only if kcContext.passwordRequired === true
|
||||||
|
*/
|
||||||
|
export function useFormValidation(params: {
|
||||||
|
kcContext: {
|
||||||
|
messagesPerField: Pick<KcContextBase.Common["messagesPerField"], "existsError" | "get">;
|
||||||
|
profile: {
|
||||||
|
attributes: Attribute[];
|
||||||
|
};
|
||||||
|
passwordRequired?: boolean;
|
||||||
|
realm: { registrationEmailAsUsername: boolean };
|
||||||
|
};
|
||||||
|
/** NOTE: Try to avoid passing a new ref every render for better performances. */
|
||||||
|
passwordValidators?: Validators;
|
||||||
|
i18n: I18nBase;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
kcContext,
|
||||||
|
passwordValidators = {
|
||||||
|
"length": {
|
||||||
|
"ignore.empty.value": true,
|
||||||
|
"min": "4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
i18n
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const attributesWithPassword = useMemo(
|
||||||
|
() =>
|
||||||
|
!kcContext.passwordRequired
|
||||||
|
? kcContext.profile.attributes
|
||||||
|
: (() => {
|
||||||
|
const name = kcContext.realm.registrationEmailAsUsername ? "email" : "username";
|
||||||
|
|
||||||
|
return kcContext.profile.attributes.reduce<Attribute[]>(
|
||||||
|
(prev, curr) => [
|
||||||
|
...prev,
|
||||||
|
...(curr.name !== name
|
||||||
|
? [curr]
|
||||||
|
: [
|
||||||
|
curr,
|
||||||
|
id<Attribute>({
|
||||||
|
"name": "password",
|
||||||
|
"displayName": id<`\${${MessageKeyBase}}`>("${password}"),
|
||||||
|
"required": true,
|
||||||
|
"readOnly": false,
|
||||||
|
"validators": passwordValidators,
|
||||||
|
"annotations": {},
|
||||||
|
"groupAnnotations": {},
|
||||||
|
"autocomplete": "new-password"
|
||||||
|
}),
|
||||||
|
id<Attribute>({
|
||||||
|
"name": "password-confirm",
|
||||||
|
"displayName": id<`\${${MessageKeyBase}}`>("${passwordConfirm}"),
|
||||||
|
"required": true,
|
||||||
|
"readOnly": false,
|
||||||
|
"validators": {
|
||||||
|
"_compareToOther": {
|
||||||
|
"name": "password",
|
||||||
|
"ignore.empty.value": true,
|
||||||
|
"shouldBe": "equal",
|
||||||
|
"error-message": id<`\${${MessageKeyBase}}`>("${invalidPasswordConfirmMessage}")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"annotations": {},
|
||||||
|
"groupAnnotations": {},
|
||||||
|
"autocomplete": "new-password"
|
||||||
|
})
|
||||||
|
])
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
})(),
|
||||||
|
[kcContext, passwordValidators]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getErrors } = useGetErrors({
|
||||||
|
"kcContext": {
|
||||||
|
"messagesPerField": kcContext.messagesPerField,
|
||||||
|
"profile": {
|
||||||
|
"attributes": attributesWithPassword
|
||||||
|
}
|
||||||
|
},
|
||||||
|
i18n
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialInternalState = useMemo(
|
||||||
|
() =>
|
||||||
|
Object.fromEntries(
|
||||||
|
attributesWithPassword
|
||||||
|
.map(attribute => ({
|
||||||
|
attribute,
|
||||||
|
"errors": getErrors({
|
||||||
|
"name": attribute.name,
|
||||||
|
"fieldValueByAttributeName": Object.fromEntries(
|
||||||
|
attributesWithPassword.map(({ name, value }) => [name, { "value": value ?? "" }])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
.map(({ attribute, errors }) => [
|
||||||
|
attribute.name,
|
||||||
|
{
|
||||||
|
"value": attribute.value ?? "",
|
||||||
|
errors,
|
||||||
|
"doDisplayPotentialErrorMessages": errors.length !== 0
|
||||||
|
}
|
||||||
|
])
|
||||||
|
),
|
||||||
|
[attributesWithPassword]
|
||||||
|
);
|
||||||
|
|
||||||
|
type InternalState = typeof initialInternalState;
|
||||||
|
|
||||||
|
const [formValidationInternalState, formValidationDispatch] = useReducer(
|
||||||
|
(
|
||||||
|
state: InternalState,
|
||||||
|
params:
|
||||||
|
| {
|
||||||
|
action: "update value";
|
||||||
|
name: string;
|
||||||
|
newValue: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
action: "focus lost";
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
): InternalState => ({
|
||||||
|
...state,
|
||||||
|
[params.name]: {
|
||||||
|
...state[params.name],
|
||||||
|
...(() => {
|
||||||
|
switch (params.action) {
|
||||||
|
case "focus lost":
|
||||||
|
return { "doDisplayPotentialErrorMessages": true };
|
||||||
|
case "update value":
|
||||||
|
return {
|
||||||
|
"value": params.newValue,
|
||||||
|
"errors": getErrors({
|
||||||
|
"name": params.name,
|
||||||
|
"fieldValueByAttributeName": {
|
||||||
|
...state,
|
||||||
|
[params.name]: { "value": params.newValue }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
initialInternalState
|
||||||
|
);
|
||||||
|
|
||||||
|
const formValidationState = useMemo(
|
||||||
|
() => ({
|
||||||
|
"fieldStateByAttributeName": Object.fromEntries(
|
||||||
|
Object.entries(formValidationInternalState).map(([name, { value, errors, doDisplayPotentialErrorMessages }]) => [
|
||||||
|
name,
|
||||||
|
{ value, "displayableErrors": doDisplayPotentialErrorMessages ? errors : [] }
|
||||||
|
])
|
||||||
|
),
|
||||||
|
"isFormSubmittable": Object.entries(formValidationInternalState).every(
|
||||||
|
([name, { value, errors }]) =>
|
||||||
|
errors.length === 0 && (value !== "" || !attributesWithPassword.find(attribute => attribute.name === name)!.required)
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
[formValidationInternalState, attributesWithPassword]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
formValidationState,
|
||||||
|
formValidationDispatch,
|
||||||
|
attributesWithPassword
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Expect to be used in a component wrapped within a <I18nProvider> */
|
||||||
|
function useGetErrors(params: {
|
||||||
|
kcContext: {
|
||||||
|
messagesPerField: Pick<KcContextBase.Common["messagesPerField"], "existsError" | "get">;
|
||||||
|
profile: {
|
||||||
|
attributes: { name: string; value?: string; validators: Validators }[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
i18n: I18nBase;
|
||||||
|
}) {
|
||||||
|
const { kcContext, i18n } = params;
|
||||||
|
|
||||||
|
const {
|
||||||
|
messagesPerField,
|
||||||
|
profile: { attributes }
|
||||||
|
} = kcContext;
|
||||||
|
|
||||||
|
const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;
|
||||||
|
|
||||||
|
const getErrors = useConstCallback((params: { name: string; fieldValueByAttributeName: Record<string, { value: string }> }) => {
|
||||||
|
const { name, fieldValueByAttributeName } = params;
|
||||||
|
|
||||||
|
const { value } = fieldValueByAttributeName[name];
|
||||||
|
|
||||||
|
const { value: defaultValue, validators } = attributes.find(attribute => attribute.name === name)!;
|
||||||
|
|
||||||
|
block: {
|
||||||
|
if (defaultValue !== value) {
|
||||||
|
break block;
|
||||||
|
}
|
||||||
|
|
||||||
|
let doesErrorExist: boolean;
|
||||||
|
|
||||||
|
try {
|
||||||
|
doesErrorExist = messagesPerField.existsError(name);
|
||||||
|
} catch {
|
||||||
|
break block;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doesErrorExist) {
|
||||||
|
break block;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessageStr = messagesPerField.get(name);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"validatorName": undefined,
|
||||||
|
errorMessageStr,
|
||||||
|
"errorMessage": <span key={0}>{errorMessageStr}</span>
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors: {
|
||||||
|
errorMessage: JSX.Element;
|
||||||
|
errorMessageStr: string;
|
||||||
|
validatorName: keyof Validators | undefined;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
scope: {
|
||||||
|
const validatorName = "length";
|
||||||
|
|
||||||
|
const validator = validators[validatorName];
|
||||||
|
|
||||||
|
if (validator === undefined) {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { "ignore.empty.value": ignoreEmptyValue = false, max, min } = validator;
|
||||||
|
|
||||||
|
if (ignoreEmptyValue && value === "") {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (max !== undefined && value.length > parseInt(max)) {
|
||||||
|
const msgArgs = ["error-invalid-length-too-long", max] as const;
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||||
|
"errorMessageStr": msgStr(...msgArgs),
|
||||||
|
validatorName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (min !== undefined && value.length < parseInt(min)) {
|
||||||
|
const msgArgs = ["error-invalid-length-too-short", min] as const;
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||||
|
"errorMessageStr": msgStr(...msgArgs),
|
||||||
|
validatorName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scope: {
|
||||||
|
const validatorName = "_compareToOther";
|
||||||
|
|
||||||
|
const validator = validators[validatorName];
|
||||||
|
|
||||||
|
if (validator === undefined) {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { "ignore.empty.value": ignoreEmptyValue = false, name: otherName, shouldBe, "error-message": errorMessageKey } = validator;
|
||||||
|
|
||||||
|
if (ignoreEmptyValue && value === "") {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { value: otherValue } = fieldValueByAttributeName[otherName];
|
||||||
|
|
||||||
|
const isValid = (() => {
|
||||||
|
switch (shouldBe) {
|
||||||
|
case "different":
|
||||||
|
return otherValue !== value;
|
||||||
|
case "equal":
|
||||||
|
return otherValue === value;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msgArg = [
|
||||||
|
errorMessageKey ??
|
||||||
|
id<MessageKeyBase>(
|
||||||
|
(() => {
|
||||||
|
switch (shouldBe) {
|
||||||
|
case "equal":
|
||||||
|
return "shouldBeEqual";
|
||||||
|
case "different":
|
||||||
|
return "shouldBeDifferent";
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
),
|
||||||
|
otherName,
|
||||||
|
name,
|
||||||
|
shouldBe
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
validatorName,
|
||||||
|
"errorMessage": <Fragment key={errors.length}>{advancedMsg(...msgArg)}</Fragment>,
|
||||||
|
"errorMessageStr": advancedMsgStr(...msgArg)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
scope: {
|
||||||
|
const validatorName = "pattern";
|
||||||
|
|
||||||
|
const validator = validators[validatorName];
|
||||||
|
|
||||||
|
if (validator === undefined) {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { "ignore.empty.value": ignoreEmptyValue = false, pattern, "error-message": errorMessageKey } = validator;
|
||||||
|
|
||||||
|
if (ignoreEmptyValue && value === "") {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new RegExp(pattern).test(value)) {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msgArgs = [errorMessageKey ?? id<MessageKeyBase>("shouldMatchPattern"), pattern] as const;
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
validatorName,
|
||||||
|
"errorMessage": <Fragment key={errors.length}>{advancedMsg(...msgArgs)}</Fragment>,
|
||||||
|
"errorMessageStr": advancedMsgStr(...msgArgs)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
scope: {
|
||||||
|
if ([...errors].reverse()[0]?.validatorName === "pattern") {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validatorName = "email";
|
||||||
|
|
||||||
|
const validator = validators[validatorName];
|
||||||
|
|
||||||
|
if (validator === undefined) {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { "ignore.empty.value": ignoreEmptyValue = false } = validator;
|
||||||
|
|
||||||
|
if (ignoreEmptyValue && value === "") {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emailRegexp.test(value)) {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msgArgs = [id<MessageKeyBase>("invalidEmailMessage")] as const;
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
validatorName,
|
||||||
|
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||||
|
"errorMessageStr": msgStr(...msgArgs)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
scope: {
|
||||||
|
const validatorName = "integer";
|
||||||
|
|
||||||
|
const validator = validators[validatorName];
|
||||||
|
|
||||||
|
if (validator === undefined) {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { "ignore.empty.value": ignoreEmptyValue = false, max, min } = validator;
|
||||||
|
|
||||||
|
if (ignoreEmptyValue && value === "") {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const intValue = parseInt(value);
|
||||||
|
|
||||||
|
if (isNaN(intValue)) {
|
||||||
|
const msgArgs = ["mustBeAnInteger"] as const;
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
validatorName,
|
||||||
|
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||||
|
"errorMessageStr": msgStr(...msgArgs)
|
||||||
|
});
|
||||||
|
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (max !== undefined && intValue > parseInt(max)) {
|
||||||
|
const msgArgs = ["error-number-out-of-range-too-big", max] as const;
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
validatorName,
|
||||||
|
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||||
|
"errorMessageStr": msgStr(...msgArgs)
|
||||||
|
});
|
||||||
|
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (min !== undefined && intValue < parseInt(min)) {
|
||||||
|
const msgArgs = ["error-number-out-of-range-too-small", min] as const;
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
validatorName,
|
||||||
|
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||||
|
"errorMessageStr": msgStr(...msgArgs)
|
||||||
|
});
|
||||||
|
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scope: {
|
||||||
|
const validatorName = "options";
|
||||||
|
|
||||||
|
const validator = validators[validatorName];
|
||||||
|
|
||||||
|
if (validator === undefined) {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === "") {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validator.options.indexOf(value) >= 0) {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msgArgs = [id<MessageKeyBase>("notAValidOption")] as const;
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
validatorName,
|
||||||
|
"errorMessage": <Fragment key={errors.length}>{advancedMsg(...msgArgs)}</Fragment>,
|
||||||
|
"errorMessageStr": advancedMsgStr(...msgArgs)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Implement missing validators.
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { getErrors };
|
||||||
|
}
|
@ -1,4 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/ban-types */
|
|
||||||
import type { FC, ComponentClass } from "react";
|
|
||||||
|
|
||||||
export type ReactComponent<Props extends Record<string, unknown> = {}> = ((props: Props) => ReturnType<FC>) | ComponentClass<Props>;
|
|
1
src/lib/tools/SetOptional.ts
Normal file
1
src/lib/tools/SetOptional.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type SetOptional<T extends Record<string, unknown>, K extends keyof T> = Omit<T, K> & Partial<Record<K, T[K]>>;
|
@ -1,7 +1,44 @@
|
|||||||
import { classnames } from "tss-react/tools/classnames";
|
import { assert } from "tsafe/assert";
|
||||||
import type { Cx } from "tss-react";
|
import { typeGuard } from "tsafe/typeGuard";
|
||||||
|
|
||||||
/** Drop in replacement for https://www.npmjs.com/package/clsx */
|
export type CxArg = undefined | null | string | boolean | Partial<Record<string, boolean | null | undefined>> | readonly CxArg[];
|
||||||
export const clsx: Cx = (...args) => {
|
|
||||||
return classnames(args);
|
export const clsx = (...args: CxArg[]): string => {
|
||||||
|
const len = args.length;
|
||||||
|
let i = 0;
|
||||||
|
let cls = "";
|
||||||
|
for (; i < len; i++) {
|
||||||
|
const arg = args[i];
|
||||||
|
if (arg == null) continue;
|
||||||
|
|
||||||
|
let toAdd;
|
||||||
|
switch (typeof arg) {
|
||||||
|
case "boolean":
|
||||||
|
break;
|
||||||
|
case "object": {
|
||||||
|
if (Array.isArray(arg)) {
|
||||||
|
toAdd = clsx(...arg);
|
||||||
|
} else {
|
||||||
|
assert(!typeGuard<{ length: number }>(arg, false));
|
||||||
|
|
||||||
|
toAdd = "";
|
||||||
|
for (const k in arg) {
|
||||||
|
if (arg[k as string] && k) {
|
||||||
|
toAdd && (toAdd += " ");
|
||||||
|
toAdd += k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
toAdd = arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toAdd) {
|
||||||
|
cls && (cls += " ");
|
||||||
|
cls += toAdd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cls;
|
||||||
};
|
};
|
||||||
|
@ -11,7 +11,7 @@ export function deepAssign(params: { target: Record<string, unknown>; source: Re
|
|||||||
Object.keys(source).forEach(key => {
|
Object.keys(source).forEach(key => {
|
||||||
var dereferencedSource = source[key];
|
var dereferencedSource = source[key];
|
||||||
|
|
||||||
if (target[key] === undefined || !(dereferencedSource instanceof Object)) {
|
if (target[key] === undefined || dereferencedSource instanceof Function || !(dereferencedSource instanceof Object)) {
|
||||||
Object.defineProperty(target, key, {
|
Object.defineProperty(target, key, {
|
||||||
"enumerable": true,
|
"enumerable": true,
|
||||||
"writable": true,
|
"writable": true,
|
||||||
|
55
src/lib/tools/memoize.ts
Normal file
55
src/lib/tools/memoize.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
type SimpleType = number | string | boolean | null | undefined;
|
||||||
|
type FuncWithSimpleParams<T extends SimpleType[], R> = (...args: T) => R;
|
||||||
|
|
||||||
|
export function memoize<T extends SimpleType[], R>(
|
||||||
|
fn: FuncWithSimpleParams<T, R>,
|
||||||
|
options?: {
|
||||||
|
argsLength?: number;
|
||||||
|
max?: number;
|
||||||
|
}
|
||||||
|
): FuncWithSimpleParams<T, R> {
|
||||||
|
const cache = new Map<string, ReturnType<FuncWithSimpleParams<T, R>>>();
|
||||||
|
|
||||||
|
const { argsLength = fn.length, max = Infinity } = options ?? {};
|
||||||
|
|
||||||
|
return ((...args: Parameters<FuncWithSimpleParams<T, R>>) => {
|
||||||
|
const key = JSON.stringify(
|
||||||
|
args
|
||||||
|
.slice(0, argsLength)
|
||||||
|
.map(v => {
|
||||||
|
if (v === null) {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
if (v === undefined) {
|
||||||
|
return "undefined";
|
||||||
|
}
|
||||||
|
switch (typeof v) {
|
||||||
|
case "number":
|
||||||
|
return `number-${v}`;
|
||||||
|
case "string":
|
||||||
|
return `string-${v}`;
|
||||||
|
case "boolean":
|
||||||
|
return `boolean-${v ? "true" : "false"}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join("-sIs9sAslOdeWlEdIos3-")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cache.has(key)) {
|
||||||
|
return cache.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (max === cache.size) {
|
||||||
|
for (const key of cache.keys()) {
|
||||||
|
cache.delete(key);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = fn(...args);
|
||||||
|
|
||||||
|
cache.set(key, value);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}) as any;
|
||||||
|
}
|
45
src/lib/tools/useCallbackFactory.ts
Normal file
45
src/lib/tools/useCallbackFactory.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import { memoize } from "./memoize";
|
||||||
|
|
||||||
|
export type CallbackFactory<FactoryArgs extends unknown[], Args extends unknown[], R> = (...factoryArgs: FactoryArgs) => (...args: Args) => R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://docs.powerhooks.dev/api-reference/usecallbackfactory
|
||||||
|
*
|
||||||
|
* const callbackFactory= useCallbackFactory(
|
||||||
|
* ([key]: [string], [params]: [{ foo: number; }]) => {
|
||||||
|
* ...
|
||||||
|
* },
|
||||||
|
* []
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* WARNING: Factory args should not be of variable length.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export function useCallbackFactory<FactoryArgs extends (string | number | boolean)[], Args extends unknown[], R = void>(
|
||||||
|
callback: (...callbackArgs: [FactoryArgs, Args]) => R
|
||||||
|
): CallbackFactory<FactoryArgs, Args, R> {
|
||||||
|
type Out = CallbackFactory<FactoryArgs, Args, R>;
|
||||||
|
|
||||||
|
const callbackRef = useRef<typeof callback>(callback);
|
||||||
|
|
||||||
|
callbackRef.current = callback;
|
||||||
|
|
||||||
|
const memoizedRef = useRef<Out | undefined>(undefined);
|
||||||
|
|
||||||
|
return useState(() =>
|
||||||
|
id<Out>((...factoryArgs) => {
|
||||||
|
if (memoizedRef.current === undefined) {
|
||||||
|
memoizedRef.current = memoize(
|
||||||
|
(...factoryArgs: FactoryArgs) =>
|
||||||
|
(...args: Args) =>
|
||||||
|
callbackRef.current(factoryArgs, args),
|
||||||
|
{ "argsLength": factoryArgs.length }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return memoizedRef.current(...factoryArgs);
|
||||||
|
})
|
||||||
|
)[0];
|
||||||
|
}
|
10
src/lib/tools/useConst.ts
Normal file
10
src/lib/tools/useConst.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute a value on first render and never again,
|
||||||
|
* Equivalent of const [x] = useState(()=> ...)
|
||||||
|
*/
|
||||||
|
export function useConst<T>(getValue: () => T): T {
|
||||||
|
const [value] = useState(getValue);
|
||||||
|
return value;
|
||||||
|
}
|
15
src/lib/tools/useConstCallback.ts
Normal file
15
src/lib/tools/useConstCallback.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { Parameters } from "tsafe/Parameters";
|
||||||
|
|
||||||
|
/** https://stackoverflow.com/questions/65890278/why-cant-usecallback-always-return-the-same-ref */
|
||||||
|
export function useConstCallback<T extends ((...args: any[]) => unknown) | undefined | null>(callback: NonNullable<T>): T {
|
||||||
|
const callbackRef = useRef<typeof callback>(null as any);
|
||||||
|
|
||||||
|
callbackRef.current = callback;
|
||||||
|
|
||||||
|
return useState(
|
||||||
|
() =>
|
||||||
|
(...args: Parameters<T>) =>
|
||||||
|
callbackRef.current(...args)
|
||||||
|
)[0] as T;
|
||||||
|
}
|
@ -1,11 +1,185 @@
|
|||||||
import "./tools/Array.prototype.every";
|
import "./tools/Array.prototype.every";
|
||||||
import React, { useMemo, useReducer, Fragment } from "react";
|
import React, { useMemo, useReducer, Fragment } from "react";
|
||||||
import type { KcContextBase, Validators, Attribute } from "./getKcContext/KcContextBase";
|
import type { KcContextBase, Validators, Attribute } from "./getKcContext/KcContextBase";
|
||||||
import type { I18n, MessageKeyBase } from "./i18n";
|
import type { I18nBase, MessageKeyBase } from "./i18n";
|
||||||
import { useConstCallback } from "powerhooks/useConstCallback";
|
import { useConstCallback } from "./tools/useConstCallback";
|
||||||
import { id } from "tsafe/id";
|
import { id } from "tsafe/id";
|
||||||
import { emailRegexp } from "./tools/emailRegExp";
|
import { emailRegexp } from "./tools/emailRegExp";
|
||||||
|
|
||||||
|
/** @deprecated: Will be removed in the next major. Use this instead:
|
||||||
|
* import { useFormValidation } from "keycloakify/lib/pages/shares/UserProfileCommons";
|
||||||
|
*
|
||||||
|
* The API is the same only the returned value formValidationReducer have been renamed formValidationDispatch
|
||||||
|
* (a it should have been named from the beginning 😬)
|
||||||
|
*/
|
||||||
|
export function useFormValidationSlice(params: {
|
||||||
|
kcContext: {
|
||||||
|
messagesPerField: Pick<KcContextBase.Common["messagesPerField"], "existsError" | "get">;
|
||||||
|
profile: {
|
||||||
|
attributes: Attribute[];
|
||||||
|
};
|
||||||
|
passwordRequired?: boolean;
|
||||||
|
realm: { registrationEmailAsUsername: boolean };
|
||||||
|
};
|
||||||
|
/** NOTE: Try to avoid passing a new ref every render for better performances. */
|
||||||
|
passwordValidators?: Validators;
|
||||||
|
i18n: I18nBase;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
kcContext,
|
||||||
|
passwordValidators = {
|
||||||
|
"length": {
|
||||||
|
"ignore.empty.value": true,
|
||||||
|
"min": "4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
i18n
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const attributesWithPassword = useMemo(
|
||||||
|
() =>
|
||||||
|
!kcContext.passwordRequired
|
||||||
|
? kcContext.profile.attributes
|
||||||
|
: (() => {
|
||||||
|
const name = kcContext.realm.registrationEmailAsUsername ? "email" : "username";
|
||||||
|
|
||||||
|
return kcContext.profile.attributes.reduce<Attribute[]>(
|
||||||
|
(prev, curr) => [
|
||||||
|
...prev,
|
||||||
|
...(curr.name !== name
|
||||||
|
? [curr]
|
||||||
|
: [
|
||||||
|
curr,
|
||||||
|
id<Attribute>({
|
||||||
|
"name": "password",
|
||||||
|
"displayName": id<`\${${MessageKeyBase}}`>("${password}"),
|
||||||
|
"required": true,
|
||||||
|
"readOnly": false,
|
||||||
|
"validators": passwordValidators,
|
||||||
|
"annotations": {},
|
||||||
|
"groupAnnotations": {},
|
||||||
|
"autocomplete": "new-password"
|
||||||
|
}),
|
||||||
|
id<Attribute>({
|
||||||
|
"name": "password-confirm",
|
||||||
|
"displayName": id<`\${${MessageKeyBase}}`>("${passwordConfirm}"),
|
||||||
|
"required": true,
|
||||||
|
"readOnly": false,
|
||||||
|
"validators": {
|
||||||
|
"_compareToOther": {
|
||||||
|
"name": "password",
|
||||||
|
"ignore.empty.value": true,
|
||||||
|
"shouldBe": "equal",
|
||||||
|
"error-message": id<`\${${MessageKeyBase}}`>("${invalidPasswordConfirmMessage}")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"annotations": {},
|
||||||
|
"groupAnnotations": {},
|
||||||
|
"autocomplete": "new-password"
|
||||||
|
})
|
||||||
|
])
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
})(),
|
||||||
|
[kcContext, passwordValidators]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getErrors } = useGetErrors({
|
||||||
|
"kcContext": {
|
||||||
|
"messagesPerField": kcContext.messagesPerField,
|
||||||
|
"profile": {
|
||||||
|
"attributes": attributesWithPassword
|
||||||
|
}
|
||||||
|
},
|
||||||
|
i18n
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialInternalState = useMemo(
|
||||||
|
() =>
|
||||||
|
Object.fromEntries(
|
||||||
|
attributesWithPassword
|
||||||
|
.map(attribute => ({
|
||||||
|
attribute,
|
||||||
|
"errors": getErrors({
|
||||||
|
"name": attribute.name,
|
||||||
|
"fieldValueByAttributeName": Object.fromEntries(
|
||||||
|
attributesWithPassword.map(({ name, value }) => [name, { "value": value ?? "" }])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
.map(({ attribute, errors }) => [
|
||||||
|
attribute.name,
|
||||||
|
{
|
||||||
|
"value": attribute.value ?? "",
|
||||||
|
errors,
|
||||||
|
"doDisplayPotentialErrorMessages": errors.length !== 0
|
||||||
|
}
|
||||||
|
])
|
||||||
|
),
|
||||||
|
[attributesWithPassword]
|
||||||
|
);
|
||||||
|
|
||||||
|
type InternalState = typeof initialInternalState;
|
||||||
|
|
||||||
|
const [formValidationInternalState, formValidationReducer] = useReducer(
|
||||||
|
(
|
||||||
|
state: InternalState,
|
||||||
|
params:
|
||||||
|
| {
|
||||||
|
action: "update value";
|
||||||
|
name: string;
|
||||||
|
newValue: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
action: "focus lost";
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
): InternalState => ({
|
||||||
|
...state,
|
||||||
|
[params.name]: {
|
||||||
|
...state[params.name],
|
||||||
|
...(() => {
|
||||||
|
switch (params.action) {
|
||||||
|
case "focus lost":
|
||||||
|
return { "doDisplayPotentialErrorMessages": true };
|
||||||
|
case "update value":
|
||||||
|
return {
|
||||||
|
"value": params.newValue,
|
||||||
|
"errors": getErrors({
|
||||||
|
"name": params.name,
|
||||||
|
"fieldValueByAttributeName": {
|
||||||
|
...state,
|
||||||
|
[params.name]: { "value": params.newValue }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
initialInternalState
|
||||||
|
);
|
||||||
|
|
||||||
|
const formValidationState = useMemo(
|
||||||
|
() => ({
|
||||||
|
"fieldStateByAttributeName": Object.fromEntries(
|
||||||
|
Object.entries(formValidationInternalState).map(([name, { value, errors, doDisplayPotentialErrorMessages }]) => [
|
||||||
|
name,
|
||||||
|
{ value, "displayableErrors": doDisplayPotentialErrorMessages ? errors : [] }
|
||||||
|
])
|
||||||
|
),
|
||||||
|
"isFormSubmittable": Object.entries(formValidationInternalState).every(
|
||||||
|
([name, { value, errors }]) =>
|
||||||
|
errors.length === 0 && (value !== "" || !attributesWithPassword.find(attribute => attribute.name === name)!.required)
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
[formValidationInternalState, attributesWithPassword]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { formValidationState, formValidationReducer, attributesWithPassword };
|
||||||
|
}
|
||||||
|
|
||||||
/** Expect to be used in a component wrapped within a <I18nProvider> */
|
/** Expect to be used in a component wrapped within a <I18nProvider> */
|
||||||
export function useGetErrors(params: {
|
export function useGetErrors(params: {
|
||||||
kcContext: {
|
kcContext: {
|
||||||
@ -14,7 +188,7 @@ export function useGetErrors(params: {
|
|||||||
attributes: { name: string; value?: string; validators: Validators }[];
|
attributes: { name: string; value?: string; validators: Validators }[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
i18n: I18n;
|
i18n: I18nBase;
|
||||||
}) {
|
}) {
|
||||||
const { kcContext, i18n } = params;
|
const { kcContext, i18n } = params;
|
||||||
|
|
||||||
@ -303,175 +477,3 @@ export function useGetErrors(params: {
|
|||||||
|
|
||||||
return { getErrors };
|
return { getErrors };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* NOTE: The attributesWithPassword returned is actually augmented with
|
|
||||||
* artificial password related attributes only if kcContext.passwordRequired === true
|
|
||||||
*/
|
|
||||||
export function useFormValidationSlice(params: {
|
|
||||||
kcContext: {
|
|
||||||
messagesPerField: Pick<KcContextBase.Common["messagesPerField"], "existsError" | "get">;
|
|
||||||
profile: {
|
|
||||||
attributes: Attribute[];
|
|
||||||
};
|
|
||||||
passwordRequired?: boolean;
|
|
||||||
realm: { registrationEmailAsUsername: boolean };
|
|
||||||
};
|
|
||||||
/** NOTE: Try to avoid passing a new ref every render for better performances. */
|
|
||||||
passwordValidators?: Validators;
|
|
||||||
i18n: I18n;
|
|
||||||
}) {
|
|
||||||
const {
|
|
||||||
kcContext,
|
|
||||||
passwordValidators = {
|
|
||||||
"length": {
|
|
||||||
"ignore.empty.value": true,
|
|
||||||
"min": "4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
i18n
|
|
||||||
} = params;
|
|
||||||
|
|
||||||
const attributesWithPassword = useMemo(
|
|
||||||
() =>
|
|
||||||
!kcContext.passwordRequired
|
|
||||||
? kcContext.profile.attributes
|
|
||||||
: (() => {
|
|
||||||
const name = kcContext.realm.registrationEmailAsUsername ? "email" : "username";
|
|
||||||
|
|
||||||
return kcContext.profile.attributes.reduce<Attribute[]>(
|
|
||||||
(prev, curr) => [
|
|
||||||
...prev,
|
|
||||||
...(curr.name !== name
|
|
||||||
? [curr]
|
|
||||||
: [
|
|
||||||
curr,
|
|
||||||
id<Attribute>({
|
|
||||||
"name": "password",
|
|
||||||
"displayName": id<`\${${MessageKeyBase}}`>("${password}"),
|
|
||||||
"required": true,
|
|
||||||
"readOnly": false,
|
|
||||||
"validators": passwordValidators,
|
|
||||||
"annotations": {},
|
|
||||||
"groupAnnotations": {},
|
|
||||||
"autocomplete": "new-password"
|
|
||||||
}),
|
|
||||||
id<Attribute>({
|
|
||||||
"name": "password-confirm",
|
|
||||||
"displayName": id<`\${${MessageKeyBase}}`>("${passwordConfirm}"),
|
|
||||||
"required": true,
|
|
||||||
"readOnly": false,
|
|
||||||
"validators": {
|
|
||||||
"_compareToOther": {
|
|
||||||
"name": "password",
|
|
||||||
"ignore.empty.value": true,
|
|
||||||
"shouldBe": "equal",
|
|
||||||
"error-message": id<`\${${MessageKeyBase}}`>("${invalidPasswordConfirmMessage}")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"annotations": {},
|
|
||||||
"groupAnnotations": {},
|
|
||||||
"autocomplete": "new-password"
|
|
||||||
})
|
|
||||||
])
|
|
||||||
],
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
})(),
|
|
||||||
[kcContext, passwordValidators]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { getErrors } = useGetErrors({
|
|
||||||
"kcContext": {
|
|
||||||
"messagesPerField": kcContext.messagesPerField,
|
|
||||||
"profile": {
|
|
||||||
"attributes": attributesWithPassword
|
|
||||||
}
|
|
||||||
},
|
|
||||||
i18n
|
|
||||||
});
|
|
||||||
|
|
||||||
const initialInternalState = useMemo(
|
|
||||||
() =>
|
|
||||||
Object.fromEntries(
|
|
||||||
attributesWithPassword
|
|
||||||
.map(attribute => ({
|
|
||||||
attribute,
|
|
||||||
"errors": getErrors({
|
|
||||||
"name": attribute.name,
|
|
||||||
"fieldValueByAttributeName": Object.fromEntries(
|
|
||||||
attributesWithPassword.map(({ name, value }) => [name, { "value": value ?? "" }])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
.map(({ attribute, errors }) => [
|
|
||||||
attribute.name,
|
|
||||||
{
|
|
||||||
"value": attribute.value ?? "",
|
|
||||||
errors,
|
|
||||||
"doDisplayPotentialErrorMessages": errors.length !== 0
|
|
||||||
}
|
|
||||||
])
|
|
||||||
),
|
|
||||||
[attributesWithPassword]
|
|
||||||
);
|
|
||||||
|
|
||||||
type InternalState = typeof initialInternalState;
|
|
||||||
|
|
||||||
const [formValidationInternalState, formValidationReducer] = useReducer(
|
|
||||||
(
|
|
||||||
state: InternalState,
|
|
||||||
params:
|
|
||||||
| {
|
|
||||||
action: "update value";
|
|
||||||
name: string;
|
|
||||||
newValue: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
action: "focus lost";
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
): InternalState => ({
|
|
||||||
...state,
|
|
||||||
[params.name]: {
|
|
||||||
...state[params.name],
|
|
||||||
...(() => {
|
|
||||||
switch (params.action) {
|
|
||||||
case "focus lost":
|
|
||||||
return { "doDisplayPotentialErrorMessages": true };
|
|
||||||
case "update value":
|
|
||||||
return {
|
|
||||||
"value": params.newValue,
|
|
||||||
"errors": getErrors({
|
|
||||||
"name": params.name,
|
|
||||||
"fieldValueByAttributeName": {
|
|
||||||
...state,
|
|
||||||
[params.name]: { "value": params.newValue }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
initialInternalState
|
|
||||||
);
|
|
||||||
|
|
||||||
const formValidationState = useMemo(
|
|
||||||
() => ({
|
|
||||||
"fieldStateByAttributeName": Object.fromEntries(
|
|
||||||
Object.entries(formValidationInternalState).map(([name, { value, errors, doDisplayPotentialErrorMessages }]) => [
|
|
||||||
name,
|
|
||||||
{ value, "displayableErrors": doDisplayPotentialErrorMessages ? errors : [] }
|
|
||||||
])
|
|
||||||
),
|
|
||||||
"isFormSubmittable": Object.entries(formValidationInternalState).every(
|
|
||||||
([name, { value, errors }]) =>
|
|
||||||
errors.length === 0 && (value !== "" || !attributesWithPassword.find(attribute => attribute.name === name)!.required)
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
[formValidationInternalState, attributesWithPassword]
|
|
||||||
);
|
|
||||||
|
|
||||||
return { formValidationState, formValidationReducer, attributesWithPassword };
|
|
||||||
}
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import "minimal-polyfills/Object.fromEntries";
|
import "minimal-polyfills/Object.fromEntries";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
||||||
import { crawl } from "./tools/crawl";
|
import { crawl } from "../bin/tools/crawl";
|
||||||
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
|
import { downloadBuiltinKeycloakTheme } from "../bin/download-builtin-keycloak-theme";
|
||||||
import { getProjectRoot } from "./tools/getProjectRoot";
|
import { getProjectRoot } from "../bin/tools/getProjectRoot";
|
||||||
import { getCliOptions } from "./tools/cliOptions";
|
import { getCliOptions } from "../bin/tools/cliOptions";
|
||||||
import { getLogger } from "./tools/logger";
|
import { getLogger } from "../bin/tools/logger";
|
||||||
|
|
||||||
//NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
|
//NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
|
||||||
// update the version array for generating for newer version.
|
// update the version array for generating for newer version.
|
140
src/scripts/link-in-app.ts
Normal file
140
src/scripts/link-in-app.ts
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import { execSync } from "child_process";
|
||||||
|
import { join as pathJoin, relative as pathRelative } from "path";
|
||||||
|
import * as fs from "fs";
|
||||||
|
|
||||||
|
const singletonDependencies: string[] = ["react", "@types/react"];
|
||||||
|
|
||||||
|
const rootDirPath = pathJoin(__dirname, "..", "..");
|
||||||
|
|
||||||
|
//NOTE: This is only required because of: https://github.com/garronej/ts-ci/blob/c0e207b9677523d4ec97fe672ddd72ccbb3c1cc4/README.md?plain=1#L54-L58
|
||||||
|
fs.writeFileSync(
|
||||||
|
pathJoin(rootDirPath, "dist", "package.json"),
|
||||||
|
Buffer.from(
|
||||||
|
JSON.stringify(
|
||||||
|
(() => {
|
||||||
|
const packageJsonParsed = JSON.parse(fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8"));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...packageJsonParsed,
|
||||||
|
"main": packageJsonParsed["main"]?.replace(/^dist\//, ""),
|
||||||
|
"types": packageJsonParsed["types"]?.replace(/^dist\//, ""),
|
||||||
|
"module": packageJsonParsed["module"]?.replace(/^dist\//, ""),
|
||||||
|
"exports": !("exports" in packageJsonParsed)
|
||||||
|
? undefined
|
||||||
|
: Object.fromEntries(
|
||||||
|
Object.entries(packageJsonParsed["exports"]).map(([key, value]) => [
|
||||||
|
key,
|
||||||
|
(value as string).replace(/^\.\/dist\//, "./")
|
||||||
|
])
|
||||||
|
)
|
||||||
|
};
|
||||||
|
})(),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const commonThirdPartyDeps = (() => {
|
||||||
|
// For example [ "@emotion" ] it's more convenient than
|
||||||
|
// having to list every sub emotion packages (@emotion/css @emotion/utils ...)
|
||||||
|
// in singletonDependencies
|
||||||
|
const namespaceSingletonDependencies: string[] = [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
...namespaceSingletonDependencies
|
||||||
|
.map(namespaceModuleName =>
|
||||||
|
fs
|
||||||
|
.readdirSync(pathJoin(rootDirPath, "node_modules", namespaceModuleName))
|
||||||
|
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
|
||||||
|
)
|
||||||
|
.reduce((prev, curr) => [...prev, ...curr], []),
|
||||||
|
...singletonDependencies
|
||||||
|
];
|
||||||
|
})();
|
||||||
|
|
||||||
|
const yarnGlobalDirPath = pathJoin(rootDirPath, ".yarn_home");
|
||||||
|
|
||||||
|
fs.rmSync(yarnGlobalDirPath, { "recursive": true, "force": true });
|
||||||
|
fs.mkdirSync(yarnGlobalDirPath);
|
||||||
|
|
||||||
|
const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
|
||||||
|
const { targetModuleName, cwd } = params;
|
||||||
|
|
||||||
|
const cmd = ["yarn", "link", ...(targetModuleName !== undefined ? [targetModuleName] : ["--no-bin-links"])].join(" ");
|
||||||
|
|
||||||
|
console.log(`$ cd ${pathRelative(rootDirPath, cwd) || "."} && ${cmd}`);
|
||||||
|
|
||||||
|
execSync(cmd, {
|
||||||
|
cwd,
|
||||||
|
"env": {
|
||||||
|
...process.env,
|
||||||
|
"HOME": yarnGlobalDirPath
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const testAppPaths = (() => {
|
||||||
|
const [, , ...testAppNames] = process.argv;
|
||||||
|
|
||||||
|
return testAppNames
|
||||||
|
.map(testAppName => {
|
||||||
|
const testAppPath = pathJoin(rootDirPath, "..", testAppName);
|
||||||
|
|
||||||
|
if (fs.existsSync(testAppPath)) {
|
||||||
|
return testAppPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(`Skipping ${testAppName} since it cant be found here: ${testAppPath}`);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
.filter((path): path is string => path !== undefined);
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (testAppPaths.length === 0) {
|
||||||
|
console.error("No test app to link into!");
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
testAppPaths.forEach(testAppPath => execSync("yarn install", { "cwd": testAppPath }));
|
||||||
|
|
||||||
|
console.log("=== Linking common dependencies ===");
|
||||||
|
|
||||||
|
const total = commonThirdPartyDeps.length;
|
||||||
|
let current = 0;
|
||||||
|
|
||||||
|
commonThirdPartyDeps.forEach(commonThirdPartyDep => {
|
||||||
|
current++;
|
||||||
|
|
||||||
|
console.log(`${current}/${total} ${commonThirdPartyDep}`);
|
||||||
|
|
||||||
|
const localInstallPath = pathJoin(
|
||||||
|
...[rootDirPath, "node_modules", ...(commonThirdPartyDep.startsWith("@") ? commonThirdPartyDep.split("/") : [commonThirdPartyDep])]
|
||||||
|
);
|
||||||
|
|
||||||
|
execYarnLink({ "cwd": localInstallPath });
|
||||||
|
});
|
||||||
|
|
||||||
|
commonThirdPartyDeps.forEach(commonThirdPartyDep =>
|
||||||
|
testAppPaths.forEach(testAppPath =>
|
||||||
|
execYarnLink({
|
||||||
|
"cwd": testAppPath,
|
||||||
|
"targetModuleName": commonThirdPartyDep
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("=== Linking in house dependencies ===");
|
||||||
|
|
||||||
|
execYarnLink({ "cwd": pathJoin(rootDirPath, "dist") });
|
||||||
|
|
||||||
|
testAppPaths.forEach(testAppPath =>
|
||||||
|
execYarnLink({
|
||||||
|
"cwd": testAppPath,
|
||||||
|
"targetModuleName": JSON.parse(fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8"))["name"]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export {};
|
Reference in New Issue
Block a user