Implement a mechanism to overload kcContext

This commit is contained in:
garronej
2021-06-28 04:04:48 +02:00
parent e160882db9
commit eac28f97b8
25 changed files with 604 additions and 48 deletions

View File

@ -1,8 +1,6 @@
import { memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import { assert } from "../tools/assert";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage";
@ -10,8 +8,6 @@ export const Error = memo(({ kcContext, ...props }: { kcContext: KcContextBase.E
const { msg } = useKcMessage();
assert(kcContext.message !== undefined);
const { message, client } = kcContext;
return (

View File

@ -1,6 +1,6 @@
import { allPropertiesValuesToUndefined } from "../tools/allPropertiesValuesToUndefined";
import { doExtends } from "evt/tools/typeSafety/doExtends";
import { doExtends } from "tsafe/doExtends";
/** Class names can be provided as an array or separated by whitespace */
export type KcPropsGeneric<CssClasses extends string> = { [key in CssClasses]: readonly string[] | string | undefined; };

View File

@ -1,7 +1,7 @@
import type { PageId } from "../../bin/build-keycloak-theme/generateFtl";
import type { KcLanguageTag } from "../i18n/KcLanguageTag";
import { doExtends } from "evt/tools/typeSafety/doExtends";
import { doExtends } from "tsafe/doExtends";
import type { MessageKey } from "../i18n/useKcMessage";
import type { LanguageLabel } from "../i18n/KcLanguageTag";
@ -150,7 +150,8 @@ export declare namespace KcContextBase {
pageId: "error.ftl";
client?: {
baseUrl?: string;
}
},
message: NonNullable<Common["message"]>;
};
export type LoginResetPassword = Common & {

View File

@ -1,28 +1,93 @@
import type { KcContextBase } from "./KcContextBase";
import type { KcContextBase } from "./KcContextBase";
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
import { ftlValuesGlobalName } from "../../bin/build-keycloak-theme/ftlValuesGlobalName";
import type { AndByDiscriminatingKey } from "../tools/AndByDiscriminatingKey";
import type { DeepPartial } from "../tools/DeepPartial";
import { deepAssign } from "../tools/deepAssign";
export function getKcContext<KcContextExtended extends { pageId: string; } = never>(
export type ExtendsKcContextBase<
KcContextExtended extends ({ pageId: string; } | undefined)
> =
KcContextExtended extends undefined ?
KcContextBase :
AndByDiscriminatingKey<
"pageId",
KcContextExtended & KcContextBase.Common,
KcContextBase
>;
export function getKcContext<KcContextExtended extends ({ pageId: string; } | undefined) = undefined>(
params?: {
mockPageId?: KcContextBase["pageId"] | KcContextExtended["pageId"];
kcContextExtendedMock?: KcContextExtended[];
mockPageId?: ExtendsKcContextBase<KcContextExtended>["pageId"];
mockData?: readonly DeepPartial<ExtendsKcContextBase<KcContextExtended>>[];
}
): { kcContext: (KcContextBase | KcContextExtended & KcContextBase.Common) | undefined; } {
): { kcContext: ExtendsKcContextBase<KcContextExtended> | undefined; } {
const { mockPageId, kcContextExtendedMock } = params ?? { "mockPageId": false };
const {
mockPageId,
mockData
} = params ?? {};
if (mockPageId !== undefined) {
return {
"pageId": mockPageId,
...(kcContextMocks.find(({ pageId }) => pageId === mockPageId) ?? kcContextCommonMock),
...(kcContextExtendedMock?.find(({ pageId }) => pageId === mockPageId) ?? {})
} as any;
//TODO maybe trow if no mock fo custom page
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
const partialKcContextCustomMock = mockData?.find(({ pageId }) => pageId === mockPageId);
if (
kcContextDefaultMock === undefined &&
partialKcContextCustomMock === undefined
) {
console.warn([
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
`Please check the documentation of the getKcContext function`
].join("\n"));
}
const kcContext: any = { "pageId": mockPageId };
deepAssign({
"target": kcContext,
"source": kcContextCommonMock
});
if (kcContextDefaultMock !== undefined) {
deepAssign({
"target": kcContext,
"source": kcContextDefaultMock
});
}
if (partialKcContextCustomMock !== undefined) {
deepAssign({
"target": kcContext,
"source": partialKcContextCustomMock
});
}
return { kcContext };
}
return (window as any)[ftlValuesGlobalName];
return {
"kcContext":
typeof window === "undefined" ?
undefined :
(window as any)[ftlValuesGlobalName]
};
}

View File

@ -5,12 +5,15 @@ import { getKcLanguageTagLabel } from "../../i18n/KcLanguageTag";
//NOTE: Aside because we want to be able to import them from node
import { resourcesCommonPath, resourcesPath } from "./urlResourcesPath";
import { id } from "tsafe/id";
import { join as pathJoin } from "path";
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
export const kcContextCommonMock: KcContextBase.Common = {
"url": {
"loginAction": "#",
"resourcesPath": `${process.env["PUBLIC_URL"]}/${resourcesPath}`,
"resourcesCommonPath": `${process.env["PUBLIC_URL"]}/${resourcesCommonPath}`,
"resourcesPath": pathJoin(PUBLIC_URL, resourcesPath),
"resourcesCommonPath": pathJoin(PUBLIC_URL, resourcesCommonPath),
"loginRestartFlowUrl": "/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg",
"loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg",
},
@ -95,7 +98,8 @@ export const kcContextCommonMock: KcContextBase.Common = {
"languageTag": "tr"
}
],
"current": null as any
//"current": null as any
"current": "English"
},
"auth": {
"showUsername": false,
@ -110,6 +114,7 @@ export const kcContextCommonMock: KcContextBase.Common = {
"isAppInitiatedAction": false,
};
Object.defineProperty(
kcContextCommonMock.locale!,
"current",
@ -188,6 +193,10 @@ export const kcContextMocks: KcContextBase[] = [
"pageId": "error.ftl",
"client": {
"baseUrl": "#"
},
"message": {
"type": "error",
"summary": "This is the error message"
}
}),
id<KcContextBase.LoginResetPassword>({

View File

@ -0,0 +1,43 @@
import { KcContextBase } from "./KcContextBase";
import type { AndByDiscriminatingKey } from "../tools/AndByDiscriminatingKey";
export type ExtendsKcContextBase<KcContextExtended extends { pageId: string; }>=
AndByDiscriminatingKey<
"pageId",
KcContextExtended & KcContextBase.Common,
KcContextBase
>;
type KcContextExtended =
{ pageId: "register.ftl"; authorizedMailDomains: string[]; } |
{ pageId: "my-extra-page-1.ftl"; } |
{ pageId: "my-extra-page-2.ftl"; someCustomValue: string; };
const y: ExtendsKcContextBase<KcContextExtended> = null as any;
if (y.pageId === "register.ftl") {
y.authorizedMailDomains;
y.realm.displayName;
y.register
}
if (y.pageId === "my-extra-page-1.ftl") {
y.realm.displayName;
}
if (y.pageId === "my-extra-page-2.ftl") {
y.realm
y.someCustomValue
}

View File

@ -1,5 +1,5 @@
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
import { objectKeys } from "tsafe/objectKeys";
import { kcMessages } from "./kcMessages/login";
export type KcLanguageTag = keyof typeof kcMessages;

View File

@ -1,7 +1,7 @@
import { kcMessages } from "../generated_kcMessages/login";
import { Evt } from "evt";
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
import { objectKeys } from "tsafe/objectKeys";
export const evtTermsUpdated = Evt.asNonPostable(Evt.create<void>());

View File

@ -2,22 +2,39 @@
import { createUseGlobalState } from "powerhooks";
import { getKcContext } from "../getKcContext";
import { getBestMatchAmongKcLanguageTag } from "./KcLanguageTag";
import type { StatefulEvt } from "powerhooks";
import { KcLanguageTag } from "./KcLanguageTag";
const { kcContext } = getKcContext();
//export const { useKcLanguageTag, evtKcLanguageTag } = createUseGlobalState(
const wrap = createUseGlobalState(
"kcLanguageTag",
() => getBestMatchAmongKcLanguageTag(
kcContext?.locale?.current ??
navigator.language
),
() => {
const { kcContext } = getKcContext();
const languageLike =
kcContext?.locale?.current ??
(
typeof navigator === "undefined" ?
undefined :
navigator.language
);
if (languageLike === undefined) {
return "en";
}
return getBestMatchAmongKcLanguageTag(languageLike);
},
{ "persistance": "localStorage" }
);
export const { useKcLanguageTag } = wrap;
export function getEvtKcLanguage() {
export function getEvtKcLanguage(): StatefulEvt<KcLanguageTag> {
return wrap.evtKcLanguageTag;
}

View File

@ -0,0 +1,35 @@
export type AndByDiscriminatingKey<
DiscriminatingKey extends string,
U1 extends Record<DiscriminatingKey, string>,
U2 extends Record<DiscriminatingKey, string>
> =
AndByDiscriminatingKey.Tf1<DiscriminatingKey, U1, U1, U2>;
export declare namespace AndByDiscriminatingKey {
export type Tf1<
DiscriminatingKey extends string,
U1,
U1Again extends Record<DiscriminatingKey, string>,
U2 extends Record<DiscriminatingKey, string>
> =
U1 extends Pick<U2, DiscriminatingKey> ?
Tf2<DiscriminatingKey, U1, U2, U1Again> :
U1;
export type Tf2<
DiscriminatingKey extends string,
SingletonU1 extends Record<DiscriminatingKey, string>,
U2,
U1 extends Record<DiscriminatingKey, string>
> =
U2 extends Pick<SingletonU1, DiscriminatingKey> ?
U2 & SingletonU1 :
U2 extends Pick<U1, DiscriminatingKey> ?
never :
U2;
}

4
src/lib/tools/DeepPartial.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
export type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};

View File

@ -1,2 +1,2 @@
export { assert } from "evt/tools/typeSafety/assert";
export { assert } from "tsafe/assert";

View File

@ -0,0 +1,56 @@
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
export function deepAssign(
params: {
target: Record<string, unknown>;
source: Record<string, unknown>;
}
) {
const { target, source } = params;
Object.keys(source).forEach(key => {
var dereferencedSource = source[key];
if (
target[key] === undefined ||
!(dereferencedSource instanceof Object)
) {
Object.defineProperty(
target,
key,
{
"enumerable": true,
"value": dereferencedSource
}
);
return;
}
const dereferencedTarget = target[key];
if (dereferencedSource instanceof Array) {
assert(is<unknown[]>(dereferencedTarget));
assert(is<unknown[]>(dereferencedSource));
dereferencedSource.forEach(entry => dereferencedTarget.push(entry));
return;
}
assert(is<Record<string, unknown>>(dereferencedTarget));
assert(is<Record<string, unknown>>(dereferencedSource));
deepAssign({
"target": dereferencedTarget,
"source": dereferencedSource
});
});
}

View File

@ -0,0 +1,4 @@
export function deepClone<T>(arg: T): T {
return JSON.parse(JSON.stringify(arg));
}