iss-ctrl 0.0.21 → 0.0.22
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.
- package/README.md +33 -0
- package/fesm2022/iss-ctrl-attachment.viewer-DVUL39xd.mjs +110 -0
- package/fesm2022/iss-ctrl-attachment.viewer-DVUL39xd.mjs.map +1 -0
- package/fesm2022/iss-ctrl-base.mjs +663 -0
- package/fesm2022/iss-ctrl-base.mjs.map +1 -0
- package/fesm2022/iss-ctrl-checkbox.field-Dj5EGLUr.mjs +92 -0
- package/fesm2022/iss-ctrl-checkbox.field-Dj5EGLUr.mjs.map +1 -0
- package/fesm2022/iss-ctrl-checkbox.mjs +143 -0
- package/fesm2022/iss-ctrl-checkbox.mjs.map +1 -0
- package/fesm2022/iss-ctrl-checkbox.viewer-C3SEUszR.mjs +31 -0
- package/fesm2022/iss-ctrl-checkbox.viewer-C3SEUszR.mjs.map +1 -0
- package/fesm2022/iss-ctrl-chips.field-BLLAjBbP.mjs +76 -0
- package/fesm2022/iss-ctrl-chips.field-BLLAjBbP.mjs.map +1 -0
- package/fesm2022/iss-ctrl-chips.mjs +168 -0
- package/fesm2022/iss-ctrl-chips.mjs.map +1 -0
- package/fesm2022/iss-ctrl-chips.viewer-BcKT2hcq.mjs +38 -0
- package/fesm2022/iss-ctrl-chips.viewer-BcKT2hcq.mjs.map +1 -0
- package/fesm2022/iss-ctrl-date-picker.field-BSpXukpO.mjs +98 -0
- package/fesm2022/iss-ctrl-date-picker.field-BSpXukpO.mjs.map +1 -0
- package/fesm2022/iss-ctrl-date-picker.mjs +200 -0
- package/fesm2022/iss-ctrl-date-picker.mjs.map +1 -0
- package/fesm2022/iss-ctrl-date-picker.viewer-Dsmsf91t.mjs +40 -0
- package/fesm2022/iss-ctrl-date-picker.viewer-Dsmsf91t.mjs.map +1 -0
- package/fesm2022/iss-ctrl-date-range-picker.field-CjNHU1RW.mjs +97 -0
- package/fesm2022/iss-ctrl-date-range-picker.field-CjNHU1RW.mjs.map +1 -0
- package/fesm2022/iss-ctrl-date-range-picker.mjs +307 -0
- package/fesm2022/iss-ctrl-date-range-picker.mjs.map +1 -0
- package/fesm2022/iss-ctrl-date-range-picker.viewer-tmTQGPFh.mjs +41 -0
- package/fesm2022/iss-ctrl-date-range-picker.viewer-tmTQGPFh.mjs.map +1 -0
- package/fesm2022/iss-ctrl-datetime-picker.field-DGMY9PaU.mjs +98 -0
- package/fesm2022/iss-ctrl-datetime-picker.field-DGMY9PaU.mjs.map +1 -0
- package/fesm2022/iss-ctrl-datetime-picker.mjs +226 -0
- package/fesm2022/iss-ctrl-datetime-picker.mjs.map +1 -0
- package/fesm2022/iss-ctrl-datetime-picker.viewer-BeUyEBKo.mjs +42 -0
- package/fesm2022/iss-ctrl-datetime-picker.viewer-BeUyEBKo.mjs.map +1 -0
- package/fesm2022/iss-ctrl-file-picker.field-CbepZaX6.mjs +210 -0
- package/fesm2022/iss-ctrl-file-picker.field-CbepZaX6.mjs.map +1 -0
- package/fesm2022/iss-ctrl-file-picker.mjs +272 -0
- package/fesm2022/iss-ctrl-file-picker.mjs.map +1 -0
- package/fesm2022/iss-ctrl-input.field-Ctlf3OCN.mjs +104 -0
- package/fesm2022/iss-ctrl-input.field-Ctlf3OCN.mjs.map +1 -0
- package/fesm2022/iss-ctrl-input.mjs +204 -0
- package/fesm2022/iss-ctrl-input.mjs.map +1 -0
- package/fesm2022/iss-ctrl-input.viewer-BOZnW0_e.mjs +22 -0
- package/fesm2022/iss-ctrl-input.viewer-BOZnW0_e.mjs.map +1 -0
- package/fesm2022/iss-ctrl-iss-ctrl-DvnkwSMG.mjs +1789 -0
- package/fesm2022/iss-ctrl-iss-ctrl-DvnkwSMG.mjs.map +1 -0
- package/fesm2022/{iss-ctrl-load-event-bOt6IlIo.mjs → iss-ctrl-load-event-CuZ7pMjk.mjs} +4 -3
- package/fesm2022/iss-ctrl-load-event-CuZ7pMjk.mjs.map +1 -0
- package/fesm2022/{iss-ctrl-load-options-event-CxyAyNMe.mjs → iss-ctrl-load-options-event-B10rhdqh.mjs} +3 -3
- package/fesm2022/{iss-ctrl-load-options-event-CxyAyNMe.mjs.map → iss-ctrl-load-options-event-B10rhdqh.mjs.map} +1 -1
- package/fesm2022/{iss-ctrl-load-value-event-DJKJ5S3h.mjs → iss-ctrl-load-value-event-C-aRgKLx.mjs} +3 -3
- package/fesm2022/{iss-ctrl-load-value-event-DJKJ5S3h.mjs.map → iss-ctrl-load-value-event-C-aRgKLx.mjs.map} +1 -1
- package/fesm2022/iss-ctrl-mobile.field-DXVYT3q-.mjs +77 -0
- package/fesm2022/iss-ctrl-mobile.field-DXVYT3q-.mjs.map +1 -0
- package/fesm2022/iss-ctrl-mobile.mjs +2465 -0
- package/fesm2022/iss-ctrl-mobile.mjs.map +1 -0
- package/fesm2022/iss-ctrl-mobile.viewer-DQgxAcR-.mjs +22 -0
- package/fesm2022/iss-ctrl-mobile.viewer-DQgxAcR-.mjs.map +1 -0
- package/fesm2022/{iss-ctrl-note.field-B_Lbp28X.mjs → iss-ctrl-note.field-C8X9-mgD.mjs} +2 -2
- package/fesm2022/{iss-ctrl-note.field-B_Lbp28X.mjs.map → iss-ctrl-note.field-C8X9-mgD.mjs.map} +1 -1
- package/fesm2022/iss-ctrl-plate.field-aI81tAOV.mjs +92 -0
- package/fesm2022/iss-ctrl-plate.field-aI81tAOV.mjs.map +1 -0
- package/fesm2022/iss-ctrl-plate.mjs +491 -0
- package/fesm2022/iss-ctrl-plate.mjs.map +1 -0
- package/fesm2022/iss-ctrl-plate.viewer-B8PUBLH8.mjs +179 -0
- package/fesm2022/iss-ctrl-plate.viewer-B8PUBLH8.mjs.map +1 -0
- package/fesm2022/iss-ctrl-radio.field-B_JjPIlb.mjs +96 -0
- package/fesm2022/iss-ctrl-radio.field-B_JjPIlb.mjs.map +1 -0
- package/fesm2022/iss-ctrl-radio.mjs +101 -0
- package/fesm2022/iss-ctrl-radio.mjs.map +1 -0
- package/fesm2022/iss-ctrl-radio.viewer-EiDliZro.mjs +30 -0
- package/fesm2022/iss-ctrl-radio.viewer-EiDliZro.mjs.map +1 -0
- package/fesm2022/{iss-ctrl-replace-api-keys-C0XYQW4i.mjs → iss-ctrl-replace-api-keys-DLnagkOr.mjs} +2 -2
- package/fesm2022/{iss-ctrl-replace-api-keys-C0XYQW4i.mjs.map → iss-ctrl-replace-api-keys-DLnagkOr.mjs.map} +1 -1
- package/fesm2022/iss-ctrl-select.field-DoU4G_8t.mjs +87 -0
- package/fesm2022/iss-ctrl-select.field-DoU4G_8t.mjs.map +1 -0
- package/fesm2022/iss-ctrl-select.mjs +387 -0
- package/fesm2022/iss-ctrl-select.mjs.map +1 -0
- package/fesm2022/iss-ctrl-select.viewer-Dv9M-lG-.mjs +30 -0
- package/fesm2022/iss-ctrl-select.viewer-Dv9M-lG-.mjs.map +1 -0
- package/fesm2022/{iss-ctrl-set-value-event-CgXhi0Bv.mjs → iss-ctrl-set-value-event-Dhw0Vj5J.mjs} +2 -2
- package/fesm2022/{iss-ctrl-set-value-event-CgXhi0Bv.mjs.map → iss-ctrl-set-value-event-Dhw0Vj5J.mjs.map} +1 -1
- package/fesm2022/iss-ctrl-slide-toggle.field-TVVbAr6p.mjs +85 -0
- package/fesm2022/iss-ctrl-slide-toggle.field-TVVbAr6p.mjs.map +1 -0
- package/fesm2022/iss-ctrl-slide-toggle.mjs +72 -0
- package/fesm2022/iss-ctrl-slide-toggle.mjs.map +1 -0
- package/fesm2022/iss-ctrl-slide-toggle.viewer-hsIHwDj3.mjs +34 -0
- package/fesm2022/iss-ctrl-slide-toggle.viewer-hsIHwDj3.mjs.map +1 -0
- package/fesm2022/{iss-ctrl-table.field-B5FxF6hn.mjs → iss-ctrl-table.field-E0r9w0os.mjs} +142 -140
- package/fesm2022/iss-ctrl-table.field-E0r9w0os.mjs.map +1 -0
- package/fesm2022/iss-ctrl-textarea.field-CmHPDpk-.mjs +82 -0
- package/fesm2022/iss-ctrl-textarea.field-CmHPDpk-.mjs.map +1 -0
- package/fesm2022/iss-ctrl-textarea.mjs +115 -0
- package/fesm2022/iss-ctrl-textarea.mjs.map +1 -0
- package/fesm2022/iss-ctrl-textarea.viewer-CpmLauik.mjs +22 -0
- package/fesm2022/iss-ctrl-textarea.viewer-CpmLauik.mjs.map +1 -0
- package/fesm2022/iss-ctrl-time-picker.field-Cb0I5Ut1.mjs +77 -0
- package/fesm2022/iss-ctrl-time-picker.field-Cb0I5Ut1.mjs.map +1 -0
- package/fesm2022/iss-ctrl-time-picker.mjs +170 -0
- package/fesm2022/iss-ctrl-time-picker.mjs.map +1 -0
- package/fesm2022/iss-ctrl-time-picker.viewer-CJeFLrdI.mjs +22 -0
- package/fesm2022/iss-ctrl-time-picker.viewer-CJeFLrdI.mjs.map +1 -0
- package/fesm2022/iss-ctrl-toggle-button.field-Bj8WrkB-.mjs +105 -0
- package/fesm2022/iss-ctrl-toggle-button.field-Bj8WrkB-.mjs.map +1 -0
- package/fesm2022/iss-ctrl-toggle-button.mjs +156 -0
- package/fesm2022/iss-ctrl-toggle-button.mjs.map +1 -0
- package/fesm2022/iss-ctrl-toggle-button.viewer-CC-8_Dy4.mjs +30 -0
- package/fesm2022/iss-ctrl-toggle-button.viewer-CC-8_Dy4.mjs.map +1 -0
- package/fesm2022/iss-ctrl-tokens.mjs +25 -0
- package/fesm2022/iss-ctrl-tokens.mjs.map +1 -0
- package/fesm2022/{iss-ctrl-trigger-based-on-json-value-2v1kLVHZ.mjs → iss-ctrl-trigger-based-on-json-value-D9rTXVD1.mjs} +2 -2
- package/fesm2022/{iss-ctrl-trigger-based-on-json-value-2v1kLVHZ.mjs.map → iss-ctrl-trigger-based-on-json-value-D9rTXVD1.mjs.map} +1 -1
- package/fesm2022/{iss-ctrl-user-picker.field-BH63sZ-X.mjs → iss-ctrl-user-picker.field-BIzB5ZX6.mjs} +151 -150
- package/fesm2022/iss-ctrl-user-picker.field-BIzB5ZX6.mjs.map +1 -0
- package/fesm2022/iss-ctrl.mjs +3 -1
- package/fesm2022/iss-ctrl.mjs.map +1 -1
- package/package.json +69 -1
- package/types/iss-ctrl-base.d.ts +306 -0
- package/types/iss-ctrl-checkbox.d.ts +45 -0
- package/types/iss-ctrl-chips.d.ts +41 -0
- package/types/iss-ctrl-date-picker.d.ts +75 -0
- package/types/iss-ctrl-date-range-picker.d.ts +95 -0
- package/types/iss-ctrl-datetime-picker.d.ts +77 -0
- package/types/iss-ctrl-file-picker.d.ts +95 -0
- package/types/iss-ctrl-input.d.ts +76 -0
- package/types/iss-ctrl-mobile.d.ts +91 -0
- package/types/iss-ctrl-plate.d.ts +54 -0
- package/types/iss-ctrl-radio.d.ts +34 -0
- package/types/iss-ctrl-select.d.ts +75 -0
- package/types/iss-ctrl-slide-toggle.d.ts +26 -0
- package/types/iss-ctrl-textarea.d.ts +30 -0
- package/types/iss-ctrl-time-picker.d.ts +48 -0
- package/types/iss-ctrl-toggle-button.d.ts +41 -0
- package/types/iss-ctrl-tokens.d.ts +21 -0
- package/types/iss-ctrl.d.ts +120 -1810
- package/fesm2022/iss-ctrl-iss-ctrl-BKu6a5HF.mjs +0 -9683
- package/fesm2022/iss-ctrl-iss-ctrl-BKu6a5HF.mjs.map +0 -1
- package/fesm2022/iss-ctrl-load-event-bOt6IlIo.mjs.map +0 -1
- package/fesm2022/iss-ctrl-table.field-B5FxF6hn.mjs.map +0 -1
- package/fesm2022/iss-ctrl-user-picker.field-BH63sZ-X.mjs.map +0 -1
|
@@ -0,0 +1,1789 @@
|
|
|
1
|
+
import { DATETIME_FORMAT } from 'iss-ctrl/base';
|
|
2
|
+
import { UI_LANGUAGE, USER_SEARCH_API, ATTACHMENT_UPLOAD_URL, ATTACHMENT_DOWNLOAD_URL, ATTACHMENT_UPLOAD_RESPONSE_MAPPER } from 'iss-ctrl/tokens';
|
|
3
|
+
import { NgComponentOutlet, AsyncPipe, KeyValuePipe } from '@angular/common';
|
|
4
|
+
import * as i0 from '@angular/core';
|
|
5
|
+
import { signal, isSignal, Injectable, inject, Injector, InjectionToken, Pipe, output, input, DestroyRef, effect, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
6
|
+
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
7
|
+
import { Validators, ControlContainer, FormControl, ReactiveFormsModule } from '@angular/forms';
|
|
8
|
+
import { BehaviorSubject, merge, of, map, from, tap, debounceTime, first, forkJoin, EMPTY } from 'rxjs';
|
|
9
|
+
import { MatError } from '@angular/material/form-field';
|
|
10
|
+
import { PLATE_LETTERS } from 'iss-ctrl/plate';
|
|
11
|
+
import { HttpClient } from '@angular/common/http';
|
|
12
|
+
import { format } from 'date-fns';
|
|
13
|
+
|
|
14
|
+
class FieldAsyncValidation {
|
|
15
|
+
alreadyExists;
|
|
16
|
+
constructor(asyncValidation) {
|
|
17
|
+
this.alreadyExists = asyncValidation?.alreadyExists;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Per-field configuration, reachable as `field.config` and consumed by the `iss-field-*`
|
|
23
|
+
* components. Every entry is a signal, so it can be changed at runtime by field events.
|
|
24
|
+
*
|
|
25
|
+
* Not every key applies to every field — the class doc of each `iss-field-*` component
|
|
26
|
+
* lists exactly which of these it reads.
|
|
27
|
+
*
|
|
28
|
+
* ### Keys by area
|
|
29
|
+
* - **Common:** `type` · `disabled` · `readonly` · `placeholder` · `suffix` · `hideLabel` · `multiple`
|
|
30
|
+
* - **Bounds:** `min` · `max` · `minLength` · `maxLength`
|
|
31
|
+
* - **Dates:** `minDate` · `maxDate` · `dateFilter` · `hijriCalendar` · `emitHijri`
|
|
32
|
+
* - **Attachments:** `allowedFileSize` · `allowedFileTypes` · `minFilesCount` · `maxFilesCount` · `templateId` · `templateUrl`
|
|
33
|
+
* - **Users:** `minUsersCount` · `maxUsersCount`
|
|
34
|
+
* - **Table:** `showInTableColumns` · `tableWithFilter` · `tableWithSort` · `tableWithPagination`
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```json
|
|
38
|
+
* { "name": "email", "type": "text_input", "config": { "type": "email", "maxLength": 120 } }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
class FieldConfig {
|
|
42
|
+
/**
|
|
43
|
+
* Native input type for `text_input` (`text`, `email`, `number`, `password`, …).
|
|
44
|
+
*/
|
|
45
|
+
type = signal(undefined, ...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
|
|
46
|
+
/**
|
|
47
|
+
* Disables the control; the underlying `FormControl` is disabled on init.
|
|
48
|
+
*/
|
|
49
|
+
disabled = signal(undefined, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
50
|
+
/**
|
|
51
|
+
* Shows the value but blocks interaction. Unlike `disabled`, the value is still submitted.
|
|
52
|
+
*/
|
|
53
|
+
readonly = signal(undefined, ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
|
|
54
|
+
/**
|
|
55
|
+
* Placeholder text shown while the field is empty.
|
|
56
|
+
*/
|
|
57
|
+
placeholder = signal(undefined, ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
58
|
+
/**
|
|
59
|
+
* Text rendered after the value (a unit or currency, for example).
|
|
60
|
+
*/
|
|
61
|
+
suffix = signal(undefined, ...(ngDevMode ? [{ debugName: "suffix" }] : /* istanbul ignore next */ []));
|
|
62
|
+
/**
|
|
63
|
+
* Hides the field label without removing it from the accessibility tree.
|
|
64
|
+
*/
|
|
65
|
+
hideLabel = signal(undefined, ...(ngDevMode ? [{ debugName: "hideLabel" }] : /* istanbul ignore next */ []));
|
|
66
|
+
/**
|
|
67
|
+
* Allows more than one value — multi select, multi file upload, multi toggle.
|
|
68
|
+
*/
|
|
69
|
+
multiple = signal(undefined, ...(ngDevMode ? [{ debugName: "multiple" }] : /* istanbul ignore next */ []));
|
|
70
|
+
/**
|
|
71
|
+
* Minimum number of characters. Source: `config.minLength`.
|
|
72
|
+
*/
|
|
73
|
+
minLength = signal(undefined, ...(ngDevMode ? [{ debugName: "minLength" }] : /* istanbul ignore next */ []));
|
|
74
|
+
/**
|
|
75
|
+
* Maximum number of characters. Source: `config.maxLength`.
|
|
76
|
+
*/
|
|
77
|
+
maxLength = signal(undefined, ...(ngDevMode ? [{ debugName: "maxLength" }] : /* istanbul ignore next */ []));
|
|
78
|
+
/**
|
|
79
|
+
* Minimum numeric value (also the minimum user count for the user picker).
|
|
80
|
+
*/
|
|
81
|
+
min = signal(undefined, ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
|
|
82
|
+
/**
|
|
83
|
+
* Maximum numeric value (also the maximum user count for the user picker).
|
|
84
|
+
*/
|
|
85
|
+
max = signal(undefined, ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
|
|
86
|
+
/**
|
|
87
|
+
* Earliest selectable date for date, datetime and date-range fields.
|
|
88
|
+
*/
|
|
89
|
+
minDate = signal(undefined, ...(ngDevMode ? [{ debugName: "minDate" }] : /* istanbul ignore next */ []));
|
|
90
|
+
/**
|
|
91
|
+
* Latest selectable date for date, datetime and date-range fields.
|
|
92
|
+
*/
|
|
93
|
+
maxDate = signal(undefined, ...(ngDevMode ? [{ debugName: "maxDate" }] : /* istanbul ignore next */ []));
|
|
94
|
+
/**
|
|
95
|
+
* Restricts the calendar to working days. Source: `config.acceptWorkDaysOnly`.
|
|
96
|
+
*/
|
|
97
|
+
dateFilter = signal(undefined, ...(ngDevMode ? [{ debugName: "dateFilter" }] : /* istanbul ignore next */ []));
|
|
98
|
+
/**
|
|
99
|
+
* Renders the calendar as an Umm al-Qura Hijri calendar. The emitted value stays Gregorian
|
|
100
|
+
* unless {@link FieldConfig.emitHijri} is also on. Source: `config.hijriCalendar`, also
|
|
101
|
+
* accepted as `config.HijriCalendar`.
|
|
102
|
+
*/
|
|
103
|
+
hijriCalendar = signal(undefined, ...(ngDevMode ? [{ debugName: "hijriCalendar" }] : /* istanbul ignore next */ []));
|
|
104
|
+
/**
|
|
105
|
+
* Emits the picked date as a Hijri date instead of a Gregorian one — the same day emitted
|
|
106
|
+
* as `1447-03-16` rather than `2025-09-08`. Implies {@link FieldConfig.hijriCalendar}: a
|
|
107
|
+
* Hijri value is always picked from a Hijri calendar.
|
|
108
|
+
*/
|
|
109
|
+
emitHijri = signal(undefined, ...(ngDevMode ? [{ debugName: "emitHijri" }] : /* istanbul ignore next */ []));
|
|
110
|
+
/**
|
|
111
|
+
* Id of a downloadable template offered next to an attachment field.
|
|
112
|
+
*/
|
|
113
|
+
templateId = signal(undefined, ...(ngDevMode ? [{ debugName: "templateId" }] : /* istanbul ignore next */ []));
|
|
114
|
+
/**
|
|
115
|
+
* Direct URL of the downloadable template for an attachment field.
|
|
116
|
+
*/
|
|
117
|
+
templateUrl = signal(undefined, ...(ngDevMode ? [{ debugName: "templateUrl" }] : /* istanbul ignore next */ []));
|
|
118
|
+
/**
|
|
119
|
+
* Maximum size per uploaded file. Source: `config.allowedSize`.
|
|
120
|
+
*/
|
|
121
|
+
allowedFileSize = signal(undefined, ...(ngDevMode ? [{ debugName: "allowedFileSize" }] : /* istanbul ignore next */ []));
|
|
122
|
+
/**
|
|
123
|
+
* Comma separated list of accepted file extensions or MIME types.
|
|
124
|
+
*/
|
|
125
|
+
allowedFileTypes = signal(undefined, ...(ngDevMode ? [{ debugName: "allowedFileTypes" }] : /* istanbul ignore next */ []));
|
|
126
|
+
/**
|
|
127
|
+
* Maximum number of files that may be attached.
|
|
128
|
+
*/
|
|
129
|
+
maxFilesCount = signal(undefined, ...(ngDevMode ? [{ debugName: "maxFilesCount" }] : /* istanbul ignore next */ []));
|
|
130
|
+
/**
|
|
131
|
+
* Minimum number of files required.
|
|
132
|
+
*/
|
|
133
|
+
minFilesCount = signal(undefined, ...(ngDevMode ? [{ debugName: "minFilesCount" }] : /* istanbul ignore next */ []));
|
|
134
|
+
/**
|
|
135
|
+
* Maximum number of users selectable in a user picker.
|
|
136
|
+
*/
|
|
137
|
+
maxUsersCount = signal(undefined, ...(ngDevMode ? [{ debugName: "maxUsersCount" }] : /* istanbul ignore next */ []));
|
|
138
|
+
/**
|
|
139
|
+
* Minimum number of users required in a user picker.
|
|
140
|
+
*/
|
|
141
|
+
minUsersCount = signal(undefined, ...(ngDevMode ? [{ debugName: "minUsersCount" }] : /* istanbul ignore next */ []));
|
|
142
|
+
/**
|
|
143
|
+
* Parsed from the field definition but not consumed yet — table columns currently come
|
|
144
|
+
* from `field.children`.
|
|
145
|
+
*/
|
|
146
|
+
showInTableColumns = signal(undefined, ...(ngDevMode ? [{ debugName: "showInTableColumns" }] : /* istanbul ignore next */ []));
|
|
147
|
+
/**
|
|
148
|
+
* Enables the filter row on a `table` field.
|
|
149
|
+
*/
|
|
150
|
+
tableWithFilter = signal(undefined, ...(ngDevMode ? [{ debugName: "tableWithFilter" }] : /* istanbul ignore next */ []));
|
|
151
|
+
/**
|
|
152
|
+
* Enables column sorting on a `table` field.
|
|
153
|
+
*/
|
|
154
|
+
tableWithSort = signal(undefined, ...(ngDevMode ? [{ debugName: "tableWithSort" }] : /* istanbul ignore next */ []));
|
|
155
|
+
/**
|
|
156
|
+
* Enables the paginator on a `table` field.
|
|
157
|
+
*/
|
|
158
|
+
tableWithPagination = signal(undefined, ...(ngDevMode ? [{ debugName: "tableWithPagination" }] : /* istanbul ignore next */ []));
|
|
159
|
+
constructor(config) {
|
|
160
|
+
this.type.set(config?.type);
|
|
161
|
+
this.suffix.set(config?.suffix);
|
|
162
|
+
this.placeholder.set(config?.placeholder);
|
|
163
|
+
this.readonly.set(config?.readonly);
|
|
164
|
+
this.disabled.set(config?.disabled);
|
|
165
|
+
this.multiple.set(config?.multiple);
|
|
166
|
+
this.hideLabel.set(config?.hideLabel);
|
|
167
|
+
this.min.set(config?.min);
|
|
168
|
+
this.max.set(config?.max);
|
|
169
|
+
this.minDate.set(config?.minDate);
|
|
170
|
+
this.maxDate.set(config?.maxDate);
|
|
171
|
+
this.dateFilter.set(config?.acceptWorkDaysOnly);
|
|
172
|
+
this.hijriCalendar.set(config?.hijriCalendar ?? config?.HijriCalendar);
|
|
173
|
+
this.emitHijri.set(config?.emitHijri);
|
|
174
|
+
this.minLength.set(config?.minLength);
|
|
175
|
+
this.maxLength.set(config?.maxLength);
|
|
176
|
+
this.allowedFileSize.set(config?.allowedSize);
|
|
177
|
+
this.allowedFileTypes.set(config?.allowedFileTypes);
|
|
178
|
+
this.maxFilesCount.set(config?.maxFilesCount);
|
|
179
|
+
this.minFilesCount.set(config?.minFilesCount);
|
|
180
|
+
this.templateId.set(config?.templateId);
|
|
181
|
+
this.templateUrl.set(config?.templateUrl);
|
|
182
|
+
this.maxUsersCount.set(config?.maxUsersCount);
|
|
183
|
+
this.minUsersCount.set(config?.minUsersCount);
|
|
184
|
+
this.showInTableColumns.set(config?.showInTableColumns);
|
|
185
|
+
this.tableWithFilter.set(config?.tableWithFilter);
|
|
186
|
+
this.tableWithSort.set(config?.tableWithSort);
|
|
187
|
+
this.tableWithPagination.set(config?.tableWithPagination);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
class FieldActionConfig {
|
|
192
|
+
operator;
|
|
193
|
+
setValueFromApiResponse;
|
|
194
|
+
setValueFromRequestDto;
|
|
195
|
+
setValueFromLoggedInUser;
|
|
196
|
+
setValueFromJsonValue;
|
|
197
|
+
triggerFromApiResponse;
|
|
198
|
+
triggerFromJsonValue;
|
|
199
|
+
replaceApiKeyFromField;
|
|
200
|
+
fn = undefined;
|
|
201
|
+
fnPromise = undefined;
|
|
202
|
+
afterFetchFn = undefined;
|
|
203
|
+
afterFetchFnPromise = undefined;
|
|
204
|
+
triggerFn;
|
|
205
|
+
triggerFnPromise = undefined;
|
|
206
|
+
disableAfterFetching = false;
|
|
207
|
+
disableAfterSetValue = false;
|
|
208
|
+
constructor(action, loader) {
|
|
209
|
+
this.operator = action.config?.operator;
|
|
210
|
+
this.setValueFromApiResponse = action.config?.setValueFromApiResponse;
|
|
211
|
+
this.setValueFromRequestDto = action.config?.setValueFromRequestDto;
|
|
212
|
+
this.setValueFromLoggedInUser = action.config?.setValueFromLoggedInUser;
|
|
213
|
+
this.setValueFromJsonValue = action.config?.setValueFromJsonValue;
|
|
214
|
+
this.triggerFromApiResponse = action.config?.fromApiResponse;
|
|
215
|
+
this.triggerFromJsonValue = action.config?.fromApiResponse;
|
|
216
|
+
this.replaceApiKeyFromField = action.config?.replaceApiKeyFromField;
|
|
217
|
+
this.disableAfterFetching = action.config?.disableAfterFetching ?? false;
|
|
218
|
+
this.disableAfterSetValue = action.config?.disableAfterSetValue ?? false;
|
|
219
|
+
if (action.config?.fn) {
|
|
220
|
+
const fn = loader.loadService(action.config.fn);
|
|
221
|
+
if (fn) {
|
|
222
|
+
this.fnPromise = fn();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (action.config?.afterFetchFn) {
|
|
226
|
+
const fn = loader.loadService(action.config.afterFetchFn);
|
|
227
|
+
if (fn) {
|
|
228
|
+
this.afterFetchFnPromise = fn();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (action.config?.triggerFn) {
|
|
232
|
+
const fn = loader.loadService(action.config.triggerFn);
|
|
233
|
+
if (fn) {
|
|
234
|
+
this.triggerFnPromise = fn();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
class FieldEvent {
|
|
241
|
+
id;
|
|
242
|
+
name;
|
|
243
|
+
value;
|
|
244
|
+
trackedFieldName;
|
|
245
|
+
triggered;
|
|
246
|
+
config;
|
|
247
|
+
fn;
|
|
248
|
+
fnPromise = undefined;
|
|
249
|
+
constructor(action, loader) {
|
|
250
|
+
this.id = action.id;
|
|
251
|
+
this.name = action.name;
|
|
252
|
+
this.value = action.value;
|
|
253
|
+
this.trackedFieldName = action.eventSourceName;
|
|
254
|
+
this.triggered = false;
|
|
255
|
+
this.config = new FieldActionConfig(action, loader);
|
|
256
|
+
const actionFn = loader.loadService(action.name);
|
|
257
|
+
if (actionFn) {
|
|
258
|
+
this.fnPromise = actionFn();
|
|
259
|
+
}
|
|
260
|
+
this.fn = undefined;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Runtime model of a single form field, built from the JSON definition returned by the API.
|
|
266
|
+
*
|
|
267
|
+
* It is what `iss-field-*` components inject through the `FIELD_DATA` token, and the object
|
|
268
|
+
* field events read from and write to. Signal-valued members can be changed at runtime and
|
|
269
|
+
* the rendered field reacts immediately.
|
|
270
|
+
*
|
|
271
|
+
* ### Properties
|
|
272
|
+
* | Property | Type | Description |
|
|
273
|
+
* | --- | --- | --- |
|
|
274
|
+
* | `id` | `number` | Unique field id. |
|
|
275
|
+
* | `name` | `string` | Control name inside the form group. |
|
|
276
|
+
* | `type` | `string` | Key into the `COMPONENTS` index (`text_input`, `dropdown_list`, …). |
|
|
277
|
+
* | `base` | `string` | Underlying data type of the field. |
|
|
278
|
+
* | `label()` | `string` | Visible label. |
|
|
279
|
+
* | `hint()` | `string` | Helper text under the field. |
|
|
280
|
+
* | `options()` | `Option[]` | Choices for select, radio, checkbox and toggle fields. |
|
|
281
|
+
* | `api()` | `string` | Endpoint used to load options or lookups. |
|
|
282
|
+
* | `mode()` | `"edit" \| "view"` | Renders the control or the read-only viewer. |
|
|
283
|
+
* | `visible()` | `boolean` | Whether the field is rendered at all. |
|
|
284
|
+
* | `loading()` | `boolean` | Shows the field's own loading state. |
|
|
285
|
+
* | `config` | `FieldConfig` | Per-type configuration — see {@link FieldConfig}. |
|
|
286
|
+
* | `validation` | `any` | Synchronous validation rules, exactly as the JSON declared them. |
|
|
287
|
+
* | `validationFns` | `Record<string, Function>` | Validator factory behind each active rule, resolved by the outlet. |
|
|
288
|
+
* | `asyncValidation` | `FieldAsyncValidation` | Remote validation rules. |
|
|
289
|
+
* | `events` | `FieldEvent[]` | Cross-field reactions (show, hide, set value, …). |
|
|
290
|
+
* | `children` | `IssField[]` | Nested fields, used by table and group fields. |
|
|
291
|
+
* | `value` / `defaultValue` | `string` | Initial and fallback raw values. |
|
|
292
|
+
* | `jsonValue` | `any` | Initial resolved value for controls with a rich payload. |
|
|
293
|
+
* | `ctrlValue()` | `string \| null` | Live raw value of the control. |
|
|
294
|
+
* | `ctrlJsonValue()` | `any` | Live resolved value (selected options, files, users, …). |
|
|
295
|
+
* | `response()` / `apiResponse` | `any` | Latest payload returned for this field. |
|
|
296
|
+
*/
|
|
297
|
+
class IssField {
|
|
298
|
+
id;
|
|
299
|
+
name;
|
|
300
|
+
type;
|
|
301
|
+
base;
|
|
302
|
+
label = signal("", ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
303
|
+
hint = signal("", ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
|
|
304
|
+
options = signal([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
305
|
+
api = signal("", ...(ngDevMode ? [{ debugName: "api" }] : /* istanbul ignore next */ []));
|
|
306
|
+
mode = signal("edit", ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
307
|
+
visible;
|
|
308
|
+
wasVisible = signal(true, ...(ngDevMode ? [{ debugName: "wasVisible" }] : /* istanbul ignore next */ []));
|
|
309
|
+
events;
|
|
310
|
+
validation;
|
|
311
|
+
// The lazily imported factory behind each active rule in `validation`, keyed by rule name.
|
|
312
|
+
// The factories are kept beside the rules instead of inside them so `validation` stays the
|
|
313
|
+
// object the JSON declared: what a rule says is the API's, how it is applied is ours.
|
|
314
|
+
validationFns = {};
|
|
315
|
+
asyncValidation;
|
|
316
|
+
children;
|
|
317
|
+
apiResponse = new BehaviorSubject(null);
|
|
318
|
+
response = signal(null, ...(ngDevMode ? [{ debugName: "response" }] : /* istanbul ignore next */ []));
|
|
319
|
+
config;
|
|
320
|
+
value;
|
|
321
|
+
jsonValue;
|
|
322
|
+
defaultValue;
|
|
323
|
+
ctrlValue = signal(null, ...(ngDevMode ? [{ debugName: "ctrlValue" }] : /* istanbul ignore next */ []));
|
|
324
|
+
ctrlJsonValue = signal(null, ...(ngDevMode ? [{ debugName: "ctrlJsonValue" }] : /* istanbul ignore next */ []));
|
|
325
|
+
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
326
|
+
constructor(field, loader) {
|
|
327
|
+
this.id = field.id;
|
|
328
|
+
this.name = field.name;
|
|
329
|
+
this.type = field.type;
|
|
330
|
+
this.base = field.base;
|
|
331
|
+
if (isSignal(field.label)) {
|
|
332
|
+
this.label = field.label;
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
this.label = signal(field.label ?? field.title, ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
336
|
+
}
|
|
337
|
+
if (isSignal(field.hint)) {
|
|
338
|
+
this.hint = field.hint;
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
this.hint = signal(field.hint, ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
|
|
342
|
+
}
|
|
343
|
+
if (isSignal(field.options)) {
|
|
344
|
+
this.options = field.options ?? [];
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
this.options = signal(field.options ?? [], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
348
|
+
}
|
|
349
|
+
if (isSignal(field.api)) {
|
|
350
|
+
this.api = field.api;
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
this.api = signal(field.api, ...(ngDevMode ? [{ debugName: "api" }] : /* istanbul ignore next */ []));
|
|
354
|
+
}
|
|
355
|
+
field.mode = field.mode ?? "edit";
|
|
356
|
+
if (isSignal(field.mode)) {
|
|
357
|
+
this.mode = field.mode;
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
this.mode = signal(field.mode, ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
361
|
+
}
|
|
362
|
+
if (!field.events) {
|
|
363
|
+
field.events = [];
|
|
364
|
+
}
|
|
365
|
+
// this.mode() and not field.mode: the caller may have passed a signal
|
|
366
|
+
this.visible = signal(!field.events.some((obj) => obj.name === "show" && this.mode() === "edit"), ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
|
|
367
|
+
this.events = field.events.map((action) => new FieldEvent(action, loader));
|
|
368
|
+
this.validation = field.validation;
|
|
369
|
+
this.asyncValidation = new FieldAsyncValidation(field.asyncValidation);
|
|
370
|
+
this.children = field.children;
|
|
371
|
+
this.config = new FieldConfig(field.config);
|
|
372
|
+
this.value = field?.value;
|
|
373
|
+
this.ctrlValue.set(field.value);
|
|
374
|
+
this.ctrlJsonValue.set(field?.jsonValue);
|
|
375
|
+
this.loading.set(field?.loading ?? false);
|
|
376
|
+
this.defaultValue = field?.defaultValue;
|
|
377
|
+
this.jsonValue = field?.jsonValue;
|
|
378
|
+
this.apiResponse = new BehaviorSubject(null);
|
|
379
|
+
}
|
|
380
|
+
updateVisibility(value) {
|
|
381
|
+
this.wasVisible.set(this.visible());
|
|
382
|
+
this.visible.set(value);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const VALIDATORS = {
|
|
387
|
+
required: () => Promise.resolve().then(function () { return core_validator; }).then((c) => c.IssValidators.required),
|
|
388
|
+
max: () => Promise.resolve().then(function () { return core_validator; }).then((c) => c.IssValidators.max),
|
|
389
|
+
min: () => Promise.resolve().then(function () { return core_validator; }).then((c) => c.IssValidators.min),
|
|
390
|
+
requiredTrue: () => Promise.resolve().then(function () { return core_validator; }).then((c) => c.IssValidators.requiredTrue),
|
|
391
|
+
email: () => Promise.resolve().then(function () { return core_validator; }).then((c) => c.IssValidators.email),
|
|
392
|
+
minLength: () => Promise.resolve().then(function () { return core_validator; }).then((c) => c.IssValidators.minLength),
|
|
393
|
+
maxLength: () => Promise.resolve().then(function () { return core_validator; }).then((c) => c.IssValidators.maxLength),
|
|
394
|
+
pattern: () => Promise.resolve().then(function () { return core_validator; }).then((c) => c.IssValidators.pattern),
|
|
395
|
+
regExp: () => Promise.resolve().then(function () { return core_validator; }).then((c) => c.IssValidators.pattern),
|
|
396
|
+
nullValidator: () => Promise.resolve().then(function () { return core_validator; }).then((c) => c.IssValidators.null),
|
|
397
|
+
beforeDate: () => Promise.resolve().then(function () { return time_validator; }).then((c) => c.TimeValidator.beforeDate),
|
|
398
|
+
afterDate: () => Promise.resolve().then(function () { return time_validator; }).then((c) => c.TimeValidator.afterDate),
|
|
399
|
+
within: () => Promise.resolve().then(function () { return time_validator; }).then((c) => c.TimeValidator.afterDate),
|
|
400
|
+
maxFilesCount: () => Promise.resolve().then(function () { return file_validator; }).then((c) => c.FileValidators.maxCount),
|
|
401
|
+
minFilesCount: () => Promise.resolve().then(function () { return file_validator; }).then((c) => c.FileValidators.minCount),
|
|
402
|
+
fileSize: () => Promise.resolve().then(function () { return file_validator; }).then((c) => c.FileValidators.size),
|
|
403
|
+
fileType: () => Promise.resolve().then(function () { return file_validator; }).then((c) => c.FileValidators.type),
|
|
404
|
+
};
|
|
405
|
+
/**
|
|
406
|
+
* Whether a rule in a field's `validation` is switched on.
|
|
407
|
+
*
|
|
408
|
+
* The rule's value is the argument its validator is built with, so it is also what says
|
|
409
|
+
* whether the rule applies at all: `{ "required": false }` is an optional field, not a
|
|
410
|
+
* required one, and a rule the API left out or sent as null is off the same way. Values
|
|
411
|
+
* that are falsy but meaningful — `0`, `""` — are rules, so only the three off states are
|
|
412
|
+
* rejected here.
|
|
413
|
+
*/
|
|
414
|
+
function isActiveValidationRule(value) {
|
|
415
|
+
return value !== undefined && value !== null && value !== false;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
class FormEngine {
|
|
419
|
+
// Fields are stored per host FormGroup, never on the service itself: a single root
|
|
420
|
+
// FormEngine serves every form on the page, including the nested outlet that
|
|
421
|
+
// iss-field-table renders against its own tableOutletForm.
|
|
422
|
+
// The outer WeakMap lets a form's entry be collected with the form. The inner Map is
|
|
423
|
+
// keyed by the outlet that registered them, so several outlets can share one host form
|
|
424
|
+
// without overwriting each other, and re-registering the same outlet replaces its own
|
|
425
|
+
// contribution instead of accumulating stale entries.
|
|
426
|
+
#registry = new WeakMap();
|
|
427
|
+
registerFields(form, owner, fields) {
|
|
428
|
+
this.#registration(form, owner).fields = fields;
|
|
429
|
+
}
|
|
430
|
+
// The names an outlet expects to see as controls on the host form. An outlet is ready
|
|
431
|
+
// once every one of them has been added, so this is registered up front in ngOnInit,
|
|
432
|
+
// before the fields themselves resolve.
|
|
433
|
+
registerExpectedFieldNames(form, owner, names) {
|
|
434
|
+
this.#registration(form, owner).expectedFieldNames = names;
|
|
435
|
+
}
|
|
436
|
+
unregisterOutlet(form, owner) {
|
|
437
|
+
const owners = this.#registry.get(form);
|
|
438
|
+
if (!owners) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
owners.delete(owner);
|
|
442
|
+
if (!owners.size) {
|
|
443
|
+
this.#registry.delete(form);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
getFields(form) {
|
|
447
|
+
return this.#registrations(form).map((registration) => registration.fields).flat();
|
|
448
|
+
}
|
|
449
|
+
getExpectedFieldNames(form) {
|
|
450
|
+
const names = this.#registrations(form).map((registration) => registration.expectedFieldNames).flat();
|
|
451
|
+
return Array.from(new Set(names));
|
|
452
|
+
}
|
|
453
|
+
// Searches nested children too, so a field inside a table can be found from the form
|
|
454
|
+
// that hosts the table.
|
|
455
|
+
getField(form, name, fields = this.getFields(form)) {
|
|
456
|
+
const match = fields.find((field) => field.name === name);
|
|
457
|
+
if (match) {
|
|
458
|
+
return match;
|
|
459
|
+
}
|
|
460
|
+
return fields
|
|
461
|
+
.filter((field) => field.children?.length)
|
|
462
|
+
.reduce((found, field) => found ?? this.getField(form, name, field.children), undefined);
|
|
463
|
+
}
|
|
464
|
+
getControlValue(form, name) {
|
|
465
|
+
const ctrlValue = form.get(name)?.getRawValue();
|
|
466
|
+
if (ctrlValue !== null && ctrlValue !== undefined && ctrlValue !== "") {
|
|
467
|
+
return ctrlValue;
|
|
468
|
+
}
|
|
469
|
+
return this.getField(form, name)?.value;
|
|
470
|
+
}
|
|
471
|
+
createValidators(field) {
|
|
472
|
+
if (!this.fieldHasValidation(field)) {
|
|
473
|
+
return [Validators.nullValidator];
|
|
474
|
+
}
|
|
475
|
+
return this.#validatorsToNgValidators(field);
|
|
476
|
+
}
|
|
477
|
+
createAsyncValidators(field) {
|
|
478
|
+
if (!(field && field.asyncValidation)) {
|
|
479
|
+
return [];
|
|
480
|
+
}
|
|
481
|
+
return this.#asyncValidatorsToNgValidators(field);
|
|
482
|
+
}
|
|
483
|
+
getFieldValue(field) {
|
|
484
|
+
let fieldValue = field.value;
|
|
485
|
+
if (field.value === undefined && field.defaultValue) {
|
|
486
|
+
fieldValue = field.defaultValue;
|
|
487
|
+
}
|
|
488
|
+
return fieldValue ?? null;
|
|
489
|
+
}
|
|
490
|
+
trackChangesEvents(field, form) {
|
|
491
|
+
const changes$ = [];
|
|
492
|
+
const masterNames = new Set();
|
|
493
|
+
field.events.forEach((action) => {
|
|
494
|
+
this.processTrackChanges(action.trackedFieldName, masterNames, changes$, field, form);
|
|
495
|
+
});
|
|
496
|
+
return merge(...changes$);
|
|
497
|
+
}
|
|
498
|
+
trackChangesValidations(field, form) {
|
|
499
|
+
const changes$ = [];
|
|
500
|
+
const masterNames = new Set();
|
|
501
|
+
// A rule that names another field — `beforeDate`, say — carries that name as its value, and
|
|
502
|
+
// `processTrackChanges` ignores every value that is not a string.
|
|
503
|
+
const iterateOn = Object.entries(field.validation ?? {});
|
|
504
|
+
iterateOn.forEach((validationObj) => {
|
|
505
|
+
this.processTrackChanges(validationObj[1], masterNames, changes$, field, form);
|
|
506
|
+
});
|
|
507
|
+
return merge(...changes$);
|
|
508
|
+
}
|
|
509
|
+
processTrackChanges(masterName, masterNames, changes$, field, form) {
|
|
510
|
+
if (typeof masterName !== "string") {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const masterCtrl = form.get(masterName);
|
|
514
|
+
const masterField = this.getField(form, masterName);
|
|
515
|
+
if (!masterField) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (!masterCtrl) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (masterNames.has(masterName)) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
masterNames.add(masterName);
|
|
525
|
+
// if the field tracks itself, will not subscribe to value changes, it avoids infinite loops
|
|
526
|
+
if (field.name === masterName) {
|
|
527
|
+
changes$.push(of({
|
|
528
|
+
trackedField: masterField,
|
|
529
|
+
trackedControl: masterCtrl,
|
|
530
|
+
value: masterCtrl.value
|
|
531
|
+
}));
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
changes$.push(this.#mapMasterFieldValueChangesToFieldChanges(masterField, masterCtrl));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// A field carries validation only where a rule is actually switched on: one sent as `false`
|
|
538
|
+
// or `null` says the field is free of it, so it counts for nothing here.
|
|
539
|
+
fieldHasValidation(field) {
|
|
540
|
+
return !!field.validation && Object.values(field.validation).some(isActiveValidationRule);
|
|
541
|
+
}
|
|
542
|
+
fieldHasAsyncValidation(field) {
|
|
543
|
+
return !!(field.asyncValidation && Object.values(field.asyncValidation).filter((v) => v).length);
|
|
544
|
+
}
|
|
545
|
+
isFormValid(form, fields) {
|
|
546
|
+
return fields.map((childField) => {
|
|
547
|
+
const currentCtrl = form.get(childField.name);
|
|
548
|
+
if (isActiveValidationRule(childField.validation?.required) && childField.visible() && childField.mode() === "edit" && currentCtrl?.status !== "DISABLED") {
|
|
549
|
+
return currentCtrl?.valid;
|
|
550
|
+
}
|
|
551
|
+
return true;
|
|
552
|
+
}).every((status) => status);
|
|
553
|
+
}
|
|
554
|
+
#registrations(form) {
|
|
555
|
+
const owners = this.#registry.get(form);
|
|
556
|
+
if (!owners) {
|
|
557
|
+
return [];
|
|
558
|
+
}
|
|
559
|
+
return Array.from(owners.values());
|
|
560
|
+
}
|
|
561
|
+
#registration(form, owner) {
|
|
562
|
+
let owners = this.#registry.get(form);
|
|
563
|
+
if (!owners) {
|
|
564
|
+
owners = new Map();
|
|
565
|
+
this.#registry.set(form, owners);
|
|
566
|
+
}
|
|
567
|
+
let registration = owners.get(owner);
|
|
568
|
+
if (!registration) {
|
|
569
|
+
registration = { fields: [], expectedFieldNames: [] };
|
|
570
|
+
owners.set(owner, registration);
|
|
571
|
+
}
|
|
572
|
+
return registration;
|
|
573
|
+
}
|
|
574
|
+
// Builds each switched-on rule into its validator, out of the value the rule was given:
|
|
575
|
+
// `{ "minLength": 3 }` is what makes the validator refuse two characters, and `{ "required":
|
|
576
|
+
// false }` builds nothing at all. The field rides along as the second argument for the rules
|
|
577
|
+
// that need their neighbours — `beforeDate` and friends — and is ignored by the rest.
|
|
578
|
+
// A rule whose factory never resolved is skipped rather than called.
|
|
579
|
+
#validatorsToNgValidators(field) {
|
|
580
|
+
return Object.entries(field.validation ?? {})
|
|
581
|
+
.filter(([key, value]) => isActiveValidationRule(value) && typeof field.validationFns?.[key] === "function")
|
|
582
|
+
.map(([key, value]) => field.validationFns[key](value, field));
|
|
583
|
+
}
|
|
584
|
+
#mapNgValidators(validationObj) {
|
|
585
|
+
return Object.entries((validationObj ? validationObj : {}))
|
|
586
|
+
.filter((validationObj) => validationObj[1] !== undefined)
|
|
587
|
+
.map((validationObj) => validationObj[1]);
|
|
588
|
+
}
|
|
589
|
+
#asyncValidatorsToNgValidators(field) {
|
|
590
|
+
return this.#mapNgValidators(field.asyncValidation).map((validatorValue) => validatorValue?.fn(field));
|
|
591
|
+
}
|
|
592
|
+
#mapMasterFieldValueChangesToFieldChanges(masterField, masterCtrl) {
|
|
593
|
+
return masterCtrl.valueChanges.pipe(map((value) => {
|
|
594
|
+
return {
|
|
595
|
+
trackedField: masterField,
|
|
596
|
+
trackedControl: masterCtrl,
|
|
597
|
+
value: value,
|
|
598
|
+
};
|
|
599
|
+
}));
|
|
600
|
+
}
|
|
601
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FormEngine, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
602
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FormEngine, providedIn: "root" });
|
|
603
|
+
}
|
|
604
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FormEngine, decorators: [{
|
|
605
|
+
type: Injectable,
|
|
606
|
+
args: [{ providedIn: "root" }]
|
|
607
|
+
}] });
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Maps a field's `type` to the `iss-field-*` component that renders it.
|
|
611
|
+
*
|
|
612
|
+
* The outlet looks the field up here and lazy-loads the component, so a `type` that is not
|
|
613
|
+
* a key of this object renders nothing. Hover a field class for the `config` keys it reads.
|
|
614
|
+
*
|
|
615
|
+
* | `type` | Component | Control | Renders |
|
|
616
|
+
* | --- | --- | --- | --- |
|
|
617
|
+
* | `text_input` | `InputField` | `iss-ctrl-input` | Single line text, number, email, password. |
|
|
618
|
+
* | `textarea` | `TextareaField` | `iss-ctrl-textarea` | Multi line free text. |
|
|
619
|
+
* | `dropdown_list` | `SelectField` | `iss-ctrl-select` | Searchable single/multi select. |
|
|
620
|
+
* | `toggle_button` | `ToggleButtonField` | `iss-ctrl-toggle-button` | Options as a button group. |
|
|
621
|
+
* | `checkbox` | `CheckboxField` | `iss-ctrl-checkbox` | Multi choice checkbox list. |
|
|
622
|
+
* | `radio` | `RadioField` | `iss-ctrl-radio` | Single choice radio list. |
|
|
623
|
+
* | `date` | `DatePickerField` | `iss-ctrl-date-picker` | A single date. |
|
|
624
|
+
* | `time` | `TimePickerField` | `iss-ctrl-time-picker` | A time of day. |
|
|
625
|
+
* | `datetime` | `DatetimePickerField` | `iss-ctrl-datetime-picker` | Date and time together. |
|
|
626
|
+
* | `date_range` | `DateRangePickerField` | `iss-ctrl-date-range-picker` | A start/end date range. |
|
|
627
|
+
* | `attachment` | `FilePickerField` | `iss-ctrl-file-picker` | File upload. |
|
|
628
|
+
* | `userPicker` | `UserPickerField` | `iss-ctrl-select` | Employee lookup. |
|
|
629
|
+
* | `vehicle_plate` | `PlateField` | `iss-ctrl-plate-number` | Saudi vehicle plate. |
|
|
630
|
+
* | `enrichment` | `NoteField` | — | Read-only explanatory text. |
|
|
631
|
+
* | `mobile` | `MobileField` | `iss-ctrl-mobile` | International phone number. |
|
|
632
|
+
* | `table` | `TableField` | `iss-ctrl-table` | Repeatable rows built from `children`. |
|
|
633
|
+
* | `chips` | `ChipsField` | `iss-ctrl-chips` | Free text as removable chips. |
|
|
634
|
+
* | `switch` | `SlideToggleField` | `iss-ctrl-slide-toggle` | Boolean on/off switch. |
|
|
635
|
+
*/
|
|
636
|
+
const COMPONENTS = {
|
|
637
|
+
text_input: () => import('./iss-ctrl-input.field-Ctlf3OCN.mjs').then((c) => c.InputField),
|
|
638
|
+
textarea: () => import('./iss-ctrl-textarea.field-CmHPDpk-.mjs').then((c) => c.TextareaField),
|
|
639
|
+
dropdown_list: () => import('./iss-ctrl-select.field-DoU4G_8t.mjs').then((c) => c.SelectField),
|
|
640
|
+
toggle_button: () => import('./iss-ctrl-toggle-button.field-Bj8WrkB-.mjs').then((c) => c.ToggleButtonField),
|
|
641
|
+
checkbox: () => import('./iss-ctrl-checkbox.field-Dj5EGLUr.mjs').then((c) => c.CheckboxField),
|
|
642
|
+
radio: () => import('./iss-ctrl-radio.field-B_JjPIlb.mjs').then((c) => c.RadioField),
|
|
643
|
+
date: () => import('./iss-ctrl-date-picker.field-BSpXukpO.mjs').then((c) => c.DatePickerField),
|
|
644
|
+
time: () => import('./iss-ctrl-time-picker.field-Cb0I5Ut1.mjs').then((c) => c.TimePickerField),
|
|
645
|
+
datetime: () => import('./iss-ctrl-datetime-picker.field-DGMY9PaU.mjs').then((c) => c.DatetimePickerField),
|
|
646
|
+
date_range: () => import('./iss-ctrl-date-range-picker.field-CjNHU1RW.mjs').then((c) => c.DateRangePickerField),
|
|
647
|
+
attachment: () => import('./iss-ctrl-file-picker.field-CbepZaX6.mjs').then((c) => c.FilePickerField),
|
|
648
|
+
userPicker: () => import('./iss-ctrl-user-picker.field-BIzB5ZX6.mjs').then((c) => c.UserPickerField),
|
|
649
|
+
vehicle_plate: () => import('./iss-ctrl-plate.field-aI81tAOV.mjs').then((c) => c.PlateField),
|
|
650
|
+
enrichment: () => import('./iss-ctrl-note.field-C8X9-mgD.mjs').then((c) => c.NoteField),
|
|
651
|
+
mobile: () => import('./iss-ctrl-mobile.field-DXVYT3q-.mjs').then((c) => c.MobileField),
|
|
652
|
+
table: () => import('./iss-ctrl-table.field-E0r9w0os.mjs').then((c) => c.TableField),
|
|
653
|
+
chips: () => import('./iss-ctrl-chips.field-BLLAjBbP.mjs').then((c) => c.ChipsField),
|
|
654
|
+
switch: () => import('./iss-ctrl-slide-toggle.field-TVVbAr6p.mjs').then((c) => c.SlideToggleField),
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
const SERVICES = {
|
|
658
|
+
setValue: () => import('./iss-ctrl-set-value-event-Dhw0Vj5J.mjs').then((s) => s.SetValueEvent),
|
|
659
|
+
show: () => import('./iss-ctrl-show-event-BcRCwAfo.mjs').then((s) => s.ShowEvent),
|
|
660
|
+
load: () => import('./iss-ctrl-load-value-event-C-aRgKLx.mjs').then((s) => s.LoadValueEvent),
|
|
661
|
+
loadOptions: () => import('./iss-ctrl-load-options-event-B10rhdqh.mjs').then((s) => s.LoadOptionsEvent),
|
|
662
|
+
triggerBasedOnJsonValue: () => import('./iss-ctrl-trigger-based-on-json-value-D9rTXVD1.mjs').then((s) => s.TriggerBasedOnJsonValue),
|
|
663
|
+
replaceApiKeyDefaultImpl: () => import('./iss-ctrl-replace-api-keys-DLnagkOr.mjs').then((s) => s.ReplaceApiKeys),
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
// Read-only counterpart of COMPONENTS: the component used to render a field's value
|
|
667
|
+
// without an editable control, keyed by the same field types.
|
|
668
|
+
const VIEWERS = {
|
|
669
|
+
date: () => import('./iss-ctrl-date-picker.viewer-Dsmsf91t.mjs').then((c) => c.DatePickerViewer),
|
|
670
|
+
datetime: () => import('./iss-ctrl-datetime-picker.viewer-BeUyEBKo.mjs').then((c) => c.DatePickerTimeViewer),
|
|
671
|
+
dateRange: () => import('./iss-ctrl-date-range-picker.viewer-tmTQGPFh.mjs').then((c) => c.DateRangePickerViewer),
|
|
672
|
+
date_range: () => import('./iss-ctrl-date-range-picker.viewer-tmTQGPFh.mjs').then((c) => c.DateRangePickerViewer),
|
|
673
|
+
select: () => import('./iss-ctrl-select.viewer-Dv9M-lG-.mjs').then((c) => c.SelectViewer),
|
|
674
|
+
input: () => import('./iss-ctrl-input.viewer-BOZnW0_e.mjs').then((c) => c.InputViewer),
|
|
675
|
+
text: () => import('./iss-ctrl-input.viewer-BOZnW0_e.mjs').then((c) => c.InputViewer),
|
|
676
|
+
checkbox: () => import('./iss-ctrl-checkbox.viewer-C3SEUszR.mjs').then((c) => c.CheckboxViewer),
|
|
677
|
+
file: () => import('./iss-ctrl-attachment.viewer-DVUL39xd.mjs').then((c) => c.AttachmentViewer),
|
|
678
|
+
file2: () => import('./iss-ctrl-attachment.viewer-DVUL39xd.mjs').then((c) => c.AttachmentViewer),
|
|
679
|
+
radio: () => import('./iss-ctrl-radio.viewer-EiDliZro.mjs').then((c) => c.RadioViewer),
|
|
680
|
+
textarea: () => import('./iss-ctrl-textarea.viewer-CpmLauik.mjs').then((c) => c.TextareaViewer),
|
|
681
|
+
chips: () => import('./iss-ctrl-chips.viewer-BcKT2hcq.mjs').then((c) => c.ChipsViewer),
|
|
682
|
+
plate: () => import('./iss-ctrl-plate.viewer-B8PUBLH8.mjs').then((c) => c.PlateViewer),
|
|
683
|
+
slideToggle: () => import('./iss-ctrl-slide-toggle.viewer-hsIHwDj3.mjs').then((c) => c.SlideToggleViewer),
|
|
684
|
+
toggle: () => import('./iss-ctrl-toggle-button.viewer-CC-8_Dy4.mjs').then((c) => c.ToggleButtonViewer),
|
|
685
|
+
time: () => import('./iss-ctrl-time-picker.viewer-CJeFLrdI.mjs').then((c) => c.TimePickerViewer),
|
|
686
|
+
mobile: () => import('./iss-ctrl-mobile.viewer-DQgxAcR-.mjs').then((c) => c.MobileViewer),
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
class Loader {
|
|
690
|
+
#injector = inject(Injector);
|
|
691
|
+
#loadedComponents = new Map();
|
|
692
|
+
#loadedViewers = new Map();
|
|
693
|
+
#loadedServices = new Map();
|
|
694
|
+
loadComponent(field) {
|
|
695
|
+
const loadedComponent = this.#loadedComponents.get(field.type);
|
|
696
|
+
if (loadedComponent) {
|
|
697
|
+
return of(loadedComponent);
|
|
698
|
+
}
|
|
699
|
+
const componentImportFn = COMPONENTS[field.type];
|
|
700
|
+
if (!componentImportFn) {
|
|
701
|
+
return of(null);
|
|
702
|
+
}
|
|
703
|
+
return this.#createObservableFromPromise(componentImportFn, field.type, this.#loadedComponents);
|
|
704
|
+
}
|
|
705
|
+
loadViewer(type) {
|
|
706
|
+
const loadedViewer = this.#loadedViewers.get(type);
|
|
707
|
+
if (loadedViewer) {
|
|
708
|
+
return of(loadedViewer);
|
|
709
|
+
}
|
|
710
|
+
const viewerImportFn = VIEWERS[type];
|
|
711
|
+
if (!viewerImportFn) {
|
|
712
|
+
return of(null);
|
|
713
|
+
}
|
|
714
|
+
return this.#createObservableFromPromise(viewerImportFn, type, this.#loadedViewers);
|
|
715
|
+
}
|
|
716
|
+
loadService(serviceName) {
|
|
717
|
+
const loadedService = this.#loadedServices.get(serviceName);
|
|
718
|
+
if (loadedService) {
|
|
719
|
+
return loadedService;
|
|
720
|
+
}
|
|
721
|
+
if (!SERVICES[serviceName]) {
|
|
722
|
+
// throw new Error(`Service ${serviceName} not found`);
|
|
723
|
+
return undefined;
|
|
724
|
+
}
|
|
725
|
+
return () => SERVICES[serviceName]()
|
|
726
|
+
.then((serviceClass) => this.#injector.get(serviceClass))
|
|
727
|
+
.then((loadedService) => {
|
|
728
|
+
this.#loadedServices.set(serviceName, () => loadedService);
|
|
729
|
+
return loadedService;
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
#createObservableFromPromise(componentImportFn, type, cache) {
|
|
733
|
+
return from(componentImportFn()).pipe(tap((component) => {
|
|
734
|
+
cache.set(type, component);
|
|
735
|
+
}));
|
|
736
|
+
}
|
|
737
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: Loader, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
738
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: Loader, providedIn: "root" });
|
|
739
|
+
}
|
|
740
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: Loader, decorators: [{
|
|
741
|
+
type: Injectable,
|
|
742
|
+
args: [{ providedIn: "root" }]
|
|
743
|
+
}] });
|
|
744
|
+
|
|
745
|
+
const FIELD_DATA = new InjectionToken("Form Field Data");
|
|
746
|
+
|
|
747
|
+
class FieldDataInjector {
|
|
748
|
+
injector = inject(Injector);
|
|
749
|
+
transform(field) {
|
|
750
|
+
return Injector.create({
|
|
751
|
+
parent: this.injector,
|
|
752
|
+
providers: [
|
|
753
|
+
{
|
|
754
|
+
provide: FIELD_DATA,
|
|
755
|
+
useValue: field,
|
|
756
|
+
},
|
|
757
|
+
],
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FieldDataInjector, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
761
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.9", ngImport: i0, type: FieldDataInjector, isStandalone: true, name: "FieldDataInjector" });
|
|
762
|
+
}
|
|
763
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FieldDataInjector, decorators: [{
|
|
764
|
+
type: Pipe,
|
|
765
|
+
args: [{ name: "FieldDataInjector" }]
|
|
766
|
+
}] });
|
|
767
|
+
|
|
768
|
+
class Outlet {
|
|
769
|
+
ready = output();
|
|
770
|
+
formReady = signal(false, ...(ngDevMode ? [{ debugName: "formReady" }] : /* istanbul ignore next */ []));
|
|
771
|
+
fields = input.required(...(ngDevMode ? [{ debugName: "fields" }] : /* istanbul ignore next */ []));
|
|
772
|
+
components = signal(null, ...(ngDevMode ? [{ debugName: "components" }] : /* istanbul ignore next */ []));
|
|
773
|
+
#controlContainer = inject(ControlContainer);
|
|
774
|
+
#destroyRef = inject(DestroyRef);
|
|
775
|
+
#formEngine = inject(FormEngine);
|
|
776
|
+
#loader = inject(Loader);
|
|
777
|
+
#effectRef = effect((onCleanup) => {
|
|
778
|
+
// Dynamic imports cannot be canceled, but an old result must never replace
|
|
779
|
+
// the components produced for a newer fields input (or a destroyed outlet).
|
|
780
|
+
let active = true;
|
|
781
|
+
onCleanup(() => {
|
|
782
|
+
active = false;
|
|
783
|
+
});
|
|
784
|
+
// Remove the previous outlet immediately. This destroys its field components
|
|
785
|
+
// (which remove their controls in ngOnDestroy) and prevents A's metadata from
|
|
786
|
+
// being returned while B is still loading.
|
|
787
|
+
this.components.set(null);
|
|
788
|
+
this.#formEngine.registerFields(this.hostFormGroup, this, []);
|
|
789
|
+
void this.#componentsFactory(this.fields()).then((components) => {
|
|
790
|
+
if (!active) {
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
this.components.set(components);
|
|
794
|
+
this.#formEngine.registerFields(this.hostFormGroup, this, components.map((component) => component.data));
|
|
795
|
+
});
|
|
796
|
+
}, ...(ngDevMode ? [{ debugName: "#effectRef" }] : /* istanbul ignore next */ []));
|
|
797
|
+
get hostFormGroup() {
|
|
798
|
+
return this.#controlContainer.control;
|
|
799
|
+
}
|
|
800
|
+
ngOnInit() {
|
|
801
|
+
const expectedFieldNames = this.fields()
|
|
802
|
+
.filter((f) => f.mode() === "edit" || (f.mode() === "view" && f.value))
|
|
803
|
+
.map((f) => f.name);
|
|
804
|
+
this.#formEngine.registerExpectedFieldNames(this.hostFormGroup, this, expectedFieldNames);
|
|
805
|
+
this.#listenToHostFormGroupChanges().pipe(takeUntilDestroyed(this.#destroyRef)).subscribe((isReady) => {
|
|
806
|
+
if (isReady) {
|
|
807
|
+
this.formReady.set(true);
|
|
808
|
+
this.ready.emit(true);
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
ngOnDestroy() {
|
|
813
|
+
this.#effectRef.destroy();
|
|
814
|
+
this.#formEngine.unregisterOutlet(this.hostFormGroup, this);
|
|
815
|
+
}
|
|
816
|
+
async #componentsFactory(fields) {
|
|
817
|
+
const fieldsComponentPromise = fields.map(async (field) => {
|
|
818
|
+
return {
|
|
819
|
+
self$: this.#loader.loadComponent(field),
|
|
820
|
+
data: await this.#initField(field)
|
|
821
|
+
};
|
|
822
|
+
});
|
|
823
|
+
return await Promise.all(fieldsComponentPromise);
|
|
824
|
+
}
|
|
825
|
+
/*todo needs refactoring to be in the issField class*/
|
|
826
|
+
async #initField(field) {
|
|
827
|
+
const f = new IssField(field, this.#loader);
|
|
828
|
+
await Promise.all([
|
|
829
|
+
this.#loadFieldEvents(f),
|
|
830
|
+
this.#loadFieldValidation(f)
|
|
831
|
+
]);
|
|
832
|
+
return f;
|
|
833
|
+
}
|
|
834
|
+
async #loadFieldEvents(f) {
|
|
835
|
+
if (!f.events?.length)
|
|
836
|
+
return;
|
|
837
|
+
await Promise.all(f.events.map(async (action) => {
|
|
838
|
+
const [fn, triggerFn, configFn] = await Promise.all([
|
|
839
|
+
action.fnPromise,
|
|
840
|
+
action.config.triggerFnPromise,
|
|
841
|
+
action.config.fnPromise
|
|
842
|
+
]);
|
|
843
|
+
action.fn = fn;
|
|
844
|
+
if (triggerFn) {
|
|
845
|
+
action.config.triggerFn = triggerFn;
|
|
846
|
+
}
|
|
847
|
+
action.config.fn = configFn;
|
|
848
|
+
}));
|
|
849
|
+
}
|
|
850
|
+
// Resolves the validator behind every rule the field switched on. `validation` itself is left
|
|
851
|
+
// exactly as it arrived — the caller's own object, which it may still read and re-render — so
|
|
852
|
+
// the factories land in `validationFns` and the rules keep the values they were given.
|
|
853
|
+
// A rule that is off carries no validator: nothing is imported for it and nothing is built
|
|
854
|
+
// from it, which is what makes `{ "required": false }` an optional field.
|
|
855
|
+
async #loadFieldValidation(f) {
|
|
856
|
+
if (!f.validation)
|
|
857
|
+
return;
|
|
858
|
+
const validationEntries = Object.entries(f.validation).filter(([key, validation]) => {
|
|
859
|
+
if (VALIDATORS[key] === undefined) {
|
|
860
|
+
console.error(`${key} not found`);
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
return isActiveValidationRule(validation);
|
|
864
|
+
});
|
|
865
|
+
await Promise.all(validationEntries.map(async ([key]) => {
|
|
866
|
+
f.validationFns[key] = await VALIDATORS[key]();
|
|
867
|
+
}));
|
|
868
|
+
}
|
|
869
|
+
#listenToHostFormGroupChanges() {
|
|
870
|
+
return this.hostFormGroup.valueChanges.pipe(debounceTime(500), map(() => {
|
|
871
|
+
const controlNames = Object.keys(this.hostFormGroup.getRawValue());
|
|
872
|
+
const fieldNames = this.#formEngine.getExpectedFieldNames(this.hostFormGroup);
|
|
873
|
+
const diff = fieldNames.filter((x) => !controlNames.includes(x));
|
|
874
|
+
return diff.length === 0;
|
|
875
|
+
}), first((v) => v));
|
|
876
|
+
}
|
|
877
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: Outlet, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
878
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: Outlet, isStandalone: true, selector: "iss-form-outlet", inputs: { fields: { classPropertyName: "fields", publicName: "fields", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { ready: "ready" }, ngImport: i0, template: `
|
|
879
|
+
@if (components() && components()?.length) {
|
|
880
|
+
@for (component of components(); track $index) {
|
|
881
|
+
<ng-container
|
|
882
|
+
[ngComponentOutlet]="component.self$ | async"
|
|
883
|
+
[ngComponentOutletInjector]="component.data | FieldDataInjector"
|
|
884
|
+
[ngComponentOutletInputs]="{formReady: formReady(), issField: component.data | FieldDataInjector}"
|
|
885
|
+
/>
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
`, isInline: true, styles: [":host{display:flex;flex-direction:column;gap:2rem}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "pipe", type: FieldDataInjector, name: "FieldDataInjector" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
889
|
+
}
|
|
890
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: Outlet, decorators: [{
|
|
891
|
+
type: Component,
|
|
892
|
+
args: [{ selector: "iss-form-outlet", imports: [
|
|
893
|
+
NgComponentOutlet,
|
|
894
|
+
FieldDataInjector,
|
|
895
|
+
AsyncPipe,
|
|
896
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
897
|
+
@if (components() && components()?.length) {
|
|
898
|
+
@for (component of components(); track $index) {
|
|
899
|
+
<ng-container
|
|
900
|
+
[ngComponentOutlet]="component.self$ | async"
|
|
901
|
+
[ngComponentOutletInjector]="component.data | FieldDataInjector"
|
|
902
|
+
[ngComponentOutletInputs]="{formReady: formReady(), issField: component.data | FieldDataInjector}"
|
|
903
|
+
/>
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
`, styles: [":host{display:flex;flex-direction:column;gap:2rem}\n"] }]
|
|
907
|
+
}], propDecorators: { ready: [{ type: i0.Output, args: ["ready"] }], fields: [{ type: i0.Input, args: [{ isSignal: true, alias: "fields", required: true }] }] } });
|
|
908
|
+
|
|
909
|
+
class EventTrigger {
|
|
910
|
+
isTriggered(field, change, action, masterField) {
|
|
911
|
+
if (field.name === action.trackedFieldName) {
|
|
912
|
+
return true;
|
|
913
|
+
}
|
|
914
|
+
return action.value.split(",").some((actionValue) => {
|
|
915
|
+
if (actionValue === "*" && change.value !== null && change.value !== undefined) {
|
|
916
|
+
return true;
|
|
917
|
+
}
|
|
918
|
+
if (!change) {
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
if (change.value === null || change.value === undefined) {
|
|
922
|
+
return false;
|
|
923
|
+
}
|
|
924
|
+
const changeValueArray = change?.value.toString().split(",").map((val) => val.trim());
|
|
925
|
+
return (changeValueArray?.includes(actionValue.trim()));
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: EventTrigger, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
929
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: EventTrigger, providedIn: "root" });
|
|
930
|
+
}
|
|
931
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: EventTrigger, decorators: [{
|
|
932
|
+
type: Injectable,
|
|
933
|
+
args: [{ providedIn: "root" }]
|
|
934
|
+
}] });
|
|
935
|
+
|
|
936
|
+
class EventTriggerManager {
|
|
937
|
+
actionTriggerService = inject(EventTrigger);
|
|
938
|
+
async getActionsToBeTriggered(field, change) {
|
|
939
|
+
const actionsToBeTriggered = [];
|
|
940
|
+
for (const action of field.events) {
|
|
941
|
+
/* todo remove await from array and use allSettle instead or any other solution */
|
|
942
|
+
const isTruthy = await this.#filterCondition(field, action, change);
|
|
943
|
+
if (isTruthy) {
|
|
944
|
+
actionsToBeTriggered.push(action);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return actionsToBeTriggered;
|
|
948
|
+
}
|
|
949
|
+
async getActionsToBeReverted(field, change) {
|
|
950
|
+
const actionsToBeReverted = [];
|
|
951
|
+
for (const action of field.events) {
|
|
952
|
+
if (action.trackedFieldName !== change.trackedField.name) {
|
|
953
|
+
/* todo remove await from array and use allSettle instead or any other solution */
|
|
954
|
+
const isTruthy = await this.#filterCondition(field, action, change);
|
|
955
|
+
if (!isTruthy && action.triggered) {
|
|
956
|
+
actionsToBeReverted.push(action);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return actionsToBeReverted;
|
|
961
|
+
}
|
|
962
|
+
async #filterCondition(field, action, change) {
|
|
963
|
+
let triggerFn = action.config.triggerFn;
|
|
964
|
+
if (!triggerFn && action.config.triggerFnPromise) {
|
|
965
|
+
triggerFn = await action.config.triggerFnPromise;
|
|
966
|
+
}
|
|
967
|
+
if (triggerFn) {
|
|
968
|
+
return triggerFn.isTriggered(field, change, action);
|
|
969
|
+
}
|
|
970
|
+
return this.actionTriggerService.isTriggered(field, change, action);
|
|
971
|
+
}
|
|
972
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: EventTriggerManager, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
973
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: EventTriggerManager, providedIn: "root" });
|
|
974
|
+
}
|
|
975
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: EventTriggerManager, decorators: [{
|
|
976
|
+
type: Injectable,
|
|
977
|
+
args: [{ providedIn: "root" }]
|
|
978
|
+
}] });
|
|
979
|
+
|
|
980
|
+
class EventHandler {
|
|
981
|
+
#actionTriggerManager = inject(EventTriggerManager);
|
|
982
|
+
async fire(control, form, field, change) {
|
|
983
|
+
let actions = await this.#actionTriggerManager.getActionsToBeTriggered(field, change);
|
|
984
|
+
/* todo need to follow up on actions on this field (self) */
|
|
985
|
+
for (let i = 0; i < actions.length; i++) {
|
|
986
|
+
let evt = actions[i].fn;
|
|
987
|
+
if (!evt) {
|
|
988
|
+
/* todo remove await from array and use allSettle instead or any other solution */
|
|
989
|
+
evt = await actions[i].fnPromise;
|
|
990
|
+
}
|
|
991
|
+
const args = this.#fieldEventArgumentFactory(control, form, field, change, actions[i]);
|
|
992
|
+
evt?.fire(args);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
async revert(control, form, field, change) {
|
|
996
|
+
const actions = await this.#actionTriggerManager.getActionsToBeReverted(field, change);
|
|
997
|
+
for (let i = 0; i < actions.length; i++) {
|
|
998
|
+
let evt = actions[i].fn;
|
|
999
|
+
if (!evt) {
|
|
1000
|
+
evt = await actions[i].fnPromise;
|
|
1001
|
+
}
|
|
1002
|
+
const args = this.#fieldEventArgumentFactory(control, form, field, change, actions[i]);
|
|
1003
|
+
evt?.revert(args);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
#fieldEventArgumentFactory(control, form, field, change, action) {
|
|
1007
|
+
return {
|
|
1008
|
+
action,
|
|
1009
|
+
change,
|
|
1010
|
+
control,
|
|
1011
|
+
field,
|
|
1012
|
+
form
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: EventHandler, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1016
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: EventHandler, providedIn: "root" });
|
|
1017
|
+
}
|
|
1018
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: EventHandler, decorators: [{
|
|
1019
|
+
type: Injectable,
|
|
1020
|
+
args: [{ providedIn: "root" }]
|
|
1021
|
+
}] });
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Base class for every `iss-field-*` field.
|
|
1025
|
+
*
|
|
1026
|
+
* A field is the form-engine layer around a control: it reads its whole configuration from
|
|
1027
|
+
* the injected {@link IssField} (label, options, config, validation, events), registers its
|
|
1028
|
+
* `FormControl` on the parent `FormGroup` on init and removes it again on destroy, and
|
|
1029
|
+
* switches between the control (`mode() === "edit"`) and the viewer (`mode() === "view"`).
|
|
1030
|
+
*
|
|
1031
|
+
* ### Inputs
|
|
1032
|
+
* | Property | Type | Description |
|
|
1033
|
+
* | --- | --- | --- |
|
|
1034
|
+
* | `issField` | `IssField` | **Required.** The field definition; also provided through the `FIELD_DATA` token. |
|
|
1035
|
+
* | `formReady` | `boolean \| undefined` | Set by the outlet once the whole form exists; gates event wiring. |
|
|
1036
|
+
*
|
|
1037
|
+
* ### Properties available on `field`
|
|
1038
|
+
* `name` · `type` · `base` · `label()` · `hint()` · `options()` · `api()` · `mode()` ·
|
|
1039
|
+
* `visible()` · `loading()` · `value` · `jsonValue` · `defaultValue` · `ctrlValue()` ·
|
|
1040
|
+
* `ctrlJsonValue()` · `events` · `validation` · `asyncValidation` · `children` ·
|
|
1041
|
+
* `response()` · `config` — see {@link IssField} and {@link FieldConfig}.
|
|
1042
|
+
*
|
|
1043
|
+
* Fields are not used directly in templates; the outlet resolves them from the field `type`
|
|
1044
|
+
* through the `COMPONENTS` index.
|
|
1045
|
+
*/
|
|
1046
|
+
class FieldBase {
|
|
1047
|
+
/**
|
|
1048
|
+
* The field definition rendered by this component. Required.
|
|
1049
|
+
*/
|
|
1050
|
+
issField = input.required(...(ngDevMode ? [{ debugName: "issField" }] : /* istanbul ignore next */ []));
|
|
1051
|
+
/**
|
|
1052
|
+
* Set by the outlet once every field is registered; field events start only after it turns true.
|
|
1053
|
+
*/
|
|
1054
|
+
formReady = input(...(ngDevMode ? [undefined, { debugName: "formReady" }] : /* istanbul ignore next */ []));
|
|
1055
|
+
/**
|
|
1056
|
+
* The {@link IssField} for this instance, injected through the `FIELD_DATA` token.
|
|
1057
|
+
*/
|
|
1058
|
+
field = inject(FIELD_DATA);
|
|
1059
|
+
parentFormGroup = inject(ControlContainer);
|
|
1060
|
+
formEngine = inject(FormEngine);
|
|
1061
|
+
eventHandler = inject(EventHandler);
|
|
1062
|
+
destroyRef = inject(DestroyRef);
|
|
1063
|
+
/**
|
|
1064
|
+
* The `FormControl` this field registers on the parent form under `field.name`.
|
|
1065
|
+
*/
|
|
1066
|
+
ctrl = new FormControl(null);
|
|
1067
|
+
ctrlValueChange = toSignal(this.ctrl.valueChanges, { initialValue: this.ctrl.getRawValue() });
|
|
1068
|
+
#valueChangeEffectRef = effect(() => {
|
|
1069
|
+
this.field.ctrlValue.set(this.ctrlValueChange());
|
|
1070
|
+
}, ...(ngDevMode ? [{ debugName: "#valueChangeEffectRef" }] : /* istanbul ignore next */ []));
|
|
1071
|
+
#eventEffectRef = effect(() => {
|
|
1072
|
+
if (this.formReady()) {
|
|
1073
|
+
this.#initiateEvents();
|
|
1074
|
+
}
|
|
1075
|
+
}, ...(ngDevMode ? [{ debugName: "#eventEffectRef" }] : /* istanbul ignore next */ []));
|
|
1076
|
+
get form() {
|
|
1077
|
+
return this.parentFormGroup.control;
|
|
1078
|
+
}
|
|
1079
|
+
ngOnInit() {
|
|
1080
|
+
this.#addControlToForm();
|
|
1081
|
+
}
|
|
1082
|
+
ngOnDestroy() {
|
|
1083
|
+
this.#valueChangeEffectRef.destroy();
|
|
1084
|
+
this.#eventEffectRef.destroy();
|
|
1085
|
+
this.#removeControlFromForm();
|
|
1086
|
+
}
|
|
1087
|
+
#addControlToForm() {
|
|
1088
|
+
this.ctrl.setValue(this.formEngine.getFieldValue(this.field));
|
|
1089
|
+
this.ctrl.setValidators(this.formEngine.createValidators(this.field));
|
|
1090
|
+
this.ctrl.setAsyncValidators(this.formEngine.createAsyncValidators(this.field));
|
|
1091
|
+
if (this.field.config.disabled() === true) {
|
|
1092
|
+
this.ctrl.disable({ emitEvent: false });
|
|
1093
|
+
}
|
|
1094
|
+
this.form.addControl(this.field.name, this.ctrl);
|
|
1095
|
+
if (this.ctrl.getRawValue()) {
|
|
1096
|
+
this.ctrl.markAsTouched();
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
// a conditionally rendered field would otherwise leave its control behind on the host
|
|
1100
|
+
// form, keeping it in the form value and in the outlet's readiness diff forever
|
|
1101
|
+
#removeControlFromForm() {
|
|
1102
|
+
if (this.form?.get(this.field.name) === this.ctrl) {
|
|
1103
|
+
this.form.removeControl(this.field.name);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
#initiateEvents() {
|
|
1107
|
+
// todo don't start events in view mode, just set value
|
|
1108
|
+
if (this.field.events && this.field.events.length) {
|
|
1109
|
+
this.#startEvents();
|
|
1110
|
+
}
|
|
1111
|
+
if (this.field.validation) {
|
|
1112
|
+
this.#updateValidityOnMasterChanges();
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
#startEvents() {
|
|
1116
|
+
this.formEngine.trackChangesEvents(this.field, this.form)
|
|
1117
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
1118
|
+
.subscribe((change) => {
|
|
1119
|
+
this.eventHandler.revert(this.ctrl, this.form, this.field, change).then();
|
|
1120
|
+
this.eventHandler.fire(this.ctrl, this.form, this.field, change).then();
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
#updateValidityOnMasterChanges() {
|
|
1124
|
+
this.formEngine.trackChangesValidations(this.field, this.form)
|
|
1125
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
1126
|
+
.subscribe(() => {
|
|
1127
|
+
this.ctrl.updateValueAndValidity({ emitEvent: false });
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FieldBase, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1131
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.9", type: FieldBase, isStandalone: true, selector: "ng-component", inputs: { issField: { classPropertyName: "issField", publicName: "issField", isSignal: true, isRequired: true, transformFunction: null }, formReady: { classPropertyName: "formReady", publicName: "formReady", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: ``, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }] });
|
|
1132
|
+
}
|
|
1133
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FieldBase, decorators: [{
|
|
1134
|
+
type: Component,
|
|
1135
|
+
args: [{
|
|
1136
|
+
template: ``,
|
|
1137
|
+
imports: [ReactiveFormsModule]
|
|
1138
|
+
}]
|
|
1139
|
+
}], propDecorators: { issField: [{ type: i0.Input, args: [{ isSignal: true, alias: "issField", required: true }] }], formReady: [{ type: i0.Input, args: [{ isSignal: true, alias: "formReady", required: false }] }] } });
|
|
1140
|
+
|
|
1141
|
+
const VIEW_PROVIDER = {
|
|
1142
|
+
provide: ControlContainer,
|
|
1143
|
+
useFactory: () => inject(ControlContainer, { skipSelf: true }),
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
const PLACEHOLDER_REGEX = /\{((\w+).)*(\w+)}/gi;
|
|
1147
|
+
|
|
1148
|
+
const VALIDATION_MESSAGES = {
|
|
1149
|
+
required: () => {
|
|
1150
|
+
return {
|
|
1151
|
+
en: "Mandatory Field",
|
|
1152
|
+
ar: "حقل إجباري",
|
|
1153
|
+
};
|
|
1154
|
+
},
|
|
1155
|
+
requiredTrue: () => {
|
|
1156
|
+
return {
|
|
1157
|
+
en: "Please Choose",
|
|
1158
|
+
ar: "يُرجى الموافقة",
|
|
1159
|
+
};
|
|
1160
|
+
},
|
|
1161
|
+
email: () => {
|
|
1162
|
+
return {
|
|
1163
|
+
en: "Please enter a valid email address",
|
|
1164
|
+
ar: "يرجى كتابة عنوان بريد إلكتروني صحيح",
|
|
1165
|
+
};
|
|
1166
|
+
},
|
|
1167
|
+
minlength: ({ requiredLength }) => {
|
|
1168
|
+
return {
|
|
1169
|
+
en: `Please enter at least ${requiredLength} characters`,
|
|
1170
|
+
ar: `يُرجى إدخال ${requiredLength} على الأقل`,
|
|
1171
|
+
};
|
|
1172
|
+
},
|
|
1173
|
+
maxlength: ({ requiredLength }) => {
|
|
1174
|
+
return {
|
|
1175
|
+
en: `Please enter at most ${requiredLength} characters`,
|
|
1176
|
+
ar: `يُرجى إدخال ${requiredLength} على الأكثر`,
|
|
1177
|
+
};
|
|
1178
|
+
},
|
|
1179
|
+
maxAllowedUsers: (max) => {
|
|
1180
|
+
return {
|
|
1181
|
+
en: `Please enter at most ${max} users`,
|
|
1182
|
+
ar: `يُرجى اختيار ${max} مستخدم على الأكثر `,
|
|
1183
|
+
};
|
|
1184
|
+
},
|
|
1185
|
+
minAllowedUsers: (min) => {
|
|
1186
|
+
return {
|
|
1187
|
+
en: `Please enter at least ${min} users`,
|
|
1188
|
+
ar: `يُرجى اختيار ${min} مستخدم على الأقل `,
|
|
1189
|
+
};
|
|
1190
|
+
},
|
|
1191
|
+
min: ({ min }) => {
|
|
1192
|
+
return {
|
|
1193
|
+
en: `Please enter at least ${min}`,
|
|
1194
|
+
ar: `يُرجى إدخال ${min} على الأقل`,
|
|
1195
|
+
};
|
|
1196
|
+
},
|
|
1197
|
+
max: ({ max }) => {
|
|
1198
|
+
return {
|
|
1199
|
+
en: `Please enter at most ${max}`,
|
|
1200
|
+
ar: `يُرجى إدخال ${max} على الأكثر`,
|
|
1201
|
+
};
|
|
1202
|
+
},
|
|
1203
|
+
pattern: () => {
|
|
1204
|
+
return {
|
|
1205
|
+
en: "Please enter a valid value",
|
|
1206
|
+
ar: "يرجى إدخال قيم صحيحة",
|
|
1207
|
+
};
|
|
1208
|
+
},
|
|
1209
|
+
wrongTimeSelected: (wrongTimeSelected) => {
|
|
1210
|
+
return {
|
|
1211
|
+
en: `Selected time should be after ${wrongTimeSelected} hours from now.`,
|
|
1212
|
+
ar: `الوقت المختار يجب أن يكون بعد ${wrongTimeSelected} من الآن. `,
|
|
1213
|
+
};
|
|
1214
|
+
},
|
|
1215
|
+
relativeTimeValidation_after: (relativeTimeValidation) => {
|
|
1216
|
+
return {
|
|
1217
|
+
en: `The selected end date should be greater than the ${relativeTimeValidation}.`,
|
|
1218
|
+
ar: ` تاريخ الانتهاء يجب ان يكون اكبر من ${relativeTimeValidation}.`,
|
|
1219
|
+
};
|
|
1220
|
+
},
|
|
1221
|
+
relativeTimeValidation_before: (relativeTimeValidation) => {
|
|
1222
|
+
return {
|
|
1223
|
+
en: `Selected time should be before ${relativeTimeValidation}.`,
|
|
1224
|
+
ar: `الوقت المختار يجب أن يكون قبل ${relativeTimeValidation}.`,
|
|
1225
|
+
};
|
|
1226
|
+
},
|
|
1227
|
+
relativeTimeValidation_before_noOfDays: (relativeTimeValidation) => {
|
|
1228
|
+
return {
|
|
1229
|
+
en: `Selected time should be before ${relativeTimeValidation} .`,
|
|
1230
|
+
ar: `الوقت المختار يجب أن يكون قبل ${relativeTimeValidation} .`,
|
|
1231
|
+
};
|
|
1232
|
+
},
|
|
1233
|
+
relativeTimeValidation_within: (relativeTimeValidation) => {
|
|
1234
|
+
return {
|
|
1235
|
+
en: `Selected time should be within ${relativeTimeValidation}.`,
|
|
1236
|
+
ar: ` يجب أن يكون الوقت المحدد ضمن ${relativeTimeValidation}.`,
|
|
1237
|
+
};
|
|
1238
|
+
},
|
|
1239
|
+
fileType: ({ validFileExtensions, inValidFileName }) => ({
|
|
1240
|
+
en: `${inValidFileName} should be ${validFileExtensions}`,
|
|
1241
|
+
ar: `يجب أن يكون ${inValidFileName} بصيغة ${validFileExtensions}`,
|
|
1242
|
+
}),
|
|
1243
|
+
fileSize: ({ limitSize, inValidFileName }) => ({
|
|
1244
|
+
en: `${inValidFileName} size should not exceed ${limitSize}.`,
|
|
1245
|
+
ar: `يجب ألا يزيد حجم ${inValidFileName}} عن ${limitSize}`,
|
|
1246
|
+
}),
|
|
1247
|
+
minFileCount: ({ minCount, fileCount }) => {
|
|
1248
|
+
return {
|
|
1249
|
+
en: `At least ${minCount} file(s) required, but only ${fileCount} selected.`,
|
|
1250
|
+
ar: `تتطلب العملية على الأقل ${minCount} ملف(ات)، لكن تم اختيار ${fileCount} فقط.`,
|
|
1251
|
+
};
|
|
1252
|
+
},
|
|
1253
|
+
maxFileCount: ({ maxCount, fileCount }) => {
|
|
1254
|
+
return {
|
|
1255
|
+
en: `You can upload a maximum of ${maxCount} file(s). You have selected ${fileCount} file(s).`,
|
|
1256
|
+
ar: `يمكنك تحميل حد أقصى من ${maxCount} ملف(ات). لقد قمت باختيار ${fileCount} ملف(ات).`
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
class TranslatePipe {
|
|
1262
|
+
currentLanguage = inject(UI_LANGUAGE);
|
|
1263
|
+
transform(value, locale = "") {
|
|
1264
|
+
if (typeof value === "string") {
|
|
1265
|
+
return value; // Return the string as is, if it's a string
|
|
1266
|
+
}
|
|
1267
|
+
if (value && locale) {
|
|
1268
|
+
return value[locale];
|
|
1269
|
+
}
|
|
1270
|
+
return value?.[this.currentLanguage];
|
|
1271
|
+
}
|
|
1272
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: TranslatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
1273
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.9", ngImport: i0, type: TranslatePipe, isStandalone: true, name: "translate" });
|
|
1274
|
+
}
|
|
1275
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: TranslatePipe, decorators: [{
|
|
1276
|
+
type: Pipe,
|
|
1277
|
+
args: [{ name: "translate" }]
|
|
1278
|
+
}] });
|
|
1279
|
+
|
|
1280
|
+
class ValidationMessage {
|
|
1281
|
+
errors = input.required(...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
|
|
1282
|
+
showMessage = input.required(...(ngDevMode ? [{ debugName: "showMessage" }] : /* istanbul ignore next */ []));
|
|
1283
|
+
getValidationMessage(error) {
|
|
1284
|
+
const errorMessageGenerator = VALIDATION_MESSAGES[error.key];
|
|
1285
|
+
if (!errorMessageGenerator) {
|
|
1286
|
+
console.warn(`THERE IS NO VALIDATION MESSAGE FOR KEY "${error.key}"`);
|
|
1287
|
+
return error.key;
|
|
1288
|
+
}
|
|
1289
|
+
return errorMessageGenerator(error.value);
|
|
1290
|
+
}
|
|
1291
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ValidationMessage, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1292
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: ValidationMessage, isStandalone: true, selector: "iss-validation-message-viewer", inputs: { errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: true, transformFunction: null }, showMessage: { classPropertyName: "showMessage", publicName: "showMessage", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
|
|
1293
|
+
@if (errors() !== null && showMessage()) {
|
|
1294
|
+
<div class="messages">
|
|
1295
|
+
@for (error of errors() ?? {} | keyvalue; track error) {
|
|
1296
|
+
<p>
|
|
1297
|
+
<mat-error>{{ getValidationMessage(error) | translate }}</mat-error>
|
|
1298
|
+
</p>
|
|
1299
|
+
}
|
|
1300
|
+
</div>
|
|
1301
|
+
}
|
|
1302
|
+
`, isInline: true, styles: [":host{display:block;position:relative}.messages{position:absolute;inset-inline:0;top:0;z-index:1}p{margin:0}\n"], dependencies: [{ kind: "directive", type: MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "pipe", type: KeyValuePipe, name: "keyvalue" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
|
|
1303
|
+
}
|
|
1304
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ValidationMessage, decorators: [{
|
|
1305
|
+
type: Component,
|
|
1306
|
+
args: [{ selector: "iss-validation-message-viewer", imports: [
|
|
1307
|
+
KeyValuePipe,
|
|
1308
|
+
TranslatePipe,
|
|
1309
|
+
MatError
|
|
1310
|
+
], template: `
|
|
1311
|
+
@if (errors() !== null && showMessage()) {
|
|
1312
|
+
<div class="messages">
|
|
1313
|
+
@for (error of errors() ?? {} | keyvalue; track error) {
|
|
1314
|
+
<p>
|
|
1315
|
+
<mat-error>{{ getValidationMessage(error) | translate }}</mat-error>
|
|
1316
|
+
</p>
|
|
1317
|
+
}
|
|
1318
|
+
</div>
|
|
1319
|
+
}
|
|
1320
|
+
`, styles: [":host{display:block;position:relative}.messages{position:absolute;inset-inline:0;top:0;z-index:1}p{margin:0}\n"] }]
|
|
1321
|
+
}], propDecorators: { errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: true }] }], showMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "showMessage", required: true }] }] } });
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* `<iss-field-viewer>` — read-only presentation of a single field as a
|
|
1325
|
+
* label / value row.
|
|
1326
|
+
*
|
|
1327
|
+
* Project the caption into the `label` slot and the rendered value into any
|
|
1328
|
+
* element carrying a `value` attribute:
|
|
1329
|
+
*
|
|
1330
|
+
* ```html
|
|
1331
|
+
* <iss-field-viewer>
|
|
1332
|
+
* <label>Request Type</label>
|
|
1333
|
+
* <span>value</span>
|
|
1334
|
+
* </iss-field-viewer>
|
|
1335
|
+
* ```
|
|
1336
|
+
*
|
|
1337
|
+
* The label sits in a fixed-width column, so every value in a stack of viewers
|
|
1338
|
+
* starts on the same vertical line. Override that column with the
|
|
1339
|
+
* `--field-viewer-label-width` custom property on any ancestor.
|
|
1340
|
+
* When no label is projected, the value spans the full width of the row.
|
|
1341
|
+
*/
|
|
1342
|
+
class FieldViewer {
|
|
1343
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FieldViewer, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1344
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.9", type: FieldViewer, isStandalone: true, selector: "iss-field-viewer", ngImport: i0, template: `
|
|
1345
|
+
<div class="field-viewer w-100">
|
|
1346
|
+
<span class="field-viewer__label"><ng-content select="label"></ng-content></span>
|
|
1347
|
+
<span class="field-viewer__value"><ng-content></ng-content></span>
|
|
1348
|
+
</div>
|
|
1349
|
+
`, isInline: true, styles: [":host{--field-viewer-label-width: 14rem;--field-viewer-gap: 1.5rem;--field-viewer-padding: .75rem 1rem;--field-viewer-label-fc: var(--dark-gray);--field-viewer-value-fc: var(--black);display:block;width:100%}.field-viewer{display:grid;grid-template-columns:var(--field-viewer-label-width) minmax(0,1fr);align-items:baseline;column-gap:var(--field-viewer-gap);padding:var(--field-viewer-padding);border-radius:var(--box-radius);font-size:.875rem;line-height:var(--default-line-height)}.field-viewer__label{color:var(--field-viewer-label-fc);font-weight:var(--font-regular);overflow-wrap:anywhere}.field-viewer__value{min-width:0;color:var(--field-viewer-value-fc);font-weight:var(--font-bold);overflow-wrap:anywhere}.field-viewer__label ::ng-deep label{display:block;margin:0;color:inherit;font:inherit}.field-viewer:not(:has(.field-viewer__label>*)){grid-template-columns:minmax(0,1fr)}.field-viewer:not(:has(.field-viewer__label>*)) .field-viewer__label{display:none}@media(max-width:575.98px){.field-viewer{grid-template-columns:minmax(0,1fr);row-gap:.25rem}}\n"] });
|
|
1350
|
+
}
|
|
1351
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FieldViewer, decorators: [{
|
|
1352
|
+
type: Component,
|
|
1353
|
+
args: [{ selector: "iss-field-viewer", imports: [], template: `
|
|
1354
|
+
<div class="field-viewer w-100">
|
|
1355
|
+
<span class="field-viewer__label"><ng-content select="label"></ng-content></span>
|
|
1356
|
+
<span class="field-viewer__value"><ng-content></ng-content></span>
|
|
1357
|
+
</div>
|
|
1358
|
+
`, styles: [":host{--field-viewer-label-width: 14rem;--field-viewer-gap: 1.5rem;--field-viewer-padding: .75rem 1rem;--field-viewer-label-fc: var(--dark-gray);--field-viewer-value-fc: var(--black);display:block;width:100%}.field-viewer{display:grid;grid-template-columns:var(--field-viewer-label-width) minmax(0,1fr);align-items:baseline;column-gap:var(--field-viewer-gap);padding:var(--field-viewer-padding);border-radius:var(--box-radius);font-size:.875rem;line-height:var(--default-line-height)}.field-viewer__label{color:var(--field-viewer-label-fc);font-weight:var(--font-regular);overflow-wrap:anywhere}.field-viewer__value{min-width:0;color:var(--field-viewer-value-fc);font-weight:var(--font-bold);overflow-wrap:anywhere}.field-viewer__label ::ng-deep label{display:block;margin:0;color:inherit;font:inherit}.field-viewer:not(:has(.field-viewer__label>*)){grid-template-columns:minmax(0,1fr)}.field-viewer:not(:has(.field-viewer__label>*)) .field-viewer__label{display:none}@media(max-width:575.98px){.field-viewer{grid-template-columns:minmax(0,1fr);row-gap:.25rem}}\n"] }]
|
|
1359
|
+
}] });
|
|
1360
|
+
|
|
1361
|
+
class ArabicNumberPipe {
|
|
1362
|
+
transform(n) {
|
|
1363
|
+
if (n === null || n === undefined) {
|
|
1364
|
+
return "";
|
|
1365
|
+
}
|
|
1366
|
+
if (Number.isNaN(+n)) {
|
|
1367
|
+
return n;
|
|
1368
|
+
}
|
|
1369
|
+
return new Intl.NumberFormat("ar-SA", {}).format(+n);
|
|
1370
|
+
}
|
|
1371
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ArabicNumberPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
1372
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.9", ngImport: i0, type: ArabicNumberPipe, isStandalone: true, name: "arabicNumber" });
|
|
1373
|
+
}
|
|
1374
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ArabicNumberPipe, decorators: [{
|
|
1375
|
+
type: Pipe,
|
|
1376
|
+
args: [{ name: "arabicNumber" }]
|
|
1377
|
+
}] });
|
|
1378
|
+
|
|
1379
|
+
class PlateLetterPipe {
|
|
1380
|
+
transform(value) {
|
|
1381
|
+
const letter = PLATE_LETTERS.find((l) => l.value === value);
|
|
1382
|
+
if (letter) {
|
|
1383
|
+
return letter.label.ar;
|
|
1384
|
+
}
|
|
1385
|
+
return value;
|
|
1386
|
+
}
|
|
1387
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: PlateLetterPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
1388
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.9", ngImport: i0, type: PlateLetterPipe, isStandalone: true, name: "plateLetter" });
|
|
1389
|
+
}
|
|
1390
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: PlateLetterPipe, decorators: [{
|
|
1391
|
+
type: Pipe,
|
|
1392
|
+
args: [{ name: "plateLetter" }]
|
|
1393
|
+
}] });
|
|
1394
|
+
|
|
1395
|
+
class Employee {
|
|
1396
|
+
loggedInUserProfile;
|
|
1397
|
+
USER_SEARCH_URL = inject(USER_SEARCH_API);
|
|
1398
|
+
userImageHost = "https://igateapp.stc.com.sa";
|
|
1399
|
+
#http = inject(HttpClient);
|
|
1400
|
+
getProfile(email) {
|
|
1401
|
+
const url = this.replacePlaceholders(this.USER_SEARCH_URL, { email: email });
|
|
1402
|
+
return this.#http.get(url).pipe(map((dto) => {
|
|
1403
|
+
dto.data.profileImageUrl = this.getProfileImageUrl(dto.data);
|
|
1404
|
+
return dto.data;
|
|
1405
|
+
}), tap((user) => {
|
|
1406
|
+
localStorage.setItem("user", JSON.stringify(user));
|
|
1407
|
+
this.loggedInUserProfile = user;
|
|
1408
|
+
}));
|
|
1409
|
+
}
|
|
1410
|
+
search(username, endpoint) {
|
|
1411
|
+
let url = this.replacePlaceholders(this.USER_SEARCH_URL, { username });
|
|
1412
|
+
if (endpoint) {
|
|
1413
|
+
if (!endpoint.includes("{username}")) {
|
|
1414
|
+
throw "endpoint should have this string {username} as a placeholder to be able to replace it";
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
if (endpoint) {
|
|
1418
|
+
url = this.replacePlaceholders(endpoint, { username });
|
|
1419
|
+
}
|
|
1420
|
+
return this.#http.get(url).pipe(map((employees) => {
|
|
1421
|
+
employees.data = employees.data.map((emp) => {
|
|
1422
|
+
emp.image = this.getProfileImageUrl(emp);
|
|
1423
|
+
return emp;
|
|
1424
|
+
});
|
|
1425
|
+
return employees.data;
|
|
1426
|
+
}));
|
|
1427
|
+
}
|
|
1428
|
+
getProfileImageUrl(user) {
|
|
1429
|
+
if (user.image && this.#checkIfImageHasValue(user.image)) {
|
|
1430
|
+
return this.userImageHost + user.image;
|
|
1431
|
+
}
|
|
1432
|
+
return "";
|
|
1433
|
+
}
|
|
1434
|
+
replacePlaceholders(str, values) {
|
|
1435
|
+
/*TODO check if placeholders passed in values object*/
|
|
1436
|
+
if (!str) {
|
|
1437
|
+
return "";
|
|
1438
|
+
}
|
|
1439
|
+
return str.replace(PLACEHOLDER_REGEX, (match) => {
|
|
1440
|
+
return values[match.replace("{", "").replace("}", "")] ?? match;
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
#checkIfImageHasValue(imageUri) {
|
|
1444
|
+
const splitResult = imageUri.split("/");
|
|
1445
|
+
return !!(splitResult[splitResult.length - 1] && splitResult[splitResult.length - 1] !== ".jpg");
|
|
1446
|
+
}
|
|
1447
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: Employee, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1448
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: Employee, providedIn: "root" });
|
|
1449
|
+
}
|
|
1450
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: Employee, decorators: [{
|
|
1451
|
+
type: Injectable,
|
|
1452
|
+
args: [{ providedIn: "root" }]
|
|
1453
|
+
}] });
|
|
1454
|
+
|
|
1455
|
+
function replacePlaceholders(str, values) {
|
|
1456
|
+
/*TODO check if placeholders passed in values object*/
|
|
1457
|
+
if (!str) {
|
|
1458
|
+
return "";
|
|
1459
|
+
}
|
|
1460
|
+
return str.replace(PLACEHOLDER_REGEX, (match) => {
|
|
1461
|
+
const key = match.replace("{", "").replace("}", "");
|
|
1462
|
+
return values[key] ?? match;
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
class FileHandler {
|
|
1467
|
+
ATTACHMENT_UPLOAD_URL = inject(ATTACHMENT_UPLOAD_URL);
|
|
1468
|
+
ATTACHMENT_DOWNLOAD_URL = inject(ATTACHMENT_DOWNLOAD_URL);
|
|
1469
|
+
attachmentAdapter = inject(ATTACHMENT_UPLOAD_RESPONSE_MAPPER);
|
|
1470
|
+
#http = inject(HttpClient);
|
|
1471
|
+
#units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
|
1472
|
+
upload(data) {
|
|
1473
|
+
const url = this.ATTACHMENT_UPLOAD_URL;
|
|
1474
|
+
// no Content-Type header: the browser adds it with the FormData boundary
|
|
1475
|
+
return this.#http.post(url, data).pipe(map((res) => this.attachmentAdapter(res)));
|
|
1476
|
+
}
|
|
1477
|
+
formDataFactory(files, multiple) {
|
|
1478
|
+
if (files.length === 0) {
|
|
1479
|
+
return null;
|
|
1480
|
+
}
|
|
1481
|
+
const fd = new FormData();
|
|
1482
|
+
let formDataKey = "file";
|
|
1483
|
+
if (multiple) {
|
|
1484
|
+
formDataKey = "files";
|
|
1485
|
+
}
|
|
1486
|
+
for (let i = 0; i < files.length; i++) {
|
|
1487
|
+
// @ts-ignore
|
|
1488
|
+
fd.append(formDataKey, files[i]);
|
|
1489
|
+
}
|
|
1490
|
+
return fd;
|
|
1491
|
+
}
|
|
1492
|
+
formDataHasValue(formData) {
|
|
1493
|
+
if (formData) {
|
|
1494
|
+
for (const key of formData.keys()) {
|
|
1495
|
+
if (formData.get(key)) {
|
|
1496
|
+
return true;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
return false;
|
|
1501
|
+
}
|
|
1502
|
+
getFileFromFormData(formData, name) {
|
|
1503
|
+
if (!formData) {
|
|
1504
|
+
return [];
|
|
1505
|
+
}
|
|
1506
|
+
const fileFD = formData.getAll("file");
|
|
1507
|
+
const filesFD = formData.getAll("files");
|
|
1508
|
+
if (filesFD.length) {
|
|
1509
|
+
return filesFD.filter((f) => f.name === name);
|
|
1510
|
+
}
|
|
1511
|
+
if (fileFD.length) {
|
|
1512
|
+
return fileFD.filter((f) => f.name === name);
|
|
1513
|
+
}
|
|
1514
|
+
return [];
|
|
1515
|
+
}
|
|
1516
|
+
formatFileSize(x) {
|
|
1517
|
+
let l = 0;
|
|
1518
|
+
let n = parseInt(x.toString(), 10) || 0;
|
|
1519
|
+
while (n >= 1024 && ++l) {
|
|
1520
|
+
n /= 1024;
|
|
1521
|
+
}
|
|
1522
|
+
return Math.floor(n) + " " + this.#units[l];
|
|
1523
|
+
}
|
|
1524
|
+
fetchFileInfo(id) {
|
|
1525
|
+
const url = replacePlaceholders(this.ATTACHMENT_DOWNLOAD_URL, { fileId: id });
|
|
1526
|
+
const httpRequestOptions = { observe: "response", responseType: "blob" };
|
|
1527
|
+
return this.#http.get(url, httpRequestOptions).pipe(map((res) => this.#processFileBinaryStream(res, id)));
|
|
1528
|
+
}
|
|
1529
|
+
fetchFiles(ids) {
|
|
1530
|
+
if (ids) {
|
|
1531
|
+
return forkJoin(ids.split(",").map((id) => {
|
|
1532
|
+
return this.fetchFileInfo(id);
|
|
1533
|
+
}));
|
|
1534
|
+
}
|
|
1535
|
+
return EMPTY;
|
|
1536
|
+
}
|
|
1537
|
+
#processFileBinaryStream(res, fileId) {
|
|
1538
|
+
const fileNameHeader = res.headers.get("content-disposition");
|
|
1539
|
+
const typeHeader = res.headers.get("content-type");
|
|
1540
|
+
let fileName = "";
|
|
1541
|
+
if (fileNameHeader) {
|
|
1542
|
+
fileName = decodeURIComponent(fileNameHeader.split(";")[1].split("=")[1].replaceAll("\"", ""));
|
|
1543
|
+
}
|
|
1544
|
+
const file = new File([res.body], fileName, { type: typeHeader });
|
|
1545
|
+
return {
|
|
1546
|
+
id: fileId,
|
|
1547
|
+
name: file.name,
|
|
1548
|
+
type: file.type,
|
|
1549
|
+
size: this.formatFileSize(file.size),
|
|
1550
|
+
file: file,
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FileHandler, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1554
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FileHandler, providedIn: "root" });
|
|
1555
|
+
}
|
|
1556
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FileHandler, decorators: [{
|
|
1557
|
+
type: Injectable,
|
|
1558
|
+
args: [{ providedIn: "root" }]
|
|
1559
|
+
}] });
|
|
1560
|
+
|
|
1561
|
+
function arrayFrom(length, valueFunction) {
|
|
1562
|
+
const valuesArray = Array(length);
|
|
1563
|
+
for (let i = 0; i < length; i++) {
|
|
1564
|
+
valuesArray[i] = valueFunction(i);
|
|
1565
|
+
}
|
|
1566
|
+
return valuesArray;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
function getPropertyByPath(obj, path) {
|
|
1570
|
+
if (obj && path) {
|
|
1571
|
+
let currentProp;
|
|
1572
|
+
const props = path.split(".");
|
|
1573
|
+
for (let i = 0; i < props.length; i++) {
|
|
1574
|
+
const propKey = props[i];
|
|
1575
|
+
if (!currentProp) {
|
|
1576
|
+
const target = obj[propKey];
|
|
1577
|
+
if (typeof target === "function") {
|
|
1578
|
+
currentProp = target();
|
|
1579
|
+
}
|
|
1580
|
+
else {
|
|
1581
|
+
currentProp = target;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
else {
|
|
1585
|
+
const target = currentProp[propKey];
|
|
1586
|
+
if (typeof target === "function") {
|
|
1587
|
+
currentProp = target();
|
|
1588
|
+
}
|
|
1589
|
+
else {
|
|
1590
|
+
currentProp = target;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
if (currentProp === undefined) {
|
|
1594
|
+
break;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
return currentProp;
|
|
1598
|
+
}
|
|
1599
|
+
return undefined;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
function formatFileSize(x) {
|
|
1603
|
+
const units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
|
1604
|
+
let l = 0;
|
|
1605
|
+
let n = parseInt(x.toString(), 10) || 0;
|
|
1606
|
+
while (n >= 1024 && ++l) {
|
|
1607
|
+
n /= 1024;
|
|
1608
|
+
}
|
|
1609
|
+
return `${Math.floor(n)}${units[l]}`;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
class IssValidators {
|
|
1613
|
+
static required() {
|
|
1614
|
+
return Validators.required;
|
|
1615
|
+
}
|
|
1616
|
+
static max(maxValue) {
|
|
1617
|
+
return Validators.max(maxValue);
|
|
1618
|
+
}
|
|
1619
|
+
static min(minValue) {
|
|
1620
|
+
return Validators.min(minValue);
|
|
1621
|
+
}
|
|
1622
|
+
static requiredTrue() {
|
|
1623
|
+
return Validators.requiredTrue;
|
|
1624
|
+
}
|
|
1625
|
+
static email() {
|
|
1626
|
+
return Validators.email;
|
|
1627
|
+
}
|
|
1628
|
+
static minLength(minLength) {
|
|
1629
|
+
return Validators.minLength(minLength);
|
|
1630
|
+
}
|
|
1631
|
+
static maxLength(maxLength) {
|
|
1632
|
+
return Validators.maxLength(maxLength);
|
|
1633
|
+
}
|
|
1634
|
+
static pattern(pattern) {
|
|
1635
|
+
return Validators.pattern(pattern);
|
|
1636
|
+
}
|
|
1637
|
+
static null() {
|
|
1638
|
+
return Validators.nullValidator;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
var core_validator = /*#__PURE__*/Object.freeze({
|
|
1643
|
+
__proto__: null,
|
|
1644
|
+
IssValidators: IssValidators
|
|
1645
|
+
});
|
|
1646
|
+
|
|
1647
|
+
class FileValidators {
|
|
1648
|
+
static size(limitSize) {
|
|
1649
|
+
return (control) => {
|
|
1650
|
+
const formData = control.value;
|
|
1651
|
+
if (!formData || !(formData instanceof FormData)) {
|
|
1652
|
+
return null; // No validation errors if there's no form data or the value is not FormData
|
|
1653
|
+
}
|
|
1654
|
+
const filesValidity = [];
|
|
1655
|
+
const inValidFileName = [];
|
|
1656
|
+
formData.forEach((value) => {
|
|
1657
|
+
if (value instanceof File) {
|
|
1658
|
+
if (value.size > limitSize) {
|
|
1659
|
+
inValidFileName.push(value.name);
|
|
1660
|
+
filesValidity.push(false);
|
|
1661
|
+
}
|
|
1662
|
+
else {
|
|
1663
|
+
filesValidity.push(true);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1667
|
+
return filesValidity.every((v) => v) ? null : {
|
|
1668
|
+
fileSize: {
|
|
1669
|
+
limitSize: formatFileSize(limitSize),
|
|
1670
|
+
inValidFileName: inValidFileName.join()
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
static type(validFileExtensions) {
|
|
1676
|
+
return (control) => {
|
|
1677
|
+
const formData = control.value;
|
|
1678
|
+
if (!formData || !(formData instanceof FormData) || validFileExtensions.length === 0) {
|
|
1679
|
+
return null; // No validation errors if there's no form data or the value is not FormData
|
|
1680
|
+
}
|
|
1681
|
+
const filesValidity = [];
|
|
1682
|
+
const inValidFileName = [];
|
|
1683
|
+
formData.forEach((value) => {
|
|
1684
|
+
if (value instanceof File) {
|
|
1685
|
+
const uploadedFileExt = value.name.split(".").pop()?.toLowerCase();
|
|
1686
|
+
const isValidExtension = validFileExtensions.some((ext) => ext.toLowerCase() === uploadedFileExt);
|
|
1687
|
+
if (!isValidExtension) {
|
|
1688
|
+
inValidFileName.push(value.name);
|
|
1689
|
+
filesValidity.push(false);
|
|
1690
|
+
}
|
|
1691
|
+
else {
|
|
1692
|
+
filesValidity.push(true);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
});
|
|
1696
|
+
return filesValidity.every((v) => v) ? null : {
|
|
1697
|
+
fileType: {
|
|
1698
|
+
validFileExtensions: validFileExtensions.join(", "),
|
|
1699
|
+
inValidFileName: inValidFileName.join(", ")
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
static maxCount(maxCount) {
|
|
1705
|
+
return (control) => {
|
|
1706
|
+
const formData = control.value;
|
|
1707
|
+
if (!formData || !(formData instanceof FormData)) {
|
|
1708
|
+
return null; // No validation errors if there's no form data or the value is not FormData
|
|
1709
|
+
}
|
|
1710
|
+
let fileCount = 0;
|
|
1711
|
+
formData.forEach((value) => {
|
|
1712
|
+
if (value instanceof File) {
|
|
1713
|
+
fileCount++;
|
|
1714
|
+
}
|
|
1715
|
+
});
|
|
1716
|
+
return fileCount <= maxCount ? null : { maxFileCount: { maxCount, fileCount } };
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
static minCount(minCount) {
|
|
1720
|
+
return (control) => {
|
|
1721
|
+
const formData = control.value;
|
|
1722
|
+
if (!formData || !(formData instanceof FormData)) {
|
|
1723
|
+
return null; // No validation errors if there's no form data or the value is not FormData
|
|
1724
|
+
}
|
|
1725
|
+
let fileCount = 0;
|
|
1726
|
+
formData.forEach((value) => {
|
|
1727
|
+
if (value instanceof File) {
|
|
1728
|
+
fileCount++;
|
|
1729
|
+
}
|
|
1730
|
+
});
|
|
1731
|
+
return fileCount >= minCount ? null : { minFileCount: { minCount, fileCount } };
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
var file_validator = /*#__PURE__*/Object.freeze({
|
|
1737
|
+
__proto__: null,
|
|
1738
|
+
FileValidators: FileValidators
|
|
1739
|
+
});
|
|
1740
|
+
|
|
1741
|
+
class TimeValidator {
|
|
1742
|
+
// The rule's value is the name of the field this one must not run past, so it arrives here
|
|
1743
|
+
// rather than being read back off the field.
|
|
1744
|
+
static beforeDate(otherFieldName) {
|
|
1745
|
+
const controlContainer = inject(ControlContainer);
|
|
1746
|
+
const parentForm = controlContainer.control;
|
|
1747
|
+
return (control) => {
|
|
1748
|
+
const anotherControl = parentForm.get(otherFieldName);
|
|
1749
|
+
if (anotherControl) {
|
|
1750
|
+
const anotherControlValue = anotherControl.getRawValue();
|
|
1751
|
+
const controlValue = control.getRawValue();
|
|
1752
|
+
if (new Date(anotherControlValue) < new Date(controlValue)) {
|
|
1753
|
+
return {
|
|
1754
|
+
relativeTimeValidation_before: format(anotherControlValue.replace("Z", ""), DATETIME_FORMAT)
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
return null;
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
static afterDate(otherFieldName) {
|
|
1762
|
+
return (control) => {
|
|
1763
|
+
return null;
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
var time_validator = /*#__PURE__*/Object.freeze({
|
|
1769
|
+
__proto__: null,
|
|
1770
|
+
TimeValidator: TimeValidator
|
|
1771
|
+
});
|
|
1772
|
+
|
|
1773
|
+
// The form engine: the outlet, the field/viewer indexes it resolves through, and everything
|
|
1774
|
+
// a host application needs to drive a form.
|
|
1775
|
+
//
|
|
1776
|
+
// Controls are NOT exported from here. Each one is its own entry point — `iss-ctrl/input`,
|
|
1777
|
+
// `iss-ctrl/select`, `iss-ctrl/mobile`, … — because re-exporting a control statically makes
|
|
1778
|
+
// the bundler inline it, and with it every control, back into whatever loads this file. The
|
|
1779
|
+
// fields below reach their control through that entry point, so an app downloads only the
|
|
1780
|
+
// controls it renders. `iss-ctrl/base` and `iss-ctrl/tokens` are re-exported for convenience;
|
|
1781
|
+
// an app configuring the tokens in its root providers should still import them directly from
|
|
1782
|
+
// `iss-ctrl/tokens` so the engine stays out of its initial bundle.
|
|
1783
|
+
|
|
1784
|
+
/**
|
|
1785
|
+
* Generated bundle index. Do not edit.
|
|
1786
|
+
*/
|
|
1787
|
+
|
|
1788
|
+
export { ArabicNumberPipe as A, COMPONENTS as C, Employee as E, FieldBase as F, IssField as I, Loader as L, Outlet as O, PLACEHOLDER_REGEX as P, SERVICES as S, TranslatePipe as T, VIEW_PROVIDER as V, ValidationMessage as a, FieldViewer as b, FileHandler as c, FormEngine as d, FieldDataInjector as e, VALIDATION_MESSAGES as f, getPropertyByPath as g, VALIDATORS as h, isActiveValidationRule as i, FIELD_DATA as j, PlateLetterPipe as k, arrayFrom as l, formatFileSize as m, IssValidators as n, FileValidators as o, TimeValidator as p, replacePlaceholders as r };
|
|
1789
|
+
//# sourceMappingURL=iss-ctrl-iss-ctrl-DvnkwSMG.mjs.map
|