Merge remote-tracking branch 'origin/main' into lordvlad/issue125
* origin/main: Update KcApp.tsx Add RegisterUserProfile customization
This commit is contained in:
commit
3faa482df1
142
package.json
142
package.json
|
@ -1,87 +1,67 @@
|
||||||
{
|
{
|
||||||
"name": "keycloakify-starter",
|
"name": "keycloakify-starter",
|
||||||
"homepage": "https://starter.keycloakify.dev",
|
"homepage": "https://starter.keycloakify.dev",
|
||||||
"version": "3.0.0",
|
"version": "3.1.0",
|
||||||
"description": "A starter/demo project for keycloakify",
|
"description": "A starter/demo project for keycloakify",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git://github.com/codegouvfr/keycloakify-starter.git"
|
"url": "git://github.com/codegouvfr/keycloakify-starter.git"
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"start": "react-scripts start",
|
|
||||||
"build": "react-scripts build",
|
|
||||||
"build-keycloak-theme": "yarn build ; keycloakify",
|
|
||||||
"download-builtin-keycloak-theme": "download-builtin-keycloak-theme 15.0.2",
|
|
||||||
"storybook": "start-storybook -p 6006 -s public",
|
|
||||||
"build-storybook": "build-storybook -s public"
|
|
||||||
},
|
|
||||||
"keycloakify": {
|
|
||||||
"extraPages": [
|
|
||||||
"my-extra-page-1.ftl",
|
|
||||||
"my-extra-page-2.ftl"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"author": "u/garronej",
|
|
||||||
"license": "MIT",
|
|
||||||
"keywords": [],
|
|
||||||
"dependencies": {
|
|
||||||
"evt": "^2.4.15",
|
|
||||||
"jwt-decode": "^3.1.2",
|
|
||||||
"keycloak-js": "^21.0.1",
|
|
||||||
"keycloakify": "^6.12.4",
|
|
||||||
"powerhooks": "^0.26.2",
|
|
||||||
"react": "18.1.0",
|
|
||||||
"react-dom": "18.1.0",
|
|
||||||
"tsafe": "^1.4.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@storybook/addon-actions": "^6.5.16",
|
|
||||||
"@storybook/addon-essentials": "^6.5.16",
|
|
||||||
"@storybook/addon-interactions": "^6.5.16",
|
|
||||||
"@storybook/addon-links": "^6.5.16",
|
|
||||||
"@storybook/builder-webpack5": "^6.5.16",
|
|
||||||
"@storybook/manager-webpack5": "^6.5.16",
|
|
||||||
"@storybook/node-logger": "^6.5.16",
|
|
||||||
"@storybook/preset-create-react-app": "^4.1.2",
|
|
||||||
"@storybook/react": "^6.5.16",
|
|
||||||
"@storybook/testing-library": "^0.0.13",
|
|
||||||
"@types/node": "^15.3.1",
|
|
||||||
"@types/react": "18.0.9",
|
|
||||||
"@types/react-dom": "18.0.4",
|
|
||||||
"react-scripts": "5.0.0",
|
|
||||||
"typescript": "~4.8.0"
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"react-app/jest"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"react-hooks/exhaustive-deps": "off",
|
|
||||||
"@typescript-eslint/no-redeclare": "off",
|
|
||||||
"no-labels": "off"
|
|
||||||
},
|
},
|
||||||
"overrides": [
|
"scripts": {
|
||||||
{
|
"start": "react-scripts start",
|
||||||
"files": [
|
"build": "react-scripts build",
|
||||||
"**/*.stories.*"
|
"build-keycloak-theme": "yarn build && keycloakify",
|
||||||
|
"download-builtin-keycloak-theme": "download-builtin-keycloak-theme 15.0.2"
|
||||||
|
},
|
||||||
|
"keycloakify": {
|
||||||
|
"extraPages": [
|
||||||
|
"my-extra-page-1.ftl",
|
||||||
|
"my-extra-page-2.ftl"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"author": "u/garronej",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [],
|
||||||
|
"dependencies": {
|
||||||
|
"evt": "^2.4.15",
|
||||||
|
"jwt-decode": "^3.1.2",
|
||||||
|
"keycloak-js": "^21.0.1",
|
||||||
|
"keycloakify": "^6.12.7",
|
||||||
|
"powerhooks": "^0.26.2",
|
||||||
|
"react": "18.1.0",
|
||||||
|
"react-dom": "18.1.0",
|
||||||
|
"tsafe": "^1.4.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^15.3.1",
|
||||||
|
"@types/react": "18.0.9",
|
||||||
|
"@types/react-dom": "18.0.4",
|
||||||
|
"react-scripts": "5.0.0",
|
||||||
|
"typescript": "~4.8.0"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"import/no-anonymous-default-export": "off"
|
"react-hooks/exhaustive-deps": "off",
|
||||||
|
"@typescript-eslint/no-redeclare": "off",
|
||||||
|
"no-labels": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
">0.2%",
|
">0.2%",
|
||||||
"not dead",
|
"not dead",
|
||||||
"not op_mini all"
|
"not op_mini all"
|
||||||
],
|
],
|
||||||
"development": [
|
"development": [
|
||||||
"last 1 chrome version",
|
"last 1 chrome version",
|
||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -5,13 +5,15 @@ import { useI18n } from "./i18n";
|
||||||
import Fallback, { defaultKcProps, type KcProps, type PageProps } from "keycloakify";
|
import Fallback, { defaultKcProps, type KcProps, type PageProps } from "keycloakify";
|
||||||
import Template from "./Template";
|
import Template from "./Template";
|
||||||
import DefaultTemplate from "keycloakify/lib/Template";
|
import DefaultTemplate from "keycloakify/lib/Template";
|
||||||
import { foo, bar } from "./valuesTransferredOverUrl";
|
|
||||||
|
|
||||||
console.log(`Values passed by the main app in the URL parameter:`, { foo, bar });
|
// You can uncomment this to see the values passed by the main app before redirecting.
|
||||||
|
//import { foo, bar } from "./valuesTransferredOverUrl";
|
||||||
|
//console.log(`Values passed by the main app in the URL parameter:`, { foo, bar });
|
||||||
|
|
||||||
const Login = lazy(()=> import("./pages/Login"));
|
const Login = lazy(()=> import("./pages/Login"));
|
||||||
// If you can, favor register-user-profile.ftl over register.ftl, see: https://docs.keycloakify.dev/realtime-input-validation
|
// If you can, favor register-user-profile.ftl over register.ftl, see: https://docs.keycloakify.dev/realtime-input-validation
|
||||||
const Register = lazy(() => import("./pages/Register"));
|
const Register = lazy(() => import("./pages/Register"));
|
||||||
|
const RegisterUserProfile = lazy(() => import("./pages/RegisterUserProfile"));
|
||||||
const Terms = lazy(() => import("./pages/Terms"));
|
const Terms = lazy(() => import("./pages/Terms"));
|
||||||
const MyExtraPage1 = lazy(() => import("./pages/MyExtraPage1"));
|
const MyExtraPage1 = lazy(() => import("./pages/MyExtraPage1"));
|
||||||
const MyExtraPage2 = lazy(() => import("./pages/MyExtraPage2"));
|
const MyExtraPage2 = lazy(() => import("./pages/MyExtraPage2"));
|
||||||
|
@ -61,6 +63,7 @@ export default function App(props: { kcContext: KcContext; }) {
|
||||||
switch (kcContext.pageId) {
|
switch (kcContext.pageId) {
|
||||||
case "login.ftl": return <Login {...{ kcContext, ...pageProps }} />;
|
case "login.ftl": return <Login {...{ kcContext, ...pageProps }} />;
|
||||||
case "register.ftl": return <Register {...{ kcContext, ...pageProps }} />;
|
case "register.ftl": return <Register {...{ kcContext, ...pageProps }} />;
|
||||||
|
case "register-user-profile.ftl": return <RegisterUserProfile {...{ kcContext, ...pageProps }} />
|
||||||
case "terms.ftl": return <Terms {...{ kcContext, ...pageProps }} />;
|
case "terms.ftl": return <Terms {...{ kcContext, ...pageProps }} />;
|
||||||
case "my-extra-page-1.ftl": return <MyExtraPage1 {...{ kcContext, ...pageProps }} />;
|
case "my-extra-page-1.ftl": return <MyExtraPage1 {...{ kcContext, ...pageProps }} />;
|
||||||
case "my-extra-page-2.ftl": return <MyExtraPage2 {...{ kcContext, ...pageProps }} />;
|
case "my-extra-page-2.ftl": return <MyExtraPage2 {...{ kcContext, ...pageProps }} />;
|
||||||
|
|
61
src/keycloak-theme/pages/RegisterUserProfile.tsx
Normal file
61
src/keycloak-theme/pages/RegisterUserProfile.tsx
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
// Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/pages/RegisterUserProfile.tsx
|
||||||
|
import { useState } from "react";
|
||||||
|
import { clsx } from "keycloakify/lib/tools/clsx";
|
||||||
|
import { UserProfileFormFields } from "./shared/UserProfileCommons";
|
||||||
|
import type { PageProps } from "keycloakify/lib/KcProps";
|
||||||
|
import type { KcContext } from "../kcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
export default function RegisterUserProfile(props: PageProps<Extract<KcContext, { pageId: "register-user-profile.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
|
||||||
|
|
||||||
|
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
||||||
|
|
||||||
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
|
const [isFomSubmittable, setIsFomSubmittable] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||||
|
displayMessage={messagesPerField.exists("global")}
|
||||||
|
displayRequiredFields={true}
|
||||||
|
headerNode={msg("registerTitle")}
|
||||||
|
formNode={
|
||||||
|
<form id="kc-register-form" className={clsx(kcProps.kcFormClass)} action={url.registrationAction} method="post">
|
||||||
|
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...kcProps} />
|
||||||
|
{recaptchaRequired && (
|
||||||
|
<div className="form-group">
|
||||||
|
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||||
|
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={clsx(kcProps.kcFormGroupClass)} style={{ "marginBottom": 30 }}>
|
||||||
|
<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(kcProps.kcFormButtonsClass)}>
|
||||||
|
<input
|
||||||
|
className={clsx(
|
||||||
|
kcProps.kcButtonClass,
|
||||||
|
kcProps.kcButtonPrimaryClass,
|
||||||
|
kcProps.kcButtonBlockClass,
|
||||||
|
kcProps.kcButtonLargeClass
|
||||||
|
)}
|
||||||
|
type="submit"
|
||||||
|
value={msgStr("doRegister")}
|
||||||
|
disabled={!isFomSubmittable}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
176
src/keycloak-theme/pages/shared/UserProfileCommons.tsx
Normal file
176
src/keycloak-theme/pages/shared/UserProfileCommons.tsx
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
//NOTE: Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/pages/shared/UserProfileCommons.tsx
|
||||||
|
|
||||||
|
import { useEffect, Fragment } from "react";
|
||||||
|
import type { KcProps } from "keycloakify/lib/KcProps";
|
||||||
|
import { clsx } from "keycloakify/lib/tools/clsx";
|
||||||
|
import type { I18nBase } from "keycloakify/lib/i18n";
|
||||||
|
import type { Attribute } from "keycloakify/lib/getKcContext";
|
||||||
|
import { useFormValidation } from "keycloakify/lib/pages/shared/UserProfileCommons";
|
||||||
|
|
||||||
|
export type UserProfileFormFieldsProps = {
|
||||||
|
kcContext: Parameters<typeof useFormValidation>[0]["kcContext"];
|
||||||
|
i18n: I18nBase;
|
||||||
|
} & KcProps &
|
||||||
|
Partial<Record<"BeforeField" | "AfterField", (props: { attribute: Attribute }) => JSX.Element | null>> & {
|
||||||
|
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function UserProfileFormFields({
|
||||||
|
kcContext,
|
||||||
|
onIsFormSubmittableValueChange,
|
||||||
|
i18n,
|
||||||
|
BeforeField,
|
||||||
|
AfterField,
|
||||||
|
...props
|
||||||
|
}: UserProfileFormFieldsProps) {
|
||||||
|
const { advancedMsg } = i18n;
|
||||||
|
|
||||||
|
const {
|
||||||
|
formValidationState: { fieldStateByAttributeName, isFormSubmittable },
|
||||||
|
formValidationDispatch,
|
||||||
|
attributesWithPassword
|
||||||
|
} = useFormValidation({
|
||||||
|
kcContext,
|
||||||
|
i18n
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onIsFormSubmittableValueChange(isFormSubmittable);
|
||||||
|
}, [isFormSubmittable]);
|
||||||
|
|
||||||
|
let currentGroup = "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{attributesWithPassword.map((attribute, i) => {
|
||||||
|
const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute;
|
||||||
|
|
||||||
|
const { value, displayableErrors } = fieldStateByAttributeName[attribute.name];
|
||||||
|
|
||||||
|
const formGroupClassName = clsx(props.kcFormGroupClass, displayableErrors.length !== 0 && props.kcFormGroupErrorClass);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment key={i}>
|
||||||
|
{group !== currentGroup && (currentGroup = group) !== "" && (
|
||||||
|
<div className={formGroupClassName}>
|
||||||
|
<div className={clsx(props.kcContentWrapperClass)}>
|
||||||
|
<label id={`header-${group}`} className={clsx(props.kcFormGroupHeader)}>
|
||||||
|
{advancedMsg(groupDisplayHeader) || currentGroup}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{groupDisplayDescription !== "" && (
|
||||||
|
<div className={clsx(props.kcLabelWrapperClass)}>
|
||||||
|
<label id={`description-${group}`} className={`${clsx(props.kcLabelClass)}`}>
|
||||||
|
{advancedMsg(groupDisplayDescription)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{BeforeField && <BeforeField attribute={attribute} />}
|
||||||
|
|
||||||
|
<div className={formGroupClassName}>
|
||||||
|
<div className={clsx(props.kcLabelWrapperClass)}>
|
||||||
|
<label htmlFor={attribute.name} className={clsx(props.kcLabelClass)}>
|
||||||
|
{advancedMsg(attribute.displayName ?? "")}
|
||||||
|
</label>
|
||||||
|
{attribute.required && <>*</>}
|
||||||
|
</div>
|
||||||
|
<div className={clsx(props.kcInputWrapperClass)}>
|
||||||
|
{(() => {
|
||||||
|
const { options } = attribute.validators;
|
||||||
|
|
||||||
|
if (options !== undefined) {
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
id={attribute.name}
|
||||||
|
name={attribute.name}
|
||||||
|
onChange={event =>
|
||||||
|
formValidationDispatch({
|
||||||
|
"action": "update value",
|
||||||
|
"name": attribute.name,
|
||||||
|
"newValue": event.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onBlur={() =>
|
||||||
|
formValidationDispatch({
|
||||||
|
"action": "focus lost",
|
||||||
|
"name": attribute.name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
value={value}
|
||||||
|
>
|
||||||
|
{options.options.map(option => (
|
||||||
|
<option key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={(() => {
|
||||||
|
switch (attribute.name) {
|
||||||
|
case "password-confirm":
|
||||||
|
case "password":
|
||||||
|
return "password";
|
||||||
|
default:
|
||||||
|
return "text";
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
id={attribute.name}
|
||||||
|
name={attribute.name}
|
||||||
|
value={value}
|
||||||
|
onChange={event =>
|
||||||
|
formValidationDispatch({
|
||||||
|
"action": "update value",
|
||||||
|
"name": attribute.name,
|
||||||
|
"newValue": event.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onBlur={() =>
|
||||||
|
formValidationDispatch({
|
||||||
|
"action": "focus lost",
|
||||||
|
"name": attribute.name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className={clsx(props.kcInputClass)}
|
||||||
|
aria-invalid={displayableErrors.length !== 0}
|
||||||
|
disabled={attribute.readOnly}
|
||||||
|
autoComplete={attribute.autocomplete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
{displayableErrors.length !== 0 &&
|
||||||
|
(() => {
|
||||||
|
const divId = `input-error-${attribute.name}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style>{`#${divId} > span: { display: block; }`}</style>
|
||||||
|
<span
|
||||||
|
id={divId}
|
||||||
|
className={clsx(props.kcInputErrorMessageClass)}
|
||||||
|
style={{
|
||||||
|
"position": displayableErrors.length === 1 ? "absolute" : undefined
|
||||||
|
}}
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
{displayableErrors.map(({ errorMessage }) => errorMessage)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{AfterField && <AfterField attribute={attribute} />}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -10336,15 +10336,15 @@ keycloak-js@^21.0.1:
|
||||||
base64-js "^1.5.1"
|
base64-js "^1.5.1"
|
||||||
js-sha256 "^0.9.0"
|
js-sha256 "^0.9.0"
|
||||||
|
|
||||||
keycloakify@^6.12.4:
|
keycloakify@^6.12.7:
|
||||||
version "6.12.4"
|
version "6.12.7"
|
||||||
resolved "https://registry.yarnpkg.com/keycloakify/-/keycloakify-6.12.4.tgz#96aa3cab5e3f76e550f20e9b4de7e6b425df79ad"
|
resolved "https://registry.yarnpkg.com/keycloakify/-/keycloakify-6.12.7.tgz#7ec97117c6b83be13999cd95cb01d27eaf4411a9"
|
||||||
integrity sha512-Po3GfnAsAUVVEzIuNiVi/Wz2Jxb4VCLHre8FheLAZilt0MFZ6h2NJ5EK7oaCb6I5pe4Ip4LsxnrM2DP2/kqcaQ==
|
integrity sha512-qCPrkD6bDjXh2/8ISqLga056Y0eExAspfn1pWZE6LE5DP7gIUsAieOCR1tgswGXxGJfgBZn2IgU2UYipm5FnYg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@octokit/rest" "^18.12.0"
|
"@octokit/rest" "^18.12.0"
|
||||||
cheerio "^1.0.0-rc.5"
|
cheerio "^1.0.0-rc.5"
|
||||||
cli-select "^1.1.2"
|
cli-select "^1.1.2"
|
||||||
evt "^2.4.13"
|
evt "^2.4.15"
|
||||||
minimal-polyfills "^2.2.2"
|
minimal-polyfills "^2.2.2"
|
||||||
minimist "^1.2.6"
|
minimist "^1.2.6"
|
||||||
path-browserify "^1.0.1"
|
path-browserify "^1.0.1"
|
||||||
|
|
Loading…
Reference in a new issue