Fully retrocompatible, factorized Register page 🚀
This commit is contained in:
parent
3abb32ec82
commit
d902859b00
@ -1,8 +1,7 @@
|
|||||||
import { useEffect, Fragment } from "react";
|
import { useEffect, Fragment } from "react";
|
||||||
import type { ClassKey } from "keycloakify/login/TemplateProps";
|
import type { ClassKey } from "keycloakify/login/TemplateProps";
|
||||||
import { clsx } from "keycloakify/tools/clsx";
|
|
||||||
import { useUserProfileForm, type KcContextLike, type FormAction, type FormFieldError } from "keycloakify/login/lib/useUserProfileForm";
|
import { useUserProfileForm, type KcContextLike, type FormAction, type FormFieldError } from "keycloakify/login/lib/useUserProfileForm";
|
||||||
import type { Attribute, LegacyAttribute } from "keycloakify/login/kcContext/KcContext";
|
import type { Attribute } from "keycloakify/login/kcContext/KcContext";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import type { I18n } from "./i18n";
|
import type { I18n } from "./i18n";
|
||||||
|
|
||||||
@ -49,20 +48,9 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps)
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{formFieldStates.map(({ attribute, displayableErrors, valueOrValues }) => {
|
{formFieldStates.map(({ attribute, displayableErrors, valueOrValues }) => {
|
||||||
const formGroupClassName = clsx(
|
|
||||||
getClassName("kcFormGroupClass"),
|
|
||||||
displayableErrors.length !== 0 && getClassName("kcFormGroupErrorClass")
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={attribute.name}>
|
<Fragment key={attribute.name}>
|
||||||
<GroupLabel
|
<GroupLabel attribute={attribute} getClassName={getClassName} i18n={i18n} groupNameRef={groupNameRef} />
|
||||||
attribute={attribute}
|
|
||||||
getClassName={getClassName}
|
|
||||||
i18n={i18n}
|
|
||||||
groupNameRef={groupNameRef}
|
|
||||||
formGroupClassName={formGroupClassName}
|
|
||||||
/>
|
|
||||||
{BeforeField !== undefined && (
|
{BeforeField !== undefined && (
|
||||||
<BeforeField
|
<BeforeField
|
||||||
attribute={attribute}
|
attribute={attribute}
|
||||||
@ -73,7 +61,7 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps)
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={formGroupClassName}
|
className={getClassName("kcFormGroupClass")}
|
||||||
style={{ "display": attribute.name === "password-confirm" && !doMakeUserConfirmPassword ? "none" : undefined }}
|
style={{ "display": attribute.name === "password-confirm" && !doMakeUserConfirmPassword ? "none" : undefined }}
|
||||||
>
|
>
|
||||||
<div className={getClassName("kcLabelWrapperClass")}>
|
<div className={getClassName("kcLabelWrapperClass")}>
|
||||||
@ -142,41 +130,11 @@ function GroupLabel(props: {
|
|||||||
groupNameRef: {
|
groupNameRef: {
|
||||||
current: string;
|
current: string;
|
||||||
};
|
};
|
||||||
formGroupClassName: string;
|
|
||||||
}) {
|
}) {
|
||||||
const { attribute, getClassName, i18n, groupNameRef, formGroupClassName } = props;
|
const { attribute, getClassName, i18n, groupNameRef } = props;
|
||||||
|
|
||||||
const { advancedMsg } = i18n;
|
const { advancedMsg } = i18n;
|
||||||
|
|
||||||
keycloak_prior_to_24: {
|
|
||||||
if (attribute.html5DataAnnotations !== undefined) {
|
|
||||||
break keycloak_prior_to_24;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute as any as LegacyAttribute;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{group !== groupNameRef.current && (groupNameRef.current = group) !== "" && (
|
|
||||||
<div className={formGroupClassName}>
|
|
||||||
<div className={getClassName("kcContentWrapperClass")}>
|
|
||||||
<label id={`header-${group}`} className={getClassName("kcFormGroupHeader")}>
|
|
||||||
{advancedMsg(groupDisplayHeader) || groupNameRef.current}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{groupDisplayDescription !== "" && (
|
|
||||||
<div className={getClassName("kcLabelWrapperClass")}>
|
|
||||||
<label id={`description-${group}`} className={getClassName("kcLabelClass")}>
|
|
||||||
{advancedMsg(groupDisplayDescription)}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attribute.group?.name !== groupNameRef.current) {
|
if (attribute.group?.name !== groupNameRef.current) {
|
||||||
groupNameRef.current = attribute.group?.name ?? "";
|
groupNameRef.current = attribute.group?.name ?? "";
|
||||||
|
|
||||||
|
@ -193,7 +193,7 @@ export declare namespace KcContext {
|
|||||||
profile: {
|
profile: {
|
||||||
attributes: Attribute[];
|
attributes: Attribute[];
|
||||||
attributesByName: Record<string, Attribute>;
|
attributesByName: Record<string, Attribute>;
|
||||||
html5DataAnnotations: Record<string, string>;
|
html5DataAnnotations?: Record<string, string>;
|
||||||
};
|
};
|
||||||
url: {
|
url: {
|
||||||
registrationAction: string;
|
registrationAction: string;
|
||||||
@ -450,8 +450,8 @@ export declare namespace KcContext {
|
|||||||
export type UpdateUserProfile = Common & {
|
export type UpdateUserProfile = Common & {
|
||||||
pageId: "update-user-profile.ftl";
|
pageId: "update-user-profile.ftl";
|
||||||
profile: {
|
profile: {
|
||||||
attributes: LegacyAttribute[];
|
attributes: Attribute[];
|
||||||
attributesByName: Record<string, LegacyAttribute>;
|
attributesByName: Record<string, Attribute>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -459,8 +459,8 @@ export declare namespace KcContext {
|
|||||||
pageId: "idp-review-user-profile.ftl";
|
pageId: "idp-review-user-profile.ftl";
|
||||||
profile: {
|
profile: {
|
||||||
context: "IDP_REVIEW";
|
context: "IDP_REVIEW";
|
||||||
attributes: LegacyAttribute[];
|
attributes: Attribute[];
|
||||||
attributesByName: Record<string, LegacyAttribute>;
|
attributesByName: Record<string, Attribute>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -517,7 +517,7 @@ export type Attribute = {
|
|||||||
name: string;
|
name: string;
|
||||||
displayDescription?: string;
|
displayDescription?: string;
|
||||||
};
|
};
|
||||||
html5DataAnnotations: {
|
html5DataAnnotations?: {
|
||||||
kcNumberFormat?: string;
|
kcNumberFormat?: string;
|
||||||
kcNumberUnFormat?: string;
|
kcNumberUnFormat?: string;
|
||||||
};
|
};
|
||||||
@ -599,13 +599,6 @@ export type Attribute = {
|
|||||||
| "photo";
|
| "photo";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LegacyAttribute = Omit<Attribute, "group" | "html5DataAnnotations"> & {
|
|
||||||
group: string;
|
|
||||||
groupDisplayHeader?: string;
|
|
||||||
groupDisplayDescription?: string;
|
|
||||||
groupAnnotations: Record<string, string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Validators = Partial<{
|
export type Validators = Partial<{
|
||||||
length: Validators.DoIgnoreEmpty & Validators.Range;
|
length: Validators.DoIgnoreEmpty & Validators.Range;
|
||||||
integer: Validators.DoIgnoreEmpty & Validators.Range;
|
integer: Validators.DoIgnoreEmpty & Validators.Range;
|
||||||
|
182
src/login/kcContext/register.tsx
Normal file
182
src/login/kcContext/register.tsx
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
|
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
||||||
|
import type { KcContext } from "../kcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
export default function Register(props: PageProps<Extract<KcContext, { pageId: "register.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||||
|
|
||||||
|
const { getClassName } = useGetClassName({
|
||||||
|
doUseDefaultCss,
|
||||||
|
classes
|
||||||
|
});
|
||||||
|
|
||||||
|
const { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
||||||
|
|
||||||
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("registerTitle")}>
|
||||||
|
<form id="kc-register-form" className={getClassName("kcFormClass")} action={url.registrationAction} method="post">
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
getClassName("kcFormGroupClass"),
|
||||||
|
messagesPerField.printIfExists("firstName", getClassName("kcFormGroupErrorClass"))
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={getClassName("kcLabelWrapperClass")}>
|
||||||
|
<label htmlFor="firstName" className={getClassName("kcLabelClass")}>
|
||||||
|
{msg("firstName")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={getClassName("kcInputWrapperClass")}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="firstName"
|
||||||
|
className={getClassName("kcInputClass")}
|
||||||
|
name="firstName"
|
||||||
|
defaultValue={register.formData.firstName ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
getClassName("kcFormGroupClass"),
|
||||||
|
messagesPerField.printIfExists("lastName", getClassName("kcFormGroupErrorClass"))
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={getClassName("kcLabelWrapperClass")}>
|
||||||
|
<label htmlFor="lastName" className={getClassName("kcLabelClass")}>
|
||||||
|
{msg("lastName")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={getClassName("kcInputWrapperClass")}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="lastName"
|
||||||
|
className={getClassName("kcInputClass")}
|
||||||
|
name="lastName"
|
||||||
|
defaultValue={register.formData.lastName ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={clsx(getClassName("kcFormGroupClass"), messagesPerField.printIfExists("email", getClassName("kcFormGroupErrorClass")))}
|
||||||
|
>
|
||||||
|
<div className={getClassName("kcLabelWrapperClass")}>
|
||||||
|
<label htmlFor="email" className={getClassName("kcLabelClass")}>
|
||||||
|
{msg("email")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={getClassName("kcInputWrapperClass")}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="email"
|
||||||
|
className={getClassName("kcInputClass")}
|
||||||
|
name="email"
|
||||||
|
defaultValue={register.formData.email ?? ""}
|
||||||
|
autoComplete="email"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!realm.registrationEmailAsUsername && (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
getClassName("kcFormGroupClass"),
|
||||||
|
messagesPerField.printIfExists("username", getClassName("kcFormGroupErrorClass"))
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={getClassName("kcLabelWrapperClass")}>
|
||||||
|
<label htmlFor="username" className={getClassName("kcLabelClass")}>
|
||||||
|
{msg("username")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={getClassName("kcInputWrapperClass")}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="username"
|
||||||
|
className={getClassName("kcInputClass")}
|
||||||
|
name="username"
|
||||||
|
defaultValue={register.formData.username ?? ""}
|
||||||
|
autoComplete="username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{passwordRequired && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
getClassName("kcFormGroupClass"),
|
||||||
|
messagesPerField.printIfExists("password", getClassName("kcFormGroupErrorClass"))
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={getClassName("kcLabelWrapperClass")}>
|
||||||
|
<label htmlFor="password" className={getClassName("kcLabelClass")}>
|
||||||
|
{msg("password")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={getClassName("kcInputWrapperClass")}>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
className={getClassName("kcInputClass")}
|
||||||
|
name="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
getClassName("kcFormGroupClass"),
|
||||||
|
messagesPerField.printIfExists("password-confirm", getClassName("kcFormGroupErrorClass"))
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={getClassName("kcLabelWrapperClass")}>
|
||||||
|
<label htmlFor="password-confirm" className={getClassName("kcLabelClass")}>
|
||||||
|
{msg("passwordConfirm")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={getClassName("kcInputWrapperClass")}>
|
||||||
|
<input type="password" id="password-confirm" className={getClassName("kcInputClass")} name="password-confirm" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{recaptchaRequired && (
|
||||||
|
<div className="form-group">
|
||||||
|
<div className={getClassName("kcInputWrapperClass")}>
|
||||||
|
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={getClassName("kcFormGroupClass")}>
|
||||||
|
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
|
||||||
|
<div className={getClassName("kcFormOptionsWrapperClass")}>
|
||||||
|
<span>
|
||||||
|
<a href={url.loginUrl}>{msg("backToLogin")}</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
|
||||||
|
<input
|
||||||
|
className={clsx(
|
||||||
|
getClassName("kcButtonClass"),
|
||||||
|
getClassName("kcButtonPrimaryClass"),
|
||||||
|
getClassName("kcButtonBlockClass"),
|
||||||
|
getClassName("kcButtonLargeClass")
|
||||||
|
)}
|
||||||
|
type="submit"
|
||||||
|
value={msgStr("doRegister")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Template>
|
||||||
|
);
|
||||||
|
}
|
@ -68,7 +68,7 @@ export type KcContextLike = {
|
|||||||
messagesPerField: Pick<KcContext.Common["messagesPerField"], "existsError" | "get">;
|
messagesPerField: Pick<KcContext.Common["messagesPerField"], "existsError" | "get">;
|
||||||
profile: {
|
profile: {
|
||||||
attributes: Attribute[];
|
attributes: Attribute[];
|
||||||
html5DataAnnotations: Record<string, string>;
|
html5DataAnnotations?: Record<string, string>;
|
||||||
};
|
};
|
||||||
passwordRequired: boolean;
|
passwordRequired: boolean;
|
||||||
realm: { registrationEmailAsUsername: boolean };
|
realm: { registrationEmailAsUsername: boolean };
|
||||||
@ -107,8 +107,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
|
|||||||
|
|
||||||
usePrepareTemplate({
|
usePrepareTemplate({
|
||||||
"styles": [],
|
"styles": [],
|
||||||
// NOTE: The ?? {} is for compat with Keycloak version prior to 24
|
"scripts": Object.keys(kcContext.profile?.html5DataAnnotations ?? {})
|
||||||
"scripts": Object.keys(kcContext.profile.html5DataAnnotations ?? {})
|
|
||||||
.filter(key => key !== "kcMultivalued" && key !== "kcNumberFormat") // NOTE: Keycloakify handles it.
|
.filter(key => key !== "kcMultivalued" && key !== "kcNumberFormat") // NOTE: Keycloakify handles it.
|
||||||
.map(key => ({
|
.map(key => ({
|
||||||
"isModule": true,
|
"isModule": true,
|
||||||
@ -126,7 +125,69 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
|
|||||||
const attributesWithPassword = useMemo(() => {
|
const attributesWithPassword = useMemo(() => {
|
||||||
const attributesWithPassword: Attribute[] = [];
|
const attributesWithPassword: Attribute[] = [];
|
||||||
|
|
||||||
for (const attribute of kcContext.profile.attributes) {
|
const attributes = (() => {
|
||||||
|
retrocompat_patch: {
|
||||||
|
if ("profile" in kcContext && "attributes" in kcContext.profile && kcContext.profile.attributes.length !== 0) {
|
||||||
|
break retrocompat_patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
kcContext.profile = {
|
||||||
|
"attributes": (["firstName", "lastName", "email", "username"] as const)
|
||||||
|
.filter(name => (name !== "username" ? true : !kcContext.realm.registrationEmailAsUsername))
|
||||||
|
.map(name =>
|
||||||
|
id<Attribute>({
|
||||||
|
"name": name,
|
||||||
|
"displayName": id<`\${${MessageKey}}`>(`\${${name}}`),
|
||||||
|
"required": true,
|
||||||
|
"value": (kcContext as any).register.formData[name] ?? "",
|
||||||
|
"html5DataAnnotations": {},
|
||||||
|
"readOnly": false,
|
||||||
|
"validators": {},
|
||||||
|
"annotations": {},
|
||||||
|
"autocomplete": (() => {
|
||||||
|
switch (name) {
|
||||||
|
case "email":
|
||||||
|
return "email";
|
||||||
|
case "username":
|
||||||
|
return "username";
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
"html5DataAnnotations": {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return kcContext.profile.attributes;
|
||||||
|
})();
|
||||||
|
|
||||||
|
for (const attribute_pre_group_patch of attributes) {
|
||||||
|
const attribute = (() => {
|
||||||
|
if (typeof attribute_pre_group_patch.group === "string" && attribute_pre_group_patch.group !== "") {
|
||||||
|
const { group, groupDisplayHeader, groupDisplayDescription, groupAnnotations, ...rest } =
|
||||||
|
attribute_pre_group_patch as Attribute & {
|
||||||
|
group: string;
|
||||||
|
groupDisplayHeader?: string;
|
||||||
|
groupDisplayDescription?: string;
|
||||||
|
groupAnnotations: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return id<Attribute>({
|
||||||
|
...rest,
|
||||||
|
"group": {
|
||||||
|
"name": group,
|
||||||
|
"displayHeader": groupDisplayHeader,
|
||||||
|
"displayDescription": groupDisplayDescription,
|
||||||
|
"html5DataAnnotations": {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return attribute_pre_group_patch;
|
||||||
|
})();
|
||||||
|
|
||||||
attributesWithPassword.push(attribute);
|
attributesWithPassword.push(attribute);
|
||||||
|
|
||||||
add_password_and_password_confirm: {
|
add_password_and_password_confirm: {
|
||||||
@ -191,7 +252,6 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
|
|||||||
apply_formatters: {
|
apply_formatters: {
|
||||||
const { attribute } = formFieldState;
|
const { attribute } = formFieldState;
|
||||||
|
|
||||||
// NOTE: The `?? {}` is for compat with Keycloak version prior to 24
|
|
||||||
const { kcNumberFormat } = attribute.html5DataAnnotations ?? {};
|
const { kcNumberFormat } = attribute.html5DataAnnotations ?? {};
|
||||||
|
|
||||||
if (kcNumberFormat === undefined) {
|
if (kcNumberFormat === undefined) {
|
||||||
@ -407,7 +467,6 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
|
|||||||
let { valueOrValues } = formFieldState;
|
let { valueOrValues } = formFieldState;
|
||||||
|
|
||||||
unFormat_number: {
|
unFormat_number: {
|
||||||
// NOTE: The `?? {}` is for compat with Keycloak version prior to 24
|
|
||||||
const { kcNumberUnFormat } = attribute.html5DataAnnotations ?? {};
|
const { kcNumberUnFormat } = attribute.html5DataAnnotations ?? {};
|
||||||
|
|
||||||
if (kcNumberUnFormat === undefined) {
|
if (kcNumberUnFormat === undefined) {
|
||||||
@ -791,7 +850,6 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
|
|||||||
assert(typeof valueOrValues === "string");
|
assert(typeof valueOrValues === "string");
|
||||||
|
|
||||||
unFormat_number: {
|
unFormat_number: {
|
||||||
// NOTE: The `?? {}` is for compat with Keycloak version prior to 24
|
|
||||||
const { kcNumberUnFormat } = attribute.html5DataAnnotations ?? {};
|
const { kcNumberUnFormat } = attribute.html5DataAnnotations ?? {};
|
||||||
|
|
||||||
if (kcNumberUnFormat === undefined) {
|
if (kcNumberUnFormat === undefined) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user