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