complete decoupling of user profile form validation logic
This commit is contained in:
parent
5892cf2ba7
commit
36dd324139
@ -1,133 +0,0 @@
|
|||||||
import "keycloakify/tools/Array.prototype.every";
|
|
||||||
import { assert } from "tsafe/assert";
|
|
||||||
import type {
|
|
||||||
PasswordPolicies,
|
|
||||||
Attribute,
|
|
||||||
Validators
|
|
||||||
} from "keycloakify/login/KcContext";
|
|
||||||
import type { KcContext } from "../KcContext";
|
|
||||||
import type { KcContextLike as KcContextLike_i18n } from "keycloakify/login/i18n";
|
|
||||||
|
|
||||||
export type FormFieldError = {
|
|
||||||
advancedMsgArgs: [string, ...string[]];
|
|
||||||
source: FormFieldError.Source;
|
|
||||||
fieldIndex: number | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export namespace FormFieldError {
|
|
||||||
export type Source =
|
|
||||||
| Source.Validator
|
|
||||||
| Source.PasswordPolicy
|
|
||||||
| Source.Server
|
|
||||||
| Source.Other;
|
|
||||||
|
|
||||||
export namespace Source {
|
|
||||||
export type Validator = {
|
|
||||||
type: "validator";
|
|
||||||
name: keyof Validators;
|
|
||||||
};
|
|
||||||
export type PasswordPolicy = {
|
|
||||||
type: "passwordPolicy";
|
|
||||||
name: keyof PasswordPolicies;
|
|
||||||
};
|
|
||||||
export type Server = {
|
|
||||||
type: "server";
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Other = {
|
|
||||||
type: "other";
|
|
||||||
rule: "passwordConfirmMatchesPassword" | "requiredField";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FormFieldState = {
|
|
||||||
attribute: Attribute;
|
|
||||||
displayableErrors: FormFieldError[];
|
|
||||||
valueOrValues: string | string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FormState = {
|
|
||||||
isFormSubmittable: boolean;
|
|
||||||
formFieldStates: FormFieldState[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FormAction =
|
|
||||||
| {
|
|
||||||
action: "update";
|
|
||||||
name: string;
|
|
||||||
valueOrValues: string | string[];
|
|
||||||
/** Default false */
|
|
||||||
displayErrorsImmediately?: boolean;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
action: "focus lost";
|
|
||||||
name: string;
|
|
||||||
fieldIndex: number | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type KcContextLike = KcContextLike_i18n &
|
|
||||||
KcContextLike_useGetErrors & {
|
|
||||||
profile: {
|
|
||||||
attributesByName: Record<string, Attribute>;
|
|
||||||
html5DataAnnotations?: Record<string, string>;
|
|
||||||
};
|
|
||||||
passwordRequired?: boolean;
|
|
||||||
realm: { registrationEmailAsUsername: boolean };
|
|
||||||
url: {
|
|
||||||
resourcesPath: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type KcContextLike_useGetErrors = KcContextLike_i18n & {
|
|
||||||
messagesPerField: Pick<KcContext["messagesPerField"], "existsError" | "get">;
|
|
||||||
passwordPolicies?: PasswordPolicies;
|
|
||||||
};
|
|
||||||
|
|
||||||
assert<
|
|
||||||
Extract<
|
|
||||||
Extract<KcContext, { profile: unknown }>,
|
|
||||||
{ pageId: "register.ftl" }
|
|
||||||
> extends KcContextLike
|
|
||||||
? true
|
|
||||||
: false
|
|
||||||
>();
|
|
||||||
|
|
||||||
export type UserProfileApi = {
|
|
||||||
getFormState: () => FormState;
|
|
||||||
subscribeToFormState: (callback: () => void) => { unsubscribe: () => void };
|
|
||||||
dispatchFormAction: (action: FormAction) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const cachedUserProfileApiByKcContext = new WeakMap<KcContextLike, UserProfileApi>();
|
|
||||||
|
|
||||||
export type ParamsOfGetUserProfileApi = {
|
|
||||||
kcContext: KcContextLike;
|
|
||||||
doMakeUserConfirmPassword: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getUserProfileApi(params: ParamsOfGetUserProfileApi): UserProfileApi {
|
|
||||||
const { kcContext } = params;
|
|
||||||
|
|
||||||
use_cache: {
|
|
||||||
const userProfileApi_cache = cachedUserProfileApiByKcContext.get(kcContext);
|
|
||||||
|
|
||||||
if (userProfileApi_cache === undefined) {
|
|
||||||
break use_cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
return userProfileApi_cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userProfileApi = getUserProfileApi_noCache(params);
|
|
||||||
|
|
||||||
cachedUserProfileApiByKcContext.set(kcContext, userProfileApi);
|
|
||||||
|
|
||||||
return userProfileApi;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUserProfileApi_noCache(
|
|
||||||
params: ParamsOfGetUserProfileApi
|
|
||||||
): UserProfileApi {
|
|
||||||
return null as any;
|
|
||||||
}
|
|
1561
src/login/lib/getUserProfileApi/getUserProfileApi.ts
Normal file
1561
src/login/lib/getUserProfileApi/getUserProfileApi.ts
Normal file
File diff suppressed because it is too large
Load Diff
1
src/login/lib/getUserProfileApi/index.ts
Normal file
1
src/login/lib/getUserProfileApi/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./getUserProfileApi";
|
109
src/login/lib/getUserProfileApi/kcNumberUnFormat.ts
Normal file
109
src/login/lib/getUserProfileApi/kcNumberUnFormat.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { assert } from "keycloakify/tools/assert";
|
||||||
|
let cleanup: (() => void) | undefined;
|
||||||
|
const handledElements = new WeakSet<HTMLElement>();
|
||||||
|
const KC_NUMBER_UNFORMAT = "kcNumberUnFormat";
|
||||||
|
const SELECTOR = `input[data-${KC_NUMBER_UNFORMAT}]`;
|
||||||
|
|
||||||
|
export function unFormatNumberOnSubmit() {
|
||||||
|
cleanup?.();
|
||||||
|
|
||||||
|
const handleElement = (element: HTMLInputElement) => {
|
||||||
|
if (handledElements.has(element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = element.closest("form");
|
||||||
|
|
||||||
|
if (form === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener("submit", () => {
|
||||||
|
const rawFormat = element.getAttribute(`data-${KC_NUMBER_UNFORMAT}`);
|
||||||
|
if (rawFormat) {
|
||||||
|
element.value = formatNumber(element.value, rawFormat);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
handledElements.add(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.querySelectorAll(SELECTOR).forEach(element => {
|
||||||
|
assert(element instanceof HTMLInputElement);
|
||||||
|
handleElement(element);
|
||||||
|
});
|
||||||
|
|
||||||
|
const observer = new MutationObserver(mutationsList => {
|
||||||
|
for (const mutation of mutationsList) {
|
||||||
|
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
|
||||||
|
mutation.addedNodes.forEach(node => {
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
const element = (node as HTMLElement).querySelector(SELECTOR);
|
||||||
|
if (element !== null) {
|
||||||
|
assert(element instanceof HTMLInputElement);
|
||||||
|
handleElement(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
cleanup = () => observer.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Keycloak code
|
||||||
|
const formatNumber = (input: string, format: string) => {
|
||||||
|
if (!input) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// array holding the patterns for the number of expected digits in each part
|
||||||
|
const digitPattern = format.match(/{\d+}/g);
|
||||||
|
|
||||||
|
if (!digitPattern) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate the maximum size of the given pattern based on the sum of the expected digits
|
||||||
|
const maxSize = digitPattern.reduce(
|
||||||
|
(total, p) => total + parseInt(p.replace("{", "").replace("}", "")),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
// keep only digits
|
||||||
|
let rawValue = input.replace(/\D+/g, "");
|
||||||
|
|
||||||
|
// make sure the value is a number
|
||||||
|
//@ts-expect-error
|
||||||
|
if (parseInt(rawValue) != rawValue) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure the number of digits does not exceed the maximum size
|
||||||
|
if (rawValue.length > maxSize) {
|
||||||
|
rawValue = rawValue.substring(0, maxSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the regex based based on the expected digits in each part
|
||||||
|
const formatter = digitPattern.reduce((result, p) => result + `(\\d${p})`, "^");
|
||||||
|
|
||||||
|
// if the current digits match the pattern we have each group of digits in an array
|
||||||
|
let digits = new RegExp(formatter).exec(rawValue);
|
||||||
|
|
||||||
|
// no match, return the raw value without any format
|
||||||
|
if (!digits) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = format;
|
||||||
|
|
||||||
|
// finally format the current digits accordingly to the given format
|
||||||
|
for (let i = 0; i < digitPattern.length; i++) {
|
||||||
|
result = result.replace(digitPattern[i], digits[i + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,9 @@
|
|||||||
import * as reactlessApi from "./getUserProfileApi";
|
import * as reactlessApi from "./getUserProfileApi/index";
|
||||||
import type { PasswordPolicies, Attribute, Validators } from "keycloakify/login/KcContext";
|
import type { PasswordPolicies, Attribute, Validators } from "keycloakify/login/KcContext";
|
||||||
import { useEffect, useState, useMemo, Fragment } from "react";
|
import { useEffect, useState, useMemo, Fragment } from "react";
|
||||||
import { assert, type Equals } from "tsafe/assert";
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
|
export { getButtonToDisplayForMultivaluedAttributeField } from "./getUserProfileApi/index";
|
||||||
|
|
||||||
export type FormFieldError = {
|
export type FormFieldError = {
|
||||||
errorMessage: JSX.Element;
|
errorMessage: JSX.Element;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user