keycloakify 11.3.7 → 11.3.9-rc.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 (36) hide show
  1. package/README.md +1 -0
  2. package/bin/526.index.js +4 -4
  3. package/login/lib/getUserProfileApi/getUserProfileApi.d.ts +88 -0
  4. package/login/lib/getUserProfileApi/getUserProfileApi.js +1076 -0
  5. package/login/lib/getUserProfileApi/getUserProfileApi.js.map +1 -0
  6. package/login/lib/getUserProfileApi/index.d.ts +1 -0
  7. package/login/lib/getUserProfileApi/index.js +2 -0
  8. package/login/lib/getUserProfileApi/index.js.map +1 -0
  9. package/login/lib/getUserProfileApi/kcNumberUnFormat.d.ts +1 -0
  10. package/login/lib/getUserProfileApi/kcNumberUnFormat.js +84 -0
  11. package/login/lib/getUserProfileApi/kcNumberUnFormat.js.map +1 -0
  12. package/login/lib/useUserProfileForm.d.ts +9 -34
  13. package/login/lib/useUserProfileForm.js +44 -998
  14. package/login/lib/useUserProfileForm.js.map +1 -1
  15. package/login/pages/LoginPasskeysConditionalAuthenticate.useScript.js +7 -1
  16. package/login/pages/LoginPasskeysConditionalAuthenticate.useScript.js.map +1 -1
  17. package/login/pages/LoginRecoveryAuthnCodeConfig.useScript.js +7 -1
  18. package/login/pages/LoginRecoveryAuthnCodeConfig.useScript.js.map +1 -1
  19. package/login/pages/WebauthnAuthenticate.useScript.js +7 -1
  20. package/login/pages/WebauthnAuthenticate.useScript.js.map +1 -1
  21. package/login/pages/WebauthnRegister.useScript.js +7 -1
  22. package/login/pages/WebauthnRegister.useScript.js.map +1 -1
  23. package/package.json +17 -1
  24. package/src/bin/start-keycloak/start-keycloak.ts +4 -4
  25. package/src/login/lib/getUserProfileApi/getUserProfileApi.ts +1561 -0
  26. package/src/login/lib/getUserProfileApi/index.ts +1 -0
  27. package/src/login/lib/getUserProfileApi/kcNumberUnFormat.ts +109 -0
  28. package/src/login/lib/useUserProfileForm.tsx +82 -1322
  29. package/src/login/pages/LoginPasskeysConditionalAuthenticate.useScript.tsx +8 -1
  30. package/src/login/pages/LoginRecoveryAuthnCodeConfig.useScript.tsx +8 -1
  31. package/src/login/pages/WebauthnAuthenticate.useScript.tsx +8 -1
  32. package/src/login/pages/WebauthnRegister.useScript.tsx +8 -1
  33. package/src/tools/waitForElementMountedOnDom.ts +30 -0
  34. package/tools/waitForElementMountedOnDom.d.ts +3 -0
  35. package/tools/waitForElementMountedOnDom.js +21 -0
  36. package/tools/waitForElementMountedOnDom.js.map +1 -0
@@ -1,18 +1,9 @@
1
- import "keycloakify/tools/Array.prototype.every";
2
- import { useMemo, useReducer, useEffect, Fragment, type Dispatch } from "react";
3
- import { assert, type Equals } from "tsafe/assert";
4
- import { id } from "tsafe/id";
5
- import { structuredCloneButFunctions } from "keycloakify/tools/structuredCloneButFunctions";
6
- import { kcSanitize } from "keycloakify/lib/kcSanitize";
7
- import { useConstCallback } from "keycloakify/tools/useConstCallback";
8
- import { emailRegexp } from "keycloakify/tools/emailRegExp";
9
- import { formatNumber } from "keycloakify/tools/formatNumber";
10
- import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
1
+ import * as reactlessApi from "./getUserProfileApi/index";
11
2
  import type { PasswordPolicies, Attribute, Validators } from "keycloakify/login/KcContext";
12
- import type { KcContext } from "../KcContext";
13
- import type { MessageKey_defaultSet } from "keycloakify/login/i18n";
14
- import type { KcContextLike as KcContextLike_i18n } from "keycloakify/login/i18n";
3
+ import { useEffect, useState, useMemo, Fragment } from "react";
4
+ import { assert, type Equals } from "tsafe/assert";
15
5
  import type { I18n } from "../i18n";
6
+ export { getButtonToDisplayForMultivaluedAttributeField } from "./getUserProfileApi/index";
16
7
 
17
8
  export type FormFieldError = {
18
9
  errorMessage: JSX.Element;
@@ -21,6 +12,13 @@ export type FormFieldError = {
21
12
  fieldIndex: number | undefined;
22
13
  };
23
14
 
15
+ {
16
+ type A = Omit<FormFieldError, "errorMessage" | "errorMessageStr">;
17
+ type B = Omit<reactlessApi.FormFieldError, "advancedMsgArgs">;
18
+
19
+ assert<Equals<A, B>>();
20
+ }
21
+
24
22
  export namespace FormFieldError {
25
23
  export type Source = Source.Validator | Source.PasswordPolicy | Source.Server | Source.Other;
26
24
 
@@ -44,17 +42,38 @@ export namespace FormFieldError {
44
42
  }
45
43
  }
46
44
 
45
+ {
46
+ type A = FormFieldError.Source;
47
+ type B = reactlessApi.FormFieldError.Source;
48
+
49
+ assert<Equals<A, B>>();
50
+ }
51
+
47
52
  export type FormFieldState = {
48
53
  attribute: Attribute;
49
54
  displayableErrors: FormFieldError[];
50
55
  valueOrValues: string | string[];
51
56
  };
52
57
 
58
+ {
59
+ type A = Omit<FormFieldState, "displayableErrors">;
60
+ type B = Omit<reactlessApi.FormFieldState, "displayableErrors">;
61
+
62
+ assert<Equals<A, B>>();
63
+ }
64
+
53
65
  export type FormState = {
54
66
  isFormSubmittable: boolean;
55
67
  formFieldStates: FormFieldState[];
56
68
  };
57
69
 
70
+ {
71
+ type A = Omit<FormState, "formFieldStates">;
72
+ type B = Omit<FormState, "formFieldStates">;
73
+
74
+ assert<Equals<A, B>>();
75
+ }
76
+
58
77
  export type FormAction =
59
78
  | {
60
79
  action: "update";
@@ -69,1337 +88,78 @@ export type FormAction =
69
88
  fieldIndex: number | undefined;
70
89
  };
71
90
 
72
- export type KcContextLike = KcContextLike_i18n &
73
- KcContextLike_useGetErrors & {
74
- profile: {
75
- attributesByName: Record<string, Attribute>;
76
- html5DataAnnotations?: Record<string, string>;
77
- };
78
- passwordRequired?: boolean;
79
- realm: { registrationEmailAsUsername: boolean };
80
- url: {
81
- resourcesPath: string;
82
- };
83
- };
91
+ {
92
+ type A = FormAction;
93
+ type B = reactlessApi.FormAction;
94
+
95
+ assert<Equals<A, B>>();
96
+ }
84
97
 
85
- assert<Extract<Extract<KcContext, { profile: unknown }>, { pageId: "register.ftl" }> extends KcContextLike ? true : false>();
98
+ export type KcContextLike = reactlessApi.KcContextLike;
86
99
 
87
- export type UseUserProfileFormParams = {
100
+ export type I18nLike = Pick<I18n, "advancedMsg" | "advancedMsgStr">;
101
+
102
+ export type ParamsOfUseUserProfileForm = {
88
103
  kcContext: KcContextLike;
89
- i18n: I18n;
90
104
  doMakeUserConfirmPassword: boolean;
105
+ i18n: I18nLike;
91
106
  };
92
107
 
93
- export type ReturnTypeOfUseUserProfileForm = {
94
- formState: FormState;
95
- dispatchFormAction: Dispatch<FormAction>;
96
- };
97
-
98
- namespace internal {
99
- export type FormFieldState = {
100
- attribute: Attribute;
101
- errors: FormFieldError[];
102
- hasLostFocusAtLeastOnce: boolean | boolean[];
103
- valueOrValues: string | string[];
104
- };
108
+ {
109
+ type A = Omit<ParamsOfUseUserProfileForm, "i18n">;
110
+ type B = reactlessApi.ParamsOfGetUserProfileApi;
105
111
 
106
- export type State = {
107
- formFieldStates: FormFieldState[];
108
- };
112
+ assert<Equals<A, B>>();
109
113
  }
110
114
 
111
- export function useUserProfileForm(params: UseUserProfileFormParams): ReturnTypeOfUseUserProfileForm {
112
- const { kcContext, i18n, doMakeUserConfirmPassword } = params;
113
-
114
- const { insertScriptTags } = useInsertScriptTags({
115
- componentOrHookName: "useUserProfileForm",
116
- scriptTags: Object.keys(kcContext.profile?.html5DataAnnotations ?? {})
117
- .filter(key => key !== "kcMultivalued" && key !== "kcNumberFormat") // NOTE: Keycloakify handles it.
118
- .map(key => ({
119
- type: "module",
120
- src: `${kcContext.url.resourcesPath}/js/${key}.js`
121
- }))
122
- });
115
+ export type ReturnTypeOfUseUserProfileForm = {
116
+ formState: FormState;
117
+ dispatchFormAction: (action: FormAction) => void;
118
+ };
123
119
 
124
- useEffect(() => {
125
- insertScriptTags();
126
- }, []);
120
+ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTypeOfUseUserProfileForm {
121
+ const { doMakeUserConfirmPassword, i18n, kcContext } = params;
127
122
 
128
- const { getErrors } = useGetErrors({
123
+ const api = reactlessApi.getUserProfileApi({
129
124
  kcContext,
130
- i18n
125
+ doMakeUserConfirmPassword
131
126
  });
132
127
 
133
- const initialState = useMemo((): internal.State => {
134
- // NOTE: We don't use te kcContext.profile.attributes directly because
135
- // they don't includes the password and password confirm fields and we want to add them.
136
- // We also want to apply some retro-compatibility and consistency patches.
137
- const attributes: Attribute[] = (() => {
138
- mock_user_profile_attributes_for_older_keycloak_versions: {
139
- if (
140
- "profile" in kcContext &&
141
- "attributesByName" in kcContext.profile &&
142
- Object.keys(kcContext.profile.attributesByName).length !== 0
143
- ) {
144
- break mock_user_profile_attributes_for_older_keycloak_versions;
145
- }
146
-
147
- if ("register" in kcContext && kcContext.register instanceof Object && "formData" in kcContext.register) {
148
- //NOTE: Handle legacy register.ftl page
149
- return (["firstName", "lastName", "email", "username"] as const)
150
- .filter(name => (name !== "username" ? true : !kcContext.realm.registrationEmailAsUsername))
151
- .map(name =>
152
- id<Attribute>({
153
- name: name,
154
- displayName: id<`\${${MessageKey_defaultSet}}`>(`\${${name}}`),
155
- required: true,
156
- value: (kcContext.register as any).formData[name] ?? "",
157
- html5DataAnnotations: {},
158
- readOnly: false,
159
- validators: {},
160
- annotations: {},
161
- autocomplete: (() => {
162
- switch (name) {
163
- case "email":
164
- return "email";
165
- case "username":
166
- return "username";
167
- default:
168
- return undefined;
169
- }
170
- })()
171
- })
172
- );
173
- }
174
-
175
- if ("user" in kcContext && kcContext.user instanceof Object) {
176
- //NOTE: Handle legacy login-update-profile.ftl
177
- return (["username", "email", "firstName", "lastName"] as const)
178
- .filter(name => (name !== "username" ? true : (kcContext.user as any).editUsernameAllowed))
179
- .map(name =>
180
- id<Attribute>({
181
- name: name,
182
- displayName: id<`\${${MessageKey_defaultSet}}`>(`\${${name}}`),
183
- required: true,
184
- value: (kcContext as any).user[name] ?? "",
185
- html5DataAnnotations: {},
186
- readOnly: false,
187
- validators: {},
188
- annotations: {},
189
- autocomplete: (() => {
190
- switch (name) {
191
- case "email":
192
- return "email";
193
- case "username":
194
- return "username";
195
- default:
196
- return undefined;
197
- }
198
- })()
199
- })
200
- );
201
- }
202
-
203
- if ("email" in kcContext && kcContext.email instanceof Object) {
204
- //NOTE: Handle legacy update-email.ftl
205
- return [
206
- id<Attribute>({
207
- name: "email",
208
- displayName: id<`\${${MessageKey_defaultSet}}`>(`\${email}`),
209
- required: true,
210
- value: (kcContext.email as any).value ?? "",
211
- html5DataAnnotations: {},
212
- readOnly: false,
213
- validators: {},
214
- annotations: {},
215
- autocomplete: "email"
216
- })
217
- ];
218
- }
219
-
220
- assert(false, "Unable to mock user profile from the current kcContext");
221
- }
222
-
223
- return Object.values(kcContext.profile.attributesByName).map(structuredCloneButFunctions);
224
- })();
128
+ const [formState_reactless, setFormState_reactless] = useState(() => api.getFormState());
225
129
 
226
- // Retro-compatibility and consistency patches
227
- attributes.forEach(attribute => {
228
- patch_legacy_group: {
229
- if (typeof attribute.group !== "string") {
230
- break patch_legacy_group;
231
- }
232
-
233
- const { group, groupDisplayHeader, groupDisplayDescription, groupAnnotations } = attribute as Attribute & {
234
- group: string;
235
- groupDisplayHeader?: string;
236
- groupDisplayDescription?: string;
237
- groupAnnotations: Record<string, string>;
238
- };
239
-
240
- delete attribute.group;
241
- // @ts-expect-error
242
- delete attribute.groupDisplayHeader;
243
- // @ts-expect-error
244
- delete attribute.groupDisplayDescription;
245
- // @ts-expect-error
246
- delete attribute.groupAnnotations;
247
-
248
- if (group === "") {
249
- break patch_legacy_group;
250
- }
251
-
252
- attribute.group = {
253
- name: group,
254
- displayHeader: groupDisplayHeader,
255
- displayDescription: groupDisplayDescription,
256
- annotations: groupAnnotations,
257
- html5DataAnnotations: {}
258
- };
259
- }
260
-
261
- // Attributes with options rendered by default as select inputs
262
- if (attribute.validators.options !== undefined && attribute.annotations.inputType === undefined) {
263
- attribute.annotations.inputType = "select";
264
- }
265
-
266
- // Consistency patch on values/value property
267
- {
268
- if (getIsMultivaluedSingleField({ attribute })) {
269
- attribute.multivalued = true;
270
- }
271
-
272
- if (attribute.multivalued) {
273
- attribute.values ??= attribute.value !== undefined ? [attribute.value] : [];
274
- delete attribute.value;
275
- } else {
276
- attribute.value ??= attribute.values?.[0];
277
- delete attribute.values;
278
- }
279
- }
130
+ useEffect(() => {
131
+ const { unsubscribe } = api.subscribeToFormState(() => {
132
+ setFormState_reactless(api.getFormState());
280
133
  });
281
134
 
282
- add_password_and_password_confirm: {
283
- if (!kcContext.passwordRequired) {
284
- break add_password_and_password_confirm;
285
- }
286
-
287
- attributes.forEach((attribute, i) => {
288
- if (attribute.name !== (kcContext.realm.registrationEmailAsUsername ? "email" : "username")) {
289
- // NOTE: We want to add password and password-confirm after the field that identifies the user.
290
- // It's either email or username.
291
- return;
292
- }
293
-
294
- attributes.splice(
295
- i + 1,
296
- 0,
297
- {
298
- name: "password",
299
- displayName: id<`\${${MessageKey_defaultSet}}`>("${password}"),
300
- required: true,
301
- readOnly: false,
302
- validators: {},
303
- annotations: {},
304
- autocomplete: "new-password",
305
- html5DataAnnotations: {}
306
- },
307
- {
308
- name: "password-confirm",
309
- displayName: id<`\${${MessageKey_defaultSet}}`>("${passwordConfirm}"),
310
- required: true,
311
- readOnly: false,
312
- validators: {},
313
- annotations: {},
314
- html5DataAnnotations: {},
315
- autocomplete: "new-password"
316
- }
317
- );
318
- });
319
- }
320
-
321
- const initialFormFieldState: {
322
- attribute: Attribute;
323
- valueOrValues: string | string[];
324
- }[] = [];
325
-
326
- for (const attribute of attributes) {
327
- handle_multi_valued_attribute: {
328
- if (!attribute.multivalued) {
329
- break handle_multi_valued_attribute;
330
- }
331
-
332
- const values = attribute.values?.length ? attribute.values : [""];
333
-
334
- apply_validator_min_range: {
335
- if (getIsMultivaluedSingleField({ attribute })) {
336
- break apply_validator_min_range;
337
- }
338
-
339
- const validator = attribute.validators.multivalued;
340
-
341
- if (validator === undefined) {
342
- break apply_validator_min_range;
343
- }
344
-
345
- const { min: minStr } = validator;
346
-
347
- if (!minStr) {
348
- break apply_validator_min_range;
349
- }
350
-
351
- const min = parseInt(`${minStr}`);
352
-
353
- for (let index = values.length; index < min; index++) {
354
- values.push("");
355
- }
356
- }
357
-
358
- initialFormFieldState.push({
359
- attribute,
360
- valueOrValues: values
361
- });
362
-
363
- continue;
364
- }
365
-
366
- initialFormFieldState.push({
367
- attribute,
368
- valueOrValues: attribute.value ?? ""
369
- });
370
- }
371
-
372
- const initialState: internal.State = {
373
- formFieldStates: initialFormFieldState.map(({ attribute, valueOrValues }) => ({
374
- attribute,
375
- errors: getErrors({
376
- attributeName: attribute.name,
377
- formFieldStates: initialFormFieldState
378
- }),
379
- hasLostFocusAtLeastOnce:
380
- valueOrValues instanceof Array && !getIsMultivaluedSingleField({ attribute }) ? valueOrValues.map(() => false) : false,
381
- valueOrValues: valueOrValues
135
+ return () => unsubscribe();
136
+ }, [api]);
137
+
138
+ const { advancedMsg, advancedMsgStr } = i18n;
139
+
140
+ const formState = useMemo(
141
+ (): FormState => ({
142
+ isFormSubmittable: formState_reactless.isFormSubmittable,
143
+ formFieldStates: formState_reactless.formFieldStates.map(formFieldState_reactless => ({
144
+ attribute: formFieldState_reactless.attribute,
145
+ valueOrValues: formFieldState_reactless.valueOrValues,
146
+ displayableErrors: formFieldState_reactless.displayableErrors.map((formFieldError_reactless, i) => ({
147
+ errorMessage: (
148
+ <Fragment key={`${formFieldState_reactless.attribute.name}-${i}`}>
149
+ {advancedMsg(...formFieldError_reactless.advancedMsgArgs)}
150
+ </Fragment>
151
+ ),
152
+ errorMessageStr: advancedMsgStr(...formFieldError_reactless.advancedMsgArgs),
153
+ source: formFieldError_reactless.source,
154
+ fieldIndex: formFieldError_reactless.fieldIndex
155
+ }))
382
156
  }))
383
- };
384
-
385
- return initialState;
386
- }, []);
387
-
388
- const [state, dispatchFormAction] = useReducer(function reducer(state: internal.State, formAction: FormAction): internal.State {
389
- const formFieldState = state.formFieldStates.find(({ attribute }) => attribute.name === formAction.name);
390
-
391
- assert(formFieldState !== undefined);
392
-
393
- (() => {
394
- switch (formAction.action) {
395
- case "update":
396
- formFieldState.valueOrValues = formAction.valueOrValues;
397
-
398
- apply_formatters: {
399
- const { attribute } = formFieldState;
400
-
401
- const { kcNumberFormat } = attribute.html5DataAnnotations ?? {};
402
-
403
- if (!kcNumberFormat) {
404
- break apply_formatters;
405
- }
406
-
407
- if (formFieldState.valueOrValues instanceof Array) {
408
- formFieldState.valueOrValues = formFieldState.valueOrValues.map(value => formatNumber(value, kcNumberFormat));
409
- } else {
410
- formFieldState.valueOrValues = formatNumber(formFieldState.valueOrValues, kcNumberFormat);
411
- }
412
- }
413
-
414
- formFieldState.errors = getErrors({
415
- attributeName: formAction.name,
416
- formFieldStates: state.formFieldStates
417
- });
418
-
419
- simulate_focus_lost: {
420
- const { displayErrorsImmediately = false } = formAction;
421
-
422
- if (!displayErrorsImmediately) {
423
- break simulate_focus_lost;
424
- }
425
-
426
- for (const fieldIndex of formAction.valueOrValues instanceof Array
427
- ? formAction.valueOrValues.map((...[, index]) => index)
428
- : [undefined]) {
429
- state = reducer(state, {
430
- action: "focus lost",
431
- name: formAction.name,
432
- fieldIndex
433
- });
434
- }
435
- }
436
-
437
- update_password_confirm: {
438
- if (doMakeUserConfirmPassword) {
439
- break update_password_confirm;
440
- }
441
-
442
- if (formAction.name !== "password") {
443
- break update_password_confirm;
444
- }
445
-
446
- state = reducer(state, {
447
- action: "update",
448
- name: "password-confirm",
449
- valueOrValues: formAction.valueOrValues,
450
- displayErrorsImmediately: formAction.displayErrorsImmediately
451
- });
452
- }
453
-
454
- trigger_password_confirm_validation_on_password_change: {
455
- if (!doMakeUserConfirmPassword) {
456
- break trigger_password_confirm_validation_on_password_change;
457
- }
458
-
459
- if (formAction.name !== "password") {
460
- break trigger_password_confirm_validation_on_password_change;
461
- }
462
-
463
- state = reducer(state, {
464
- action: "update",
465
- name: "password-confirm",
466
- valueOrValues: (() => {
467
- const formFieldState = state.formFieldStates.find(({ attribute }) => attribute.name === "password-confirm");
468
-
469
- assert(formFieldState !== undefined);
470
-
471
- return formFieldState.valueOrValues;
472
- })(),
473
- displayErrorsImmediately: formAction.displayErrorsImmediately
474
- });
475
- }
476
-
477
- return;
478
- case "focus lost":
479
- if (formFieldState.hasLostFocusAtLeastOnce instanceof Array) {
480
- const { fieldIndex } = formAction;
481
- assert(fieldIndex !== undefined);
482
- formFieldState.hasLostFocusAtLeastOnce[fieldIndex] = true;
483
- return;
484
- }
485
-
486
- formFieldState.hasLostFocusAtLeastOnce = true;
487
- return;
488
- }
489
- assert<Equals<typeof formAction, never>>(false);
490
- })();
491
-
492
- return { ...state };
493
- }, initialState);
494
-
495
- const formState: FormState = useMemo(
496
- () => ({
497
- formFieldStates: state.formFieldStates.map(
498
- ({ errors, hasLostFocusAtLeastOnce: hasLostFocusAtLeastOnceOrArr, attribute, ...valueOrValuesWrap }) => ({
499
- displayableErrors: errors.filter(error => {
500
- const hasLostFocusAtLeastOnce =
501
- typeof hasLostFocusAtLeastOnceOrArr === "boolean"
502
- ? hasLostFocusAtLeastOnceOrArr
503
- : error.fieldIndex !== undefined
504
- ? hasLostFocusAtLeastOnceOrArr[error.fieldIndex]
505
- : hasLostFocusAtLeastOnceOrArr[hasLostFocusAtLeastOnceOrArr.length - 1];
506
-
507
- switch (error.source.type) {
508
- case "server":
509
- return true;
510
- case "other":
511
- switch (error.source.rule) {
512
- case "requiredField":
513
- return hasLostFocusAtLeastOnce;
514
- case "passwordConfirmMatchesPassword":
515
- return hasLostFocusAtLeastOnce;
516
- }
517
- assert<Equals<typeof error.source.rule, never>>(false);
518
- case "passwordPolicy":
519
- switch (error.source.name) {
520
- case "length":
521
- return hasLostFocusAtLeastOnce;
522
- case "digits":
523
- return hasLostFocusAtLeastOnce;
524
- case "lowerCase":
525
- return hasLostFocusAtLeastOnce;
526
- case "upperCase":
527
- return hasLostFocusAtLeastOnce;
528
- case "specialChars":
529
- return hasLostFocusAtLeastOnce;
530
- case "notUsername":
531
- return true;
532
- case "notEmail":
533
- return true;
534
- }
535
- assert<Equals<typeof error.source, never>>(false);
536
- case "validator":
537
- switch (error.source.name) {
538
- case "length":
539
- return hasLostFocusAtLeastOnce;
540
- case "pattern":
541
- return hasLostFocusAtLeastOnce;
542
- case "email":
543
- return hasLostFocusAtLeastOnce;
544
- case "integer":
545
- return hasLostFocusAtLeastOnce;
546
- case "multivalued":
547
- return hasLostFocusAtLeastOnce;
548
- case "options":
549
- return hasLostFocusAtLeastOnce;
550
- }
551
- assert<Equals<typeof error.source, never>>(false);
552
- }
553
- }),
554
- attribute,
555
- ...valueOrValuesWrap
556
- })
557
- ),
558
- isFormSubmittable: state.formFieldStates.every(({ errors }) => errors.length === 0)
559
157
  }),
560
- [state]
158
+ [formState_reactless]
561
159
  );
562
160
 
563
161
  return {
564
162
  formState,
565
- dispatchFormAction
163
+ dispatchFormAction: api.dispatchFormAction
566
164
  };
567
165
  }
568
-
569
- type KcContextLike_useGetErrors = KcContextLike_i18n & {
570
- messagesPerField: Pick<KcContext["messagesPerField"], "existsError" | "get">;
571
- passwordPolicies?: PasswordPolicies;
572
- };
573
-
574
- assert<KcContextLike extends KcContextLike_useGetErrors ? true : false>();
575
-
576
- function useGetErrors(params: { kcContext: KcContextLike_useGetErrors; i18n: I18n }) {
577
- const { kcContext, i18n } = params;
578
-
579
- const { messagesPerField, passwordPolicies } = kcContext;
580
-
581
- const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;
582
-
583
- const getErrors = useConstCallback(
584
- (params: {
585
- attributeName: string;
586
- formFieldStates: {
587
- attribute: Attribute;
588
- valueOrValues: string | string[];
589
- }[];
590
- }): FormFieldError[] => {
591
- const { attributeName, formFieldStates } = params;
592
-
593
- const formFieldState = formFieldStates.find(({ attribute }) => attribute.name === attributeName);
594
-
595
- assert(formFieldState !== undefined);
596
-
597
- const { attribute } = formFieldState;
598
-
599
- const valueOrValues = (() => {
600
- let { valueOrValues } = formFieldState;
601
-
602
- unFormat_number: {
603
- const { kcNumberUnFormat } = attribute.html5DataAnnotations ?? {};
604
-
605
- if (!kcNumberUnFormat) {
606
- break unFormat_number;
607
- }
608
-
609
- if (valueOrValues instanceof Array) {
610
- valueOrValues = valueOrValues.map(value => formatNumber(value, kcNumberUnFormat));
611
- } else {
612
- valueOrValues = formatNumber(valueOrValues, kcNumberUnFormat);
613
- }
614
- }
615
-
616
- return valueOrValues;
617
- })();
618
-
619
- assert(attribute !== undefined);
620
-
621
- server_side_error: {
622
- if (attribute.multivalued) {
623
- const defaultValues = attribute.values?.length ? attribute.values : [""];
624
-
625
- assert(valueOrValues instanceof Array);
626
-
627
- const values = valueOrValues;
628
-
629
- if (JSON.stringify(defaultValues) !== JSON.stringify(values.slice(0, defaultValues.length))) {
630
- break server_side_error;
631
- }
632
- } else {
633
- const defaultValue = attribute.value ?? "";
634
-
635
- assert(typeof valueOrValues === "string");
636
-
637
- const value = valueOrValues;
638
-
639
- if (defaultValue !== value) {
640
- break server_side_error;
641
- }
642
- }
643
-
644
- let doesErrorExist: boolean;
645
-
646
- try {
647
- doesErrorExist = messagesPerField.existsError(attributeName);
648
- } catch {
649
- break server_side_error;
650
- }
651
-
652
- if (!doesErrorExist) {
653
- break server_side_error;
654
- }
655
-
656
- const errorMessageStr = messagesPerField.get(attributeName);
657
-
658
- return [
659
- {
660
- errorMessageStr,
661
- errorMessage: (
662
- <span
663
- key={0}
664
- dangerouslySetInnerHTML={{
665
- __html: kcSanitize(errorMessageStr)
666
- }}
667
- />
668
- ),
669
- fieldIndex: undefined,
670
- source: {
671
- type: "server"
672
- }
673
- }
674
- ];
675
- }
676
-
677
- handle_multi_valued_multi_fields: {
678
- if (!attribute.multivalued) {
679
- break handle_multi_valued_multi_fields;
680
- }
681
-
682
- if (getIsMultivaluedSingleField({ attribute })) {
683
- break handle_multi_valued_multi_fields;
684
- }
685
-
686
- assert(valueOrValues instanceof Array);
687
-
688
- const values = valueOrValues;
689
-
690
- const errors = values
691
- .map((...[, index]) => {
692
- const specificValueErrors = getErrors({
693
- attributeName,
694
- formFieldStates: formFieldStates.map(formFieldState => {
695
- if (formFieldState.attribute.name === attributeName) {
696
- assert(formFieldState.valueOrValues instanceof Array);
697
- return {
698
- attribute: {
699
- ...attribute,
700
- annotations: {
701
- ...attribute.annotations,
702
- inputType: undefined
703
- },
704
- multivalued: false
705
- },
706
- valueOrValues: formFieldState.valueOrValues[index]
707
- };
708
- }
709
-
710
- return formFieldState;
711
- })
712
- });
713
-
714
- return specificValueErrors
715
- .filter(error => {
716
- if (error.source.type === "other" && error.source.rule === "requiredField") {
717
- return false;
718
- }
719
-
720
- return true;
721
- })
722
- .map(
723
- (error): FormFieldError => ({
724
- ...error,
725
- fieldIndex: index
726
- })
727
- );
728
- })
729
- .reduce((acc, errors) => [...acc, ...errors], []);
730
-
731
- required_field: {
732
- if (!attribute.required) {
733
- break required_field;
734
- }
735
-
736
- if (values.every(value => value !== "")) {
737
- break required_field;
738
- }
739
-
740
- const msgArgs = ["error-user-attribute-required"] as const;
741
-
742
- errors.push({
743
- errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
744
- errorMessageStr: msgStr(...msgArgs),
745
- fieldIndex: undefined,
746
- source: {
747
- type: "other",
748
- rule: "requiredField"
749
- }
750
- });
751
- }
752
-
753
- return errors;
754
- }
755
-
756
- handle_multi_valued_single_field: {
757
- if (!attribute.multivalued) {
758
- break handle_multi_valued_single_field;
759
- }
760
-
761
- if (!getIsMultivaluedSingleField({ attribute })) {
762
- break handle_multi_valued_single_field;
763
- }
764
-
765
- const validatorName = "multivalued";
766
-
767
- const validator = attribute.validators[validatorName];
768
-
769
- if (validator === undefined) {
770
- return [];
771
- }
772
-
773
- const { min: minStr } = validator;
774
-
775
- const min = minStr ? parseInt(`${minStr}`) : attribute.required ? 1 : 0;
776
-
777
- assert(!isNaN(min));
778
-
779
- const { max: maxStr } = validator;
780
-
781
- const max = !maxStr ? Infinity : parseInt(`${maxStr}`);
782
-
783
- assert(!isNaN(max));
784
-
785
- assert(valueOrValues instanceof Array);
786
-
787
- const values = valueOrValues;
788
-
789
- if (min <= values.length && values.length <= max) {
790
- return [];
791
- }
792
-
793
- const msgArgs = ["error-invalid-multivalued-size", `${min}`, `${max}`] as const;
794
-
795
- return [
796
- {
797
- errorMessage: <Fragment key={0}>{msg(...msgArgs)}</Fragment>,
798
- errorMessageStr: msgStr(...msgArgs),
799
- fieldIndex: undefined,
800
- source: {
801
- type: "validator",
802
- name: validatorName
803
- }
804
- }
805
- ];
806
- }
807
-
808
- assert(typeof valueOrValues === "string");
809
-
810
- const value = valueOrValues;
811
-
812
- const errors: FormFieldError[] = [];
813
-
814
- check_password_policies: {
815
- if (attributeName !== "password") {
816
- break check_password_policies;
817
- }
818
-
819
- if (passwordPolicies === undefined) {
820
- break check_password_policies;
821
- }
822
-
823
- check_password_policy_x: {
824
- const policyName = "length";
825
-
826
- const policy = passwordPolicies[policyName];
827
-
828
- if (!policy) {
829
- break check_password_policy_x;
830
- }
831
-
832
- const minLength = policy;
833
-
834
- if (value.length >= minLength) {
835
- break check_password_policy_x;
836
- }
837
-
838
- const msgArgs = ["invalidPasswordMinLengthMessage", `${minLength}`] as const;
839
-
840
- errors.push({
841
- errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
842
- errorMessageStr: msgStr(...msgArgs),
843
- fieldIndex: undefined,
844
- source: {
845
- type: "passwordPolicy",
846
- name: policyName
847
- }
848
- });
849
- }
850
-
851
- check_password_policy_x: {
852
- const policyName = "digits";
853
-
854
- const policy = passwordPolicies[policyName];
855
-
856
- if (!policy) {
857
- break check_password_policy_x;
858
- }
859
-
860
- const minNumberOfDigits = policy;
861
-
862
- if (value.split("").filter(char => !isNaN(parseInt(char))).length >= minNumberOfDigits) {
863
- break check_password_policy_x;
864
- }
865
-
866
- const msgArgs = ["invalidPasswordMinDigitsMessage", `${minNumberOfDigits}`] as const;
867
-
868
- errors.push({
869
- errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
870
- errorMessageStr: msgStr(...msgArgs),
871
- fieldIndex: undefined,
872
- source: {
873
- type: "passwordPolicy",
874
- name: policyName
875
- }
876
- });
877
- }
878
-
879
- check_password_policy_x: {
880
- const policyName = "lowerCase";
881
-
882
- const policy = passwordPolicies[policyName];
883
-
884
- if (!policy) {
885
- break check_password_policy_x;
886
- }
887
-
888
- const minNumberOfLowerCaseChar = policy;
889
-
890
- if (
891
- value.split("").filter(char => char === char.toLowerCase() && char !== char.toUpperCase()).length >= minNumberOfLowerCaseChar
892
- ) {
893
- break check_password_policy_x;
894
- }
895
-
896
- const msgArgs = ["invalidPasswordMinLowerCaseCharsMessage", `${minNumberOfLowerCaseChar}`] as const;
897
-
898
- errors.push({
899
- errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
900
- errorMessageStr: msgStr(...msgArgs),
901
- fieldIndex: undefined,
902
- source: {
903
- type: "passwordPolicy",
904
- name: policyName
905
- }
906
- });
907
- }
908
-
909
- check_password_policy_x: {
910
- const policyName = "upperCase";
911
-
912
- const policy = passwordPolicies[policyName];
913
-
914
- if (!policy) {
915
- break check_password_policy_x;
916
- }
917
-
918
- const minNumberOfUpperCaseChar = policy;
919
-
920
- if (
921
- value.split("").filter(char => char === char.toUpperCase() && char !== char.toLowerCase()).length >= minNumberOfUpperCaseChar
922
- ) {
923
- break check_password_policy_x;
924
- }
925
-
926
- const msgArgs = ["invalidPasswordMinUpperCaseCharsMessage", `${minNumberOfUpperCaseChar}`] as const;
927
-
928
- errors.push({
929
- errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
930
- errorMessageStr: msgStr(...msgArgs),
931
- fieldIndex: undefined,
932
- source: {
933
- type: "passwordPolicy",
934
- name: policyName
935
- }
936
- });
937
- }
938
-
939
- check_password_policy_x: {
940
- const policyName = "specialChars";
941
-
942
- const policy = passwordPolicies[policyName];
943
-
944
- if (!policy) {
945
- break check_password_policy_x;
946
- }
947
-
948
- const minNumberOfSpecialChar = policy;
949
-
950
- if (value.split("").filter(char => !char.match(/[a-zA-Z0-9]/)).length >= minNumberOfSpecialChar) {
951
- break check_password_policy_x;
952
- }
953
-
954
- const msgArgs = ["invalidPasswordMinSpecialCharsMessage", `${minNumberOfSpecialChar}`] as const;
955
-
956
- errors.push({
957
- errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
958
- errorMessageStr: msgStr(...msgArgs),
959
- fieldIndex: undefined,
960
- source: {
961
- type: "passwordPolicy",
962
- name: policyName
963
- }
964
- });
965
- }
966
-
967
- check_password_policy_x: {
968
- const policyName = "notUsername";
969
-
970
- const notUsername = passwordPolicies[policyName];
971
-
972
- if (!notUsername) {
973
- break check_password_policy_x;
974
- }
975
-
976
- const usernameFormFieldState = formFieldStates.find(formFieldState => formFieldState.attribute.name === "username");
977
-
978
- if (!usernameFormFieldState) {
979
- break check_password_policy_x;
980
- }
981
-
982
- const usernameValue = (() => {
983
- let { valueOrValues } = usernameFormFieldState;
984
-
985
- assert(typeof valueOrValues === "string");
986
-
987
- unFormat_number: {
988
- const { kcNumberUnFormat } = attribute.html5DataAnnotations ?? {};
989
-
990
- if (!kcNumberUnFormat) {
991
- break unFormat_number;
992
- }
993
-
994
- valueOrValues = formatNumber(valueOrValues, kcNumberUnFormat);
995
- }
996
-
997
- return valueOrValues;
998
- })();
999
-
1000
- if (usernameValue === "") {
1001
- break check_password_policy_x;
1002
- }
1003
-
1004
- if (value !== usernameValue) {
1005
- break check_password_policy_x;
1006
- }
1007
-
1008
- const msgArgs = ["invalidPasswordNotUsernameMessage"] as const;
1009
-
1010
- errors.push({
1011
- errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
1012
- errorMessageStr: msgStr(...msgArgs),
1013
- fieldIndex: undefined,
1014
- source: {
1015
- type: "passwordPolicy",
1016
- name: policyName
1017
- }
1018
- });
1019
- }
1020
-
1021
- check_password_policy_x: {
1022
- const policyName = "notEmail";
1023
-
1024
- const notEmail = passwordPolicies[policyName];
1025
-
1026
- if (!notEmail) {
1027
- break check_password_policy_x;
1028
- }
1029
-
1030
- const emailFormFieldState = formFieldStates.find(formFieldState => formFieldState.attribute.name === "email");
1031
-
1032
- if (!emailFormFieldState) {
1033
- break check_password_policy_x;
1034
- }
1035
-
1036
- assert(typeof emailFormFieldState.valueOrValues === "string");
1037
-
1038
- {
1039
- const emailValue = emailFormFieldState.valueOrValues;
1040
-
1041
- if (emailValue === "") {
1042
- break check_password_policy_x;
1043
- }
1044
-
1045
- if (value !== emailValue) {
1046
- break check_password_policy_x;
1047
- }
1048
- }
1049
-
1050
- const msgArgs = ["invalidPasswordNotEmailMessage"] as const;
1051
-
1052
- errors.push({
1053
- errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
1054
- errorMessageStr: msgStr(...msgArgs),
1055
- fieldIndex: undefined,
1056
- source: {
1057
- type: "passwordPolicy",
1058
- name: policyName
1059
- }
1060
- });
1061
- }
1062
- }
1063
-
1064
- password_confirm_matches_password: {
1065
- if (attributeName !== "password-confirm") {
1066
- break password_confirm_matches_password;
1067
- }
1068
-
1069
- const passwordFormFieldState = formFieldStates.find(formFieldState => formFieldState.attribute.name === "password");
1070
-
1071
- assert(passwordFormFieldState !== undefined);
1072
-
1073
- assert(typeof passwordFormFieldState.valueOrValues === "string");
1074
-
1075
- {
1076
- const passwordValue = passwordFormFieldState.valueOrValues;
1077
-
1078
- if (value === passwordValue) {
1079
- break password_confirm_matches_password;
1080
- }
1081
- }
1082
-
1083
- const msgArgs = ["invalidPasswordConfirmMessage"] as const;
1084
-
1085
- errors.push({
1086
- errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
1087
- errorMessageStr: msgStr(...msgArgs),
1088
- fieldIndex: undefined,
1089
- source: {
1090
- type: "other",
1091
- rule: "passwordConfirmMatchesPassword"
1092
- }
1093
- });
1094
- }
1095
-
1096
- const { validators } = attribute;
1097
-
1098
- required_field: {
1099
- if (!attribute.required) {
1100
- break required_field;
1101
- }
1102
-
1103
- if (value !== "") {
1104
- break required_field;
1105
- }
1106
-
1107
- const msgArgs = ["error-user-attribute-required"] as const;
1108
-
1109
- errors.push({
1110
- errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
1111
- errorMessageStr: msgStr(...msgArgs),
1112
- fieldIndex: undefined,
1113
- source: {
1114
- type: "other",
1115
- rule: "requiredField"
1116
- }
1117
- });
1118
- }
1119
-
1120
- validator_x: {
1121
- const validatorName = "length";
1122
-
1123
- const validator = validators[validatorName];
1124
-
1125
- if (!validator) {
1126
- break validator_x;
1127
- }
1128
-
1129
- const { "ignore.empty.value": ignoreEmptyValue = false, max, min } = validator;
1130
-
1131
- if (ignoreEmptyValue && value === "") {
1132
- break validator_x;
1133
- }
1134
-
1135
- const source: FormFieldError.Source = {
1136
- type: "validator",
1137
- name: validatorName
1138
- };
1139
-
1140
- if (max && value.length > parseInt(`${max}`)) {
1141
- const msgArgs = ["error-invalid-length-too-long", `${max}`] as const;
1142
-
1143
- errors.push({
1144
- errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
1145
- errorMessageStr: msgStr(...msgArgs),
1146
- fieldIndex: undefined,
1147
- source
1148
- });
1149
- }
1150
-
1151
- if (min && value.length < parseInt(`${min}`)) {
1152
- const msgArgs = ["error-invalid-length-too-short", `${min}`] as const;
1153
-
1154
- errors.push({
1155
- errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
1156
- errorMessageStr: msgStr(...msgArgs),
1157
- fieldIndex: undefined,
1158
- source
1159
- });
1160
- }
1161
- }
1162
-
1163
- validator_x: {
1164
- const validatorName = "pattern";
1165
-
1166
- const validator = validators[validatorName];
1167
-
1168
- if (validator === undefined) {
1169
- break validator_x;
1170
- }
1171
-
1172
- const { "ignore.empty.value": ignoreEmptyValue = false, pattern, "error-message": errorMessageKey } = validator;
1173
-
1174
- if (ignoreEmptyValue && value === "") {
1175
- break validator_x;
1176
- }
1177
-
1178
- if (new RegExp(pattern).test(value)) {
1179
- break validator_x;
1180
- }
1181
-
1182
- const msgArgs = [errorMessageKey ?? id<MessageKey_defaultSet>("shouldMatchPattern"), pattern] as const;
1183
-
1184
- errors.push({
1185
- errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{advancedMsg(...msgArgs)}</Fragment>,
1186
- errorMessageStr: advancedMsgStr(...msgArgs),
1187
- fieldIndex: undefined,
1188
- source: {
1189
- type: "validator",
1190
- name: validatorName
1191
- }
1192
- });
1193
- }
1194
-
1195
- validator_x: {
1196
- {
1197
- const lastError = errors[errors.length - 1];
1198
- if (lastError !== undefined && lastError.source.type === "validator" && lastError.source.name === "pattern") {
1199
- break validator_x;
1200
- }
1201
- }
1202
-
1203
- const validatorName = "email";
1204
-
1205
- const validator = validators[validatorName];
1206
-
1207
- if (validator === undefined) {
1208
- break validator_x;
1209
- }
1210
-
1211
- const { "ignore.empty.value": ignoreEmptyValue = false } = validator;
1212
-
1213
- if (ignoreEmptyValue && value === "") {
1214
- break validator_x;
1215
- }
1216
-
1217
- if (emailRegexp.test(value)) {
1218
- break validator_x;
1219
- }
1220
-
1221
- const msgArgs = [id<MessageKey_defaultSet>("invalidEmailMessage")] as const;
1222
-
1223
- errors.push({
1224
- errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
1225
- errorMessageStr: msgStr(...msgArgs),
1226
- fieldIndex: undefined,
1227
- source: {
1228
- type: "validator",
1229
- name: validatorName
1230
- }
1231
- });
1232
- }
1233
-
1234
- validator_x: {
1235
- const validatorName = "integer";
1236
-
1237
- const validator = validators[validatorName];
1238
-
1239
- if (validator === undefined) {
1240
- break validator_x;
1241
- }
1242
-
1243
- const { "ignore.empty.value": ignoreEmptyValue = false, max, min } = validator;
1244
-
1245
- if (ignoreEmptyValue && value === "") {
1246
- break validator_x;
1247
- }
1248
-
1249
- const intValue = parseInt(value);
1250
-
1251
- const source: FormFieldError.Source = {
1252
- type: "validator",
1253
- name: validatorName
1254
- };
1255
-
1256
- if (isNaN(intValue)) {
1257
- const msgArgs = ["mustBeAnInteger"] as const;
1258
-
1259
- errors.push({
1260
- errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
1261
- errorMessageStr: msgStr(...msgArgs),
1262
- fieldIndex: undefined,
1263
- source
1264
- });
1265
-
1266
- break validator_x;
1267
- }
1268
-
1269
- if (max && intValue > parseInt(`${max}`)) {
1270
- const msgArgs = ["error-number-out-of-range-too-big", `${max}`] as const;
1271
-
1272
- errors.push({
1273
- errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
1274
- errorMessageStr: msgStr(...msgArgs),
1275
- fieldIndex: undefined,
1276
- source
1277
- });
1278
-
1279
- break validator_x;
1280
- }
1281
-
1282
- if (min && intValue < parseInt(`${min}`)) {
1283
- const msgArgs = ["error-number-out-of-range-too-small", `${min}`] as const;
1284
-
1285
- errors.push({
1286
- errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
1287
- errorMessageStr: msgStr(...msgArgs),
1288
- fieldIndex: undefined,
1289
- source
1290
- });
1291
-
1292
- break validator_x;
1293
- }
1294
- }
1295
-
1296
- validator_x: {
1297
- const validatorName = "options";
1298
-
1299
- const validator = validators[validatorName];
1300
-
1301
- if (validator === undefined) {
1302
- break validator_x;
1303
- }
1304
-
1305
- if (value === "") {
1306
- break validator_x;
1307
- }
1308
-
1309
- if (validator.options.indexOf(value) >= 0) {
1310
- break validator_x;
1311
- }
1312
-
1313
- const msgArgs = [id<MessageKey_defaultSet>("notAValidOption")] as const;
1314
-
1315
- errors.push({
1316
- errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
1317
- errorMessageStr: msgStr(...msgArgs),
1318
- fieldIndex: undefined,
1319
- source: {
1320
- type: "validator",
1321
- name: validatorName
1322
- }
1323
- });
1324
- }
1325
-
1326
- //TODO: Implement missing validators. See Validators type definition.
1327
-
1328
- return errors;
1329
- }
1330
- );
1331
-
1332
- return { getErrors };
1333
- }
1334
-
1335
- function getIsMultivaluedSingleField(params: { attribute: Attribute }) {
1336
- const { attribute } = params;
1337
-
1338
- return attribute.annotations.inputType?.startsWith("multiselect") ?? false;
1339
- }
1340
-
1341
- export function getButtonToDisplayForMultivaluedAttributeField(params: { attribute: Attribute; values: string[]; fieldIndex: number }) {
1342
- const { attribute, values, fieldIndex } = params;
1343
-
1344
- const hasRemove = (() => {
1345
- if (values.length === 1) {
1346
- return false;
1347
- }
1348
-
1349
- const minCount = (() => {
1350
- const { multivalued } = attribute.validators;
1351
-
1352
- if (multivalued === undefined) {
1353
- return undefined;
1354
- }
1355
-
1356
- const minStr = multivalued.min;
1357
-
1358
- if (minStr === undefined) {
1359
- return undefined;
1360
- }
1361
-
1362
- return parseInt(`${minStr}`);
1363
- })();
1364
-
1365
- if (minCount === undefined) {
1366
- return true;
1367
- }
1368
-
1369
- if (values.length === minCount) {
1370
- return false;
1371
- }
1372
-
1373
- return true;
1374
- })();
1375
-
1376
- const hasAdd = (() => {
1377
- if (fieldIndex + 1 !== values.length) {
1378
- return false;
1379
- }
1380
-
1381
- const maxCount = (() => {
1382
- const { multivalued } = attribute.validators;
1383
-
1384
- if (multivalued === undefined) {
1385
- return undefined;
1386
- }
1387
-
1388
- const maxStr = multivalued.max;
1389
-
1390
- if (maxStr === undefined) {
1391
- return undefined;
1392
- }
1393
-
1394
- return parseInt(`${maxStr}`);
1395
- })();
1396
-
1397
- if (maxCount === undefined) {
1398
- return true;
1399
- }
1400
-
1401
- return values.length !== maxCount;
1402
- })();
1403
-
1404
- return { hasRemove, hasAdd };
1405
- }