Support register-user-profile.ftl
This commit is contained in:
parent
c388c77f4a
commit
4ca2bc59b6
@ -134,97 +134,67 @@
|
||||
obj,
|
||||
{
|
||||
"messagesPerField": {
|
||||
"printIfExists": function (key, x) {
|
||||
switch(key){
|
||||
case "userLabel": return (function (){
|
||||
|
||||
<#assign fieldNames = ["global", "userLabel", "username", "email", "firstName", "lastName", "password", "password-confirm"]>
|
||||
|
||||
<#attempt>
|
||||
<#list profile.attributes as attribute>
|
||||
<#assign fieldNames += [attribute.name]>
|
||||
</#list>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
"printIfExists": function (fieldName, x) {
|
||||
<#list fieldNames as fieldName>
|
||||
if(fieldName === "${fieldName}" ){
|
||||
<#attempt>
|
||||
return "${messagesPerField.printIfExists('userLabel','1')}" ? x : undefined;
|
||||
return "${messagesPerField.printIfExists(fieldName,'1')}" ? x : undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})();
|
||||
case "username": return (function (){
|
||||
<#attempt>
|
||||
return "${messagesPerField.printIfExists('username','1')}" ? x : undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})();
|
||||
case "email": return (function (){
|
||||
<#attempt>
|
||||
return "${messagesPerField.printIfExists('email','1')}" ? x : undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})();
|
||||
case "firstName": return (function (){
|
||||
<#attempt>
|
||||
return "${messagesPerField.printIfExists('firstName','1')}" ? x : undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})();
|
||||
case "lastName": return (function (){
|
||||
<#attempt>
|
||||
return "${messagesPerField.printIfExists('lastName','1')}" ? x : undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})();
|
||||
case "password": return (function (){
|
||||
<#attempt>
|
||||
return "${messagesPerField.printIfExists('password','1')}" ? x : undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})();
|
||||
case "password-confirm": return (function (){
|
||||
<#attempt>
|
||||
return "${messagesPerField.printIfExists('password-confirm','1')}" ? x : undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})();
|
||||
}
|
||||
}
|
||||
</#list>
|
||||
throw new Error("There is no " + fieldName " field");
|
||||
},
|
||||
"existsError": function (key) {
|
||||
|
||||
<#attempt>
|
||||
|
||||
<#list profile.attributes as attribute>
|
||||
|
||||
if(key === "${attribute.name}" ){
|
||||
|
||||
return <#if messagesPerField.existsError('${attribute.name}')>true<#else>false</#if>;
|
||||
|
||||
}
|
||||
|
||||
</#list>
|
||||
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
throw new Error(key + " is not an exsisting profile attribute name");
|
||||
|
||||
},
|
||||
"get": function (key) {
|
||||
|
||||
<#attempt>
|
||||
|
||||
<#list profile.attributes as attribute>
|
||||
|
||||
<#if messagesPerField.existsError('${attribute.name}')>
|
||||
|
||||
if(key === "${attribute.name}" ){
|
||||
return "${messagesPerField.get('${attribute.name}')?no_esc}"
|
||||
}
|
||||
|
||||
</#if>
|
||||
|
||||
</#list>
|
||||
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
throw new Error(" there is no message for " + key);
|
||||
|
||||
}
|
||||
"existsError": function (fieldName) {
|
||||
<#list fieldNames as fieldName>
|
||||
if(fieldName === "${fieldName}" ){
|
||||
<#attempt>
|
||||
return <#if messagesPerField.existsError('${fieldName}')>true<#else>false</#if>;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
}
|
||||
</#list>
|
||||
throw new Error("There is no " + fieldName " field");
|
||||
},
|
||||
"get": function (fieldName) {
|
||||
<#list fieldNames as fieldName>
|
||||
if(fieldName === "${fieldName}" ){
|
||||
<#attempt>
|
||||
<#if messagesPerField.existsError('${fieldName}')>
|
||||
if(fieldName === "${fieldName}" ){
|
||||
return "${messagesPerField.get('${fieldName}')?no_esc}";
|
||||
}
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
}
|
||||
</#list>
|
||||
throw new Error("There is no " + fieldName " field");
|
||||
},
|
||||
"exists": function (fieldName) {
|
||||
<#list fieldNames as fieldName>
|
||||
if(fieldName === "${fieldName}" ){
|
||||
<#attempt>
|
||||
return <#if messagesPerField.exists('${fieldName}')>true<#else>false</#if>;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
}
|
||||
</#list>
|
||||
throw new Error("There is no " + fieldName " field");
|
||||
}
|
||||
},
|
||||
"msg": function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); },
|
||||
"advancedMsg": function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); },
|
||||
"advancedMsg": function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); }
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
import cheerio from "cheerio";
|
||||
import {
|
||||
replaceImportsFromStaticInJsCode,
|
||||
@ -12,11 +10,10 @@ import { objectKeys } from "tsafe/objectKeys";
|
||||
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
||||
|
||||
export const pageIds = [
|
||||
"login.ftl", "register.ftl", "info.ftl",
|
||||
"error.ftl", "login-reset-password.ftl",
|
||||
"login-verify-email.ftl", "terms.ftl",
|
||||
"login-otp.ftl", "login-update-profile.ftl",
|
||||
"login-idp-link-confirm.ftl"
|
||||
"login.ftl", "register.ftl", "register-user-profile.ftl",
|
||||
"info.ftl", "error.ftl", "login-reset-password.ftl",
|
||||
"login-verify-email.ftl", "terms.ftl", "login-otp.ftl",
|
||||
"login-update-profile.ftl", "login-idp-link-confirm.ftl"
|
||||
] as const;
|
||||
|
||||
export type PageId = typeof pageIds[number];
|
||||
|
@ -4,6 +4,7 @@ import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||
import type { KcProps } from "./KcProps";
|
||||
import { Login } from "./Login";
|
||||
import { Register } from "./Register";
|
||||
import { RegisterUserProfile } from "./RegisterUserProfile";
|
||||
import { Info } from "./Info";
|
||||
import { Error } from "./Error";
|
||||
import { LoginResetPassword } from "./LoginResetPassword";
|
||||
@ -17,6 +18,7 @@ export const KcApp = memo(({ kcContext, ...props }: { kcContext: KcContextBase;
|
||||
switch (kcContext.pageId) {
|
||||
case "login.ftl": return <Login {...{ kcContext, ...props }} />;
|
||||
case "register.ftl": return <Register {...{ kcContext, ...props }} />;
|
||||
case "register-user-profile.ftl": return <RegisterUserProfile {...{ kcContext, ...props }} />;
|
||||
case "info.ftl": return <Info {...{ kcContext, ...props }} />;
|
||||
case "error.ftl": return <Error {...{ kcContext, ...props }} />;
|
||||
case "login-reset-password.ftl": return <LoginResetPassword {...{ kcContext, ...props }} />;
|
||||
|
@ -94,12 +94,14 @@ export type KcProps = KcPropsGeneric<
|
||||
"kcFormGroupErrorClass" |
|
||||
"kcLabelClass" |
|
||||
"kcInputClass" |
|
||||
"kcInputErrorMessageClass" |
|
||||
"kcInputWrapperClass" |
|
||||
"kcFormOptionsClass" |
|
||||
"kcFormButtonsClass" |
|
||||
"kcFormSettingClass" |
|
||||
"kcTextareaClass" |
|
||||
"kcInfoAreaClass" |
|
||||
"kcFormGroupHeader" |
|
||||
"kcButtonClass" |
|
||||
"kcButtonPrimaryClass" |
|
||||
"kcButtonDefaultClass" |
|
||||
@ -147,6 +149,7 @@ export const defaultKcProps = {
|
||||
"kcFormGroupErrorClass": ["has-error"],
|
||||
"kcLabelClass": ["control-label"],
|
||||
"kcInputClass": ["form-control"],
|
||||
"kcInputErrorMessageClass": ["pf-c-form__helper-text", "pf-m-error", "required", "kc-feedback-text"],
|
||||
"kcInputWrapperClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"],
|
||||
"kcFormOptionsClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"],
|
||||
"kcFormButtonsClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"],
|
||||
@ -155,6 +158,9 @@ export const defaultKcProps = {
|
||||
|
||||
"kcInfoAreaClass": ["col-xs-12", "col-sm-4", "col-md-4", "col-lg-5", "details"],
|
||||
|
||||
// user-profile grouping
|
||||
"kcFormGroupHeader": ["pf-c-form__group"],
|
||||
|
||||
// css classes for form buttons main class used for all buttons
|
||||
"kcButtonClass": ["btn"],
|
||||
// classes defining priority of the button - primary or default (there is typically only one priority button for the form)
|
||||
|
226
src/lib/components/RegisterUserProfile.tsx
Normal file
226
src/lib/components/RegisterUserProfile.tsx
Normal file
@ -0,0 +1,226 @@
|
||||
|
||||
import { memo, Fragment } from "react";
|
||||
import { Template } from "./Template";
|
||||
import type { KcProps } from "./KcProps";
|
||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||
import { useKcMessage } from "../i18n/useKcMessage";
|
||||
import { useCssAndCx } from "tss-react";
|
||||
import type { ReactComponent } from "../tools/ReactComponent";
|
||||
|
||||
export const RegisterUserProfile = memo(({ kcContext, ...props }: { kcContext: KcContextBase.RegisterUserProfile; } & KcProps) => {
|
||||
|
||||
const {
|
||||
url,
|
||||
messagesPerField,
|
||||
realm,
|
||||
passwordRequired,
|
||||
recaptchaRequired,
|
||||
recaptchaSiteKey
|
||||
} = kcContext;
|
||||
|
||||
const { msg, msgStr } = useKcMessage();
|
||||
|
||||
const { cx } = useCssAndCx();
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, ...props }}
|
||||
displayMessage={messagesPerField.exists("global")}
|
||||
displayRequiredFields={true}
|
||||
doFetchDefaultThemeResources={true}
|
||||
headerNode={msg("registerTitle")}
|
||||
formNode={
|
||||
<form
|
||||
id="kc-register-form"
|
||||
className={cx(props.kcFormClass)}
|
||||
action={url.registrationAction}
|
||||
method="post"
|
||||
>
|
||||
<UserProfileFormFields
|
||||
kcContext={kcContext}
|
||||
{...props}
|
||||
AfterField={({ attribute }) =>
|
||||
/*render password fields just under the username or email (if used as username)*/
|
||||
passwordRequired && (attribute.name == "username" || (attribute.name == "email" && realm.registrationEmailAsUsername)) &&
|
||||
<>
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div className={cx(props.kcLabelWrapperClass)}>
|
||||
<label htmlFor="password" className={cx(props.kcLabelClass)}>
|
||||
{msg("password")}
|
||||
</label> *
|
||||
</div>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<input type="password" id="password" className={cx(props.kcInputClass)} name="password"
|
||||
autoComplete="new-password"
|
||||
aria-invalid={
|
||||
messagesPerField.existsError("password") ||
|
||||
messagesPerField.existsError("password-confirm")
|
||||
}
|
||||
/>
|
||||
{
|
||||
messagesPerField.existsError("password") &&
|
||||
<span id="input-error-password" className={cx(props.kcInputErrorMessageClass)} aria-live="polite">
|
||||
{messagesPerField.get('password')}
|
||||
</span>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div className={cx(props.kcLabelWrapperClass)}>
|
||||
<label htmlFor="password-confirm"
|
||||
className={cx(props.kcLabelClass)}>
|
||||
{msg("passwordConfirm")}
|
||||
</label> *
|
||||
</div>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<input type="password" id="password-confirm" className={cx(props.kcInputClass)}
|
||||
name="password-confirm"
|
||||
aria-invalid={messagesPerField.existsError("password-confirm")}
|
||||
/>
|
||||
{
|
||||
messagesPerField.existsError("password-confirm") &&
|
||||
<span id="input-error-password-confirm" className={cx(props.kcInputErrorMessageClass)} aria-live="polite">
|
||||
{messagesPerField.get('password-confirm')}
|
||||
</span>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</> || null
|
||||
}
|
||||
/>
|
||||
{
|
||||
recaptchaRequired &&
|
||||
<div className="form-group">
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
|
||||
<div className={cx(props.kcFormOptionsWrapperClass)}>
|
||||
<span><a href={url.loginUrl}>{msg("backToLogin")}</a></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
|
||||
<input
|
||||
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
|
||||
type="submit" value={msgStr("doRegister")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form >
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const UserProfileFormFields = memo(
|
||||
(
|
||||
{
|
||||
kcContext,
|
||||
BeforeField = () => null,
|
||||
AfterField = () => null,
|
||||
...props
|
||||
}:
|
||||
{ kcContext: KcContextBase.RegisterUserProfile; } &
|
||||
KcProps &
|
||||
Partial<Record<
|
||||
"BeforeField" | "AfterField",
|
||||
ReactComponent<{ attribute: KcContextBase.RegisterUserProfile["profile"]["attributes"][number]; }>
|
||||
>>
|
||||
) => {
|
||||
|
||||
const { messagesPerField } = kcContext;
|
||||
|
||||
const { cx } = useCssAndCx();
|
||||
|
||||
const { advancedMsg } = useKcMessage();
|
||||
|
||||
let currentGroup = "";
|
||||
|
||||
return (
|
||||
<>
|
||||
{kcContext.profile.attributes.map((attribute, i) => {
|
||||
|
||||
const {
|
||||
group = "",
|
||||
groupDisplayHeader = "",
|
||||
groupDisplayDescription = ""
|
||||
} = attribute;
|
||||
|
||||
if (group === currentGroup) return null;
|
||||
|
||||
currentGroup = group;
|
||||
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
{group !== "" &&
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div className={cx(props.kcContentWrapperClass)}>
|
||||
<label
|
||||
id={`header-${group}`}
|
||||
className={cx(props.kcFormGroupHeader)}
|
||||
>
|
||||
{groupDisplayHeader !== "" && advancedMsg(groupDisplayHeader) || currentGroup}
|
||||
</label>
|
||||
</div>
|
||||
{groupDisplayDescription !== "" &&
|
||||
<div className={cx(props.kcLabelWrapperClass)}>
|
||||
<label
|
||||
id={`description-${group}`}
|
||||
className={`${cx(props.kcLabelClass)}`}
|
||||
>
|
||||
{advancedMsg(groupDisplayDescription) ?? ""}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
</div>}
|
||||
<BeforeField attribute={attribute} />
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div className={cx(props.kcLabelWrapperClass)}>
|
||||
<label
|
||||
htmlFor={attribute.name}
|
||||
className={cx(props.kcLabelClass)}
|
||||
>
|
||||
{advancedMsg(attribute.displayName ?? "")}
|
||||
</label>
|
||||
{attribute.required && <>*</>}
|
||||
</div>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="text"
|
||||
id={attribute.name}
|
||||
name={attribute.name}
|
||||
value={attribute.value ?? ""}
|
||||
className={cx(props.kcInputClass)}
|
||||
aria-invalid={messagesPerField.existsError(attribute.name)}
|
||||
disabled={attribute.readOnly}
|
||||
{...(attribute.autocomplete === undefined ? {} : {
|
||||
"autoComplete": attribute.autocomplete
|
||||
})}
|
||||
/>
|
||||
{
|
||||
kcContext.messagesPerField.existsError(attribute.name) &&
|
||||
<span
|
||||
id={`input-error-${attribute.name}`}
|
||||
className={cx(props.kcInputErrorMessageClass)}
|
||||
aria-live="polite"
|
||||
>
|
||||
{messagesPerField.get(attribute.name)}
|
||||
</span>
|
||||
}
|
||||
</div >
|
||||
</div >
|
||||
<AfterField attribute={attribute} />
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
);
|
@ -13,7 +13,7 @@ type ExtractAfterStartingWith<Prefix extends string, StrEnum> =
|
||||
* (ex: url.loginAction is undefined on error.ftl)
|
||||
*/
|
||||
export type KcContextBase =
|
||||
KcContextBase.Login | KcContextBase.Register | KcContextBase.Info |
|
||||
KcContextBase.Login | KcContextBase.Register | KcContextBase.RegisterUserProfile | KcContextBase.Info |
|
||||
KcContextBase.Error | KcContextBase.LoginResetPassword | KcContextBase.LoginVerifyEmail |
|
||||
KcContextBase.Terms | KcContextBase.LoginOtp | KcContextBase.LoginUpdateProfile |
|
||||
KcContextBase.LoginIdpLinkConfirm;
|
||||
@ -62,6 +62,12 @@ export declare namespace KcContextBase {
|
||||
name?: string;
|
||||
}
|
||||
isAppInitiatedAction: boolean;
|
||||
messagesPerField: {
|
||||
printIfExists: <T>(fieldName: string, x: T) => T | undefined;
|
||||
existsError: (fieldName: string) => boolean;
|
||||
get: (fieldName: string) => string;
|
||||
exists: (fieldName: string) => boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type Login = Common & {
|
||||
@ -97,35 +103,10 @@ export declare namespace KcContextBase {
|
||||
};
|
||||
};
|
||||
|
||||
export type Register = Common & {
|
||||
pageId: "register.ftl";
|
||||
export type RegisterCommon = Common & {
|
||||
url: {
|
||||
registrationAction: string;
|
||||
};
|
||||
messagesPerField: {
|
||||
printIfExists: <T>(
|
||||
key:
|
||||
"userLabel" |
|
||||
"username" |
|
||||
"email" |
|
||||
"firstName" |
|
||||
"lastName" |
|
||||
"password" |
|
||||
"password-confirm",
|
||||
x: T
|
||||
)=> T | undefined;
|
||||
existsError: (key: string)=> boolean;
|
||||
get: (key: string) => string;
|
||||
};
|
||||
register: {
|
||||
formData: {
|
||||
firstName?: string;
|
||||
displayName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
username?: string;
|
||||
}
|
||||
};
|
||||
passwordRequired: boolean;
|
||||
recaptchaRequired: boolean;
|
||||
recaptchaSiteKey?: string;
|
||||
@ -140,6 +121,37 @@ export declare namespace KcContextBase {
|
||||
};
|
||||
};
|
||||
|
||||
export type Register = RegisterCommon & {
|
||||
pageId: "register.ftl";
|
||||
register: {
|
||||
formData: {
|
||||
firstName?: string;
|
||||
displayName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
username?: string;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export type RegisterUserProfile = RegisterCommon & {
|
||||
pageId: "register-user-profile.ftl";
|
||||
profile: {
|
||||
attributes: {
|
||||
name: string;
|
||||
displayName?: string;
|
||||
required: boolean;
|
||||
value?: string;
|
||||
group?: string;
|
||||
groupDisplayHeader?: string;
|
||||
groupDisplayDescription?: string;
|
||||
readOnly: boolean;
|
||||
autocomplete?: string;
|
||||
}[];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export type Info = Common & {
|
||||
pageId: "info.ftl";
|
||||
messageHeader?: string;
|
||||
@ -191,13 +203,6 @@ export declare namespace KcContextBase {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
};
|
||||
messagesPerField: {
|
||||
printIfExists<T>(
|
||||
key: "username" | "email" | "firstName" | "lastName",
|
||||
x: T
|
||||
): T | undefined;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
export type LoginIdpLinkConfirm = Common & {
|
||||
|
@ -5,6 +5,9 @@ import { kcMessages, evtTermsUpdated } from "./kcMessages/login";
|
||||
import { useEvt } from "evt/hooks";
|
||||
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { id } from "tsafe/id";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { is } from "tsafe/is";
|
||||
|
||||
export type MessageKey = keyof typeof kcMessages["en"];
|
||||
|
||||
@ -53,7 +56,7 @@ export function useKcMessage() {
|
||||
);
|
||||
|
||||
const advancedMsg = useCallback(
|
||||
(key: string): string => {
|
||||
(key: string): string | undefined => {
|
||||
|
||||
const match = key.match(/^\$\{([^{]+)\}$/);
|
||||
|
||||
@ -61,7 +64,10 @@ export function useKcMessage() {
|
||||
return key;
|
||||
}
|
||||
|
||||
return msgStr(match[1] as MessageKey);
|
||||
return (
|
||||
id<Record<string, string | undefined>>(kcMessages[kcLanguageTag])[key] ??
|
||||
id<Record<string, string | undefined>>(kcMessages["en"])[key]
|
||||
);
|
||||
|
||||
},
|
||||
[msgStr]
|
||||
|
6
src/lib/tools/ReactComponent.ts
Normal file
6
src/lib/tools/ReactComponent.ts
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
import type { FC, ComponentClass } from "react";
|
||||
|
||||
export type ReactComponent<Props extends Record<string, unknown> = {}> =
|
||||
| ((props: Props) => ReturnType<FC>)
|
||||
| ComponentClass<Props>;
|
Loading…
x
Reference in New Issue
Block a user