keycloakify 6.1.0 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/bin/keycloakify/generateFtl/generateFtl.d.ts +1 -1
  2. package/bin/keycloakify/generateFtl/generateFtl.js +2 -1
  3. package/bin/keycloakify/generateFtl/generateFtl.js.map +1 -1
  4. package/bin/tsconfig.tsbuildinfo +1 -1
  5. package/lib/components/KcApp.js +3 -0
  6. package/lib/components/KcApp.js.map +1 -1
  7. package/lib/components/RegisterUserProfile.js +3 -62
  8. package/lib/components/RegisterUserProfile.js.map +1 -1
  9. package/lib/components/UpdateUserProfile.d.ts +9 -0
  10. package/lib/components/UpdateUserProfile.js +32 -0
  11. package/lib/components/UpdateUserProfile.js.map +1 -0
  12. package/lib/components/shared/UserProfileCommons.d.ts +17 -0
  13. package/lib/components/shared/UserProfileCommons.js +76 -0
  14. package/lib/components/shared/UserProfileCommons.js.map +1 -0
  15. package/lib/getKcContext/KcContextBase.d.ts +9 -1
  16. package/lib/getKcContext/KcContextBase.js.map +1 -1
  17. package/lib/getKcContext/kcContextMocks/kcContextMocks.js +103 -98
  18. package/lib/getKcContext/kcContextMocks/kcContextMocks.js.map +1 -1
  19. package/lib/tsconfig.tsbuildinfo +1 -1
  20. package/lib/useFormValidationSlice.d.ts +1 -1
  21. package/package.json +9 -1
  22. package/src/bin/keycloakify/generateFtl/generateFtl.ts +2 -1
  23. package/src/lib/components/KcApp.tsx +3 -0
  24. package/src/lib/components/RegisterUserProfile.tsx +10 -157
  25. package/src/lib/components/UpdateUserProfile.tsx +77 -0
  26. package/src/lib/components/shared/UserProfileCommons.tsx +172 -0
  27. package/src/lib/getKcContext/KcContextBase.ts +11 -1
  28. package/src/lib/getKcContext/kcContextMocks/kcContextMocks.ts +105 -98
  29. package/src/lib/useFormValidationSlice.tsx +1 -1
@@ -33,7 +33,7 @@ export declare function useFormValidationSlice(params: {
33
33
  profile: {
34
34
  attributes: Attribute[];
35
35
  };
36
- passwordRequired: boolean;
36
+ passwordRequired?: boolean;
37
37
  realm: {
38
38
  registrationEmailAsUsername: boolean;
39
39
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keycloakify",
3
- "version": "6.1.0",
3
+ "version": "6.2.0",
4
4
  "description": "Keycloak theme generator for Reacts app",
5
5
  "repository": {
6
6
  "type": "git",
@@ -78,6 +78,8 @@
78
78
  "src/lib/components/RegisterUserProfile.tsx",
79
79
  "src/lib/components/Template.tsx",
80
80
  "src/lib/components/Terms.tsx",
81
+ "src/lib/components/UpdateUserProfile.tsx",
82
+ "src/lib/components/shared/UserProfileCommons.tsx",
81
83
  "src/lib/getKcContext/KcContextBase.ts",
82
84
  "src/lib/getKcContext/getKcContext.ts",
83
85
  "src/lib/getKcContext/getKcContextFromWindow.ts",
@@ -487,6 +489,12 @@
487
489
  "lib/components/Terms.d.ts",
488
490
  "lib/components/Terms.js",
489
491
  "lib/components/Terms.js.map",
492
+ "lib/components/UpdateUserProfile.d.ts",
493
+ "lib/components/UpdateUserProfile.js",
494
+ "lib/components/UpdateUserProfile.js.map",
495
+ "lib/components/shared/UserProfileCommons.d.ts",
496
+ "lib/components/shared/UserProfileCommons.js",
497
+ "lib/components/shared/UserProfileCommons.js.map",
490
498
  "lib/getKcContext/KcContextBase.d.ts",
491
499
  "lib/getKcContext/KcContextBase.js",
492
500
  "lib/getKcContext/KcContextBase.js.map",
@@ -27,7 +27,8 @@ export const pageIds = [
27
27
  "login-idp-link-email.ftl",
28
28
  "login-page-expired.ftl",
29
29
  "login-config-totp.ftl",
30
- "logout-confirm.ftl"
30
+ "logout-confirm.ftl",
31
+ "update-user-profile.ftl"
31
32
  ] as const;
32
33
 
33
34
  export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
@@ -20,6 +20,7 @@ const LoginPageExpired = lazy(() => import("./LoginPageExpired"));
20
20
  const LoginIdpLinkEmail = lazy(() => import("./LoginIdpLinkEmail"));
21
21
  const LoginConfigTotp = lazy(() => import("./LoginConfigTotp"));
22
22
  const LogoutConfirm = lazy(() => import("./LogoutConfirm"));
23
+ const UpdateUserProfile = lazy(() => import("./UpdateUserProfile"));
23
24
 
24
25
  const KcApp = memo(({ kcContext, i18n: userProvidedI18n, ...kcProps }: { kcContext: KcContextBase; i18n?: I18n } & KcProps) => {
25
26
  const i18n = (function useClosure() {
@@ -74,6 +75,8 @@ const KcApp = memo(({ kcContext, i18n: userProvidedI18n, ...kcProps }: { kcConte
74
75
  return <LoginConfigTotp {...{ kcContext, ...props }} />;
75
76
  case "logout-confirm.ftl":
76
77
  return <LogoutConfirm {...{ kcContext, ...props }} />;
78
+ case "update-user-profile.ftl":
79
+ return <UpdateUserProfile {...{ kcContext, ...props }} />;
77
80
  }
78
81
  })()}
79
82
  </Suspense>
@@ -1,12 +1,10 @@
1
- import React, { useMemo, memo, useEffect, useState, Fragment } from "react";
1
+ import React, { useMemo, memo, useState } from "react";
2
2
  import Template from "./Template";
3
3
  import type { KcProps } from "./KcProps";
4
- import type { KcContextBase, Attribute } from "../getKcContext/KcContextBase";
4
+ import type { KcContextBase } from "../getKcContext/KcContextBase";
5
5
  import { useCssAndCx } from "../tools/useCssAndCx";
6
- import type { ReactComponent } from "../tools/ReactComponent";
7
- import { useCallbackFactory } from "powerhooks/useCallbackFactory";
8
- import { useFormValidationSlice } from "../useFormValidationSlice";
9
6
  import type { I18n } from "../i18n";
7
+ import { UserProfileFormFields } from "./shared/UserProfileCommons";
10
8
 
11
9
  const RegisterUserProfile = memo(({ kcContext, i18n, ...props_ }: { kcContext: KcContextBase.RegisterUserProfile; i18n: I18n } & KcProps) => {
12
10
  const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
@@ -34,7 +32,13 @@ const RegisterUserProfile = memo(({ kcContext, i18n, ...props_ }: { kcContext: K
34
32
  headerNode={msg("registerTitle")}
35
33
  formNode={
36
34
  <form id="kc-register-form" className={cx(props.kcFormClass)} action={url.registrationAction} method="post">
37
- <UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...props} />
35
+ <UserProfileFormFields
36
+ kcContext={kcContext}
37
+ doInsertPasswordFields={true}
38
+ onIsFormSubmittableValueChange={setIsFomSubmittable}
39
+ i18n={i18n}
40
+ {...props}
41
+ />
38
42
  {recaptchaRequired && (
39
43
  <div className="form-group">
40
44
  <div className={cx(props.kcInputWrapperClass)}>
@@ -66,155 +70,4 @@ const RegisterUserProfile = memo(({ kcContext, i18n, ...props_ }: { kcContext: K
66
70
  );
67
71
  });
68
72
 
69
- type UserProfileFormFieldsProps = { kcContext: KcContextBase.RegisterUserProfile; i18n: I18n } & KcProps &
70
- Partial<Record<"BeforeField" | "AfterField", ReactComponent<{ attribute: Attribute }>>> & {
71
- onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
72
- };
73
-
74
- const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange, i18n, ...props }: UserProfileFormFieldsProps) => {
75
- const { cx, css } = useCssAndCx();
76
-
77
- const { advancedMsg } = i18n;
78
-
79
- const {
80
- formValidationState: { fieldStateByAttributeName, isFormSubmittable },
81
- formValidationReducer,
82
- attributesWithPassword
83
- } = useFormValidationSlice({
84
- kcContext,
85
- i18n
86
- });
87
-
88
- useEffect(() => {
89
- onIsFormSubmittableValueChange(isFormSubmittable);
90
- }, [isFormSubmittable]);
91
-
92
- const onChangeFactory = useCallbackFactory(
93
- (
94
- [name]: [string],
95
- [
96
- {
97
- target: { value }
98
- }
99
- ]: [React.ChangeEvent<HTMLInputElement | HTMLSelectElement>]
100
- ) =>
101
- formValidationReducer({
102
- "action": "update value",
103
- name,
104
- "newValue": value
105
- })
106
- );
107
-
108
- const onBlurFactory = useCallbackFactory(([name]: [string]) =>
109
- formValidationReducer({
110
- "action": "focus lost",
111
- name
112
- })
113
- );
114
-
115
- let currentGroup = "";
116
-
117
- return (
118
- <>
119
- {attributesWithPassword.map((attribute, i) => {
120
- const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute;
121
-
122
- const { value, displayableErrors } = fieldStateByAttributeName[attribute.name];
123
-
124
- const formGroupClassName = cx(props.kcFormGroupClass, displayableErrors.length !== 0 && props.kcFormGroupErrorClass);
125
-
126
- return (
127
- <Fragment key={i}>
128
- {group !== currentGroup && (currentGroup = group) !== "" && (
129
- <div className={formGroupClassName}>
130
- <div className={cx(props.kcContentWrapperClass)}>
131
- <label id={`header-${group}`} className={cx(props.kcFormGroupHeader)}>
132
- {advancedMsg(groupDisplayHeader) || currentGroup}
133
- </label>
134
- </div>
135
- {groupDisplayDescription !== "" && (
136
- <div className={cx(props.kcLabelWrapperClass)}>
137
- <label id={`description-${group}`} className={`${cx(props.kcLabelClass)}`}>
138
- {advancedMsg(groupDisplayDescription)}
139
- </label>
140
- </div>
141
- )}
142
- </div>
143
- )}
144
- <div className={formGroupClassName}>
145
- <div className={cx(props.kcLabelWrapperClass)}>
146
- <label htmlFor={attribute.name} className={cx(props.kcLabelClass)}>
147
- {advancedMsg(attribute.displayName ?? "")}
148
- </label>
149
- {attribute.required && <>*</>}
150
- </div>
151
- <div className={cx(props.kcInputWrapperClass)}>
152
- {(() => {
153
- const { options } = attribute.validators;
154
-
155
- if (options !== undefined) {
156
- return (
157
- <select
158
- id={attribute.name}
159
- name={attribute.name}
160
- onChange={onChangeFactory(attribute.name)}
161
- onBlur={onBlurFactory(attribute.name)}
162
- value={value}
163
- >
164
- {options.options.map(option => (
165
- <option key={option} value={option}>
166
- {option}
167
- </option>
168
- ))}
169
- </select>
170
- );
171
- }
172
-
173
- return (
174
- <input
175
- type={(() => {
176
- switch (attribute.name) {
177
- case "password-confirm":
178
- case "password":
179
- return "password";
180
- default:
181
- return "text";
182
- }
183
- })()}
184
- id={attribute.name}
185
- name={attribute.name}
186
- value={value}
187
- onChange={onChangeFactory(attribute.name)}
188
- className={cx(props.kcInputClass)}
189
- aria-invalid={displayableErrors.length !== 0}
190
- disabled={attribute.readOnly}
191
- autoComplete={attribute.autocomplete}
192
- onBlur={onBlurFactory(attribute.name)}
193
- />
194
- );
195
- })()}
196
- {displayableErrors.length !== 0 && (
197
- <span
198
- id={`input-error-${attribute.name}`}
199
- className={cx(
200
- props.kcInputErrorMessageClass,
201
- css({
202
- "position": displayableErrors.length === 1 ? "absolute" : undefined,
203
- "& > span": { "display": "block" }
204
- })
205
- )}
206
- aria-live="polite"
207
- >
208
- {displayableErrors.map(({ errorMessage }) => errorMessage)}
209
- </span>
210
- )}
211
- </div>
212
- </div>
213
- </Fragment>
214
- );
215
- })}
216
- </>
217
- );
218
- });
219
-
220
73
  export default RegisterUserProfile;
@@ -0,0 +1,77 @@
1
+ import React, { useState, memo } from "react";
2
+ import Template from "./Template";
3
+ import type { KcProps } from "./KcProps";
4
+ import type { KcContextBase } from "../getKcContext/KcContextBase";
5
+ import { useCssAndCx } from "../tools/useCssAndCx";
6
+ import type { I18n } from "../i18n";
7
+ import { UserProfileFormFields } from "./shared/UserProfileCommons";
8
+
9
+ const LoginUpdateProfile = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.UpdateUserProfile; i18n: I18n } & KcProps) => {
10
+ const { cx } = useCssAndCx();
11
+
12
+ const { msg, msgStr } = i18n;
13
+
14
+ const { url, isAppInitiatedAction } = kcContext;
15
+
16
+ const [isFomSubmittable, setIsFomSubmittable] = useState(false);
17
+
18
+ return (
19
+ <Template
20
+ {...{ kcContext, i18n, ...props }}
21
+ doFetchDefaultThemeResources={true}
22
+ headerNode={msg("loginProfileTitle")}
23
+ formNode={
24
+ <form id="kc-update-profile-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
25
+ <UserProfileFormFields
26
+ kcContext={kcContext}
27
+ doInsertPasswordFields={true}
28
+ onIsFormSubmittableValueChange={setIsFomSubmittable}
29
+ i18n={i18n}
30
+ {...props}
31
+ />
32
+
33
+ <div className={cx(props.kcFormGroupClass)}>
34
+ <div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
35
+ <div className={cx(props.kcFormOptionsWrapperClass)}></div>
36
+ </div>
37
+
38
+ <div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
39
+ {isAppInitiatedAction ? (
40
+ <>
41
+ <input
42
+ className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
43
+ type="submit"
44
+ value={msgStr("doSubmit")}
45
+ />
46
+ <button
47
+ className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)}
48
+ type="submit"
49
+ name="cancel-aia"
50
+ value="true"
51
+ formNoValidate
52
+ >
53
+ {msg("doCancel")}
54
+ </button>
55
+ </>
56
+ ) : (
57
+ <input
58
+ className={cx(
59
+ props.kcButtonClass,
60
+ props.kcButtonPrimaryClass,
61
+ props.kcButtonBlockClass,
62
+ props.kcButtonLargeClass
63
+ )}
64
+ type="submit"
65
+ defaultValue={msgStr("doSubmit")}
66
+ disabled={!isFomSubmittable}
67
+ />
68
+ )}
69
+ </div>
70
+ </div>
71
+ </form>
72
+ }
73
+ />
74
+ );
75
+ });
76
+
77
+ export default LoginUpdateProfile;
@@ -0,0 +1,172 @@
1
+ import React, { memo, useEffect, Fragment } from "react";
2
+ import type { KcProps } from "../KcProps";
3
+ import type { Attribute } from "../../getKcContext/KcContextBase";
4
+ import { useCssAndCx } from "../../tools/useCssAndCx";
5
+ import type { ReactComponent } from "../../tools/ReactComponent";
6
+ import { useCallbackFactory } from "powerhooks/useCallbackFactory";
7
+ import { useFormValidationSlice } from "../../useFormValidationSlice";
8
+ import type { I18n } from "../../i18n";
9
+ import type { Param0 } from "tsafe/Param0";
10
+
11
+ export type UserProfileFormFieldsProps = {
12
+ //kcContext: KcContextBase.RegisterUserProfile;
13
+ kcContext: Param0<typeof useFormValidationSlice>["kcContext"];
14
+ doInsertPasswordFields: boolean;
15
+ i18n: I18n;
16
+ } & KcProps &
17
+ Partial<Record<"BeforeField" | "AfterField", ReactComponent<{ attribute: Attribute }>>> & {
18
+ onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
19
+ };
20
+
21
+ export const UserProfileFormFields = memo(
22
+ ({ kcContext, doInsertPasswordFields, onIsFormSubmittableValueChange, i18n, BeforeField, AfterField, ...props }: UserProfileFormFieldsProps) => {
23
+ const { cx, css } = useCssAndCx();
24
+
25
+ const { advancedMsg } = i18n;
26
+
27
+ const {
28
+ formValidationState: { fieldStateByAttributeName, isFormSubmittable },
29
+ formValidationReducer,
30
+ attributesWithPassword
31
+ } = useFormValidationSlice({
32
+ kcContext,
33
+ i18n
34
+ });
35
+
36
+ useEffect(() => {
37
+ onIsFormSubmittableValueChange(isFormSubmittable);
38
+ }, [isFormSubmittable]);
39
+
40
+ const onChangeFactory = useCallbackFactory(
41
+ (
42
+ [name]: [string],
43
+ [
44
+ {
45
+ target: { value }
46
+ }
47
+ ]: [React.ChangeEvent<HTMLInputElement | HTMLSelectElement>]
48
+ ) =>
49
+ formValidationReducer({
50
+ "action": "update value",
51
+ name,
52
+ "newValue": value
53
+ })
54
+ );
55
+
56
+ const onBlurFactory = useCallbackFactory(([name]: [string]) =>
57
+ formValidationReducer({
58
+ "action": "focus lost",
59
+ name
60
+ })
61
+ );
62
+
63
+ let currentGroup = "";
64
+
65
+ return (
66
+ <>
67
+ {(doInsertPasswordFields ? attributesWithPassword : kcContext.profile.attributes).map((attribute, i) => {
68
+ const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute;
69
+
70
+ const { value, displayableErrors } = fieldStateByAttributeName[attribute.name];
71
+
72
+ const formGroupClassName = cx(props.kcFormGroupClass, displayableErrors.length !== 0 && props.kcFormGroupErrorClass);
73
+
74
+ return (
75
+ <Fragment key={i}>
76
+ {group !== currentGroup && (currentGroup = group) !== "" && (
77
+ <div className={formGroupClassName}>
78
+ <div className={cx(props.kcContentWrapperClass)}>
79
+ <label id={`header-${group}`} className={cx(props.kcFormGroupHeader)}>
80
+ {advancedMsg(groupDisplayHeader) || currentGroup}
81
+ </label>
82
+ </div>
83
+ {groupDisplayDescription !== "" && (
84
+ <div className={cx(props.kcLabelWrapperClass)}>
85
+ <label id={`description-${group}`} className={`${cx(props.kcLabelClass)}`}>
86
+ {advancedMsg(groupDisplayDescription)}
87
+ </label>
88
+ </div>
89
+ )}
90
+ </div>
91
+ )}
92
+
93
+ {BeforeField && <BeforeField attribute={attribute} />}
94
+
95
+ <div className={formGroupClassName}>
96
+ <div className={cx(props.kcLabelWrapperClass)}>
97
+ <label htmlFor={attribute.name} className={cx(props.kcLabelClass)}>
98
+ {advancedMsg(attribute.displayName ?? "")}
99
+ </label>
100
+ {attribute.required && <>*</>}
101
+ </div>
102
+ <div className={cx(props.kcInputWrapperClass)}>
103
+ {(() => {
104
+ const { options } = attribute.validators;
105
+
106
+ if (options !== undefined) {
107
+ return (
108
+ <select
109
+ id={attribute.name}
110
+ name={attribute.name}
111
+ onChange={onChangeFactory(attribute.name)}
112
+ onBlur={onBlurFactory(attribute.name)}
113
+ value={value}
114
+ >
115
+ {options.options.map(option => (
116
+ <option key={option} value={option}>
117
+ {option}
118
+ </option>
119
+ ))}
120
+ </select>
121
+ );
122
+ }
123
+
124
+ return (
125
+ <input
126
+ type={(() => {
127
+ switch (attribute.name) {
128
+ case "password-confirm":
129
+ case "password":
130
+ return "password";
131
+ default:
132
+ return "text";
133
+ }
134
+ })()}
135
+ id={attribute.name}
136
+ name={attribute.name}
137
+ value={value}
138
+ onChange={onChangeFactory(attribute.name)}
139
+ className={cx(props.kcInputClass)}
140
+ aria-invalid={displayableErrors.length !== 0}
141
+ disabled={attribute.readOnly}
142
+ autoComplete={attribute.autocomplete}
143
+ onBlur={onBlurFactory(attribute.name)}
144
+ />
145
+ );
146
+ })()}
147
+ {displayableErrors.length !== 0 && (
148
+ <span
149
+ id={`input-error-${attribute.name}`}
150
+ className={cx(
151
+ props.kcInputErrorMessageClass,
152
+ css({
153
+ "position": displayableErrors.length === 1 ? "absolute" : undefined,
154
+ "& > span": { "display": "block" }
155
+ })
156
+ )}
157
+ aria-live="polite"
158
+ >
159
+ {displayableErrors.map(({ errorMessage }) => errorMessage)}
160
+ </span>
161
+ )}
162
+ </div>
163
+ </div>
164
+
165
+ {AfterField && <AfterField attribute={attribute} />}
166
+ </Fragment>
167
+ );
168
+ })}
169
+ </>
170
+ );
171
+ }
172
+ );
@@ -25,7 +25,8 @@ export type KcContextBase =
25
25
  | KcContextBase.LoginIdpLinkEmail
26
26
  | KcContextBase.LoginPageExpired
27
27
  | KcContextBase.LoginConfigTotp
28
- | KcContextBase.LogoutConfirm;
28
+ | KcContextBase.LogoutConfirm
29
+ | KcContextBase.UpdateUserProfile;
29
30
 
30
31
  export declare namespace KcContextBase {
31
32
  export type Common = {
@@ -270,6 +271,15 @@ export declare namespace KcContextBase {
270
271
  skipLink?: boolean;
271
272
  };
272
273
  };
274
+
275
+ export type UpdateUserProfile = Common & {
276
+ pageId: "update-user-profile.ftl";
277
+ profile: {
278
+ context: "REGISTRATION_PROFILE";
279
+ attributes: Attribute[];
280
+ attributesByName: Record<string, Attribute>;
281
+ };
282
+ };
273
283
  }
274
284
 
275
285
  export type Attribute = {