Compare commits
82 Commits
v10.0.0-rc
...
v10.0.0-rc
Author | SHA1 | Date | |
---|---|---|---|
fd67f2402a | |||
60a65ede2f | |||
1fa659ce61 | |||
0ab903dbc7 | |||
70b0a04793 | |||
c0df9aa939 | |||
60a1886942 | |||
1ebf97871b | |||
72e321aa32 | |||
b0f602b565 | |||
84c774503d | |||
9bbc7cc651 | |||
458083fb6d | |||
8dcfc840b4 | |||
9d06a3a6ad | |||
86cd08b954 | |||
144c3cc082 | |||
802cef41a6 | |||
e128e8f0a9 | |||
8a25b93ab2 | |||
7a040935e9 | |||
2015882688 | |||
379301eb9d | |||
5d86b05cdb | |||
73c99d3157 | |||
acba197c94 | |||
2441d8ed8a | |||
9c123f37c8 | |||
b48dbd99cf | |||
25c8599d8f | |||
3453a17c15 | |||
6e95dacd3a | |||
a286e252e9 | |||
a8997e92c3 | |||
89137153a0 | |||
e3382de8e0 | |||
1a48681591 | |||
8f006f0009 | |||
77e32aad2a | |||
8d365dae53 | |||
01fb89674c | |||
e3144adc61 | |||
c9fb0ca6ae | |||
82d7e1371e | |||
e1341dfdba | |||
7f917311d8 | |||
2bfb856f07 | |||
702f52f1c9 | |||
7ba8649940 | |||
485ca28a29 | |||
33460afaf2 | |||
2421ac2c11 | |||
f0cdb0b80b | |||
2af953927e | |||
dcb9fbd0f7 | |||
5bc1f6479d | |||
f3e4bca468 | |||
54645f5cff | |||
a7f3e00821 | |||
108c281b0c | |||
58892cbb56 | |||
dae1053ca8 | |||
83a9778c30 | |||
c52157bfb9 | |||
62bf846d5f | |||
148f7fa316 | |||
f488327885 | |||
593b929254 | |||
9218e97315 | |||
beb0e8bd77 | |||
cace66e9f8 | |||
ef850c71fd | |||
aa8dc1919f | |||
c7c9b19853 | |||
68c26e0f5b | |||
6bcdf286ef | |||
d9345396e8 | |||
4c423900d4 | |||
504419b26d | |||
6e058eafed | |||
08fc9d8631 | |||
e8a11991a0 |
@ -7,13 +7,11 @@
|
|||||||
background-color: #393939;
|
background-color: #393939;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
body.sb-show-preparing-docs > .sb-wrapper {
|
body.sb-show-preparing-docs > .sb-wrapper {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
body .sb-preparing-story {
|
body .sb-preparing-story {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "keycloakify",
|
"name": "keycloakify",
|
||||||
"version": "10.0.0-rc.27",
|
"version": "10.0.0-rc.47",
|
||||||
"description": "Create Keycloak themes using React",
|
"description": "Create Keycloak themes using React",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -44,6 +44,7 @@
|
|||||||
"dist/bin/shared/constants.js",
|
"dist/bin/shared/constants.js",
|
||||||
"dist/bin/shared/constants.d.ts",
|
"dist/bin/shared/constants.d.ts",
|
||||||
"dist/bin/shared/constants.js.map",
|
"dist/bin/shared/constants.js.map",
|
||||||
|
"dist/bin/shared/buildContext.d.ts",
|
||||||
"!dist/vite-plugin/",
|
"!dist/vite-plugin/",
|
||||||
"dist/vite-plugin/index.d.ts",
|
"dist/vite-plugin/index.d.ts",
|
||||||
"dist/vite-plugin/vite-plugin.d.ts",
|
"dist/vite-plugin/vite-plugin.d.ts",
|
||||||
@ -65,7 +66,6 @@
|
|||||||
"react": "*"
|
"react": "*"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimal-polyfills": "^2.2.3",
|
|
||||||
"react-markdown": "^5.0.3",
|
"react-markdown": "^5.0.3",
|
||||||
"tsafe": "^1.6.6"
|
"tsafe": "^1.6.6"
|
||||||
},
|
},
|
||||||
@ -118,7 +118,7 @@
|
|||||||
"tss-react": "^4.9.10",
|
"tss-react": "^4.9.10",
|
||||||
"typescript": "^4.9.1-beta",
|
"typescript": "^4.9.1-beta",
|
||||||
"vite": "^5.2.11",
|
"vite": "^5.2.11",
|
||||||
"vitest": "^0.29.8",
|
"vitest": "^1.6.0",
|
||||||
"yauzl": "^2.10.0",
|
"yauzl": "^2.10.0",
|
||||||
"zod": "^3.17.10",
|
"zod": "^3.17.10",
|
||||||
"evt": "^2.5.7"
|
"evt": "^2.5.7"
|
||||||
|
@ -52,7 +52,25 @@ transformCodebase({
|
|||||||
|
|
||||||
fs.rmSync(join("dist", "ncc_out"), { recursive: true });
|
fs.rmSync(join("dist", "ncc_out"), { recursive: true });
|
||||||
|
|
||||||
patchDeprecatedBufferApiUsage(join("dist", "bin", "main.js"));
|
{
|
||||||
|
let hasBeenPatched = false;
|
||||||
|
|
||||||
|
fs.readdirSync(join("dist", "bin")).forEach(fileBasename => {
|
||||||
|
if (fileBasename !== "main.js" && !fileBasename.endsWith(".index.js")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hasBeenPatched: hasBeenPatched_i } = patchDeprecatedBufferApiUsage(
|
||||||
|
join("dist", "bin", fileBasename)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasBeenPatched_i) {
|
||||||
|
hasBeenPatched = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(hasBeenPatched);
|
||||||
|
}
|
||||||
|
|
||||||
fs.chmodSync(
|
fs.chmodSync(
|
||||||
join("dist", "bin", "main.js"),
|
join("dist", "bin", "main.js"),
|
||||||
@ -93,6 +111,10 @@ run(
|
|||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
fs.readdirSync(join("dist", "ncc_out")).forEach(fileBasename =>
|
||||||
|
assert(!fileBasename.endsWith(".index.js"))
|
||||||
|
);
|
||||||
|
|
||||||
transformCodebase({
|
transformCodebase({
|
||||||
srcDirPath: join("dist", "ncc_out"),
|
srcDirPath: join("dist", "ncc_out"),
|
||||||
destDirPath: join("dist", "vite-plugin"),
|
destDirPath: join("dist", "vite-plugin"),
|
||||||
@ -105,12 +127,30 @@ transformCodebase({
|
|||||||
|
|
||||||
fs.rmSync(join("dist", "ncc_out"), { recursive: true });
|
fs.rmSync(join("dist", "ncc_out"), { recursive: true });
|
||||||
|
|
||||||
patchDeprecatedBufferApiUsage(join("dist", "vite-plugin", "index.js"));
|
{
|
||||||
|
const { hasBeenPatched } = patchDeprecatedBufferApiUsage(
|
||||||
|
join("dist", "vite-plugin", "index.js")
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(hasBeenPatched);
|
||||||
|
}
|
||||||
|
|
||||||
fs.rmSync(join("dist", "src"), { recursive: true, force: true });
|
fs.rmSync(join("dist", "src"), { recursive: true, force: true });
|
||||||
|
|
||||||
fs.cpSync("src", join("dist", "src"), { recursive: true });
|
fs.cpSync("src", join("dist", "src"), { recursive: true });
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
srcDirPath: join("stories"),
|
||||||
|
destDirPath: join("dist", "stories"),
|
||||||
|
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||||
|
if (!fileRelativePath.endsWith(".stories.tsx")) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { modifiedSourceCode: sourceCode };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
console.log(chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`));
|
console.log(chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`));
|
||||||
|
|
||||||
function run(command: string) {
|
function run(command: string) {
|
||||||
@ -127,7 +167,9 @@ function patchDeprecatedBufferApiUsage(filePath: string) {
|
|||||||
`var buffer = Buffer.allocUnsafe ? Buffer.allocUnsafe(toRead) : new Buffer(toRead);`
|
`var buffer = Buffer.allocUnsafe ? Buffer.allocUnsafe(toRead) : new Buffer(toRead);`
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(after !== before, `Patch failed for ${relative(process.cwd(), filePath)}`);
|
|
||||||
|
|
||||||
fs.writeFileSync(filePath, Buffer.from(after, "utf8"));
|
fs.writeFileSync(filePath, Buffer.from(after, "utf8"));
|
||||||
|
|
||||||
|
const hasBeenPatched = after !== before;
|
||||||
|
|
||||||
|
return { hasBeenPatched };
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,12 @@ import {
|
|||||||
dirname as pathDirname,
|
dirname as pathDirname,
|
||||||
sep as pathSep
|
sep as pathSep
|
||||||
} from "path";
|
} from "path";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { same } from "evt/tools/inDepth";
|
||||||
import { crawl } from "../src/bin/tools/crawl";
|
import { crawl } from "../src/bin/tools/crawl";
|
||||||
import { downloadKeycloakDefaultTheme } from "../src/bin/shared/downloadKeycloakDefaultTheme";
|
import { downloadKeycloakDefaultTheme } from "../src/bin/shared/downloadKeycloakDefaultTheme";
|
||||||
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
||||||
import { rmSync } from "../src/bin/tools/fs.rmSync";
|
import { deepAssign } from "../src/tools/deepAssign";
|
||||||
|
|
||||||
// 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.
|
||||||
@ -24,7 +26,7 @@ async function main() {
|
|||||||
|
|
||||||
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
||||||
keycloakVersion,
|
keycloakVersion,
|
||||||
buildOptions: {
|
buildContext: {
|
||||||
cacheDirPath: pathJoin(
|
cacheDirPath: pathJoin(
|
||||||
thisCodebaseRootDirPath,
|
thisCodebaseRootDirPath,
|
||||||
"node_modules",
|
"node_modules",
|
||||||
@ -73,12 +75,35 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(record).forEach(themeType => {
|
Object.keys(record).forEach(themeType => {
|
||||||
const recordForPageType = record[themeType];
|
|
||||||
|
|
||||||
if (themeType !== "login" && themeType !== "account") {
|
if (themeType !== "login" && themeType !== "account") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recordForThemeType = record[themeType];
|
||||||
|
|
||||||
|
const languages = Object.keys(recordForThemeType);
|
||||||
|
|
||||||
|
const keycloakifyExtraMessages = (() => {
|
||||||
|
switch (themeType) {
|
||||||
|
case "login":
|
||||||
|
return keycloakifyExtraMessages_login;
|
||||||
|
case "account":
|
||||||
|
return keycloakifyExtraMessages_account;
|
||||||
|
}
|
||||||
|
assert(false);
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert(
|
||||||
|
same(languages, Object.keys(keycloakifyExtraMessages), {
|
||||||
|
takeIntoAccountArraysOrdering: false
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
deepAssign({
|
||||||
|
target: recordForThemeType,
|
||||||
|
source: keycloakifyExtraMessages
|
||||||
|
});
|
||||||
|
|
||||||
const baseMessagesDirPath = pathJoin(
|
const baseMessagesDirPath = pathJoin(
|
||||||
thisCodebaseRootDirPath,
|
thisCodebaseRootDirPath,
|
||||||
"src",
|
"src",
|
||||||
@ -87,8 +112,6 @@ async function main() {
|
|||||||
"baseMessages"
|
"baseMessages"
|
||||||
);
|
);
|
||||||
|
|
||||||
const languages = Object.keys(recordForPageType);
|
|
||||||
|
|
||||||
const generatedFileHeader = [
|
const generatedFileHeader = [
|
||||||
`//This code was automatically generated by running ${pathRelative(
|
`//This code was automatically generated by running ${pathRelative(
|
||||||
thisCodebaseRootDirPath,
|
thisCodebaseRootDirPath,
|
||||||
@ -110,7 +133,7 @@ async function main() {
|
|||||||
"",
|
"",
|
||||||
"/* spell-checker: disable */",
|
"/* spell-checker: disable */",
|
||||||
`const messages= ${JSON.stringify(
|
`const messages= ${JSON.stringify(
|
||||||
recordForPageType[language],
|
recordForThemeType[language],
|
||||||
null,
|
null,
|
||||||
2
|
2
|
||||||
)};`,
|
)};`,
|
||||||
@ -154,6 +177,491 @@ async function main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const keycloakifyExtraMessages_login: Record<
|
||||||
|
| "en"
|
||||||
|
| "ar"
|
||||||
|
| "ca"
|
||||||
|
| "cs"
|
||||||
|
| "da"
|
||||||
|
| "de"
|
||||||
|
| "el"
|
||||||
|
| "es"
|
||||||
|
| "fa"
|
||||||
|
| "fi"
|
||||||
|
| "fr"
|
||||||
|
| "hu"
|
||||||
|
| "it"
|
||||||
|
| "ja"
|
||||||
|
| "lt"
|
||||||
|
| "lv"
|
||||||
|
| "nl"
|
||||||
|
| "no"
|
||||||
|
| "pl"
|
||||||
|
| "pt-BR"
|
||||||
|
| "ru"
|
||||||
|
| "sk"
|
||||||
|
| "sv"
|
||||||
|
| "th"
|
||||||
|
| "tr"
|
||||||
|
| "uk"
|
||||||
|
| "zh-CN",
|
||||||
|
Record<
|
||||||
|
| "shouldBeEqual"
|
||||||
|
| "shouldBeDifferent"
|
||||||
|
| "shouldMatchPattern"
|
||||||
|
| "mustBeAnInteger"
|
||||||
|
| "notAValidOption"
|
||||||
|
| "selectAnOption"
|
||||||
|
| "remove"
|
||||||
|
| "addValue"
|
||||||
|
| "languages",
|
||||||
|
string
|
||||||
|
>
|
||||||
|
> = {
|
||||||
|
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",
|
||||||
|
selectAnOption: "Select an option",
|
||||||
|
remove: "Remove",
|
||||||
|
addValue: "Add value",
|
||||||
|
languages: "Languages"
|
||||||
|
},
|
||||||
|
/* spell-checker: disable */
|
||||||
|
ar: {
|
||||||
|
shouldBeEqual: "{0} يجب أن يكون مساويًا لـ {1}",
|
||||||
|
shouldBeDifferent: "{0} يجب أن يكون مختلفًا عن {1}",
|
||||||
|
shouldMatchPattern: "`/يجب أن يطابق النمط: `/{0}/",
|
||||||
|
mustBeAnInteger: "يجب أن يكون عددًا صحيحًا",
|
||||||
|
notAValidOption: "ليس خيارًا صالحًا",
|
||||||
|
selectAnOption: "اختر خيارًا",
|
||||||
|
remove: "إزالة",
|
||||||
|
addValue: "أضف قيمة",
|
||||||
|
languages: "اللغات"
|
||||||
|
},
|
||||||
|
ca: {
|
||||||
|
shouldBeEqual: "{0} hauria de ser igual a {1}",
|
||||||
|
shouldBeDifferent: "{0} hauria de ser diferent de {1}",
|
||||||
|
shouldMatchPattern: "El patró hauria de coincidir: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Ha de ser un enter",
|
||||||
|
notAValidOption: "No és una opció vàlida",
|
||||||
|
selectAnOption: "Selecciona una opció",
|
||||||
|
remove: "Elimina",
|
||||||
|
addValue: "Afegeix valor",
|
||||||
|
languages: "Idiomes"
|
||||||
|
},
|
||||||
|
cs: {
|
||||||
|
shouldBeEqual: "{0} by měl být roven {1}",
|
||||||
|
shouldBeDifferent: "{0} by měl být odlišný od {1}",
|
||||||
|
shouldMatchPattern: "Vzor by měl odpovídat: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Musí být celé číslo",
|
||||||
|
notAValidOption: "Není platná možnost",
|
||||||
|
selectAnOption: "Vyberte možnost",
|
||||||
|
remove: "Odstranit",
|
||||||
|
addValue: "Přidat hodnotu",
|
||||||
|
languages: "Jazyky"
|
||||||
|
},
|
||||||
|
da: {
|
||||||
|
shouldBeEqual: "{0} bør være lig med {1}",
|
||||||
|
shouldBeDifferent: "{0} bør være forskellig fra {1}",
|
||||||
|
shouldMatchPattern: "Mønsteret bør matche: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Skal være et heltal",
|
||||||
|
notAValidOption: "Ikke en gyldig mulighed",
|
||||||
|
selectAnOption: "Vælg en mulighed",
|
||||||
|
remove: "Fjern",
|
||||||
|
addValue: "Tilføj værdi",
|
||||||
|
languages: "Sprog"
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
shouldBeEqual: "{0} sollte gleich {1} sein",
|
||||||
|
shouldBeDifferent: "{0} sollte sich von {1} unterscheiden",
|
||||||
|
shouldMatchPattern: "Muster sollte übereinstimmen: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Muss eine ganze Zahl sein",
|
||||||
|
notAValidOption: "Keine gültige Option",
|
||||||
|
selectAnOption: "Wählen Sie eine Option",
|
||||||
|
remove: "Entfernen",
|
||||||
|
addValue: "Wert hinzufügen",
|
||||||
|
languages: "Sprachen"
|
||||||
|
},
|
||||||
|
el: {
|
||||||
|
shouldBeEqual: "Το {0} πρέπει να είναι ίσο με {1}",
|
||||||
|
shouldBeDifferent: "Το {0} πρέπει να διαφέρει από το {1}",
|
||||||
|
shouldMatchPattern: "Το πρότυπο πρέπει να ταιριάζει: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Πρέπει να είναι ακέραιος",
|
||||||
|
notAValidOption: "Δεν είναι μια έγκυρη επιλογή",
|
||||||
|
selectAnOption: "Επιλέξτε μια επιλογή",
|
||||||
|
remove: "Αφαίρεση",
|
||||||
|
addValue: "Προσθήκη τιμής",
|
||||||
|
languages: "Γλώσσες"
|
||||||
|
},
|
||||||
|
es: {
|
||||||
|
shouldBeEqual: "{0} debería ser igual a {1}",
|
||||||
|
shouldBeDifferent: "{0} debería ser diferente a {1}",
|
||||||
|
shouldMatchPattern: "El patrón debería coincidir: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Debe ser un número entero",
|
||||||
|
notAValidOption: "No es una opción válida",
|
||||||
|
selectAnOption: "Selecciona una opción",
|
||||||
|
remove: "Eliminar",
|
||||||
|
addValue: "Añadir valor",
|
||||||
|
languages: "Idiomas"
|
||||||
|
},
|
||||||
|
fa: {
|
||||||
|
shouldBeEqual: "{0} باید برابر باشد با {1}",
|
||||||
|
shouldBeDifferent: "{0} باید متفاوت باشد از {1}",
|
||||||
|
shouldMatchPattern: "الگو باید مطابقت داشته باشد: `/{0}/`",
|
||||||
|
mustBeAnInteger: "باید یک عدد صحیح باشد",
|
||||||
|
notAValidOption: "یک گزینه معتبر نیست",
|
||||||
|
selectAnOption: "یک گزینه انتخاب کنید",
|
||||||
|
remove: "حذف",
|
||||||
|
addValue: "افزودن مقدار",
|
||||||
|
languages: "زبانها"
|
||||||
|
},
|
||||||
|
fi: {
|
||||||
|
shouldBeEqual: "{0} pitäisi olla yhtä suuri kuin {1}",
|
||||||
|
shouldBeDifferent: "{0} pitäisi olla erilainen kuin {1}",
|
||||||
|
shouldMatchPattern: "Mallin tulisi vastata: `/{0}/`",
|
||||||
|
mustBeAnInteger: "On oltava kokonaisluku",
|
||||||
|
notAValidOption: "Ei ole kelvollinen vaihtoehto",
|
||||||
|
selectAnOption: "Valitse vaihtoehto",
|
||||||
|
remove: "Poista",
|
||||||
|
addValue: "Lisää arvo",
|
||||||
|
languages: "Kielet"
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
shouldBeEqual: "{0} devrait être égal à {1}",
|
||||||
|
shouldBeDifferent: "{0} devrait être différent de {1}",
|
||||||
|
shouldMatchPattern: "Le motif devrait correspondre: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Doit être un entier",
|
||||||
|
notAValidOption: "Pas une option valide",
|
||||||
|
selectAnOption: "Sélectionnez une option",
|
||||||
|
remove: "Supprimer",
|
||||||
|
addValue: "Ajouter une valeur",
|
||||||
|
languages: "Langues"
|
||||||
|
},
|
||||||
|
hu: {
|
||||||
|
shouldBeEqual: "{0} egyenlő kell legyen {1}-vel",
|
||||||
|
shouldBeDifferent: "{0} különbözőnek kell lennie, mint {1}",
|
||||||
|
shouldMatchPattern: "A mintának egyeznie kell: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Egész számnak kell lennie",
|
||||||
|
notAValidOption: "Nem érvényes opció",
|
||||||
|
selectAnOption: "Válasszon egy lehetőséget",
|
||||||
|
remove: "Eltávolítás",
|
||||||
|
addValue: "Érték hozzáadása",
|
||||||
|
languages: "Nyelvek"
|
||||||
|
},
|
||||||
|
it: {
|
||||||
|
shouldBeEqual: "{0} dovrebbe essere uguale a {1}",
|
||||||
|
shouldBeDifferent: "{0} dovrebbe essere diverso da {1}",
|
||||||
|
shouldMatchPattern: "Il modello dovrebbe corrispondere: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Deve essere un numero intero",
|
||||||
|
notAValidOption: "Non è un'opzione valida",
|
||||||
|
selectAnOption: "Seleziona un'opzione",
|
||||||
|
remove: "Rimuovi",
|
||||||
|
addValue: "Aggiungi valore",
|
||||||
|
languages: "Lingue"
|
||||||
|
},
|
||||||
|
ja: {
|
||||||
|
shouldBeEqual: "{0} は {1} と等しい必要があります",
|
||||||
|
shouldBeDifferent: "{0} は {1} と異なる必要があります",
|
||||||
|
shouldMatchPattern: "パターンは一致する必要があります: `/{0}/`",
|
||||||
|
mustBeAnInteger: "整数である必要があります",
|
||||||
|
notAValidOption: "有効なオプションではありません",
|
||||||
|
selectAnOption: "オプションを選択",
|
||||||
|
remove: "削除",
|
||||||
|
addValue: "値を追加",
|
||||||
|
languages: "言語"
|
||||||
|
},
|
||||||
|
lt: {
|
||||||
|
shouldBeEqual: "{0} turėtų būti lygus {1}",
|
||||||
|
shouldBeDifferent: "{0} turėtų skirtis nuo {1}",
|
||||||
|
shouldMatchPattern: "Šablonas turėtų atitikti: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Turi būti sveikasis skaičius",
|
||||||
|
notAValidOption: "Netinkama parinktis",
|
||||||
|
selectAnOption: "Pasirinkite parinktį",
|
||||||
|
remove: "Pašalinti",
|
||||||
|
addValue: "Pridėti reikšmę",
|
||||||
|
languages: "Kalbos"
|
||||||
|
},
|
||||||
|
lv: {
|
||||||
|
shouldBeEqual: "{0} jābūt vienādam ar {1}",
|
||||||
|
shouldBeDifferent: "{0} jābūt atšķirīgam no {1}",
|
||||||
|
shouldMatchPattern: "Mustrim jāsakrīt: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Jābūt veselam skaitlim",
|
||||||
|
notAValidOption: "Nav derīga opcija",
|
||||||
|
selectAnOption: "Izvēlieties opciju",
|
||||||
|
remove: "Noņemt",
|
||||||
|
addValue: "Pievienot vērtību",
|
||||||
|
languages: "Valodas"
|
||||||
|
},
|
||||||
|
nl: {
|
||||||
|
shouldBeEqual: "{0} moet gelijk zijn aan {1}",
|
||||||
|
shouldBeDifferent: "{0} moet verschillen van {1}",
|
||||||
|
shouldMatchPattern: "Patroon moet overeenkomen: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Moet een geheel getal zijn",
|
||||||
|
notAValidOption: "Geen geldige optie",
|
||||||
|
selectAnOption: "Selecteer een optie",
|
||||||
|
remove: "Verwijderen",
|
||||||
|
addValue: "Waarde toevoegen",
|
||||||
|
languages: "Talen"
|
||||||
|
},
|
||||||
|
no: {
|
||||||
|
shouldBeEqual: "{0} skal være lik {1}",
|
||||||
|
shouldBeDifferent: "{0} skal være forskjellig fra {1}",
|
||||||
|
shouldMatchPattern: "Mønsteret skal matche: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Må være et heltall",
|
||||||
|
notAValidOption: "Ikke et gyldig alternativ",
|
||||||
|
selectAnOption: "Velg et alternativ",
|
||||||
|
remove: "Fjern",
|
||||||
|
addValue: "Legg til verdi",
|
||||||
|
languages: "Språk"
|
||||||
|
},
|
||||||
|
pl: {
|
||||||
|
shouldBeEqual: "{0} powinno być równe {1}",
|
||||||
|
shouldBeDifferent: "{0} powinno być różne od {1}",
|
||||||
|
shouldMatchPattern: "Wzór pow inien pasować: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Musi być liczbą całkowitą",
|
||||||
|
notAValidOption: "Nieprawidłowa opcja",
|
||||||
|
selectAnOption: "Wybierz opcję",
|
||||||
|
remove: "Usuń",
|
||||||
|
addValue: "Dodaj wartość",
|
||||||
|
languages: "Języki"
|
||||||
|
},
|
||||||
|
"pt-BR": {
|
||||||
|
shouldBeEqual: "{0} deve ser igual a {1}",
|
||||||
|
shouldBeDifferent: "{0} deve ser diferente de {1}",
|
||||||
|
shouldMatchPattern: "O padrão deve corresponder: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Deve ser um número inteiro",
|
||||||
|
notAValidOption: "Não é uma opção válida",
|
||||||
|
selectAnOption: "Selecione uma opção",
|
||||||
|
remove: "Remover",
|
||||||
|
addValue: "Adicionar valor",
|
||||||
|
languages: "Idiomas"
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
shouldBeEqual: "{0} должно быть равно {1}",
|
||||||
|
shouldBeDifferent: "{0} должно отличаться от {1}",
|
||||||
|
shouldMatchPattern: "Шаблон должен соответствовать: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Должно быть целым числом",
|
||||||
|
notAValidOption: "Недопустимый вариант",
|
||||||
|
selectAnOption: "Выберите вариант",
|
||||||
|
remove: "Удалить",
|
||||||
|
addValue: "Добавить значение",
|
||||||
|
languages: "Языки"
|
||||||
|
},
|
||||||
|
sk: {
|
||||||
|
shouldBeEqual: "{0} by mal byť rovnaký ako {1}",
|
||||||
|
shouldBeDifferent: "{0} by mal byť odlišný od {1}",
|
||||||
|
shouldMatchPattern: "Vzor by mal zodpovedať: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Musí byť celé číslo",
|
||||||
|
notAValidOption: "Nie je platná možnosť",
|
||||||
|
selectAnOption: "Vyberte možnosť",
|
||||||
|
remove: "Odstrániť",
|
||||||
|
addValue: "Pridať hodnotu",
|
||||||
|
languages: "Jazyky"
|
||||||
|
},
|
||||||
|
sv: {
|
||||||
|
shouldBeEqual: "{0} bör vara lika med {1}",
|
||||||
|
shouldBeDifferent: "{0} bör vara annorlunda än {1}",
|
||||||
|
shouldMatchPattern: "Mönstret bör matcha: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Måste vara ett heltal",
|
||||||
|
notAValidOption: "Inte ett giltigt alternativ",
|
||||||
|
selectAnOption: "Välj ett alternativ",
|
||||||
|
remove: "Ta bort",
|
||||||
|
addValue: "Lägg till värde",
|
||||||
|
languages: "Språk"
|
||||||
|
},
|
||||||
|
th: {
|
||||||
|
shouldBeEqual: "{0} ควรเท่ากับ {1}",
|
||||||
|
shouldBeDifferent: "{0} ควรแตกต่างจาก {1}",
|
||||||
|
shouldMatchPattern: "รูปแบบควรตรงกับ: `/{0}/`",
|
||||||
|
mustBeAnInteger: "ต้องเป็นจำนวนเต็ม",
|
||||||
|
notAValidOption: "ไม่ใช่ตัวเลือกที่ถูกต้อง",
|
||||||
|
selectAnOption: "เลือกตัวเลือก",
|
||||||
|
remove: "ลบ",
|
||||||
|
addValue: "เพิ่มค่า",
|
||||||
|
languages: "ภาษา"
|
||||||
|
},
|
||||||
|
tr: {
|
||||||
|
shouldBeEqual: "{0} {1} eşit olmalıdır",
|
||||||
|
shouldBeDifferent: "{0} {1} farklı olmalıdır",
|
||||||
|
shouldMatchPattern: "Desen eşleşmelidir: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Tam sayı olmalıdır",
|
||||||
|
notAValidOption: "Geçerli bir seçenek değil",
|
||||||
|
selectAnOption: "Bir seçenek seçin",
|
||||||
|
remove: "Kaldır",
|
||||||
|
addValue: "Değer ekle",
|
||||||
|
languages: "Diller"
|
||||||
|
},
|
||||||
|
uk: {
|
||||||
|
shouldBeEqual: "{0} повинно бути рівним {1}",
|
||||||
|
shouldBeDifferent: "{0} повинно відрізнятися від {1}",
|
||||||
|
shouldMatchPattern: "Шаблон повинен відповідати: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Повинно бути цілим числом",
|
||||||
|
notAValidOption: "Не є дійсною опцією",
|
||||||
|
selectAnOption: "Виберіть опцію",
|
||||||
|
remove: "Видалити",
|
||||||
|
addValue: "Додати значення",
|
||||||
|
languages: "Мови"
|
||||||
|
},
|
||||||
|
"zh-CN": {
|
||||||
|
shouldBeEqual: "{0} 应该等于 {1}",
|
||||||
|
shouldBeDifferent: "{0} 应该不同于 {1}",
|
||||||
|
shouldMatchPattern: "模式应匹配: `/{0}/`",
|
||||||
|
mustBeAnInteger: "必须是整数",
|
||||||
|
notAValidOption: "不是有效选项",
|
||||||
|
selectAnOption: "选择一个选项",
|
||||||
|
remove: "移除",
|
||||||
|
addValue: "添加值",
|
||||||
|
languages: "语言"
|
||||||
|
}
|
||||||
|
/* spell-checker: enable */
|
||||||
|
};
|
||||||
|
|
||||||
|
const keycloakifyExtraMessages_account: Record<
|
||||||
|
| "en"
|
||||||
|
| "ar"
|
||||||
|
| "ca"
|
||||||
|
| "cs"
|
||||||
|
| "da"
|
||||||
|
| "de"
|
||||||
|
| "el"
|
||||||
|
| "es"
|
||||||
|
| "fa"
|
||||||
|
| "fi"
|
||||||
|
| "fr"
|
||||||
|
| "hu"
|
||||||
|
| "it"
|
||||||
|
| "ja"
|
||||||
|
| "lt"
|
||||||
|
| "lv"
|
||||||
|
| "nl"
|
||||||
|
| "no"
|
||||||
|
| "pl"
|
||||||
|
| "pt-BR"
|
||||||
|
| "ru"
|
||||||
|
| "sk"
|
||||||
|
| "sv"
|
||||||
|
| "th"
|
||||||
|
| "tr"
|
||||||
|
| "uk"
|
||||||
|
| "zh-CN",
|
||||||
|
Record<"newPasswordSameAsOld" | "passwordConfirmNotMatch", string>
|
||||||
|
> = {
|
||||||
|
en: {
|
||||||
|
newPasswordSameAsOld: "New password must be different from the old one",
|
||||||
|
passwordConfirmNotMatch: "Password confirmation does not match"
|
||||||
|
},
|
||||||
|
/* spell-checker: disable */
|
||||||
|
ar: {
|
||||||
|
newPasswordSameAsOld: "يجب أن تكون كلمة المرور الجديدة مختلفة عن القديمة",
|
||||||
|
passwordConfirmNotMatch: "تأكيد كلمة المرور لا يتطابق"
|
||||||
|
},
|
||||||
|
ca: {
|
||||||
|
newPasswordSameAsOld: "La nova contrasenya ha de ser diferent de l'anterior",
|
||||||
|
passwordConfirmNotMatch: "La confirmació de la contrasenya no coincideix"
|
||||||
|
},
|
||||||
|
cs: {
|
||||||
|
newPasswordSameAsOld: "Nové heslo musí být odlišné od starého",
|
||||||
|
passwordConfirmNotMatch: "Potvrzení hesla se neshoduje"
|
||||||
|
},
|
||||||
|
da: {
|
||||||
|
newPasswordSameAsOld: "Det nye kodeord skal være forskelligt fra det gamle",
|
||||||
|
passwordConfirmNotMatch: "Adgangskodebekræftelse matcher ikke"
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
newPasswordSameAsOld: "Das neue Passwort muss sich vom alten unterscheiden",
|
||||||
|
passwordConfirmNotMatch: "Passwortbestätigung stimmt nicht überein"
|
||||||
|
},
|
||||||
|
el: {
|
||||||
|
newPasswordSameAsOld: "Ο νέος κωδικός πρόσβασης πρέπει να διαφέρει από τον παλιό",
|
||||||
|
passwordConfirmNotMatch: "Η επιβεβαίωση του κωδικού πρόσβασης δεν ταιριάζει"
|
||||||
|
},
|
||||||
|
es: {
|
||||||
|
newPasswordSameAsOld: "La nueva contraseña debe ser diferente de la anterior",
|
||||||
|
passwordConfirmNotMatch: "La confirmación de la contraseña no coincide"
|
||||||
|
},
|
||||||
|
fa: {
|
||||||
|
newPasswordSameAsOld: "رمز عبور جدید باید با رمز عبور قبلی متفاوت باشد",
|
||||||
|
passwordConfirmNotMatch: "تأیید رمز عبور مطابقت ندارد"
|
||||||
|
},
|
||||||
|
fi: {
|
||||||
|
newPasswordSameAsOld: "Uusi salasana on oltava erilainen kuin vanha",
|
||||||
|
passwordConfirmNotMatch: "Salasanan vahvistus ei täsmää"
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
newPasswordSameAsOld: "Le nouveau mot de passe doit être différent de l'ancien",
|
||||||
|
passwordConfirmNotMatch: "La confirmation du mot de passe ne correspond pas"
|
||||||
|
},
|
||||||
|
hu: {
|
||||||
|
newPasswordSameAsOld: "Az új jelszónak különböznie kell az előzőtől",
|
||||||
|
passwordConfirmNotMatch: "A jelszó megerősítése nem egyezik"
|
||||||
|
},
|
||||||
|
it: {
|
||||||
|
newPasswordSameAsOld:
|
||||||
|
"La nuova password deve essere diversa da quella precedente",
|
||||||
|
passwordConfirmNotMatch: "La conferma della password non corrisponde"
|
||||||
|
},
|
||||||
|
ja: {
|
||||||
|
newPasswordSameAsOld: "新しいパスワードは古いパスワードと異なる必要があります",
|
||||||
|
passwordConfirmNotMatch: "パスワード確認が一致しません"
|
||||||
|
},
|
||||||
|
lt: {
|
||||||
|
newPasswordSameAsOld: "Naujas slaptažodis turi skirtis nuo seno",
|
||||||
|
passwordConfirmNotMatch: "Slaptažodžio patvirtinimas neatitinka"
|
||||||
|
},
|
||||||
|
lv: {
|
||||||
|
newPasswordSameAsOld: "Jaunajam parolam jābūt atšķirīgam no vecā",
|
||||||
|
passwordConfirmNotMatch: "Paroles apstiprināšana neatbilst"
|
||||||
|
},
|
||||||
|
nl: {
|
||||||
|
newPasswordSameAsOld: "Het nieuwe wachtwoord moet verschillend zijn van het oude",
|
||||||
|
passwordConfirmNotMatch: "Wachtwoordbevestiging komt niet overeen"
|
||||||
|
},
|
||||||
|
no: {
|
||||||
|
newPasswordSameAsOld: "Det nye passordet må være forskjellig fra det gamle",
|
||||||
|
passwordConfirmNotMatch: "Passordbekreftelsen stemmer ikke"
|
||||||
|
},
|
||||||
|
pl: {
|
||||||
|
newPasswordSameAsOld: "Nowe hasło musi być inne niż stare",
|
||||||
|
passwordConfirmNotMatch: "Potwierdzenie hasła nie pasuje"
|
||||||
|
},
|
||||||
|
"pt-BR": {
|
||||||
|
newPasswordSameAsOld: "A nova senha deve ser diferente da antiga",
|
||||||
|
passwordConfirmNotMatch: "A confirmação da senha não corresponde"
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
newPasswordSameAsOld: "Новый пароль должен отличаться от старого",
|
||||||
|
passwordConfirmNotMatch: "Подтверждение пароля не совпадает"
|
||||||
|
},
|
||||||
|
sk: {
|
||||||
|
newPasswordSameAsOld: "Nové heslo musí byť odlišné od starého",
|
||||||
|
passwordConfirmNotMatch: "Potvrdenie hesla sa nezhoduje"
|
||||||
|
},
|
||||||
|
sv: {
|
||||||
|
newPasswordSameAsOld: "Det nya lösenordet måste skilja sig från det gamla",
|
||||||
|
passwordConfirmNotMatch: "Lösenordsbekräftelsen matchar inte"
|
||||||
|
},
|
||||||
|
th: {
|
||||||
|
newPasswordSameAsOld: "รหัสผ่านใหม่ต้องต่างจากรหัสผ่านเดิม",
|
||||||
|
passwordConfirmNotMatch: "การยืนยันรหัสผ่านไม่ตรงกัน"
|
||||||
|
},
|
||||||
|
tr: {
|
||||||
|
newPasswordSameAsOld: "Yeni şifre eskisinden farklı olmalıdır",
|
||||||
|
passwordConfirmNotMatch: "Şifre doğrulama eşleşmiyor"
|
||||||
|
},
|
||||||
|
uk: {
|
||||||
|
newPasswordSameAsOld: "Новий пароль повинен відрізнятися від старого",
|
||||||
|
passwordConfirmNotMatch: "Підтвердження пароля не співпадає"
|
||||||
|
},
|
||||||
|
"zh-CN": {
|
||||||
|
newPasswordSameAsOld: "新密码必须与旧密码不同",
|
||||||
|
passwordConfirmNotMatch: "密码确认不匹配"
|
||||||
|
}
|
||||||
|
/* spell-checker: enable */
|
||||||
|
};
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
main();
|
main();
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ export function startRebuildOnSrcChange() {
|
|||||||
|
|
||||||
console.log(chalk.green("Watching for changes in src/"));
|
console.log(chalk.green("Watching for changes in src/"));
|
||||||
|
|
||||||
chokidar.watch("src", { ignoreInitial: true }).on("all", async () => {
|
chokidar.watch(["src", "stories"], { ignoreInitial: true }).on("all", async () => {
|
||||||
await waitForDebounce();
|
await waitForDebounce();
|
||||||
|
|
||||||
runYarnBuild();
|
runYarnBuild();
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { lazy, Suspense } from "react";
|
import { lazy, Suspense } from "react";
|
||||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
|
||||||
import type { I18n } from "keycloakify/account/i18n";
|
|
||||||
import type { KcContext } from "./kcContext";
|
|
||||||
import { assert, type Equals } from "tsafe/assert";
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
import FederatedIdentity from "./pages/FederatedIdentity";
|
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||||
|
import type { KcContext } from "keycloakify/account/KcContext";
|
||||||
|
import { I18n } from "keycloakify/account/i18n";
|
||||||
|
|
||||||
const Password = lazy(() => import("keycloakify/account/pages/Password"));
|
const Password = lazy(() => import("keycloakify/account/pages/Password"));
|
||||||
const Account = lazy(() => import("keycloakify/account/pages/Account"));
|
const Account = lazy(() => import("keycloakify/account/pages/Account"));
|
||||||
@ -11,8 +10,9 @@ const Sessions = lazy(() => import("keycloakify/account/pages/Sessions"));
|
|||||||
const Totp = lazy(() => import("keycloakify/account/pages/Totp"));
|
const Totp = lazy(() => import("keycloakify/account/pages/Totp"));
|
||||||
const Applications = lazy(() => import("keycloakify/account/pages/Applications"));
|
const Applications = lazy(() => import("keycloakify/account/pages/Applications"));
|
||||||
const Log = lazy(() => import("keycloakify/account/pages/Log"));
|
const Log = lazy(() => import("keycloakify/account/pages/Log"));
|
||||||
|
const FederatedIdentity = lazy(() => import("keycloakify/account/pages/FederatedIdentity"));
|
||||||
|
|
||||||
export default function Fallback(props: PageProps<KcContext, I18n>) {
|
export default function DefaultPage(props: PageProps<KcContext, I18n>) {
|
||||||
const { kcContext, ...rest } = props;
|
const { kcContext, ...rest } = props;
|
||||||
|
|
||||||
return (
|
return (
|
@ -4,20 +4,20 @@ import { assert } from "tsafe/assert";
|
|||||||
import type { Equals } from "tsafe";
|
import type { Equals } from "tsafe";
|
||||||
|
|
||||||
export type ExtendKcContext<
|
export type ExtendKcContext<
|
||||||
KcContextExtraProperties extends { properties?: Record<string, string | undefined> },
|
KcContextExtension extends { properties?: Record<string, string | undefined> },
|
||||||
KcContextExtraPropertiesPerPage extends Record<string, Record<string, unknown>>
|
KcContextExtensionPerPage extends Record<string, Record<string, unknown>>
|
||||||
> = ValueOf<{
|
> = ValueOf<{
|
||||||
[PageId in keyof KcContextExtraPropertiesPerPage | KcContext["pageId"]]: Extract<
|
[PageId in keyof KcContextExtensionPerPage | KcContext["pageId"]]: Extract<
|
||||||
KcContext,
|
KcContext,
|
||||||
{ pageId: PageId }
|
{ pageId: PageId }
|
||||||
> extends never
|
> extends never
|
||||||
? KcContext.Common &
|
? KcContext.Common &
|
||||||
KcContextExtraProperties & {
|
KcContextExtension & {
|
||||||
pageId: PageId;
|
pageId: PageId;
|
||||||
} & KcContextExtraPropertiesPerPage[PageId]
|
} & KcContextExtensionPerPage[PageId]
|
||||||
: Extract<KcContext, { pageId: PageId }> &
|
: Extract<KcContext, { pageId: PageId }> &
|
||||||
KcContextExtraProperties &
|
KcContextExtension &
|
||||||
KcContextExtraPropertiesPerPage[PageId];
|
KcContextExtensionPerPage[PageId];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type KcContext =
|
export type KcContext =
|
@ -7,43 +7,32 @@ import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
|
|||||||
import { exclude } from "tsafe/exclude";
|
import { exclude } from "tsafe/exclude";
|
||||||
|
|
||||||
export function createGetKcContextMock<
|
export function createGetKcContextMock<
|
||||||
KcContextExtraProperties extends { properties?: Record<string, string | undefined> },
|
KcContextExtension extends { properties?: Record<string, string | undefined> },
|
||||||
KcContextExtraPropertiesPerPage extends Record<
|
KcContextExtensionPerPage extends Record<`${string}.ftl`, Record<string, unknown>>
|
||||||
`${string}.ftl`,
|
|
||||||
Record<string, unknown>
|
|
||||||
>
|
|
||||||
>(params: {
|
>(params: {
|
||||||
kcContextExtraProperties: KcContextExtraProperties;
|
kcContextExtension: KcContextExtension;
|
||||||
kcContextExtraPropertiesPerPage: KcContextExtraPropertiesPerPage;
|
kcContextExtensionPerPage: KcContextExtensionPerPage;
|
||||||
overrides?: DeepPartial<KcContextExtraProperties & KcContextBase.Common>;
|
overrides?: DeepPartial<KcContextExtension & KcContextBase.Common>;
|
||||||
overridesPerPage?: {
|
overridesPerPage?: {
|
||||||
[PageId in
|
[PageId in AccountThemePageId | keyof KcContextExtensionPerPage]?: DeepPartial<
|
||||||
| AccountThemePageId
|
|
||||||
| keyof KcContextExtraPropertiesPerPage]?: DeepPartial<
|
|
||||||
Extract<
|
Extract<
|
||||||
ExtendKcContext<
|
ExtendKcContext<KcContextExtension, KcContextExtensionPerPage>,
|
||||||
KcContextExtraProperties,
|
|
||||||
KcContextExtraPropertiesPerPage
|
|
||||||
>,
|
|
||||||
{ pageId: PageId }
|
{ pageId: PageId }
|
||||||
>
|
>
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
kcContextExtraProperties,
|
kcContextExtension,
|
||||||
kcContextExtraPropertiesPerPage,
|
kcContextExtensionPerPage,
|
||||||
overrides: overrides_global,
|
overrides: overrides_global,
|
||||||
overridesPerPage: overridesPerPage_global
|
overridesPerPage: overridesPerPage_global
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
type KcContext = ExtendKcContext<
|
type KcContext = ExtendKcContext<KcContextExtension, KcContextExtensionPerPage>;
|
||||||
KcContextExtraProperties,
|
|
||||||
KcContextExtraPropertiesPerPage
|
|
||||||
>;
|
|
||||||
|
|
||||||
function getKcContextMock<
|
function getKcContextMock<
|
||||||
PageId extends AccountThemePageId | keyof KcContextExtraPropertiesPerPage
|
PageId extends AccountThemePageId | keyof KcContextExtensionPerPage
|
||||||
>(params: {
|
>(params: {
|
||||||
pageId: PageId;
|
pageId: PageId;
|
||||||
overrides?: DeepPartial<Extract<KcContext, { pageId: PageId }>>;
|
overrides?: DeepPartial<Extract<KcContext, { pageId: PageId }>>;
|
||||||
@ -58,8 +47,8 @@ export function createGetKcContextMock<
|
|||||||
);
|
);
|
||||||
|
|
||||||
[
|
[
|
||||||
kcContextExtraProperties,
|
kcContextExtension,
|
||||||
kcContextExtraPropertiesPerPage[pageId],
|
kcContextExtensionPerPage[pageId],
|
||||||
overrides_global,
|
overrides_global,
|
||||||
overridesPerPage_global?.[pageId],
|
overridesPerPage_global?.[pageId],
|
||||||
overrides
|
overrides
|
@ -1,4 +1,4 @@
|
|||||||
import "minimal-polyfills/Object.fromEntries";
|
import "keycloakify/tools/Object.fromEntries";
|
||||||
import { resources_common, keycloak_resources } from "keycloakify/bin/shared/constants";
|
import { resources_common, keycloak_resources } from "keycloakify/bin/shared/constants";
|
||||||
import { id } from "tsafe/id";
|
import { id } from "tsafe/id";
|
||||||
import type { KcContext } from "./KcContext";
|
import type { KcContext } from "./KcContext";
|
@ -1,17 +1,17 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { assert } from "keycloakify/tools/assert";
|
||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
import { type TemplateProps } from "keycloakify/account/TemplateProps";
|
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
|
||||||
import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
|
import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
|
||||||
import { useSetClassName } from "keycloakify/tools/useSetClassName";
|
import { useSetClassName } from "keycloakify/tools/useSetClassName";
|
||||||
import type { KcContext } from "./kcContext";
|
import type { TemplateProps } from "keycloakify/account/TemplateProps";
|
||||||
import type { I18n } from "./i18n";
|
import type { I18n } from "./i18n";
|
||||||
import { assert } from "keycloakify/tools/assert";
|
import type { KcContext } from "./KcContext";
|
||||||
|
|
||||||
export default function Template(props: TemplateProps<KcContext, I18n>) {
|
export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||||
const { kcContext, i18n, doUseDefaultCss, active, classes, children } = props;
|
const { kcContext, i18n, doUseDefaultCss, active, classes, children } = props;
|
||||||
|
|
||||||
const { getClassName } = useGetClassName({ doUseDefaultCss, classes });
|
const { kcClsx } = getKcClsx({ doUseDefaultCss, classes });
|
||||||
|
|
||||||
const { msg, msgStr, getChangeLocalUrl, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
|
const { msg, msgStr, getChangeLocalUrl, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
|
||||||
|
|
||||||
@ -23,12 +23,12 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
|||||||
|
|
||||||
useSetClassName({
|
useSetClassName({
|
||||||
qualifiedName: "html",
|
qualifiedName: "html",
|
||||||
className: getClassName("kcHtmlClass")
|
className: kcClsx("kcHtmlClass")
|
||||||
});
|
});
|
||||||
|
|
||||||
useSetClassName({
|
useSetClassName({
|
||||||
qualifiedName: "body",
|
qualifiedName: "body",
|
||||||
className: clsx("admin-console", "user", getClassName("kcBodyClass"))
|
className: clsx("admin-console", "user", kcClsx("kcBodyClass"))
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -73,7 +73,6 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
|||||||
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
|
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
|
||||||
<li>
|
<li>
|
||||||
<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>
|
||||||
|
@ -1,17 +1,13 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import type { KcContext } from "./kcContext";
|
|
||||||
import type { I18n } from "./i18n";
|
|
||||||
|
|
||||||
export type TemplateProps<
|
export type TemplateProps<KcContext, I18n> = {
|
||||||
KcContext extends KcContext.Common,
|
|
||||||
I18nExtended extends I18n
|
|
||||||
> = {
|
|
||||||
kcContext: KcContext;
|
kcContext: KcContext;
|
||||||
i18n: I18nExtended;
|
i18n: I18n;
|
||||||
doUseDefaultCss: boolean;
|
doUseDefaultCss: boolean;
|
||||||
active: string;
|
|
||||||
classes?: Partial<Record<ClassKey, string>>;
|
classes?: Partial<Record<ClassKey, string>>;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
|
||||||
|
active: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ClassKey =
|
export type ClassKey =
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import "minimal-polyfills/Object.fromEntries";
|
import "keycloakify/tools/Object.fromEntries";
|
||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import fallbackMessages from "./baseMessages/en";
|
|
||||||
import { getMessages } from "./baseMessages";
|
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import type { KcContext } from "../kcContext/KcContext";
|
import messages_fallbackLanguage from "./baseMessages/en";
|
||||||
|
import { getMessages } from "./baseMessages";
|
||||||
|
import type { KcContext } from "../KcContext";
|
||||||
import { Reflect } from "tsafe/Reflect";
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
|
||||||
export const fallbackLanguageTag = "en";
|
export const fallbackLanguageTag = "en";
|
||||||
@ -17,7 +17,7 @@ export type KcContextLike = {
|
|||||||
|
|
||||||
assert<KcContext extends KcContextLike ? true : false>();
|
assert<KcContext extends KcContextLike ? true : false>();
|
||||||
|
|
||||||
export type MessageKey = keyof typeof fallbackMessages | keyof (typeof keycloakifyExtraMessages)[typeof fallbackLanguageTag];
|
export type MessageKey = keyof typeof messages_fallbackLanguage;
|
||||||
|
|
||||||
export type GenericI18n<MessageKey extends string> = {
|
export type GenericI18n<MessageKey extends string> = {
|
||||||
/**
|
/**
|
||||||
@ -79,195 +79,245 @@ export type GenericI18n<MessageKey extends string> = {
|
|||||||
* See advancedMsg() but instead of returning a JSX.Element it returns a string.
|
* See advancedMsg() but instead of returning a JSX.Element it returns a string.
|
||||||
*/
|
*/
|
||||||
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
|
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initially the messages are in english (fallback language).
|
||||||
|
* The translations in the current language are being fetched dynamically.
|
||||||
|
* This property is true while the translations are being fetched.
|
||||||
|
*/
|
||||||
|
isFetchingTranslations: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type I18n = GenericI18n<MessageKey>;
|
function createGetI18n<ExtraMessageKey extends string = never>(extraMessages: { [languageTag: string]: { [key in ExtraMessageKey]: string } }) {
|
||||||
|
type I18n = GenericI18n<MessageKey | ExtraMessageKey>;
|
||||||
|
|
||||||
|
type Result = { i18n: I18n; prI18n_currentLanguage: Promise<I18n> | undefined };
|
||||||
|
|
||||||
|
const cachedResultByKcContext = new WeakMap<KcContextLike, Result>();
|
||||||
|
|
||||||
|
function getI18n(params: { kcContext: KcContextLike }): Result {
|
||||||
|
const { kcContext } = params;
|
||||||
|
|
||||||
|
use_cache: {
|
||||||
|
const cachedResult = cachedResultByKcContext.get(kcContext);
|
||||||
|
|
||||||
|
if (cachedResult === undefined) {
|
||||||
|
break use_cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const partialI18n: Pick<I18n, "currentLanguageTag" | "getChangeLocalUrl" | "labelBySupportedLanguageTag"> = {
|
||||||
|
currentLanguageTag: kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag,
|
||||||
|
getChangeLocalUrl: 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`);
|
||||||
|
|
||||||
|
return targetSupportedLocale.url;
|
||||||
|
},
|
||||||
|
labelBySupportedLanguageTag: Object.fromEntries((kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label]))
|
||||||
|
};
|
||||||
|
|
||||||
|
const { createI18nTranslationFunctions } = createI18nTranslationFunctionsFactory<MessageKey, ExtraMessageKey>({
|
||||||
|
messages_fallbackLanguage,
|
||||||
|
extraMessages_fallbackLanguage: extraMessages[fallbackLanguageTag],
|
||||||
|
extraMessages: extraMessages[partialI18n.currentLanguageTag]
|
||||||
|
});
|
||||||
|
|
||||||
|
const isCurrentLanguageFallbackLanguage = partialI18n.currentLanguageTag === fallbackLanguageTag;
|
||||||
|
|
||||||
|
const result: Result = {
|
||||||
|
i18n: {
|
||||||
|
...partialI18n,
|
||||||
|
...createI18nTranslationFunctions({ messages: undefined }),
|
||||||
|
isFetchingTranslations: !isCurrentLanguageFallbackLanguage
|
||||||
|
},
|
||||||
|
prI18n_currentLanguage: isCurrentLanguageFallbackLanguage
|
||||||
|
? undefined
|
||||||
|
: (async () => {
|
||||||
|
const messages = await getMessages(partialI18n.currentLanguageTag);
|
||||||
|
|
||||||
|
const i18n_currentLanguage: I18n = {
|
||||||
|
...partialI18n,
|
||||||
|
...createI18nTranslationFunctions({ messages }),
|
||||||
|
isFetchingTranslations: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// NOTE: This promise.resolve is just because without it we TypeScript
|
||||||
|
// gives a Variable 'result' is used before being assigned. error
|
||||||
|
await Promise.resolve().then(() => {
|
||||||
|
result.i18n = i18n_currentLanguage;
|
||||||
|
result.prI18n_currentLanguage = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
return i18n_currentLanguage;
|
||||||
|
})()
|
||||||
|
};
|
||||||
|
|
||||||
|
cachedResultByKcContext.set(kcContext, result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getI18n };
|
||||||
|
}
|
||||||
|
|
||||||
export function createUseI18n<ExtraMessageKey extends string = never>(extraMessages: {
|
export function createUseI18n<ExtraMessageKey extends string = never>(extraMessages: {
|
||||||
[languageTag: string]: { [key in ExtraMessageKey]: string };
|
[languageTag: string]: { [key in ExtraMessageKey]: string };
|
||||||
}) {
|
}) {
|
||||||
function useI18n(params: { kcContext: KcContextLike }): GenericI18n<MessageKey | ExtraMessageKey> | null {
|
type I18n = GenericI18n<MessageKey | ExtraMessageKey>;
|
||||||
|
|
||||||
|
const { getI18n } = createGetI18n(extraMessages);
|
||||||
|
|
||||||
|
function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n } {
|
||||||
const { kcContext } = params;
|
const { kcContext } = params;
|
||||||
|
|
||||||
const [i18n, setI18n] = useState<GenericI18n<ExtraMessageKey | MessageKey> | undefined>(undefined);
|
const { i18n, prI18n_currentLanguage } = getI18n({ kcContext });
|
||||||
|
|
||||||
const refHasStartedFetching = useRef(false);
|
const [i18n_toReturn, setI18n_toReturn] = useState<I18n>(i18n);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (refHasStartedFetching.current) {
|
let isActive = true;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
refHasStartedFetching.current = true;
|
prI18n_currentLanguage?.then(i18n => {
|
||||||
|
if (!isActive) {
|
||||||
(async () => {
|
|
||||||
const { currentLanguageTag = fallbackLanguageTag } = kcContext.locale ?? {};
|
|
||||||
|
|
||||||
setI18n({
|
|
||||||
...createI18nTranslationFunctions({
|
|
||||||
fallbackMessages: {
|
|
||||||
...fallbackMessages,
|
|
||||||
...(keycloakifyExtraMessages[fallbackLanguageTag] ?? {}),
|
|
||||||
...(extraMessages[fallbackLanguageTag] ?? {})
|
|
||||||
} as any,
|
|
||||||
messages: {
|
|
||||||
...(await getMessages(currentLanguageTag)),
|
|
||||||
...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}),
|
|
||||||
...(extraMessages[currentLanguageTag] ?? {})
|
|
||||||
} as any
|
|
||||||
}),
|
|
||||||
currentLanguageTag,
|
|
||||||
getChangeLocalUrl: 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`);
|
|
||||||
|
|
||||||
return targetSupportedLocale.url;
|
|
||||||
},
|
|
||||||
labelBySupportedLanguageTag: Object.fromEntries(
|
|
||||||
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])
|
|
||||||
)
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return i18n ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
useI18n,
|
|
||||||
ofTypeI18n: Reflect<GenericI18n<MessageKey | ExtraMessageKey>>()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createI18nTranslationFunctions<MessageKey extends string>(params: {
|
|
||||||
fallbackMessages: Record<MessageKey, string>;
|
|
||||||
messages: Record<MessageKey, string>;
|
|
||||||
}): Pick<GenericI18n<MessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
|
|
||||||
const { fallbackMessages, messages } = params;
|
|
||||||
|
|
||||||
function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): string | JSX.Element | undefined {
|
|
||||||
const { key, args, doRenderAsHtml } = 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
messageWithArgsInjected = messageWithArgsInjected.replace(
|
setI18n_toReturn(i18n);
|
||||||
new RegExp(`\\{${i + startIndex}\\}`, "g"),
|
|
||||||
arg.replace(/</g, "<").replace(/>/g, ">")
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return messageWithArgsInjected;
|
return () => {
|
||||||
})();
|
isActive = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return doRenderAsHtml ? (
|
return { i18n: i18n_toReturn };
|
||||||
<span
|
|
||||||
// NOTE: The message is trusted. The arguments are not but are escaped.
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: messageWithArgsInjectedIfAny
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
messageWithArgsInjectedIfAny
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): JSX.Element | string {
|
return { useI18n, ofTypeI18n: Reflect<I18n>() };
|
||||||
const { key, args, doRenderAsHtml } = props;
|
|
||||||
|
|
||||||
if (!/\$\{[^}]+\}/.test(key)) {
|
|
||||||
const resolvedMessage = resolveMsg({ key, args, doRenderAsHtml });
|
|
||||||
|
|
||||||
if (resolvedMessage === undefined) {
|
|
||||||
return doRenderAsHtml ? <span dangerouslySetInnerHTML={{ __html: key }} /> : key;
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolvedMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
let isFirstMatch = true;
|
|
||||||
|
|
||||||
const resolvedComplexMessage = key.replace(/\$\{([^}]+)\}/g, (...[, key_i]) => {
|
|
||||||
const replaceBy = resolveMsg({ key: key_i, args: isFirstMatch ? args : [], doRenderAsHtml: false }) ?? key_i;
|
|
||||||
|
|
||||||
isFirstMatch = false;
|
|
||||||
|
|
||||||
return replaceBy;
|
|
||||||
});
|
|
||||||
|
|
||||||
return doRenderAsHtml ? <span dangerouslySetInnerHTML={{ __html: resolvedComplexMessage }} /> : resolvedComplexMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
msgStr: (key, ...args) => resolveMsg({ key, args, doRenderAsHtml: false }) as string,
|
|
||||||
msg: (key, ...args) => resolveMsg({ key, args, doRenderAsHtml: true }) as JSX.Element,
|
|
||||||
advancedMsg: (key, ...args) =>
|
|
||||||
resolveMsgAdvanced({
|
|
||||||
key,
|
|
||||||
args,
|
|
||||||
doRenderAsHtml: true
|
|
||||||
}) as JSX.Element,
|
|
||||||
advancedMsgStr: (key, ...args) =>
|
|
||||||
resolveMsgAdvanced({
|
|
||||||
key,
|
|
||||||
args,
|
|
||||||
doRenderAsHtml: false
|
|
||||||
}) as string
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const keycloakifyExtraMessages = {
|
function createI18nTranslationFunctionsFactory<MessageKey extends string, ExtraMessageKey extends string>(params: {
|
||||||
en: {
|
messages_fallbackLanguage: Record<MessageKey, string>;
|
||||||
shouldBeEqual: "{0} should be equal to {1}",
|
extraMessages_fallbackLanguage: Record<ExtraMessageKey, string> | undefined;
|
||||||
shouldBeDifferent: "{0} should be different to {1}",
|
extraMessages: Partial<Record<ExtraMessageKey, string>> | undefined;
|
||||||
shouldMatchPattern: "Pattern should match: `/{0}/`",
|
}) {
|
||||||
mustBeAnInteger: "Must be an integer",
|
const { extraMessages } = params;
|
||||||
notAValidOption: "Not a valid option",
|
|
||||||
newPasswordSameAsOld: "New password must be different from the old one",
|
|
||||||
passwordConfirmNotMatch: "Password confirmation does not match"
|
|
||||||
},
|
|
||||||
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",
|
const messages_fallbackLanguage = {
|
||||||
logoutConfirmHeader: "Êtes-vous sûr(e) de vouloir vous déconnecter ?",
|
...params.messages_fallbackLanguage,
|
||||||
doLogout: "Se déconnecter",
|
...params.extraMessages_fallbackLanguage
|
||||||
newPasswordSameAsOld: "Le nouveau mot de passe doit être différent de l'ancien",
|
};
|
||||||
passwordConfirmNotMatch: "La confirmation du mot de passe ne correspond pas"
|
|
||||||
/* spell-checker: enable */
|
function createI18nTranslationFunctions(params: {
|
||||||
|
messages: Partial<Record<MessageKey, string>> | undefined;
|
||||||
|
}): Pick<GenericI18n<MessageKey | ExtraMessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
|
||||||
|
const messages = {
|
||||||
|
...params.messages,
|
||||||
|
...extraMessages
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): string | JSX.Element | undefined {
|
||||||
|
const { key, args, doRenderAsHtml } = props;
|
||||||
|
|
||||||
|
const messageOrUndefined: string | undefined = (messages as any)[key] ?? (messages_fallbackLanguage 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.replace(/</g, "<").replace(/>/g, ">")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return messageWithArgsInjected;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return doRenderAsHtml ? (
|
||||||
|
<span
|
||||||
|
// NOTE: The message is trusted. The arguments are not but are escaped.
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: messageWithArgsInjectedIfAny
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
messageWithArgsInjectedIfAny
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): JSX.Element | string {
|
||||||
|
const { key, args, doRenderAsHtml } = props;
|
||||||
|
|
||||||
|
if (!/\$\{[^}]+\}/.test(key)) {
|
||||||
|
const resolvedMessage = resolveMsg({ key, args, doRenderAsHtml });
|
||||||
|
|
||||||
|
if (resolvedMessage === undefined) {
|
||||||
|
return doRenderAsHtml ? <span dangerouslySetInnerHTML={{ __html: key }} /> : key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isFirstMatch = true;
|
||||||
|
|
||||||
|
const resolvedComplexMessage = key.replace(/\$\{([^}]+)\}/g, (...[, key_i]) => {
|
||||||
|
const replaceBy = resolveMsg({ key: key_i, args: isFirstMatch ? args : [], doRenderAsHtml: false }) ?? key_i;
|
||||||
|
|
||||||
|
isFirstMatch = false;
|
||||||
|
|
||||||
|
return replaceBy;
|
||||||
|
});
|
||||||
|
|
||||||
|
return doRenderAsHtml ? <span dangerouslySetInnerHTML={{ __html: resolvedComplexMessage }} /> : resolvedComplexMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
msgStr: (key, ...args) => resolveMsg({ key, args, doRenderAsHtml: false }) as string,
|
||||||
|
msg: (key, ...args) => resolveMsg({ key, args, doRenderAsHtml: true }) as JSX.Element,
|
||||||
|
advancedMsg: (key, ...args) =>
|
||||||
|
resolveMsgAdvanced({
|
||||||
|
key,
|
||||||
|
args,
|
||||||
|
doRenderAsHtml: true
|
||||||
|
}) as JSX.Element,
|
||||||
|
advancedMsgStr: (key, ...args) =>
|
||||||
|
resolveMsgAdvanced({
|
||||||
|
key,
|
||||||
|
args,
|
||||||
|
doRenderAsHtml: false
|
||||||
|
}) as string
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
return { createI18nTranslationFunctions };
|
||||||
|
}
|
||||||
|
@ -1 +1,5 @@
|
|||||||
export type { I18n } from "./i18n";
|
import type { GenericI18n, MessageKey, KcContextLike } from "./i18n";
|
||||||
|
export type { MessageKey, KcContextLike };
|
||||||
|
export type I18n = GenericI18n<MessageKey>;
|
||||||
|
export { createUseI18n } from "./i18n";
|
||||||
|
export { fallbackLanguageTag } from "./i18n";
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
export type { AccountThemePageId as PageId } from "keycloakify/bin/shared/constants";
|
export type { ExtendKcContext } from "keycloakify/account/KcContext";
|
||||||
export { createUseI18n } from "keycloakify/account/i18n/i18n";
|
export type { ClassKey } from "keycloakify/account/TemplateProps";
|
||||||
export type { ExtendKcContext } from "keycloakify/account/kcContext";
|
export { createUseI18n } from "keycloakify/account/i18n";
|
||||||
export { createGetKcContextMock } from "keycloakify/account/kcContext";
|
|
||||||
|
|
||||||
export type { PageProps } from "keycloakify/account/pages/PageProps";
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { createUseClassName } from "keycloakify/lib/useGetClassName";
|
import { createGetKcClsx } from "keycloakify/lib/getKcClsx";
|
||||||
import type { ClassKey } from "keycloakify/account/TemplateProps";
|
import type { ClassKey } from "keycloakify/account/TemplateProps";
|
||||||
|
|
||||||
export const { useGetClassName } = createUseClassName<ClassKey>({
|
export const { getKcClsx } = createGetKcClsx<ClassKey>({
|
||||||
defaultClasses: {
|
defaultClasses: {
|
||||||
kcHtmlClass: undefined,
|
kcHtmlClass: undefined,
|
||||||
kcBodyClass: undefined,
|
kcBodyClass: undefined,
|
||||||
@ -19,3 +19,7 @@ export const { useGetClassName } = createUseClassName<ClassKey>({
|
|||||||
"pf-c-form__helper-text pf-m-error required kc-feedback-text"
|
"pf-c-form__helper-text pf-m-error required kc-feedback-text"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type { ClassKey };
|
||||||
|
|
||||||
|
export type KcClsx = ReturnType<typeof getKcClsx>["kcClsx"];
|
@ -1,18 +1,20 @@
|
|||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||||
import type { KcContext } from "../kcContext";
|
import type { KcContext } from "../KcContext";
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
export default function Account(props: PageProps<Extract<KcContext, { pageId: "account.ftl" }>, I18n>) {
|
export default function Account(props: PageProps<Extract<KcContext, { pageId: "account.ftl" }>, I18n>) {
|
||||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
const { kcContext, i18n, doUseDefaultCss, Template } = props;
|
||||||
|
|
||||||
const { getClassName } = useGetClassName({
|
const classes = {
|
||||||
|
...props.classes,
|
||||||
|
kcBodyClass: clsx(props.classes?.kcBodyClass, "user")
|
||||||
|
};
|
||||||
|
|
||||||
|
const { kcClsx } = getKcClsx({
|
||||||
doUseDefaultCss,
|
doUseDefaultCss,
|
||||||
classes: {
|
classes
|
||||||
...classes,
|
|
||||||
kcBodyClass: clsx(classes?.kcBodyClass, "user")
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { url, realm, messagesPerField, stateChecker, account, referrer } = kcContext;
|
const { url, realm, messagesPerField, stateChecker, account, referrer } = kcContext;
|
||||||
@ -102,11 +104,7 @@ export default function Account(props: PageProps<Extract<KcContext, { pageId: "a
|
|||||||
{referrer !== undefined && <a href={referrer?.url}>{msg("backToApplication")}</a>}
|
{referrer !== undefined && <a href={referrer?.url}>{msg("backToApplication")}</a>}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={clsx(
|
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonLargeClass")}
|
||||||
getClassName("kcButtonClass"),
|
|
||||||
getClassName("kcButtonPrimaryClass"),
|
|
||||||
getClassName("kcButtonLargeClass")
|
|
||||||
)}
|
|
||||||
name="submitAction"
|
name="submitAction"
|
||||||
value="Save"
|
value="Save"
|
||||||
>
|
>
|
||||||
@ -114,11 +112,7 @@ export default function Account(props: PageProps<Extract<KcContext, { pageId: "a
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={clsx(
|
className={kcClsx("kcButtonClass", "kcButtonDefaultClass", "kcButtonLargeClass")}
|
||||||
getClassName("kcButtonClass"),
|
|
||||||
getClassName("kcButtonDefaultClass"),
|
|
||||||
getClassName("kcButtonLargeClass")
|
|
||||||
)}
|
|
||||||
name="submitAction"
|
name="submitAction"
|
||||||
value="Cancel"
|
value="Cancel"
|
||||||
>
|
>
|
||||||
|
@ -1,17 +1,12 @@
|
|||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
import type { KcContext } from "../KcContext";
|
||||||
import type { KcContext } from "../kcContext";
|
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
function isArrayWithEmptyObject(variable: any): boolean {
|
|
||||||
return Array.isArray(variable) && variable.length === 1 && typeof variable[0] === "object" && Object.keys(variable[0]).length === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Applications(props: PageProps<Extract<KcContext, { pageId: "applications.ftl" }>, I18n>) {
|
export default function Applications(props: PageProps<Extract<KcContext, { pageId: "applications.ftl" }>, I18n>) {
|
||||||
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
|
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
|
||||||
|
|
||||||
const { getClassName } = useGetClassName({
|
const { kcClsx } = getKcClsx({
|
||||||
doUseDefaultCss,
|
doUseDefaultCss,
|
||||||
classes
|
classes
|
||||||
});
|
});
|
||||||
@ -118,7 +113,7 @@ export default function Applications(props: PageProps<Extract<KcContext, { pageI
|
|||||||
application.additionalGrants.length > 0 ? (
|
application.additionalGrants.length > 0 ? (
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={clsx(getClassName("kcButtonPrimaryClass"), getClassName("kcButtonClass"))}
|
className={kcClsx("kcButtonPrimaryClass", "kcButtonClass")}
|
||||||
id={`revoke-${application.client.clientId}`}
|
id={`revoke-${application.client.clientId}`}
|
||||||
name="clientId"
|
name="clientId"
|
||||||
value={application.client.id}
|
value={application.client.id}
|
||||||
@ -136,3 +131,7 @@ export default function Applications(props: PageProps<Extract<KcContext, { pageI
|
|||||||
</Template>
|
</Template>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isArrayWithEmptyObject(variable: any): boolean {
|
||||||
|
return Array.isArray(variable) && variable.length === 1 && typeof variable[0] === "object" && Object.keys(variable[0]).length === 0;
|
||||||
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { PageProps } from "keycloakify/account";
|
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||||
import { I18n } from "keycloakify/account/i18n";
|
import type { KcContext } from "../KcContext";
|
||||||
import { KcContext } from "keycloakify/account/kcContext";
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
export default function FederatedIdentity(props: PageProps<Extract<KcContext, { pageId: "federatedIdentity.ftl" }>, I18n>) {
|
export default function FederatedIdentity(props: PageProps<Extract<KcContext, { pageId: "federatedIdentity.ftl" }>, I18n>) {
|
||||||
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
|
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
|
import type { Key } from "react";
|
||||||
|
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||||
import type { KcContext } from "../kcContext";
|
import type { KcContext } from "../KcContext";
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
import { Key } from "react";
|
|
||||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
|
||||||
|
|
||||||
export default function Log(props: PageProps<Extract<KcContext, { pageId: "log.ftl" }>, I18n>) {
|
export default function Log(props: PageProps<Extract<KcContext, { pageId: "log.ftl" }>, I18n>) {
|
||||||
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
|
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
|
||||||
|
|
||||||
const { getClassName } = useGetClassName({
|
const { kcClsx } = getKcClsx({
|
||||||
doUseDefaultCss,
|
doUseDefaultCss,
|
||||||
classes
|
classes
|
||||||
});
|
});
|
||||||
@ -18,7 +18,7 @@ export default function Log(props: PageProps<Extract<KcContext, { pageId: "log.f
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="log">
|
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="log">
|
||||||
<div className={getClassName("kcContentWrapperClass")}>
|
<div className={kcClsx("kcContentWrapperClass")}>
|
||||||
<div className="col-md-10">
|
<div className="col-md-10">
|
||||||
<h2>{msg("accountLogHtmlTitle")}</h2>
|
<h2>{msg("accountLogHtmlTitle")}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import type { I18n } from "keycloakify/account/i18n";
|
import { type TemplateProps, type ClassKey } from "keycloakify/account/TemplateProps";
|
||||||
import type { TemplateProps, ClassKey } from "keycloakify/account/TemplateProps";
|
|
||||||
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
|
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
|
||||||
import type { KcContext } from "keycloakify/account/kcContext";
|
|
||||||
|
|
||||||
export type PageProps<NarowedKcContext = KcContext, I18nExtended extends I18n = I18n> = {
|
export type PageProps<NarrowedKcContext, I18n> = {
|
||||||
Template: LazyOrNot<(props: TemplateProps<any, any>) => JSX.Element | null>;
|
Template: LazyOrNot<(props: TemplateProps<any, any>) => JSX.Element | null>;
|
||||||
kcContext: NarowedKcContext;
|
kcContext: NarrowedKcContext;
|
||||||
i18n: I18nExtended;
|
i18n: I18n;
|
||||||
doUseDefaultCss: boolean;
|
doUseDefaultCss: boolean;
|
||||||
classes?: Partial<Record<ClassKey, string>>;
|
classes?: Partial<Record<ClassKey, string>>;
|
||||||
};
|
};
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
|
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
import type { KcContext } from "../KcContext";
|
||||||
import type { KcContext } from "../kcContext";
|
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
export default function Password(props: PageProps<Extract<KcContext, { pageId: "password.ftl" }>, I18n>) {
|
export default function Password(props: PageProps<Extract<KcContext, { pageId: "password.ftl" }>, I18n>) {
|
||||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
const { kcContext, i18n, doUseDefaultCss, Template } = props;
|
||||||
|
|
||||||
const { getClassName } = useGetClassName({
|
const classes = {
|
||||||
|
...props.classes,
|
||||||
|
kcBodyClass: clsx(props.classes?.kcBodyClass, "password")
|
||||||
|
};
|
||||||
|
|
||||||
|
const { kcClsx } = getKcClsx({
|
||||||
doUseDefaultCss,
|
doUseDefaultCss,
|
||||||
classes: {
|
classes
|
||||||
...classes,
|
|
||||||
kcBodyClass: clsx(classes?.kcBodyClass, "password")
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { url, password, account, stateChecker } = kcContext;
|
const { url, password, account, stateChecker } = kcContext;
|
||||||
@ -192,11 +194,7 @@ export default function Password(props: PageProps<Extract<KcContext, { pageId: "
|
|||||||
<button
|
<button
|
||||||
disabled={newPasswordError !== "" || newPasswordConfirmError !== ""}
|
disabled={newPasswordError !== "" || newPasswordConfirmError !== ""}
|
||||||
type="submit"
|
type="submit"
|
||||||
className={clsx(
|
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonLargeClass")}
|
||||||
getClassName("kcButtonClass"),
|
|
||||||
getClassName("kcButtonPrimaryClass"),
|
|
||||||
getClassName("kcButtonLargeClass")
|
|
||||||
)}
|
|
||||||
name="submitAction"
|
name="submitAction"
|
||||||
value="Save"
|
value="Save"
|
||||||
>
|
>
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
import type { KcContext } from "../KcContext";
|
||||||
import type { KcContext } from "../kcContext";
|
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
export default function Sessions(props: PageProps<Extract<KcContext, { pageId: "sessions.ftl" }>, I18n>) {
|
export default function Sessions(props: PageProps<Extract<KcContext, { pageId: "sessions.ftl" }>, I18n>) {
|
||||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||||
|
|
||||||
const { getClassName } = useGetClassName({
|
const { kcClsx } = getKcClsx({
|
||||||
doUseDefaultCss,
|
doUseDefaultCss,
|
||||||
classes
|
classes
|
||||||
});
|
});
|
||||||
@ -17,7 +16,7 @@ export default function Sessions(props: PageProps<Extract<KcContext, { pageId: "
|
|||||||
const { msg } = i18n;
|
const { msg } = i18n;
|
||||||
return (
|
return (
|
||||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="sessions">
|
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="sessions">
|
||||||
<div className={getClassName("kcContentWrapperClass")}>
|
<div className={kcClsx("kcContentWrapperClass")}>
|
||||||
<div className="col-md-10">
|
<div className="col-md-10">
|
||||||
<h2>{msg("sessionsHtmlTitle")}</h2>
|
<h2>{msg("sessionsHtmlTitle")}</h2>
|
||||||
</div>
|
</div>
|
||||||
@ -56,7 +55,7 @@ export default function Sessions(props: PageProps<Extract<KcContext, { pageId: "
|
|||||||
|
|
||||||
<form action={url.sessionsUrl} method="post">
|
<form action={url.sessionsUrl} method="post">
|
||||||
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
|
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
|
||||||
<button id="logout-all-sessions" type="submit" className={clsx(getClassName("kcButtonDefaultClass"), getClassName("kcButtonClass"))}>
|
<button id="logout-all-sessions" type="submit" className={kcClsx("kcButtonDefaultClass", "kcButtonClass")}>
|
||||||
{msg("doLogOutAllSessions")}
|
{msg("doLogOutAllSessions")}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
|
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
import type { KcContext } from "../KcContext";
|
||||||
import type { KcContext } from "../kcContext";
|
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp.ftl" }>, I18n>) {
|
export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp.ftl" }>, I18n>) {
|
||||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||||
|
|
||||||
const { getClassName } = useGetClassName({
|
const { kcClsx } = getKcClsx({
|
||||||
doUseDefaultCss,
|
doUseDefaultCss,
|
||||||
classes
|
classes
|
||||||
});
|
});
|
||||||
@ -140,9 +140,9 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
|
|||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<hr />
|
<hr />
|
||||||
<form action={url.totpUrl} className={getClassName("kcFormClass")} id="kc-totp-settings-form" method="post">
|
<form action={url.totpUrl} className={kcClsx("kcFormClass")} id="kc-totp-settings-form" method="post">
|
||||||
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
|
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
|
||||||
<div className={getClassName("kcFormGroupClass")}>
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
<div className="col-sm-2 col-md-2">
|
<div className="col-sm-2 col-md-2">
|
||||||
<label htmlFor="totp" className="control-label">
|
<label htmlFor="totp" className="control-label">
|
||||||
{msg("authenticatorCode")}
|
{msg("authenticatorCode")}
|
||||||
@ -155,12 +155,12 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
|
|||||||
id="totp"
|
id="totp"
|
||||||
name="totp"
|
name="totp"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className={getClassName("kcInputClass")}
|
className={kcClsx("kcInputClass")}
|
||||||
aria-invalid={messagesPerField.existsError("totp")}
|
aria-invalid={messagesPerField.existsError("totp")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{messagesPerField.existsError("totp") && (
|
{messagesPerField.existsError("totp") && (
|
||||||
<span id="input-error-otp-code" className={getClassName("kcInputErrorMessageClass")} aria-live="polite">
|
<span id="input-error-otp-code" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||||
{messagesPerField.get("totp")}
|
{messagesPerField.get("totp")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -169,9 +169,9 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
|
|||||||
{mode && <input type="hidden" id="mode" value={mode} />}
|
{mode && <input type="hidden" id="mode" value={mode} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={getClassName("kcFormGroupClass")}>
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
<div className="col-sm-2 col-md-2">
|
<div className="col-sm-2 col-md-2">
|
||||||
<label htmlFor="userLabel" className={getClassName("kcLabelClass")}>
|
<label htmlFor="userLabel" className={kcClsx("kcLabelClass")}>
|
||||||
{msg("totpDeviceName")}
|
{msg("totpDeviceName")}
|
||||||
</label>
|
</label>
|
||||||
{totp.otpCredentials.length >= 1 && <span className="required">*</span>}
|
{totp.otpCredentials.length >= 1 && <span className="required">*</span>}
|
||||||
@ -182,37 +182,28 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
|
|||||||
id="userLabel"
|
id="userLabel"
|
||||||
name="userLabel"
|
name="userLabel"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className={getClassName("kcInputClass")}
|
className={kcClsx("kcInputClass")}
|
||||||
aria-invalid={messagesPerField.existsError("userLabel")}
|
aria-invalid={messagesPerField.existsError("userLabel")}
|
||||||
/>
|
/>
|
||||||
{messagesPerField.existsError("userLabel") && (
|
{messagesPerField.existsError("userLabel") && (
|
||||||
<span id="input-error-otp-label" className={getClassName("kcInputErrorMessageClass")} aria-live="polite">
|
<span id="input-error-otp-label" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||||
{messagesPerField.get("userLabel")}
|
{messagesPerField.get("userLabel")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="kc-form-buttons" className={clsx(getClassName("kcFormGroupClass"), "text-right")}>
|
<div id="kc-form-buttons" className={clsx(kcClsx("kcFormGroupClass"), "text-right")}>
|
||||||
<div className={getClassName("kcInputWrapperClass")}>
|
<div className={kcClsx("kcInputWrapperClass")}>
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
className={clsx(
|
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonLargeClass")}
|
||||||
getClassName("kcButtonClass"),
|
|
||||||
getClassName("kcButtonPrimaryClass"),
|
|
||||||
getClassName("kcButtonLargeClass")
|
|
||||||
)}
|
|
||||||
id="saveTOTPBtn"
|
id="saveTOTPBtn"
|
||||||
value={msgStr("doSave")}
|
value={msgStr("doSave")}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={clsx(
|
className={kcClsx("kcButtonClass", "kcButtonDefaultClass", "kcButtonLargeClass", "kcButtonLargeClass")}
|
||||||
getClassName("kcButtonClass"),
|
|
||||||
getClassName("kcButtonDefaultClass"),
|
|
||||||
getClassName("kcButtonLargeClass"),
|
|
||||||
getClassName("kcButtonLargeClass")
|
|
||||||
)}
|
|
||||||
id="cancelTOTPBtn"
|
id="cancelTOTPBtn"
|
||||||
name="submitAction"
|
name="submitAction"
|
||||||
value="Cancel"
|
value="Cancel"
|
||||||
|
109
src/bin/add-story.ts
Normal file
109
src/bin/add-story.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
|
||||||
|
import cliSelect from "cli-select";
|
||||||
|
import {
|
||||||
|
loginThemePageIds,
|
||||||
|
accountThemePageIds,
|
||||||
|
type LoginThemePageId,
|
||||||
|
type AccountThemePageId,
|
||||||
|
themeTypes,
|
||||||
|
type ThemeType
|
||||||
|
} from "./shared/constants";
|
||||||
|
import { capitalize } from "tsafe/capitalize";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
||||||
|
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
||||||
|
import { assert, Equals } from "tsafe/assert";
|
||||||
|
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
|
||||||
|
import type { CliCommandOptions } from "./main";
|
||||||
|
import { getBuildContext } from "./shared/buildContext";
|
||||||
|
import chalk from "chalk";
|
||||||
|
|
||||||
|
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||||
|
const { cliCommandOptions } = params;
|
||||||
|
|
||||||
|
const buildContext = getBuildContext({
|
||||||
|
cliCommandOptions
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(chalk.cyan("Theme type:"));
|
||||||
|
|
||||||
|
const { value: themeType } = await cliSelect<ThemeType>({
|
||||||
|
values: [...themeTypes]
|
||||||
|
}).catch(() => {
|
||||||
|
process.exit(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`→ ${themeType}`);
|
||||||
|
|
||||||
|
console.log(chalk.cyan("Select the page you want to create a Storybook for:"));
|
||||||
|
|
||||||
|
const { value: pageId } = await cliSelect<LoginThemePageId | AccountThemePageId>({
|
||||||
|
values: (() => {
|
||||||
|
switch (themeType) {
|
||||||
|
case "login":
|
||||||
|
return [...loginThemePageIds];
|
||||||
|
case "account":
|
||||||
|
return [...accountThemePageIds];
|
||||||
|
}
|
||||||
|
assert<Equals<typeof themeType, never>>(false);
|
||||||
|
})()
|
||||||
|
}).catch(() => {
|
||||||
|
process.exit(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`→ ${pageId}`);
|
||||||
|
|
||||||
|
const { themeSrcDirPath } = getThemeSrcDirPath({
|
||||||
|
projectDirPath: buildContext.projectDirPath
|
||||||
|
});
|
||||||
|
|
||||||
|
const componentBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(
|
||||||
|
/ftl$/,
|
||||||
|
"stories.tsx"
|
||||||
|
);
|
||||||
|
|
||||||
|
const targetFilePath = pathJoin(
|
||||||
|
themeSrcDirPath,
|
||||||
|
themeType,
|
||||||
|
"pages",
|
||||||
|
componentBasename
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fs.existsSync(targetFilePath)) {
|
||||||
|
console.log(`${pathRelative(process.cwd(), targetFilePath)} already exists`);
|
||||||
|
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentCode = fs
|
||||||
|
.readFileSync(
|
||||||
|
pathJoin(
|
||||||
|
getThisCodebaseRootDirPath(),
|
||||||
|
"stories",
|
||||||
|
themeType,
|
||||||
|
"pages",
|
||||||
|
componentBasename
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.toString("utf8")
|
||||||
|
.replace('import React from "react";\n', "");
|
||||||
|
|
||||||
|
{
|
||||||
|
const targetDirPath = pathDirname(targetFilePath);
|
||||||
|
|
||||||
|
if (!fs.existsSync(targetDirPath)) {
|
||||||
|
fs.mkdirSync(targetDirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8"));
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
[
|
||||||
|
`${chalk.green("✓")} ${chalk.bold(
|
||||||
|
pathJoin(".", pathRelative(process.cwd(), targetFilePath))
|
||||||
|
)} copy pasted from the Keycloakify source code into your project`,
|
||||||
|
`You can start storybook with ${chalk.bold("yarn storybook")}`
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
}
|
@ -1,13 +1,13 @@
|
|||||||
import { copyKeycloakResourcesToPublic } from "./shared/copyKeycloakResourcesToPublic";
|
import { copyKeycloakResourcesToPublic } from "./shared/copyKeycloakResourcesToPublic";
|
||||||
import { readBuildOptions } from "./shared/buildOptions";
|
import { getBuildContext } from "./shared/buildContext";
|
||||||
import type { CliCommandOptions } from "./main";
|
import type { CliCommandOptions } from "./main";
|
||||||
|
|
||||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||||
const { cliCommandOptions } = params;
|
const { cliCommandOptions } = params;
|
||||||
|
|
||||||
const buildOptions = readBuildOptions({ cliCommandOptions });
|
const buildContext = getBuildContext({ cliCommandOptions });
|
||||||
|
|
||||||
await copyKeycloakResourcesToPublic({
|
await copyKeycloakResourcesToPublic({
|
||||||
buildOptions
|
buildContext
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
|
|
||||||
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
|
|
||||||
import { readBuildOptions } from "./shared/buildOptions";
|
|
||||||
import { downloadKeycloakDefaultTheme } from "./shared/downloadKeycloakDefaultTheme";
|
|
||||||
import { transformCodebase } from "./tools/transformCodebase";
|
|
||||||
import type { CliCommandOptions } from "./main";
|
|
||||||
import chalk from "chalk";
|
|
||||||
|
|
||||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
|
||||||
const { cliCommandOptions } = params;
|
|
||||||
|
|
||||||
const buildOptions = readBuildOptions({
|
|
||||||
cliCommandOptions
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
chalk.cyan(
|
|
||||||
"Select the Keycloak version from which you want to download the builtins theme:"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const { keycloakVersion } = await promptKeycloakVersion({
|
|
||||||
startingFromMajor: undefined,
|
|
||||||
cacheDirPath: buildOptions.cacheDirPath
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`→ ${keycloakVersion}`);
|
|
||||||
|
|
||||||
const destDirPath = pathJoin(
|
|
||||||
buildOptions.keycloakifyBuildDirPath,
|
|
||||||
"src",
|
|
||||||
"main",
|
|
||||||
"resources",
|
|
||||||
"theme"
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
[
|
|
||||||
`Downloading builtins theme of Keycloak ${keycloakVersion} here:`,
|
|
||||||
`- ${chalk.bold(
|
|
||||||
`.${pathSep}${pathJoin(pathRelative(process.cwd(), destDirPath), "base")}`
|
|
||||||
)}`,
|
|
||||||
`- ${chalk.bold(
|
|
||||||
`.${pathSep}${pathJoin(
|
|
||||||
pathRelative(process.cwd(), destDirPath),
|
|
||||||
"keycloak"
|
|
||||||
)}`
|
|
||||||
)}`
|
|
||||||
].join("\n")
|
|
||||||
);
|
|
||||||
|
|
||||||
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
|
||||||
keycloakVersion,
|
|
||||||
buildOptions
|
|
||||||
});
|
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
srcDirPath: defaultThemeDirPath,
|
|
||||||
destDirPath
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(chalk.green(`✓ done`));
|
|
||||||
}
|
|
@ -17,13 +17,13 @@ import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
|||||||
import { assert, Equals } from "tsafe/assert";
|
import { assert, Equals } from "tsafe/assert";
|
||||||
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
|
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
|
||||||
import type { CliCommandOptions } from "./main";
|
import type { CliCommandOptions } from "./main";
|
||||||
import { readBuildOptions } from "./shared/buildOptions";
|
import { getBuildContext } from "./shared/buildContext";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
|
||||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||||
const { cliCommandOptions } = params;
|
const { cliCommandOptions } = params;
|
||||||
|
|
||||||
const buildOptions = readBuildOptions({
|
const buildContext = getBuildContext({
|
||||||
cliCommandOptions
|
cliCommandOptions
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -39,13 +39,26 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
|
|
||||||
console.log(chalk.cyan("Select the page you want to customize:"));
|
console.log(chalk.cyan("Select the page you want to customize:"));
|
||||||
|
|
||||||
const { value: pageId } = await cliSelect<LoginThemePageId | AccountThemePageId>({
|
const templateValue = "Template.tsx (Layout common to every page)";
|
||||||
|
const userProfileFormFieldsValue =
|
||||||
|
"UserProfileFormFields.tsx (Renders the form of the register.ftl, login-update-profile.ftl, update-email.ftl and idp-review-user-profile.ftl)";
|
||||||
|
|
||||||
|
const { value: pageIdOrComponent } = await cliSelect<
|
||||||
|
| LoginThemePageId
|
||||||
|
| AccountThemePageId
|
||||||
|
| typeof templateValue
|
||||||
|
| typeof userProfileFormFieldsValue
|
||||||
|
>({
|
||||||
values: (() => {
|
values: (() => {
|
||||||
switch (themeType) {
|
switch (themeType) {
|
||||||
case "login":
|
case "login":
|
||||||
return [...loginThemePageIds];
|
return [
|
||||||
|
templateValue,
|
||||||
|
userProfileFormFieldsValue,
|
||||||
|
...loginThemePageIds
|
||||||
|
];
|
||||||
case "account":
|
case "account":
|
||||||
return [...accountThemePageIds];
|
return [templateValue, ...accountThemePageIds];
|
||||||
}
|
}
|
||||||
assert<Equals<typeof themeType, never>>(false);
|
assert<Equals<typeof themeType, never>>(false);
|
||||||
})()
|
})()
|
||||||
@ -53,27 +66,45 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`→ ${pageId}`);
|
console.log(`→ ${pageIdOrComponent}`);
|
||||||
|
|
||||||
const componentPageBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(
|
|
||||||
/ftl$/,
|
|
||||||
"tsx"
|
|
||||||
);
|
|
||||||
|
|
||||||
const { themeSrcDirPath } = getThemeSrcDirPath({
|
const { themeSrcDirPath } = getThemeSrcDirPath({
|
||||||
reactAppRootDirPath: buildOptions.reactAppRootDirPath
|
projectDirPath: buildContext.projectDirPath
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const componentBasename = (() => {
|
||||||
|
if (pageIdOrComponent === templateValue) {
|
||||||
|
return "Template.tsx";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageIdOrComponent === userProfileFormFieldsValue) {
|
||||||
|
return "UserProfileFormFields.tsx";
|
||||||
|
}
|
||||||
|
|
||||||
|
return capitalize(kebabCaseToCamelCase(pageIdOrComponent)).replace(/ftl$/, "tsx");
|
||||||
|
})();
|
||||||
|
|
||||||
|
const pagesOrDot = (() => {
|
||||||
|
if (
|
||||||
|
pageIdOrComponent === templateValue ||
|
||||||
|
pageIdOrComponent === userProfileFormFieldsValue
|
||||||
|
) {
|
||||||
|
return ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "pages";
|
||||||
|
})();
|
||||||
|
|
||||||
const targetFilePath = pathJoin(
|
const targetFilePath = pathJoin(
|
||||||
themeSrcDirPath,
|
themeSrcDirPath,
|
||||||
themeType,
|
themeType,
|
||||||
"pages",
|
pagesOrDot,
|
||||||
componentPageBasename
|
componentBasename
|
||||||
);
|
);
|
||||||
|
|
||||||
if (fs.existsSync(targetFilePath)) {
|
if (fs.existsSync(targetFilePath)) {
|
||||||
console.log(
|
console.log(
|
||||||
`${pageId} is already ejected, ${pathRelative(
|
`${pageIdOrComponent} is already ejected, ${pathRelative(
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
targetFilePath
|
targetFilePath
|
||||||
)} already exists`
|
)} already exists`
|
||||||
@ -82,6 +113,18 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const componentCode = fs
|
||||||
|
.readFileSync(
|
||||||
|
pathJoin(
|
||||||
|
getThisCodebaseRootDirPath(),
|
||||||
|
"src",
|
||||||
|
themeType,
|
||||||
|
pagesOrDot,
|
||||||
|
componentBasename
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.toString("utf8");
|
||||||
|
|
||||||
{
|
{
|
||||||
const targetDirPath = pathDirname(targetFilePath);
|
const targetDirPath = pathDirname(targetFilePath);
|
||||||
|
|
||||||
@ -90,28 +133,66 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const componentPageContent = fs
|
fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8"));
|
||||||
.readFileSync(
|
|
||||||
pathJoin(
|
|
||||||
getThisCodebaseRootDirPath(),
|
|
||||||
"src",
|
|
||||||
themeType,
|
|
||||||
"pages",
|
|
||||||
componentPageBasename
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.toString("utf8");
|
|
||||||
|
|
||||||
fs.writeFileSync(targetFilePath, Buffer.from(componentPageContent, "utf8"));
|
console.log(
|
||||||
|
`${chalk.green("✓")} ${chalk.bold(
|
||||||
|
pathJoin(".", pathRelative(process.cwd(), targetFilePath))
|
||||||
|
)} copy pasted from the Keycloakify source code into your project`
|
||||||
|
);
|
||||||
|
|
||||||
|
edit_KcApp: {
|
||||||
|
if (
|
||||||
|
pageIdOrComponent !== templateValue &&
|
||||||
|
pageIdOrComponent !== userProfileFormFieldsValue
|
||||||
|
) {
|
||||||
|
break edit_KcApp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kcAppTsxPath = pathJoin(themeSrcDirPath, themeType, "KcPage.tsx");
|
||||||
|
|
||||||
|
const kcAppTsxCode = fs.readFileSync(kcAppTsxPath).toString("utf8");
|
||||||
|
|
||||||
|
const modifiedKcAppTsxCode = (() => {
|
||||||
|
switch (pageIdOrComponent) {
|
||||||
|
case templateValue:
|
||||||
|
return kcAppTsxCode.replace(
|
||||||
|
`keycloakify/${themeType}/Template`,
|
||||||
|
"./Template"
|
||||||
|
);
|
||||||
|
case userProfileFormFieldsValue:
|
||||||
|
return kcAppTsxCode.replace(
|
||||||
|
`keycloakify/login/UserProfileFormFields`,
|
||||||
|
"./UserProfileFormFields"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert<Equals<typeof pageIdOrComponent, never>>(false);
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (kcAppTsxCode === modifiedKcAppTsxCode) {
|
||||||
|
console.log(
|
||||||
|
chalk.red(
|
||||||
|
"Unable to automatically update KcPage.tsx, please update it manually"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(kcAppTsxPath, Buffer.from(modifiedKcAppTsxCode, "utf8"));
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`${chalk.green("✓")} ${chalk.bold(
|
||||||
|
pathJoin(".", pathRelative(process.cwd(), kcAppTsxPath))
|
||||||
|
)} Updated`
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const userProfileFormFieldComponentName = "UserProfileFormFields";
|
const userProfileFormFieldComponentName = "UserProfileFormFields";
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
[
|
[
|
||||||
``,
|
|
||||||
`${chalk.green("✓")} ${chalk.bold(
|
|
||||||
pathJoin(".", pathRelative(process.cwd(), targetFilePath))
|
|
||||||
)} copy pasted from the Keycloakify source code into your project`,
|
|
||||||
``,
|
``,
|
||||||
`You now need to update your page router:`,
|
`You now need to update your page router:`,
|
||||||
``,
|
``,
|
||||||
@ -120,21 +201,21 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
".",
|
".",
|
||||||
pathRelative(process.cwd(), themeSrcDirPath),
|
pathRelative(process.cwd(), themeSrcDirPath),
|
||||||
themeType,
|
themeType,
|
||||||
"KcApp.tsx"
|
"KcPage.tsx"
|
||||||
)
|
)
|
||||||
)}:`,
|
)}:`,
|
||||||
chalk.grey("```"),
|
chalk.grey("```"),
|
||||||
`// ...`,
|
`// ...`,
|
||||||
``,
|
``,
|
||||||
chalk.green(
|
chalk.green(
|
||||||
`+const ${componentPageBasename.replace(
|
`+const ${componentBasename.replace(
|
||||||
/.tsx$/,
|
/.tsx$/,
|
||||||
""
|
""
|
||||||
)} = lazy(() => import("./pages/${componentPageBasename}"));`
|
)} = lazy(() => import("./pages/${componentBasename}"));`
|
||||||
),
|
),
|
||||||
...[
|
...[
|
||||||
``,
|
``,
|
||||||
` export default function KcApp(props: { kcContext: KcContext; }) {`,
|
` export default function KcPage(props: { kcContext: KcContext; }) {`,
|
||||||
``,
|
``,
|
||||||
` // ...`,
|
` // ...`,
|
||||||
``,
|
``,
|
||||||
@ -143,16 +224,17 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
` {(() => {`,
|
` {(() => {`,
|
||||||
` switch (kcContext.pageId) {`,
|
` switch (kcContext.pageId) {`,
|
||||||
` // ...`,
|
` // ...`,
|
||||||
`+ case "${pageId}": return (`,
|
`+ case "${pageIdOrComponent}": return (`,
|
||||||
`+ <Login`,
|
`+ <${componentBasename}`,
|
||||||
`+ {...{ kcContext, i18n, classes }}`,
|
`+ {...{ kcContext, i18n, classes }}`,
|
||||||
`+ Template={Template}`,
|
`+ Template={Template}`,
|
||||||
...(!componentPageContent.includes(userProfileFormFieldComponentName)
|
`+ doUseDefaultCss={true}`,
|
||||||
|
...(!componentCode.includes(userProfileFormFieldComponentName)
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
`+ ${userProfileFormFieldComponentName}={${userProfileFormFieldComponentName}}`
|
`+ ${userProfileFormFieldComponentName}={${userProfileFormFieldComponentName}}`,
|
||||||
|
`+ doMakeUserConfirmPassword={doMakeUserConfirmPassword}`
|
||||||
]),
|
]),
|
||||||
`+ doUseDefaultCss={true}`,
|
|
||||||
`+ />`,
|
`+ />`,
|
||||||
`+ );`,
|
`+ );`,
|
||||||
` default: return <Fallback /* .. */ />;`,
|
` default: return <Fallback /* .. */ />;`,
|
||||||
|
@ -2,7 +2,7 @@ import { downloadKeycloakDefaultTheme } from "./shared/downloadKeycloakDefaultTh
|
|||||||
import { join as pathJoin, relative as pathRelative } from "path";
|
import { join as pathJoin, relative as pathRelative } from "path";
|
||||||
import { transformCodebase } from "./tools/transformCodebase";
|
import { transformCodebase } from "./tools/transformCodebase";
|
||||||
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
|
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
|
||||||
import { readBuildOptions } from "./shared/buildOptions";
|
import { getBuildContext } from "./shared/buildContext";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
|
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
|
||||||
import type { CliCommandOptions } from "./main";
|
import type { CliCommandOptions } from "./main";
|
||||||
@ -10,10 +10,10 @@ import type { CliCommandOptions } from "./main";
|
|||||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||||
const { cliCommandOptions } = params;
|
const { cliCommandOptions } = params;
|
||||||
|
|
||||||
const buildOptions = readBuildOptions({ cliCommandOptions });
|
const buildContext = getBuildContext({ cliCommandOptions });
|
||||||
|
|
||||||
const { themeSrcDirPath } = getThemeSrcDirPath({
|
const { themeSrcDirPath } = getThemeSrcDirPath({
|
||||||
reactAppRootDirPath: buildOptions.reactAppRootDirPath
|
projectDirPath: buildContext.projectDirPath
|
||||||
});
|
});
|
||||||
|
|
||||||
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
|
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
|
||||||
@ -34,12 +34,12 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
const { keycloakVersion } = await promptKeycloakVersion({
|
const { keycloakVersion } = await promptKeycloakVersion({
|
||||||
// NOTE: This is arbitrary
|
// NOTE: This is arbitrary
|
||||||
startingFromMajor: 17,
|
startingFromMajor: 17,
|
||||||
cacheDirPath: buildOptions.cacheDirPath
|
cacheDirPath: buildContext.cacheDirPath
|
||||||
});
|
});
|
||||||
|
|
||||||
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
||||||
keycloakVersion,
|
keycloakVersion,
|
||||||
buildOptions
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
transformCodebase({
|
transformCodebase({
|
||||||
|
@ -5,12 +5,12 @@ import type {
|
|||||||
} from "./extensionVersions";
|
} from "./extensionVersions";
|
||||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||||
import { transformCodebase } from "../../tools/transformCodebase";
|
import { transformCodebase } from "../../tools/transformCodebase";
|
||||||
import type { BuildOptions } from "../../shared/buildOptions";
|
import type { BuildContext } from "../../shared/buildContext";
|
||||||
import * as fs from "fs/promises";
|
import * as fs from "fs/promises";
|
||||||
import { accountV1ThemeName } from "../../shared/constants";
|
import { accountV1ThemeName } from "../../shared/constants";
|
||||||
import {
|
import {
|
||||||
generatePom,
|
generatePom,
|
||||||
BuildOptionsLike as BuildOptionsLike_generatePom
|
BuildContextLike as BuildContextLike_generatePom
|
||||||
} from "./generatePom";
|
} from "./generatePom";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { isInside } from "../../tools/isInside";
|
import { isInside } from "../../tools/isInside";
|
||||||
@ -18,7 +18,7 @@ import child_process from "child_process";
|
|||||||
import { rmSync } from "../../tools/fs.rmSync";
|
import { rmSync } from "../../tools/fs.rmSync";
|
||||||
import { getMetaInfKeycloakThemesJsonFilePath } from "../../shared/metaInfKeycloakThemes";
|
import { getMetaInfKeycloakThemesJsonFilePath } from "../../shared/metaInfKeycloakThemes";
|
||||||
|
|
||||||
export type BuildOptionsLike = BuildOptionsLike_generatePom & {
|
export type BuildContextLike = BuildContextLike_generatePom & {
|
||||||
keycloakifyBuildDirPath: string;
|
keycloakifyBuildDirPath: string;
|
||||||
themeNames: string[];
|
themeNames: string[];
|
||||||
artifactId: string;
|
artifactId: string;
|
||||||
@ -26,23 +26,25 @@ export type BuildOptionsLike = BuildOptionsLike_generatePom & {
|
|||||||
cacheDirPath: string;
|
cacheDirPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export async function buildJar(params: {
|
export async function buildJar(params: {
|
||||||
jarFileBasename: string;
|
jarFileBasename: string;
|
||||||
keycloakAccountV1Version: KeycloakAccountV1Version;
|
keycloakAccountV1Version: KeycloakAccountV1Version;
|
||||||
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
|
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
|
||||||
buildOptions: BuildOptionsLike;
|
resourcesDirPath: string;
|
||||||
|
buildContext: BuildContextLike;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const {
|
const {
|
||||||
jarFileBasename,
|
jarFileBasename,
|
||||||
keycloakAccountV1Version,
|
keycloakAccountV1Version,
|
||||||
keycloakThemeAdditionalInfoExtensionVersion,
|
keycloakThemeAdditionalInfoExtensionVersion,
|
||||||
buildOptions
|
resourcesDirPath,
|
||||||
|
buildContext
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
const keycloakifyBuildTmpDirPath = pathJoin(
|
const keycloakifyBuildTmpDirPath = pathJoin(
|
||||||
buildOptions.cacheDirPath,
|
buildContext.cacheDirPath,
|
||||||
jarFileBasename.replace(".jar", "")
|
jarFileBasename.replace(".jar", "")
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -57,15 +59,15 @@ export async function buildJar(params: {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
fileRelativePath ===
|
fileRelativePath ===
|
||||||
getMetaInfKeycloakThemesJsonFilePath({ keycloakifyBuildDirPath: "." })
|
getMetaInfKeycloakThemesJsonFilePath({ resourcesDirPath: "." })
|
||||||
) {
|
) {
|
||||||
return { modifiedSourceCode: sourceCode };
|
return { modifiedSourceCode: sourceCode };
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const themeName of [...buildOptions.themeNames, accountV1ThemeName]) {
|
for (const themeName of [...buildContext.themeNames, accountV1ThemeName]) {
|
||||||
if (
|
if (
|
||||||
isInside({
|
isInside({
|
||||||
dirPath: pathJoin("src", "main", "resources", "theme", themeName),
|
dirPath: pathJoin("theme", themeName),
|
||||||
filePath: fileRelativePath
|
filePath: fileRelativePath
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
@ -87,13 +89,7 @@ export async function buildJar(params: {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
isInside({
|
isInside({
|
||||||
dirPath: pathJoin(
|
dirPath: pathJoin("theme", accountV1ThemeName),
|
||||||
"src",
|
|
||||||
"main",
|
|
||||||
"resources",
|
|
||||||
"theme",
|
|
||||||
accountV1ThemeName
|
|
||||||
),
|
|
||||||
filePath: fileRelativePath
|
filePath: fileRelativePath
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
@ -103,7 +99,7 @@ export async function buildJar(params: {
|
|||||||
if (
|
if (
|
||||||
fileRelativePath ===
|
fileRelativePath ===
|
||||||
getMetaInfKeycloakThemesJsonFilePath({
|
getMetaInfKeycloakThemesJsonFilePath({
|
||||||
keycloakifyBuildDirPath: "."
|
resourcesDirPath: "."
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
const keycloakThemesJsonParsed = JSON.parse(
|
const keycloakThemesJsonParsed = JSON.parse(
|
||||||
@ -125,18 +121,10 @@ export async function buildJar(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const themeName of buildOptions.themeNames) {
|
for (const themeName of buildContext.themeNames) {
|
||||||
if (
|
if (
|
||||||
fileRelativePath ===
|
fileRelativePath ===
|
||||||
pathJoin(
|
pathJoin("theme", themeName, "account", "theme.properties")
|
||||||
"src",
|
|
||||||
"main",
|
|
||||||
"resources",
|
|
||||||
"theme",
|
|
||||||
themeName,
|
|
||||||
"account",
|
|
||||||
"theme.properties"
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
const modifiedSourceCode = Buffer.from(
|
const modifiedSourceCode = Buffer.from(
|
||||||
sourceCode
|
sourceCode
|
||||||
@ -160,8 +148,8 @@ export async function buildJar(params: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
transformCodebase({
|
transformCodebase({
|
||||||
srcDirPath: buildOptions.keycloakifyBuildDirPath,
|
srcDirPath: resourcesDirPath,
|
||||||
destDirPath: keycloakifyBuildTmpDirPath,
|
destDirPath: pathJoin(keycloakifyBuildTmpDirPath, "src", "main", "resources"),
|
||||||
transformSourceCode: params => {
|
transformSourceCode: params => {
|
||||||
const resultCommon = transformCodebase_common(params);
|
const resultCommon = transformCodebase_common(params);
|
||||||
|
|
||||||
@ -203,7 +191,7 @@ export async function buildJar(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
(["register.ftl", "login-update-profile.ftl"] as const).forEach(pageId =>
|
(["register.ftl", "login-update-profile.ftl"] as const).forEach(pageId =>
|
||||||
buildOptions.themeNames.map(themeName => {
|
buildContext.themeNames.map(themeName => {
|
||||||
const ftlFilePath = pathJoin(
|
const ftlFilePath = pathJoin(
|
||||||
keycloakifyBuildTmpDirPath,
|
keycloakifyBuildTmpDirPath,
|
||||||
"src",
|
"src",
|
||||||
@ -244,7 +232,7 @@ export async function buildJar(params: {
|
|||||||
|
|
||||||
{
|
{
|
||||||
const { pomFileCode } = generatePom({
|
const { pomFileCode } = generatePom({
|
||||||
buildOptions,
|
buildContext,
|
||||||
keycloakAccountV1Version,
|
keycloakAccountV1Version,
|
||||||
keycloakThemeAdditionalInfoExtensionVersion
|
keycloakThemeAdditionalInfoExtensionVersion
|
||||||
});
|
});
|
||||||
@ -285,9 +273,9 @@ export async function buildJar(params: {
|
|||||||
pathJoin(
|
pathJoin(
|
||||||
keycloakifyBuildTmpDirPath,
|
keycloakifyBuildTmpDirPath,
|
||||||
"target",
|
"target",
|
||||||
`${buildOptions.artifactId}-${buildOptions.themeVersion}.jar`
|
`${buildContext.artifactId}-${buildContext.themeVersion}.jar`
|
||||||
),
|
),
|
||||||
pathJoin(buildOptions.keycloakifyBuildDirPath, jarFileBasename)
|
pathJoin(buildContext.keycloakifyBuildDirPath, jarFileBasename)
|
||||||
);
|
);
|
||||||
|
|
||||||
rmSync(keycloakifyBuildTmpDirPath, { recursive: true });
|
rmSync(keycloakifyBuildTmpDirPath, { recursive: true });
|
||||||
|
@ -5,25 +5,27 @@ import {
|
|||||||
keycloakThemeAdditionalInfoExtensionVersions
|
keycloakThemeAdditionalInfoExtensionVersions
|
||||||
} from "./extensionVersions";
|
} from "./extensionVersions";
|
||||||
import { getKeycloakVersionRangeForJar } from "./getKeycloakVersionRangeForJar";
|
import { getKeycloakVersionRangeForJar } from "./getKeycloakVersionRangeForJar";
|
||||||
import { buildJar, BuildOptionsLike as BuildOptionsLike_buildJar } from "./buildJar";
|
import { buildJar, BuildContextLike as BuildContextLike_buildJar } from "./buildJar";
|
||||||
import type { BuildOptions } from "../../shared/buildOptions";
|
import type { BuildContext } from "../../shared/buildContext";
|
||||||
import { getJarFileBasename } from "../../shared/getJarFileBasename";
|
import { getJarFileBasename } from "../../shared/getJarFileBasename";
|
||||||
import { readMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
|
import { readMetaInfKeycloakThemes_fromResourcesDirPath } from "../../shared/metaInfKeycloakThemes";
|
||||||
import { accountV1ThemeName } from "../../shared/constants";
|
import { accountV1ThemeName } from "../../shared/constants";
|
||||||
|
|
||||||
export type BuildOptionsLike = BuildOptionsLike_buildJar & {
|
export type BuildContextLike = BuildContextLike_buildJar & {
|
||||||
keycloakifyBuildDirPath: string;
|
keycloakifyBuildDirPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export async function buildJars(params: {
|
export async function buildJars(params: {
|
||||||
buildOptions: BuildOptionsLike;
|
resourcesDirPath: string;
|
||||||
|
onlyBuildJarFileBasename: string | undefined;
|
||||||
|
buildContext: BuildContextLike;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { buildOptions } = params;
|
const { onlyBuildJarFileBasename, resourcesDirPath, buildContext } = params;
|
||||||
|
|
||||||
const doesImplementAccountTheme = readMetaInfKeycloakThemes({
|
const doesImplementAccountTheme = readMetaInfKeycloakThemes_fromResourcesDirPath({
|
||||||
keycloakifyBuildDirPath: buildOptions.keycloakifyBuildDirPath
|
resourcesDirPath
|
||||||
}).themes.some(({ name }) => name === accountV1ThemeName);
|
}).themes.some(({ name }) => name === accountV1ThemeName);
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@ -56,12 +58,20 @@ export async function buildJars(params: {
|
|||||||
keycloakVersionRange
|
keycloakVersionRange
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
onlyBuildJarFileBasename !== undefined &&
|
||||||
|
onlyBuildJarFileBasename !== jarFileBasename
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
keycloakThemeAdditionalInfoExtensionVersion,
|
keycloakThemeAdditionalInfoExtensionVersion,
|
||||||
jarFileBasename
|
jarFileBasename
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
.filter(exclude(undefined))
|
||||||
.map(
|
.map(
|
||||||
({
|
({
|
||||||
keycloakThemeAdditionalInfoExtensionVersion,
|
keycloakThemeAdditionalInfoExtensionVersion,
|
||||||
@ -71,7 +81,8 @@ export async function buildJars(params: {
|
|||||||
jarFileBasename,
|
jarFileBasename,
|
||||||
keycloakAccountV1Version,
|
keycloakAccountV1Version,
|
||||||
keycloakThemeAdditionalInfoExtensionVersion,
|
keycloakThemeAdditionalInfoExtensionVersion,
|
||||||
buildOptions
|
resourcesDirPath,
|
||||||
|
buildContext
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// NOTE: v0.5 is a dummy version.
|
// NOTE: v0.5 is a dummy version.
|
||||||
export const keycloakAccountV1Versions = [null, "0.3", "0.4"] as const;
|
export const keycloakAccountV1Versions = [null, "0.3", "0.4", "0.6"] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://central.sonatype.com/artifact/io.phasetwo.keycloak/keycloak-account-v1
|
* https://central.sonatype.com/artifact/io.phasetwo.keycloak/keycloak-account-v1
|
||||||
|
@ -1,27 +1,27 @@
|
|||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import type { BuildOptions } from "../../shared/buildOptions";
|
import type { BuildContext } from "../../shared/buildContext";
|
||||||
import type {
|
import type {
|
||||||
KeycloakAccountV1Version,
|
KeycloakAccountV1Version,
|
||||||
KeycloakThemeAdditionalInfoExtensionVersion
|
KeycloakThemeAdditionalInfoExtensionVersion
|
||||||
} from "./extensionVersions";
|
} from "./extensionVersions";
|
||||||
|
|
||||||
export type BuildOptionsLike = {
|
export type BuildContextLike = {
|
||||||
groupId: string;
|
groupId: string;
|
||||||
artifactId: string;
|
artifactId: string;
|
||||||
themeVersion: string;
|
themeVersion: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export function generatePom(params: {
|
export function generatePom(params: {
|
||||||
keycloakAccountV1Version: KeycloakAccountV1Version;
|
keycloakAccountV1Version: KeycloakAccountV1Version;
|
||||||
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
|
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
|
||||||
buildOptions: BuildOptionsLike;
|
buildContext: BuildContextLike;
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
keycloakAccountV1Version,
|
keycloakAccountV1Version,
|
||||||
keycloakThemeAdditionalInfoExtensionVersion,
|
keycloakThemeAdditionalInfoExtensionVersion,
|
||||||
buildOptions
|
buildContext
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
const { pomFileCode } = (function generatePomFileCode(): {
|
const { pomFileCode } = (function generatePomFileCode(): {
|
||||||
@ -33,10 +33,10 @@ export function generatePom(params: {
|
|||||||
` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`,
|
` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`,
|
||||||
` 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>${buildOptions.groupId}</groupId>`,
|
` <groupId>${buildContext.groupId}</groupId>`,
|
||||||
` <artifactId>${buildOptions.artifactId}</artifactId>`,
|
` <artifactId>${buildContext.artifactId}</artifactId>`,
|
||||||
` <version>${buildOptions.themeVersion}</version>`,
|
` <version>${buildContext.themeVersion}</version>`,
|
||||||
` <name>${buildOptions.artifactId}</name>`,
|
` <name>${buildContext.artifactId}</name>`,
|
||||||
` <description />`,
|
` <description />`,
|
||||||
` <packaging>jar</packaging>`,
|
` <packaging>jar</packaging>`,
|
||||||
` <properties>`,
|
` <properties>`,
|
||||||
|
@ -44,12 +44,20 @@ export function getKeycloakVersionRangeForJar(params: {
|
|||||||
case null:
|
case null:
|
||||||
return undefined;
|
return undefined;
|
||||||
case "1.1.5":
|
case "1.1.5":
|
||||||
return "24-and-above" as const;
|
return "24" as const;
|
||||||
}
|
}
|
||||||
assert<
|
assert<
|
||||||
Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>
|
Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>
|
||||||
>(false);
|
>(false);
|
||||||
|
case "0.6":
|
||||||
|
switch (keycloakThemeAdditionalInfoExtensionVersion) {
|
||||||
|
case null:
|
||||||
|
return undefined;
|
||||||
|
case "1.1.5":
|
||||||
|
return "25-and-above" as const;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
assert<Equals<typeof keycloakAccountV1Version, never>>(false);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
assert<
|
assert<
|
||||||
@ -65,7 +73,6 @@ export function getKeycloakVersionRangeForJar(params: {
|
|||||||
if (keycloakAccountV1Version !== null) {
|
if (keycloakAccountV1Version !== null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (keycloakThemeAdditionalInfoExtensionVersion) {
|
switch (keycloakThemeAdditionalInfoExtensionVersion) {
|
||||||
case null:
|
case null:
|
||||||
return "21-and-below";
|
return "21-and-below";
|
||||||
|
@ -184,6 +184,28 @@ try {
|
|||||||
};
|
};
|
||||||
</#if>
|
</#if>
|
||||||
|
|
||||||
|
attributes_to_attributesByName: {
|
||||||
|
|
||||||
|
if( !out["profile"] ){
|
||||||
|
break attributes_to_attributesByName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( !out["profile"]["attributes"] ){
|
||||||
|
break attributes_to_attributesByName;
|
||||||
|
}
|
||||||
|
|
||||||
|
var attributes = out["profile"]["attributes"];
|
||||||
|
|
||||||
|
delete out["profile"]["attributes"];
|
||||||
|
|
||||||
|
out["profile"]["attributesByName"] = {};
|
||||||
|
|
||||||
|
attributes.forEach(function(attribute){
|
||||||
|
out["profile"]["attributesByName"][attribute.name] = attribute;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
|
|
||||||
function decodeHtmlEntities(htmlStr){
|
function decodeHtmlEntities(htmlStr){
|
||||||
@ -287,8 +309,8 @@ function decodeHtmlEntities(htmlStr){
|
|||||||
key == "realmAttributes" &&
|
key == "realmAttributes" &&
|
||||||
are_same_path(path, [])
|
are_same_path(path, [])
|
||||||
) || (
|
) || (
|
||||||
<#-- attributesByName adds a lot of noise to the output and is not needed -->
|
<#-- attributesByName adds a lot of noise to the output and is not needed, we already have profile.attributes -->
|
||||||
key == "attributes" &&
|
key == "attributesByName" &&
|
||||||
are_same_path(path, ["profile"])
|
are_same_path(path, ["profile"])
|
||||||
) || (
|
) || (
|
||||||
<#-- We already have the attributes in profile speedup the rendering by filtering it out from the register object -->
|
<#-- We already have the attributes in profile speedup the rendering by filtering it out from the register object -->
|
||||||
@ -300,6 +322,8 @@ function decodeHtmlEntities(htmlStr){
|
|||||||
<#continue>
|
<#continue>
|
||||||
</#if>
|
</#if>
|
||||||
|
|
||||||
|
USER_DEFINED_EXCLUSIONS_eKsaY4ZsZ4eMr2
|
||||||
|
|
||||||
<#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
|
<#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
|
||||||
<#if (
|
<#if (
|
||||||
["register.ftl", "register-user-profile.ftl", "info.ftl", "login.ftl", "login-update-password.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) &&
|
["register.ftl", "register-user-profile.ftl", "info.ftl", "login.ftl", "login-update-password.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) &&
|
||||||
|
@ -4,7 +4,7 @@ import { generateCssCodeToDefineGlobals } from "../replacers/replaceImportsInCss
|
|||||||
import { replaceImportsInInlineCssCode } from "../replacers/replaceImportsInInlineCssCode";
|
import { replaceImportsInInlineCssCode } from "../replacers/replaceImportsInInlineCssCode";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import type { BuildOptions } from "../../shared/buildOptions";
|
import type { BuildContext } from "../../shared/buildContext";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import {
|
import {
|
||||||
type ThemeType,
|
type ThemeType,
|
||||||
@ -15,21 +15,22 @@ import {
|
|||||||
} from "../../shared/constants";
|
} from "../../shared/constants";
|
||||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||||
|
|
||||||
export type BuildOptionsLike = {
|
export type BuildContextLike = {
|
||||||
bundler: "vite" | "webpack";
|
bundler: "vite" | "webpack";
|
||||||
themeVersion: string;
|
themeVersion: string;
|
||||||
urlPathname: string | undefined;
|
urlPathname: string | undefined;
|
||||||
reactAppBuildDirPath: string;
|
projectBuildDirPath: string;
|
||||||
assetsDirPath: string;
|
assetsDirPath: string;
|
||||||
|
kcContextExclusionsFtlCode: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export function generateFtlFilesCodeFactory(params: {
|
export function generateFtlFilesCodeFactory(params: {
|
||||||
themeName: string;
|
themeName: string;
|
||||||
indexHtmlCode: string;
|
indexHtmlCode: string;
|
||||||
cssGlobalsToDefine: Record<string, string>;
|
cssGlobalsToDefine: Record<string, string>;
|
||||||
buildOptions: BuildOptionsLike;
|
buildContext: BuildContextLike;
|
||||||
keycloakifyVersion: string;
|
keycloakifyVersion: string;
|
||||||
themeType: ThemeType;
|
themeType: ThemeType;
|
||||||
fieldNames: string[];
|
fieldNames: string[];
|
||||||
@ -38,7 +39,7 @@ export function generateFtlFilesCodeFactory(params: {
|
|||||||
themeName,
|
themeName,
|
||||||
cssGlobalsToDefine,
|
cssGlobalsToDefine,
|
||||||
indexHtmlCode,
|
indexHtmlCode,
|
||||||
buildOptions,
|
buildContext,
|
||||||
keycloakifyVersion,
|
keycloakifyVersion,
|
||||||
themeType,
|
themeType,
|
||||||
fieldNames
|
fieldNames
|
||||||
@ -54,7 +55,7 @@ export function generateFtlFilesCodeFactory(params: {
|
|||||||
|
|
||||||
const { fixedJsCode } = replaceImportsInJsCode({
|
const { fixedJsCode } = replaceImportsInJsCode({
|
||||||
jsCode,
|
jsCode,
|
||||||
buildOptions
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
$(element).text(fixedJsCode);
|
$(element).text(fixedJsCode);
|
||||||
@ -67,7 +68,7 @@ export function generateFtlFilesCodeFactory(params: {
|
|||||||
|
|
||||||
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||||
cssCode,
|
cssCode,
|
||||||
buildOptions
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
$(element).text(fixedCssCode);
|
$(element).text(fixedCssCode);
|
||||||
@ -90,7 +91,7 @@ export function generateFtlFilesCodeFactory(params: {
|
|||||||
attrName,
|
attrName,
|
||||||
href.replace(
|
href.replace(
|
||||||
new RegExp(
|
new RegExp(
|
||||||
`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`
|
`^${(buildContext.urlPathname ?? "/").replace(/\//g, "\\/")}`
|
||||||
),
|
),
|
||||||
`\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/`
|
`\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/`
|
||||||
)
|
)
|
||||||
@ -105,7 +106,7 @@ export function generateFtlFilesCodeFactory(params: {
|
|||||||
"<style>",
|
"<style>",
|
||||||
generateCssCodeToDefineGlobals({
|
generateCssCodeToDefineGlobals({
|
||||||
cssGlobalsToDefine,
|
cssGlobalsToDefine,
|
||||||
buildOptions
|
buildContext
|
||||||
}).cssCodeToPrependInHead,
|
}).cssCodeToPrependInHead,
|
||||||
"</style>",
|
"</style>",
|
||||||
""
|
""
|
||||||
@ -133,13 +134,17 @@ export function generateFtlFilesCodeFactory(params: {
|
|||||||
fieldNames.map(name => `"${name}"`).join(", ")
|
fieldNames.map(name => `"${name}"`).join(", ")
|
||||||
)
|
)
|
||||||
.replace("KEYCLOAKIFY_VERSION_xEdKd3xEdr", keycloakifyVersion)
|
.replace("KEYCLOAKIFY_VERSION_xEdKd3xEdr", keycloakifyVersion)
|
||||||
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion)
|
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildContext.themeVersion)
|
||||||
.replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType)
|
.replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType)
|
||||||
.replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", themeName)
|
.replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", themeName)
|
||||||
.replace("RESOURCES_COMMON_cLsLsMrtDkpVv", resources_common)
|
.replace("RESOURCES_COMMON_cLsLsMrtDkpVv", resources_common)
|
||||||
.replace(
|
.replace(
|
||||||
"lOCALIZATION_REALM_OVERRIDES_USER_PROFILE_PROPERTY_KEY_aaGLsPgGIdeeX",
|
"lOCALIZATION_REALM_OVERRIDES_USER_PROFILE_PROPERTY_KEY_aaGLsPgGIdeeX",
|
||||||
nameOfTheLocalizationRealmOverridesUserProfileProperty
|
nameOfTheLocalizationRealmOverridesUserProfileProperty
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"USER_DEFINED_EXCLUSIONS_eKsaY4ZsZ4eMr2",
|
||||||
|
buildContext.kcContextExclusionsFtlCode ?? ""
|
||||||
);
|
);
|
||||||
const ftlObjectToJsCodeDeclaringAnObjectPlaceholder =
|
const ftlObjectToJsCodeDeclaringAnObjectPlaceholder =
|
||||||
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }';
|
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import type { BuildOptions } from "../../shared/buildOptions";
|
import type { BuildContext } from "../../shared/buildContext";
|
||||||
import {
|
import {
|
||||||
resources_common,
|
resources_common,
|
||||||
lastKeycloakVersionWithAccountV1,
|
lastKeycloakVersionWithAccountV1,
|
||||||
@ -10,27 +10,26 @@ import {
|
|||||||
import { downloadKeycloakDefaultTheme } from "../../shared/downloadKeycloakDefaultTheme";
|
import { downloadKeycloakDefaultTheme } from "../../shared/downloadKeycloakDefaultTheme";
|
||||||
import { transformCodebase } from "../../tools/transformCodebase";
|
import { transformCodebase } from "../../tools/transformCodebase";
|
||||||
|
|
||||||
type BuildOptionsLike = {
|
export type BuildContextLike = {
|
||||||
cacheDirPath: string;
|
cacheDirPath: string;
|
||||||
npmWorkspaceRootDirPath: string;
|
npmWorkspaceRootDirPath: string;
|
||||||
keycloakifyBuildDirPath: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike }) {
|
export async function bringInAccountV1(params: {
|
||||||
const { buildOptions } = params;
|
resourcesDirPath: string;
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
}) {
|
||||||
|
const { resourcesDirPath, buildContext } = params;
|
||||||
|
|
||||||
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
||||||
keycloakVersion: lastKeycloakVersionWithAccountV1,
|
keycloakVersion: lastKeycloakVersionWithAccountV1,
|
||||||
buildOptions
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
const accountV1DirPath = pathJoin(
|
const accountV1DirPath = pathJoin(
|
||||||
buildOptions.keycloakifyBuildDirPath,
|
resourcesDirPath,
|
||||||
"src",
|
|
||||||
"main",
|
|
||||||
"resources",
|
|
||||||
"theme",
|
"theme",
|
||||||
accountV1ThemeName,
|
accountV1ThemeName,
|
||||||
"account"
|
"account"
|
||||||
|
@ -8,6 +8,7 @@ import * as recast from "recast";
|
|||||||
import * as babelParser from "@babel/parser";
|
import * as babelParser from "@babel/parser";
|
||||||
import babelGenerate from "@babel/generator";
|
import babelGenerate from "@babel/generator";
|
||||||
import * as babelTypes from "@babel/types";
|
import * as babelTypes from "@babel/types";
|
||||||
|
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
|
||||||
|
|
||||||
export function generateMessageProperties(params: {
|
export function generateMessageProperties(params: {
|
||||||
themeSrcDirPath: string;
|
themeSrcDirPath: string;
|
||||||
@ -146,7 +147,7 @@ export function generateMessageProperties(params: {
|
|||||||
|
|
||||||
for (const [languageTag, keyValueMap] of Object.entries(keyValueMapByLanguageTag)) {
|
for (const [languageTag, keyValueMap] of Object.entries(keyValueMapByLanguageTag)) {
|
||||||
const propertiesFileSource = Object.entries(keyValueMap)
|
const propertiesFileSource = Object.entries(keyValueMap)
|
||||||
.map(([key, value]) => `${key}=${escapeString(value)}`)
|
.map(([key, value]) => `${key}=${escapeStringForPropertiesFile(value)}`)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
out.push({
|
out.push({
|
||||||
@ -164,68 +165,3 @@ export function generateMessageProperties(params: {
|
|||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert a JavaScript string to UTF-16 encoding
|
|
||||||
function toUTF16(codePoint: number): string {
|
|
||||||
if (codePoint <= 0xffff) {
|
|
||||||
// BMP character
|
|
||||||
return "\\u" + codePoint.toString(16).padStart(4, "0");
|
|
||||||
} else {
|
|
||||||
// Non-BMP character
|
|
||||||
codePoint -= 0x10000;
|
|
||||||
let highSurrogate = (codePoint >> 10) + 0xd800;
|
|
||||||
let lowSurrogate = (codePoint % 0x400) + 0xdc00;
|
|
||||||
return (
|
|
||||||
"\\u" +
|
|
||||||
highSurrogate.toString(16).padStart(4, "0") +
|
|
||||||
"\\u" +
|
|
||||||
lowSurrogate.toString(16).padStart(4, "0")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Escapes special characters for use in a .properties file
|
|
||||||
function escapeString(str: string): string {
|
|
||||||
let escapedStr = "";
|
|
||||||
for (const char of [...str]) {
|
|
||||||
const codePoint = char.codePointAt(0);
|
|
||||||
if (!codePoint) continue;
|
|
||||||
|
|
||||||
switch (char) {
|
|
||||||
case "\n":
|
|
||||||
escapedStr += "\\n";
|
|
||||||
break;
|
|
||||||
case "\r":
|
|
||||||
escapedStr += "\\r";
|
|
||||||
break;
|
|
||||||
case "\t":
|
|
||||||
escapedStr += "\\t";
|
|
||||||
break;
|
|
||||||
case "\\":
|
|
||||||
escapedStr += "\\\\";
|
|
||||||
break;
|
|
||||||
case ":":
|
|
||||||
escapedStr += "\\:";
|
|
||||||
break;
|
|
||||||
case "=":
|
|
||||||
escapedStr += "\\=";
|
|
||||||
break;
|
|
||||||
case "#":
|
|
||||||
escapedStr += "\\#";
|
|
||||||
break;
|
|
||||||
case "!":
|
|
||||||
escapedStr += "\\!";
|
|
||||||
break;
|
|
||||||
case "'":
|
|
||||||
escapedStr += "''";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (codePoint > 0x7f) {
|
|
||||||
escapedStr += toUTF16(codePoint); // Non-ASCII characters
|
|
||||||
} else {
|
|
||||||
escapedStr += char; // ASCII character needs no escape
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return escapedStr;
|
|
||||||
}
|
|
||||||
|
@ -1,34 +1,42 @@
|
|||||||
import type { BuildOptions } from "../../shared/buildOptions";
|
import type { BuildContext } from "../../shared/buildContext";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import {
|
import {
|
||||||
generateSrcMainResourcesForMainTheme,
|
generateSrcMainResourcesForMainTheme,
|
||||||
type BuildOptionsLike as BuildOptionsLike_generateSrcMainResourcesForMainTheme
|
type BuildContextLike as BuildContextLike_generateSrcMainResourcesForMainTheme
|
||||||
} from "./generateSrcMainResourcesForMainTheme";
|
} from "./generateSrcMainResourcesForMainTheme";
|
||||||
import { generateSrcMainResourcesForThemeVariant } from "./generateSrcMainResourcesForThemeVariant";
|
import { generateSrcMainResourcesForThemeVariant } from "./generateSrcMainResourcesForThemeVariant";
|
||||||
|
import fs from "fs";
|
||||||
|
import { rmSync } from "../../tools/fs.rmSync";
|
||||||
|
|
||||||
export type BuildOptionsLike = BuildOptionsLike_generateSrcMainResourcesForMainTheme & {
|
export type BuildContextLike = BuildContextLike_generateSrcMainResourcesForMainTheme & {
|
||||||
themeNames: string[];
|
themeNames: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export async function generateSrcMainResources(params: {
|
export async function generateSrcMainResources(params: {
|
||||||
buildOptions: BuildOptionsLike;
|
buildContext: BuildContextLike;
|
||||||
|
resourcesDirPath: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { buildOptions } = params;
|
const { resourcesDirPath, buildContext } = params;
|
||||||
|
|
||||||
const [themeName, ...themeVariantNames] = buildOptions.themeNames;
|
const [themeName, ...themeVariantNames] = buildContext.themeNames;
|
||||||
|
|
||||||
|
if (fs.existsSync(resourcesDirPath)) {
|
||||||
|
rmSync(resourcesDirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
await generateSrcMainResourcesForMainTheme({
|
await generateSrcMainResourcesForMainTheme({
|
||||||
|
resourcesDirPath,
|
||||||
themeName,
|
themeName,
|
||||||
buildOptions
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const themeVariantName of themeVariantNames) {
|
for (const themeVariantName of themeVariantNames) {
|
||||||
generateSrcMainResourcesForThemeVariant({
|
generateSrcMainResourcesForThemeVariant({
|
||||||
|
resourcesDirPath,
|
||||||
themeName,
|
themeName,
|
||||||
themeVariantName,
|
themeVariantName
|
||||||
buildOptions
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,10 @@ import * as fs from "fs";
|
|||||||
import { join as pathJoin, resolve as pathResolve } from "path";
|
import { join as pathJoin, resolve as pathResolve } from "path";
|
||||||
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
||||||
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
|
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
|
||||||
import { generateFtlFilesCodeFactory } from "../generateFtl";
|
import {
|
||||||
|
generateFtlFilesCodeFactory,
|
||||||
|
type BuildContextLike as BuildContextLike_kcContextExclusionsFtlCode
|
||||||
|
} from "../generateFtl";
|
||||||
import {
|
import {
|
||||||
type ThemeType,
|
type ThemeType,
|
||||||
lastKeycloakVersionWithAccountV1,
|
lastKeycloakVersionWithAccountV1,
|
||||||
@ -14,13 +17,19 @@ import {
|
|||||||
accountThemePageIds
|
accountThemePageIds
|
||||||
} from "../../shared/constants";
|
} from "../../shared/constants";
|
||||||
import { isInside } from "../../tools/isInside";
|
import { isInside } from "../../tools/isInside";
|
||||||
import type { BuildOptions } from "../../shared/buildOptions";
|
import type { BuildContext } from "../../shared/buildContext";
|
||||||
import { assert, type Equals } from "tsafe/assert";
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
import { downloadKeycloakStaticResources } from "../../shared/downloadKeycloakStaticResources";
|
import {
|
||||||
|
downloadKeycloakStaticResources,
|
||||||
|
type BuildContextLike as BuildContextLike_downloadKeycloakStaticResources
|
||||||
|
} from "../../shared/downloadKeycloakStaticResources";
|
||||||
import { readFieldNameUsage } from "./readFieldNameUsage";
|
import { readFieldNameUsage } from "./readFieldNameUsage";
|
||||||
import { readExtraPagesNames } from "./readExtraPageNames";
|
import { readExtraPagesNames } from "./readExtraPageNames";
|
||||||
import { generateMessageProperties } from "./generateMessageProperties";
|
import { generateMessageProperties } from "./generateMessageProperties";
|
||||||
import { bringInAccountV1 } from "./bringInAccountV1";
|
import {
|
||||||
|
bringInAccountV1,
|
||||||
|
type BuildContextLike as BuildContextLike_bringInAccountV1
|
||||||
|
} from "./bringInAccountV1";
|
||||||
import { getThemeSrcDirPath } from "../../shared/getThemeSrcDirPath";
|
import { getThemeSrcDirPath } from "../../shared/getThemeSrcDirPath";
|
||||||
import { rmSync } from "../../tools/fs.rmSync";
|
import { rmSync } from "../../tools/fs.rmSync";
|
||||||
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
|
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
|
||||||
@ -29,44 +38,34 @@ import {
|
|||||||
type MetaInfKeycloakTheme
|
type MetaInfKeycloakTheme
|
||||||
} from "../../shared/metaInfKeycloakThemes";
|
} from "../../shared/metaInfKeycloakThemes";
|
||||||
import { objectEntries } from "tsafe/objectEntries";
|
import { objectEntries } from "tsafe/objectEntries";
|
||||||
|
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
|
||||||
|
|
||||||
export type BuildOptionsLike = {
|
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
||||||
bundler: "vite" | "webpack";
|
BuildContextLike_downloadKeycloakStaticResources &
|
||||||
extraThemeProperties: string[] | undefined;
|
BuildContextLike_bringInAccountV1 & {
|
||||||
themeVersion: string;
|
extraThemeProperties: string[] | undefined;
|
||||||
loginThemeResourcesFromKeycloakVersion: string;
|
loginThemeResourcesFromKeycloakVersion: string;
|
||||||
reactAppBuildDirPath: string;
|
projectDirPath: string;
|
||||||
cacheDirPath: string;
|
projectBuildDirPath: string;
|
||||||
assetsDirPath: string;
|
environmentVariables: { name: string; default: string }[];
|
||||||
urlPathname: string | undefined;
|
};
|
||||||
npmWorkspaceRootDirPath: string;
|
|
||||||
reactAppRootDirPath: string;
|
|
||||||
keycloakifyBuildDirPath: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export async function generateSrcMainResourcesForMainTheme(params: {
|
export async function generateSrcMainResourcesForMainTheme(params: {
|
||||||
themeName: string;
|
themeName: string;
|
||||||
buildOptions: BuildOptionsLike;
|
resourcesDirPath: string;
|
||||||
|
buildContext: BuildContextLike;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { themeName, buildOptions } = params;
|
const { themeName, resourcesDirPath, buildContext } = params;
|
||||||
|
|
||||||
const { themeSrcDirPath } = getThemeSrcDirPath({
|
const { themeSrcDirPath } = getThemeSrcDirPath({
|
||||||
reactAppRootDirPath: buildOptions.reactAppRootDirPath
|
projectDirPath: buildContext.projectDirPath
|
||||||
});
|
});
|
||||||
|
|
||||||
const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => {
|
const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => {
|
||||||
const { themeType } = params;
|
const { themeType } = params;
|
||||||
return pathJoin(
|
return pathJoin(resourcesDirPath, "theme", themeName, themeType);
|
||||||
buildOptions.keycloakifyBuildDirPath,
|
|
||||||
"src",
|
|
||||||
"main",
|
|
||||||
"resources",
|
|
||||||
"theme",
|
|
||||||
themeName,
|
|
||||||
themeType
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const cssGlobalsToDefine: Record<string, string> = {};
|
const cssGlobalsToDefine: Record<string, string> = {};
|
||||||
@ -114,7 +113,7 @@ export async function generateSrcMainResourcesForMainTheme(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
transformCodebase({
|
transformCodebase({
|
||||||
srcDirPath: buildOptions.reactAppBuildDirPath,
|
srcDirPath: buildContext.projectBuildDirPath,
|
||||||
destDirPath,
|
destDirPath,
|
||||||
transformSourceCode: ({ filePath, sourceCode }) => {
|
transformSourceCode: ({ filePath, sourceCode }) => {
|
||||||
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
|
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
|
||||||
@ -122,7 +121,7 @@ export async function generateSrcMainResourcesForMainTheme(params: {
|
|||||||
if (
|
if (
|
||||||
isInside({
|
isInside({
|
||||||
dirPath: pathJoin(
|
dirPath: pathJoin(
|
||||||
buildOptions.reactAppBuildDirPath,
|
buildContext.projectBuildDirPath,
|
||||||
keycloak_resources
|
keycloak_resources
|
||||||
),
|
),
|
||||||
filePath
|
filePath
|
||||||
@ -153,7 +152,7 @@ export async function generateSrcMainResourcesForMainTheme(params: {
|
|||||||
if (/\.js?$/i.test(filePath)) {
|
if (/\.js?$/i.test(filePath)) {
|
||||||
const { fixedJsCode } = replaceImportsInJsCode({
|
const { fixedJsCode } = replaceImportsInJsCode({
|
||||||
jsCode: sourceCode.toString("utf8"),
|
jsCode: sourceCode.toString("utf8"),
|
||||||
buildOptions
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -169,10 +168,10 @@ export async function generateSrcMainResourcesForMainTheme(params: {
|
|||||||
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
||||||
themeName,
|
themeName,
|
||||||
indexHtmlCode: fs
|
indexHtmlCode: fs
|
||||||
.readFileSync(pathJoin(buildOptions.reactAppBuildDirPath, "index.html"))
|
.readFileSync(pathJoin(buildContext.projectBuildDirPath, "index.html"))
|
||||||
.toString("utf8"),
|
.toString("utf8"),
|
||||||
cssGlobalsToDefine,
|
cssGlobalsToDefine,
|
||||||
buildOptions,
|
buildContext,
|
||||||
keycloakifyVersion: readThisNpmPackageVersion(),
|
keycloakifyVersion: readThisNpmPackageVersion(),
|
||||||
themeType,
|
themeType,
|
||||||
fieldNames: readFieldNameUsage({
|
fieldNames: readFieldNameUsage({
|
||||||
@ -197,8 +196,6 @@ export async function generateSrcMainResourcesForMainTheme(params: {
|
|||||||
].forEach(pageId => {
|
].forEach(pageId => {
|
||||||
const { ftlCode } = generateFtlFilesCode({ pageId });
|
const { ftlCode } = generateFtlFilesCode({ pageId });
|
||||||
|
|
||||||
fs.mkdirSync(themeTypeDirPath, { recursive: true });
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
pathJoin(themeTypeDirPath, pageId),
|
pathJoin(themeTypeDirPath, pageId),
|
||||||
Buffer.from(ftlCode, "utf8")
|
Buffer.from(ftlCode, "utf8")
|
||||||
@ -232,12 +229,12 @@ export async function generateSrcMainResourcesForMainTheme(params: {
|
|||||||
case "account":
|
case "account":
|
||||||
return lastKeycloakVersionWithAccountV1;
|
return lastKeycloakVersionWithAccountV1;
|
||||||
case "login":
|
case "login":
|
||||||
return buildOptions.loginThemeResourcesFromKeycloakVersion;
|
return buildContext.loginThemeResourcesFromKeycloakVersion;
|
||||||
}
|
}
|
||||||
})(),
|
})(),
|
||||||
themeDirPath: pathResolve(pathJoin(themeTypeDirPath, "..")),
|
themeDirPath: pathResolve(pathJoin(themeTypeDirPath, "..")),
|
||||||
themeType,
|
themeType,
|
||||||
buildOptions
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
@ -253,7 +250,11 @@ export async function generateSrcMainResourcesForMainTheme(params: {
|
|||||||
}
|
}
|
||||||
assert<Equals<typeof themeType, never>>(false);
|
assert<Equals<typeof themeType, never>>(false);
|
||||||
})()}`,
|
})()}`,
|
||||||
...(buildOptions.extraThemeProperties ?? [])
|
...(buildContext.extraThemeProperties ?? []),
|
||||||
|
buildContext.environmentVariables.map(
|
||||||
|
({ name, default: defaultValue }) =>
|
||||||
|
`${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}`
|
||||||
|
)
|
||||||
].join("\n\n"),
|
].join("\n\n"),
|
||||||
"utf8"
|
"utf8"
|
||||||
)
|
)
|
||||||
@ -277,7 +278,8 @@ export async function generateSrcMainResourcesForMainTheme(params: {
|
|||||||
|
|
||||||
if (implementedThemeTypes.account) {
|
if (implementedThemeTypes.account) {
|
||||||
await bringInAccountV1({
|
await bringInAccountV1({
|
||||||
buildOptions
|
resourcesDirPath,
|
||||||
|
buildContext
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,7 +301,7 @@ export async function generateSrcMainResourcesForMainTheme(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
writeMetaInfKeycloakThemes({
|
writeMetaInfKeycloakThemes({
|
||||||
keycloakifyBuildDirPath: buildOptions.keycloakifyBuildDirPath,
|
resourcesDirPath,
|
||||||
metaInfKeycloakThemes
|
metaInfKeycloakThemes
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,33 +1,26 @@
|
|||||||
import { join as pathJoin, extname as pathExtname, sep as pathSep } from "path";
|
import { join as pathJoin, extname as pathExtname, sep as pathSep } from "path";
|
||||||
import { transformCodebase } from "../../tools/transformCodebase";
|
import { transformCodebase } from "../../tools/transformCodebase";
|
||||||
import type { BuildOptions } from "../../shared/buildOptions";
|
import type { BuildContext } from "../../shared/buildContext";
|
||||||
import {
|
import {
|
||||||
readMetaInfKeycloakThemes,
|
readMetaInfKeycloakThemes_fromResourcesDirPath,
|
||||||
writeMetaInfKeycloakThemes
|
writeMetaInfKeycloakThemes
|
||||||
} from "../../shared/metaInfKeycloakThemes";
|
} from "../../shared/metaInfKeycloakThemes";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
|
|
||||||
export type BuildOptionsLike = {
|
export type BuildContextLike = {
|
||||||
keycloakifyBuildDirPath: string;
|
keycloakifyBuildDirPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export function generateSrcMainResourcesForThemeVariant(params: {
|
export function generateSrcMainResourcesForThemeVariant(params: {
|
||||||
|
resourcesDirPath: string;
|
||||||
themeName: string;
|
themeName: string;
|
||||||
themeVariantName: string;
|
themeVariantName: string;
|
||||||
buildOptions: BuildOptionsLike;
|
|
||||||
}) {
|
}) {
|
||||||
const { themeName, themeVariantName, buildOptions } = params;
|
const { resourcesDirPath, themeName, themeVariantName } = params;
|
||||||
|
|
||||||
const mainThemeDirPath = pathJoin(
|
const mainThemeDirPath = pathJoin(resourcesDirPath, "theme", themeName);
|
||||||
buildOptions.keycloakifyBuildDirPath,
|
|
||||||
"src",
|
|
||||||
"main",
|
|
||||||
"resources",
|
|
||||||
"theme",
|
|
||||||
themeName
|
|
||||||
);
|
|
||||||
|
|
||||||
transformCodebase({
|
transformCodebase({
|
||||||
srcDirPath: mainThemeDirPath,
|
srcDirPath: mainThemeDirPath,
|
||||||
@ -57,9 +50,10 @@ export function generateSrcMainResourcesForThemeVariant(params: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
const updatedMetaInfKeycloakThemes = readMetaInfKeycloakThemes({
|
const updatedMetaInfKeycloakThemes =
|
||||||
keycloakifyBuildDirPath: buildOptions.keycloakifyBuildDirPath
|
readMetaInfKeycloakThemes_fromResourcesDirPath({
|
||||||
});
|
resourcesDirPath
|
||||||
|
});
|
||||||
|
|
||||||
updatedMetaInfKeycloakThemes.themes.push({
|
updatedMetaInfKeycloakThemes.themes.push({
|
||||||
name: themeVariantName,
|
name: themeVariantName,
|
||||||
@ -73,7 +67,7 @@ export function generateSrcMainResourcesForThemeVariant(params: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
writeMetaInfKeycloakThemes({
|
writeMetaInfKeycloakThemes({
|
||||||
keycloakifyBuildDirPath: buildOptions.keycloakifyBuildDirPath,
|
resourcesDirPath,
|
||||||
metaInfKeycloakThemes: updatedMetaInfKeycloakThemes
|
metaInfKeycloakThemes: updatedMetaInfKeycloakThemes
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ export function readExtraPagesNames(params: {
|
|||||||
}).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
|
}).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
|
||||||
|
|
||||||
const candidateFilePaths = filePaths.filter(filePath =>
|
const candidateFilePaths = filePaths.filter(filePath =>
|
||||||
/kcContext\.[^.]+$/.test(filePath)
|
/[kK]cContext\.[^.]+$/.test(filePath)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (candidateFilePaths.length === 0) {
|
if (candidateFilePaths.length === 0) {
|
||||||
|
@ -1,74 +0,0 @@
|
|||||||
import * as fs from "fs";
|
|
||||||
import {
|
|
||||||
join as pathJoin,
|
|
||||||
relative as pathRelative,
|
|
||||||
basename as pathBasename
|
|
||||||
} from "path";
|
|
||||||
import { assert } from "tsafe/assert";
|
|
||||||
import type { BuildOptions } from "../shared/buildOptions";
|
|
||||||
import { accountV1ThemeName } from "../shared/constants";
|
|
||||||
|
|
||||||
export type BuildOptionsLike = {
|
|
||||||
keycloakifyBuildDirPath: string;
|
|
||||||
themeNames: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
|
||||||
|
|
||||||
generateStartKeycloakTestingContainer.basename = "start_keycloak_testing_container.sh";
|
|
||||||
|
|
||||||
const containerName = "keycloak-testing-container";
|
|
||||||
const keycloakVersion = "24.0.4";
|
|
||||||
|
|
||||||
/** Files for being able to run a hot reload keycloak container */
|
|
||||||
export function generateStartKeycloakTestingContainer(params: {
|
|
||||||
jarFilePath: string;
|
|
||||||
doesImplementAccountTheme: boolean;
|
|
||||||
buildOptions: BuildOptionsLike;
|
|
||||||
}) {
|
|
||||||
const { jarFilePath, doesImplementAccountTheme, buildOptions } = params;
|
|
||||||
|
|
||||||
const themeRelativeDirPath = pathJoin("src", "main", "resources", "theme");
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
pathJoin(
|
|
||||||
buildOptions.keycloakifyBuildDirPath,
|
|
||||||
generateStartKeycloakTestingContainer.basename
|
|
||||||
),
|
|
||||||
Buffer.from(
|
|
||||||
[
|
|
||||||
"#!/usr/bin/env bash",
|
|
||||||
"",
|
|
||||||
`docker rm ${containerName} || true`,
|
|
||||||
"",
|
|
||||||
`cd "${buildOptions.keycloakifyBuildDirPath}"`,
|
|
||||||
"",
|
|
||||||
"docker run \\",
|
|
||||||
" -p 8080:8080 \\",
|
|
||||||
` --name ${containerName} \\`,
|
|
||||||
" -e KEYCLOAK_ADMIN=admin \\",
|
|
||||||
" -e KEYCLOAK_ADMIN_PASSWORD=admin \\",
|
|
||||||
` -v "${pathJoin(
|
|
||||||
"$(pwd)",
|
|
||||||
pathRelative(buildOptions.keycloakifyBuildDirPath, jarFilePath)
|
|
||||||
)}":"/opt/keycloak/providers/${pathBasename(jarFilePath)}" \\`,
|
|
||||||
[
|
|
||||||
...(doesImplementAccountTheme ? [accountV1ThemeName] : []),
|
|
||||||
...buildOptions.themeNames
|
|
||||||
].map(
|
|
||||||
themeName =>
|
|
||||||
` -v "${pathJoin(
|
|
||||||
"$(pwd)",
|
|
||||||
themeRelativeDirPath,
|
|
||||||
themeName
|
|
||||||
).replace(/\\/g, "/")}":"/opt/keycloak/themes/${themeName}":rw \\`
|
|
||||||
),
|
|
||||||
` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`,
|
|
||||||
` start-dev`,
|
|
||||||
""
|
|
||||||
].join("\n"),
|
|
||||||
"utf8"
|
|
||||||
),
|
|
||||||
{ mode: 0o755 }
|
|
||||||
);
|
|
||||||
}
|
|
@ -2,13 +2,17 @@ import { generateSrcMainResources } from "./generateSrcMainResources";
|
|||||||
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
|
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
|
||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { readBuildOptions } from "../shared/buildOptions";
|
import { getBuildContext } from "../shared/buildContext";
|
||||||
import { vitePluginSubScriptEnvNames, skipBuildJarsEnvName } from "../shared/constants";
|
import {
|
||||||
|
vitePluginSubScriptEnvNames,
|
||||||
|
onlyBuildJarFileBasenameEnvName
|
||||||
|
} from "../shared/constants";
|
||||||
import { buildJars } from "./buildJars";
|
import { buildJars } from "./buildJars";
|
||||||
import type { CliCommandOptions } from "../main";
|
import type { CliCommandOptions } from "../main";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
|
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
|
||||||
import * as os from "os";
|
import * as os from "os";
|
||||||
|
import { rmSync } from "../tools/fs.rmSync";
|
||||||
|
|
||||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||||
exit_if_maven_not_installed: {
|
exit_if_maven_not_installed: {
|
||||||
@ -47,7 +51,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
|
|
||||||
const { cliCommandOptions } = params;
|
const { cliCommandOptions } = params;
|
||||||
|
|
||||||
const buildOptions = readBuildOptions({ cliCommandOptions });
|
const buildContext = getBuildContext({ cliCommandOptions });
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
[
|
[
|
||||||
@ -55,7 +59,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
chalk.green(
|
chalk.green(
|
||||||
`Building the keycloak theme in .${pathSep}${pathRelative(
|
`Building the keycloak theme in .${pathSep}${pathRelative(
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
buildOptions.keycloakifyBuildDirPath
|
buildContext.keycloakifyBuildDirPath
|
||||||
)} ...`
|
)} ...`
|
||||||
)
|
)
|
||||||
].join(" ")
|
].join(" ")
|
||||||
@ -64,44 +68,51 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
{
|
{
|
||||||
if (!fs.existsSync(buildOptions.keycloakifyBuildDirPath)) {
|
if (!fs.existsSync(buildContext.keycloakifyBuildDirPath)) {
|
||||||
fs.mkdirSync(buildOptions.keycloakifyBuildDirPath, {
|
fs.mkdirSync(buildContext.keycloakifyBuildDirPath, {
|
||||||
recursive: true
|
recursive: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
pathJoin(buildOptions.keycloakifyBuildDirPath, ".gitignore"),
|
pathJoin(buildContext.keycloakifyBuildDirPath, ".gitignore"),
|
||||||
Buffer.from("*", "utf8")
|
Buffer.from("*", "utf8")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await generateSrcMainResources({ buildOptions });
|
const resourcesDirPath = pathJoin(buildContext.keycloakifyBuildDirPath, "resources");
|
||||||
|
|
||||||
|
await generateSrcMainResources({
|
||||||
|
resourcesDirPath,
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
run_post_build_script: {
|
run_post_build_script: {
|
||||||
if (buildOptions.bundler !== "vite") {
|
if (buildContext.bundler !== "vite") {
|
||||||
break run_post_build_script;
|
break run_post_build_script;
|
||||||
}
|
}
|
||||||
|
|
||||||
child_process.execSync("npx vite", {
|
child_process.execSync("npx vite", {
|
||||||
cwd: buildOptions.reactAppRootDirPath,
|
cwd: buildContext.projectDirPath,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
[vitePluginSubScriptEnvNames.runPostBuildScript]:
|
[vitePluginSubScriptEnvNames.runPostBuildScript]:
|
||||||
JSON.stringify(buildOptions)
|
JSON.stringify(buildContext)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
build_jars: {
|
await buildJars({
|
||||||
if (process.env[skipBuildJarsEnvName]) {
|
resourcesDirPath,
|
||||||
break build_jars;
|
buildContext,
|
||||||
}
|
onlyBuildJarFileBasename: process.env[onlyBuildJarFileBasenameEnvName]
|
||||||
|
});
|
||||||
|
|
||||||
await buildJars({ buildOptions });
|
rmSync(resourcesDirPath, { recursive: true });
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`)
|
chalk.green(
|
||||||
|
`✓ keycloak theme built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
import type { BuildOptions } from "../../shared/buildOptions";
|
import type { BuildContext } from "../../shared/buildContext";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import { basenameOfTheKeycloakifyResourcesDir } from "../../shared/constants";
|
import { basenameOfTheKeycloakifyResourcesDir } from "../../shared/constants";
|
||||||
|
|
||||||
export type BuildOptionsLike = {
|
export type BuildContextLike = {
|
||||||
urlPathname: string | undefined;
|
urlPathname: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export function replaceImportsInCssCode(params: { cssCode: string }): {
|
export function replaceImportsInCssCode(params: { cssCode: string }): {
|
||||||
fixedCssCode: string;
|
fixedCssCode: string;
|
||||||
@ -44,11 +44,11 @@ export function replaceImportsInCssCode(params: { cssCode: string }): {
|
|||||||
|
|
||||||
export function generateCssCodeToDefineGlobals(params: {
|
export function generateCssCodeToDefineGlobals(params: {
|
||||||
cssGlobalsToDefine: Record<string, string>;
|
cssGlobalsToDefine: Record<string, string>;
|
||||||
buildOptions: BuildOptionsLike;
|
buildContext: BuildContextLike;
|
||||||
}): {
|
}): {
|
||||||
cssCodeToPrependInHead: string;
|
cssCodeToPrependInHead: string;
|
||||||
} {
|
} {
|
||||||
const { cssGlobalsToDefine, buildOptions } = params;
|
const { cssGlobalsToDefine, buildContext } = params;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cssCodeToPrependInHead: [
|
cssCodeToPrependInHead: [
|
||||||
@ -59,7 +59,7 @@ export function generateCssCodeToDefineGlobals(params: {
|
|||||||
`--${cssVariableName}:`,
|
`--${cssVariableName}:`,
|
||||||
cssGlobalsToDefine[cssVariableName].replace(
|
cssGlobalsToDefine[cssVariableName].replace(
|
||||||
new RegExp(
|
new RegExp(
|
||||||
`url\\(${(buildOptions.urlPathname ?? "/").replace(
|
`url\\(${(buildContext.urlPathname ?? "/").replace(
|
||||||
/\//g,
|
/\//g,
|
||||||
"\\/"
|
"\\/"
|
||||||
)}`,
|
)}`,
|
||||||
|
@ -1,25 +1,25 @@
|
|||||||
import type { BuildOptions } from "../../shared/buildOptions";
|
import type { BuildContext } from "../../shared/buildContext";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import { basenameOfTheKeycloakifyResourcesDir } from "../../shared/constants";
|
import { basenameOfTheKeycloakifyResourcesDir } from "../../shared/constants";
|
||||||
|
|
||||||
export type BuildOptionsLike = {
|
export type BuildContextLike = {
|
||||||
urlPathname: string | undefined;
|
urlPathname: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export function replaceImportsInInlineCssCode(params: {
|
export function replaceImportsInInlineCssCode(params: {
|
||||||
cssCode: string;
|
cssCode: string;
|
||||||
buildOptions: BuildOptionsLike;
|
buildContext: BuildContextLike;
|
||||||
}): {
|
}): {
|
||||||
fixedCssCode: string;
|
fixedCssCode: string;
|
||||||
} {
|
} {
|
||||||
const { cssCode, buildOptions } = params;
|
const { cssCode, buildContext } = params;
|
||||||
|
|
||||||
const fixedCssCode = cssCode.replace(
|
const fixedCssCode = cssCode.replace(
|
||||||
buildOptions.urlPathname === undefined
|
buildContext.urlPathname === undefined
|
||||||
? /url\(["']?\/([^/][^)"']+)["']?\)/g
|
? /url\(["']?\/([^/][^)"']+)["']?\)/g
|
||||||
: new RegExp(`url\\(["']?${buildOptions.urlPathname}([^)"']+)["']?\\)`, "g"),
|
: new RegExp(`url\\(["']?${buildContext.urlPathname}([^)"']+)["']?\\)`, "g"),
|
||||||
(...[, group]) =>
|
(...[, group]) =>
|
||||||
`url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/${group})`
|
`url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/${group})`
|
||||||
);
|
);
|
||||||
|
@ -1,38 +1,38 @@
|
|||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import type { BuildOptions } from "../../../shared/buildOptions";
|
import type { BuildContext } from "../../../shared/buildContext";
|
||||||
import { replaceImportsInJsCode_vite } from "./vite";
|
import { replaceImportsInJsCode_vite } from "./vite";
|
||||||
import { replaceImportsInJsCode_webpack } from "./webpack";
|
import { replaceImportsInJsCode_webpack } from "./webpack";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
|
|
||||||
export type BuildOptionsLike = {
|
export type BuildContextLike = {
|
||||||
reactAppBuildDirPath: string;
|
projectBuildDirPath: string;
|
||||||
assetsDirPath: string;
|
assetsDirPath: string;
|
||||||
urlPathname: string | undefined;
|
urlPathname: string | undefined;
|
||||||
bundler: "vite" | "webpack";
|
bundler: "vite" | "webpack";
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export function replaceImportsInJsCode(params: {
|
export function replaceImportsInJsCode(params: {
|
||||||
jsCode: string;
|
jsCode: string;
|
||||||
buildOptions: BuildOptionsLike;
|
buildContext: BuildContextLike;
|
||||||
}) {
|
}) {
|
||||||
const { jsCode, buildOptions } = params;
|
const { jsCode, buildContext } = params;
|
||||||
|
|
||||||
const { fixedJsCode } = (() => {
|
const { fixedJsCode } = (() => {
|
||||||
switch (buildOptions.bundler) {
|
switch (buildContext.bundler) {
|
||||||
case "vite":
|
case "vite":
|
||||||
return replaceImportsInJsCode_vite({
|
return replaceImportsInJsCode_vite({
|
||||||
jsCode,
|
jsCode,
|
||||||
buildOptions,
|
buildContext,
|
||||||
basenameOfAssetsFiles: readAssetsDirSync({
|
basenameOfAssetsFiles: readAssetsDirSync({
|
||||||
assetsDirPath: params.buildOptions.assetsDirPath
|
assetsDirPath: params.buildContext.assetsDirPath
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
case "webpack":
|
case "webpack":
|
||||||
return replaceImportsInJsCode_webpack({
|
return replaceImportsInJsCode_webpack({
|
||||||
jsCode,
|
jsCode,
|
||||||
buildOptions
|
buildContext
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -3,21 +3,21 @@ import {
|
|||||||
basenameOfTheKeycloakifyResourcesDir
|
basenameOfTheKeycloakifyResourcesDir
|
||||||
} from "../../../shared/constants";
|
} from "../../../shared/constants";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import type { BuildOptions } from "../../../shared/buildOptions";
|
import type { BuildContext } from "../../../shared/buildContext";
|
||||||
import * as nodePath from "path";
|
import * as nodePath from "path";
|
||||||
import { replaceAll } from "../../../tools/String.prototype.replaceAll";
|
import { replaceAll } from "../../../tools/String.prototype.replaceAll";
|
||||||
|
|
||||||
export type BuildOptionsLike = {
|
export type BuildContextLike = {
|
||||||
reactAppBuildDirPath: string;
|
projectBuildDirPath: string;
|
||||||
assetsDirPath: string;
|
assetsDirPath: string;
|
||||||
urlPathname: string | undefined;
|
urlPathname: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export function replaceImportsInJsCode_vite(params: {
|
export function replaceImportsInJsCode_vite(params: {
|
||||||
jsCode: string;
|
jsCode: string;
|
||||||
buildOptions: BuildOptionsLike;
|
buildContext: BuildContextLike;
|
||||||
basenameOfAssetsFiles: string[];
|
basenameOfAssetsFiles: string[];
|
||||||
systemType?: "posix" | "win32";
|
systemType?: "posix" | "win32";
|
||||||
}): {
|
}): {
|
||||||
@ -25,7 +25,7 @@ export function replaceImportsInJsCode_vite(params: {
|
|||||||
} {
|
} {
|
||||||
const {
|
const {
|
||||||
jsCode,
|
jsCode,
|
||||||
buildOptions,
|
buildContext,
|
||||||
basenameOfAssetsFiles,
|
basenameOfAssetsFiles,
|
||||||
systemType = nodePath.sep === "/" ? "posix" : "win32"
|
systemType = nodePath.sep === "/" ? "posix" : "win32"
|
||||||
} = params;
|
} = params;
|
||||||
@ -35,11 +35,11 @@ export function replaceImportsInJsCode_vite(params: {
|
|||||||
let fixedJsCode = jsCode;
|
let fixedJsCode = jsCode;
|
||||||
|
|
||||||
replace_base_javacript_import: {
|
replace_base_javacript_import: {
|
||||||
if (buildOptions.urlPathname === undefined) {
|
if (buildContext.urlPathname === undefined) {
|
||||||
break replace_base_javacript_import;
|
break replace_base_javacript_import;
|
||||||
}
|
}
|
||||||
// Optimization
|
// Optimization
|
||||||
if (!jsCode.includes(buildOptions.urlPathname)) {
|
if (!jsCode.includes(buildContext.urlPathname)) {
|
||||||
break replace_base_javacript_import;
|
break replace_base_javacript_import;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ export function replaceImportsInJsCode_vite(params: {
|
|||||||
fixedJsCode = fixedJsCode.replace(
|
fixedJsCode = fixedJsCode.replace(
|
||||||
new RegExp(
|
new RegExp(
|
||||||
`([\\w\\$][\\w\\d\\$]*)=function\\(([\\w\\$][\\w\\d\\$]*)\\)\\{return"${replaceAll(
|
`([\\w\\$][\\w\\d\\$]*)=function\\(([\\w\\$][\\w\\d\\$]*)\\)\\{return"${replaceAll(
|
||||||
buildOptions.urlPathname,
|
buildContext.urlPathname,
|
||||||
"/",
|
"/",
|
||||||
"\\/"
|
"\\/"
|
||||||
)}"\\+\\2\\}`,
|
)}"\\+\\2\\}`,
|
||||||
@ -62,15 +62,15 @@ export function replaceImportsInJsCode_vite(params: {
|
|||||||
// Example: "assets/ or "foo/bar/"
|
// Example: "assets/ or "foo/bar/"
|
||||||
const staticDir = (() => {
|
const staticDir = (() => {
|
||||||
let out = pathRelative(
|
let out = pathRelative(
|
||||||
buildOptions.reactAppBuildDirPath,
|
buildContext.projectBuildDirPath,
|
||||||
buildOptions.assetsDirPath
|
buildContext.assetsDirPath
|
||||||
);
|
);
|
||||||
|
|
||||||
out = replaceAll(out, pathSep, "/") + "/";
|
out = replaceAll(out, pathSep, "/") + "/";
|
||||||
|
|
||||||
if (out === "/") {
|
if (out === "/") {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The assetsDirPath must be a subdirectory of reactAppBuildDirPath`
|
`The assetsDirPath must be a subdirectory of projectBuildDirPath`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +93,7 @@ export function replaceImportsInJsCode_vite(params: {
|
|||||||
|
|
||||||
fixedJsCode = replaceAll(
|
fixedJsCode = replaceAll(
|
||||||
fixedJsCode,
|
fixedJsCode,
|
||||||
`"${buildOptions.urlPathname ?? "/"}${relativePathOfAssetFile}"`,
|
`"${buildContext.urlPathname ?? "/"}${relativePathOfAssetFile}"`,
|
||||||
`(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/${relativePathOfAssetFile}")`
|
`(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/${relativePathOfAssetFile}")`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -3,28 +3,28 @@ import {
|
|||||||
basenameOfTheKeycloakifyResourcesDir
|
basenameOfTheKeycloakifyResourcesDir
|
||||||
} from "../../../shared/constants";
|
} from "../../../shared/constants";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import type { BuildOptions } from "../../../shared/buildOptions";
|
import type { BuildContext } from "../../../shared/buildContext";
|
||||||
import * as nodePath from "path";
|
import * as nodePath from "path";
|
||||||
import { replaceAll } from "../../../tools/String.prototype.replaceAll";
|
import { replaceAll } from "../../../tools/String.prototype.replaceAll";
|
||||||
|
|
||||||
export type BuildOptionsLike = {
|
export type BuildContextLike = {
|
||||||
reactAppBuildDirPath: string;
|
projectBuildDirPath: string;
|
||||||
assetsDirPath: string;
|
assetsDirPath: string;
|
||||||
urlPathname: string | undefined;
|
urlPathname: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export function replaceImportsInJsCode_webpack(params: {
|
export function replaceImportsInJsCode_webpack(params: {
|
||||||
jsCode: string;
|
jsCode: string;
|
||||||
buildOptions: BuildOptionsLike;
|
buildContext: BuildContextLike;
|
||||||
systemType?: "posix" | "win32";
|
systemType?: "posix" | "win32";
|
||||||
}): {
|
}): {
|
||||||
fixedJsCode: string;
|
fixedJsCode: string;
|
||||||
} {
|
} {
|
||||||
const {
|
const {
|
||||||
jsCode,
|
jsCode,
|
||||||
buildOptions,
|
buildContext,
|
||||||
systemType = nodePath.sep === "/" ? "posix" : "win32"
|
systemType = nodePath.sep === "/" ? "posix" : "win32"
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
@ -32,12 +32,12 @@ export function replaceImportsInJsCode_webpack(params: {
|
|||||||
|
|
||||||
let fixedJsCode = jsCode;
|
let fixedJsCode = jsCode;
|
||||||
|
|
||||||
if (buildOptions.urlPathname !== undefined) {
|
if (buildContext.urlPathname !== undefined) {
|
||||||
// "__esModule",{value:!0})},n.p="/foo-bar/",function(){if("undefined" -> ... n.p="/" ...
|
// "__esModule",{value:!0})},n.p="/foo-bar/",function(){if("undefined" -> ... n.p="/" ...
|
||||||
fixedJsCode = fixedJsCode.replace(
|
fixedJsCode = fixedJsCode.replace(
|
||||||
new RegExp(
|
new RegExp(
|
||||||
`,([a-zA-Z]\\.[a-zA-Z])="${replaceAll(
|
`,([a-zA-Z]\\.[a-zA-Z])="${replaceAll(
|
||||||
buildOptions.urlPathname,
|
buildContext.urlPathname,
|
||||||
"/",
|
"/",
|
||||||
"\\/"
|
"\\/"
|
||||||
)}",`,
|
)}",`,
|
||||||
@ -50,15 +50,15 @@ export function replaceImportsInJsCode_webpack(params: {
|
|||||||
// Example: "static/ or "foo/bar/"
|
// Example: "static/ or "foo/bar/"
|
||||||
const staticDir = (() => {
|
const staticDir = (() => {
|
||||||
let out = pathRelative(
|
let out = pathRelative(
|
||||||
buildOptions.reactAppBuildDirPath,
|
buildContext.projectBuildDirPath,
|
||||||
buildOptions.assetsDirPath
|
buildContext.assetsDirPath
|
||||||
);
|
);
|
||||||
|
|
||||||
out = replaceAll(out, pathSep, "/") + "/";
|
out = replaceAll(out, pathSep, "/") + "/";
|
||||||
|
|
||||||
if (out === "/") {
|
if (out === "/") {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The assetsDirPath must be a subdirectory of reactAppBuildDirPath`
|
`The assetsDirPath must be a subdirectory of projectBuildDirPath`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
|
|||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
|
|
||||||
export type CliCommandOptions = {
|
export type CliCommandOptions = {
|
||||||
reactAppRootDirPath: string | undefined;
|
projectDirPath: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const program = termost<CliCommandOptions>(
|
const program = termost<CliCommandOptions>(
|
||||||
@ -25,7 +25,7 @@ const program = termost<CliCommandOptions>(
|
|||||||
const optionsKeys: string[] = [];
|
const optionsKeys: string[] = [];
|
||||||
|
|
||||||
program.option({
|
program.option({
|
||||||
key: "reactAppRootDirPath",
|
key: "projectDirPath",
|
||||||
name: (() => {
|
name: (() => {
|
||||||
const long = "project";
|
const long = "project";
|
||||||
const short = "p";
|
const short = "p";
|
||||||
@ -134,20 +134,6 @@ program
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
program
|
|
||||||
.command({
|
|
||||||
name: "download-keycloak-default-theme",
|
|
||||||
description: "Download the built-in Keycloak theme."
|
|
||||||
})
|
|
||||||
.task({
|
|
||||||
skip,
|
|
||||||
handler: async cliCommandOptions => {
|
|
||||||
const { command } = await import("./download-keycloak-default-theme");
|
|
||||||
|
|
||||||
await command({ cliCommandOptions });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
program
|
program
|
||||||
.command({
|
.command({
|
||||||
name: "eject-page",
|
name: "eject-page",
|
||||||
@ -162,6 +148,20 @@ program
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command({
|
||||||
|
name: "add-story",
|
||||||
|
description: "Add *.stories.tsx file for a specific page to in your Storybook."
|
||||||
|
})
|
||||||
|
.task({
|
||||||
|
skip,
|
||||||
|
handler: async cliCommandOptions => {
|
||||||
|
const { command } = await import("./add-story");
|
||||||
|
|
||||||
|
await command({ cliCommandOptions });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
program
|
program
|
||||||
.command({
|
.command({
|
||||||
name: "initialize-email-theme",
|
name: "initialize-email-theme",
|
||||||
@ -191,6 +191,21 @@ program
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command({
|
||||||
|
name: "update-kc-gen",
|
||||||
|
description:
|
||||||
|
"(Webpack/Create-React-App only) Create/update the kc.gen.ts file in your project."
|
||||||
|
})
|
||||||
|
.task({
|
||||||
|
skip,
|
||||||
|
handler: async cliCommandOptions => {
|
||||||
|
const { command } = await import("./update-kc-gen");
|
||||||
|
|
||||||
|
await command({ cliCommandOptions });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Fallback to build command if no command is provided
|
// Fallback to build command if no command is provided
|
||||||
{
|
{
|
||||||
const [, , ...rest] = process.argv;
|
const [, , ...rest] = process.argv;
|
||||||
|
@ -5,5 +5,5 @@ export type KeycloakVersionRange =
|
|||||||
export namespace KeycloakVersionRange {
|
export namespace KeycloakVersionRange {
|
||||||
export type WithoutAccountTheme = "21-and-below" | "22-and-above";
|
export type WithoutAccountTheme = "21-and-below" | "22-and-above";
|
||||||
|
|
||||||
export type WithAccountTheme = "21-and-below" | "23" | "24-and-above";
|
export type WithAccountTheme = "21-and-below" | "23" | "24" | "25-and-above";
|
||||||
}
|
}
|
||||||
|
@ -9,18 +9,16 @@ import { assert } from "tsafe";
|
|||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import { vitePluginSubScriptEnvNames } from "./constants";
|
import { vitePluginSubScriptEnvNames } from "./constants";
|
||||||
|
|
||||||
/** Consolidated build option gathered form CLI arguments and config in package.json */
|
export type BuildContext = {
|
||||||
export type BuildOptions = {
|
|
||||||
bundler: "vite" | "webpack";
|
bundler: "vite" | "webpack";
|
||||||
themeVersion: string;
|
themeVersion: string;
|
||||||
themeNames: string[];
|
themeNames: [string, ...string[]];
|
||||||
extraThemeProperties: string[] | undefined;
|
extraThemeProperties: string[] | undefined;
|
||||||
groupId: string;
|
groupId: string;
|
||||||
artifactId: string;
|
artifactId: string;
|
||||||
loginThemeResourcesFromKeycloakVersion: string;
|
loginThemeResourcesFromKeycloakVersion: string;
|
||||||
reactAppRootDirPath: string;
|
projectDirPath: string;
|
||||||
// TODO: Remove from vite type
|
projectBuildDirPath: string;
|
||||||
reactAppBuildDirPath: string;
|
|
||||||
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
|
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
|
||||||
keycloakifyBuildDirPath: string;
|
keycloakifyBuildDirPath: string;
|
||||||
publicDirPath: string;
|
publicDirPath: string;
|
||||||
@ -30,15 +28,19 @@ export type BuildOptions = {
|
|||||||
urlPathname: string | undefined;
|
urlPathname: string | undefined;
|
||||||
assetsDirPath: string;
|
assetsDirPath: string;
|
||||||
npmWorkspaceRootDirPath: string;
|
npmWorkspaceRootDirPath: string;
|
||||||
|
kcContextExclusionsFtlCode: string | undefined;
|
||||||
|
environmentVariables: { name: string; default: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserProvidedBuildOptions = {
|
export type BuildOptions = {
|
||||||
|
themeName?: string | string[];
|
||||||
|
environmentVariables?: { name: string; default: string }[];
|
||||||
extraThemeProperties?: string[];
|
extraThemeProperties?: string[];
|
||||||
artifactId?: string;
|
artifactId?: string;
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
loginThemeResourcesFromKeycloakVersion?: string;
|
loginThemeResourcesFromKeycloakVersion?: string;
|
||||||
keycloakifyBuildDirPath?: string;
|
keycloakifyBuildDirPath?: string;
|
||||||
themeName?: string | string[];
|
kcContextExclusionsFtl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ResolvedViteConfig = {
|
export type ResolvedViteConfig = {
|
||||||
@ -46,21 +48,21 @@ export type ResolvedViteConfig = {
|
|||||||
publicDir: string;
|
publicDir: string;
|
||||||
assetsDir: string;
|
assetsDir: string;
|
||||||
urlPathname: string | undefined;
|
urlPathname: string | undefined;
|
||||||
userProvidedBuildOptions: UserProvidedBuildOptions;
|
buildOptions: BuildOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function readBuildOptions(params: {
|
export function getBuildContext(params: {
|
||||||
cliCommandOptions: CliCommandOptions;
|
cliCommandOptions: CliCommandOptions;
|
||||||
}): BuildOptions {
|
}): BuildContext {
|
||||||
const { cliCommandOptions } = params;
|
const { cliCommandOptions } = params;
|
||||||
|
|
||||||
const reactAppRootDirPath = (() => {
|
const projectDirPath = (() => {
|
||||||
if (cliCommandOptions.reactAppRootDirPath === undefined) {
|
if (cliCommandOptions.projectDirPath === undefined) {
|
||||||
return process.cwd();
|
return process.cwd();
|
||||||
}
|
}
|
||||||
|
|
||||||
return getAbsoluteAndInOsFormatPath({
|
return getAbsoluteAndInOsFormatPath({
|
||||||
pathIsh: cliCommandOptions.reactAppRootDirPath,
|
pathIsh: cliCommandOptions.projectDirPath,
|
||||||
cwd: process.cwd()
|
cwd: process.cwd()
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
@ -68,7 +70,7 @@ export function readBuildOptions(params: {
|
|||||||
const { resolvedViteConfig } = (() => {
|
const { resolvedViteConfig } = (() => {
|
||||||
if (
|
if (
|
||||||
fs
|
fs
|
||||||
.readdirSync(reactAppRootDirPath)
|
.readdirSync(projectDirPath)
|
||||||
.find(fileBasename => fileBasename.startsWith("vite.config")) ===
|
.find(fileBasename => fileBasename.startsWith("vite.config")) ===
|
||||||
undefined
|
undefined
|
||||||
) {
|
) {
|
||||||
@ -77,7 +79,7 @@ export function readBuildOptions(params: {
|
|||||||
|
|
||||||
const output = child_process
|
const output = child_process
|
||||||
.execSync("npx vite", {
|
.execSync("npx vite", {
|
||||||
cwd: reactAppRootDirPath,
|
cwd: projectDirPath,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
[vitePluginSubScriptEnvNames.resolveViteConfig]: "true"
|
[vitePluginSubScriptEnvNames.resolveViteConfig]: "true"
|
||||||
@ -104,8 +106,8 @@ export function readBuildOptions(params: {
|
|||||||
name: string;
|
name: string;
|
||||||
version?: string;
|
version?: string;
|
||||||
homepage?: string;
|
homepage?: string;
|
||||||
keycloakify?: UserProvidedBuildOptions & {
|
keycloakify?: BuildOptions & {
|
||||||
reactAppBuildDirPath?: string;
|
projectBuildDirPath?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -119,7 +121,7 @@ export function readBuildOptions(params: {
|
|||||||
artifactId: z.string().optional(),
|
artifactId: z.string().optional(),
|
||||||
groupId: z.string().optional(),
|
groupId: z.string().optional(),
|
||||||
loginThemeResourcesFromKeycloakVersion: z.string().optional(),
|
loginThemeResourcesFromKeycloakVersion: z.string().optional(),
|
||||||
reactAppBuildDirPath: z.string().optional(),
|
projectBuildDirPath: z.string().optional(),
|
||||||
keycloakifyBuildDirPath: z.string().optional(),
|
keycloakifyBuildDirPath: z.string().optional(),
|
||||||
themeName: z.union([z.string(), z.array(z.string())]).optional()
|
themeName: z.union([z.string(), z.array(z.string())]).optional()
|
||||||
})
|
})
|
||||||
@ -135,20 +137,18 @@ export function readBuildOptions(params: {
|
|||||||
|
|
||||||
return zParsedPackageJson.parse(
|
return zParsedPackageJson.parse(
|
||||||
JSON.parse(
|
JSON.parse(
|
||||||
fs
|
fs.readFileSync(pathJoin(projectDirPath, "package.json")).toString("utf8")
|
||||||
.readFileSync(pathJoin(reactAppRootDirPath, "package.json"))
|
|
||||||
.toString("utf8")
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const userProvidedBuildOptions: UserProvidedBuildOptions = {
|
const buildOptions: BuildOptions = {
|
||||||
...parsedPackageJson.keycloakify,
|
...parsedPackageJson.keycloakify,
|
||||||
...resolvedViteConfig?.userProvidedBuildOptions
|
...resolvedViteConfig?.buildOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
const themeNames = (() => {
|
const themeNames = ((): [string, ...string[]] => {
|
||||||
if (userProvidedBuildOptions.themeName === undefined) {
|
if (buildOptions.themeName === undefined) {
|
||||||
return [
|
return [
|
||||||
parsedPackageJson.name
|
parsedPackageJson.name
|
||||||
.replace(/^@(.*)/, "$1")
|
.replace(/^@(.*)/, "$1")
|
||||||
@ -157,34 +157,38 @@ export function readBuildOptions(params: {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof userProvidedBuildOptions.themeName === "string") {
|
if (typeof buildOptions.themeName === "string") {
|
||||||
return [userProvidedBuildOptions.themeName];
|
return [buildOptions.themeName];
|
||||||
}
|
}
|
||||||
|
|
||||||
return userProvidedBuildOptions.themeName;
|
const [mainThemeName, ...themeVariantNames] = buildOptions.themeName;
|
||||||
|
|
||||||
|
assert(mainThemeName !== undefined);
|
||||||
|
|
||||||
|
return [mainThemeName, ...themeVariantNames];
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const reactAppBuildDirPath = (() => {
|
const projectBuildDirPath = (() => {
|
||||||
webpack: {
|
webpack: {
|
||||||
if (resolvedViteConfig !== undefined) {
|
if (resolvedViteConfig !== undefined) {
|
||||||
break webpack;
|
break webpack;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedPackageJson.keycloakify?.reactAppBuildDirPath !== undefined) {
|
if (parsedPackageJson.keycloakify?.projectBuildDirPath !== undefined) {
|
||||||
return getAbsoluteAndInOsFormatPath({
|
return getAbsoluteAndInOsFormatPath({
|
||||||
pathIsh: parsedPackageJson.keycloakify.reactAppBuildDirPath,
|
pathIsh: parsedPackageJson.keycloakify.projectBuildDirPath,
|
||||||
cwd: reactAppRootDirPath
|
cwd: projectDirPath
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return pathJoin(reactAppRootDirPath, "build");
|
return pathJoin(projectDirPath, "build");
|
||||||
}
|
}
|
||||||
|
|
||||||
return pathJoin(reactAppRootDirPath, resolvedViteConfig.buildDir);
|
return pathJoin(projectDirPath, resolvedViteConfig.buildDir);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({
|
const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({
|
||||||
reactAppRootDirPath,
|
projectDirPath,
|
||||||
dependencyExpected: "keycloakify"
|
dependencyExpected: "keycloakify"
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -193,13 +197,13 @@ export function readBuildOptions(params: {
|
|||||||
themeVersion:
|
themeVersion:
|
||||||
process.env.KEYCLOAKIFY_THEME_VERSION ?? parsedPackageJson.version ?? "0.0.0",
|
process.env.KEYCLOAKIFY_THEME_VERSION ?? parsedPackageJson.version ?? "0.0.0",
|
||||||
themeNames,
|
themeNames,
|
||||||
extraThemeProperties: userProvidedBuildOptions.extraThemeProperties,
|
extraThemeProperties: buildOptions.extraThemeProperties,
|
||||||
groupId: (() => {
|
groupId: (() => {
|
||||||
const fallbackGroupId = `${themeNames[0]}.keycloak`;
|
const fallbackGroupId = `${themeNames[0]}.keycloak`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
process.env.KEYCLOAKIFY_GROUP_ID ??
|
process.env.KEYCLOAKIFY_GROUP_ID ??
|
||||||
userProvidedBuildOptions.groupId ??
|
buildOptions.groupId ??
|
||||||
(parsedPackageJson.homepage === undefined
|
(parsedPackageJson.homepage === undefined
|
||||||
? fallbackGroupId
|
? fallbackGroupId
|
||||||
: urlParse(parsedPackageJson.homepage)
|
: urlParse(parsedPackageJson.homepage)
|
||||||
@ -211,22 +215,22 @@ export function readBuildOptions(params: {
|
|||||||
})(),
|
})(),
|
||||||
artifactId:
|
artifactId:
|
||||||
process.env.KEYCLOAKIFY_ARTIFACT_ID ??
|
process.env.KEYCLOAKIFY_ARTIFACT_ID ??
|
||||||
userProvidedBuildOptions.artifactId ??
|
buildOptions.artifactId ??
|
||||||
`${themeNames[0]}-keycloak-theme`,
|
`${themeNames[0]}-keycloak-theme`,
|
||||||
loginThemeResourcesFromKeycloakVersion:
|
loginThemeResourcesFromKeycloakVersion:
|
||||||
userProvidedBuildOptions.loginThemeResourcesFromKeycloakVersion ?? "24.0.4",
|
buildOptions.loginThemeResourcesFromKeycloakVersion ?? "24.0.4",
|
||||||
reactAppRootDirPath,
|
projectDirPath,
|
||||||
reactAppBuildDirPath,
|
projectBuildDirPath,
|
||||||
keycloakifyBuildDirPath: (() => {
|
keycloakifyBuildDirPath: (() => {
|
||||||
if (userProvidedBuildOptions.keycloakifyBuildDirPath !== undefined) {
|
if (buildOptions.keycloakifyBuildDirPath !== undefined) {
|
||||||
return getAbsoluteAndInOsFormatPath({
|
return getAbsoluteAndInOsFormatPath({
|
||||||
pathIsh: userProvidedBuildOptions.keycloakifyBuildDirPath,
|
pathIsh: buildOptions.keycloakifyBuildDirPath,
|
||||||
cwd: reactAppRootDirPath
|
cwd: projectDirPath
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return pathJoin(
|
return pathJoin(
|
||||||
reactAppRootDirPath,
|
projectDirPath,
|
||||||
resolvedViteConfig?.buildDir === undefined
|
resolvedViteConfig?.buildDir === undefined
|
||||||
? "build_keycloak"
|
? "build_keycloak"
|
||||||
: `${resolvedViteConfig.buildDir}_keycloak`
|
: `${resolvedViteConfig.buildDir}_keycloak`
|
||||||
@ -241,14 +245,14 @@ export function readBuildOptions(params: {
|
|||||||
if (process.env.PUBLIC_DIR_PATH !== undefined) {
|
if (process.env.PUBLIC_DIR_PATH !== undefined) {
|
||||||
return getAbsoluteAndInOsFormatPath({
|
return getAbsoluteAndInOsFormatPath({
|
||||||
pathIsh: process.env.PUBLIC_DIR_PATH,
|
pathIsh: process.env.PUBLIC_DIR_PATH,
|
||||||
cwd: reactAppRootDirPath
|
cwd: projectDirPath
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return pathJoin(reactAppRootDirPath, "public");
|
return pathJoin(projectDirPath, "public");
|
||||||
}
|
}
|
||||||
|
|
||||||
return pathJoin(reactAppRootDirPath, resolvedViteConfig.publicDir);
|
return pathJoin(projectDirPath, resolvedViteConfig.publicDir);
|
||||||
})(),
|
})(),
|
||||||
cacheDirPath: (() => {
|
cacheDirPath: (() => {
|
||||||
const cacheDirPath = pathJoin(
|
const cacheDirPath = pathJoin(
|
||||||
@ -297,11 +301,28 @@ export function readBuildOptions(params: {
|
|||||||
break webpack;
|
break webpack;
|
||||||
}
|
}
|
||||||
|
|
||||||
return pathJoin(reactAppBuildDirPath, "static");
|
return pathJoin(projectBuildDirPath, "static");
|
||||||
}
|
}
|
||||||
|
|
||||||
return pathJoin(reactAppBuildDirPath, resolvedViteConfig.assetsDir);
|
return pathJoin(projectBuildDirPath, resolvedViteConfig.assetsDir);
|
||||||
})(),
|
})(),
|
||||||
npmWorkspaceRootDirPath
|
npmWorkspaceRootDirPath,
|
||||||
|
kcContextExclusionsFtlCode: (() => {
|
||||||
|
if (buildOptions.kcContextExclusionsFtl === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildOptions.kcContextExclusionsFtl.endsWith(".ftl")) {
|
||||||
|
const kcContextExclusionsFtlPath = getAbsoluteAndInOsFormatPath({
|
||||||
|
pathIsh: buildOptions.kcContextExclusionsFtl,
|
||||||
|
cwd: projectDirPath
|
||||||
|
});
|
||||||
|
|
||||||
|
return fs.readFileSync(kcContextExclusionsFtlPath).toString("utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildOptions.kcContextExclusionsFtl;
|
||||||
|
})(),
|
||||||
|
environmentVariables: buildOptions.environmentVariables ?? []
|
||||||
};
|
};
|
||||||
}
|
}
|
@ -16,7 +16,7 @@ export const vitePluginSubScriptEnvNames = {
|
|||||||
resolveViteConfig: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG"
|
resolveViteConfig: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG"
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const skipBuildJarsEnvName = "KEYCLOAKIFY_SKIP_BUILD_JAR";
|
export const onlyBuildJarFileBasenameEnvName = "KEYCLOAKIFY_ONLY_BUILD_JAR_FILE_BASENAME";
|
||||||
|
|
||||||
export const loginThemePageIds = [
|
export const loginThemePageIds = [
|
||||||
"login.ftl",
|
"login.ftl",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
downloadKeycloakStaticResources,
|
downloadKeycloakStaticResources,
|
||||||
type BuildOptionsLike as BuildOptionsLike_downloadKeycloakStaticResources
|
type BuildContextLike as BuildContextLike_downloadKeycloakStaticResources
|
||||||
} from "./downloadKeycloakStaticResources";
|
} from "./downloadKeycloakStaticResources";
|
||||||
import { join as pathJoin, relative as pathRelative } from "path";
|
import { join as pathJoin, relative as pathRelative } from "path";
|
||||||
import {
|
import {
|
||||||
@ -12,21 +12,21 @@ import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
|
|||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { rmSync } from "../tools/fs.rmSync";
|
import { rmSync } from "../tools/fs.rmSync";
|
||||||
import type { BuildOptions } from "./buildOptions";
|
import type { BuildContext } from "./buildContext";
|
||||||
|
|
||||||
export type BuildOptionsLike = BuildOptionsLike_downloadKeycloakStaticResources & {
|
export type BuildContextLike = BuildContextLike_downloadKeycloakStaticResources & {
|
||||||
loginThemeResourcesFromKeycloakVersion: string;
|
loginThemeResourcesFromKeycloakVersion: string;
|
||||||
publicDirPath: string;
|
publicDirPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export async function copyKeycloakResourcesToPublic(params: {
|
export async function copyKeycloakResourcesToPublic(params: {
|
||||||
buildOptions: BuildOptionsLike;
|
buildContext: BuildContextLike;
|
||||||
}) {
|
}) {
|
||||||
const { buildOptions } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const destDirPath = pathJoin(buildOptions.publicDirPath, keycloak_resources);
|
const destDirPath = pathJoin(buildContext.publicDirPath, keycloak_resources);
|
||||||
|
|
||||||
const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
|
const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
|
||||||
|
|
||||||
@ -34,12 +34,12 @@ export async function copyKeycloakResourcesToPublic(params: {
|
|||||||
{
|
{
|
||||||
destDirPath,
|
destDirPath,
|
||||||
keycloakifyVersion: readThisNpmPackageVersion(),
|
keycloakifyVersion: readThisNpmPackageVersion(),
|
||||||
buildOptions: {
|
buildContext: {
|
||||||
loginThemeResourcesFromKeycloakVersion: readThisNpmPackageVersion(),
|
loginThemeResourcesFromKeycloakVersion: readThisNpmPackageVersion(),
|
||||||
cacheDirPath: pathRelative(destDirPath, buildOptions.cacheDirPath),
|
cacheDirPath: pathRelative(destDirPath, buildContext.cacheDirPath),
|
||||||
npmWorkspaceRootDirPath: pathRelative(
|
npmWorkspaceRootDirPath: pathRelative(
|
||||||
destDirPath,
|
destDirPath,
|
||||||
buildOptions.npmWorkspaceRootDirPath
|
buildContext.npmWorkspaceRootDirPath
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -74,14 +74,14 @@ export async function copyKeycloakResourcesToPublic(params: {
|
|||||||
keycloakVersion: (() => {
|
keycloakVersion: (() => {
|
||||||
switch (themeType) {
|
switch (themeType) {
|
||||||
case "login":
|
case "login":
|
||||||
return buildOptions.loginThemeResourcesFromKeycloakVersion;
|
return buildContext.loginThemeResourcesFromKeycloakVersion;
|
||||||
case "account":
|
case "account":
|
||||||
return lastKeycloakVersionWithAccountV1;
|
return lastKeycloakVersionWithAccountV1;
|
||||||
}
|
}
|
||||||
})(),
|
})(),
|
||||||
themeType,
|
themeType,
|
||||||
themeDirPath: destDirPath,
|
themeDirPath: destDirPath,
|
||||||
buildOptions
|
buildContext
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,27 +1,27 @@
|
|||||||
import { join as pathJoin, relative as pathRelative } from "path";
|
import { join as pathJoin, relative as pathRelative } from "path";
|
||||||
import { type BuildOptions } from "./buildOptions";
|
import { type BuildContext } from "./buildContext";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import { lastKeycloakVersionWithAccountV1 } from "./constants";
|
import { lastKeycloakVersionWithAccountV1 } from "./constants";
|
||||||
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
|
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
|
||||||
import { isInside } from "../tools/isInside";
|
import { isInside } from "../tools/isInside";
|
||||||
|
|
||||||
export type BuildOptionsLike = {
|
export type BuildContextLike = {
|
||||||
cacheDirPath: string;
|
cacheDirPath: string;
|
||||||
npmWorkspaceRootDirPath: string;
|
npmWorkspaceRootDirPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export async function downloadKeycloakDefaultTheme(params: {
|
export async function downloadKeycloakDefaultTheme(params: {
|
||||||
keycloakVersion: string;
|
keycloakVersion: string;
|
||||||
buildOptions: BuildOptionsLike;
|
buildContext: BuildContextLike;
|
||||||
}): Promise<{ defaultThemeDirPath: string }> {
|
}): Promise<{ defaultThemeDirPath: string }> {
|
||||||
const { keycloakVersion, buildOptions } = params;
|
const { keycloakVersion, buildContext } = params;
|
||||||
|
|
||||||
const { extractedDirPath } = await downloadAndExtractArchive({
|
const { extractedDirPath } = await downloadAndExtractArchive({
|
||||||
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
|
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
|
||||||
cacheDirPath: buildOptions.cacheDirPath,
|
cacheDirPath: buildContext.cacheDirPath,
|
||||||
npmWorkspaceRootDirPath: buildOptions.npmWorkspaceRootDirPath,
|
npmWorkspaceRootDirPath: buildContext.npmWorkspaceRootDirPath,
|
||||||
uniqueIdOfOnOnArchiveFile: "downloadKeycloakDefaultTheme",
|
uniqueIdOfOnOnArchiveFile: "downloadKeycloakDefaultTheme",
|
||||||
onArchiveFile: async params => {
|
onArchiveFile: async params => {
|
||||||
if (!isInside({ dirPath: "theme", filePath: params.fileRelativePath })) {
|
if (!isInside({ dirPath: "theme", filePath: params.fileRelativePath })) {
|
||||||
|
@ -2,28 +2,28 @@ import { transformCodebase } from "../tools/transformCodebase";
|
|||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import {
|
import {
|
||||||
downloadKeycloakDefaultTheme,
|
downloadKeycloakDefaultTheme,
|
||||||
type BuildOptionsLike as BuildOptionsLike_downloadKeycloakDefaultTheme
|
type BuildContextLike as BuildContextLike_downloadKeycloakDefaultTheme
|
||||||
} from "./downloadKeycloakDefaultTheme";
|
} from "./downloadKeycloakDefaultTheme";
|
||||||
import { resources_common, type ThemeType } from "./constants";
|
import { resources_common, type ThemeType } from "./constants";
|
||||||
import type { BuildOptions } from "./buildOptions";
|
import type { BuildContext } from "./buildContext";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import { existsAsync } from "../tools/fs.existsAsync";
|
import { existsAsync } from "../tools/fs.existsAsync";
|
||||||
|
|
||||||
export type BuildOptionsLike = BuildOptionsLike_downloadKeycloakDefaultTheme & {};
|
export type BuildContextLike = BuildContextLike_downloadKeycloakDefaultTheme & {};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export async function downloadKeycloakStaticResources(params: {
|
export async function downloadKeycloakStaticResources(params: {
|
||||||
themeType: ThemeType;
|
themeType: ThemeType;
|
||||||
themeDirPath: string;
|
themeDirPath: string;
|
||||||
keycloakVersion: string;
|
keycloakVersion: string;
|
||||||
buildOptions: BuildOptionsLike;
|
buildContext: BuildContextLike;
|
||||||
}) {
|
}) {
|
||||||
const { themeType, themeDirPath, keycloakVersion, buildOptions } = params;
|
const { themeType, themeDirPath, keycloakVersion, buildContext } = params;
|
||||||
|
|
||||||
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
||||||
keycloakVersion,
|
keycloakVersion,
|
||||||
buildOptions
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
const resourcesDirPath = pathJoin(themeDirPath, themeType, "resources");
|
const resourcesDirPath = pathJoin(themeDirPath, themeType, "resources");
|
||||||
|
72
src/bin/shared/generateKcGenTs.ts
Normal file
72
src/bin/shared/generateKcGenTs.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { BuildContext } from "./buildContext";
|
||||||
|
import { getThemeSrcDirPath } from "./getThemeSrcDirPath";
|
||||||
|
import * as fs from "fs/promises";
|
||||||
|
import { join as pathJoin } from "path";
|
||||||
|
import { existsAsync } from "../tools/fs.existsAsync";
|
||||||
|
|
||||||
|
export type BuildContextLike = {
|
||||||
|
projectDirPath: string;
|
||||||
|
themeNames: string[];
|
||||||
|
environmentVariables: { name: string; default: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
|
export async function generateKcGenTs(params: {
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
}): Promise<void> {
|
||||||
|
const { buildContext } = params;
|
||||||
|
|
||||||
|
const { themeSrcDirPath } = getThemeSrcDirPath({
|
||||||
|
projectDirPath: buildContext.projectDirPath
|
||||||
|
});
|
||||||
|
|
||||||
|
const filePath = pathJoin(themeSrcDirPath, "kc.gen.ts");
|
||||||
|
|
||||||
|
const currentContent = (await existsAsync(filePath))
|
||||||
|
? await fs.readFile(filePath)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const newContent = Buffer.from(
|
||||||
|
[
|
||||||
|
`/* prettier-ignore-start */`,
|
||||||
|
``,
|
||||||
|
`/* eslint-disable */`,
|
||||||
|
``,
|
||||||
|
`// @ts-nocheck`,
|
||||||
|
``,
|
||||||
|
`// noinspection JSUnusedGlobalSymbols`,
|
||||||
|
``,
|
||||||
|
`// This file is auto-generated by Keycloakify`,
|
||||||
|
``,
|
||||||
|
`export type ThemeName = ${buildContext.themeNames.map(themeName => `"${themeName}"`).join(" | ")};`,
|
||||||
|
``,
|
||||||
|
`export const themeNames: ThemeName[] = [${buildContext.themeNames.map(themeName => `"${themeName}"`).join(", ")}];`,
|
||||||
|
``,
|
||||||
|
`export type KcEnvName = ${buildContext.environmentVariables.length === 0 ? "never" : buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(" | ")};`,
|
||||||
|
``,
|
||||||
|
`export const KcEnvNames: KcEnvName[] = [${buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(", ")}];`,
|
||||||
|
``,
|
||||||
|
`export const kcEnvDefaults: Record<KcEnvName, string> = ${JSON.stringify(
|
||||||
|
Object.fromEntries(
|
||||||
|
buildContext.environmentVariables.map(
|
||||||
|
({ name, default: defaultValue }) => [name, defaultValue]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)};`,
|
||||||
|
``,
|
||||||
|
`/* prettier-ignore-end */`,
|
||||||
|
``
|
||||||
|
].join("\n"),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentContent !== undefined && currentContent.equals(newContent)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(filePath, newContent);
|
||||||
|
}
|
@ -7,10 +7,10 @@ import { themeTypes } from "./constants";
|
|||||||
const themeSrcDirBasenames = ["keycloak-theme", "keycloak_theme"];
|
const themeSrcDirBasenames = ["keycloak-theme", "keycloak_theme"];
|
||||||
|
|
||||||
/** Can't catch error, if the directory isn't found, this function will just exit the process with an error message. */
|
/** Can't catch error, if the directory isn't found, this function will just exit the process with an error message. */
|
||||||
export function getThemeSrcDirPath(params: { reactAppRootDirPath: string }) {
|
export function getThemeSrcDirPath(params: { projectDirPath: string }) {
|
||||||
const { reactAppRootDirPath } = params;
|
const { projectDirPath } = params;
|
||||||
|
|
||||||
const srcDirPath = pathJoin(reactAppRootDirPath, "src");
|
const srcDirPath = pathJoin(projectDirPath, "src");
|
||||||
|
|
||||||
const themeSrcDirPath: string | undefined = crawl({
|
const themeSrcDirPath: string | undefined = crawl({
|
||||||
dirPath: srcDirPath,
|
dirPath: srcDirPath,
|
||||||
|
@ -1,50 +1,73 @@
|
|||||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||||
import type { ThemeType } from "./constants";
|
import type { ThemeType } from "./constants";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { extractArchive } from "../tools/extractArchive";
|
||||||
|
|
||||||
export type MetaInfKeycloakTheme = {
|
export type MetaInfKeycloakTheme = {
|
||||||
themes: { name: string; types: (ThemeType | "email")[] }[];
|
themes: { name: string; types: (ThemeType | "email")[] }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getMetaInfKeycloakThemesJsonFilePath(params: {
|
export function getMetaInfKeycloakThemesJsonFilePath(params: {
|
||||||
keycloakifyBuildDirPath: string;
|
resourcesDirPath: string;
|
||||||
}) {
|
}) {
|
||||||
const { keycloakifyBuildDirPath } = params;
|
const { resourcesDirPath } = params;
|
||||||
|
|
||||||
return pathJoin(
|
return pathJoin(
|
||||||
keycloakifyBuildDirPath === "." ? "" : keycloakifyBuildDirPath,
|
resourcesDirPath === "." ? "" : resourcesDirPath,
|
||||||
"src",
|
|
||||||
"main",
|
|
||||||
"resources",
|
|
||||||
"META-INF",
|
"META-INF",
|
||||||
"keycloak-themes.json"
|
"keycloak-themes.json"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readMetaInfKeycloakThemes(params: {
|
export function readMetaInfKeycloakThemes_fromResourcesDirPath(params: {
|
||||||
keycloakifyBuildDirPath: string;
|
resourcesDirPath: string;
|
||||||
}): MetaInfKeycloakTheme {
|
}) {
|
||||||
const { keycloakifyBuildDirPath } = params;
|
const { resourcesDirPath } = params;
|
||||||
|
|
||||||
return JSON.parse(
|
return JSON.parse(
|
||||||
fs
|
fs
|
||||||
.readFileSync(
|
.readFileSync(
|
||||||
getMetaInfKeycloakThemesJsonFilePath({
|
getMetaInfKeycloakThemesJsonFilePath({
|
||||||
keycloakifyBuildDirPath
|
resourcesDirPath
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.toString("utf8")
|
.toString("utf8")
|
||||||
) as MetaInfKeycloakTheme;
|
) as MetaInfKeycloakTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function readMetaInfKeycloakThemes_fromJar(params: {
|
||||||
|
jarFilePath: string;
|
||||||
|
}): Promise<MetaInfKeycloakTheme> {
|
||||||
|
const { jarFilePath } = params;
|
||||||
|
let metaInfKeycloakThemes: MetaInfKeycloakTheme | undefined = undefined;
|
||||||
|
|
||||||
|
await extractArchive({
|
||||||
|
archiveFilePath: jarFilePath,
|
||||||
|
onArchiveFile: async ({ relativeFilePathInArchive, readFile, earlyExit }) => {
|
||||||
|
if (
|
||||||
|
relativeFilePathInArchive ===
|
||||||
|
getMetaInfKeycloakThemesJsonFilePath({ resourcesDirPath: "." })
|
||||||
|
) {
|
||||||
|
metaInfKeycloakThemes = JSON.parse((await readFile()).toString("utf8"));
|
||||||
|
earlyExit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(metaInfKeycloakThemes !== undefined);
|
||||||
|
|
||||||
|
return metaInfKeycloakThemes;
|
||||||
|
}
|
||||||
|
|
||||||
export function writeMetaInfKeycloakThemes(params: {
|
export function writeMetaInfKeycloakThemes(params: {
|
||||||
keycloakifyBuildDirPath: string;
|
resourcesDirPath: string;
|
||||||
metaInfKeycloakThemes: MetaInfKeycloakTheme;
|
metaInfKeycloakThemes: MetaInfKeycloakTheme;
|
||||||
}) {
|
}) {
|
||||||
const { keycloakifyBuildDirPath, metaInfKeycloakThemes } = params;
|
const { resourcesDirPath, metaInfKeycloakThemes } = params;
|
||||||
|
|
||||||
const metaInfKeycloakThemesJsonPath = getMetaInfKeycloakThemesJsonFilePath({
|
const metaInfKeycloakThemesJsonPath = getMetaInfKeycloakThemesJsonFilePath({
|
||||||
keycloakifyBuildDirPath
|
resourcesDirPath
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -2,26 +2,26 @@ import * as child_process from "child_process";
|
|||||||
import { Deferred } from "evt/tools/Deferred";
|
import { Deferred } from "evt/tools/Deferred";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import { is } from "tsafe/is";
|
import { is } from "tsafe/is";
|
||||||
import type { BuildOptions } from "../shared/buildOptions";
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
|
|
||||||
export type BuildOptionsLike = {
|
export type BuildContextLike = {
|
||||||
reactAppRootDirPath: string;
|
projectDirPath: string;
|
||||||
keycloakifyBuildDirPath: string;
|
keycloakifyBuildDirPath: string;
|
||||||
bundler: "vite" | "webpack";
|
bundler: "vite" | "webpack";
|
||||||
npmWorkspaceRootDirPath: string;
|
npmWorkspaceRootDirPath: string;
|
||||||
reactAppBuildDirPath: string;
|
projectBuildDirPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export async function appBuild(params: {
|
export async function appBuild(params: {
|
||||||
buildOptions: BuildOptionsLike;
|
buildContext: BuildContextLike;
|
||||||
}): Promise<{ isAppBuildSuccess: boolean }> {
|
}): Promise<{ isAppBuildSuccess: boolean }> {
|
||||||
const { buildOptions } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const { bundler } = buildOptions;
|
const { bundler } = buildContext;
|
||||||
|
|
||||||
const { command, args, cwd } = (() => {
|
const { command, args, cwd } = (() => {
|
||||||
switch (bundler) {
|
switch (bundler) {
|
||||||
@ -29,12 +29,12 @@ export async function appBuild(params: {
|
|||||||
return {
|
return {
|
||||||
command: "npx",
|
command: "npx",
|
||||||
args: ["vite", "build"],
|
args: ["vite", "build"],
|
||||||
cwd: buildOptions.reactAppRootDirPath
|
cwd: buildContext.projectDirPath
|
||||||
};
|
};
|
||||||
case "webpack": {
|
case "webpack": {
|
||||||
for (const dirPath of [
|
for (const dirPath of [
|
||||||
buildOptions.reactAppRootDirPath,
|
buildContext.projectDirPath,
|
||||||
buildOptions.npmWorkspaceRootDirPath
|
buildContext.npmWorkspaceRootDirPath
|
||||||
]) {
|
]) {
|
||||||
try {
|
try {
|
||||||
const parsedPackageJson = JSON.parse(
|
const parsedPackageJson = JSON.parse(
|
||||||
|
@ -1,31 +1,31 @@
|
|||||||
import { skipBuildJarsEnvName } from "../shared/constants";
|
import { onlyBuildJarFileBasenameEnvName } from "../shared/constants";
|
||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import { Deferred } from "evt/tools/Deferred";
|
import { Deferred } from "evt/tools/Deferred";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import type { BuildOptions } from "../shared/buildOptions";
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
|
|
||||||
export type BuildOptionsLike = {
|
export type BuildContextLike = {
|
||||||
reactAppRootDirPath: string;
|
projectDirPath: string;
|
||||||
keycloakifyBuildDirPath: string;
|
keycloakifyBuildDirPath: string;
|
||||||
bundler: "vite" | "webpack";
|
bundler: "vite" | "webpack";
|
||||||
npmWorkspaceRootDirPath: string;
|
npmWorkspaceRootDirPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export async function keycloakifyBuild(params: {
|
export async function keycloakifyBuild(params: {
|
||||||
doSkipBuildJars: boolean;
|
onlyBuildJarFileBasename: string | undefined;
|
||||||
buildOptions: BuildOptionsLike;
|
buildContext: BuildContextLike;
|
||||||
}): Promise<{ isKeycloakifyBuildSuccess: boolean }> {
|
}): Promise<{ isKeycloakifyBuildSuccess: boolean }> {
|
||||||
const { buildOptions, doSkipBuildJars } = params;
|
const { buildContext, onlyBuildJarFileBasename } = params;
|
||||||
|
|
||||||
const dResult = new Deferred<{ isSuccess: boolean }>();
|
const dResult = new Deferred<{ isSuccess: boolean }>();
|
||||||
|
|
||||||
const child = child_process.spawn("npx", ["keycloakify", "build"], {
|
const child = child_process.spawn("npx", ["keycloakify", "build"], {
|
||||||
cwd: buildOptions.reactAppRootDirPath,
|
cwd: buildContext.projectDirPath,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
...(doSkipBuildJars ? { [skipBuildJarsEnvName]: "true" } : {})
|
[onlyBuildJarFileBasenameEnvName]: onlyBuildJarFileBasename
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { readBuildOptions } from "../shared/buildOptions";
|
import { getBuildContext } from "../shared/buildContext";
|
||||||
|
import { exclude } from "tsafe/exclude";
|
||||||
import type { CliCommandOptions as CliCommandOptions_common } from "../main";
|
import type { CliCommandOptions as CliCommandOptions_common } from "../main";
|
||||||
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
|
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
|
||||||
import { readMetaInfKeycloakThemes } from "../shared/metaInfKeycloakThemes";
|
import { readMetaInfKeycloakThemes_fromJar } from "../shared/metaInfKeycloakThemes";
|
||||||
import { accountV1ThemeName, containerName } from "../shared/constants";
|
import { accountV1ThemeName, containerName } from "../shared/constants";
|
||||||
import { SemVer } from "../tools/SemVer";
|
import { SemVer } from "../tools/SemVer";
|
||||||
import type { KeycloakVersionRange } from "../shared/KeycloakVersionRange";
|
import type { KeycloakVersionRange } from "../shared/KeycloakVersionRange";
|
||||||
@ -20,6 +21,9 @@ import * as runExclusive from "run-exclusive";
|
|||||||
import { extractArchive } from "../tools/extractArchive";
|
import { extractArchive } from "../tools/extractArchive";
|
||||||
import { appBuild } from "./appBuild";
|
import { appBuild } from "./appBuild";
|
||||||
import { keycloakifyBuild } from "./keycloakifyBuild";
|
import { keycloakifyBuild } from "./keycloakifyBuild";
|
||||||
|
import { isInside } from "../tools/isInside";
|
||||||
|
import { existsAsync } from "../tools/fs.existsAsync";
|
||||||
|
import { rm } from "../tools/fs.rm";
|
||||||
|
|
||||||
export type CliCommandOptions = CliCommandOptions_common & {
|
export type CliCommandOptions = CliCommandOptions_common & {
|
||||||
port: number;
|
port: number;
|
||||||
@ -80,11 +84,11 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
|
|
||||||
const { cliCommandOptions } = params;
|
const { cliCommandOptions } = params;
|
||||||
|
|
||||||
const buildOptions = readBuildOptions({ cliCommandOptions });
|
const buildContext = getBuildContext({ cliCommandOptions });
|
||||||
|
|
||||||
{
|
{
|
||||||
const { isAppBuildSuccess } = await appBuild({
|
const { isAppBuildSuccess } = await appBuild({
|
||||||
buildOptions
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isAppBuildSuccess) {
|
if (!isAppBuildSuccess) {
|
||||||
@ -97,8 +101,8 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({
|
const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({
|
||||||
doSkipBuildJars: false,
|
onlyBuildJarFileBasename: undefined,
|
||||||
buildOptions
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isKeycloakifyBuildSuccess) {
|
if (!isKeycloakifyBuildSuccess) {
|
||||||
@ -111,13 +115,31 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const metaInfKeycloakThemes = readMetaInfKeycloakThemes({
|
const { doesImplementAccountTheme } = await (async () => {
|
||||||
keycloakifyBuildDirPath: buildOptions.keycloakifyBuildDirPath
|
const latestJarFilePath = fs
|
||||||
});
|
.readdirSync(buildContext.keycloakifyBuildDirPath)
|
||||||
|
.filter(fileBasename => fileBasename.endsWith(".jar"))
|
||||||
|
.map(fileBasename =>
|
||||||
|
pathJoin(buildContext.keycloakifyBuildDirPath, fileBasename)
|
||||||
|
)
|
||||||
|
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0];
|
||||||
|
|
||||||
const doesImplementAccountTheme = metaInfKeycloakThemes.themes.some(
|
assert(latestJarFilePath !== undefined);
|
||||||
({ name }) => name === accountV1ThemeName
|
|
||||||
);
|
const metaInfKeycloakThemes = await readMetaInfKeycloakThemes_fromJar({
|
||||||
|
jarFilePath: latestJarFilePath
|
||||||
|
});
|
||||||
|
|
||||||
|
const mainThemeEntry = metaInfKeycloakThemes.themes.find(
|
||||||
|
({ name }) => name === buildContext.themeNames[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(mainThemeEntry !== undefined);
|
||||||
|
|
||||||
|
const doesImplementAccountTheme = mainThemeEntry.types.includes("account");
|
||||||
|
|
||||||
|
return { doesImplementAccountTheme };
|
||||||
|
})();
|
||||||
|
|
||||||
const { keycloakVersion, keycloakMajorNumber: keycloakMajorVersionNumber } =
|
const { keycloakVersion, keycloakMajorNumber: keycloakMajorVersionNumber } =
|
||||||
await (async function getKeycloakMajor(): Promise<{
|
await (async function getKeycloakMajor(): Promise<{
|
||||||
@ -138,7 +160,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
|
|
||||||
const { keycloakVersion } = await promptKeycloakVersion({
|
const { keycloakVersion } = await promptKeycloakVersion({
|
||||||
startingFromMajor: 17,
|
startingFromMajor: 17,
|
||||||
cacheDirPath: buildOptions.cacheDirPath
|
cacheDirPath: buildContext.cacheDirPath
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`→ ${keycloakVersion}`);
|
console.log(`→ ${keycloakVersion}`);
|
||||||
@ -171,7 +193,11 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
return "23" as const;
|
return "23" as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "24-and-above" as const;
|
if (keycloakMajorVersionNumber === 24) {
|
||||||
|
return "24" as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "25-and-above" as const;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
assert<
|
assert<
|
||||||
@ -259,67 +285,32 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
return pathJoin(dirPath, value);
|
return pathJoin(dirPath, value);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const jarFilePath = pathJoin(buildOptions.keycloakifyBuildDirPath, jarFileBasename);
|
const jarFilePath = pathJoin(buildContext.keycloakifyBuildDirPath, jarFileBasename);
|
||||||
|
|
||||||
const { doUseBuiltInAccountV1Theme } = await (async () => {
|
|
||||||
let doUseBuiltInAccountV1Theme = false;
|
|
||||||
|
|
||||||
|
async function extractThemeResourcesFromJar() {
|
||||||
await extractArchive({
|
await extractArchive({
|
||||||
archiveFilePath: jarFilePath,
|
archiveFilePath: jarFilePath,
|
||||||
onArchiveFile: async ({ relativeFilePathInArchive, readFile, earlyExit }) => {
|
onArchiveFile: async ({ relativeFilePathInArchive, writeFile }) => {
|
||||||
for (const themeName of buildOptions.themeNames) {
|
if (isInside({ dirPath: "theme", filePath: relativeFilePathInArchive })) {
|
||||||
if (
|
await writeFile({
|
||||||
relativeFilePathInArchive ===
|
filePath: pathJoin(
|
||||||
pathJoin("theme", themeName, "account", "theme.properties")
|
buildContext.keycloakifyBuildDirPath,
|
||||||
) {
|
relativeFilePathInArchive
|
||||||
if (
|
)
|
||||||
(await readFile())
|
});
|
||||||
.toString("utf8")
|
|
||||||
.includes("parent=keycloak")
|
|
||||||
) {
|
|
||||||
doUseBuiltInAccountV1Theme = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
earlyExit();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return { doUseBuiltInAccountV1Theme };
|
{
|
||||||
})();
|
const destDirPath = pathJoin(buildContext.keycloakifyBuildDirPath, "theme");
|
||||||
|
if (await existsAsync(destDirPath)) {
|
||||||
|
await rm(destDirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const accountThemePropertyPatch = !doUseBuiltInAccountV1Theme
|
await extractThemeResourcesFromJar();
|
||||||
? undefined
|
|
||||||
: () => {
|
|
||||||
for (const themeName of buildOptions.themeNames) {
|
|
||||||
const filePath = pathJoin(
|
|
||||||
buildOptions.keycloakifyBuildDirPath,
|
|
||||||
"src",
|
|
||||||
"main",
|
|
||||||
"resources",
|
|
||||||
"theme",
|
|
||||||
themeName,
|
|
||||||
"account",
|
|
||||||
"theme.properties"
|
|
||||||
);
|
|
||||||
|
|
||||||
const sourceCode = fs.readFileSync(filePath);
|
|
||||||
|
|
||||||
const modifiedSourceCode = Buffer.from(
|
|
||||||
sourceCode
|
|
||||||
.toString("utf8")
|
|
||||||
.replace(`parent=${accountV1ThemeName}`, "parent=keycloak"),
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert(Buffer.compare(modifiedSourceCode, sourceCode) !== 0);
|
|
||||||
|
|
||||||
fs.writeFileSync(filePath, modifiedSourceCode);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
accountThemePropertyPatch?.();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
child_process.execSync(`docker rm --force ${containerName}`, {
|
child_process.execSync(`docker rm --force ${containerName}`, {
|
||||||
@ -346,15 +337,20 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
? ["-e", "JAVA_OPTS=-Dkeycloak.profile=preview"]
|
? ["-e", "JAVA_OPTS=-Dkeycloak.profile=preview"]
|
||||||
: []),
|
: []),
|
||||||
...[
|
...[
|
||||||
...buildOptions.themeNames,
|
...buildContext.themeNames,
|
||||||
...(doUseBuiltInAccountV1Theme ? [] : [accountV1ThemeName])
|
...(fs.existsSync(
|
||||||
|
pathJoin(
|
||||||
|
buildContext.keycloakifyBuildDirPath,
|
||||||
|
"theme",
|
||||||
|
accountV1ThemeName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
? [accountV1ThemeName]
|
||||||
|
: [])
|
||||||
]
|
]
|
||||||
.map(themeName => ({
|
.map(themeName => ({
|
||||||
localDirPath: pathJoin(
|
localDirPath: pathJoin(
|
||||||
buildOptions.keycloakifyBuildDirPath,
|
buildContext.keycloakifyBuildDirPath,
|
||||||
"src",
|
|
||||||
"main",
|
|
||||||
"resources",
|
|
||||||
"theme",
|
"theme",
|
||||||
themeName
|
themeName
|
||||||
),
|
),
|
||||||
@ -365,6 +361,17 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
`${localDirPath}:${containerDirPath}:rw`
|
`${localDirPath}:${containerDirPath}:rw`
|
||||||
])
|
])
|
||||||
.flat(),
|
.flat(),
|
||||||
|
...buildContext.environmentVariables
|
||||||
|
.map(({ name }) => ({ name, envValue: process.env[name] }))
|
||||||
|
.map(({ name, envValue }) =>
|
||||||
|
envValue === undefined ? undefined : { name, envValue }
|
||||||
|
)
|
||||||
|
.filter(exclude(undefined))
|
||||||
|
.map(({ name, envValue }) => [
|
||||||
|
"--env",
|
||||||
|
`${name}='${envValue.replace(/'/g, "'\\''")}'`
|
||||||
|
])
|
||||||
|
.flat(),
|
||||||
`quay.io/keycloak/keycloak:${keycloakVersion}`,
|
`quay.io/keycloak/keycloak:${keycloakVersion}`,
|
||||||
"start-dev",
|
"start-dev",
|
||||||
...(21 <= keycloakMajorVersionNumber && keycloakMajorVersionNumber < 24
|
...(21 <= keycloakMajorVersionNumber && keycloakMajorVersionNumber < 24
|
||||||
@ -373,7 +380,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
...(realmJsonFilePath === undefined ? [] : ["--import-realm"])
|
...(realmJsonFilePath === undefined ? [] : ["--import-realm"])
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
cwd: buildOptions.keycloakifyBuildDirPath
|
cwd: buildContext.keycloakifyBuildDirPath
|
||||||
}
|
}
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@ -385,7 +392,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
|
|
||||||
child.on("exit", process.exit);
|
child.on("exit", process.exit);
|
||||||
|
|
||||||
const srcDirPath = pathJoin(buildOptions.reactAppRootDirPath, "src");
|
const srcDirPath = pathJoin(buildContext.projectDirPath, "src");
|
||||||
|
|
||||||
{
|
{
|
||||||
const handler = async (data: Buffer) => {
|
const handler = async (data: Buffer) => {
|
||||||
@ -417,7 +424,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
`- password: ${chalk.cyan.bold("password123")}`,
|
`- password: ${chalk.cyan.bold("password123")}`,
|
||||||
"",
|
"",
|
||||||
`Watching for changes in ${chalk.bold(
|
`Watching for changes in ${chalk.bold(
|
||||||
`.${pathSep}${pathRelative(process.cwd(), buildOptions.reactAppRootDirPath)}`
|
`.${pathSep}${pathRelative(process.cwd(), buildContext.projectDirPath)}`
|
||||||
)}`
|
)}`
|
||||||
].join("\n")
|
].join("\n")
|
||||||
);
|
);
|
||||||
@ -431,7 +438,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
console.log(chalk.cyan("Detected changes in the theme. Rebuilding ..."));
|
console.log(chalk.cyan("Detected changes in the theme. Rebuilding ..."));
|
||||||
|
|
||||||
const { isAppBuildSuccess } = await appBuild({
|
const { isAppBuildSuccess } = await appBuild({
|
||||||
buildOptions
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isAppBuildSuccess) {
|
if (!isAppBuildSuccess) {
|
||||||
@ -439,15 +446,15 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({
|
const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({
|
||||||
doSkipBuildJars: true,
|
onlyBuildJarFileBasename: jarFileBasename,
|
||||||
buildOptions
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isKeycloakifyBuildSuccess) {
|
if (!isKeycloakifyBuildSuccess) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
accountThemePropertyPatch?.();
|
await extractThemeResourcesFromJar();
|
||||||
|
|
||||||
console.log(chalk.green("Theme rebuilt and updated in Keycloak."));
|
console.log(chalk.green("Theme rebuilt and updated in Keycloak."));
|
||||||
});
|
});
|
||||||
@ -458,11 +465,11 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
.watch(
|
.watch(
|
||||||
[
|
[
|
||||||
srcDirPath,
|
srcDirPath,
|
||||||
buildOptions.publicDirPath,
|
buildContext.publicDirPath,
|
||||||
pathJoin(buildOptions.reactAppRootDirPath, "package.json"),
|
pathJoin(buildContext.projectDirPath, "package.json"),
|
||||||
pathJoin(buildOptions.reactAppRootDirPath, "vite.config.ts"),
|
pathJoin(buildContext.projectDirPath, "vite.config.ts"),
|
||||||
pathJoin(buildOptions.reactAppRootDirPath, "vite.config.js"),
|
pathJoin(buildContext.projectDirPath, "vite.config.js"),
|
||||||
pathJoin(buildOptions.reactAppRootDirPath, "index.html"),
|
pathJoin(buildContext.projectDirPath, "index.html"),
|
||||||
pathJoin(getThisCodebaseRootDirPath(), "src")
|
pathJoin(getThisCodebaseRootDirPath(), "src")
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
|
64
src/bin/tools/escapeStringForPropertiesFile.ts
Normal file
64
src/bin/tools/escapeStringForPropertiesFile.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// Convert a JavaScript string to UTF-16 encoding
|
||||||
|
function toUTF16(codePoint: number): string {
|
||||||
|
if (codePoint <= 0xffff) {
|
||||||
|
// BMP character
|
||||||
|
return "\\u" + codePoint.toString(16).padStart(4, "0");
|
||||||
|
} else {
|
||||||
|
// Non-BMP character
|
||||||
|
codePoint -= 0x10000;
|
||||||
|
let highSurrogate = (codePoint >> 10) + 0xd800;
|
||||||
|
let lowSurrogate = (codePoint % 0x400) + 0xdc00;
|
||||||
|
return (
|
||||||
|
"\\u" +
|
||||||
|
highSurrogate.toString(16).padStart(4, "0") +
|
||||||
|
"\\u" +
|
||||||
|
lowSurrogate.toString(16).padStart(4, "0")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escapes special characters for use in a .properties file
|
||||||
|
export function escapeStringForPropertiesFile(str: string): string {
|
||||||
|
let escapedStr = "";
|
||||||
|
for (const char of [...str]) {
|
||||||
|
const codePoint = char.codePointAt(0);
|
||||||
|
if (!codePoint) continue;
|
||||||
|
|
||||||
|
switch (char) {
|
||||||
|
case "\n":
|
||||||
|
escapedStr += "\\n";
|
||||||
|
break;
|
||||||
|
case "\r":
|
||||||
|
escapedStr += "\\r";
|
||||||
|
break;
|
||||||
|
case "\t":
|
||||||
|
escapedStr += "\\t";
|
||||||
|
break;
|
||||||
|
case "\\":
|
||||||
|
escapedStr += "\\\\";
|
||||||
|
break;
|
||||||
|
case ":":
|
||||||
|
escapedStr += "\\:";
|
||||||
|
break;
|
||||||
|
case "=":
|
||||||
|
escapedStr += "\\=";
|
||||||
|
break;
|
||||||
|
case "#":
|
||||||
|
escapedStr += "\\#";
|
||||||
|
break;
|
||||||
|
case "!":
|
||||||
|
escapedStr += "\\!";
|
||||||
|
break;
|
||||||
|
case "'":
|
||||||
|
escapedStr += "''";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (codePoint > 0x7f) {
|
||||||
|
escapedStr += toUTF16(codePoint); // Non-ASCII characters
|
||||||
|
} else {
|
||||||
|
escapedStr += char; // ASCII character needs no escape
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return escapedStr;
|
||||||
|
}
|
@ -4,14 +4,14 @@ import { assert } from "tsafe/assert";
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
|
|
||||||
export function getNpmWorkspaceRootDirPath(params: {
|
export function getNpmWorkspaceRootDirPath(params: {
|
||||||
reactAppRootDirPath: string;
|
projectDirPath: string;
|
||||||
dependencyExpected: string;
|
dependencyExpected: string;
|
||||||
}) {
|
}) {
|
||||||
const { reactAppRootDirPath, dependencyExpected } = params;
|
const { projectDirPath, dependencyExpected } = params;
|
||||||
|
|
||||||
const npmWorkspaceRootDirPath = (function callee(depth: number): string {
|
const npmWorkspaceRootDirPath = (function callee(depth: number): string {
|
||||||
const cwd = pathResolve(
|
const cwd = pathResolve(
|
||||||
pathJoin(...[reactAppRootDirPath, ...Array(depth).fill("..")])
|
pathJoin(...[projectDirPath, ...Array(depth).fill("..")])
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(cwd !== pathSep, "NPM workspace not found");
|
assert(cwd !== pathSep, "NPM workspace not found");
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsproject.json",
|
"extends": "../../tsproject.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "CommonJS",
|
"module": "ES2020",
|
||||||
"target": "ES5",
|
"target": "ES2017",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"lib": ["es2015", "DOM", "ES2019.Object"],
|
"lib": ["es2015", "ES2019.Object"],
|
||||||
|
"moduleResolution": "node",
|
||||||
"outDir": "../../dist/bin",
|
"outDir": "../../dist/bin",
|
||||||
"rootDir": "."
|
"rootDir": "."
|
||||||
}
|
}
|
||||||
|
13
src/bin/update-kc-gen.ts
Normal file
13
src/bin/update-kc-gen.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import type { CliCommandOptions } from "./main";
|
||||||
|
import { getBuildContext } from "./shared/buildContext";
|
||||||
|
import { generateKcGenTs } from "./shared/generateKcGenTs";
|
||||||
|
|
||||||
|
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||||
|
const { cliCommandOptions } = params;
|
||||||
|
|
||||||
|
const buildContext = getBuildContext({
|
||||||
|
cliCommandOptions
|
||||||
|
});
|
||||||
|
|
||||||
|
await generateKcGenTs({ buildContext });
|
||||||
|
}
|
89
src/lib/getKcClsx.ts
Normal file
89
src/lib/getKcClsx.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import type { Param0 } from "tsafe";
|
||||||
|
import { type CxArg, clsx_withTransform } from "../tools/clsx_withTransform";
|
||||||
|
import { clsx } from "../tools/clsx";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { is } from "tsafe/is";
|
||||||
|
|
||||||
|
export function createGetKcClsx<ClassKey extends string>(params: {
|
||||||
|
defaultClasses: Record<ClassKey, string | undefined>;
|
||||||
|
}) {
|
||||||
|
const { defaultClasses } = params;
|
||||||
|
|
||||||
|
function areSameParams(
|
||||||
|
params1: Param0<typeof getKcClsx>,
|
||||||
|
params2: Param0<typeof getKcClsx>
|
||||||
|
): boolean {
|
||||||
|
if (params1.doUseDefaultCss !== params2.doUseDefaultCss) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params1.classes === params2.classes) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params1.classes === undefined || params2.classes === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(params1.classes).length !== Object.keys(params2.classes).length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in params1.classes) {
|
||||||
|
if (params1.classes[key] !== params2.classes[key]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cache:
|
||||||
|
| {
|
||||||
|
params: Param0<typeof getKcClsx>;
|
||||||
|
result: ReturnType<typeof getKcClsx>;
|
||||||
|
}
|
||||||
|
| undefined = undefined;
|
||||||
|
|
||||||
|
function getKcClsx(params: {
|
||||||
|
doUseDefaultCss: boolean;
|
||||||
|
classes: Partial<Record<ClassKey, string>> | undefined;
|
||||||
|
}): { kcClsx: (...args: CxArg<ClassKey>[]) => string } {
|
||||||
|
// NOTE: We implement a cache here only so that getClassName can be stable across renders.
|
||||||
|
// We don't want to use useConstCallback because we want this to be useable outside of React.
|
||||||
|
use_cache: {
|
||||||
|
if (cache === undefined) {
|
||||||
|
break use_cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!areSameParams(cache.params, params)) {
|
||||||
|
break use_cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { classes, doUseDefaultCss } = params;
|
||||||
|
|
||||||
|
function kcClsx(...args: CxArg<ClassKey>[]): string {
|
||||||
|
return clsx_withTransform({
|
||||||
|
args,
|
||||||
|
transform: classKey => {
|
||||||
|
assert(is<ClassKey>(classKey));
|
||||||
|
|
||||||
|
return clsx(
|
||||||
|
classKey,
|
||||||
|
doUseDefaultCss ? defaultClasses[classKey] : undefined,
|
||||||
|
classes?.[classKey]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cache = { params, result: { kcClsx } };
|
||||||
|
|
||||||
|
return { kcClsx };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getKcClsx };
|
||||||
|
}
|
@ -1,3 +0,0 @@
|
|||||||
export const isStorybook =
|
|
||||||
typeof window === "object" &&
|
|
||||||
Object.keys(window).find(key => key.startsWith("__STORYBOOK")) !== undefined;
|
|
@ -1,27 +0,0 @@
|
|||||||
import { clsx } from "keycloakify/tools/clsx";
|
|
||||||
import { useConstCallback } from "keycloakify/tools/useConstCallback";
|
|
||||||
|
|
||||||
export function createUseClassName<ClassKey extends string>(params: {
|
|
||||||
defaultClasses: Record<ClassKey, string | undefined>;
|
|
||||||
}) {
|
|
||||||
const { defaultClasses } = params;
|
|
||||||
|
|
||||||
function useGetClassName(params: {
|
|
||||||
doUseDefaultCss: boolean;
|
|
||||||
classes: Partial<Record<ClassKey, string>> | undefined;
|
|
||||||
}) {
|
|
||||||
const { classes, doUseDefaultCss } = params;
|
|
||||||
|
|
||||||
const getClassName = useConstCallback((classKey: ClassKey): string => {
|
|
||||||
return clsx(
|
|
||||||
classKey,
|
|
||||||
doUseDefaultCss ? defaultClasses[classKey] : undefined,
|
|
||||||
classes?.[classKey]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return { getClassName };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { useGetClassName };
|
|
||||||
}
|
|
@ -1,9 +1,9 @@
|
|||||||
import { lazy, Suspense } from "react";
|
import { lazy, Suspense } from "react";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
|
||||||
import { assert, type Equals } from "tsafe/assert";
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
import type { I18n } from "./i18n";
|
|
||||||
import type { KcContext } from "./kcContext";
|
|
||||||
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
|
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
|
||||||
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
|
import type { I18n } from "keycloakify/login/i18n";
|
||||||
|
import type { KcContext } from "keycloakify/login/KcContext";
|
||||||
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields";
|
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields";
|
||||||
|
|
||||||
const Login = lazy(() => import("keycloakify/login/pages/Login"));
|
const Login = lazy(() => import("keycloakify/login/pages/Login"));
|
||||||
@ -41,11 +41,12 @@ const LoginResetOtp = lazy(() => import("keycloakify/login/pages/LoginResetOtp")
|
|||||||
const LoginX509Info = lazy(() => import("keycloakify/login/pages/LoginX509Info"));
|
const LoginX509Info = lazy(() => import("keycloakify/login/pages/LoginX509Info"));
|
||||||
const WebauthnError = lazy(() => import("keycloakify/login/pages/WebauthnError"));
|
const WebauthnError = lazy(() => import("keycloakify/login/pages/WebauthnError"));
|
||||||
|
|
||||||
type FallbackProps = PageProps<KcContext, I18n> & {
|
type DefaultPageProps = PageProps<KcContext, I18n> & {
|
||||||
UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>;
|
UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>;
|
||||||
|
doMakeUserConfirmPassword: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Fallback(props: FallbackProps) {
|
export default function DefaultPage(props: DefaultPageProps) {
|
||||||
const { kcContext, ...rest } = props;
|
const { kcContext, ...rest } = props;
|
||||||
|
|
||||||
return (
|
return (
|
@ -10,20 +10,20 @@ import type { Equals } from "tsafe";
|
|||||||
import type { MessageKey } from "../i18n/i18n";
|
import type { MessageKey } from "../i18n/i18n";
|
||||||
|
|
||||||
export type ExtendKcContext<
|
export type ExtendKcContext<
|
||||||
KcContextExtraProperties extends { properties?: Record<string, string | undefined> },
|
KcContextExtension extends { properties?: Record<string, string | undefined> },
|
||||||
KcContextExtraPropertiesPerPage extends Record<string, Record<string, unknown>>
|
KcContextExtensionPerPage extends Record<string, Record<string, unknown>>
|
||||||
> = ValueOf<{
|
> = ValueOf<{
|
||||||
[PageId in keyof KcContextExtraPropertiesPerPage | KcContext["pageId"]]: Extract<
|
[PageId in keyof KcContextExtensionPerPage | KcContext["pageId"]]: Extract<
|
||||||
KcContext,
|
KcContext,
|
||||||
{ pageId: PageId }
|
{ pageId: PageId }
|
||||||
> extends never
|
> extends never
|
||||||
? KcContext.Common &
|
? KcContext.Common &
|
||||||
KcContextExtraProperties & {
|
KcContextExtension & {
|
||||||
pageId: PageId;
|
pageId: PageId;
|
||||||
} & KcContextExtraPropertiesPerPage[PageId]
|
} & KcContextExtensionPerPage[PageId]
|
||||||
: Extract<KcContext, { pageId: PageId }> &
|
: Extract<KcContext, { pageId: PageId }> &
|
||||||
KcContextExtraProperties &
|
KcContextExtension &
|
||||||
KcContextExtraPropertiesPerPage[PageId];
|
KcContextExtensionPerPage[PageId];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
/** Take theses type definition with a grain of salt.
|
/** Take theses type definition with a grain of salt.
|
@ -7,43 +7,32 @@ import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
|
|||||||
import { exclude } from "tsafe/exclude";
|
import { exclude } from "tsafe/exclude";
|
||||||
|
|
||||||
export function createGetKcContextMock<
|
export function createGetKcContextMock<
|
||||||
KcContextExtraProperties extends { properties?: Record<string, string | undefined> },
|
KcContextExtension extends { properties?: Record<string, string | undefined> },
|
||||||
KcContextExtraPropertiesPerPage extends Record<
|
KcContextExtensionPerPage extends Record<`${string}.ftl`, Record<string, unknown>>
|
||||||
`${string}.ftl`,
|
|
||||||
Record<string, unknown>
|
|
||||||
>
|
|
||||||
>(params: {
|
>(params: {
|
||||||
kcContextExtraProperties: KcContextExtraProperties;
|
kcContextExtension: KcContextExtension;
|
||||||
kcContextExtraPropertiesPerPage: KcContextExtraPropertiesPerPage;
|
kcContextExtensionPerPage: KcContextExtensionPerPage;
|
||||||
overrides?: DeepPartial<KcContextExtraProperties & KcContextBase.Common>;
|
overrides?: DeepPartial<KcContextExtension & KcContextBase.Common>;
|
||||||
overridesPerPage?: {
|
overridesPerPage?: {
|
||||||
[PageId in
|
[PageId in LoginThemePageId | keyof KcContextExtensionPerPage]?: DeepPartial<
|
||||||
| LoginThemePageId
|
|
||||||
| keyof KcContextExtraPropertiesPerPage]?: DeepPartial<
|
|
||||||
Extract<
|
Extract<
|
||||||
ExtendKcContext<
|
ExtendKcContext<KcContextExtension, KcContextExtensionPerPage>,
|
||||||
KcContextExtraProperties,
|
|
||||||
KcContextExtraPropertiesPerPage
|
|
||||||
>,
|
|
||||||
{ pageId: PageId }
|
{ pageId: PageId }
|
||||||
>
|
>
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
kcContextExtraProperties,
|
kcContextExtension,
|
||||||
kcContextExtraPropertiesPerPage,
|
kcContextExtensionPerPage,
|
||||||
overrides: overrides_global,
|
overrides: overrides_global,
|
||||||
overridesPerPage: overridesPerPage_global
|
overridesPerPage: overridesPerPage_global
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
type KcContext = ExtendKcContext<
|
type KcContext = ExtendKcContext<KcContextExtension, KcContextExtensionPerPage>;
|
||||||
KcContextExtraProperties,
|
|
||||||
KcContextExtraPropertiesPerPage
|
|
||||||
>;
|
|
||||||
|
|
||||||
function getKcContextMock<
|
function getKcContextMock<
|
||||||
PageId extends LoginThemePageId | keyof KcContextExtraPropertiesPerPage
|
PageId extends LoginThemePageId | keyof KcContextExtensionPerPage
|
||||||
>(params: {
|
>(params: {
|
||||||
pageId: PageId;
|
pageId: PageId;
|
||||||
overrides?: DeepPartial<Extract<KcContext, { pageId: PageId }>>;
|
overrides?: DeepPartial<Extract<KcContext, { pageId: PageId }>>;
|
||||||
@ -58,8 +47,8 @@ export function createGetKcContextMock<
|
|||||||
);
|
);
|
||||||
|
|
||||||
[
|
[
|
||||||
kcContextExtraProperties,
|
kcContextExtension,
|
||||||
kcContextExtraPropertiesPerPage[pageId],
|
kcContextExtensionPerPage[pageId],
|
||||||
overrides_global,
|
overrides_global,
|
||||||
overridesPerPage_global?.[pageId],
|
overridesPerPage_global?.[pageId],
|
||||||
overrides
|
overrides
|
@ -2,6 +2,7 @@ export type {
|
|||||||
ExtendKcContext,
|
ExtendKcContext,
|
||||||
KcContext,
|
KcContext,
|
||||||
Attribute,
|
Attribute,
|
||||||
PasswordPolicies
|
PasswordPolicies,
|
||||||
|
Validators
|
||||||
} from "./KcContext";
|
} from "./KcContext";
|
||||||
export { createGetKcContextMock } from "./getKcContextMock";
|
export { createGetKcContextMock } from "./getKcContextMock";
|
@ -1,4 +1,4 @@
|
|||||||
import "minimal-polyfills/Object.fromEntries";
|
import "keycloakify/tools/Object.fromEntries";
|
||||||
import type { KcContext, Attribute } from "./KcContext";
|
import type { KcContext, Attribute } from "./KcContext";
|
||||||
import {
|
import {
|
||||||
resources_common,
|
resources_common,
|
||||||
@ -87,12 +87,9 @@ export const kcContextCommonMock: KcContext.Common = {
|
|||||||
loginAction: "#",
|
loginAction: "#",
|
||||||
resourcesPath,
|
resourcesPath,
|
||||||
resourcesCommonPath: `${resourcesPath}/${resources_common}`,
|
resourcesCommonPath: `${resourcesPath}/${resources_common}`,
|
||||||
loginRestartFlowUrl:
|
loginRestartFlowUrl: "#",
|
||||||
"/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg",
|
loginUrl: "#",
|
||||||
loginUrl:
|
ssoLoginInOtherTabsUrl: "#"
|
||||||
"/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg",
|
|
||||||
ssoLoginInOtherTabsUrl:
|
|
||||||
"/auth/realms/myrealm/login-actions/switch?client_id=account&tab_id=HoAx28ja4xg"
|
|
||||||
},
|
},
|
||||||
realm: {
|
realm: {
|
||||||
name: "myrealm",
|
name: "myrealm",
|
||||||
@ -160,13 +157,10 @@ export const kcContextCommonMock: KcContext.Common = {
|
|||||||
|
|
||||||
const loginUrl = {
|
const loginUrl = {
|
||||||
...kcContextCommonMock.url,
|
...kcContextCommonMock.url,
|
||||||
loginResetCredentialsUrl:
|
loginResetCredentialsUrl: "#",
|
||||||
"/auth/realms/myrealm/login-actions/reset-credentials?client_id=account&tab_id=HoAx28ja4xg",
|
registrationUrl: "#",
|
||||||
registrationUrl:
|
oauth2DeviceVerificationAction: "#",
|
||||||
"/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg",
|
oauthAction: "#"
|
||||||
oauth2DeviceVerificationAction: "/auth/realms/myrealm/device",
|
|
||||||
oauthAction:
|
|
||||||
"/auth/realms/myrealm/login-actions/consent?client_id=account&tab_id=HoAx28ja4xg"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const kcContextMocks = [
|
export const kcContextMocks = [
|
||||||
@ -194,8 +188,7 @@ export const kcContextMocks = [
|
|||||||
...kcContextCommonMock,
|
...kcContextCommonMock,
|
||||||
url: {
|
url: {
|
||||||
...loginUrl,
|
...loginUrl,
|
||||||
registrationAction:
|
registrationAction: "#"
|
||||||
"http://localhost:8080/auth/realms/myrealm/login-actions/registration?session_code=gwZdUeO7pbYpFTRxiIxRg_QtzMbtFTKrNu6XW_f8asM&execution=12146ce0-b139-4bbd-b25b-0eccfee6577e&client_id=account&tab_id=uS8lYfebLa0"
|
|
||||||
},
|
},
|
||||||
isAppInitiatedAction: false,
|
isAppInitiatedAction: false,
|
||||||
passwordRequired: true,
|
passwordRequired: true,
|
||||||
@ -445,7 +438,7 @@ export const kcContextMocks = [
|
|||||||
...kcContextCommonMock,
|
...kcContextCommonMock,
|
||||||
pageId: "saml-post-form.ftl",
|
pageId: "saml-post-form.ftl",
|
||||||
samlPost: {
|
samlPost: {
|
||||||
url: ""
|
url: "#"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
id<KcContext.LoginPageExpired>({
|
id<KcContext.LoginPageExpired>({
|
@ -1,13 +1,13 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { assert } from "keycloakify/tools/assert";
|
import { assert } from "keycloakify/tools/assert";
|
||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
import { type TemplateProps } from "keycloakify/login/TemplateProps";
|
import type { TemplateProps } from "keycloakify/login/TemplateProps";
|
||||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
|
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
|
||||||
import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
|
import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
|
||||||
import { useSetClassName } from "keycloakify/tools/useSetClassName";
|
import { useSetClassName } from "keycloakify/tools/useSetClassName";
|
||||||
import type { KcContext } from "./kcContext";
|
|
||||||
import type { I18n } from "./i18n";
|
import type { I18n } from "./i18n";
|
||||||
|
import type { KcContext } from "./KcContext";
|
||||||
|
|
||||||
export default function Template(props: TemplateProps<KcContext, I18n>) {
|
export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||||
const {
|
const {
|
||||||
@ -27,7 +27,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
|||||||
children
|
children
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { getClassName } = useGetClassName({ doUseDefaultCss, classes });
|
const { kcClsx } = getKcClsx({ doUseDefaultCss, classes });
|
||||||
|
|
||||||
const { msg, msgStr, getChangeLocalUrl, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
|
const { msg, msgStr, getChangeLocalUrl, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
|
||||||
|
|
||||||
@ -39,12 +39,12 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
|||||||
|
|
||||||
useSetClassName({
|
useSetClassName({
|
||||||
qualifiedName: "html",
|
qualifiedName: "html",
|
||||||
className: getClassName("kcHtmlClass")
|
className: kcClsx("kcHtmlClass")
|
||||||
});
|
});
|
||||||
|
|
||||||
useSetClassName({
|
useSetClassName({
|
||||||
qualifiedName: "body",
|
qualifiedName: "body",
|
||||||
className: bodyClassName ?? getClassName("kcBodyClass")
|
className: bodyClassName ?? kcClsx("kcBodyClass")
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -116,24 +116,23 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={getClassName("kcLoginClass")}>
|
<div className={kcClsx("kcLoginClass")}>
|
||||||
<div id="kc-header" className={getClassName("kcHeaderClass")}>
|
<div id="kc-header" className={kcClsx("kcHeaderClass")}>
|
||||||
<div id="kc-header-wrapper" className={getClassName("kcHeaderWrapperClass")}>
|
<div id="kc-header-wrapper" className={kcClsx("kcHeaderWrapperClass")}>
|
||||||
{msg("loginTitleHtml", realm.displayNameHtml)}
|
{msg("loginTitleHtml", realm.displayNameHtml)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={getClassName("kcFormCardClass")}>
|
<div className={kcClsx("kcFormCardClass")}>
|
||||||
<header className={getClassName("kcFormHeaderClass")}>
|
<header className={kcClsx("kcFormHeaderClass")}>
|
||||||
{realm.internationalizationEnabled && (assert(locale !== undefined), locale.supported.length > 1) && (
|
{realm.internationalizationEnabled && (assert(locale !== undefined), locale.supported.length > 1) && (
|
||||||
<div className={getClassName("kcLocaleMainClass")} id="kc-locale">
|
<div className={kcClsx("kcLocaleMainClass")} id="kc-locale">
|
||||||
<div id="kc-locale-wrapper" className={getClassName("kcLocaleWrapperClass")}>
|
<div id="kc-locale-wrapper" className={kcClsx("kcLocaleWrapperClass")}>
|
||||||
<div id="kc-locale-dropdown" className={clsx("menu-button-links", getClassName("kcLocaleDropDownClass"))}>
|
<div id="kc-locale-dropdown" className={clsx("menu-button-links", kcClsx("kcLocaleDropDownClass"))}>
|
||||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
|
||||||
<button
|
<button
|
||||||
tabIndex={1}
|
tabIndex={1}
|
||||||
id="kc-current-locale-link"
|
id="kc-current-locale-link"
|
||||||
aria-label={msgStr("languages" as any)}
|
aria-label={msgStr("languages")}
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-controls="language-switch1"
|
aria-controls="language-switch1"
|
||||||
@ -146,14 +145,14 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
|||||||
aria-labelledby="kc-current-locale-link"
|
aria-labelledby="kc-current-locale-link"
|
||||||
aria-activedescendant=""
|
aria-activedescendant=""
|
||||||
id="language-switch1"
|
id="language-switch1"
|
||||||
className={getClassName("kcLocaleListClass")}
|
className={kcClsx("kcLocaleListClass")}
|
||||||
>
|
>
|
||||||
{locale.supported.map(({ languageTag }, i) => (
|
{locale.supported.map(({ languageTag }, i) => (
|
||||||
<li key={languageTag} className={getClassName("kcLocaleListItemClass")} role="none">
|
<li key={languageTag} className={kcClsx("kcLocaleListItemClass")} role="none">
|
||||||
<a
|
<a
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
id={`language-${i + 1}`}
|
id={`language-${i + 1}`}
|
||||||
className={getClassName("kcLocaleItemClass")}
|
className={kcClsx("kcLocaleItemClass")}
|
||||||
href={getChangeLocalUrl(languageTag)}
|
href={getChangeLocalUrl(languageTag)}
|
||||||
>
|
>
|
||||||
{labelBySupportedLanguageTag[languageTag]}
|
{labelBySupportedLanguageTag[languageTag]}
|
||||||
@ -167,8 +166,8 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
|||||||
)}
|
)}
|
||||||
{!(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? (
|
{!(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? (
|
||||||
displayRequiredFields ? (
|
displayRequiredFields ? (
|
||||||
<div className={getClassName("kcContentWrapperClass")}>
|
<div className={kcClsx("kcContentWrapperClass")}>
|
||||||
<div className={clsx(getClassName("kcLabelWrapperClass"), "subtitle")}>
|
<div className={clsx(kcClsx("kcLabelWrapperClass"), "subtitle")}>
|
||||||
<span className="subtitle">
|
<span className="subtitle">
|
||||||
<span className="required">*</span>
|
<span className="required">*</span>
|
||||||
{msg("requiredFields")}
|
{msg("requiredFields")}
|
||||||
@ -182,19 +181,19 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
|||||||
<h1 id="kc-page-title">{headerNode}</h1>
|
<h1 id="kc-page-title">{headerNode}</h1>
|
||||||
)
|
)
|
||||||
) : displayRequiredFields ? (
|
) : displayRequiredFields ? (
|
||||||
<div className={getClassName("kcContentWrapperClass")}>
|
<div className={kcClsx("kcContentWrapperClass")}>
|
||||||
<div className={clsx(getClassName("kcLabelWrapperClass"), "subtitle")}>
|
<div className={clsx(kcClsx("kcLabelWrapperClass"), "subtitle")}>
|
||||||
<span className="subtitle">
|
<span className="subtitle">
|
||||||
<span className="required">*</span> {msg("requiredFields")}
|
<span className="required">*</span> {msg("requiredFields")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-10">
|
<div className="col-md-10">
|
||||||
{showUsernameNode}
|
{showUsernameNode}
|
||||||
<div id="kc-username" className={getClassName("kcFormGroupClass")}>
|
<div id="kc-username" className={kcClsx("kcFormGroupClass")}>
|
||||||
<label id="kc-attempted-username">{auth.attemptedUsername}</label>
|
<label id="kc-attempted-username">{auth.attemptedUsername}</label>
|
||||||
<a id="reset-login" href={url.loginRestartFlowUrl} aria-label={msgStr("restartLoginTooltip")}>
|
<a id="reset-login" href={url.loginRestartFlowUrl} aria-label={msgStr("restartLoginTooltip")}>
|
||||||
<div className="kc-login-tooltip">
|
<div className="kc-login-tooltip">
|
||||||
<i className={getClassName("kcResetFlowIcon")}></i>
|
<i className={kcClsx("kcResetFlowIcon")}></i>
|
||||||
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
|
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@ -204,11 +203,11 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{showUsernameNode}
|
{showUsernameNode}
|
||||||
<div id="kc-username" className={getClassName("kcFormGroupClass")}>
|
<div id="kc-username" className={kcClsx("kcFormGroupClass")}>
|
||||||
<label id="kc-attempted-username">{auth.attemptedUsername}</label>
|
<label id="kc-attempted-username">{auth.attemptedUsername}</label>
|
||||||
<a id="reset-login" href={url.loginRestartFlowUrl} aria-label={msgStr("restartLoginTooltip")}>
|
<a id="reset-login" href={url.loginRestartFlowUrl} aria-label={msgStr("restartLoginTooltip")}>
|
||||||
<div className="kc-login-tooltip">
|
<div className="kc-login-tooltip">
|
||||||
<i className={getClassName("kcResetFlowIcon")}></i>
|
<i className={kcClsx("kcResetFlowIcon")}></i>
|
||||||
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
|
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@ -223,18 +222,18 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
|||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
`alert-${message.type}`,
|
`alert-${message.type}`,
|
||||||
getClassName("kcAlertClass"),
|
kcClsx("kcAlertClass"),
|
||||||
`pf-m-${message?.type === "error" ? "danger" : message.type}`
|
`pf-m-${message?.type === "error" ? "danger" : message.type}`
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="pf-c-alert__icon">
|
<div className="pf-c-alert__icon">
|
||||||
{message.type === "success" && <span className={getClassName("kcFeedbackSuccessIcon")}></span>}
|
{message.type === "success" && <span className={kcClsx("kcFeedbackSuccessIcon")}></span>}
|
||||||
{message.type === "warning" && <span className={getClassName("kcFeedbackWarningIcon")}></span>}
|
{message.type === "warning" && <span className={kcClsx("kcFeedbackWarningIcon")}></span>}
|
||||||
{message.type === "error" && <span className={getClassName("kcFeedbackErrorIcon")}></span>}
|
{message.type === "error" && <span className={kcClsx("kcFeedbackErrorIcon")}></span>}
|
||||||
{message.type === "info" && <span className={getClassName("kcFeedbackInfoIcon")}></span>}
|
{message.type === "info" && <span className={kcClsx("kcFeedbackInfoIcon")}></span>}
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={getClassName("kcAlertTitleClass")}
|
className={kcClsx("kcAlertTitleClass")}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: message.summary
|
__html: message.summary
|
||||||
}}
|
}}
|
||||||
@ -244,10 +243,9 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
|||||||
{children}
|
{children}
|
||||||
{auth !== undefined && auth.showTryAnotherWayLink && (
|
{auth !== undefined && auth.showTryAnotherWayLink && (
|
||||||
<form id="kc-select-try-another-way-form" action={url.loginAction} method="post">
|
<form id="kc-select-try-another-way-form" action={url.loginAction} method="post">
|
||||||
<div className={getClassName("kcFormGroupClass")}>
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
<div className={getClassName("kcFormGroupClass")}>
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
<input type="hidden" name="tryAnotherWay" value="on" />
|
<input type="hidden" name="tryAnotherWay" value="on" />
|
||||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
id="try-another-way"
|
id="try-another-way"
|
||||||
@ -264,8 +262,8 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
|||||||
)}
|
)}
|
||||||
{socialProvidersNode}
|
{socialProvidersNode}
|
||||||
{displayInfo && (
|
{displayInfo && (
|
||||||
<div id="kc-info" className={getClassName("kcSignUpClass")}>
|
<div id="kc-info" className={kcClsx("kcSignUpClass")}>
|
||||||
<div id="kc-info-wrapper" className={getClassName("kcInfoAreaWrapperClass")}>
|
<div id="kc-info-wrapper" className={kcClsx("kcInfoAreaWrapperClass")}>
|
||||||
{infoNode}
|
{infoNode}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import type { KcContext } from "./kcContext";
|
|
||||||
import type { I18n } from "./i18n";
|
|
||||||
|
|
||||||
export type TemplateProps<
|
export type TemplateProps<KcContext, I18n> = {
|
||||||
KcContext extends KcContext.Common,
|
|
||||||
I18nExtended extends I18n
|
|
||||||
> = {
|
|
||||||
kcContext: KcContext;
|
kcContext: KcContext;
|
||||||
i18n: I18nExtended;
|
i18n: I18n;
|
||||||
doUseDefaultCss: boolean;
|
doUseDefaultCss: boolean;
|
||||||
classes?: Partial<Record<ClassKey, string>>;
|
classes?: Partial<Record<ClassKey, string>>;
|
||||||
|
children: ReactNode;
|
||||||
|
|
||||||
displayInfo?: boolean;
|
displayInfo?: boolean;
|
||||||
displayMessage?: boolean;
|
displayMessage?: boolean;
|
||||||
@ -21,8 +17,6 @@ export type TemplateProps<
|
|||||||
infoNode?: ReactNode;
|
infoNode?: ReactNode;
|
||||||
documentTitle?: string;
|
documentTitle?: string;
|
||||||
bodyClassName?: string;
|
bodyClassName?: string;
|
||||||
|
|
||||||
children: ReactNode;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ClassKey =
|
export type ClassKey =
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useReducer, Fragment } from "react";
|
import { useEffect, useReducer, Fragment } from "react";
|
||||||
import type { ClassKey } from "keycloakify/login/TemplateProps";
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
import {
|
import {
|
||||||
useUserProfileForm,
|
useUserProfileForm,
|
||||||
getButtonToDisplayForMultivaluedAttributeField,
|
getButtonToDisplayForMultivaluedAttributeField,
|
||||||
@ -7,15 +8,15 @@ import {
|
|||||||
type FormAction,
|
type FormAction,
|
||||||
type FormFieldError
|
type FormFieldError
|
||||||
} from "keycloakify/login/lib/useUserProfileForm";
|
} from "keycloakify/login/lib/useUserProfileForm";
|
||||||
import type { Attribute } from "keycloakify/login/kcContext/KcContext";
|
import type { Attribute } from "keycloakify/login/KcContext";
|
||||||
import { assert } from "tsafe/assert";
|
|
||||||
import type { I18n } from "./i18n";
|
import type { I18n } from "./i18n";
|
||||||
|
|
||||||
export type UserProfileFormFieldsProps = {
|
export type UserProfileFormFieldsProps = {
|
||||||
kcContext: KcContextLike;
|
kcContext: KcContextLike;
|
||||||
i18n: I18n;
|
i18n: I18n;
|
||||||
getClassName: (classKey: ClassKey) => string;
|
kcClsx: KcClsx;
|
||||||
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
|
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
|
||||||
|
doMakeUserConfirmPassword: boolean;
|
||||||
BeforeField?: (props: BeforeAfterFieldProps) => JSX.Element | null;
|
BeforeField?: (props: BeforeAfterFieldProps) => JSX.Element | null;
|
||||||
AfterField?: (props: BeforeAfterFieldProps) => JSX.Element | null;
|
AfterField?: (props: BeforeAfterFieldProps) => JSX.Element | null;
|
||||||
};
|
};
|
||||||
@ -24,15 +25,13 @@ type BeforeAfterFieldProps = {
|
|||||||
attribute: Attribute;
|
attribute: Attribute;
|
||||||
dispatchFormAction: React.Dispatch<FormAction>;
|
dispatchFormAction: React.Dispatch<FormAction>;
|
||||||
displayableErrors: FormFieldError[];
|
displayableErrors: FormFieldError[];
|
||||||
i18n: I18n;
|
|
||||||
valueOrValues: string | string[];
|
valueOrValues: string | string[];
|
||||||
|
kcClsx: KcClsx;
|
||||||
|
i18n: I18n;
|
||||||
};
|
};
|
||||||
|
|
||||||
// NOTE: Enabled by default but it's a UX best practice to set it to false.
|
|
||||||
const doMakeUserConfirmPassword = true;
|
|
||||||
|
|
||||||
export default function UserProfileFormFields(props: UserProfileFormFieldsProps) {
|
export default function UserProfileFormFields(props: UserProfileFormFieldsProps) {
|
||||||
const { kcContext, onIsFormSubmittableValueChange, i18n, getClassName, BeforeField, AfterField } = props;
|
const { kcContext, i18n, kcClsx, onIsFormSubmittableValueChange, doMakeUserConfirmPassword, BeforeField, AfterField } = props;
|
||||||
|
|
||||||
const { advancedMsg } = i18n;
|
const { advancedMsg } = i18n;
|
||||||
|
|
||||||
@ -56,32 +55,33 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps)
|
|||||||
{formFieldStates.map(({ attribute, displayableErrors, valueOrValues }) => {
|
{formFieldStates.map(({ attribute, displayableErrors, valueOrValues }) => {
|
||||||
return (
|
return (
|
||||||
<Fragment key={attribute.name}>
|
<Fragment key={attribute.name}>
|
||||||
<GroupLabel attribute={attribute} getClassName={getClassName} i18n={i18n} groupNameRef={groupNameRef} />
|
<GroupLabel attribute={attribute} groupNameRef={groupNameRef} i18n={i18n} kcClsx={kcClsx} />
|
||||||
{BeforeField !== undefined && (
|
{BeforeField !== undefined && (
|
||||||
<BeforeField
|
<BeforeField
|
||||||
attribute={attribute}
|
attribute={attribute}
|
||||||
dispatchFormAction={dispatchFormAction}
|
dispatchFormAction={dispatchFormAction}
|
||||||
displayableErrors={displayableErrors}
|
displayableErrors={displayableErrors}
|
||||||
i18n={i18n}
|
|
||||||
valueOrValues={valueOrValues}
|
valueOrValues={valueOrValues}
|
||||||
|
kcClsx={kcClsx}
|
||||||
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={getClassName("kcFormGroupClass")}
|
className={kcClsx("kcFormGroupClass")}
|
||||||
style={{
|
style={{
|
||||||
display: attribute.name === "password-confirm" && !doMakeUserConfirmPassword ? "none" : undefined
|
display: attribute.name === "password-confirm" && !doMakeUserConfirmPassword ? "none" : undefined
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={getClassName("kcLabelWrapperClass")}>
|
<div className={kcClsx("kcLabelWrapperClass")}>
|
||||||
<label htmlFor={attribute.name} className={getClassName("kcLabelClass")}>
|
<label htmlFor={attribute.name} className={kcClsx("kcLabelClass")}>
|
||||||
{advancedMsg(attribute.displayName ?? "")}
|
{advancedMsg(attribute.displayName ?? "")}
|
||||||
</label>
|
</label>
|
||||||
{attribute.required && <>*</>}
|
{attribute.required && <>*</>}
|
||||||
</div>
|
</div>
|
||||||
<div className={getClassName("kcInputWrapperClass")}>
|
<div className={kcClsx("kcInputWrapperClass")}>
|
||||||
{attribute.annotations.inputHelperTextBefore !== undefined && (
|
{attribute.annotations.inputHelperTextBefore !== undefined && (
|
||||||
<div
|
<div
|
||||||
className={getClassName("kcInputHelperTextBeforeClass")}
|
className={kcClsx("kcInputHelperTextBeforeClass")}
|
||||||
id={`form-help-text-before-${attribute.name}`}
|
id={`form-help-text-before-${attribute.name}`}
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
>
|
>
|
||||||
@ -92,19 +92,14 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps)
|
|||||||
attribute={attribute}
|
attribute={attribute}
|
||||||
valueOrValues={valueOrValues}
|
valueOrValues={valueOrValues}
|
||||||
displayableErrors={displayableErrors}
|
displayableErrors={displayableErrors}
|
||||||
formValidationDispatch={dispatchFormAction}
|
dispatchFormAction={dispatchFormAction}
|
||||||
getClassName={getClassName}
|
kcClsx={kcClsx}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
<FieldErrors
|
<FieldErrors attribute={attribute} displayableErrors={displayableErrors} kcClsx={kcClsx} fieldIndex={undefined} />
|
||||||
attribute={attribute}
|
|
||||||
getClassName={getClassName}
|
|
||||||
displayableErrors={displayableErrors}
|
|
||||||
fieldIndex={undefined}
|
|
||||||
/>
|
|
||||||
{attribute.annotations.inputHelperTextAfter !== undefined && (
|
{attribute.annotations.inputHelperTextAfter !== undefined && (
|
||||||
<div
|
<div
|
||||||
className={getClassName("kcInputHelperTextAfterClass")}
|
className={kcClsx("kcInputHelperTextAfterClass")}
|
||||||
id={`form-help-text-after-${attribute.name}`}
|
id={`form-help-text-after-${attribute.name}`}
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
>
|
>
|
||||||
@ -117,8 +112,9 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps)
|
|||||||
attribute={attribute}
|
attribute={attribute}
|
||||||
dispatchFormAction={dispatchFormAction}
|
dispatchFormAction={dispatchFormAction}
|
||||||
displayableErrors={displayableErrors}
|
displayableErrors={displayableErrors}
|
||||||
i18n={i18n}
|
|
||||||
valueOrValues={valueOrValues}
|
valueOrValues={valueOrValues}
|
||||||
|
kcClsx={kcClsx}
|
||||||
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* NOTE: Downloading of html5DataAnnotations scripts is done in the useUserProfileForm hook */}
|
{/* NOTE: Downloading of html5DataAnnotations scripts is done in the useUserProfileForm hook */}
|
||||||
@ -133,13 +129,13 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps)
|
|||||||
|
|
||||||
function GroupLabel(props: {
|
function GroupLabel(props: {
|
||||||
attribute: Attribute;
|
attribute: Attribute;
|
||||||
getClassName: UserProfileFormFieldsProps["getClassName"];
|
|
||||||
i18n: I18n;
|
|
||||||
groupNameRef: {
|
groupNameRef: {
|
||||||
current: string;
|
current: string;
|
||||||
};
|
};
|
||||||
|
i18n: I18n;
|
||||||
|
kcClsx: KcClsx;
|
||||||
}) {
|
}) {
|
||||||
const { attribute, getClassName, i18n, groupNameRef } = props;
|
const { attribute, groupNameRef, i18n, kcClsx } = props;
|
||||||
|
|
||||||
const { advancedMsg } = i18n;
|
const { advancedMsg } = i18n;
|
||||||
|
|
||||||
@ -151,7 +147,7 @@ function GroupLabel(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={getClassName("kcFormGroupClass")}
|
className={kcClsx("kcFormGroupClass")}
|
||||||
{...Object.fromEntries(Object.entries(attribute.group.html5DataAnnotations).map(([key, value]) => [`data-${key}`, value]))}
|
{...Object.fromEntries(Object.entries(attribute.group.html5DataAnnotations).map(([key, value]) => [`data-${key}`, value]))}
|
||||||
>
|
>
|
||||||
{(() => {
|
{(() => {
|
||||||
@ -159,8 +155,8 @@ function GroupLabel(props: {
|
|||||||
const groupHeaderText = groupDisplayHeader !== "" ? advancedMsg(groupDisplayHeader) : attribute.group.name;
|
const groupHeaderText = groupDisplayHeader !== "" ? advancedMsg(groupDisplayHeader) : attribute.group.name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={getClassName("kcContentWrapperClass")}>
|
<div className={kcClsx("kcContentWrapperClass")}>
|
||||||
<label id={`header-${attribute.group.name}`} className={getClassName("kcFormGroupHeader")}>
|
<label id={`header-${attribute.group.name}`} className={kcClsx("kcFormGroupHeader")}>
|
||||||
{groupHeaderText}
|
{groupHeaderText}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -173,8 +169,8 @@ function GroupLabel(props: {
|
|||||||
const groupDescriptionText = advancedMsg(groupDisplayDescription);
|
const groupDescriptionText = advancedMsg(groupDisplayDescription);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={getClassName("kcLabelWrapperClass")}>
|
<div className={kcClsx("kcLabelWrapperClass")}>
|
||||||
<label id={`description-${attribute.group.name}`} className={getClassName("kcLabelClass")}>
|
<label id={`description-${attribute.group.name}`} className={kcClsx("kcLabelClass")}>
|
||||||
{groupDescriptionText}
|
{groupDescriptionText}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -191,13 +187,8 @@ function GroupLabel(props: {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldErrors(props: {
|
function FieldErrors(props: { attribute: Attribute; displayableErrors: FormFieldError[]; fieldIndex: number | undefined; kcClsx: KcClsx }) {
|
||||||
attribute: Attribute;
|
const { attribute, fieldIndex, kcClsx } = props;
|
||||||
getClassName: UserProfileFormFieldsProps["getClassName"];
|
|
||||||
displayableErrors: FormFieldError[];
|
|
||||||
fieldIndex: number | undefined;
|
|
||||||
}) {
|
|
||||||
const { attribute, getClassName, fieldIndex } = props;
|
|
||||||
|
|
||||||
const displayableErrors = props.displayableErrors.filter(error => error.fieldIndex === fieldIndex);
|
const displayableErrors = props.displayableErrors.filter(error => error.fieldIndex === fieldIndex);
|
||||||
|
|
||||||
@ -208,7 +199,7 @@ function FieldErrors(props: {
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
id={`input-error-${attribute.name}${fieldIndex === undefined ? "" : `-${fieldIndex}`}`}
|
id={`input-error-${attribute.name}${fieldIndex === undefined ? "" : `-${fieldIndex}`}`}
|
||||||
className={getClassName("kcInputErrorMessageClass")}
|
className={kcClsx("kcInputErrorMessageClass")}
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
>
|
>
|
||||||
{displayableErrors
|
{displayableErrors
|
||||||
@ -227,9 +218,9 @@ type InputFiledByTypeProps = {
|
|||||||
attribute: Attribute;
|
attribute: Attribute;
|
||||||
valueOrValues: string | string[];
|
valueOrValues: string | string[];
|
||||||
displayableErrors: FormFieldError[];
|
displayableErrors: FormFieldError[];
|
||||||
formValidationDispatch: React.Dispatch<FormAction>;
|
dispatchFormAction: React.Dispatch<FormAction>;
|
||||||
getClassName: UserProfileFormFieldsProps["getClassName"];
|
|
||||||
i18n: I18n;
|
i18n: I18n;
|
||||||
|
kcClsx: KcClsx;
|
||||||
};
|
};
|
||||||
|
|
||||||
function InputFiledByType(props: InputFiledByTypeProps) {
|
function InputFiledByType(props: InputFiledByTypeProps) {
|
||||||
@ -259,7 +250,7 @@ function InputFiledByType(props: InputFiledByTypeProps) {
|
|||||||
|
|
||||||
if (attribute.name === "password" || attribute.name === "password-confirm") {
|
if (attribute.name === "password" || attribute.name === "password-confirm") {
|
||||||
return (
|
return (
|
||||||
<PasswordWrapper getClassName={props.getClassName} i18n={props.i18n} passwordInputId={attribute.name}>
|
<PasswordWrapper kcClsx={props.kcClsx} i18n={props.i18n} passwordInputId={attribute.name}>
|
||||||
{inputNode}
|
{inputNode}
|
||||||
</PasswordWrapper>
|
</PasswordWrapper>
|
||||||
);
|
);
|
||||||
@ -270,8 +261,8 @@ function InputFiledByType(props: InputFiledByTypeProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function PasswordWrapper(props: { getClassName: (classKey: ClassKey) => string; i18n: I18n; passwordInputId: string; children: JSX.Element }) {
|
function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: string; children: JSX.Element }) {
|
||||||
const { getClassName, i18n, passwordInputId, children } = props;
|
const { kcClsx, i18n, passwordInputId, children } = props;
|
||||||
|
|
||||||
const { msgStr } = i18n;
|
const { msgStr } = i18n;
|
||||||
|
|
||||||
@ -286,26 +277,23 @@ function PasswordWrapper(props: { getClassName: (classKey: ClassKey) => string;
|
|||||||
}, [isPasswordRevealed]);
|
}, [isPasswordRevealed]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={getClassName("kcInputGroup")}>
|
<div className={kcClsx("kcInputGroup")}>
|
||||||
{children}
|
{children}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={getClassName("kcFormPasswordVisibilityButtonClass")}
|
className={kcClsx("kcFormPasswordVisibilityButtonClass")}
|
||||||
aria-label={msgStr(isPasswordRevealed ? "hidePassword" : "showPassword")}
|
aria-label={msgStr(isPasswordRevealed ? "hidePassword" : "showPassword")}
|
||||||
aria-controls={passwordInputId}
|
aria-controls={passwordInputId}
|
||||||
onClick={toggleIsPasswordRevealed}
|
onClick={toggleIsPasswordRevealed}
|
||||||
>
|
>
|
||||||
<i
|
<i className={kcClsx(isPasswordRevealed ? "kcFormPasswordVisibilityIconHide" : "kcFormPasswordVisibilityIconShow")} aria-hidden />
|
||||||
className={getClassName(isPasswordRevealed ? "kcFormPasswordVisibilityIconHide" : "kcFormPasswordVisibilityIconShow")}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InputTag(props: InputFiledByTypeProps & { fieldIndex: number | undefined }) {
|
function InputTag(props: InputFiledByTypeProps & { fieldIndex: number | undefined }) {
|
||||||
const { attribute, fieldIndex, getClassName, formValidationDispatch, valueOrValues, i18n, displayableErrors } = props;
|
const { attribute, fieldIndex, kcClsx, dispatchFormAction, valueOrValues, i18n, displayableErrors } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -331,7 +319,7 @@ function InputTag(props: InputFiledByTypeProps & { fieldIndex: number | undefine
|
|||||||
|
|
||||||
return valueOrValues;
|
return valueOrValues;
|
||||||
})()}
|
})()}
|
||||||
className={getClassName("kcInputClass")}
|
className={kcClsx("kcInputClass")}
|
||||||
aria-invalid={displayableErrors.find(error => error.fieldIndex === fieldIndex) !== undefined}
|
aria-invalid={displayableErrors.find(error => error.fieldIndex === fieldIndex) !== undefined}
|
||||||
disabled={attribute.readOnly}
|
disabled={attribute.readOnly}
|
||||||
autoComplete={attribute.autocomplete}
|
autoComplete={attribute.autocomplete}
|
||||||
@ -349,7 +337,7 @@ function InputTag(props: InputFiledByTypeProps & { fieldIndex: number | undefine
|
|||||||
step={attribute.annotations.inputTypeStep}
|
step={attribute.annotations.inputTypeStep}
|
||||||
{...Object.fromEntries(Object.entries(attribute.html5DataAnnotations ?? {}).map(([key, value]) => [`data-${key}`, value]))}
|
{...Object.fromEntries(Object.entries(attribute.html5DataAnnotations ?? {}).map(([key, value]) => [`data-${key}`, value]))}
|
||||||
onChange={event =>
|
onChange={event =>
|
||||||
formValidationDispatch({
|
dispatchFormAction({
|
||||||
action: "update",
|
action: "update",
|
||||||
name: attribute.name,
|
name: attribute.name,
|
||||||
valueOrValues: (() => {
|
valueOrValues: (() => {
|
||||||
@ -370,7 +358,7 @@ function InputTag(props: InputFiledByTypeProps & { fieldIndex: number | undefine
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
onBlur={() =>
|
onBlur={() =>
|
||||||
props.formValidationDispatch({
|
dispatchFormAction({
|
||||||
action: "focus lost",
|
action: "focus lost",
|
||||||
name: attribute.name,
|
name: attribute.name,
|
||||||
fieldIndex: fieldIndex
|
fieldIndex: fieldIndex
|
||||||
@ -388,17 +376,12 @@ function InputTag(props: InputFiledByTypeProps & { fieldIndex: number | undefine
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FieldErrors
|
<FieldErrors attribute={attribute} kcClsx={kcClsx} displayableErrors={displayableErrors} fieldIndex={fieldIndex} />
|
||||||
attribute={attribute}
|
|
||||||
getClassName={getClassName}
|
|
||||||
displayableErrors={displayableErrors}
|
|
||||||
fieldIndex={fieldIndex}
|
|
||||||
/>
|
|
||||||
<AddRemoveButtonsMultiValuedAttribute
|
<AddRemoveButtonsMultiValuedAttribute
|
||||||
attribute={attribute}
|
attribute={attribute}
|
||||||
values={values}
|
values={values}
|
||||||
fieldIndex={fieldIndex}
|
fieldIndex={fieldIndex}
|
||||||
dispatchFormAction={formValidationDispatch}
|
dispatchFormAction={dispatchFormAction}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@ -465,7 +448,7 @@ function AddRemoveButtonsMultiValuedAttribute(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function InputTagSelects(props: InputFiledByTypeProps) {
|
function InputTagSelects(props: InputFiledByTypeProps) {
|
||||||
const { attribute, formValidationDispatch, getClassName, valueOrValues } = props;
|
const { attribute, dispatchFormAction, kcClsx, valueOrValues } = props;
|
||||||
|
|
||||||
const { advancedMsg } = props.i18n;
|
const { advancedMsg } = props.i18n;
|
||||||
|
|
||||||
@ -478,16 +461,16 @@ function InputTagSelects(props: InputFiledByTypeProps) {
|
|||||||
case "select-radiobuttons":
|
case "select-radiobuttons":
|
||||||
return {
|
return {
|
||||||
inputType: "radio",
|
inputType: "radio",
|
||||||
classDiv: getClassName("kcInputClassRadio"),
|
classDiv: kcClsx("kcInputClassRadio"),
|
||||||
classInput: getClassName("kcInputClassRadioInput"),
|
classInput: kcClsx("kcInputClassRadioInput"),
|
||||||
classLabel: getClassName("kcInputClassRadioLabel")
|
classLabel: kcClsx("kcInputClassRadioLabel")
|
||||||
};
|
};
|
||||||
case "multiselect-checkboxes":
|
case "multiselect-checkboxes":
|
||||||
return {
|
return {
|
||||||
inputType: "checkbox",
|
inputType: "checkbox",
|
||||||
classDiv: getClassName("kcInputClassCheckbox"),
|
classDiv: kcClsx("kcInputClassCheckbox"),
|
||||||
classInput: getClassName("kcInputClassCheckboxInput"),
|
classInput: kcClsx("kcInputClassCheckboxInput"),
|
||||||
classLabel: getClassName("kcInputClassCheckboxLabel")
|
classLabel: kcClsx("kcInputClassCheckboxLabel")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@ -530,7 +513,7 @@ function InputTagSelects(props: InputFiledByTypeProps) {
|
|||||||
disabled={attribute.readOnly}
|
disabled={attribute.readOnly}
|
||||||
checked={valueOrValues instanceof Array ? valueOrValues.includes(option) : valueOrValues === option}
|
checked={valueOrValues instanceof Array ? valueOrValues.includes(option) : valueOrValues === option}
|
||||||
onChange={event =>
|
onChange={event =>
|
||||||
formValidationDispatch({
|
dispatchFormAction({
|
||||||
action: "update",
|
action: "update",
|
||||||
name: attribute.name,
|
name: attribute.name,
|
||||||
valueOrValues: (() => {
|
valueOrValues: (() => {
|
||||||
@ -553,7 +536,7 @@ function InputTagSelects(props: InputFiledByTypeProps) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
onBlur={() =>
|
onBlur={() =>
|
||||||
formValidationDispatch({
|
dispatchFormAction({
|
||||||
action: "focus lost",
|
action: "focus lost",
|
||||||
name: attribute.name,
|
name: attribute.name,
|
||||||
fieldIndex: undefined
|
fieldIndex: undefined
|
||||||
@ -562,7 +545,7 @@ function InputTagSelects(props: InputFiledByTypeProps) {
|
|||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor={`${attribute.name}-${option}`}
|
htmlFor={`${attribute.name}-${option}`}
|
||||||
className={`${classLabel}${attribute.readOnly ? ` ${getClassName("kcInputClassRadioCheckboxLabelDisabled")}` : ""}`}
|
className={`${classLabel}${attribute.readOnly ? ` ${kcClsx("kcInputClassRadioCheckboxLabelDisabled")}` : ""}`}
|
||||||
>
|
>
|
||||||
{advancedMsg(option)}
|
{advancedMsg(option)}
|
||||||
</label>
|
</label>
|
||||||
@ -573,7 +556,7 @@ function InputTagSelects(props: InputFiledByTypeProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function TextareaTag(props: InputFiledByTypeProps) {
|
function TextareaTag(props: InputFiledByTypeProps) {
|
||||||
const { attribute, formValidationDispatch, getClassName, displayableErrors, valueOrValues } = props;
|
const { attribute, dispatchFormAction, kcClsx, displayableErrors, valueOrValues } = props;
|
||||||
|
|
||||||
assert(typeof valueOrValues === "string");
|
assert(typeof valueOrValues === "string");
|
||||||
|
|
||||||
@ -583,7 +566,7 @@ function TextareaTag(props: InputFiledByTypeProps) {
|
|||||||
<textarea
|
<textarea
|
||||||
id={attribute.name}
|
id={attribute.name}
|
||||||
name={attribute.name}
|
name={attribute.name}
|
||||||
className={getClassName("kcInputClass")}
|
className={kcClsx("kcInputClass")}
|
||||||
aria-invalid={displayableErrors.length !== 0}
|
aria-invalid={displayableErrors.length !== 0}
|
||||||
disabled={attribute.readOnly}
|
disabled={attribute.readOnly}
|
||||||
cols={attribute.annotations.inputTypeCols === undefined ? undefined : parseInt(`${attribute.annotations.inputTypeCols}`)}
|
cols={attribute.annotations.inputTypeCols === undefined ? undefined : parseInt(`${attribute.annotations.inputTypeCols}`)}
|
||||||
@ -591,14 +574,14 @@ function TextareaTag(props: InputFiledByTypeProps) {
|
|||||||
maxLength={attribute.annotations.inputTypeMaxlength === undefined ? undefined : parseInt(`${attribute.annotations.inputTypeMaxlength}`)}
|
maxLength={attribute.annotations.inputTypeMaxlength === undefined ? undefined : parseInt(`${attribute.annotations.inputTypeMaxlength}`)}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={event =>
|
onChange={event =>
|
||||||
formValidationDispatch({
|
dispatchFormAction({
|
||||||
action: "update",
|
action: "update",
|
||||||
name: attribute.name,
|
name: attribute.name,
|
||||||
valueOrValues: event.target.value
|
valueOrValues: event.target.value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onBlur={() =>
|
onBlur={() =>
|
||||||
formValidationDispatch({
|
dispatchFormAction({
|
||||||
action: "focus lost",
|
action: "focus lost",
|
||||||
name: attribute.name,
|
name: attribute.name,
|
||||||
fieldIndex: undefined
|
fieldIndex: undefined
|
||||||
@ -609,7 +592,7 @@ function TextareaTag(props: InputFiledByTypeProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SelectTag(props: InputFiledByTypeProps) {
|
function SelectTag(props: InputFiledByTypeProps) {
|
||||||
const { attribute, formValidationDispatch, getClassName, displayableErrors, i18n, valueOrValues } = props;
|
const { attribute, dispatchFormAction, kcClsx, displayableErrors, i18n, valueOrValues } = props;
|
||||||
|
|
||||||
const { advancedMsg } = i18n;
|
const { advancedMsg } = i18n;
|
||||||
|
|
||||||
@ -619,14 +602,14 @@ function SelectTag(props: InputFiledByTypeProps) {
|
|||||||
<select
|
<select
|
||||||
id={attribute.name}
|
id={attribute.name}
|
||||||
name={attribute.name}
|
name={attribute.name}
|
||||||
className={getClassName("kcInputClass")}
|
className={kcClsx("kcInputClass")}
|
||||||
aria-invalid={displayableErrors.length !== 0}
|
aria-invalid={displayableErrors.length !== 0}
|
||||||
disabled={attribute.readOnly}
|
disabled={attribute.readOnly}
|
||||||
multiple={isMultiple}
|
multiple={isMultiple}
|
||||||
size={attribute.annotations.inputTypeSize === undefined ? undefined : parseInt(`${attribute.annotations.inputTypeSize}`)}
|
size={attribute.annotations.inputTypeSize === undefined ? undefined : parseInt(`${attribute.annotations.inputTypeSize}`)}
|
||||||
value={valueOrValues}
|
value={valueOrValues}
|
||||||
onChange={event =>
|
onChange={event =>
|
||||||
formValidationDispatch({
|
dispatchFormAction({
|
||||||
action: "update",
|
action: "update",
|
||||||
name: attribute.name,
|
name: attribute.name,
|
||||||
valueOrValues: (() => {
|
valueOrValues: (() => {
|
||||||
@ -639,7 +622,7 @@ function SelectTag(props: InputFiledByTypeProps) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
onBlur={() =>
|
onBlur={() =>
|
||||||
formValidationDispatch({
|
dispatchFormAction({
|
||||||
action: "focus lost",
|
action: "focus lost",
|
||||||
name: attribute.name,
|
name: attribute.name,
|
||||||
fieldIndex: undefined
|
fieldIndex: undefined
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import "minimal-polyfills/Object.fromEntries";
|
import "keycloakify/tools/Object.fromEntries";
|
||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import fallbackMessages from "./baseMessages/en";
|
|
||||||
import { getMessages } from "./baseMessages";
|
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import type { KcContext } from "../kcContext/KcContext";
|
import messages_fallbackLanguage from "./baseMessages/en";
|
||||||
|
import { getMessages } from "./baseMessages";
|
||||||
|
import type { KcContext } from "../KcContext";
|
||||||
import { Reflect } from "tsafe/Reflect";
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
|
||||||
export const fallbackLanguageTag = "en";
|
export const fallbackLanguageTag = "en";
|
||||||
@ -18,7 +18,7 @@ export type KcContextLike = {
|
|||||||
|
|
||||||
assert<KcContext extends KcContextLike ? true : false>();
|
assert<KcContext extends KcContextLike ? true : false>();
|
||||||
|
|
||||||
export type MessageKey = keyof typeof fallbackMessages | keyof (typeof keycloakifyExtraMessages)[typeof fallbackLanguageTag];
|
export type MessageKey = keyof typeof messages_fallbackLanguage;
|
||||||
|
|
||||||
export type GenericI18n<MessageKey extends string> = {
|
export type GenericI18n<MessageKey extends string> = {
|
||||||
/**
|
/**
|
||||||
@ -80,214 +80,262 @@ export type GenericI18n<MessageKey extends string> = {
|
|||||||
* See advancedMsg() but instead of returning a JSX.Element it returns a string.
|
* See advancedMsg() but instead of returning a JSX.Element it returns a string.
|
||||||
*/
|
*/
|
||||||
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
|
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initially the messages are in english (fallback language).
|
||||||
|
* The translations in the current language are being fetched dynamically.
|
||||||
|
* This property is true while the translations are being fetched.
|
||||||
|
*/
|
||||||
|
isFetchingTranslations: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type I18n = GenericI18n<MessageKey>;
|
function createGetI18n<ExtraMessageKey extends string = never>(extraMessages: { [languageTag: string]: { [key in ExtraMessageKey]: string } }) {
|
||||||
|
type I18n = GenericI18n<MessageKey | ExtraMessageKey>;
|
||||||
|
|
||||||
|
type Result = { i18n: I18n; prI18n_currentLanguage: Promise<I18n> | undefined };
|
||||||
|
|
||||||
|
const cachedResultByKcContext = new WeakMap<KcContextLike, Result>();
|
||||||
|
|
||||||
|
function getI18n(params: { kcContext: KcContextLike }): Result {
|
||||||
|
const { kcContext } = params;
|
||||||
|
|
||||||
|
use_cache: {
|
||||||
|
const cachedResult = cachedResultByKcContext.get(kcContext);
|
||||||
|
|
||||||
|
if (cachedResult === undefined) {
|
||||||
|
break use_cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const partialI18n: Pick<I18n, "currentLanguageTag" | "getChangeLocalUrl" | "labelBySupportedLanguageTag"> = {
|
||||||
|
currentLanguageTag: kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag,
|
||||||
|
getChangeLocalUrl: 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`);
|
||||||
|
|
||||||
|
return targetSupportedLocale.url;
|
||||||
|
},
|
||||||
|
labelBySupportedLanguageTag: Object.fromEntries((kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label]))
|
||||||
|
};
|
||||||
|
|
||||||
|
const { createI18nTranslationFunctions } = createI18nTranslationFunctionsFactory<MessageKey, ExtraMessageKey>({
|
||||||
|
messages_fallbackLanguage,
|
||||||
|
extraMessages_fallbackLanguage: extraMessages[fallbackLanguageTag],
|
||||||
|
extraMessages: extraMessages[partialI18n.currentLanguageTag],
|
||||||
|
__localizationRealmOverridesUserProfile: kcContext.__localizationRealmOverridesUserProfile
|
||||||
|
});
|
||||||
|
|
||||||
|
const isCurrentLanguageFallbackLanguage = partialI18n.currentLanguageTag === fallbackLanguageTag;
|
||||||
|
|
||||||
|
const result: Result = {
|
||||||
|
i18n: {
|
||||||
|
...partialI18n,
|
||||||
|
...createI18nTranslationFunctions({ messages: undefined }),
|
||||||
|
isFetchingTranslations: !isCurrentLanguageFallbackLanguage
|
||||||
|
},
|
||||||
|
prI18n_currentLanguage: isCurrentLanguageFallbackLanguage
|
||||||
|
? undefined
|
||||||
|
: (async () => {
|
||||||
|
const messages = await getMessages(partialI18n.currentLanguageTag);
|
||||||
|
|
||||||
|
const i18n_currentLanguage: I18n = {
|
||||||
|
...partialI18n,
|
||||||
|
...createI18nTranslationFunctions({ messages }),
|
||||||
|
isFetchingTranslations: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// NOTE: This promise.resolve is just because without it we TypeScript
|
||||||
|
// gives a Variable 'result' is used before being assigned. error
|
||||||
|
await Promise.resolve().then(() => {
|
||||||
|
result.i18n = i18n_currentLanguage;
|
||||||
|
result.prI18n_currentLanguage = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
return i18n_currentLanguage;
|
||||||
|
})()
|
||||||
|
};
|
||||||
|
|
||||||
|
cachedResultByKcContext.set(kcContext, result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getI18n };
|
||||||
|
}
|
||||||
|
|
||||||
export function createUseI18n<ExtraMessageKey extends string = never>(extraMessages: {
|
export function createUseI18n<ExtraMessageKey extends string = never>(extraMessages: {
|
||||||
[languageTag: string]: { [key in ExtraMessageKey]: string };
|
[languageTag: string]: { [key in ExtraMessageKey]: string };
|
||||||
}) {
|
}) {
|
||||||
function useI18n(params: { kcContext: KcContextLike }): GenericI18n<MessageKey | ExtraMessageKey> | null {
|
type I18n = GenericI18n<MessageKey | ExtraMessageKey>;
|
||||||
|
|
||||||
|
const { getI18n } = createGetI18n(extraMessages);
|
||||||
|
|
||||||
|
function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n } {
|
||||||
const { kcContext } = params;
|
const { kcContext } = params;
|
||||||
|
|
||||||
const [i18n, setI18n] = useState<GenericI18n<ExtraMessageKey | MessageKey> | undefined>(undefined);
|
const { i18n, prI18n_currentLanguage } = getI18n({ kcContext });
|
||||||
|
|
||||||
const refHasStartedFetching = useRef(false);
|
const [i18n_toReturn, setI18n_toReturn] = useState<I18n>(i18n);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (refHasStartedFetching.current) {
|
let isActive = true;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
refHasStartedFetching.current = true;
|
prI18n_currentLanguage?.then(i18n => {
|
||||||
|
if (!isActive) {
|
||||||
(async () => {
|
|
||||||
const { currentLanguageTag = fallbackLanguageTag } = kcContext.locale ?? {};
|
|
||||||
|
|
||||||
setI18n({
|
|
||||||
...createI18nTranslationFunctions({
|
|
||||||
fallbackMessages: {
|
|
||||||
...fallbackMessages,
|
|
||||||
...(keycloakifyExtraMessages[fallbackLanguageTag] ?? {}),
|
|
||||||
...(extraMessages[fallbackLanguageTag] ?? {})
|
|
||||||
} as any,
|
|
||||||
messages: {
|
|
||||||
...(await getMessages(currentLanguageTag)),
|
|
||||||
...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}),
|
|
||||||
...(extraMessages[currentLanguageTag] ?? {})
|
|
||||||
} as any,
|
|
||||||
__localizationRealmOverridesUserProfile: kcContext.__localizationRealmOverridesUserProfile
|
|
||||||
}),
|
|
||||||
currentLanguageTag,
|
|
||||||
getChangeLocalUrl: 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`);
|
|
||||||
|
|
||||||
return targetSupportedLocale.url;
|
|
||||||
},
|
|
||||||
labelBySupportedLanguageTag: Object.fromEntries(
|
|
||||||
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])
|
|
||||||
)
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return i18n ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
useI18n,
|
|
||||||
ofTypeI18n: Reflect<GenericI18n<MessageKey | ExtraMessageKey>>()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createI18nTranslationFunctions<MessageKey extends string>(params: {
|
|
||||||
fallbackMessages: Record<MessageKey, string>;
|
|
||||||
messages: Record<MessageKey, string>;
|
|
||||||
__localizationRealmOverridesUserProfile: Record<string, string> | undefined;
|
|
||||||
}): Pick<GenericI18n<MessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
|
|
||||||
const { fallbackMessages, messages, __localizationRealmOverridesUserProfile } = params;
|
|
||||||
|
|
||||||
function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): string | JSX.Element | undefined {
|
|
||||||
const { key, args, doRenderAsHtml } = 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
messageWithArgsInjected = messageWithArgsInjected.replace(
|
setI18n_toReturn(i18n);
|
||||||
new RegExp(`\\{${i + startIndex}\\}`, "g"),
|
|
||||||
arg.replace(/</g, "<").replace(/>/g, ">")
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return messageWithArgsInjected;
|
return () => {
|
||||||
})();
|
isActive = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return doRenderAsHtml ? (
|
return { i18n: i18n_toReturn };
|
||||||
<span
|
|
||||||
// NOTE: The message is trusted. The arguments are not but are escaped.
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: messageWithArgsInjectedIfAny
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
messageWithArgsInjectedIfAny
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): JSX.Element | string {
|
return { useI18n, ofTypeI18n: Reflect<I18n>() };
|
||||||
const { key, args, doRenderAsHtml } = props;
|
}
|
||||||
|
|
||||||
if (__localizationRealmOverridesUserProfile !== undefined && key in __localizationRealmOverridesUserProfile) {
|
function createI18nTranslationFunctionsFactory<MessageKey extends string, ExtraMessageKey extends string>(params: {
|
||||||
const resolvedMessage = __localizationRealmOverridesUserProfile[key];
|
messages_fallbackLanguage: Record<MessageKey, string>;
|
||||||
|
extraMessages_fallbackLanguage: Record<ExtraMessageKey, string> | undefined;
|
||||||
|
extraMessages: Partial<Record<ExtraMessageKey, string>> | undefined;
|
||||||
|
__localizationRealmOverridesUserProfile: Record<string, string> | undefined;
|
||||||
|
}) {
|
||||||
|
const { __localizationRealmOverridesUserProfile, extraMessages } = params;
|
||||||
|
|
||||||
|
const messages_fallbackLanguage = {
|
||||||
|
...params.messages_fallbackLanguage,
|
||||||
|
...params.extraMessages_fallbackLanguage
|
||||||
|
};
|
||||||
|
|
||||||
|
function createI18nTranslationFunctions(params: {
|
||||||
|
messages: Partial<Record<MessageKey, string>> | undefined;
|
||||||
|
}): Pick<GenericI18n<MessageKey | ExtraMessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
|
||||||
|
const messages = {
|
||||||
|
...params.messages,
|
||||||
|
...extraMessages
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): string | JSX.Element | undefined {
|
||||||
|
const { key, args, doRenderAsHtml } = props;
|
||||||
|
|
||||||
|
const messageOrUndefined: string | undefined = (messages as any)[key] ?? (messages_fallbackLanguage 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.replace(/</g, "<").replace(/>/g, ">")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return messageWithArgsInjected;
|
||||||
|
})();
|
||||||
|
|
||||||
return doRenderAsHtml ? (
|
return doRenderAsHtml ? (
|
||||||
<span
|
<span
|
||||||
// NOTE: The message is trusted. The arguments are not but are escaped.
|
// NOTE: The message is trusted. The arguments are not but are escaped.
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: resolvedMessage
|
__html: messageWithArgsInjectedIfAny
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
resolvedMessage
|
messageWithArgsInjectedIfAny
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!/\$\{[^}]+\}/.test(key)) {
|
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): JSX.Element | string {
|
||||||
const resolvedMessage = resolveMsg({ key, args, doRenderAsHtml });
|
const { key, args, doRenderAsHtml } = props;
|
||||||
|
|
||||||
if (resolvedMessage === undefined) {
|
if (__localizationRealmOverridesUserProfile !== undefined && key in __localizationRealmOverridesUserProfile) {
|
||||||
return doRenderAsHtml ? <span dangerouslySetInnerHTML={{ __html: key }} /> : key;
|
const resolvedMessage = __localizationRealmOverridesUserProfile[key];
|
||||||
|
|
||||||
|
return doRenderAsHtml ? (
|
||||||
|
<span
|
||||||
|
// NOTE: The message is trusted. The arguments are not but are escaped.
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: resolvedMessage
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
resolvedMessage
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolvedMessage;
|
if (!/\$\{[^}]+\}/.test(key)) {
|
||||||
|
const resolvedMessage = resolveMsg({ key, args, doRenderAsHtml });
|
||||||
|
|
||||||
|
if (resolvedMessage === undefined) {
|
||||||
|
return doRenderAsHtml ? <span dangerouslySetInnerHTML={{ __html: key }} /> : key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isFirstMatch = true;
|
||||||
|
|
||||||
|
const resolvedComplexMessage = key.replace(/\$\{([^}]+)\}/g, (...[, key_i]) => {
|
||||||
|
const replaceBy = resolveMsg({ key: key_i, args: isFirstMatch ? args : [], doRenderAsHtml: false }) ?? key_i;
|
||||||
|
|
||||||
|
isFirstMatch = false;
|
||||||
|
|
||||||
|
return replaceBy;
|
||||||
|
});
|
||||||
|
|
||||||
|
return doRenderAsHtml ? <span dangerouslySetInnerHTML={{ __html: resolvedComplexMessage }} /> : resolvedComplexMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
let isFirstMatch = true;
|
return {
|
||||||
|
msgStr: (key, ...args) => resolveMsg({ key, args, doRenderAsHtml: false }) as string,
|
||||||
const resolvedComplexMessage = key.replace(/\$\{([^}]+)\}/g, (...[, key_i]) => {
|
msg: (key, ...args) => resolveMsg({ key, args, doRenderAsHtml: true }) as JSX.Element,
|
||||||
const replaceBy = resolveMsg({ key: key_i, args: isFirstMatch ? args : [], doRenderAsHtml: false }) ?? key_i;
|
advancedMsg: (key, ...args) =>
|
||||||
|
resolveMsgAdvanced({
|
||||||
isFirstMatch = false;
|
key,
|
||||||
|
args,
|
||||||
return replaceBy;
|
doRenderAsHtml: true
|
||||||
});
|
}) as JSX.Element,
|
||||||
|
advancedMsgStr: (key, ...args) =>
|
||||||
return doRenderAsHtml ? <span dangerouslySetInnerHTML={{ __html: resolvedComplexMessage }} /> : resolvedComplexMessage;
|
resolveMsgAdvanced({
|
||||||
|
key,
|
||||||
|
args,
|
||||||
|
doRenderAsHtml: false
|
||||||
|
}) as string
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return { createI18nTranslationFunctions };
|
||||||
msgStr: (key, ...args) => resolveMsg({ key, args, doRenderAsHtml: false }) as string,
|
|
||||||
msg: (key, ...args) => resolveMsg({ key, args, doRenderAsHtml: true }) as JSX.Element,
|
|
||||||
advancedMsg: (key, ...args) =>
|
|
||||||
resolveMsgAdvanced({
|
|
||||||
key,
|
|
||||||
args,
|
|
||||||
doRenderAsHtml: true
|
|
||||||
}) as JSX.Element,
|
|
||||||
advancedMsgStr: (key, ...args) =>
|
|
||||||
resolveMsgAdvanced({
|
|
||||||
key,
|
|
||||||
args,
|
|
||||||
doRenderAsHtml: 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",
|
|
||||||
selectAnOption: "Select an option",
|
|
||||||
remove: "Remove",
|
|
||||||
addValue: "Add value"
|
|
||||||
},
|
|
||||||
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",
|
|
||||||
selectAnOption: "Sélectionner une option",
|
|
||||||
remove: "Supprimer",
|
|
||||||
addValue: "Ajouter une valeur"
|
|
||||||
/* spell-checker: enable */
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
@ -1 +1,5 @@
|
|||||||
export type { I18n } from "./i18n";
|
import type { GenericI18n, MessageKey, KcContextLike } from "./i18n";
|
||||||
|
export type { MessageKey, KcContextLike };
|
||||||
|
export type I18n = GenericI18n<MessageKey>;
|
||||||
|
export { createUseI18n } from "./i18n";
|
||||||
|
export { fallbackLanguageTag } from "./i18n";
|
||||||
|
@ -1,10 +1,4 @@
|
|||||||
|
export type { ExtendKcContext, Attribute } from "keycloakify/login/KcContext";
|
||||||
|
export type { ClassKey } from "keycloakify/login/TemplateProps";
|
||||||
export { useDownloadTerms } from "keycloakify/login/lib/useDownloadTerms";
|
export { useDownloadTerms } from "keycloakify/login/lib/useDownloadTerms";
|
||||||
export { createUseI18n } from "keycloakify/login/i18n/i18n";
|
export { createUseI18n } from "keycloakify/login/i18n";
|
||||||
export type {
|
|
||||||
ExtendKcContext,
|
|
||||||
Attribute,
|
|
||||||
PasswordPolicies
|
|
||||||
} from "keycloakify/login/kcContext";
|
|
||||||
export { createGetKcContextMock } from "keycloakify/login/kcContext";
|
|
||||||
|
|
||||||
export type { PageProps } from "keycloakify/login/pages/PageProps";
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { createUseClassName } from "keycloakify/lib/useGetClassName";
|
import { createGetKcClsx } from "keycloakify/lib/getKcClsx";
|
||||||
import type { ClassKey } from "keycloakify/login/TemplateProps";
|
import type { ClassKey } from "keycloakify/login/TemplateProps";
|
||||||
|
|
||||||
export const { useGetClassName } = createUseClassName<ClassKey>({
|
export const { getKcClsx } = createGetKcClsx<ClassKey>({
|
||||||
defaultClasses: {
|
defaultClasses: {
|
||||||
kcHtmlClass: "login-pf",
|
kcHtmlClass: "login-pf",
|
||||||
kcBodyClass: undefined,
|
kcBodyClass: undefined,
|
||||||
@ -137,3 +137,7 @@ export const { useGetClassName } = createUseClassName<ClassKey>({
|
|||||||
kcLabelClass: "pf-c-form__label pf-c-form__label-text"
|
kcLabelClass: "pf-c-form__label pf-c-form__label-text"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type { ClassKey };
|
||||||
|
|
||||||
|
export type KcClsx = ReturnType<typeof getKcClsx>["kcClsx"];
|
@ -1,13 +1,19 @@
|
|||||||
import { fallbackLanguageTag } from "keycloakify/login/i18n/i18n";
|
import { fallbackLanguageTag } from "keycloakify/login/i18n";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import {
|
import {
|
||||||
createStatefulObservable,
|
createStatefulObservable,
|
||||||
useRerenderOnChange
|
useRerenderOnChange
|
||||||
} from "keycloakify/tools/StatefulObservable";
|
} from "keycloakify/tools/StatefulObservable";
|
||||||
import { KcContext } from "../kcContext";
|
|
||||||
import { useOnFistMount } from "keycloakify/tools/useOnFirstMount";
|
import { useOnFistMount } from "keycloakify/tools/useOnFirstMount";
|
||||||
|
import { KcContext } from "../KcContext";
|
||||||
|
|
||||||
const obsTermsMarkdown = createStatefulObservable<string | undefined>(() => undefined);
|
const obs = createStatefulObservable<
|
||||||
|
| {
|
||||||
|
termsMarkdown: string;
|
||||||
|
termsLanguageTag: string | undefined;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
>(() => undefined);
|
||||||
|
|
||||||
export type KcContextLike = {
|
export type KcContextLike = {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
@ -22,26 +28,30 @@ assert<KcContext extends KcContextLike ? true : false>();
|
|||||||
/** Allow to avoid bundling the terms and download it on demand*/
|
/** Allow to avoid bundling the terms and download it on demand*/
|
||||||
export function useDownloadTerms(params: {
|
export function useDownloadTerms(params: {
|
||||||
kcContext: KcContextLike;
|
kcContext: KcContextLike;
|
||||||
downloadTermMarkdown: (params: { currentLanguageTag: string }) => Promise<string>;
|
downloadTermsMarkdown: (params: {
|
||||||
|
currentLanguageTag: string;
|
||||||
|
}) => Promise<{ termsMarkdown: string; termsLanguageTag: string | undefined }>;
|
||||||
}) {
|
}) {
|
||||||
const { kcContext, downloadTermMarkdown } = params;
|
const { kcContext, downloadTermsMarkdown } = params;
|
||||||
|
|
||||||
useOnFistMount(async () => {
|
useOnFistMount(async () => {
|
||||||
if (kcContext.pageId === "terms.ftl" || kcContext.termsAcceptanceRequired) {
|
if (kcContext.pageId === "terms.ftl" || kcContext.termsAcceptanceRequired) {
|
||||||
const termsMarkdown = await downloadTermMarkdown({
|
obs.current = await downloadTermsMarkdown({
|
||||||
currentLanguageTag:
|
currentLanguageTag:
|
||||||
kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag
|
kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag
|
||||||
});
|
});
|
||||||
|
|
||||||
obsTermsMarkdown.current = termsMarkdown;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTermsMarkdown() {
|
export function useTermsMarkdown() {
|
||||||
useRerenderOnChange(obsTermsMarkdown);
|
useRerenderOnChange(obs);
|
||||||
|
|
||||||
const termsMarkdown = obsTermsMarkdown.current;
|
if (obs.current === undefined) {
|
||||||
|
return { isDownloadComplete: false as const };
|
||||||
|
}
|
||||||
|
|
||||||
return { termsMarkdown };
|
const { termsMarkdown, termsLanguageTag } = obs.current;
|
||||||
|
|
||||||
|
return { isDownloadComplete: true, termsMarkdown, termsLanguageTag };
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import "keycloakify/tools/Array.prototype.every";
|
import "keycloakify/tools/Array.prototype.every";
|
||||||
import { useMemo, useReducer, useEffect, Fragment, type Dispatch } from "react";
|
import { useMemo, useReducer, useEffect, Fragment, type Dispatch } from "react";
|
||||||
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
import { id } from "tsafe/id";
|
import { id } from "tsafe/id";
|
||||||
import type { MessageKey } from "keycloakify/login/i18n/i18n";
|
import { structuredCloneButFunctions } from "keycloakify/tools/structuredCloneButFunctions";
|
||||||
import type { Attribute, Validators } from "keycloakify/login/kcContext/KcContext";
|
|
||||||
import { useConstCallback } from "keycloakify/tools/useConstCallback";
|
import { useConstCallback } from "keycloakify/tools/useConstCallback";
|
||||||
import { emailRegexp } from "keycloakify/tools/emailRegExp";
|
import { emailRegexp } from "keycloakify/tools/emailRegExp";
|
||||||
import type { KcContext, PasswordPolicies } from "keycloakify/login/kcContext/KcContext";
|
|
||||||
import { assert, type Equals } from "tsafe/assert";
|
|
||||||
import { formatNumber } from "keycloakify/tools/formatNumber";
|
import { formatNumber } from "keycloakify/tools/formatNumber";
|
||||||
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
|
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
|
||||||
import { structuredCloneButFunctions } from "keycloakify/tools/structuredCloneButFunctions";
|
import type { PasswordPolicies, Attribute, Validators } from "keycloakify/login/KcContext";
|
||||||
|
import type { KcContext } from "../KcContext";
|
||||||
|
import type { MessageKey } from "keycloakify/login/i18n";
|
||||||
|
import { KcContextLike as KcContextLike_i18n } from "keycloakify/login/i18n";
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
export type FormFieldError = {
|
export type FormFieldError = {
|
||||||
@ -65,19 +66,20 @@ export type FormAction =
|
|||||||
fieldIndex: number | undefined;
|
fieldIndex: number | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type KcContextLike = {
|
export type KcContextLike = KcContextLike_i18n &
|
||||||
messagesPerField: Pick<KcContext.Common["messagesPerField"], "existsError" | "get">;
|
KcContextLike_useGetErrors & {
|
||||||
profile: {
|
profile: {
|
||||||
attributesByName: Record<string, Attribute>;
|
attributesByName: Record<string, Attribute>;
|
||||||
html5DataAnnotations?: Record<string, string>;
|
html5DataAnnotations?: Record<string, string>;
|
||||||
|
};
|
||||||
|
passwordRequired?: boolean;
|
||||||
|
realm: { registrationEmailAsUsername: boolean };
|
||||||
|
url: {
|
||||||
|
resourcesPath: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
passwordRequired?: boolean;
|
|
||||||
realm: { registrationEmailAsUsername: boolean };
|
assert<Extract<KcContext.Register, { pageId: "register.ftl" }> extends KcContextLike ? true : false>();
|
||||||
passwordPolicies?: PasswordPolicies;
|
|
||||||
url: {
|
|
||||||
resourcesPath: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ParamsOfUseUserProfileForm = {
|
export type ParamsOfUseUserProfileForm = {
|
||||||
kcContext: KcContextLike;
|
kcContext: KcContextLike;
|
||||||
@ -516,7 +518,14 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField" | "passwordPolicies">; i18n: I18n }) {
|
type KcContextLike_useGetErrors = KcContextLike_i18n & {
|
||||||
|
messagesPerField: Pick<KcContext["messagesPerField"], "existsError" | "get">;
|
||||||
|
passwordPolicies?: PasswordPolicies;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<KcContextLike extends KcContextLike_useGetErrors ? true : false>();
|
||||||
|
|
||||||
|
function useGetErrors(params: { kcContext: KcContextLike_useGetErrors; i18n: I18n }) {
|
||||||
const { kcContext, i18n } = params;
|
const { kcContext, i18n } = params;
|
||||||
|
|
||||||
const { messagesPerField, passwordPolicies } = kcContext;
|
const { messagesPerField, passwordPolicies } = kcContext;
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
|
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
import type { KcContext } from "../KcContext";
|
||||||
import type { KcContext } from "../kcContext";
|
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
export default function Code(props: PageProps<Extract<KcContext, { pageId: "code.ftl" }>, I18n>) {
|
export default function Code(props: PageProps<Extract<KcContext, { pageId: "code.ftl" }>, I18n>) {
|
||||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||||
|
|
||||||
const { getClassName } = useGetClassName({
|
const { kcClsx } = getKcClsx({
|
||||||
doUseDefaultCss,
|
doUseDefaultCss,
|
||||||
classes
|
classes
|
||||||
});
|
});
|
||||||
@ -17,14 +17,17 @@ export default function Code(props: PageProps<Extract<KcContext, { pageId: "code
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Template
|
<Template
|
||||||
{...{ kcContext, i18n, doUseDefaultCss, classes }}
|
kcContext={kcContext}
|
||||||
|
i18n={i18n}
|
||||||
|
doUseDefaultCss={doUseDefaultCss}
|
||||||
|
classes={classes}
|
||||||
headerNode={code.success ? msg("codeSuccessTitle") : msg("codeErrorTitle", code.error)}
|
headerNode={code.success ? msg("codeSuccessTitle") : msg("codeErrorTitle", code.error)}
|
||||||
>
|
>
|
||||||
<div id="kc-code">
|
<div id="kc-code">
|
||||||
{code.success ? (
|
{code.success ? (
|
||||||
<>
|
<>
|
||||||
<p>{msg("copyCodeInstruction")}</p>
|
<p>{msg("copyCodeInstruction")}</p>
|
||||||
<input id="code" className={getClassName("kcTextareaClass")} defaultValue={code.code} />
|
<input id="code" className={kcClsx("kcTextareaClass")} defaultValue={code.code} />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p id="error">{code.error}</p>
|
<p id="error">{code.error}</p>
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
import type { KcContext } from "../KcContext";
|
||||||
import type { KcContext } from "../kcContext";
|
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
export default function DeleteAccountConfirm(props: PageProps<Extract<KcContext, { pageId: "delete-account-confirm.ftl" }>, I18n>) {
|
export default function DeleteAccountConfirm(props: PageProps<Extract<KcContext, { pageId: "delete-account-confirm.ftl" }>, I18n>) {
|
||||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||||
|
|
||||||
const { getClassName } = useGetClassName({
|
const { kcClsx } = getKcClsx({
|
||||||
doUseDefaultCss,
|
doUseDefaultCss,
|
||||||
classes
|
classes
|
||||||
});
|
});
|
||||||
@ -17,7 +16,7 @@ export default function DeleteAccountConfirm(props: PageProps<Extract<KcContext,
|
|||||||
const { msg, msgStr } = i18n;
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("deleteAccountConfirm")}>
|
<Template kcContext={kcContext} i18n={i18n} doUseDefaultCss={doUseDefaultCss} classes={classes} headerNode={msg("deleteAccountConfirm")}>
|
||||||
<form action={url.loginAction} className="form-vertical" method="post">
|
<form action={url.loginAction} className="form-vertical" method="post">
|
||||||
<div className="alert alert-warning" style={{ marginTop: "0", marginBottom: "30px" }}>
|
<div className="alert alert-warning" style={{ marginTop: "0", marginBottom: "30px" }}>
|
||||||
<span className="pficon pficon-warning-triangle-o"></span>
|
<span className="pficon pficon-warning-triangle-o"></span>
|
||||||
@ -37,13 +36,13 @@ export default function DeleteAccountConfirm(props: PageProps<Extract<KcContext,
|
|||||||
<p className="delete-account-text">{msg("finalDeletionConfirmation")}</p>
|
<p className="delete-account-text">{msg("finalDeletionConfirmation")}</p>
|
||||||
<div id="kc-form-buttons">
|
<div id="kc-form-buttons">
|
||||||
<input
|
<input
|
||||||
className={clsx(getClassName("kcButtonClass"), getClassName("kcButtonPrimaryClass"), getClassName("kcButtonLargeClass"))}
|
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonLargeClass")}
|
||||||
type="submit"
|
type="submit"
|
||||||
value={msgStr("doConfirmDelete")}
|
value={msgStr("doConfirmDelete")}
|
||||||
/>
|
/>
|
||||||
{triggered_from_aia && (
|
{triggered_from_aia && (
|
||||||
<button
|
<button
|
||||||
className={clsx(getClassName("kcButtonClass"), getClassName("kcButtonDefaultClass"), getClassName("kcButtonLargeClass"))}
|
className={kcClsx("kcButtonClass", "kcButtonDefaultClass", "kcButtonLargeClass")}
|
||||||
style={{ marginLeft: "calc(100% - 220px)" }}
|
style={{ marginLeft: "calc(100% - 220px)" }}
|
||||||
type="submit"
|
type="submit"
|
||||||
name="cancel-aia"
|
name="cancel-aia"
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import type { KcContext } from "../kcContext";
|
import type { KcContext } from "../KcContext";
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
|
||||||
|
|
||||||
export default function DeleteCredential(props: PageProps<Extract<KcContext, { pageId: "delete-credential.ftl" }>, I18n>) {
|
export default function DeleteCredential(props: PageProps<Extract<KcContext, { pageId: "delete-credential.ftl" }>, I18n>) {
|
||||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||||
|
|
||||||
const { msgStr, msg } = i18n;
|
const { msgStr, msg } = i18n;
|
||||||
|
|
||||||
const { getClassName } = useGetClassName({
|
const { kcClsx } = getKcClsx({
|
||||||
doUseDefaultCss,
|
doUseDefaultCss,
|
||||||
classes
|
classes
|
||||||
});
|
});
|
||||||
@ -18,21 +17,24 @@ export default function DeleteCredential(props: PageProps<Extract<KcContext, { p
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Template
|
<Template
|
||||||
{...{ kcContext, i18n, doUseDefaultCss, classes }}
|
kcContext={kcContext}
|
||||||
|
i18n={i18n}
|
||||||
|
doUseDefaultCss={doUseDefaultCss}
|
||||||
|
classes={classes}
|
||||||
displayMessage={false}
|
displayMessage={false}
|
||||||
headerNode={msg("deleteCredentialTitle", credentialLabel)}
|
headerNode={msg("deleteCredentialTitle", credentialLabel)}
|
||||||
>
|
>
|
||||||
<div id="kc-delete-text">{msg("deleteCredentialMessage", credentialLabel)}</div>
|
<div id="kc-delete-text">{msg("deleteCredentialMessage", credentialLabel)}</div>
|
||||||
<form className="form-actions" action={url.loginAction} method="POST">
|
<form className="form-actions" action={url.loginAction} method="POST">
|
||||||
<input
|
<input
|
||||||
className={clsx(getClassName("kcButtonClass"), getClassName("kcButtonPrimaryClass"), getClassName("kcButtonLargeClass"))}
|
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonLargeClass")}
|
||||||
name="accept"
|
name="accept"
|
||||||
id="kc-accept"
|
id="kc-accept"
|
||||||
type="submit"
|
type="submit"
|
||||||
value={msgStr("doConfirmDelete")}
|
value={msgStr("doConfirmDelete")}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
className={clsx(getClassName("kcButtonClass"), getClassName("kcButtonDefaultClass"), getClassName("kcButtonLargeClass"))}
|
className={kcClsx("kcButtonClass", "kcButtonDefaultClass", "kcButtonLargeClass")}
|
||||||
name="cancel-aia"
|
name="cancel-aia"
|
||||||
value={msgStr("doCancel")}
|
value={msgStr("doCancel")}
|
||||||
id="kc-decline"
|
id="kc-decline"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import type { KcContext } from "../kcContext";
|
import type { KcContext } from "../KcContext";
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
export default function Error(props: PageProps<Extract<KcContext, { pageId: "error.ftl" }>, I18n>) {
|
export default function Error(props: PageProps<Extract<KcContext, { pageId: "error.ftl" }>, I18n>) {
|
||||||
@ -10,7 +10,14 @@ export default function Error(props: PageProps<Extract<KcContext, { pageId: "err
|
|||||||
const { msg } = i18n;
|
const { msg } = i18n;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} displayMessage={false} headerNode={msg("errorTitle")}>
|
<Template
|
||||||
|
kcContext={kcContext}
|
||||||
|
i18n={i18n}
|
||||||
|
doUseDefaultCss={doUseDefaultCss}
|
||||||
|
classes={classes}
|
||||||
|
displayMessage={false}
|
||||||
|
headerNode={msg("errorTitle")}
|
||||||
|
>
|
||||||
<div id="kc-error-message">
|
<div id="kc-error-message">
|
||||||
<p className="instruction">{message.summary}</p>
|
<p className="instruction">{message.summary}</p>
|
||||||
{!skipLink && client !== undefined && client.baseUrl !== undefined && (
|
{!skipLink && client !== undefined && client.baseUrl !== undefined && (
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import type { KcContext } from "../kcContext";
|
import type { KcContext } from "../KcContext";
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
export default function FrontchannelLogout(props: PageProps<Extract<KcContext, { pageId: "frontchannel-logout.ftl" }>, I18n>) {
|
export default function FrontchannelLogout(props: PageProps<Extract<KcContext, { pageId: "frontchannel-logout.ftl" }>, I18n>) {
|
||||||
@ -18,7 +18,10 @@ export default function FrontchannelLogout(props: PageProps<Extract<KcContext, {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Template
|
<Template
|
||||||
{...{ kcContext, i18n, doUseDefaultCss, classes }}
|
kcContext={kcContext}
|
||||||
|
i18n={i18n}
|
||||||
|
doUseDefaultCss={doUseDefaultCss}
|
||||||
|
classes={classes}
|
||||||
documentTitle={msgStr("frontchannel-logout.title")}
|
documentTitle={msgStr("frontchannel-logout.title")}
|
||||||
headerNode={msg("frontchannel-logout.title")}
|
headerNode={msg("frontchannel-logout.title")}
|
||||||
>
|
>
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { clsx } from "keycloakify/tools/clsx";
|
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
|
||||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
|
||||||
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
|
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
|
||||||
|
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields";
|
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields";
|
||||||
import type { KcContext } from "../kcContext";
|
import type { KcContext } from "../KcContext";
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
type IdpReviewUserProfileProps = PageProps<Extract<KcContext, { pageId: "idp-review-user-profile.ftl" }>, I18n> & {
|
type IdpReviewUserProfileProps = PageProps<Extract<KcContext, { pageId: "idp-review-user-profile.ftl" }>, I18n> & {
|
||||||
UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>;
|
UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>;
|
||||||
|
doMakeUserConfirmPassword: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function IdpReviewUserProfile(props: IdpReviewUserProfileProps) {
|
export default function IdpReviewUserProfile(props: IdpReviewUserProfileProps) {
|
||||||
const { kcContext, i18n, doUseDefaultCss, Template, classes, UserProfileFormFields } = props;
|
const { kcContext, i18n, doUseDefaultCss, Template, classes, UserProfileFormFields, doMakeUserConfirmPassword } = props;
|
||||||
|
|
||||||
const { getClassName } = useGetClassName({
|
const { kcClsx } = getKcClsx({
|
||||||
doUseDefaultCss,
|
doUseDefaultCss,
|
||||||
classes
|
classes
|
||||||
});
|
});
|
||||||
@ -27,30 +27,29 @@ export default function IdpReviewUserProfile(props: IdpReviewUserProfileProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Template
|
<Template
|
||||||
{...{ kcContext, i18n, doUseDefaultCss, classes }}
|
kcContext={kcContext}
|
||||||
|
i18n={i18n}
|
||||||
|
doUseDefaultCss={doUseDefaultCss}
|
||||||
|
classes={classes}
|
||||||
displayMessage={messagesPerField.exists("global")}
|
displayMessage={messagesPerField.exists("global")}
|
||||||
displayRequiredFields
|
displayRequiredFields
|
||||||
headerNode={msg("loginIdpReviewProfileTitle")}
|
headerNode={msg("loginIdpReviewProfileTitle")}
|
||||||
>
|
>
|
||||||
<form id="kc-idp-review-profile-form" className={getClassName("kcFormClass")} action={url.loginAction} method="post">
|
<form id="kc-idp-review-profile-form" className={kcClsx("kcFormClass")} action={url.loginAction} method="post">
|
||||||
<UserProfileFormFields
|
<UserProfileFormFields
|
||||||
kcContext={kcContext}
|
kcContext={kcContext}
|
||||||
onIsFormSubmittableValueChange={setIsFomSubmittable}
|
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
getClassName={getClassName}
|
onIsFormSubmittableValueChange={setIsFomSubmittable}
|
||||||
|
kcClsx={kcClsx}
|
||||||
|
doMakeUserConfirmPassword={doMakeUserConfirmPassword}
|
||||||
/>
|
/>
|
||||||
<div className={getClassName("kcFormGroupClass")}>
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
|
<div id="kc-form-options" className={kcClsx("kcFormOptionsClass")}>
|
||||||
<div className={getClassName("kcFormOptionsWrapperClass")} />
|
<div className={kcClsx("kcFormOptionsWrapperClass")} />
|
||||||
</div>
|
</div>
|
||||||
<div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
|
<div id="kc-form-buttons" className={kcClsx("kcFormButtonsClass")}>
|
||||||
<input
|
<input
|
||||||
className={clsx(
|
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonBlockClass", "kcButtonLargeClass")}
|
||||||
getClassName("kcButtonClass"),
|
|
||||||
getClassName("kcButtonPrimaryClass"),
|
|
||||||
getClassName("kcButtonBlockClass"),
|
|
||||||
getClassName("kcButtonLargeClass")
|
|
||||||
)}
|
|
||||||
type="submit"
|
type="submit"
|
||||||
value={msgStr("doSubmit")}
|
value={msgStr("doSubmit")}
|
||||||
disabled={!isFomSubmittable}
|
disabled={!isFomSubmittable}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { assert } from "keycloakify/tools/assert";
|
import { assert } from "keycloakify/tools/assert";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import type { KcContext } from "../kcContext";
|
import type { KcContext } from "../KcContext";
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
export default function Info(props: PageProps<Extract<KcContext, { pageId: "info.ftl" }>, I18n>) {
|
export default function Info(props: PageProps<Extract<KcContext, { pageId: "info.ftl" }>, I18n>) {
|
||||||
@ -17,7 +17,10 @@ export default function Info(props: PageProps<Extract<KcContext, { pageId: "info
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Template
|
<Template
|
||||||
{...{ kcContext, i18n, doUseDefaultCss, classes }}
|
kcContext={kcContext}
|
||||||
|
i18n={i18n}
|
||||||
|
doUseDefaultCss={doUseDefaultCss}
|
||||||
|
classes={classes}
|
||||||
displayMessage={false}
|
displayMessage={false}
|
||||||
headerNode={messageHeader !== undefined ? <>{messageHeader}</> : <>{message.summary}</>}
|
headerNode={messageHeader !== undefined ? <>{messageHeader}</> : <>{message.summary}</>}
|
||||||
>
|
>
|
||||||
|
@ -2,14 +2,14 @@ import { useState, useEffect, useReducer } from "react";
|
|||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
import type { KcContext } from "../kcContext";
|
import type { KcContext } from "../KcContext";
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
export default function Login(props: PageProps<Extract<KcContext, { pageId: "login.ftl" }>, I18n>) {
|
export default function Login(props: PageProps<Extract<KcContext, { pageId: "login.ftl" }>, I18n>) {
|
||||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||||
|
|
||||||
const { getClassName } = useGetClassName({
|
const { kcClsx } = getKcClsx({
|
||||||
doUseDefaultCss,
|
doUseDefaultCss,
|
||||||
classes
|
classes
|
||||||
});
|
});
|
||||||
@ -22,7 +22,10 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Template
|
<Template
|
||||||
{...{ kcContext, i18n, doUseDefaultCss, classes }}
|
kcContext={kcContext}
|
||||||
|
i18n={i18n}
|
||||||
|
doUseDefaultCss={doUseDefaultCss}
|
||||||
|
classes={classes}
|
||||||
displayMessage={!messagesPerField.existsError("username", "password")}
|
displayMessage={!messagesPerField.existsError("username", "password")}
|
||||||
headerNode={msg("loginAccountTitle")}
|
headerNode={msg("loginAccountTitle")}
|
||||||
displayInfo={realm.password && realm.registrationAllowed && !registrationDisabled}
|
displayInfo={realm.password && realm.registrationAllowed && !registrationDisabled}
|
||||||
@ -41,32 +44,23 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
|
|||||||
socialProvidersNode={
|
socialProvidersNode={
|
||||||
<>
|
<>
|
||||||
{realm.password && social.providers?.length && (
|
{realm.password && social.providers?.length && (
|
||||||
<div id="kc-social-providers" className={getClassName("kcFormSocialAccountSectionClass")}>
|
<div id="kc-social-providers" className={kcClsx("kcFormSocialAccountSectionClass")}>
|
||||||
<hr />
|
<hr />
|
||||||
<h2>{msg("identity-provider-login-label")}</h2>
|
<h2>{msg("identity-provider-login-label")}</h2>
|
||||||
<ul
|
<ul className={kcClsx("kcFormSocialAccountListClass", social.providers.length > 3 && "kcFormSocialAccountListGridClass")}>
|
||||||
className={clsx(
|
|
||||||
getClassName("kcFormSocialAccountListClass"),
|
|
||||||
social.providers.length > 3 && getClassName("kcFormSocialAccountListGridClass")
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{social.providers.map((...[p, , providers]) => (
|
{social.providers.map((...[p, , providers]) => (
|
||||||
<li key={p.alias}>
|
<li key={p.alias}>
|
||||||
<a
|
<a
|
||||||
id={`social-${p.alias}`}
|
id={`social-${p.alias}`}
|
||||||
className={clsx(
|
className={kcClsx(
|
||||||
getClassName("kcFormSocialAccountListButtonClass"),
|
"kcFormSocialAccountListButtonClass",
|
||||||
providers.length > 3 && getClassName("kcFormSocialAccountGridItem")
|
providers.length > 3 && "kcFormSocialAccountGridItem"
|
||||||
)}
|
)}
|
||||||
type="button"
|
type="button"
|
||||||
href={p.loginUrl}
|
href={p.loginUrl}
|
||||||
>
|
>
|
||||||
{p.iconClasses && (
|
{p.iconClasses && <i className={clsx(kcClsx("kcCommonLogoIdP"), p.iconClasses)} aria-hidden="true"></i>}
|
||||||
<i className={clsx(getClassName("kcCommonLogoIdP"), p.iconClasses)} aria-hidden="true"></i>
|
<span className={clsx(kcClsx("kcFormSocialAccountNameClass"), p.iconClasses && "kc-social-icon-text")}>
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className={clsx(getClassName("kcFormSocialAccountNameClass"), p.iconClasses && "kc-social-icon-text")}
|
|
||||||
>
|
|
||||||
{p.displayName}
|
{p.displayName}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@ -91,8 +85,8 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
|
|||||||
method="post"
|
method="post"
|
||||||
>
|
>
|
||||||
{!usernameHidden && (
|
{!usernameHidden && (
|
||||||
<div className={getClassName("kcFormGroupClass")}>
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
<label htmlFor="username" className={getClassName("kcLabelClass")}>
|
<label htmlFor="username" className={kcClsx("kcLabelClass")}>
|
||||||
{!realm.loginWithEmailAllowed
|
{!realm.loginWithEmailAllowed
|
||||||
? msg("username")
|
? msg("username")
|
||||||
: !realm.registrationEmailAsUsername
|
: !realm.registrationEmailAsUsername
|
||||||
@ -102,7 +96,7 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
|
|||||||
<input
|
<input
|
||||||
tabIndex={2}
|
tabIndex={2}
|
||||||
id="username"
|
id="username"
|
||||||
className={getClassName("kcInputClass")}
|
className={kcClsx("kcInputClass")}
|
||||||
name="username"
|
name="username"
|
||||||
defaultValue={login.username ?? ""}
|
defaultValue={login.username ?? ""}
|
||||||
type="text"
|
type="text"
|
||||||
@ -111,22 +105,22 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
|
|||||||
aria-invalid={messagesPerField.existsError("username", "password")}
|
aria-invalid={messagesPerField.existsError("username", "password")}
|
||||||
/>
|
/>
|
||||||
{messagesPerField.existsError("username", "password") && (
|
{messagesPerField.existsError("username", "password") && (
|
||||||
<span id="input-error" className={getClassName("kcInputErrorMessageClass")} aria-live="polite">
|
<span id="input-error" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||||
{messagesPerField.getFirstError("username", "password")}
|
{messagesPerField.getFirstError("username", "password")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={getClassName("kcFormGroupClass")}>
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
<label htmlFor="password" className={getClassName("kcLabelClass")}>
|
<label htmlFor="password" className={kcClsx("kcLabelClass")}>
|
||||||
{msg("password")}
|
{msg("password")}
|
||||||
</label>
|
</label>
|
||||||
<PasswordWrapper getClassName={getClassName} i18n={i18n} passwordInputId="password">
|
<PasswordWrapper kcClsx={kcClsx} i18n={i18n} passwordInputId="password">
|
||||||
<input
|
<input
|
||||||
tabIndex={3}
|
tabIndex={3}
|
||||||
id="password"
|
id="password"
|
||||||
className={getClassName("kcInputClass")}
|
className={kcClsx("kcInputClass")}
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
@ -134,13 +128,13 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
|
|||||||
/>
|
/>
|
||||||
</PasswordWrapper>
|
</PasswordWrapper>
|
||||||
{usernameHidden && messagesPerField.existsError("username", "password") && (
|
{usernameHidden && messagesPerField.existsError("username", "password") && (
|
||||||
<span id="input-error" className={getClassName("kcInputErrorMessageClass")} aria-live="polite">
|
<span id="input-error" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||||
{messagesPerField.getFirstError("username", "password")}
|
{messagesPerField.getFirstError("username", "password")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={clsx(getClassName("kcFormGroupClass"), getClassName("kcFormSettingClass"))}>
|
<div className={kcClsx("kcFormGroupClass", "kcFormSettingClass")}>
|
||||||
<div id="kc-form-options">
|
<div id="kc-form-options">
|
||||||
{realm.rememberMe && !usernameHidden && (
|
{realm.rememberMe && !usernameHidden && (
|
||||||
<div className="checkbox">
|
<div className="checkbox">
|
||||||
@ -157,7 +151,7 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={getClassName("kcFormOptionsWrapperClass")}>
|
<div className={kcClsx("kcFormOptionsWrapperClass")}>
|
||||||
{realm.resetPasswordAllowed && (
|
{realm.resetPasswordAllowed && (
|
||||||
<span>
|
<span>
|
||||||
<a tabIndex={6} href={url.loginResetCredentialsUrl}>
|
<a tabIndex={6} href={url.loginResetCredentialsUrl}>
|
||||||
@ -168,17 +162,12 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="kc-form-buttons" className={getClassName("kcFormGroupClass")}>
|
<div id="kc-form-buttons" className={kcClsx("kcFormGroupClass")}>
|
||||||
<input type="hidden" id="id-hidden-input" name="credentialId" value={auth.selectedCredential} />
|
<input type="hidden" id="id-hidden-input" name="credentialId" value={auth.selectedCredential} />
|
||||||
<input
|
<input
|
||||||
tabIndex={7}
|
tabIndex={7}
|
||||||
disabled={isLoginButtonDisabled}
|
disabled={isLoginButtonDisabled}
|
||||||
className={clsx(
|
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonBlockClass", "kcButtonLargeClass")}
|
||||||
getClassName("kcButtonClass"),
|
|
||||||
getClassName("kcButtonPrimaryClass"),
|
|
||||||
getClassName("kcButtonBlockClass"),
|
|
||||||
getClassName("kcButtonLargeClass")
|
|
||||||
)}
|
|
||||||
name="login"
|
name="login"
|
||||||
id="kc-login"
|
id="kc-login"
|
||||||
type="submit"
|
type="submit"
|
||||||
@ -193,13 +182,8 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PasswordWrapper(props: {
|
function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: string; children: JSX.Element }) {
|
||||||
getClassName: ReturnType<typeof useGetClassName>["getClassName"];
|
const { kcClsx, i18n, passwordInputId, children } = props;
|
||||||
i18n: I18n;
|
|
||||||
passwordInputId: string;
|
|
||||||
children: JSX.Element;
|
|
||||||
}) {
|
|
||||||
const { getClassName, i18n, passwordInputId, children } = props;
|
|
||||||
|
|
||||||
const { msgStr } = i18n;
|
const { msgStr } = i18n;
|
||||||
|
|
||||||
@ -214,19 +198,16 @@ function PasswordWrapper(props: {
|
|||||||
}, [isPasswordRevealed]);
|
}, [isPasswordRevealed]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={getClassName("kcInputGroup")}>
|
<div className={kcClsx("kcInputGroup")}>
|
||||||
{children}
|
{children}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={getClassName("kcFormPasswordVisibilityButtonClass")}
|
className={kcClsx("kcFormPasswordVisibilityButtonClass")}
|
||||||
aria-label={msgStr(isPasswordRevealed ? "hidePassword" : "showPassword")}
|
aria-label={msgStr(isPasswordRevealed ? "hidePassword" : "showPassword")}
|
||||||
aria-controls={passwordInputId}
|
aria-controls={passwordInputId}
|
||||||
onClick={toggleIsPasswordRevealed}
|
onClick={toggleIsPasswordRevealed}
|
||||||
>
|
>
|
||||||
<i
|
<i className={kcClsx(isPasswordRevealed ? "kcFormPasswordVisibilityIconHide" : "kcFormPasswordVisibilityIconShow")} aria-hidden />
|
||||||
className={getClassName(isPasswordRevealed ? "kcFormPasswordVisibilityIconHide" : "kcFormPasswordVisibilityIconShow")}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { getKcClsx, KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
import type { KcContext } from "../KcContext";
|
||||||
import type { KcContext } from "../kcContext";
|
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pageId: "login-config-totp.ftl" }>, I18n>) {
|
export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pageId: "login-config-totp.ftl" }>, I18n>) {
|
||||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||||
|
|
||||||
const { getClassName } = useGetClassName({
|
const { kcClsx } = getKcClsx({
|
||||||
doUseDefaultCss,
|
doUseDefaultCss,
|
||||||
classes
|
classes
|
||||||
});
|
});
|
||||||
@ -17,7 +16,7 @@ export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pa
|
|||||||
const { msg, msgStr, advancedMsg } = i18n;
|
const { msg, msgStr, advancedMsg } = i18n;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("loginTotpTitle")}>
|
<Template kcContext={kcContext} i18n={i18n} doUseDefaultCss={doUseDefaultCss} classes={classes} headerNode={msg("loginTotpTitle")}>
|
||||||
<>
|
<>
|
||||||
<ol id="kc-totp-settings">
|
<ol id="kc-totp-settings">
|
||||||
<li>
|
<li>
|
||||||
@ -87,26 +86,26 @@ export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pa
|
|||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<form action={url.loginAction} className={getClassName("kcFormClass")} id="kc-totp-settings-form" method="post">
|
<form action={url.loginAction} className={kcClsx("kcFormClass")} id="kc-totp-settings-form" method="post">
|
||||||
<div className={getClassName("kcFormGroupClass")}>
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
<div className={getClassName("kcInputWrapperClass")}>
|
<div className={kcClsx("kcInputWrapperClass")}>
|
||||||
<label htmlFor="totp" className={getClassName("kcLabelClass")}>
|
<label htmlFor="totp" className={kcClsx("kcLabelClass")}>
|
||||||
{msg("authenticatorCode")}
|
{msg("authenticatorCode")}
|
||||||
</label>{" "}
|
</label>{" "}
|
||||||
<span className="required">*</span>
|
<span className="required">*</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={getClassName("kcInputWrapperClass")}>
|
<div className={kcClsx("kcInputWrapperClass")}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="totp"
|
id="totp"
|
||||||
name="totp"
|
name="totp"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className={getClassName("kcInputClass")}
|
className={kcClsx("kcInputClass")}
|
||||||
aria-invalid={messagesPerField.existsError("totp")}
|
aria-invalid={messagesPerField.existsError("totp")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{messagesPerField.existsError("totp") && (
|
{messagesPerField.existsError("totp") && (
|
||||||
<span id="input-error-otp-code" className={getClassName("kcInputErrorMessageClass")} aria-live="polite">
|
<span id="input-error-otp-code" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||||
{messagesPerField.get("totp")}
|
{messagesPerField.get("totp")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -115,54 +114,45 @@ export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pa
|
|||||||
{mode && <input type="hidden" id="mode" value={mode} />}
|
{mode && <input type="hidden" id="mode" value={mode} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={getClassName("kcFormGroupClass")}>
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
<div className={getClassName("kcInputWrapperClass")}>
|
<div className={kcClsx("kcInputWrapperClass")}>
|
||||||
<label htmlFor="userLabel" className={getClassName("kcLabelClass")}>
|
<label htmlFor="userLabel" className={kcClsx("kcLabelClass")}>
|
||||||
{msg("loginTotpDeviceName")}
|
{msg("loginTotpDeviceName")}
|
||||||
</label>{" "}
|
</label>{" "}
|
||||||
{totp.otpCredentials.length >= 1 && <span className="required">*</span>}
|
{totp.otpCredentials.length >= 1 && <span className="required">*</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className={getClassName("kcInputWrapperClass")}>
|
<div className={kcClsx("kcInputWrapperClass")}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="userLabel"
|
id="userLabel"
|
||||||
name="userLabel"
|
name="userLabel"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className={getClassName("kcInputClass")}
|
className={kcClsx("kcInputClass")}
|
||||||
aria-invalid={messagesPerField.existsError("userLabel")}
|
aria-invalid={messagesPerField.existsError("userLabel")}
|
||||||
/>
|
/>
|
||||||
{messagesPerField.existsError("userLabel") && (
|
{messagesPerField.existsError("userLabel") && (
|
||||||
<span id="input-error-otp-label" className={getClassName("kcInputErrorMessageClass")} aria-live="polite">
|
<span id="input-error-otp-label" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||||
{messagesPerField.get("userLabel")}
|
{messagesPerField.get("userLabel")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={getClassName("kcFormGroupClass")}>
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
<LogoutOtherSessions {...{ getClassName, i18n }} />
|
<LogoutOtherSessions kcClsx={kcClsx} i18n={i18n} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isAppInitiatedAction ? (
|
{isAppInitiatedAction ? (
|
||||||
<>
|
<>
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
className={clsx(
|
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonLargeClass")}
|
||||||
getClassName("kcButtonClass"),
|
|
||||||
getClassName("kcButtonPrimaryClass"),
|
|
||||||
getClassName("kcButtonLargeClass")
|
|
||||||
)}
|
|
||||||
id="saveTOTPBtn"
|
id="saveTOTPBtn"
|
||||||
value={msgStr("doSubmit")}
|
value={msgStr("doSubmit")}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={clsx(
|
className={kcClsx("kcButtonClass", "kcButtonDefaultClass", "kcButtonLargeClass", "kcButtonLargeClass")}
|
||||||
getClassName("kcButtonClass"),
|
|
||||||
getClassName("kcButtonDefaultClass"),
|
|
||||||
getClassName("kcButtonLargeClass"),
|
|
||||||
getClassName("kcButtonLargeClass")
|
|
||||||
)}
|
|
||||||
id="cancelTOTPBtn"
|
id="cancelTOTPBtn"
|
||||||
name="cancel-aia"
|
name="cancel-aia"
|
||||||
value="true"
|
value="true"
|
||||||
@ -173,7 +163,7 @@ export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pa
|
|||||||
) : (
|
) : (
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
className={clsx(getClassName("kcButtonClass"), getClassName("kcButtonPrimaryClass"), getClassName("kcButtonLargeClass"))}
|
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonLargeClass")}
|
||||||
id="saveTOTPBtn"
|
id="saveTOTPBtn"
|
||||||
value={msgStr("doSubmit")}
|
value={msgStr("doSubmit")}
|
||||||
/>
|
/>
|
||||||
@ -184,14 +174,14 @@ export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pa
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LogoutOtherSessions(props: { getClassName: ReturnType<typeof useGetClassName>["getClassName"]; i18n: I18n }) {
|
function LogoutOtherSessions(props: { kcClsx: KcClsx; i18n: I18n }) {
|
||||||
const { getClassName, i18n } = props;
|
const { kcClsx, i18n } = props;
|
||||||
|
|
||||||
const { msg } = i18n;
|
const { msg } = i18n;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
|
<div id="kc-form-options" className={kcClsx("kcFormOptionsClass")}>
|
||||||
<div className={getClassName("kcFormOptionsWrapperClass")}>
|
<div className={kcClsx("kcFormOptionsWrapperClass")}>
|
||||||
<div className="checkbox">
|
<div className="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="logout-sessions" name="logout-sessions" value="on" defaultChecked={true} />
|
<input type="checkbox" id="logout-sessions" name="logout-sessions" value="on" defaultChecked={true} />
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
import type { KcContext } from "../KcContext";
|
||||||
import type { KcContext } from "../kcContext";
|
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
export default function LoginIdpLinkConfirm(props: PageProps<Extract<KcContext, { pageId: "login-idp-link-confirm.ftl" }>, I18n>) {
|
export default function LoginIdpLinkConfirm(props: PageProps<Extract<KcContext, { pageId: "login-idp-link-confirm.ftl" }>, I18n>) {
|
||||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||||
|
|
||||||
const { getClassName } = useGetClassName({
|
const { kcClsx } = getKcClsx({
|
||||||
doUseDefaultCss,
|
doUseDefaultCss,
|
||||||
classes
|
classes
|
||||||
});
|
});
|
||||||
@ -17,17 +16,12 @@ export default function LoginIdpLinkConfirm(props: PageProps<Extract<KcContext,
|
|||||||
const { msg } = i18n;
|
const { msg } = i18n;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("confirmLinkIdpTitle")}>
|
<Template kcContext={kcContext} i18n={i18n} doUseDefaultCss={doUseDefaultCss} classes={classes} headerNode={msg("confirmLinkIdpTitle")}>
|
||||||
<form id="kc-register-form" action={url.loginAction} method="post">
|
<form id="kc-register-form" action={url.loginAction} method="post">
|
||||||
<div className={getClassName("kcFormGroupClass")}>
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={clsx(
|
className={kcClsx("kcButtonClass", "kcButtonDefaultClass", "kcButtonBlockClass", "kcButtonLargeClass")}
|
||||||
getClassName("kcButtonClass"),
|
|
||||||
getClassName("kcButtonDefaultClass"),
|
|
||||||
getClassName("kcButtonBlockClass"),
|
|
||||||
getClassName("kcButtonLargeClass")
|
|
||||||
)}
|
|
||||||
name="submitAction"
|
name="submitAction"
|
||||||
id="updateProfile"
|
id="updateProfile"
|
||||||
value="updateProfile"
|
value="updateProfile"
|
||||||
@ -36,12 +30,7 @@ export default function LoginIdpLinkConfirm(props: PageProps<Extract<KcContext,
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={clsx(
|
className={kcClsx("kcButtonClass", "kcButtonDefaultClass", "kcButtonBlockClass", "kcButtonLargeClass")}
|
||||||
getClassName("kcButtonClass"),
|
|
||||||
getClassName("kcButtonDefaultClass"),
|
|
||||||
getClassName("kcButtonBlockClass"),
|
|
||||||
getClassName("kcButtonLargeClass")
|
|
||||||
)}
|
|
||||||
name="submitAction"
|
name="submitAction"
|
||||||
id="linkAccount"
|
id="linkAccount"
|
||||||
value="linkAccount"
|
value="linkAccount"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { KcContext } from "keycloakify/login/kcContext";
|
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import type { I18n } from "keycloakify/login/i18n";
|
import type { KcContext } from "../KcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
export default function LoginIdpLinkEmail(props: PageProps<Extract<KcContext, { pageId: "login-idp-link-email.ftl" }>, I18n>) {
|
export default function LoginIdpLinkEmail(props: PageProps<Extract<KcContext, { pageId: "login-idp-link-email.ftl" }>, I18n>) {
|
||||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||||
@ -10,7 +10,13 @@ export default function LoginIdpLinkEmail(props: PageProps<Extract<KcContext, {
|
|||||||
const { msg } = i18n;
|
const { msg } = i18n;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("emailLinkIdpTitle", idpAlias)}>
|
<Template
|
||||||
|
kcContext={kcContext}
|
||||||
|
i18n={i18n}
|
||||||
|
doUseDefaultCss={doUseDefaultCss}
|
||||||
|
classes={classes}
|
||||||
|
headerNode={msg("emailLinkIdpTitle", idpAlias)}
|
||||||
|
>
|
||||||
<p id="instruction1" className="instruction">
|
<p id="instruction1" className="instruction">
|
||||||
{msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)}
|
{msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)}
|
||||||
</p>
|
</p>
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
import { I18n } from "../i18n";
|
import { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import { KcContext } from "../kcContext";
|
import { KcContext } from "../KcContext";
|
||||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
import type { I18n } from "../i18n";
|
||||||
import { PageProps } from "./PageProps";
|
|
||||||
|
|
||||||
export default function LoginOauth2DeviceVerifyUserCode(
|
export default function LoginOauth2DeviceVerifyUserCode(
|
||||||
props: PageProps<Extract<KcContext, { pageId: "login-oauth2-device-verify-user-code.ftl" }>, I18n>
|
props: PageProps<Extract<KcContext, { pageId: "login-oauth2-device-verify-user-code.ftl" }>, I18n>
|
||||||
@ -12,51 +11,53 @@ export default function LoginOauth2DeviceVerifyUserCode(
|
|||||||
|
|
||||||
const { msg, msgStr } = i18n;
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
const { getClassName } = useGetClassName({
|
const { kcClsx } = getKcClsx({
|
||||||
doUseDefaultCss,
|
doUseDefaultCss,
|
||||||
classes
|
classes
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("oauth2DeviceVerificationTitle")}>
|
<Template
|
||||||
|
kcContext={kcContext}
|
||||||
|
i18n={i18n}
|
||||||
|
doUseDefaultCss={doUseDefaultCss}
|
||||||
|
classes={classes}
|
||||||
|
headerNode={msg("oauth2DeviceVerificationTitle")}
|
||||||
|
>
|
||||||
<form
|
<form
|
||||||
id="kc-user-verify-device-user-code-form"
|
id="kc-user-verify-device-user-code-form"
|
||||||
className={getClassName("kcFormClass")}
|
className={kcClsx("kcFormClass")}
|
||||||
action={url.oauth2DeviceVerificationAction}
|
action={url.oauth2DeviceVerificationAction}
|
||||||
method="post"
|
method="post"
|
||||||
>
|
>
|
||||||
<div className={getClassName("kcFormGroupClass")}>
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
<div className={getClassName("kcLabelWrapperClass")}>
|
<div className={kcClsx("kcLabelWrapperClass")}>
|
||||||
<label htmlFor="device-user-code" className={getClassName("kcLabelClass")}>
|
<label htmlFor="device-user-code" className={kcClsx("kcLabelClass")}>
|
||||||
{msg("verifyOAuth2DeviceUserCode")}
|
{msg("verifyOAuth2DeviceUserCode")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={getClassName("kcInputWrapperClass")}>
|
<div className={kcClsx("kcInputWrapperClass")}>
|
||||||
<input
|
<input
|
||||||
id="device-user-code"
|
id="device-user-code"
|
||||||
name="device_user_code"
|
name="device_user_code"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
type="text"
|
type="text"
|
||||||
className={getClassName("kcInputClass")}
|
className={kcClsx("kcInputClass")}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={getClassName("kcFormGroupClass")}>
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
|
<div id="kc-form-options" className={kcClsx("kcFormOptionsClass")}>
|
||||||
<div className={getClassName("kcFormOptionsWrapperClass")}></div>
|
<div className={kcClsx("kcFormOptionsWrapperClass")}></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
|
<div id="kc-form-buttons" className={kcClsx("kcFormButtonsClass")}>
|
||||||
<div className={getClassName("kcFormButtonsWrapperClass")}>
|
<div className={kcClsx("kcFormButtonsWrapperClass")}>
|
||||||
<input
|
<input
|
||||||
className={clsx(
|
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonLargeClass")}
|
||||||
getClassName("kcButtonClass"),
|
|
||||||
getClassName("kcButtonPrimaryClass"),
|
|
||||||
getClassName("kcButtonLargeClass")
|
|
||||||
)}
|
|
||||||
type="submit"
|
type="submit"
|
||||||
value={msgStr("doSubmit")}
|
value={msgStr("doSubmit")}
|
||||||
/>
|
/>
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
import { PageProps } from "./PageProps";
|
import { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import { KcContext } from "../kcContext";
|
import { KcContext } from "../KcContext";
|
||||||
import { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
|
||||||
|
|
||||||
export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pageId: "login-oauth-grant.ftl" }>, I18n>) {
|
export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pageId: "login-oauth-grant.ftl" }>, I18n>) {
|
||||||
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
|
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
|
||||||
@ -10,14 +9,17 @@ export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pa
|
|||||||
|
|
||||||
const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;
|
const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;
|
||||||
|
|
||||||
const { getClassName } = useGetClassName({
|
const { kcClsx } = getKcClsx({
|
||||||
doUseDefaultCss,
|
doUseDefaultCss,
|
||||||
classes
|
classes
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Template
|
<Template
|
||||||
{...{ kcContext, i18n, doUseDefaultCss, classes }}
|
kcContext={kcContext}
|
||||||
|
i18n={i18n}
|
||||||
|
doUseDefaultCss={doUseDefaultCss}
|
||||||
|
classes={classes}
|
||||||
bodyClassName="oauth"
|
bodyClassName="oauth"
|
||||||
headerNode={
|
headerNode={
|
||||||
<>
|
<>
|
||||||
@ -68,30 +70,22 @@ export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pa
|
|||||||
|
|
||||||
<form className="form-actions" action={url.oauthAction} method="POST">
|
<form className="form-actions" action={url.oauthAction} method="POST">
|
||||||
<input type="hidden" name="code" value={oauth.code} />
|
<input type="hidden" name="code" value={oauth.code} />
|
||||||
<div className={getClassName("kcFormGroupClass")}>
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
<div id="kc-form-options">
|
<div id="kc-form-options">
|
||||||
<div className={getClassName("kcFormOptionsWrapperClass")}></div>
|
<div className={kcClsx("kcFormOptionsWrapperClass")}></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="kc-form-buttons">
|
<div id="kc-form-buttons">
|
||||||
<div className={getClassName("kcFormButtonsWrapperClass")}>
|
<div className={kcClsx("kcFormButtonsWrapperClass")}>
|
||||||
<input
|
<input
|
||||||
className={clsx(
|
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonLargeClass")}
|
||||||
getClassName("kcButtonClass"),
|
|
||||||
getClassName("kcButtonPrimaryClass"),
|
|
||||||
getClassName("kcButtonLargeClass")
|
|
||||||
)}
|
|
||||||
name="accept"
|
name="accept"
|
||||||
id="kc-login"
|
id="kc-login"
|
||||||
type="submit"
|
type="submit"
|
||||||
value={msgStr("doYes")}
|
value={msgStr("doYes")}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
className={clsx(
|
className={kcClsx("kcButtonClass", "kcButtonDefaultClass", "kcButtonLargeClass")}
|
||||||
getClassName("kcButtonClass"),
|
|
||||||
getClassName("kcButtonDefaultClass"),
|
|
||||||
getClassName("kcButtonLargeClass")
|
|
||||||
)}
|
|
||||||
name="cancel"
|
name="cancel"
|
||||||
id="kc-cancel"
|
id="kc-cancel"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
import type { KcContext } from "../KcContext";
|
||||||
import type { KcContext } from "../kcContext";
|
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "login-otp.ftl" }>, I18n>) {
|
export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "login-otp.ftl" }>, I18n>) {
|
||||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||||
|
|
||||||
const { getClassName } = useGetClassName({
|
const { kcClsx } = getKcClsx({
|
||||||
doUseDefaultCss,
|
doUseDefaultCss,
|
||||||
classes
|
classes
|
||||||
});
|
});
|
||||||
@ -19,30 +18,33 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Template
|
<Template
|
||||||
{...{ kcContext, i18n, doUseDefaultCss, classes }}
|
kcContext={kcContext}
|
||||||
|
i18n={i18n}
|
||||||
|
doUseDefaultCss={doUseDefaultCss}
|
||||||
|
classes={classes}
|
||||||
displayMessage={!messagesPerField.existsError("totp")}
|
displayMessage={!messagesPerField.existsError("totp")}
|
||||||
headerNode={msg("doLogIn")}
|
headerNode={msg("doLogIn")}
|
||||||
>
|
>
|
||||||
<form id="kc-otp-login-form" className={clsx(getClassName("kcFormClass"))} action={url.loginAction} method="post">
|
<form id="kc-otp-login-form" className={kcClsx("kcFormClass")} action={url.loginAction} method="post">
|
||||||
{otpLogin.userOtpCredentials.length > 1 && (
|
{otpLogin.userOtpCredentials.length > 1 && (
|
||||||
<div className={getClassName("kcFormGroupClass")}>
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
<div className={getClassName("kcInputWrapperClass")}>
|
<div className={kcClsx("kcInputWrapperClass")}>
|
||||||
{otpLogin.userOtpCredentials.map((otpCredential, index) => (
|
{otpLogin.userOtpCredentials.map((otpCredential, index) => (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
<input
|
<input
|
||||||
id={`kc-otp-credential-${index}`}
|
id={`kc-otp-credential-${index}`}
|
||||||
className={getClassName("kcLoginOTPListInputClass")}
|
className={kcClsx("kcLoginOTPListInputClass")}
|
||||||
type="radio"
|
type="radio"
|
||||||
name="selectedCredentialId"
|
name="selectedCredentialId"
|
||||||
value={otpCredential.id}
|
value={otpCredential.id}
|
||||||
defaultChecked={otpCredential.id === otpLogin.selectedCredentialId}
|
defaultChecked={otpCredential.id === otpLogin.selectedCredentialId}
|
||||||
/>
|
/>
|
||||||
<label htmlFor={`kc-otp-credential-${index}`} className={getClassName("kcLoginOTPListClass")} tabIndex={index}>
|
<label htmlFor={`kc-otp-credential-${index}`} className={kcClsx("kcLoginOTPListClass")} tabIndex={index}>
|
||||||
<span className={getClassName("kcLoginOTPListItemHeaderClass")}>
|
<span className={kcClsx("kcLoginOTPListItemHeaderClass")}>
|
||||||
<span className={getClassName("kcLoginOTPListItemIconBodyClass")}>
|
<span className={kcClsx("kcLoginOTPListItemIconBodyClass")}>
|
||||||
<i className={getClassName("kcLoginOTPListItemIconClass")} aria-hidden="true"></i>
|
<i className={kcClsx("kcLoginOTPListItemIconClass")} aria-hidden="true"></i>
|
||||||
</span>
|
</span>
|
||||||
<span className={getClassName("kcLoginOTPListItemTitleClass")}>{otpCredential.userLabel}</span>
|
<span className={kcClsx("kcLoginOTPListItemTitleClass")}>{otpCredential.userLabel}</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@ -51,42 +53,37 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={getClassName("kcFormGroupClass")}>
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
<div className={getClassName("kcLabelWrapperClass")}>
|
<div className={kcClsx("kcLabelWrapperClass")}>
|
||||||
<label htmlFor="otp" className={getClassName("kcLabelClass")}>
|
<label htmlFor="otp" className={kcClsx("kcLabelClass")}>
|
||||||
{msg("loginOtpOneTime")}
|
{msg("loginOtpOneTime")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className={getClassName("kcInputWrapperClass")}>
|
<div className={kcClsx("kcInputWrapperClass")}>
|
||||||
<input
|
<input
|
||||||
id="otp"
|
id="otp"
|
||||||
name="otp"
|
name="otp"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
type="text"
|
type="text"
|
||||||
className={getClassName("kcInputClass")}
|
className={kcClsx("kcInputClass")}
|
||||||
autoFocus
|
autoFocus
|
||||||
aria-invalid={messagesPerField.existsError("totp")}
|
aria-invalid={messagesPerField.existsError("totp")}
|
||||||
/>
|
/>
|
||||||
{messagesPerField.existsError("totp") && (
|
{messagesPerField.existsError("totp") && (
|
||||||
<span id="input-error-otp-code" className={getClassName("kcInputErrorMessageClass")} aria-live="polite">
|
<span id="input-error-otp-code" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||||
{messagesPerField.get("totp")}
|
{messagesPerField.get("totp")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={getClassName("kcFormGroupClass")}>
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
|
<div id="kc-form-options" className={kcClsx("kcFormOptionsClass")}>
|
||||||
<div className={getClassName("kcFormOptionsWrapperClass")}></div>
|
<div className={kcClsx("kcFormOptionsWrapperClass")}></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
|
<div id="kc-form-buttons" className={kcClsx("kcFormButtonsClass")}>
|
||||||
<input
|
<input
|
||||||
className={clsx(
|
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonBlockClass", "kcButtonLargeClass")}
|
||||||
getClassName("kcButtonClass"),
|
|
||||||
getClassName("kcButtonPrimaryClass"),
|
|
||||||
getClassName("kcButtonBlockClass"),
|
|
||||||
getClassName("kcButtonLargeClass")
|
|
||||||
)}
|
|
||||||
name="login"
|
name="login"
|
||||||
id="kc-login"
|
id="kc-login"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user