Commit for saving
This commit is contained in:
parent
1539980f66
commit
3d0736e72b
|
@ -19,7 +19,7 @@ yarn
|
|||
yarn keycloak # Build the theme one time (some assets will be copied to
|
||||
# public/keycloak_static, they are needed to dev your page outside of Keycloak)
|
||||
yarn start # See the Hello World app
|
||||
# Uncomment line 15 of src/KcApp/kcContext, reload https://localhost:3000
|
||||
# Uncomment line 15 of src/keycloakTheme/kcContext, reload https://localhost:3000
|
||||
# You can now develop your Login pages.
|
||||
|
||||
# Think your theme is ready? Run
|
||||
|
@ -89,14 +89,15 @@ and remove unnecessary file.
|
|||
|
||||
```bash
|
||||
rm -r src/App
|
||||
rm src/KcApp/index.ts
|
||||
mv src/KcApp/* src/
|
||||
rm src/keycloakTheme/index.ts
|
||||
mv src/keycloakTheme/* src/
|
||||
rm -r src/keycloakTheme
|
||||
|
||||
cat << EOF > src/index.tsx
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { StrictMode } from "react";
|
||||
import { kcContext } from "./kcContext";
|
||||
import KcApp from "KcApp";
|
||||
import KcApp from "./KcApp";
|
||||
|
||||
if( kcContext === undefined ){
|
||||
throw new Error(
|
||||
|
|
|
@ -33,13 +33,16 @@
|
|||
"@types/react": "18.0.9",
|
||||
"@types/react-dom": "18.0.4",
|
||||
"react-scripts": "5.0.0",
|
||||
"typescript": "^4.7.3"
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
],
|
||||
"rules": {
|
||||
"react-hooks/exhaustive-deps": "off"
|
||||
}
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
import "./KcApp.css";
|
||||
import { lazy, Suspense } from "react";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import KcAppBase, { defaultKcProps } from "keycloakify";
|
||||
import { useI18n } from "./i18n";
|
||||
|
||||
const Register = lazy(() => import("./Register"));
|
||||
const Terms = lazy(() => import("./Terms"));
|
||||
const MyExtraPage1 = lazy(() => import("./MyExtraPage1"));
|
||||
const MyExtraPage2 = lazy(() => import("./MyExtraPage2"));
|
||||
|
||||
export type Props = {
|
||||
kcContext: KcContext;
|
||||
};
|
||||
|
||||
export default function KcApp({ kcContext }: Props) {
|
||||
const i18n = useI18n({ kcContext });
|
||||
|
||||
//NOTE: Locales not yet downloaded
|
||||
if (i18n === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const props = {
|
||||
i18n,
|
||||
...defaultKcProps,
|
||||
// NOTE: The classes are defined in ./KcApp.css
|
||||
"kcHeaderWrapperClass": "my-color my-font",
|
||||
};
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
{(() => {
|
||||
switch (kcContext.pageId) {
|
||||
case "register.ftl": return <Register {...{ kcContext, ...props }} />;
|
||||
case "terms.ftl": return <Terms {...{ kcContext, ...props }} />;
|
||||
case "my-extra-page-1.ftl": return <MyExtraPage1 {...{ kcContext, ...props }} />;
|
||||
case "my-extra-page-2.ftl": return <MyExtraPage2 {...{ kcContext, ...props }} />;
|
||||
default: return <KcAppBase {...{ kcContext, ...props }} />;
|
||||
}
|
||||
})()}
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
import { memo } from "react";
|
||||
import type { KcProps } from "keycloakify";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import type { I18n } from "./i18n";
|
||||
|
||||
type KcContext_MyExtraPage1 = Extract<KcContext, { pageId: "my-extra-page-1.ftl"; }>;
|
||||
|
||||
const MyExtraPage1 = memo(({ kcContext, i18n, ...props }: { kcContext: KcContext_MyExtraPage1; i18n: I18n; } & KcProps) => {
|
||||
|
||||
return <>It is up to you to implement this page</>
|
||||
|
||||
});
|
||||
|
||||
export default MyExtraPage1;
|
|
@ -1,16 +0,0 @@
|
|||
import { memo } from "react";
|
||||
import type { KcProps } from "keycloakify";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import type { I18n } from "./i18n";
|
||||
|
||||
type KcContext_MyExtraPage2 = Extract<KcContext, { pageId: "my-extra-page-2.ftl"; }>;
|
||||
|
||||
const MyExtraPage2 = memo(({ kcContext, i18n, ...props }: { kcContext: KcContext_MyExtraPage2; i18n: I18n; } & KcProps) => {
|
||||
|
||||
console.log(`TODO: Do something with: ${kcContext.someCustomValue}`);
|
||||
|
||||
return <>It is up to you to implement this page</>
|
||||
|
||||
});
|
||||
|
||||
export default MyExtraPage2;
|
|
@ -1,94 +0,0 @@
|
|||
import { memo } from "react";
|
||||
import Template from "keycloakify/lib/components/Template";
|
||||
import type { KcProps } from "keycloakify";
|
||||
import { useDownloadTerms } from "keycloakify";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import type { I18n } from "./i18n";
|
||||
import { evtTermMarkdown } from "keycloakify/lib/components/Terms";
|
||||
import { useRerenderOnStateChange } from "evt/hooks";
|
||||
import tos_en_url from "./tos_en.md";
|
||||
import tos_fr_url from "./tos_fr.md";
|
||||
import { clsx } from "keycloakify/lib/tools/clsx";
|
||||
|
||||
/**
|
||||
* NOTE: Yo do not need to do all this to put your own Terms and conditions
|
||||
* this is if you want component level customization.
|
||||
* If the default works for you you can just use the useDownloadTerms hook
|
||||
* in the KcApp.tsx
|
||||
* Example: https://github.com/garronej/keycloakify-starter/blob/a20c21b2aae7c6dc6dbea294f3d321955ddf9355/src/KcApp/KcApp.tsx#L14-L30
|
||||
*/
|
||||
|
||||
type KcContext_Terms = Extract<KcContext, { pageId: "terms.ftl" }>;
|
||||
|
||||
const Terms = memo(
|
||||
({
|
||||
kcContext,
|
||||
i18n,
|
||||
...props
|
||||
}: { kcContext: KcContext_Terms; i18n: I18n } & KcProps) => {
|
||||
const { url } = kcContext;
|
||||
|
||||
useDownloadTerms({
|
||||
kcContext,
|
||||
"downloadTermMarkdown": async ({ currentLanguageTag }) => {
|
||||
|
||||
const markdownString = await fetch((() => {
|
||||
switch (currentLanguageTag) {
|
||||
case "fr": return tos_fr_url;
|
||||
default: return tos_en_url;
|
||||
}
|
||||
})()).then(response => response.text());
|
||||
|
||||
return markdownString;
|
||||
},
|
||||
});
|
||||
|
||||
useRerenderOnStateChange(evtTermMarkdown);
|
||||
|
||||
if (evtTermMarkdown.state === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, ...props }}
|
||||
doFetchDefaultThemeResources={true}
|
||||
displayMessage={false}
|
||||
headerNode={msg("termsTitle")}
|
||||
formNode={
|
||||
<>
|
||||
<div id="kc-terms-text">{evtTermMarkdown.state}</div>
|
||||
<form className="form-actions" action={url.loginAction} method="POST">
|
||||
<input
|
||||
className={clsx(
|
||||
props.kcButtonClass,
|
||||
props.kcButtonClass,
|
||||
props.kcButtonClass,
|
||||
props.kcButtonPrimaryClass,
|
||||
props.kcButtonLargeClass
|
||||
)}
|
||||
name="accept"
|
||||
id="kc-accept"
|
||||
type="submit"
|
||||
value={msgStr("doAccept")}
|
||||
/>
|
||||
<input
|
||||
className={clsx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)}
|
||||
name="cancel"
|
||||
id="kc-decline"
|
||||
type="submit"
|
||||
value={msgStr("doDecline")}
|
||||
/>
|
||||
</form>
|
||||
<div className="clearfix" />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
},
|
||||
);
|
||||
|
||||
export default Terms;
|
|
@ -1,3 +0,0 @@
|
|||
import KcApp from "./KcApp";
|
||||
export * from "./KcApp";
|
||||
export default KcApp;
|
|
@ -1,79 +0,0 @@
|
|||
import { getKcContext } from "keycloakify/lib/getKcContext";
|
||||
|
||||
export const { kcContext } = getKcContext<
|
||||
// NOTE: A 'keycloakify' field must be added
|
||||
// in the package.json to generate theses pages
|
||||
// https://docs.keycloakify.dev/build-options#keycloakify.extrapages
|
||||
| { pageId: "my-extra-page-1.ftl"; }
|
||||
| { pageId: "my-extra-page-2.ftl"; someCustomValue: string; }
|
||||
// NOTE: register.ftl is deprecated in favor of register-user-profile.ftl
|
||||
// but let's say we use it anyway and have this plugin enabled: https://github.com/micedre/keycloak-mail-whitelisting
|
||||
// keycloak-mail-whitelisting define the non standard ftl global authorizedMailDomains, we declare it here.
|
||||
| { pageId: "register.ftl"; authorizedMailDomains: string[]; }
|
||||
>({
|
||||
// Uncomment to test the login page for development.
|
||||
//"mockPageId": "login.ftl",
|
||||
"mockData": [
|
||||
{
|
||||
"pageId": "login.ftl",
|
||||
"locale": {
|
||||
//When we test the login page we do it in french
|
||||
"currentLanguageTag": "fr",
|
||||
},
|
||||
},
|
||||
{
|
||||
"pageId": "my-extra-page-2.ftl",
|
||||
"someCustomValue": "foo bar baz"
|
||||
},
|
||||
{
|
||||
"pageId": "register.ftl",
|
||||
"authorizedMailDomains": [
|
||||
"example.com",
|
||||
"another-example.com",
|
||||
"*.yet-another-example.com",
|
||||
"*.example.com",
|
||||
"hello-world.com"
|
||||
]
|
||||
},
|
||||
{
|
||||
//NOTE: You will either use register.ftl (legacy) or register-user-profile.ftl, not both
|
||||
"pageId": "register-user-profile.ftl",
|
||||
"locale": {
|
||||
"currentLanguageTag": "fr"
|
||||
},
|
||||
"profile": {
|
||||
"attributes": [
|
||||
{
|
||||
"validators": {
|
||||
"pattern": {
|
||||
"pattern": "^[a-zA-Z0-9]+$",
|
||||
"ignore.empty.value": true,
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
"error-message": "${alphanumericalCharsOnly}",
|
||||
},
|
||||
},
|
||||
//NOTE: To override the default mock value
|
||||
"value": undefined,
|
||||
"name": "username"
|
||||
},
|
||||
{
|
||||
"validators": {
|
||||
"options": {
|
||||
"options": ["male", "female", "non-binary", "transgender", "intersex", "non_communicated"]
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
"displayName": "${gender}",
|
||||
"annotations": {},
|
||||
"required": true,
|
||||
"groupAnnotations": {},
|
||||
"readOnly": false,
|
||||
"name": "gender"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export type KcContext = NonNullable<typeof kcContext>;
|
|
@ -1,13 +1,9 @@
|
|||
import { createRoot } from "react-dom/client";
|
||||
import { StrictMode, lazy, Suspense } from "react";
|
||||
import { kcContext } from "./KcApp/kcContext";
|
||||
import { kcContext } from "./keycloakTheme/kcContext";
|
||||
|
||||
const App = lazy(() => import("./App"));
|
||||
const KcApp = lazy(() => import("./KcApp"));
|
||||
|
||||
if (kcContext !== undefined) {
|
||||
console.log(kcContext);
|
||||
}
|
||||
const KcApp = lazy(() => import("./keycloakTheme/KcApp"));
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
|
|
80
src/keycloakTheme/KcApp.tsx
Normal file
80
src/keycloakTheme/KcApp.tsx
Normal file
|
@ -0,0 +1,80 @@
|
|||
import "./KcApp.css";
|
||||
import { lazy, Suspense } from "react";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import { useI18n, type I18n } from "./i18n";
|
||||
import Fallback, { defaultKcProps, type PageProps } from "keycloakify";
|
||||
import Template from "./Template";
|
||||
import { KcContextBase } from "keycloakify/lib/getKcContext";
|
||||
import type { I18nBase } from "keycloakify/lib/i18n";
|
||||
import type { TemplateProps } from "keycloakify";
|
||||
|
||||
const Login = lazy(()=> import("keycloakify/lib/pages/Login"));
|
||||
const Register = lazy(() => import("./pages/Register"));
|
||||
const Terms = lazy(() => import("./pages/Terms"));
|
||||
const MyExtraPage1 = lazy(() => import("./pages/MyExtraPage1"));
|
||||
const MyExtraPage2 = lazy(() => import("./pages/MyExtraPage2"));
|
||||
|
||||
type Props = {
|
||||
kcContext: KcContext;
|
||||
};
|
||||
|
||||
export default function App({ kcContext }: Props) {
|
||||
const i18n = useI18n({ kcContext });
|
||||
|
||||
//NOTE: Locales not yet downloaded
|
||||
if (i18n === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const props = {
|
||||
i18n,
|
||||
Template,
|
||||
...defaultKcProps,
|
||||
// NOTE: The classes are defined in ./KcApp.css
|
||||
"kcHeaderWrapperClass": "my-color my-font"
|
||||
} satisfies Omit<PageProps<any, I18n>, "kcContext">;
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
{(() => {
|
||||
switch (kcContext.pageId) {
|
||||
case "login.ftl": return <Login {...{kcContext, ...props }} />;
|
||||
case "register.ftl": return <Register {...{ kcContext, ...props }} />;
|
||||
case "terms.ftl": return <Terms {...{ kcContext, ...props }} />;
|
||||
case "my-extra-page-1.ftl": return <MyExtraPage1 {...{ kcContext, ...props }} />;
|
||||
case "my-extra-page-2.ftl": return <MyExtraPage2 {...{ kcContext, ...props }} />;
|
||||
default:
|
||||
|
||||
//console.log(xxxx);
|
||||
|
||||
//const x: KcContextBase = kcContext;
|
||||
//console.log(Template2, x);
|
||||
|
||||
//const y: I18nBase = i18n;
|
||||
|
||||
//const zz: TemplateProps<KcContextBase, I18nBase> = null as any as TemplateProps<KcContext, I18n>;
|
||||
//const z: TemplateProps<KcContextBase, I18nBase> = null as any as TemplateProps<typeof kcContext, I18n>;
|
||||
type XX = typeof kcContext;
|
||||
const Template2: (props: TemplateProps<KcContextBase, I18nBase>) => JSX.Element | null= null as any as (( props: TemplateProps<XX, I18n>)=> JSX.Element | null);
|
||||
|
||||
|
||||
//const Template3= (props: TemplateProps<typeof kcContext, I18n>)=> <Template {...props}/>;
|
||||
|
||||
/*
|
||||
const xxxx: PageProps<KcContextBase, I18nBase> = {
|
||||
"kcContext": kcContext,
|
||||
...defaultKcProps,
|
||||
"Template": Template3,
|
||||
"i18n": i18n
|
||||
};
|
||||
*/
|
||||
|
||||
return <Fallback {...{ kcContext, ...props }} Template={Template3} />;
|
||||
}
|
||||
})()}
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
}
|
237
src/keycloakTheme/Template.tsx
Normal file
237
src/keycloakTheme/Template.tsx
Normal file
|
@ -0,0 +1,237 @@
|
|||
// Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/components/shared/Template.tsx
|
||||
import { useReducer, useEffect } from "react";
|
||||
// You can replace all relative imports by cherry picking files from the keycloakify module.
|
||||
// For example, the following import:
|
||||
// import { headInsert } from "./tools/headInsert";
|
||||
// becomes:
|
||||
import { headInsert } from "keycloakify/lib/tools/headInsert";
|
||||
import { assert } from "keycloakify/lib/tools/assert";
|
||||
import { clsx } from "keycloakify/lib/tools/clsx";
|
||||
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
||||
import type { TemplateProps } from "keycloakify/lib/KcProps";
|
||||
//import type { KcContextBase } from "keycloakify/lib/getKcContext";
|
||||
import type { KcContext } from "./kcContext";
|
||||
// Here Instead of KcContextBase.Common you should provide your own context
|
||||
import type { I18n } from "./i18n";
|
||||
|
||||
export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
const {
|
||||
displayInfo = false,
|
||||
displayMessage = true,
|
||||
displayRequiredFields = false,
|
||||
displayWide = false,
|
||||
showAnotherWayIfPresent = true,
|
||||
headerNode,
|
||||
showUsernameNode = null,
|
||||
formNode,
|
||||
infoNode = null,
|
||||
kcContext,
|
||||
i18n,
|
||||
doFetchDefaultThemeResources
|
||||
} = props;
|
||||
|
||||
const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
|
||||
|
||||
const { realm, locale, auth, url, message, isAppInitiatedAction } = kcContext;
|
||||
|
||||
const [isExtraCssLoaded, setExtraCssLoaded] = useReducer(() => true, false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!doFetchDefaultThemeResources) {
|
||||
setExtraCssLoaded();
|
||||
return;
|
||||
}
|
||||
|
||||
let isUnmounted = false;
|
||||
const cleanups: (() => void)[] = [];
|
||||
|
||||
const toArr = (x: string | readonly string[] | undefined) => (typeof x === "string" ? x.split(" ") : x ?? []);
|
||||
|
||||
Promise.all(
|
||||
[
|
||||
...toArr(props.stylesCommon).map(relativePath => pathJoin(url.resourcesCommonPath, relativePath)),
|
||||
...toArr(props.styles).map(relativePath => pathJoin(url.resourcesPath, relativePath))
|
||||
]
|
||||
.reverse()
|
||||
.map(href =>
|
||||
headInsert({
|
||||
"type": "css",
|
||||
href,
|
||||
"position": "prepend"
|
||||
})
|
||||
)
|
||||
).then(() => {
|
||||
if (isUnmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setExtraCssLoaded();
|
||||
});
|
||||
|
||||
toArr(props.scripts).forEach(relativePath =>
|
||||
headInsert({
|
||||
"type": "javascript",
|
||||
"src": pathJoin(url.resourcesPath, relativePath)
|
||||
})
|
||||
);
|
||||
|
||||
if (props.kcHtmlClass !== undefined) {
|
||||
const htmlClassList = document.getElementsByTagName("html")[0].classList;
|
||||
|
||||
const tokens = clsx(props.kcHtmlClass).split(" ");
|
||||
|
||||
htmlClassList.add(...tokens);
|
||||
|
||||
cleanups.push(() => htmlClassList.remove(...tokens));
|
||||
}
|
||||
|
||||
return () => {
|
||||
isUnmounted = true;
|
||||
|
||||
cleanups.forEach(f => f());
|
||||
};
|
||||
}, [props.kcHtmlClass]);
|
||||
|
||||
if (!isExtraCssLoaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx(props.kcLoginClass)}>
|
||||
<div id="kc-header" className={clsx(props.kcHeaderClass)}>
|
||||
<div id="kc-header-wrapper" className={clsx(props.kcHeaderWrapperClass)}>
|
||||
{msg("loginTitleHtml", realm.displayNameHtml)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={clsx(props.kcFormCardClass, displayWide && props.kcFormCardAccountClass)}>
|
||||
<header className={clsx(props.kcFormHeaderClass)}>
|
||||
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
|
||||
<div id="kc-locale">
|
||||
<div id="kc-locale-wrapper" className={clsx(props.kcLocaleWrapperClass)}>
|
||||
<div className="kc-dropdown" id="kc-locale-dropdown">
|
||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||
<a href="#" id="kc-current-locale-link">
|
||||
{labelBySupportedLanguageTag[currentLanguageTag]}
|
||||
</a>
|
||||
<ul>
|
||||
{locale.supported.map(({ languageTag }) => (
|
||||
<li key={languageTag} className="kc-dropdown-item">
|
||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||
<a href="#" onClick={()=> changeLocale(languageTag)}>
|
||||
{labelBySupportedLanguageTag[languageTag]}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? (
|
||||
displayRequiredFields ? (
|
||||
<div className={clsx(props.kcContentWrapperClass)}>
|
||||
<div className={clsx(props.kcLabelWrapperClass, "subtitle")}>
|
||||
<span className="subtitle">
|
||||
<span className="required">*</span>
|
||||
{msg("requiredFields")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-md-10">
|
||||
<h1 id="kc-page-title">{headerNode}</h1>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<h1 id="kc-page-title">{headerNode}</h1>
|
||||
)
|
||||
) : displayRequiredFields ? (
|
||||
<div className={clsx(props.kcContentWrapperClass)}>
|
||||
<div className={clsx(props.kcLabelWrapperClass, "subtitle")}>
|
||||
<span className="subtitle">
|
||||
<span className="required">*</span> {msg("requiredFields")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-md-10">
|
||||
{showUsernameNode}
|
||||
<div className={clsx(props.kcFormGroupClass)}>
|
||||
<div id="kc-username">
|
||||
<label id="kc-attempted-username">{auth?.attemptedUsername}</label>
|
||||
<a id="reset-login" href={url.loginRestartFlowUrl}>
|
||||
<div className="kc-login-tooltip">
|
||||
<i className={clsx(props.kcResetFlowIcon)}></i>
|
||||
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{showUsernameNode}
|
||||
<div className={clsx(props.kcFormGroupClass)}>
|
||||
<div id="kc-username">
|
||||
<label id="kc-attempted-username">{auth?.attemptedUsername}</label>
|
||||
<a id="reset-login" href={url.loginRestartFlowUrl}>
|
||||
<div className="kc-login-tooltip">
|
||||
<i className={clsx(props.kcResetFlowIcon)}></i>
|
||||
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</header>
|
||||
<div id="kc-content">
|
||||
<div id="kc-content-wrapper">
|
||||
{/* App-initiated actions should not see warning messages about the need to complete the action during login. */}
|
||||
{displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction) && (
|
||||
<div className={clsx("alert", `alert-${message.type}`)}>
|
||||
{message.type === "success" && <span className={clsx(props.kcFeedbackSuccessIcon)}></span>}
|
||||
{message.type === "warning" && <span className={clsx(props.kcFeedbackWarningIcon)}></span>}
|
||||
{message.type === "error" && <span className={clsx(props.kcFeedbackErrorIcon)}></span>}
|
||||
{message.type === "info" && <span className={clsx(props.kcFeedbackInfoIcon)}></span>}
|
||||
<span
|
||||
className="kc-feedback-text"
|
||||
dangerouslySetInnerHTML={{
|
||||
"__html": message.summary
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{formNode}
|
||||
{auth !== undefined && auth.showTryAnotherWayLink && showAnotherWayIfPresent && (
|
||||
<form
|
||||
id="kc-select-try-another-way-form"
|
||||
action={url.loginAction}
|
||||
method="post"
|
||||
className={clsx(displayWide && props.kcContentWrapperClass)}
|
||||
>
|
||||
<div className={clsx(displayWide && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass])}>
|
||||
<div className={clsx(props.kcFormGroupClass)}>
|
||||
<input type="hidden" name="tryAnotherWay" value="on" />
|
||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||
<a href="#" id="try-another-way" onClick={()=>{
|
||||
document.forms["kc-select-try-another-way-form" as never].submit();
|
||||
return false;
|
||||
}}>
|
||||
{msg("doTryAnotherWay")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
{displayInfo && (
|
||||
<div id="kc-info" className={clsx(props.kcSignUpClass)}>
|
||||
<div id="kc-info-wrapper" className={clsx(props.kcInfoAreaWrapperClass)}>
|
||||
{infoNode}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
88
src/keycloakTheme/kcContext.ts
Normal file
88
src/keycloakTheme/kcContext.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
import { getKcContext } from "keycloakify/lib/kcContext";
|
||||
|
||||
//NOTE: In most of the cases you do not need to overload the KcContext, you can
|
||||
// just call getKcContext(...) without type arguments.
|
||||
// You want to overload the KcContext only if:
|
||||
// - You have custom plugins that add some values to the context (like https://github.com/micedre/keycloak-mail-whitelisting that adds authorizedMailDomains)
|
||||
// - You want to add support for extra pages that are not yey featured by default, see: https://docs.keycloakify.dev/contributing#adding-support-for-a-new-page
|
||||
export const { kcContext } = getKcContext<
|
||||
// NOTE: A 'keycloakify' field must be added
|
||||
// in the package.json to generate theses extra pages
|
||||
// https://docs.keycloakify.dev/build-options#keycloakify.extrapages
|
||||
| { pageId: "my-extra-page-1.ftl"; }
|
||||
| { pageId: "my-extra-page-2.ftl"; someCustomValue: string; }
|
||||
// NOTE: register.ftl is deprecated in favor of register-user-profile.ftl
|
||||
// but let's say we use it anyway and have this plugin enabled: https://github.com/micedre/keycloak-mail-whitelisting
|
||||
// keycloak-mail-whitelisting define the non standard ftl global authorizedMailDomains, we declare it here.
|
||||
| { pageId: "register.ftl"; authorizedMailDomains: string[]; }
|
||||
>({
|
||||
// Uncomment to test the login page for development.
|
||||
//"mockPageId": "login.ftl",
|
||||
mockData: [
|
||||
{
|
||||
pageId: "login.ftl",
|
||||
locale: {
|
||||
//When we test the login page we do it in french
|
||||
currentLanguageTag: "fr",
|
||||
},
|
||||
//Uncomment the following line for hiding the Alert message
|
||||
//"message": undefined
|
||||
//Uncomment the following line for showing an Error message
|
||||
//message: { type: "error", summary: "This is an error" }
|
||||
},
|
||||
{
|
||||
pageId: "my-extra-page-2.ftl",
|
||||
someCustomValue: "foo bar baz"
|
||||
},
|
||||
{
|
||||
pageId: "register.ftl",
|
||||
authorizedMailDomains: [
|
||||
"example.com",
|
||||
"another-example.com",
|
||||
"*.yet-another-example.com",
|
||||
"*.example.com",
|
||||
"hello-world.com"
|
||||
]
|
||||
},
|
||||
{
|
||||
//NOTE: You will either use register.ftl (legacy) or register-user-profile.ftl, not both
|
||||
pageId: "register-user-profile.ftl",
|
||||
locale: {
|
||||
currentLanguageTag: "fr"
|
||||
},
|
||||
profile: {
|
||||
attributes: [
|
||||
{
|
||||
validators: {
|
||||
pattern: {
|
||||
pattern: "^[a-zA-Z0-9]+$",
|
||||
"ignore.empty.value": true,
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
"error-message": "${alphanumericalCharsOnly}",
|
||||
},
|
||||
},
|
||||
//NOTE: To override the default mock value
|
||||
value: undefined,
|
||||
name: "username"
|
||||
},
|
||||
{
|
||||
validators: {
|
||||
options: {
|
||||
options: ["male", "female", "non-binary", "transgender", "intersex", "non_communicated"]
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
displayName: "${gender}",
|
||||
annotations: {},
|
||||
required: true,
|
||||
groupAnnotations: {},
|
||||
readOnly: false,
|
||||
name: "gender"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export type KcContext = NonNullable<typeof kcContext>;
|
22
src/keycloakTheme/pages/MyExtraPage1.tsx
Normal file
22
src/keycloakTheme/pages/MyExtraPage1.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import type { PageProps } from "keycloakify";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function MyExtraPage1(props: PageProps<Extract<KcContext, { pageId: "my-extra-page-1.ftl"; }>, I18n>) {
|
||||
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
headerNode={<>Header <i>text</i></>}
|
||||
formNode={
|
||||
<form>
|
||||
{/*...*/}
|
||||
</form>
|
||||
}
|
||||
infoNode={<span>footer</span> }
|
||||
/>
|
||||
);
|
||||
|
||||
}
|
25
src/keycloakTheme/pages/MyExtraPage2.tsx
Normal file
25
src/keycloakTheme/pages/MyExtraPage2.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import type { PageProps } from "keycloakify";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function MyExtraPage1(props: PageProps<Extract<KcContext, { pageId: "my-extra-page-2.ftl"; }>, I18n>) {
|
||||
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||
|
||||
// someCustomValue is declared by you in ../kcContext.ts
|
||||
console.log(`TODO: Do something with: ${kcContext.someCustomValue}`);
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
headerNode={<>Header <i>text</i></>}
|
||||
formNode={
|
||||
<form>
|
||||
{/*...*/}
|
||||
</form>
|
||||
}
|
||||
infoNode={<span>footer</span> }
|
||||
/>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,77 +1,73 @@
|
|||
// This is a copy paste from https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/components/Register.tsx
|
||||
// This is a copy paste from https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/pages/Register.tsx
|
||||
// It is now up to us to implement a special behavior to leverage the non standard authorizedMailDomains
|
||||
// provided by the plugin: https://github.com/micedre/keycloak-mail-whitelisting installed on our keycloak server.
|
||||
// Note that it is no longer recommended to use register.ftl, it's best to use register-user-profile.ftl
|
||||
// See: https://docs.keycloakify.dev/realtime-input-validation
|
||||
|
||||
import { memo } from "react";
|
||||
import Template from "keycloakify/lib/components/Template";
|
||||
import type { KcProps } from "keycloakify";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import { clsx } from "keycloakify/lib/tools/clsx";
|
||||
import type { I18n } from "./i18n";
|
||||
import type { PageProps } from "keycloakify";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
type KcContext_Register = Extract<KcContext, { pageId: "register.ftl"; }>;
|
||||
export default function Register(props: PageProps<Extract<KcContext, { pageId: "register.ftl"; }>, I18n>) {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||
|
||||
const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContext_Register; i18n: I18n; } & KcProps) => {
|
||||
const { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
console.log(`NOTE: It is up to you do do something meaningful with ${kcContext.authorizedMailDomains}`)
|
||||
console.log(`NOTE: It is up to you do do something meaningful with ${kcContext.authorizedMailDomains}`);
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, ...props }}
|
||||
doFetchDefaultThemeResources={true}
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
headerNode={msg("registerTitle")}
|
||||
formNode={
|
||||
<form id="kc-register-form" className={clsx(props.kcFormClass)} action={url.registrationAction} method="post">
|
||||
<div className={clsx(props.kcFormGroupClass, messagesPerField.printIfExists("firstName", props.kcFormGroupErrorClass))}>
|
||||
<div className={clsx(props.kcLabelWrapperClass)}>
|
||||
<label htmlFor="firstName" className={clsx(props.kcLabelClass)}>
|
||||
<form id="kc-register-form" className={clsx(kcProps.kcFormClass)} action={url.registrationAction} method="post">
|
||||
<div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("firstName", kcProps.kcFormGroupErrorClass))}>
|
||||
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||
<label htmlFor="firstName" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("firstName")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={clsx(props.kcInputWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="text"
|
||||
id="firstName"
|
||||
className={clsx(props.kcInputClass)}
|
||||
className={clsx(kcProps.kcInputClass)}
|
||||
name="firstName"
|
||||
defaultValue={register.formData.firstName ?? ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={clsx(props.kcFormGroupClass, messagesPerField.printIfExists("lastName", props.kcFormGroupErrorClass))}>
|
||||
<div className={clsx(props.kcLabelWrapperClass)}>
|
||||
<label htmlFor="lastName" className={clsx(props.kcLabelClass)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("lastName", kcProps.kcFormGroupErrorClass))}>
|
||||
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||
<label htmlFor="lastName" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("lastName")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={clsx(props.kcInputWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
className={clsx(props.kcInputClass)}
|
||||
className={clsx(kcProps.kcInputClass)}
|
||||
name="lastName"
|
||||
defaultValue={register.formData.lastName ?? ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={clsx(props.kcFormGroupClass, messagesPerField.printIfExists("email", props.kcFormGroupErrorClass))}>
|
||||
<div className={clsx(props.kcLabelWrapperClass)}>
|
||||
<label htmlFor="email" className={clsx(props.kcLabelClass)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("email", kcProps.kcFormGroupErrorClass))}>
|
||||
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||
<label htmlFor="email" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("email")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={clsx(props.kcInputWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="text"
|
||||
id="email"
|
||||
className={clsx(props.kcInputClass)}
|
||||
className={clsx(kcProps.kcInputClass)}
|
||||
name="email"
|
||||
defaultValue={register.formData.email ?? ""}
|
||||
autoComplete="email"
|
||||
|
@ -79,17 +75,17 @@ const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContext_Reg
|
|||
</div>
|
||||
</div>
|
||||
{!realm.registrationEmailAsUsername && (
|
||||
<div className={clsx(props.kcFormGroupClass, messagesPerField.printIfExists("username", props.kcFormGroupErrorClass))}>
|
||||
<div className={clsx(props.kcLabelWrapperClass)}>
|
||||
<label htmlFor="username" className={clsx(props.kcLabelClass)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("username", kcProps.kcFormGroupErrorClass))}>
|
||||
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||
<label htmlFor="username" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("username")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={clsx(props.kcInputWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
className={clsx(props.kcInputClass)}
|
||||
className={clsx(kcProps.kcInputClass)}
|
||||
name="username"
|
||||
defaultValue={register.formData.username ?? ""}
|
||||
autoComplete="username"
|
||||
|
@ -99,17 +95,19 @@ const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContext_Reg
|
|||
)}
|
||||
{passwordRequired && (
|
||||
<>
|
||||
<div className={clsx(props.kcFormGroupClass, messagesPerField.printIfExists("password", props.kcFormGroupErrorClass))}>
|
||||
<div className={clsx(props.kcLabelWrapperClass)}>
|
||||
<label htmlFor="password" className={clsx(props.kcLabelClass)}>
|
||||
<div
|
||||
className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("password", kcProps.kcFormGroupErrorClass))}
|
||||
>
|
||||
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||
<label htmlFor="password" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("password")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={clsx(props.kcInputWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
className={clsx(props.kcInputClass)}
|
||||
className={clsx(kcProps.kcInputClass)}
|
||||
name="password"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
|
@ -118,40 +116,45 @@ const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContext_Reg
|
|||
|
||||
<div
|
||||
className={clsx(
|
||||
props.kcFormGroupClass,
|
||||
messagesPerField.printIfExists("password-confirm", props.kcFormGroupErrorClass)
|
||||
kcProps.kcFormGroupClass,
|
||||
messagesPerField.printIfExists("password-confirm", kcProps.kcFormGroupErrorClass)
|
||||
)}
|
||||
>
|
||||
<div className={clsx(props.kcLabelWrapperClass)}>
|
||||
<label htmlFor="password-confirm" className={clsx(props.kcLabelClass)}>
|
||||
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||
<label htmlFor="password-confirm" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("passwordConfirm")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={clsx(props.kcInputWrapperClass)}>
|
||||
<input type="password" id="password-confirm" className={clsx(props.kcInputClass)} name="password-confirm" />
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input type="password" id="password-confirm" className={clsx(kcProps.kcInputClass)} name="password-confirm" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{recaptchaRequired && (
|
||||
<div className="form-group">
|
||||
<div className={clsx(props.kcInputWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey}></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={clsx(props.kcFormGroupClass)}>
|
||||
<div id="kc-form-options" className={clsx(props.kcFormOptionsClass)}>
|
||||
<div className={clsx(props.kcFormOptionsWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
|
||||
<div className={clsx(kcProps.kcFormOptionsWrapperClass)}>
|
||||
<span>
|
||||
<a href={url.loginUrl}>{msg("backToLogin")}</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="kc-form-buttons" className={clsx(props.kcFormButtonsClass)}>
|
||||
<div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
|
||||
<input
|
||||
className={clsx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonPrimaryClass,
|
||||
kcProps.kcButtonBlockClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
type="submit"
|
||||
value={msgStr("doRegister")}
|
||||
/>
|
||||
|
@ -161,6 +164,5 @@ const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContext_Reg
|
|||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default Register;
|
82
src/keycloakTheme/pages/Terms.tsx
Normal file
82
src/keycloakTheme/pages/Terms.tsx
Normal file
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* NOTE: Yo do not need to do all this to put your own Terms and conditions
|
||||
* this is if you want component level customization.
|
||||
* If the default works for you you can just use the useDownloadTerms hook
|
||||
* in the KcApp.tsx
|
||||
* Example: https://github.com/garronej/keycloakify-starter/blob/a20c21b2aae7c6dc6dbea294f3d321955ddf9355/src/KcApp/KcApp.tsx#L14-L30
|
||||
*/
|
||||
|
||||
import { clsx } from "keycloakify/lib/tools/clsx";
|
||||
import { useRerenderOnStateChange } from "evt/hooks";
|
||||
import { Markdown } from "keycloakify/lib/tools/Markdown";
|
||||
import { evtTermMarkdown, useDownloadTerms } from "keycloakify/lib/pages/Terms";
|
||||
import tos_en_url from "../assets/tos_en.md";
|
||||
import tos_fr_url from "../assets/tos_fr.md";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { PageProps } from "keycloakify/lib/KcProps";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function Terms(props: PageProps<Extract<KcContext, { pageId: "terms.ftl"; }>, I18n>) {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
useDownloadTerms({
|
||||
kcContext,
|
||||
"downloadTermMarkdown": async ({ currentLanguageTag }) => {
|
||||
|
||||
const markdownString = await fetch((() => {
|
||||
switch (currentLanguageTag) {
|
||||
case "fr": return tos_fr_url;
|
||||
default: return tos_en_url;
|
||||
}
|
||||
})()).then(response => response.text());
|
||||
|
||||
return markdownString;
|
||||
},
|
||||
});
|
||||
|
||||
useRerenderOnStateChange(evtTermMarkdown);
|
||||
|
||||
const { url } = kcContext;
|
||||
|
||||
if (evtTermMarkdown.state === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
displayMessage={false}
|
||||
headerNode={msg("termsTitle")}
|
||||
formNode={
|
||||
<>
|
||||
<div id="kc-terms-text">{evtTermMarkdown.state && <Markdown>{evtTermMarkdown.state}</Markdown>}</div>
|
||||
<form className="form-actions" action={url.loginAction} method="POST">
|
||||
<input
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonPrimaryClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
name="accept"
|
||||
id="kc-accept"
|
||||
type="submit"
|
||||
value={msgStr("doAccept")}
|
||||
/>
|
||||
<input
|
||||
className={clsx(kcProps.kcButtonClass, kcProps.kcButtonDefaultClass, kcProps.kcButtonLargeClass)}
|
||||
name="cancel"
|
||||
id="kc-decline"
|
||||
type="submit"
|
||||
value={msgStr("doDecline")}
|
||||
/>
|
||||
</form>
|
||||
<div className="clearfix" />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -8810,10 +8810,10 @@ typedarray-to-buffer@^3.1.5:
|
|||
dependencies:
|
||||
is-typedarray "^1.0.0"
|
||||
|
||||
typescript@^4.7.3:
|
||||
version "4.7.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
|
||||
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
|
||||
typescript@^4.9.5:
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
|
||||
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
|
||||
|
||||
unbox-primitive@^1.0.2:
|
||||
version "1.0.2"
|
||||
|
|
Loading…
Reference in a new issue