ngx-dynamic-entity 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/esm2022/lib/field-types/date-time-field.component.mjs +138 -0
- package/esm2022/lib/field-types/time-field.component.mjs +96 -0
- package/esm2022/lib/form/dynamic-field/dynamic-field.component.mjs +4 -3
- package/esm2022/lib/form/dynamic-form.component.mjs +13 -4
- package/esm2022/lib/form/dynamic-record-form.component.mjs +54 -17
- package/esm2022/lib/providers/provide-field-types.mjs +5 -2
- package/esm2022/public-api.mjs +4 -2
- package/fesm2022/ngx-dynamic-entity.mjs +295 -22
- package/fesm2022/ngx-dynamic-entity.mjs.map +1 -1
- package/lib/field-types/date-time-field.component.d.ts +30 -0
- package/lib/field-types/time-field.component.d.ts +27 -0
- package/lib/form/dynamic-form.component.d.ts +2 -0
- package/lib/form/dynamic-record-form.component.d.ts +19 -0
- package/package.json +2 -2
- package/public-api.d.ts +2 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, makeEnvironmentProviders, inject, Injectable, signal, Input, ChangeDetectionStrategy, Component, DestroyRef, Injector, isDevMode, ViewContainerRef, ViewChild, forwardRef, ChangeDetectorRef, Optional, Inject, EventEmitter, computed, HostListener, ViewChildren, Output } from '@angular/core';
|
|
2
|
+
import { InjectionToken, makeEnvironmentProviders, inject, Injectable, signal, Input, ChangeDetectionStrategy, Component, DestroyRef, Injector, isDevMode, ViewContainerRef, ViewChild, forwardRef, ChangeDetectorRef, Optional, Inject, EventEmitter, computed, HostListener, ViewChildren, Output, ElementRef, afterNextRender } from '@angular/core';
|
|
3
3
|
import * as i1 from '@angular/forms';
|
|
4
4
|
import { ReactiveFormsModule, Validators, FormBuilder, FormArray, FormGroup } from '@angular/forms';
|
|
5
|
-
import { resolveLabel, normalizeLookupValues, lookupValuesToOptions, valuesMatch, getOptionStoredValue, resolveOptionLabel, resolveOptionValue, buildReferenceCacheKey, normalizeReferenceOptions, entityKeyFromCacheKey, applyCascadeFilter, resolveEffectiveMask, shouldMaskField, findTab, fileRefName, evaluateFormRules, filterRulesForTab, evaluateFieldVisibility, normalizeConfigOptions, migrateRecord, getTabData, getValueByPath, setTabData, setValueByPath, normalizeArrayStructures, getTabPath, applyAutoPatch, applyPatchOnTrue
|
|
5
|
+
import { resolveLabel, formatDisplayValue, normalizeLookupValues, lookupValuesToOptions, valuesMatch, getOptionStoredValue, resolveOptionLabel, resolveOptionValue, buildReferenceCacheKey, normalizeReferenceOptions, entityKeyFromCacheKey, applyCascadeFilter, resolveEffectiveMask, shouldMaskField, findTab, fileRefName, evaluateFormRules, filterRulesForTab, evaluateFieldVisibility, normalizeConfigOptions, migrateRecord, getTabData, getValueByPath, setTabData, setValueByPath, normalizeArrayStructures, getTabPath, applyAutoPatch, applyPatchOnTrue } from '@dynamic-entity/core';
|
|
6
6
|
export { formatDisplayValue, normalizeConfig, normalizeOption, resolveLabel, resolveOptionLabel, resolveOptionValue, valuesMatch } from '@dynamic-entity/core';
|
|
7
7
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
8
8
|
import { Subject, isObservable, firstValueFrom } from 'rxjs';
|
|
@@ -1090,6 +1090,231 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1090
1090
|
type: Input
|
|
1091
1091
|
}] } });
|
|
1092
1092
|
|
|
1093
|
+
/**
|
|
1094
|
+
* Reads a stored value into a `Date`.
|
|
1095
|
+
*
|
|
1096
|
+
* A bare calendar date is pinned to *local* midnight. `new Date('2020-01-01')` reads a
|
|
1097
|
+
* date-only string as UTC midnight, which renders as the previous day anywhere west of
|
|
1098
|
+
* Greenwich — and every legacy `datetime` value has exactly that shape, because the type
|
|
1099
|
+
* used to render through `DateFieldComponent` and its `type="date"` input.
|
|
1100
|
+
*/
|
|
1101
|
+
function parseStored(value) {
|
|
1102
|
+
if (value === null || value === undefined || value === '')
|
|
1103
|
+
return null;
|
|
1104
|
+
if (value instanceof Date)
|
|
1105
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
1106
|
+
const raw = String(value);
|
|
1107
|
+
const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw);
|
|
1108
|
+
if (dateOnly)
|
|
1109
|
+
return new Date(+dateOnly[1], +dateOnly[2] - 1, +dateOnly[3]);
|
|
1110
|
+
const parsed = new Date(raw);
|
|
1111
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
1112
|
+
}
|
|
1113
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
1114
|
+
/**
|
|
1115
|
+
* Date & Time field: a single `datetime-local` input.
|
|
1116
|
+
*
|
|
1117
|
+
* The type is stored as ISO 8601 UTC, per the same convention `date` follows, while the
|
|
1118
|
+
* input works in the viewer's local zone — `datetime-local` has no offset, so the two
|
|
1119
|
+
* conversions here are what keep a value stable across a round trip.
|
|
1120
|
+
*/
|
|
1121
|
+
class DateTimeFieldComponent {
|
|
1122
|
+
field;
|
|
1123
|
+
control;
|
|
1124
|
+
language = 'en';
|
|
1125
|
+
readonly = false;
|
|
1126
|
+
masked = false;
|
|
1127
|
+
get label() {
|
|
1128
|
+
return resolveLabel(this.field?.label, this.language);
|
|
1129
|
+
}
|
|
1130
|
+
/** Stored ISO 8601 → the `YYYY-MM-DDTHH:mm` local form the input requires. */
|
|
1131
|
+
get inputValue() {
|
|
1132
|
+
const d = parseStored(this.control?.value);
|
|
1133
|
+
if (!d)
|
|
1134
|
+
return '';
|
|
1135
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
1136
|
+
}
|
|
1137
|
+
/** The input's local `YYYY-MM-DDTHH:mm` → stored ISO 8601 UTC. */
|
|
1138
|
+
onInput(value) {
|
|
1139
|
+
if (!value) {
|
|
1140
|
+
this.control.setValue(null);
|
|
1141
|
+
this.control.markAsTouched();
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
const d = new Date(value);
|
|
1145
|
+
// An unparseable value is stored verbatim rather than silently discarded, so a validator
|
|
1146
|
+
// sees what the user actually entered.
|
|
1147
|
+
this.control.setValue(Number.isNaN(d.getTime()) ? value : d.toISOString());
|
|
1148
|
+
this.control.markAsTouched();
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Displays date *and* time. `DateFieldComponent` used `toLocaleDateString()` here, so a
|
|
1152
|
+
* datetime read back through the field showed no time while the same value in the record
|
|
1153
|
+
* summary — which goes through core's `formatDisplayValue` — showed it.
|
|
1154
|
+
*/
|
|
1155
|
+
formatDateTime(value) {
|
|
1156
|
+
const d = parseStored(value);
|
|
1157
|
+
return d ? d.toLocaleString() : '—';
|
|
1158
|
+
}
|
|
1159
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DateTimeFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1160
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DateTimeFieldComponent, isStandalone: true, selector: "ngx-date-time-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
1161
|
+
<div class="ngx-field ngx-field--datetime"
|
|
1162
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
1163
|
+
<label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
|
|
1164
|
+
@if (masked) {
|
|
1165
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1166
|
+
} @else if (readonly) {
|
|
1167
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatDateTime(control.value) }}</span>
|
|
1168
|
+
} @else {
|
|
1169
|
+
<input
|
|
1170
|
+
[id]="field.id"
|
|
1171
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
1172
|
+
type="datetime-local"
|
|
1173
|
+
[value]="inputValue"
|
|
1174
|
+
(change)="onInput($any($event.target).value)"
|
|
1175
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
1176
|
+
/>
|
|
1177
|
+
@if (control.invalid && control.touched) {
|
|
1178
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
</div>
|
|
1182
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1183
|
+
}
|
|
1184
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DateTimeFieldComponent, decorators: [{
|
|
1185
|
+
type: Component,
|
|
1186
|
+
args: [{
|
|
1187
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1188
|
+
selector: 'ngx-date-time-field',
|
|
1189
|
+
standalone: true,
|
|
1190
|
+
imports: [ReactiveFormsModule],
|
|
1191
|
+
template: `
|
|
1192
|
+
<div class="ngx-field ngx-field--datetime"
|
|
1193
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
1194
|
+
<label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
|
|
1195
|
+
@if (masked) {
|
|
1196
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1197
|
+
} @else if (readonly) {
|
|
1198
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatDateTime(control.value) }}</span>
|
|
1199
|
+
} @else {
|
|
1200
|
+
<input
|
|
1201
|
+
[id]="field.id"
|
|
1202
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
1203
|
+
type="datetime-local"
|
|
1204
|
+
[value]="inputValue"
|
|
1205
|
+
(change)="onInput($any($event.target).value)"
|
|
1206
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
1207
|
+
/>
|
|
1208
|
+
@if (control.invalid && control.touched) {
|
|
1209
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
</div>
|
|
1213
|
+
`,
|
|
1214
|
+
}]
|
|
1215
|
+
}], propDecorators: { field: [{
|
|
1216
|
+
type: Input
|
|
1217
|
+
}], control: [{
|
|
1218
|
+
type: Input
|
|
1219
|
+
}], language: [{
|
|
1220
|
+
type: Input
|
|
1221
|
+
}], readonly: [{
|
|
1222
|
+
type: Input
|
|
1223
|
+
}], masked: [{
|
|
1224
|
+
type: Input
|
|
1225
|
+
}] } });
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* Time field: a bare time of day, with no date and no zone attached.
|
|
1229
|
+
*
|
|
1230
|
+
* Stored as `HH:mm` — exactly the value `<input type="time">` reads and writes, so the
|
|
1231
|
+
* control binds straight through with no conversion. That is the whole reason this is a
|
|
1232
|
+
* separate type from `datetime`: a recurring 09:00 opening time is not a moment in time,
|
|
1233
|
+
* and converting it to UTC would move it whenever the offset changed.
|
|
1234
|
+
*/
|
|
1235
|
+
class TimeFieldComponent {
|
|
1236
|
+
field;
|
|
1237
|
+
control;
|
|
1238
|
+
language = 'en';
|
|
1239
|
+
readonly = false;
|
|
1240
|
+
masked = false;
|
|
1241
|
+
get label() {
|
|
1242
|
+
return resolveLabel(this.field?.label, this.language);
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Defers to core rather than parsing `HH:mm` locally. The other date-ish components each
|
|
1246
|
+
* grew their own formatter, and `datetime` ended up showing one thing in the field and
|
|
1247
|
+
* another in the record summary because the two drifted apart.
|
|
1248
|
+
*/
|
|
1249
|
+
formatTime(value) {
|
|
1250
|
+
return formatDisplayValue('time', undefined, value, this.language);
|
|
1251
|
+
}
|
|
1252
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TimeFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1253
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: TimeFieldComponent, isStandalone: true, selector: "ngx-time-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
1254
|
+
<div class="ngx-field ngx-field--time"
|
|
1255
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
1256
|
+
<label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
|
|
1257
|
+
@if (masked) {
|
|
1258
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1259
|
+
} @else if (readonly) {
|
|
1260
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatTime(control.value) }}</span>
|
|
1261
|
+
} @else {
|
|
1262
|
+
<input
|
|
1263
|
+
[id]="field.id"
|
|
1264
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
1265
|
+
type="time"
|
|
1266
|
+
[formControl]="$any(control)"
|
|
1267
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
1268
|
+
/>
|
|
1269
|
+
@if (control.invalid && control.touched) {
|
|
1270
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
</div>
|
|
1274
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1275
|
+
}
|
|
1276
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TimeFieldComponent, decorators: [{
|
|
1277
|
+
type: Component,
|
|
1278
|
+
args: [{
|
|
1279
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1280
|
+
selector: 'ngx-time-field',
|
|
1281
|
+
standalone: true,
|
|
1282
|
+
imports: [ReactiveFormsModule],
|
|
1283
|
+
template: `
|
|
1284
|
+
<div class="ngx-field ngx-field--time"
|
|
1285
|
+
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
1286
|
+
<label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
|
|
1287
|
+
@if (masked) {
|
|
1288
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
1289
|
+
} @else if (readonly) {
|
|
1290
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatTime(control.value) }}</span>
|
|
1291
|
+
} @else {
|
|
1292
|
+
<input
|
|
1293
|
+
[id]="field.id"
|
|
1294
|
+
class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
|
|
1295
|
+
type="time"
|
|
1296
|
+
[formControl]="$any(control)"
|
|
1297
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
1298
|
+
/>
|
|
1299
|
+
@if (control.invalid && control.touched) {
|
|
1300
|
+
<span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
</div>
|
|
1304
|
+
`,
|
|
1305
|
+
}]
|
|
1306
|
+
}], propDecorators: { field: [{
|
|
1307
|
+
type: Input
|
|
1308
|
+
}], control: [{
|
|
1309
|
+
type: Input
|
|
1310
|
+
}], language: [{
|
|
1311
|
+
type: Input
|
|
1312
|
+
}], readonly: [{
|
|
1313
|
+
type: Input
|
|
1314
|
+
}], masked: [{
|
|
1315
|
+
type: Input
|
|
1316
|
+
}] } });
|
|
1317
|
+
|
|
1093
1318
|
const MONTH_NAMES = [
|
|
1094
1319
|
'January', 'February', 'March', 'April', 'May', 'June',
|
|
1095
1320
|
'July', 'August', 'September', 'October', 'November', 'December',
|
|
@@ -2673,11 +2898,12 @@ class DynamicFieldComponent {
|
|
|
2673
2898
|
ref.setInput('masked', masked);
|
|
2674
2899
|
}
|
|
2675
2900
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DynamicFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2676
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: DynamicFieldComponent, isStandalone: true, selector: "ngx-dynamic-field", inputs: { field: "field", control: "control", config: "config", currentTabId: "currentTabId", language: "language", readonly: "readonly", userRoles: "userRoles" }, viewQueries: [{ propertyName: "fieldHost", first: true, predicate: ["fieldHost"], descendants: true, read: ViewContainerRef, static: true }], usesOnChanges: true, ngImport: i0, template: `<ng-container #fieldHost></ng-container>`, isInline: true });
|
|
2901
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: DynamicFieldComponent, isStandalone: true, selector: "ngx-dynamic-field", inputs: { field: "field", control: "control", config: "config", currentTabId: "currentTabId", language: "language", readonly: "readonly", userRoles: "userRoles" }, viewQueries: [{ propertyName: "fieldHost", first: true, predicate: ["fieldHost"], descendants: true, read: ViewContainerRef, static: true }], usesOnChanges: true, ngImport: i0, template: `<ng-container #fieldHost></ng-container>`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2677
2902
|
}
|
|
2678
2903
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DynamicFieldComponent, decorators: [{
|
|
2679
2904
|
type: Component,
|
|
2680
2905
|
args: [{
|
|
2906
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2681
2907
|
selector: 'ngx-dynamic-field',
|
|
2682
2908
|
standalone: true,
|
|
2683
2909
|
template: `<ng-container #fieldHost></ng-container>`,
|
|
@@ -3475,7 +3701,8 @@ function builtInFieldTypes() {
|
|
|
3475
3701
|
checkbox: CheckboxFieldComponent,
|
|
3476
3702
|
boolean: BooleanFieldComponent,
|
|
3477
3703
|
date: DateFieldComponent,
|
|
3478
|
-
datetime:
|
|
3704
|
+
datetime: DateTimeFieldComponent,
|
|
3705
|
+
time: TimeFieldComponent,
|
|
3479
3706
|
monthYear: MonthYearFieldComponent,
|
|
3480
3707
|
dropdown: DropdownFieldComponent,
|
|
3481
3708
|
radio: RadioFieldComponent,
|
|
@@ -3675,6 +3902,7 @@ class DynamicFormComponent {
|
|
|
3675
3902
|
*/
|
|
3676
3903
|
saveRejected = new EventEmitter();
|
|
3677
3904
|
// ─── Services ─────────────────────────────────────────────────────────────
|
|
3905
|
+
cdr = inject(ChangeDetectorRef);
|
|
3678
3906
|
fb = inject(FormBuilder);
|
|
3679
3907
|
validatorRegistry = inject(ValidatorRegistryService);
|
|
3680
3908
|
hookRegistry = inject(HookRegistryService);
|
|
@@ -3700,6 +3928,7 @@ class DynamicFormComponent {
|
|
|
3700
3928
|
// ─── Form ─────────────────────────────────────────────────────────────────
|
|
3701
3929
|
form;
|
|
3702
3930
|
valueSub;
|
|
3931
|
+
statusSub;
|
|
3703
3932
|
selectionSub;
|
|
3704
3933
|
/** Previous values, for detecting `patchOnTrue` false→true transitions. */
|
|
3705
3934
|
previousValues = {};
|
|
@@ -3864,6 +4093,7 @@ class DynamicFormComponent {
|
|
|
3864
4093
|
}
|
|
3865
4094
|
ngOnDestroy() {
|
|
3866
4095
|
this.valueSub?.unsubscribe();
|
|
4096
|
+
this.statusSub?.unsubscribe();
|
|
3867
4097
|
this.selectionSub?.unsubscribe();
|
|
3868
4098
|
}
|
|
3869
4099
|
setActiveTab(tabId) {
|
|
@@ -3971,6 +4201,12 @@ class DynamicFormComponent {
|
|
|
3971
4201
|
const changes$ = this.changeDebounceMs > 0
|
|
3972
4202
|
? this.form.valueChanges.pipe(debounceTime(this.changeDebounceMs))
|
|
3973
4203
|
: this.form.valueChanges;
|
|
4204
|
+
// Validity is not value: an async validator settling flips `pending` and then `invalid`
|
|
4205
|
+
// with no value change and no template event, so under OnPush the Save button would
|
|
4206
|
+
// never re-enable. submitBlocked reads form.pending and form.invalid, so this is what
|
|
4207
|
+
// keeps it honest.
|
|
4208
|
+
this.statusSub?.unsubscribe();
|
|
4209
|
+
this.statusSub = this.form.statusChanges.subscribe(() => this.cdr.markForCheck());
|
|
3974
4210
|
this.valueSub = changes$.subscribe(() => {
|
|
3975
4211
|
const flattened = this.flattenFormValues();
|
|
3976
4212
|
this.formValues.set(flattened);
|
|
@@ -4432,11 +4668,11 @@ class DynamicFormComponent {
|
|
|
4432
4668
|
this.formReset.emit();
|
|
4433
4669
|
}
|
|
4434
4670
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DynamicFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4435
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DynamicFormComponent, isStandalone: true, selector: "ngx-dynamic-form", inputs: { config: "config", rules: "rules", initialData: "initialData", originalData: "originalData", userRoles: "userRoles", language: "language", readonly: "readonly", readOnlyFields: "readOnlyFields", showInfoBanners: "showInfoBanners", loading: "loading", error: "error", changeDebounceMs: "changeDebounceMs", preview: "preview" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", activeTabChange: "activeTabChange", saveRejected: "saveRejected" }, host: { listeners: { "window:keydown": "handleKeyboardEvent($event)" } }, providers: [EntityRefSelectionService], viewQueries: [{ propertyName: "formPanel", first: true, predicate: ["formPanel"], descendants: true }, { propertyName: "fieldHosts", predicate: DynamicFieldComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "@if (canView) {\n <form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"ngx-form\" novalidate>\n\n <!-- Error message banner -->\n @if (error) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" data-testid=\"form-error\">{{ error }}</div>\n }\n\n <!-- Rules Info Banners -->\n @for (infoKey of showInfoBanners ? Object.keys(ruleResult().infoBanners) : []; track infoKey) {\n <div class=\"ngx-form__info-banner\" role=\"status\" [attr.data-testid]=\"'info-banner-' + infoKey\">\n \u2139\uFE0F {{ ruleResult().infoBanners[infoKey] }}\n </div>\n }\n\n <!-- Rules Validation Errors & Warnings -->\n @for (errKey of Object.keys(ruleResult().validationErrors); track errKey) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" [attr.data-testid]=\"'rule-error-' + errKey\">\n \u26A0\uFE0F {{ ruleResult().validationErrors[errKey] }}\n </div>\n }\n @for (warnKey of Object.keys(ruleResult().validationWarnings); track warnKey) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" [attr.data-testid]=\"'rule-warning-' + warnKey\">\n \u26A1 {{ ruleResult().validationWarnings[warnKey] }}\n </div>\n }\n\n <!-- Deferred VALUE_CHANGED notice for criticalField edits (vs session baseline) -->\n @if (changedCriticalFields().length > 0) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" data-testid=\"critical-change-banner\">\n \uD83D\uDD12 Critical field changed:\n @for (field of changedCriticalFields(); track field.id) {\n <span>{{ resolveFieldLabel(field) }}{{ $last ? '' : ', ' }}</span>\n }\n \u2014 this differs from the value at the start of this session.\n </div>\n }\n\n <!-- Primary Tab bar \u2014 only if visible tabs defined -->\n @if (visibleTabs.length > 0) {\n <div class=\"ngx-form__tabs\" role=\"tablist\" data-testid=\"tab-strip\">\n @for (tab of visibleTabs; track tab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab\"\n [attr.data-testid]=\"'tab-' + tab.id\"\n [class.ngx-form__tab--active]=\"activeTab() === tab.id\"\n (click)=\"setActiveTab(tab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab() === tab.id\"\n >\n {{ resolveTabLabel(tab) }}\n </button>\n }\n </div>\n }\n\n <!-- Sub-tab bar \u2014 only if active tab has sub-tabs -->\n @if (visibleSubTabs.length > 0) {\n <div class=\"ngx-form__tabs ngx-form__tabs--sub\" role=\"tablist\" data-testid=\"subtab-strip\" style=\"margin-top: -12px; margin-bottom: 12px; font-size: 13px;\">\n @for (subTab of visibleSubTabs; track subTab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab ngx-form__tab--sub\"\n [attr.data-testid]=\"'subtab-' + subTab.id\"\n [class.ngx-form__tab--active]=\"(activeSubTabConfig?.id) === subTab.id\"\n (click)=\"setActiveSubTab(subTab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"(activeSubTabConfig?.id) === subTab.id\"\n >\n {{ resolveTabLabel(subTab) }}\n </button>\n }\n </div>\n }\n\n <!--\n One tabpanel per tab, wrapping both branches. Focus moves here on a tab switch, and a\n module tab is still a tab \u2014 putting the panel semantics on only the fields branch left\n a module tab with nowhere for focus to land.\n -->\n <div\n role=\"tabpanel\"\n tabindex=\"-1\"\n #formPanel\n class=\"ngx-form__tabpanel\"\n [attr.aria-label]=\"activeTabLabel\"\n >\n @if (activeTabModuleComponent) {\n <div class=\"ngx-form__module-panel\" data-testid=\"module-panel\">\n <ng-container *ngComponentOutlet=\"activeTabModuleComponent; inputs: (activeSubTabConfig || activeTabConfig)?.moduleInputs\"></ng-container>\n </div>\n } @else {\n <!-- Fields for active tab (Responsive 12-column grid) -->\n <div class=\"ngx-form__panel\" data-testid=\"form-panel\">\n @for (field of fieldsForActiveTab; track field.id) {\n <div\n class=\"ngx-form__field-slot\"\n [style.gridColumn]=\"getFieldSpan(field)\"\n [attr.id]=\"'field-container-' + field.id\"\n >\n <ngx-dynamic-field\n [field]=\"field\"\n [control]=\"getControl(field.id, (activeSubTabConfig || activeTabConfig)?.id)!\"\n [config]=\"config\"\n [currentTabId]=\"(activeSubTabConfig || activeTabConfig)?.id\"\n [language]=\"language\"\n [readonly]=\"isFieldReadonly(field)\"\n [userRoles]=\"userRoles\"\n />\n @if (field.hasDrift) {\n <!--\n Drift was previously visible only in the builder, so a referenced field whose\n source had changed looked completely normal to the person filling the form in.\n role=\"status\" rather than \"alert\": it is information, not an error.\n -->\n <span\n class=\"ngx-field__drift\"\n role=\"status\"\n [attr.data-testid]=\"'field-' + field.id + '-drift'\"\n [title]=\"driftHint(field)\"\n >\u26A0 {{ driftHint(field) }}</span>\n }\n @if (field.criticalField && !readonly) {\n <button\n type=\"button\"\n class=\"ngx-form__lock\"\n [attr.data-testid]=\"'lock-' + field.id\"\n [attr.aria-pressed]=\"!isFieldLocked(field)\"\n [attr.aria-label]=\"\n (isFieldLocked(field) ? 'Unlock ' : 'Lock ') + resolveFieldLabel(field)\n \"\n [title]=\"\n isFieldLocked(field)\n ? 'Critical field \u2014 click to unlock for editing'\n : 'Critical field \u2014 click to lock'\n \"\n (click)=\"toggleFieldLock(field)\"\n >\n {{ isFieldLocked(field) ? '\uD83D\uDD12' : '\uD83D\uDD13' }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Submit button \u2014 gated by RBAC and isSaving lock -->\n @if (!readonly && permissions.canEdit) {\n <div class=\"ngx-form__actions\" data-testid=\"form-actions\">\n <button\n type=\"button\"\n class=\"ngx-form__reset\"\n data-testid=\"form-reset\"\n (click)=\"reset()\"\n [disabled]=\"isSaving()\"\n >\n Reset\n </button>\n <button\n type=\"submit\"\n class=\"ngx-form__submit\"\n data-testid=\"form-submit\"\n [disabled]=\"submitBlocked\"\n title=\"Shortcut: Ctrl+S\"\n >\n @if (isSaving() || loading) {\n Saving\u2026\n } @else {\n Save\n }\n </button>\n </div>\n }\n\n </form>\n} @else {\n <!-- permissions.view denied: render nothing of the record.\n Presentational only \u2014 the server must not send data this user may not see. -->\n <div class=\"ngx-form__denied\" role=\"status\" data-testid=\"form-access-denied\">\n You do not have permission to view this record.\n </div>\n}\n", styles: [".ngx-form__panel{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:16px 20px;align-items:start}.ngx-form__field-slot{display:flex;align-items:flex-start;gap:6px;min-width:0}.ngx-form__field-slot ngx-dynamic-field{flex:1;min-width:0}.ngx-form__lock{flex:0 0 auto;margin-top:22px;background:none;border:1px solid transparent;border-radius:6px;cursor:pointer;font-size:14px;line-height:1;padding:4px 6px}.ngx-form__lock:hover{border-color:#d1d5db;background:#f9fafb}@media (max-width: 768px){.ngx-form__panel{grid-template-columns:1fr}.ngx-form__field-slot{grid-column:span 12!important}}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: DynamicFieldComponent, selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"] }] });
|
|
4671
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DynamicFormComponent, isStandalone: true, selector: "ngx-dynamic-form", inputs: { config: "config", rules: "rules", initialData: "initialData", originalData: "originalData", userRoles: "userRoles", language: "language", readonly: "readonly", readOnlyFields: "readOnlyFields", showInfoBanners: "showInfoBanners", loading: "loading", error: "error", changeDebounceMs: "changeDebounceMs", preview: "preview" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", activeTabChange: "activeTabChange", saveRejected: "saveRejected" }, host: { listeners: { "window:keydown": "handleKeyboardEvent($event)" } }, providers: [EntityRefSelectionService], viewQueries: [{ propertyName: "formPanel", first: true, predicate: ["formPanel"], descendants: true }, { propertyName: "fieldHosts", predicate: DynamicFieldComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "@if (canView) {\n <form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"ngx-form\" novalidate>\n\n <!-- Error message banner -->\n @if (error) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" data-testid=\"form-error\">{{ error }}</div>\n }\n\n <!-- Rules Info Banners -->\n @for (infoKey of showInfoBanners ? Object.keys(ruleResult().infoBanners) : []; track infoKey) {\n <div class=\"ngx-form__info-banner\" role=\"status\" [attr.data-testid]=\"'info-banner-' + infoKey\">\n \u2139\uFE0F {{ ruleResult().infoBanners[infoKey] }}\n </div>\n }\n\n <!-- Rules Validation Errors & Warnings -->\n @for (errKey of Object.keys(ruleResult().validationErrors); track errKey) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" [attr.data-testid]=\"'rule-error-' + errKey\">\n \u26A0\uFE0F {{ ruleResult().validationErrors[errKey] }}\n </div>\n }\n @for (warnKey of Object.keys(ruleResult().validationWarnings); track warnKey) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" [attr.data-testid]=\"'rule-warning-' + warnKey\">\n \u26A1 {{ ruleResult().validationWarnings[warnKey] }}\n </div>\n }\n\n <!-- Deferred VALUE_CHANGED notice for criticalField edits (vs session baseline) -->\n @if (changedCriticalFields().length > 0) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" data-testid=\"critical-change-banner\">\n \uD83D\uDD12 Critical field changed:\n @for (field of changedCriticalFields(); track field.id) {\n <span>{{ resolveFieldLabel(field) }}{{ $last ? '' : ', ' }}</span>\n }\n \u2014 this differs from the value at the start of this session.\n </div>\n }\n\n <!-- Primary Tab bar \u2014 only if visible tabs defined -->\n @if (visibleTabs.length > 0) {\n <div class=\"ngx-form__tabs\" role=\"tablist\" data-testid=\"tab-strip\">\n @for (tab of visibleTabs; track tab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab\"\n [attr.data-testid]=\"'tab-' + tab.id\"\n [class.ngx-form__tab--active]=\"activeTab() === tab.id\"\n (click)=\"setActiveTab(tab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab() === tab.id\"\n >\n {{ resolveTabLabel(tab) }}\n </button>\n }\n </div>\n }\n\n <!-- Sub-tab bar \u2014 only if active tab has sub-tabs -->\n @if (visibleSubTabs.length > 0) {\n <div class=\"ngx-form__tabs ngx-form__tabs--sub\" role=\"tablist\" data-testid=\"subtab-strip\" style=\"margin-top: -12px; margin-bottom: 12px; font-size: 13px;\">\n @for (subTab of visibleSubTabs; track subTab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab ngx-form__tab--sub\"\n [attr.data-testid]=\"'subtab-' + subTab.id\"\n [class.ngx-form__tab--active]=\"(activeSubTabConfig?.id) === subTab.id\"\n (click)=\"setActiveSubTab(subTab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"(activeSubTabConfig?.id) === subTab.id\"\n >\n {{ resolveTabLabel(subTab) }}\n </button>\n }\n </div>\n }\n\n <!--\n One tabpanel per tab, wrapping both branches. Focus moves here on a tab switch, and a\n module tab is still a tab \u2014 putting the panel semantics on only the fields branch left\n a module tab with nowhere for focus to land.\n -->\n <div\n role=\"tabpanel\"\n tabindex=\"-1\"\n #formPanel\n class=\"ngx-form__tabpanel\"\n [attr.aria-label]=\"activeTabLabel\"\n >\n @if (activeTabModuleComponent) {\n <div class=\"ngx-form__module-panel\" data-testid=\"module-panel\">\n <ng-container *ngComponentOutlet=\"activeTabModuleComponent; inputs: (activeSubTabConfig || activeTabConfig)?.moduleInputs\"></ng-container>\n </div>\n } @else {\n <!-- Fields for active tab (Responsive 12-column grid) -->\n <div class=\"ngx-form__panel\" data-testid=\"form-panel\">\n @for (field of fieldsForActiveTab; track field.id) {\n <div\n class=\"ngx-form__field-slot\"\n [style.gridColumn]=\"getFieldSpan(field)\"\n [attr.id]=\"'field-container-' + field.id\"\n tabindex=\"-1\"\n >\n <ngx-dynamic-field\n [field]=\"field\"\n [control]=\"getControl(field.id, (activeSubTabConfig || activeTabConfig)?.id)!\"\n [config]=\"config\"\n [currentTabId]=\"(activeSubTabConfig || activeTabConfig)?.id\"\n [language]=\"language\"\n [readonly]=\"isFieldReadonly(field)\"\n [userRoles]=\"userRoles\"\n />\n @if (field.hasDrift) {\n <!--\n Drift was previously visible only in the builder, so a referenced field whose\n source had changed looked completely normal to the person filling the form in.\n role=\"status\" rather than \"alert\": it is information, not an error.\n -->\n <span\n class=\"ngx-field__drift\"\n role=\"status\"\n [attr.data-testid]=\"'field-' + field.id + '-drift'\"\n [title]=\"driftHint(field)\"\n >\u26A0 {{ driftHint(field) }}</span>\n }\n @if (field.criticalField && !readonly) {\n <button\n type=\"button\"\n class=\"ngx-form__lock\"\n [attr.data-testid]=\"'lock-' + field.id\"\n [attr.aria-pressed]=\"!isFieldLocked(field)\"\n [attr.aria-label]=\"\n (isFieldLocked(field) ? 'Unlock ' : 'Lock ') + resolveFieldLabel(field)\n \"\n [title]=\"\n isFieldLocked(field)\n ? 'Critical field \u2014 click to unlock for editing'\n : 'Critical field \u2014 click to lock'\n \"\n (click)=\"toggleFieldLock(field)\"\n >\n {{ isFieldLocked(field) ? '\uD83D\uDD12' : '\uD83D\uDD13' }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Submit button \u2014 gated by RBAC and isSaving lock -->\n @if (!readonly && permissions.canEdit) {\n <div class=\"ngx-form__actions\" data-testid=\"form-actions\">\n <button\n type=\"button\"\n class=\"ngx-form__reset\"\n data-testid=\"form-reset\"\n (click)=\"reset()\"\n [disabled]=\"isSaving()\"\n >\n Reset\n </button>\n <button\n type=\"submit\"\n class=\"ngx-form__submit\"\n data-testid=\"form-submit\"\n [disabled]=\"submitBlocked\"\n title=\"Shortcut: Ctrl+S\"\n >\n @if (isSaving() || loading) {\n Saving\u2026\n } @else {\n Save\n }\n </button>\n </div>\n }\n\n </form>\n} @else {\n <!-- permissions.view denied: render nothing of the record.\n Presentational only \u2014 the server must not send data this user may not see. -->\n <div class=\"ngx-form__denied\" role=\"status\" data-testid=\"form-access-denied\">\n You do not have permission to view this record.\n </div>\n}\n", styles: [".ngx-form__panel{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:16px 20px;align-items:start}.ngx-form__field-slot{display:flex;align-items:flex-start;gap:6px;min-width:0}.ngx-form__field-slot ngx-dynamic-field{flex:1;min-width:0}.ngx-form__lock{flex:0 0 auto;margin-top:22px;background:none;border:1px solid transparent;border-radius:6px;cursor:pointer;font-size:14px;line-height:1;padding:4px 6px}.ngx-form__lock:hover{border-color:#d1d5db;background:#f9fafb}@media (max-width: 768px){.ngx-form__panel{grid-template-columns:1fr}.ngx-form__field-slot{grid-column:span 12!important}}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: DynamicFieldComponent, selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4436
4672
|
}
|
|
4437
4673
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DynamicFormComponent, decorators: [{
|
|
4438
4674
|
type: Component,
|
|
4439
|
-
args: [{ selector: 'ngx-dynamic-form', standalone: true, imports: [ReactiveFormsModule, DynamicFieldComponent, NgComponentOutlet], providers: [EntityRefSelectionService], template: "@if (canView) {\n <form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"ngx-form\" novalidate>\n\n <!-- Error message banner -->\n @if (error) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" data-testid=\"form-error\">{{ error }}</div>\n }\n\n <!-- Rules Info Banners -->\n @for (infoKey of showInfoBanners ? Object.keys(ruleResult().infoBanners) : []; track infoKey) {\n <div class=\"ngx-form__info-banner\" role=\"status\" [attr.data-testid]=\"'info-banner-' + infoKey\">\n \u2139\uFE0F {{ ruleResult().infoBanners[infoKey] }}\n </div>\n }\n\n <!-- Rules Validation Errors & Warnings -->\n @for (errKey of Object.keys(ruleResult().validationErrors); track errKey) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" [attr.data-testid]=\"'rule-error-' + errKey\">\n \u26A0\uFE0F {{ ruleResult().validationErrors[errKey] }}\n </div>\n }\n @for (warnKey of Object.keys(ruleResult().validationWarnings); track warnKey) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" [attr.data-testid]=\"'rule-warning-' + warnKey\">\n \u26A1 {{ ruleResult().validationWarnings[warnKey] }}\n </div>\n }\n\n <!-- Deferred VALUE_CHANGED notice for criticalField edits (vs session baseline) -->\n @if (changedCriticalFields().length > 0) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" data-testid=\"critical-change-banner\">\n \uD83D\uDD12 Critical field changed:\n @for (field of changedCriticalFields(); track field.id) {\n <span>{{ resolveFieldLabel(field) }}{{ $last ? '' : ', ' }}</span>\n }\n \u2014 this differs from the value at the start of this session.\n </div>\n }\n\n <!-- Primary Tab bar \u2014 only if visible tabs defined -->\n @if (visibleTabs.length > 0) {\n <div class=\"ngx-form__tabs\" role=\"tablist\" data-testid=\"tab-strip\">\n @for (tab of visibleTabs; track tab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab\"\n [attr.data-testid]=\"'tab-' + tab.id\"\n [class.ngx-form__tab--active]=\"activeTab() === tab.id\"\n (click)=\"setActiveTab(tab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab() === tab.id\"\n >\n {{ resolveTabLabel(tab) }}\n </button>\n }\n </div>\n }\n\n <!-- Sub-tab bar \u2014 only if active tab has sub-tabs -->\n @if (visibleSubTabs.length > 0) {\n <div class=\"ngx-form__tabs ngx-form__tabs--sub\" role=\"tablist\" data-testid=\"subtab-strip\" style=\"margin-top: -12px; margin-bottom: 12px; font-size: 13px;\">\n @for (subTab of visibleSubTabs; track subTab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab ngx-form__tab--sub\"\n [attr.data-testid]=\"'subtab-' + subTab.id\"\n [class.ngx-form__tab--active]=\"(activeSubTabConfig?.id) === subTab.id\"\n (click)=\"setActiveSubTab(subTab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"(activeSubTabConfig?.id) === subTab.id\"\n >\n {{ resolveTabLabel(subTab) }}\n </button>\n }\n </div>\n }\n\n <!--\n One tabpanel per tab, wrapping both branches. Focus moves here on a tab switch, and a\n module tab is still a tab \u2014 putting the panel semantics on only the fields branch left\n a module tab with nowhere for focus to land.\n -->\n <div\n role=\"tabpanel\"\n tabindex=\"-1\"\n #formPanel\n class=\"ngx-form__tabpanel\"\n [attr.aria-label]=\"activeTabLabel\"\n >\n @if (activeTabModuleComponent) {\n <div class=\"ngx-form__module-panel\" data-testid=\"module-panel\">\n <ng-container *ngComponentOutlet=\"activeTabModuleComponent; inputs: (activeSubTabConfig || activeTabConfig)?.moduleInputs\"></ng-container>\n </div>\n } @else {\n <!-- Fields for active tab (Responsive 12-column grid) -->\n <div class=\"ngx-form__panel\" data-testid=\"form-panel\">\n @for (field of fieldsForActiveTab; track field.id) {\n <div\n class=\"ngx-form__field-slot\"\n [style.gridColumn]=\"getFieldSpan(field)\"\n [attr.id]=\"'field-container-' + field.id\"\n >\n <ngx-dynamic-field\n [field]=\"field\"\n [control]=\"getControl(field.id, (activeSubTabConfig || activeTabConfig)?.id)!\"\n [config]=\"config\"\n [currentTabId]=\"(activeSubTabConfig || activeTabConfig)?.id\"\n [language]=\"language\"\n [readonly]=\"isFieldReadonly(field)\"\n [userRoles]=\"userRoles\"\n />\n @if (field.hasDrift) {\n <!--\n Drift was previously visible only in the builder, so a referenced field whose\n source had changed looked completely normal to the person filling the form in.\n role=\"status\" rather than \"alert\": it is information, not an error.\n -->\n <span\n class=\"ngx-field__drift\"\n role=\"status\"\n [attr.data-testid]=\"'field-' + field.id + '-drift'\"\n [title]=\"driftHint(field)\"\n >\u26A0 {{ driftHint(field) }}</span>\n }\n @if (field.criticalField && !readonly) {\n <button\n type=\"button\"\n class=\"ngx-form__lock\"\n [attr.data-testid]=\"'lock-' + field.id\"\n [attr.aria-pressed]=\"!isFieldLocked(field)\"\n [attr.aria-label]=\"\n (isFieldLocked(field) ? 'Unlock ' : 'Lock ') + resolveFieldLabel(field)\n \"\n [title]=\"\n isFieldLocked(field)\n ? 'Critical field \u2014 click to unlock for editing'\n : 'Critical field \u2014 click to lock'\n \"\n (click)=\"toggleFieldLock(field)\"\n >\n {{ isFieldLocked(field) ? '\uD83D\uDD12' : '\uD83D\uDD13' }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Submit button \u2014 gated by RBAC and isSaving lock -->\n @if (!readonly && permissions.canEdit) {\n <div class=\"ngx-form__actions\" data-testid=\"form-actions\">\n <button\n type=\"button\"\n class=\"ngx-form__reset\"\n data-testid=\"form-reset\"\n (click)=\"reset()\"\n [disabled]=\"isSaving()\"\n >\n Reset\n </button>\n <button\n type=\"submit\"\n class=\"ngx-form__submit\"\n data-testid=\"form-submit\"\n [disabled]=\"submitBlocked\"\n title=\"Shortcut: Ctrl+S\"\n >\n @if (isSaving() || loading) {\n Saving\u2026\n } @else {\n Save\n }\n </button>\n </div>\n }\n\n </form>\n} @else {\n <!-- permissions.view denied: render nothing of the record.\n Presentational only \u2014 the server must not send data this user may not see. -->\n <div class=\"ngx-form__denied\" role=\"status\" data-testid=\"form-access-denied\">\n You do not have permission to view this record.\n </div>\n}\n", styles: [".ngx-form__panel{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:16px 20px;align-items:start}.ngx-form__field-slot{display:flex;align-items:flex-start;gap:6px;min-width:0}.ngx-form__field-slot ngx-dynamic-field{flex:1;min-width:0}.ngx-form__lock{flex:0 0 auto;margin-top:22px;background:none;border:1px solid transparent;border-radius:6px;cursor:pointer;font-size:14px;line-height:1;padding:4px 6px}.ngx-form__lock:hover{border-color:#d1d5db;background:#f9fafb}@media (max-width: 768px){.ngx-form__panel{grid-template-columns:1fr}.ngx-form__field-slot{grid-column:span 12!important}}\n"] }]
|
|
4675
|
+
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-dynamic-form', standalone: true, imports: [ReactiveFormsModule, DynamicFieldComponent, NgComponentOutlet], providers: [EntityRefSelectionService], template: "@if (canView) {\n <form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"ngx-form\" novalidate>\n\n <!-- Error message banner -->\n @if (error) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" data-testid=\"form-error\">{{ error }}</div>\n }\n\n <!-- Rules Info Banners -->\n @for (infoKey of showInfoBanners ? Object.keys(ruleResult().infoBanners) : []; track infoKey) {\n <div class=\"ngx-form__info-banner\" role=\"status\" [attr.data-testid]=\"'info-banner-' + infoKey\">\n \u2139\uFE0F {{ ruleResult().infoBanners[infoKey] }}\n </div>\n }\n\n <!-- Rules Validation Errors & Warnings -->\n @for (errKey of Object.keys(ruleResult().validationErrors); track errKey) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" [attr.data-testid]=\"'rule-error-' + errKey\">\n \u26A0\uFE0F {{ ruleResult().validationErrors[errKey] }}\n </div>\n }\n @for (warnKey of Object.keys(ruleResult().validationWarnings); track warnKey) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" [attr.data-testid]=\"'rule-warning-' + warnKey\">\n \u26A1 {{ ruleResult().validationWarnings[warnKey] }}\n </div>\n }\n\n <!-- Deferred VALUE_CHANGED notice for criticalField edits (vs session baseline) -->\n @if (changedCriticalFields().length > 0) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" data-testid=\"critical-change-banner\">\n \uD83D\uDD12 Critical field changed:\n @for (field of changedCriticalFields(); track field.id) {\n <span>{{ resolveFieldLabel(field) }}{{ $last ? '' : ', ' }}</span>\n }\n \u2014 this differs from the value at the start of this session.\n </div>\n }\n\n <!-- Primary Tab bar \u2014 only if visible tabs defined -->\n @if (visibleTabs.length > 0) {\n <div class=\"ngx-form__tabs\" role=\"tablist\" data-testid=\"tab-strip\">\n @for (tab of visibleTabs; track tab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab\"\n [attr.data-testid]=\"'tab-' + tab.id\"\n [class.ngx-form__tab--active]=\"activeTab() === tab.id\"\n (click)=\"setActiveTab(tab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab() === tab.id\"\n >\n {{ resolveTabLabel(tab) }}\n </button>\n }\n </div>\n }\n\n <!-- Sub-tab bar \u2014 only if active tab has sub-tabs -->\n @if (visibleSubTabs.length > 0) {\n <div class=\"ngx-form__tabs ngx-form__tabs--sub\" role=\"tablist\" data-testid=\"subtab-strip\" style=\"margin-top: -12px; margin-bottom: 12px; font-size: 13px;\">\n @for (subTab of visibleSubTabs; track subTab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab ngx-form__tab--sub\"\n [attr.data-testid]=\"'subtab-' + subTab.id\"\n [class.ngx-form__tab--active]=\"(activeSubTabConfig?.id) === subTab.id\"\n (click)=\"setActiveSubTab(subTab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"(activeSubTabConfig?.id) === subTab.id\"\n >\n {{ resolveTabLabel(subTab) }}\n </button>\n }\n </div>\n }\n\n <!--\n One tabpanel per tab, wrapping both branches. Focus moves here on a tab switch, and a\n module tab is still a tab \u2014 putting the panel semantics on only the fields branch left\n a module tab with nowhere for focus to land.\n -->\n <div\n role=\"tabpanel\"\n tabindex=\"-1\"\n #formPanel\n class=\"ngx-form__tabpanel\"\n [attr.aria-label]=\"activeTabLabel\"\n >\n @if (activeTabModuleComponent) {\n <div class=\"ngx-form__module-panel\" data-testid=\"module-panel\">\n <ng-container *ngComponentOutlet=\"activeTabModuleComponent; inputs: (activeSubTabConfig || activeTabConfig)?.moduleInputs\"></ng-container>\n </div>\n } @else {\n <!-- Fields for active tab (Responsive 12-column grid) -->\n <div class=\"ngx-form__panel\" data-testid=\"form-panel\">\n @for (field of fieldsForActiveTab; track field.id) {\n <div\n class=\"ngx-form__field-slot\"\n [style.gridColumn]=\"getFieldSpan(field)\"\n [attr.id]=\"'field-container-' + field.id\"\n tabindex=\"-1\"\n >\n <ngx-dynamic-field\n [field]=\"field\"\n [control]=\"getControl(field.id, (activeSubTabConfig || activeTabConfig)?.id)!\"\n [config]=\"config\"\n [currentTabId]=\"(activeSubTabConfig || activeTabConfig)?.id\"\n [language]=\"language\"\n [readonly]=\"isFieldReadonly(field)\"\n [userRoles]=\"userRoles\"\n />\n @if (field.hasDrift) {\n <!--\n Drift was previously visible only in the builder, so a referenced field whose\n source had changed looked completely normal to the person filling the form in.\n role=\"status\" rather than \"alert\": it is information, not an error.\n -->\n <span\n class=\"ngx-field__drift\"\n role=\"status\"\n [attr.data-testid]=\"'field-' + field.id + '-drift'\"\n [title]=\"driftHint(field)\"\n >\u26A0 {{ driftHint(field) }}</span>\n }\n @if (field.criticalField && !readonly) {\n <button\n type=\"button\"\n class=\"ngx-form__lock\"\n [attr.data-testid]=\"'lock-' + field.id\"\n [attr.aria-pressed]=\"!isFieldLocked(field)\"\n [attr.aria-label]=\"\n (isFieldLocked(field) ? 'Unlock ' : 'Lock ') + resolveFieldLabel(field)\n \"\n [title]=\"\n isFieldLocked(field)\n ? 'Critical field \u2014 click to unlock for editing'\n : 'Critical field \u2014 click to lock'\n \"\n (click)=\"toggleFieldLock(field)\"\n >\n {{ isFieldLocked(field) ? '\uD83D\uDD12' : '\uD83D\uDD13' }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Submit button \u2014 gated by RBAC and isSaving lock -->\n @if (!readonly && permissions.canEdit) {\n <div class=\"ngx-form__actions\" data-testid=\"form-actions\">\n <button\n type=\"button\"\n class=\"ngx-form__reset\"\n data-testid=\"form-reset\"\n (click)=\"reset()\"\n [disabled]=\"isSaving()\"\n >\n Reset\n </button>\n <button\n type=\"submit\"\n class=\"ngx-form__submit\"\n data-testid=\"form-submit\"\n [disabled]=\"submitBlocked\"\n title=\"Shortcut: Ctrl+S\"\n >\n @if (isSaving() || loading) {\n Saving\u2026\n } @else {\n Save\n }\n </button>\n </div>\n }\n\n </form>\n} @else {\n <!-- permissions.view denied: render nothing of the record.\n Presentational only \u2014 the server must not send data this user may not see. -->\n <div class=\"ngx-form__denied\" role=\"status\" data-testid=\"form-access-denied\">\n You do not have permission to view this record.\n </div>\n}\n", styles: [".ngx-form__panel{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:16px 20px;align-items:start}.ngx-form__field-slot{display:flex;align-items:flex-start;gap:6px;min-width:0}.ngx-form__field-slot ngx-dynamic-field{flex:1;min-width:0}.ngx-form__lock{flex:0 0 auto;margin-top:22px;background:none;border:1px solid transparent;border-radius:6px;cursor:pointer;font-size:14px;line-height:1;padding:4px 6px}.ngx-form__lock:hover{border-color:#d1d5db;background:#f9fafb}@media (max-width: 768px){.ngx-form__panel{grid-template-columns:1fr}.ngx-form__field-slot{grid-column:span 12!important}}\n"] }]
|
|
4440
4676
|
}], propDecorators: { config: [{
|
|
4441
4677
|
type: Input
|
|
4442
4678
|
}], rules: [{
|
|
@@ -4522,6 +4758,8 @@ class DynamicRecordFormComponent {
|
|
|
4522
4758
|
sectionSave = new EventEmitter();
|
|
4523
4759
|
dynamicFormComp;
|
|
4524
4760
|
rulesEvaluation = inject(RulesEvaluationService);
|
|
4761
|
+
host = inject(ElementRef);
|
|
4762
|
+
injector = inject(Injector);
|
|
4525
4763
|
currentData = signal({});
|
|
4526
4764
|
originalBaseline = signal({});
|
|
4527
4765
|
/**
|
|
@@ -4911,29 +5149,64 @@ class DynamicRecordFormComponent {
|
|
|
4911
5149
|
formatFieldValue(field) {
|
|
4912
5150
|
return formatDisplayValue(field.type, field.options, this.fieldValue(field.id), this.language);
|
|
4913
5151
|
}
|
|
4914
|
-
|
|
4915
|
-
|
|
5152
|
+
/**
|
|
5153
|
+
* Finds which tab — and, where the field lives one level down, which sub-tab — owns a
|
|
5154
|
+
* field. The walk used to check top-level `fields` only, so every field in a sub-tab was
|
|
5155
|
+
* simply not found and the jump did nothing at all.
|
|
5156
|
+
*/
|
|
5157
|
+
locateField(fieldId) {
|
|
4916
5158
|
for (const tab of this.config?.tabs || []) {
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
4924
|
-
el.focus();
|
|
4925
|
-
}
|
|
4926
|
-
}, 50);
|
|
4927
|
-
break;
|
|
5159
|
+
if ((tab.fields || []).some(f => f.id === fieldId))
|
|
5160
|
+
return { tabId: tab.id };
|
|
5161
|
+
for (const child of tab.children || []) {
|
|
5162
|
+
if ((child.fields || []).some(f => f.id === fieldId)) {
|
|
5163
|
+
return { tabId: tab.id, subTabId: child.id };
|
|
5164
|
+
}
|
|
4928
5165
|
}
|
|
4929
5166
|
}
|
|
5167
|
+
return null;
|
|
5168
|
+
}
|
|
5169
|
+
/**
|
|
5170
|
+
* Switches to the tab holding `fieldId`, then scrolls it into view and moves focus to it.
|
|
5171
|
+
*
|
|
5172
|
+
* The wait for the new tab to render used to be a 50 ms `setTimeout`, which was wrong three
|
|
5173
|
+
* ways: `document` is undefined on a server render, 50 ms is a guess that a large tab can
|
|
5174
|
+
* outrun, and nothing cancelled the timer if the component went away first.
|
|
5175
|
+
* `afterNextRender` fixes all three — it never runs on the server, it runs when the panel
|
|
5176
|
+
* has actually rendered rather than when a guess expires, and it is tied to the injector so
|
|
5177
|
+
* destroying the component cancels it. The query is scoped to this component's own element,
|
|
5178
|
+
* so the library no longer reaches for the global `document` at all.
|
|
5179
|
+
*/
|
|
5180
|
+
jumpToField(fieldId) {
|
|
5181
|
+
const location = this.locateField(fieldId);
|
|
5182
|
+
if (!location)
|
|
5183
|
+
return;
|
|
5184
|
+
this.dynamicFormComp?.setActiveTab(location.tabId);
|
|
5185
|
+
// `setActiveTab` resets to a tab's first child, so the sub-tab has to be selected after
|
|
5186
|
+
// it, not before. Without this the walk found sub-tab fields and then rendered the wrong
|
|
5187
|
+
// panel, so the element the callback below looks for never existed.
|
|
5188
|
+
if (location.subTabId)
|
|
5189
|
+
this.dynamicFormComp?.setActiveSubTab(location.subTabId);
|
|
5190
|
+
afterNextRender(() => {
|
|
5191
|
+
// The field id comes from config, so it never goes into a selector string: no
|
|
5192
|
+
// escaping to get wrong, and no need for `CSS.escape`, which jsdom and older
|
|
5193
|
+
// browsers do not provide and whose absence throws silently inside a render hook.
|
|
5194
|
+
const wanted = `field-container-${fieldId}`;
|
|
5195
|
+
const el = Array.from(this.host.nativeElement.querySelectorAll('[id^="field-container-"]')).find(slot => slot.id === wanted);
|
|
5196
|
+
if (!el)
|
|
5197
|
+
return;
|
|
5198
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
5199
|
+
// The slot carries tabindex="-1" so this actually moves focus. Without it the jump
|
|
5200
|
+
// scrolled the field into view and left focus behind on the link.
|
|
5201
|
+
el.focus();
|
|
5202
|
+
}, { injector: this.injector });
|
|
4930
5203
|
}
|
|
4931
5204
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DynamicRecordFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4932
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DynamicRecordFormComponent, isStandalone: true, selector: "ngx-dynamic-record-form", inputs: { config: "config", rules: "rules", initialData: "initialData", userRoles: "userRoles", language: "language", readonly: "readonly", loading: "loading", error: "error", isReadOnly: "isReadOnly", readOnlyFields: "readOnlyFields", viewMode: "viewMode" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", sectionSave: "sectionSave" }, viewQueries: [{ propertyName: "dynamicFormComp", first: true, predicate: DynamicFormComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"ngx-record-editor\">\n <!-- Record Header -->\n <div class=\"ngx-record-editor__header\">\n <div class=\"ngx-record-editor__title-group\">\n @if (profileImageUrl) {\n <img\n class=\"ngx-record-editor__avatar\"\n data-testid=\"header-profile-image\"\n [src]=\"profileImageUrl\"\n [alt]=\"recordTitle\"\n />\n } @else {\n <div class=\"ngx-record-editor__avatar\" data-testid=\"header-avatar-initial\">\n {{ avatarLetter }}\n </div>\n }\n <div>\n <h2 class=\"ngx-record-editor__title\" data-testid=\"record-title\">\n {{ recordTitle }}\n </h2>\n <span class=\"ngx-record-editor__subtitle\">Entity: {{ config.entity }}</span>\n </div>\n </div>\n\n <!-- Record-level status switch, outside section editing -->\n @if (headerToggleField; as toggleField) {\n <label class=\"ngx-record-editor__header-toggle\">\n <span>{{ formatFieldLabel(toggleField) }}</span>\n <input\n type=\"checkbox\"\n role=\"switch\"\n data-testid=\"header-toggle\"\n [checked]=\"headerToggleValue\"\n [disabled]=\"recordReadOnly\"\n [attr.aria-checked]=\"headerToggleValue\"\n (change)=\"toggleHeaderStatus()\"\n />\n <span class=\"ngx-record-editor__header-toggle-text\">{{ headerToggleValue ? 'Yes' : 'No' }}</span>\n </label>\n }\n </div>\n\n <!-- Dismissible info banners (rules `info` action) -->\n @for (banner of infoBanners; track banner.fieldId) {\n <div\n class=\"ngx-record-editor__banner ngx-record-editor__banner--info\"\n role=\"status\"\n [attr.data-testid]=\"'info-banner-' + banner.fieldId\"\n >\n <span>\u2139\uFE0F {{ banner.message }}</span>\n <button\n type=\"button\"\n class=\"ngx-record-editor__banner-dismiss\"\n [attr.data-testid]=\"'dismiss-' + banner.fieldId\"\n [attr.aria-label]=\"'Dismiss: ' + banner.message\"\n (click)=\"dismissInfoBanner(banner.fieldId)\"\n >\u2715</button>\n </div>\n }\n\n <!-- Modification baseline banner -->\n @if (isModified()) {\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--warning\">\n \uD83D\uDD12 Record modified from session baseline \u2014 unsaved changes pending.\n </div>\n }\n\n <!-- Summary Panel (showOnMinimize fields with interactive jump) -->\n @if (summaryFields().length > 0) {\n <div class=\"ngx-record-editor__summary-panel\" data-testid=\"summary-panel\">\n @for (sf of summaryFields(); track sf.id) {\n <div\n class=\"ngx-record-editor__summary-item\"\n [attr.data-testid]=\"'summary-' + sf.id\"\n (click)=\"jumpToField(sf.id)\"\n [title]=\"'Click to jump to ' + formatFieldLabel(sf)\"\n >\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(sf) }}</span>\n <span class=\"ngx-record-editor__summary-val\">{{ formatFieldValue(sf) }}</span>\n </div>\n }\n </div>\n }\n\n <!-- Section edit bar: one tab is edited at a time, and validated on its own -->\n @if (viewMode && !recordReadOnly) {\n <div class=\"ngx-record-editor__section-bar\">\n @if (isEditingActiveTab) {\n <span class=\"ngx-record-editor__section-hint\">Editing this section</span>\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-section\" (click)=\"cancelSection()\">\n Cancel\n </button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-section\" (click)=\"saveSection()\">\n Save section\n </button>\n } @else {\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"edit-section\" (click)=\"editSection()\">\n Edit section\n </button>\n }\n </div>\n }\n\n @if (Object.keys(sectionErrors()).length > 0) {\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--error\" role=\"alert\" data-testid=\"section-errors\">\n @for (key of Object.keys(sectionErrors()); track key) {\n <div>\u26A0\uFE0F {{ sectionErrors()[key] }}</div>\n }\n </div>\n }\n\n <!-- Array rows: listed here, edited in the drawer below -->\n @for (arrayField of arrayFieldsForActiveTab; track arrayField.id) {\n <div class=\"ngx-record-editor__rows\" [attr.data-testid]=\"'rows-' + arrayField.id\">\n <div class=\"ngx-record-editor__rows-head\">\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(arrayField) }}</span>\n @if (!sectionReadOnly) {\n <button\n type=\"button\"\n class=\"ngx-form__reset\"\n [attr.data-testid]=\"'add-row-' + arrayField.id\"\n (click)=\"openAddRow(arrayField)\"\n >+ Row</button>\n }\n </div>\n\n @for (row of rowsOf(arrayField); track $index) {\n <div class=\"ngx-record-editor__row\" [attr.data-testid]=\"'row-' + arrayField.id + '-' + $index\">\n <span class=\"ngx-record-editor__row-text\">{{ rowSummary(arrayField, row) }}</span>\n @if (!sectionReadOnly) {\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'edit-row-' + arrayField.id + '-' + $index\"\n (click)=\"openEditRow(arrayField, $index)\">Edit</button>\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'delete-row-' + arrayField.id + '-' + $index\"\n (click)=\"deleteRow(arrayField, $index)\">Delete</button>\n }\n </div>\n } @empty {\n <p class=\"ngx-record-editor__row-empty\">No rows yet.</p>\n }\n </div>\n }\n\n <!--\n Row drawer. Deliberately outside the tab panel: building a row's FormGroup inside the\n tab view races the tab's own initialisation (the reference hit this with FormArray).\n -->\n @if (inlineRowField(); as rowField) {\n <div class=\"ngx-record-editor__drawer\" data-testid=\"row-drawer\" [formGroup]=\"inlineRowForm!\">\n <div class=\"ngx-record-editor__rows-head\">\n <strong>{{ inlineRowIndex() === null ? 'Add' : 'Edit' }} {{ formatFieldLabel(rowField) }} row</strong>\n </div>\n\n <div class=\"ngx-form__panel\">\n @for (child of rowField.children ?? []; track child.id) {\n <ngx-dynamic-field\n [field]=\"child\"\n [control]=\"inlineRowForm!.get(child.id)!\"\n [config]=\"config\"\n [language]=\"language\"\n [readonly]=\"false\"\n [userRoles]=\"userRoles\"\n [style.gridColumn]=\"'span 12'\"\n />\n }\n </div>\n\n <div class=\"ngx-record-editor__section-bar\">\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-row\" (click)=\"cancelRow()\">Cancel</button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-row\" (click)=\"saveRow()\">Save row</button>\n </div>\n </div>\n }\n\n <!-- Main Dynamic Form -->\n <ngx-dynamic-form\n [config]=\"config\"\n [rules]=\"rules\"\n [initialData]=\"initialData\"\n [originalData]=\"originalBaseline()\"\n [userRoles]=\"userRoles\"\n [language]=\"language\"\n [readonly]=\"sectionReadOnly\"\n [readOnlyFields]=\"readOnlyFields\"\n [showInfoBanners]=\"false\"\n [loading]=\"loading\"\n [error]=\"error\"\n (formChange)=\"onFormChange($event)\"\n (formSubmit)=\"onFormSubmit($event)\"\n (formReset)=\"onFormReset()\"\n (activeTabChange)=\"onActiveTabChange($event)\"\n />\n</div>\n", styles: [".ngx-record-editor{display:flex;flex-direction:column;gap:16px;background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:20px;box-shadow:0 4px 6px -1px #0000000d}.ngx-record-editor__header{display:flex;align-items:center;justify-content:space-between;padding-bottom:16px;border-bottom:1px solid #f3f4f6}.ngx-record-editor__title-group{display:flex;align-items:center;gap:12px}.ngx-record-editor__avatar{width:48px;height:48px;border-radius:50%;object-fit:cover;background:#e0e7ff;color:#4f46e5;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px}.ngx-record-editor__title{font-size:20px;font-weight:700;color:#111827;margin:0}.ngx-record-editor__subtitle{font-size:13px;color:#6b7280}.ngx-record-editor__header-toggle{display:flex;align-items:center;gap:8px;font-size:13px;color:#374151}.ngx-record-editor__header-toggle-text{font-weight:600}.ngx-record-editor__banner{padding:10px 14px;border-radius:8px;font-size:13px;font-weight:500}.ngx-record-editor__banner--info{background:#eff6ff;border:1px solid #dbeafe;color:#1d4ed8;display:flex;align-items:center;justify-content:space-between;gap:12px}.ngx-record-editor__banner-dismiss{background:none;border:none;cursor:pointer;color:inherit;font-size:14px;line-height:1;padding:2px 6px;border-radius:4px}.ngx-record-editor__banner-dismiss:hover{background:#0000000f}.ngx-record-editor__banner--error{background:#fef2f2;border:1px solid #fecaca;color:#b91c1c}.ngx-record-editor__section-bar{display:flex;align-items:center;justify-content:flex-end;gap:8px}.ngx-record-editor__section-hint{margin-right:auto;font-size:12px;color:#6b7280}.ngx-record-editor__banner--warning{background:#fffbeb;border:1px solid #fef3c7;color:#b45309}.ngx-record-editor__rows{border:1px solid #f3f4f6;border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:6px}.ngx-record-editor__rows-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.ngx-record-editor__row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:#f9fafb}.ngx-record-editor__row-text{flex:1;font-size:13px;overflow-wrap:anywhere}.ngx-record-editor__row-btn{background:none;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;font-size:12px;padding:2px 8px}.ngx-record-editor__row-empty{font-size:12px;color:#6b7280;margin:0}.ngx-record-editor__drawer{border:1px solid #dbeafe;background:#f8fafc;border-radius:8px;padding:14px;display:flex;flex-direction:column;gap:12px}.ngx-record-editor__summary-panel{background:#f9fafb;border:1px solid #f3f4f6;border-radius:8px;padding:12px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}.ngx-record-editor__summary-item{display:flex;flex-direction:column;cursor:pointer;padding:6px 10px;border-radius:6px;transition:background .15s}.ngx-record-editor__summary-item:hover{background:#eff6ff}.ngx-record-editor__summary-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;font-weight:600}.ngx-record-editor__summary-val{font-size:14px;font-weight:600;color:#1f2937}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: DynamicFormComponent, selector: "ngx-dynamic-form", inputs: ["config", "rules", "initialData", "originalData", "userRoles", "language", "readonly", "readOnlyFields", "showInfoBanners", "loading", "error", "changeDebounceMs", "preview"], outputs: ["formSubmit", "formChange", "formReset", "activeTabChange", "saveRejected"] }, { kind: "component", type: DynamicFieldComponent, selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }] });
|
|
5205
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DynamicRecordFormComponent, isStandalone: true, selector: "ngx-dynamic-record-form", inputs: { config: "config", rules: "rules", initialData: "initialData", userRoles: "userRoles", language: "language", readonly: "readonly", loading: "loading", error: "error", isReadOnly: "isReadOnly", readOnlyFields: "readOnlyFields", viewMode: "viewMode" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", sectionSave: "sectionSave" }, viewQueries: [{ propertyName: "dynamicFormComp", first: true, predicate: DynamicFormComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"ngx-record-editor\">\n <!-- Record Header -->\n <div class=\"ngx-record-editor__header\">\n <div class=\"ngx-record-editor__title-group\">\n @if (profileImageUrl) {\n <img\n class=\"ngx-record-editor__avatar\"\n data-testid=\"header-profile-image\"\n [src]=\"profileImageUrl\"\n [alt]=\"recordTitle\"\n />\n } @else {\n <div class=\"ngx-record-editor__avatar\" data-testid=\"header-avatar-initial\">\n {{ avatarLetter }}\n </div>\n }\n <div>\n <h2 class=\"ngx-record-editor__title\" data-testid=\"record-title\">\n {{ recordTitle }}\n </h2>\n <span class=\"ngx-record-editor__subtitle\">Entity: {{ config.entity }}</span>\n </div>\n </div>\n\n <!-- Record-level status switch, outside section editing -->\n @if (headerToggleField; as toggleField) {\n <label class=\"ngx-record-editor__header-toggle\">\n <span>{{ formatFieldLabel(toggleField) }}</span>\n <input\n type=\"checkbox\"\n role=\"switch\"\n data-testid=\"header-toggle\"\n [checked]=\"headerToggleValue\"\n [disabled]=\"recordReadOnly\"\n [attr.aria-checked]=\"headerToggleValue\"\n (change)=\"toggleHeaderStatus()\"\n />\n <span class=\"ngx-record-editor__header-toggle-text\">{{ headerToggleValue ? 'Yes' : 'No' }}</span>\n </label>\n }\n </div>\n\n <!-- Dismissible info banners (rules `info` action) -->\n @for (banner of infoBanners; track banner.fieldId) {\n <div\n class=\"ngx-record-editor__banner ngx-record-editor__banner--info\"\n role=\"status\"\n [attr.data-testid]=\"'info-banner-' + banner.fieldId\"\n >\n <span>\u2139\uFE0F {{ banner.message }}</span>\n <button\n type=\"button\"\n class=\"ngx-record-editor__banner-dismiss\"\n [attr.data-testid]=\"'dismiss-' + banner.fieldId\"\n [attr.aria-label]=\"'Dismiss: ' + banner.message\"\n (click)=\"dismissInfoBanner(banner.fieldId)\"\n >\u2715</button>\n </div>\n }\n\n <!-- Modification baseline banner -->\n @if (isModified()) {\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--warning\">\n \uD83D\uDD12 Record modified from session baseline \u2014 unsaved changes pending.\n </div>\n }\n\n <!-- Summary Panel (showOnMinimize fields with interactive jump) -->\n @if (summaryFields().length > 0) {\n <div class=\"ngx-record-editor__summary-panel\" data-testid=\"summary-panel\">\n @for (sf of summaryFields(); track sf.id) {\n <div\n class=\"ngx-record-editor__summary-item\"\n [attr.data-testid]=\"'summary-' + sf.id\"\n (click)=\"jumpToField(sf.id)\"\n [title]=\"'Click to jump to ' + formatFieldLabel(sf)\"\n >\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(sf) }}</span>\n <span class=\"ngx-record-editor__summary-val\">{{ formatFieldValue(sf) }}</span>\n </div>\n }\n </div>\n }\n\n <!-- Section edit bar: one tab is edited at a time, and validated on its own -->\n @if (viewMode && !recordReadOnly) {\n <div class=\"ngx-record-editor__section-bar\">\n @if (isEditingActiveTab) {\n <span class=\"ngx-record-editor__section-hint\">Editing this section</span>\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-section\" (click)=\"cancelSection()\">\n Cancel\n </button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-section\" (click)=\"saveSection()\">\n Save section\n </button>\n } @else {\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"edit-section\" (click)=\"editSection()\">\n Edit section\n </button>\n }\n </div>\n }\n\n @if (Object.keys(sectionErrors()).length > 0) {\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--error\" role=\"alert\" data-testid=\"section-errors\">\n @for (key of Object.keys(sectionErrors()); track key) {\n <div>\u26A0\uFE0F {{ sectionErrors()[key] }}</div>\n }\n </div>\n }\n\n <!-- Array rows: listed here, edited in the drawer below -->\n @for (arrayField of arrayFieldsForActiveTab; track arrayField.id) {\n <div class=\"ngx-record-editor__rows\" [attr.data-testid]=\"'rows-' + arrayField.id\">\n <div class=\"ngx-record-editor__rows-head\">\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(arrayField) }}</span>\n @if (!sectionReadOnly) {\n <button\n type=\"button\"\n class=\"ngx-form__reset\"\n [attr.data-testid]=\"'add-row-' + arrayField.id\"\n (click)=\"openAddRow(arrayField)\"\n >+ Row</button>\n }\n </div>\n\n @for (row of rowsOf(arrayField); track $index) {\n <div class=\"ngx-record-editor__row\" [attr.data-testid]=\"'row-' + arrayField.id + '-' + $index\">\n <span class=\"ngx-record-editor__row-text\">{{ rowSummary(arrayField, row) }}</span>\n @if (!sectionReadOnly) {\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'edit-row-' + arrayField.id + '-' + $index\"\n (click)=\"openEditRow(arrayField, $index)\">Edit</button>\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'delete-row-' + arrayField.id + '-' + $index\"\n (click)=\"deleteRow(arrayField, $index)\">Delete</button>\n }\n </div>\n } @empty {\n <p class=\"ngx-record-editor__row-empty\">No rows yet.</p>\n }\n </div>\n }\n\n <!--\n Row drawer. Deliberately outside the tab panel: building a row's FormGroup inside the\n tab view races the tab's own initialisation (the reference hit this with FormArray).\n -->\n @if (inlineRowField(); as rowField) {\n <div class=\"ngx-record-editor__drawer\" data-testid=\"row-drawer\" [formGroup]=\"inlineRowForm!\">\n <div class=\"ngx-record-editor__rows-head\">\n <strong>{{ inlineRowIndex() === null ? 'Add' : 'Edit' }} {{ formatFieldLabel(rowField) }} row</strong>\n </div>\n\n <div class=\"ngx-form__panel\">\n @for (child of rowField.children ?? []; track child.id) {\n <ngx-dynamic-field\n [field]=\"child\"\n [control]=\"inlineRowForm!.get(child.id)!\"\n [config]=\"config\"\n [language]=\"language\"\n [readonly]=\"false\"\n [userRoles]=\"userRoles\"\n [style.gridColumn]=\"'span 12'\"\n />\n }\n </div>\n\n <div class=\"ngx-record-editor__section-bar\">\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-row\" (click)=\"cancelRow()\">Cancel</button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-row\" (click)=\"saveRow()\">Save row</button>\n </div>\n </div>\n }\n\n <!-- Main Dynamic Form -->\n <ngx-dynamic-form\n [config]=\"config\"\n [rules]=\"rules\"\n [initialData]=\"initialData\"\n [originalData]=\"originalBaseline()\"\n [userRoles]=\"userRoles\"\n [language]=\"language\"\n [readonly]=\"sectionReadOnly\"\n [readOnlyFields]=\"readOnlyFields\"\n [showInfoBanners]=\"false\"\n [loading]=\"loading\"\n [error]=\"error\"\n (formChange)=\"onFormChange($event)\"\n (formSubmit)=\"onFormSubmit($event)\"\n (formReset)=\"onFormReset()\"\n (activeTabChange)=\"onActiveTabChange($event)\"\n />\n</div>\n", styles: [".ngx-record-editor{display:flex;flex-direction:column;gap:16px;background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:20px;box-shadow:0 4px 6px -1px #0000000d}.ngx-record-editor__header{display:flex;align-items:center;justify-content:space-between;padding-bottom:16px;border-bottom:1px solid #f3f4f6}.ngx-record-editor__title-group{display:flex;align-items:center;gap:12px}.ngx-record-editor__avatar{width:48px;height:48px;border-radius:50%;object-fit:cover;background:#e0e7ff;color:#4f46e5;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px}.ngx-record-editor__title{font-size:20px;font-weight:700;color:#111827;margin:0}.ngx-record-editor__subtitle{font-size:13px;color:#6b7280}.ngx-record-editor__header-toggle{display:flex;align-items:center;gap:8px;font-size:13px;color:#374151}.ngx-record-editor__header-toggle-text{font-weight:600}.ngx-record-editor__banner{padding:10px 14px;border-radius:8px;font-size:13px;font-weight:500}.ngx-record-editor__banner--info{background:#eff6ff;border:1px solid #dbeafe;color:#1d4ed8;display:flex;align-items:center;justify-content:space-between;gap:12px}.ngx-record-editor__banner-dismiss{background:none;border:none;cursor:pointer;color:inherit;font-size:14px;line-height:1;padding:2px 6px;border-radius:4px}.ngx-record-editor__banner-dismiss:hover{background:#0000000f}.ngx-record-editor__banner--error{background:#fef2f2;border:1px solid #fecaca;color:#b91c1c}.ngx-record-editor__section-bar{display:flex;align-items:center;justify-content:flex-end;gap:8px}.ngx-record-editor__section-hint{margin-right:auto;font-size:12px;color:#6b7280}.ngx-record-editor__banner--warning{background:#fffbeb;border:1px solid #fef3c7;color:#b45309}.ngx-record-editor__rows{border:1px solid #f3f4f6;border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:6px}.ngx-record-editor__rows-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.ngx-record-editor__row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:#f9fafb}.ngx-record-editor__row-text{flex:1;font-size:13px;overflow-wrap:anywhere}.ngx-record-editor__row-btn{background:none;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;font-size:12px;padding:2px 8px}.ngx-record-editor__row-empty{font-size:12px;color:#6b7280;margin:0}.ngx-record-editor__drawer{border:1px solid #dbeafe;background:#f8fafc;border-radius:8px;padding:14px;display:flex;flex-direction:column;gap:12px}.ngx-record-editor__summary-panel{background:#f9fafb;border:1px solid #f3f4f6;border-radius:8px;padding:12px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}.ngx-record-editor__summary-item{display:flex;flex-direction:column;cursor:pointer;padding:6px 10px;border-radius:6px;transition:background .15s}.ngx-record-editor__summary-item:hover{background:#eff6ff}.ngx-record-editor__summary-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;font-weight:600}.ngx-record-editor__summary-val{font-size:14px;font-weight:600;color:#1f2937}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: DynamicFormComponent, selector: "ngx-dynamic-form", inputs: ["config", "rules", "initialData", "originalData", "userRoles", "language", "readonly", "readOnlyFields", "showInfoBanners", "loading", "error", "changeDebounceMs", "preview"], outputs: ["formSubmit", "formChange", "formReset", "activeTabChange", "saveRejected"] }, { kind: "component", type: DynamicFieldComponent, selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4933
5206
|
}
|
|
4934
5207
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DynamicRecordFormComponent, decorators: [{
|
|
4935
5208
|
type: Component,
|
|
4936
|
-
args: [{ selector: 'ngx-dynamic-record-form', standalone: true, imports: [CommonModule, ReactiveFormsModule, DynamicFormComponent, DynamicFieldComponent], template: "<div class=\"ngx-record-editor\">\n <!-- Record Header -->\n <div class=\"ngx-record-editor__header\">\n <div class=\"ngx-record-editor__title-group\">\n @if (profileImageUrl) {\n <img\n class=\"ngx-record-editor__avatar\"\n data-testid=\"header-profile-image\"\n [src]=\"profileImageUrl\"\n [alt]=\"recordTitle\"\n />\n } @else {\n <div class=\"ngx-record-editor__avatar\" data-testid=\"header-avatar-initial\">\n {{ avatarLetter }}\n </div>\n }\n <div>\n <h2 class=\"ngx-record-editor__title\" data-testid=\"record-title\">\n {{ recordTitle }}\n </h2>\n <span class=\"ngx-record-editor__subtitle\">Entity: {{ config.entity }}</span>\n </div>\n </div>\n\n <!-- Record-level status switch, outside section editing -->\n @if (headerToggleField; as toggleField) {\n <label class=\"ngx-record-editor__header-toggle\">\n <span>{{ formatFieldLabel(toggleField) }}</span>\n <input\n type=\"checkbox\"\n role=\"switch\"\n data-testid=\"header-toggle\"\n [checked]=\"headerToggleValue\"\n [disabled]=\"recordReadOnly\"\n [attr.aria-checked]=\"headerToggleValue\"\n (change)=\"toggleHeaderStatus()\"\n />\n <span class=\"ngx-record-editor__header-toggle-text\">{{ headerToggleValue ? 'Yes' : 'No' }}</span>\n </label>\n }\n </div>\n\n <!-- Dismissible info banners (rules `info` action) -->\n @for (banner of infoBanners; track banner.fieldId) {\n <div\n class=\"ngx-record-editor__banner ngx-record-editor__banner--info\"\n role=\"status\"\n [attr.data-testid]=\"'info-banner-' + banner.fieldId\"\n >\n <span>\u2139\uFE0F {{ banner.message }}</span>\n <button\n type=\"button\"\n class=\"ngx-record-editor__banner-dismiss\"\n [attr.data-testid]=\"'dismiss-' + banner.fieldId\"\n [attr.aria-label]=\"'Dismiss: ' + banner.message\"\n (click)=\"dismissInfoBanner(banner.fieldId)\"\n >\u2715</button>\n </div>\n }\n\n <!-- Modification baseline banner -->\n @if (isModified()) {\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--warning\">\n \uD83D\uDD12 Record modified from session baseline \u2014 unsaved changes pending.\n </div>\n }\n\n <!-- Summary Panel (showOnMinimize fields with interactive jump) -->\n @if (summaryFields().length > 0) {\n <div class=\"ngx-record-editor__summary-panel\" data-testid=\"summary-panel\">\n @for (sf of summaryFields(); track sf.id) {\n <div\n class=\"ngx-record-editor__summary-item\"\n [attr.data-testid]=\"'summary-' + sf.id\"\n (click)=\"jumpToField(sf.id)\"\n [title]=\"'Click to jump to ' + formatFieldLabel(sf)\"\n >\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(sf) }}</span>\n <span class=\"ngx-record-editor__summary-val\">{{ formatFieldValue(sf) }}</span>\n </div>\n }\n </div>\n }\n\n <!-- Section edit bar: one tab is edited at a time, and validated on its own -->\n @if (viewMode && !recordReadOnly) {\n <div class=\"ngx-record-editor__section-bar\">\n @if (isEditingActiveTab) {\n <span class=\"ngx-record-editor__section-hint\">Editing this section</span>\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-section\" (click)=\"cancelSection()\">\n Cancel\n </button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-section\" (click)=\"saveSection()\">\n Save section\n </button>\n } @else {\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"edit-section\" (click)=\"editSection()\">\n Edit section\n </button>\n }\n </div>\n }\n\n @if (Object.keys(sectionErrors()).length > 0) {\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--error\" role=\"alert\" data-testid=\"section-errors\">\n @for (key of Object.keys(sectionErrors()); track key) {\n <div>\u26A0\uFE0F {{ sectionErrors()[key] }}</div>\n }\n </div>\n }\n\n <!-- Array rows: listed here, edited in the drawer below -->\n @for (arrayField of arrayFieldsForActiveTab; track arrayField.id) {\n <div class=\"ngx-record-editor__rows\" [attr.data-testid]=\"'rows-' + arrayField.id\">\n <div class=\"ngx-record-editor__rows-head\">\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(arrayField) }}</span>\n @if (!sectionReadOnly) {\n <button\n type=\"button\"\n class=\"ngx-form__reset\"\n [attr.data-testid]=\"'add-row-' + arrayField.id\"\n (click)=\"openAddRow(arrayField)\"\n >+ Row</button>\n }\n </div>\n\n @for (row of rowsOf(arrayField); track $index) {\n <div class=\"ngx-record-editor__row\" [attr.data-testid]=\"'row-' + arrayField.id + '-' + $index\">\n <span class=\"ngx-record-editor__row-text\">{{ rowSummary(arrayField, row) }}</span>\n @if (!sectionReadOnly) {\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'edit-row-' + arrayField.id + '-' + $index\"\n (click)=\"openEditRow(arrayField, $index)\">Edit</button>\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'delete-row-' + arrayField.id + '-' + $index\"\n (click)=\"deleteRow(arrayField, $index)\">Delete</button>\n }\n </div>\n } @empty {\n <p class=\"ngx-record-editor__row-empty\">No rows yet.</p>\n }\n </div>\n }\n\n <!--\n Row drawer. Deliberately outside the tab panel: building a row's FormGroup inside the\n tab view races the tab's own initialisation (the reference hit this with FormArray).\n -->\n @if (inlineRowField(); as rowField) {\n <div class=\"ngx-record-editor__drawer\" data-testid=\"row-drawer\" [formGroup]=\"inlineRowForm!\">\n <div class=\"ngx-record-editor__rows-head\">\n <strong>{{ inlineRowIndex() === null ? 'Add' : 'Edit' }} {{ formatFieldLabel(rowField) }} row</strong>\n </div>\n\n <div class=\"ngx-form__panel\">\n @for (child of rowField.children ?? []; track child.id) {\n <ngx-dynamic-field\n [field]=\"child\"\n [control]=\"inlineRowForm!.get(child.id)!\"\n [config]=\"config\"\n [language]=\"language\"\n [readonly]=\"false\"\n [userRoles]=\"userRoles\"\n [style.gridColumn]=\"'span 12'\"\n />\n }\n </div>\n\n <div class=\"ngx-record-editor__section-bar\">\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-row\" (click)=\"cancelRow()\">Cancel</button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-row\" (click)=\"saveRow()\">Save row</button>\n </div>\n </div>\n }\n\n <!-- Main Dynamic Form -->\n <ngx-dynamic-form\n [config]=\"config\"\n [rules]=\"rules\"\n [initialData]=\"initialData\"\n [originalData]=\"originalBaseline()\"\n [userRoles]=\"userRoles\"\n [language]=\"language\"\n [readonly]=\"sectionReadOnly\"\n [readOnlyFields]=\"readOnlyFields\"\n [showInfoBanners]=\"false\"\n [loading]=\"loading\"\n [error]=\"error\"\n (formChange)=\"onFormChange($event)\"\n (formSubmit)=\"onFormSubmit($event)\"\n (formReset)=\"onFormReset()\"\n (activeTabChange)=\"onActiveTabChange($event)\"\n />\n</div>\n", styles: [".ngx-record-editor{display:flex;flex-direction:column;gap:16px;background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:20px;box-shadow:0 4px 6px -1px #0000000d}.ngx-record-editor__header{display:flex;align-items:center;justify-content:space-between;padding-bottom:16px;border-bottom:1px solid #f3f4f6}.ngx-record-editor__title-group{display:flex;align-items:center;gap:12px}.ngx-record-editor__avatar{width:48px;height:48px;border-radius:50%;object-fit:cover;background:#e0e7ff;color:#4f46e5;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px}.ngx-record-editor__title{font-size:20px;font-weight:700;color:#111827;margin:0}.ngx-record-editor__subtitle{font-size:13px;color:#6b7280}.ngx-record-editor__header-toggle{display:flex;align-items:center;gap:8px;font-size:13px;color:#374151}.ngx-record-editor__header-toggle-text{font-weight:600}.ngx-record-editor__banner{padding:10px 14px;border-radius:8px;font-size:13px;font-weight:500}.ngx-record-editor__banner--info{background:#eff6ff;border:1px solid #dbeafe;color:#1d4ed8;display:flex;align-items:center;justify-content:space-between;gap:12px}.ngx-record-editor__banner-dismiss{background:none;border:none;cursor:pointer;color:inherit;font-size:14px;line-height:1;padding:2px 6px;border-radius:4px}.ngx-record-editor__banner-dismiss:hover{background:#0000000f}.ngx-record-editor__banner--error{background:#fef2f2;border:1px solid #fecaca;color:#b91c1c}.ngx-record-editor__section-bar{display:flex;align-items:center;justify-content:flex-end;gap:8px}.ngx-record-editor__section-hint{margin-right:auto;font-size:12px;color:#6b7280}.ngx-record-editor__banner--warning{background:#fffbeb;border:1px solid #fef3c7;color:#b45309}.ngx-record-editor__rows{border:1px solid #f3f4f6;border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:6px}.ngx-record-editor__rows-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.ngx-record-editor__row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:#f9fafb}.ngx-record-editor__row-text{flex:1;font-size:13px;overflow-wrap:anywhere}.ngx-record-editor__row-btn{background:none;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;font-size:12px;padding:2px 8px}.ngx-record-editor__row-empty{font-size:12px;color:#6b7280;margin:0}.ngx-record-editor__drawer{border:1px solid #dbeafe;background:#f8fafc;border-radius:8px;padding:14px;display:flex;flex-direction:column;gap:12px}.ngx-record-editor__summary-panel{background:#f9fafb;border:1px solid #f3f4f6;border-radius:8px;padding:12px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}.ngx-record-editor__summary-item{display:flex;flex-direction:column;cursor:pointer;padding:6px 10px;border-radius:6px;transition:background .15s}.ngx-record-editor__summary-item:hover{background:#eff6ff}.ngx-record-editor__summary-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;font-weight:600}.ngx-record-editor__summary-val{font-size:14px;font-weight:600;color:#1f2937}\n"] }]
|
|
5209
|
+
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-dynamic-record-form', standalone: true, imports: [CommonModule, ReactiveFormsModule, DynamicFormComponent, DynamicFieldComponent], template: "<div class=\"ngx-record-editor\">\n <!-- Record Header -->\n <div class=\"ngx-record-editor__header\">\n <div class=\"ngx-record-editor__title-group\">\n @if (profileImageUrl) {\n <img\n class=\"ngx-record-editor__avatar\"\n data-testid=\"header-profile-image\"\n [src]=\"profileImageUrl\"\n [alt]=\"recordTitle\"\n />\n } @else {\n <div class=\"ngx-record-editor__avatar\" data-testid=\"header-avatar-initial\">\n {{ avatarLetter }}\n </div>\n }\n <div>\n <h2 class=\"ngx-record-editor__title\" data-testid=\"record-title\">\n {{ recordTitle }}\n </h2>\n <span class=\"ngx-record-editor__subtitle\">Entity: {{ config.entity }}</span>\n </div>\n </div>\n\n <!-- Record-level status switch, outside section editing -->\n @if (headerToggleField; as toggleField) {\n <label class=\"ngx-record-editor__header-toggle\">\n <span>{{ formatFieldLabel(toggleField) }}</span>\n <input\n type=\"checkbox\"\n role=\"switch\"\n data-testid=\"header-toggle\"\n [checked]=\"headerToggleValue\"\n [disabled]=\"recordReadOnly\"\n [attr.aria-checked]=\"headerToggleValue\"\n (change)=\"toggleHeaderStatus()\"\n />\n <span class=\"ngx-record-editor__header-toggle-text\">{{ headerToggleValue ? 'Yes' : 'No' }}</span>\n </label>\n }\n </div>\n\n <!-- Dismissible info banners (rules `info` action) -->\n @for (banner of infoBanners; track banner.fieldId) {\n <div\n class=\"ngx-record-editor__banner ngx-record-editor__banner--info\"\n role=\"status\"\n [attr.data-testid]=\"'info-banner-' + banner.fieldId\"\n >\n <span>\u2139\uFE0F {{ banner.message }}</span>\n <button\n type=\"button\"\n class=\"ngx-record-editor__banner-dismiss\"\n [attr.data-testid]=\"'dismiss-' + banner.fieldId\"\n [attr.aria-label]=\"'Dismiss: ' + banner.message\"\n (click)=\"dismissInfoBanner(banner.fieldId)\"\n >\u2715</button>\n </div>\n }\n\n <!-- Modification baseline banner -->\n @if (isModified()) {\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--warning\">\n \uD83D\uDD12 Record modified from session baseline \u2014 unsaved changes pending.\n </div>\n }\n\n <!-- Summary Panel (showOnMinimize fields with interactive jump) -->\n @if (summaryFields().length > 0) {\n <div class=\"ngx-record-editor__summary-panel\" data-testid=\"summary-panel\">\n @for (sf of summaryFields(); track sf.id) {\n <div\n class=\"ngx-record-editor__summary-item\"\n [attr.data-testid]=\"'summary-' + sf.id\"\n (click)=\"jumpToField(sf.id)\"\n [title]=\"'Click to jump to ' + formatFieldLabel(sf)\"\n >\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(sf) }}</span>\n <span class=\"ngx-record-editor__summary-val\">{{ formatFieldValue(sf) }}</span>\n </div>\n }\n </div>\n }\n\n <!-- Section edit bar: one tab is edited at a time, and validated on its own -->\n @if (viewMode && !recordReadOnly) {\n <div class=\"ngx-record-editor__section-bar\">\n @if (isEditingActiveTab) {\n <span class=\"ngx-record-editor__section-hint\">Editing this section</span>\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-section\" (click)=\"cancelSection()\">\n Cancel\n </button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-section\" (click)=\"saveSection()\">\n Save section\n </button>\n } @else {\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"edit-section\" (click)=\"editSection()\">\n Edit section\n </button>\n }\n </div>\n }\n\n @if (Object.keys(sectionErrors()).length > 0) {\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--error\" role=\"alert\" data-testid=\"section-errors\">\n @for (key of Object.keys(sectionErrors()); track key) {\n <div>\u26A0\uFE0F {{ sectionErrors()[key] }}</div>\n }\n </div>\n }\n\n <!-- Array rows: listed here, edited in the drawer below -->\n @for (arrayField of arrayFieldsForActiveTab; track arrayField.id) {\n <div class=\"ngx-record-editor__rows\" [attr.data-testid]=\"'rows-' + arrayField.id\">\n <div class=\"ngx-record-editor__rows-head\">\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(arrayField) }}</span>\n @if (!sectionReadOnly) {\n <button\n type=\"button\"\n class=\"ngx-form__reset\"\n [attr.data-testid]=\"'add-row-' + arrayField.id\"\n (click)=\"openAddRow(arrayField)\"\n >+ Row</button>\n }\n </div>\n\n @for (row of rowsOf(arrayField); track $index) {\n <div class=\"ngx-record-editor__row\" [attr.data-testid]=\"'row-' + arrayField.id + '-' + $index\">\n <span class=\"ngx-record-editor__row-text\">{{ rowSummary(arrayField, row) }}</span>\n @if (!sectionReadOnly) {\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'edit-row-' + arrayField.id + '-' + $index\"\n (click)=\"openEditRow(arrayField, $index)\">Edit</button>\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'delete-row-' + arrayField.id + '-' + $index\"\n (click)=\"deleteRow(arrayField, $index)\">Delete</button>\n }\n </div>\n } @empty {\n <p class=\"ngx-record-editor__row-empty\">No rows yet.</p>\n }\n </div>\n }\n\n <!--\n Row drawer. Deliberately outside the tab panel: building a row's FormGroup inside the\n tab view races the tab's own initialisation (the reference hit this with FormArray).\n -->\n @if (inlineRowField(); as rowField) {\n <div class=\"ngx-record-editor__drawer\" data-testid=\"row-drawer\" [formGroup]=\"inlineRowForm!\">\n <div class=\"ngx-record-editor__rows-head\">\n <strong>{{ inlineRowIndex() === null ? 'Add' : 'Edit' }} {{ formatFieldLabel(rowField) }} row</strong>\n </div>\n\n <div class=\"ngx-form__panel\">\n @for (child of rowField.children ?? []; track child.id) {\n <ngx-dynamic-field\n [field]=\"child\"\n [control]=\"inlineRowForm!.get(child.id)!\"\n [config]=\"config\"\n [language]=\"language\"\n [readonly]=\"false\"\n [userRoles]=\"userRoles\"\n [style.gridColumn]=\"'span 12'\"\n />\n }\n </div>\n\n <div class=\"ngx-record-editor__section-bar\">\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-row\" (click)=\"cancelRow()\">Cancel</button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-row\" (click)=\"saveRow()\">Save row</button>\n </div>\n </div>\n }\n\n <!-- Main Dynamic Form -->\n <ngx-dynamic-form\n [config]=\"config\"\n [rules]=\"rules\"\n [initialData]=\"initialData\"\n [originalData]=\"originalBaseline()\"\n [userRoles]=\"userRoles\"\n [language]=\"language\"\n [readonly]=\"sectionReadOnly\"\n [readOnlyFields]=\"readOnlyFields\"\n [showInfoBanners]=\"false\"\n [loading]=\"loading\"\n [error]=\"error\"\n (formChange)=\"onFormChange($event)\"\n (formSubmit)=\"onFormSubmit($event)\"\n (formReset)=\"onFormReset()\"\n (activeTabChange)=\"onActiveTabChange($event)\"\n />\n</div>\n", styles: [".ngx-record-editor{display:flex;flex-direction:column;gap:16px;background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:20px;box-shadow:0 4px 6px -1px #0000000d}.ngx-record-editor__header{display:flex;align-items:center;justify-content:space-between;padding-bottom:16px;border-bottom:1px solid #f3f4f6}.ngx-record-editor__title-group{display:flex;align-items:center;gap:12px}.ngx-record-editor__avatar{width:48px;height:48px;border-radius:50%;object-fit:cover;background:#e0e7ff;color:#4f46e5;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px}.ngx-record-editor__title{font-size:20px;font-weight:700;color:#111827;margin:0}.ngx-record-editor__subtitle{font-size:13px;color:#6b7280}.ngx-record-editor__header-toggle{display:flex;align-items:center;gap:8px;font-size:13px;color:#374151}.ngx-record-editor__header-toggle-text{font-weight:600}.ngx-record-editor__banner{padding:10px 14px;border-radius:8px;font-size:13px;font-weight:500}.ngx-record-editor__banner--info{background:#eff6ff;border:1px solid #dbeafe;color:#1d4ed8;display:flex;align-items:center;justify-content:space-between;gap:12px}.ngx-record-editor__banner-dismiss{background:none;border:none;cursor:pointer;color:inherit;font-size:14px;line-height:1;padding:2px 6px;border-radius:4px}.ngx-record-editor__banner-dismiss:hover{background:#0000000f}.ngx-record-editor__banner--error{background:#fef2f2;border:1px solid #fecaca;color:#b91c1c}.ngx-record-editor__section-bar{display:flex;align-items:center;justify-content:flex-end;gap:8px}.ngx-record-editor__section-hint{margin-right:auto;font-size:12px;color:#6b7280}.ngx-record-editor__banner--warning{background:#fffbeb;border:1px solid #fef3c7;color:#b45309}.ngx-record-editor__rows{border:1px solid #f3f4f6;border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:6px}.ngx-record-editor__rows-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.ngx-record-editor__row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:#f9fafb}.ngx-record-editor__row-text{flex:1;font-size:13px;overflow-wrap:anywhere}.ngx-record-editor__row-btn{background:none;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;font-size:12px;padding:2px 8px}.ngx-record-editor__row-empty{font-size:12px;color:#6b7280;margin:0}.ngx-record-editor__drawer{border:1px solid #dbeafe;background:#f8fafc;border-radius:8px;padding:14px;display:flex;flex-direction:column;gap:12px}.ngx-record-editor__summary-panel{background:#f9fafb;border:1px solid #f3f4f6;border-radius:8px;padding:12px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}.ngx-record-editor__summary-item{display:flex;flex-direction:column;cursor:pointer;padding:6px 10px;border-radius:6px;transition:background .15s}.ngx-record-editor__summary-item:hover{background:#eff6ff}.ngx-record-editor__summary-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;font-weight:600}.ngx-record-editor__summary-val{font-size:14px;font-weight:600;color:#1f2937}\n"] }]
|
|
4937
5210
|
}], propDecorators: { config: [{
|
|
4938
5211
|
type: Input
|
|
4939
5212
|
}], rules: [{
|
|
@@ -4975,5 +5248,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
4975
5248
|
* Generated bundle index. Do not edit.
|
|
4976
5249
|
*/
|
|
4977
5250
|
|
|
4978
|
-
export { ASYNC_VALIDATOR_REGISTRY, ArrayFieldComponent, BooleanFieldComponent, COMMON_MODULES_REGISTRY, CONFIG_SOURCE, CascadeDataService, CheckboxFieldComponent, ConfigSourceService, CurrencyFieldComponent, DEFAULT_VALIDATION_MESSAGES, DateFieldComponent, DropdownFieldComponent, DynamicFieldComponent, DynamicFormComponent, DynamicRecordFormComponent, ENTITY_REF_CACHE_STORE, ENTITY_REF_REGISTRY, EmailFieldComponent, EntityRefFieldComponent, EntityRefRegistryService, EntityRefSelectionService, EntityReferenceService, FIELD_TYPE_REGISTRY, FIELD_TYPE_SETS, FieldRegistryService, FileFieldComponent, GroupFieldComponent, HOOK_REGISTRY, HookRegistryService, ImageFieldComponent, InMemoryEntityRefCacheStore, LOOKUP_REGISTRY, LookupRegistryService, MASKED_ROLES, MonthYearFieldComponent, MultiSelectFieldComponent, NumberFieldComponent, PasswordFieldComponent, RECORD_MIGRATIONS, RadioFieldComponent, RbacService, RulesEvaluationService, SYSTEM_DEFAULT_CAN_EDIT, TextFieldComponent, TextareaFieldComponent, UPLOAD_HANDLER, VALIDATION_MESSAGES, VALIDATOR_REGISTRY, ValidationMessagesService, ValidatorRegistryService, builtInFieldTypes, provideBuiltInFieldTypes, provideFieldTypes, provideNgxDynamicEntity, refreshChoiceOptions };
|
|
5251
|
+
export { ASYNC_VALIDATOR_REGISTRY, ArrayFieldComponent, BooleanFieldComponent, COMMON_MODULES_REGISTRY, CONFIG_SOURCE, CascadeDataService, CheckboxFieldComponent, ConfigSourceService, CurrencyFieldComponent, DEFAULT_VALIDATION_MESSAGES, DateFieldComponent, DateTimeFieldComponent, DropdownFieldComponent, DynamicFieldComponent, DynamicFormComponent, DynamicRecordFormComponent, ENTITY_REF_CACHE_STORE, ENTITY_REF_REGISTRY, EmailFieldComponent, EntityRefFieldComponent, EntityRefRegistryService, EntityRefSelectionService, EntityReferenceService, FIELD_TYPE_REGISTRY, FIELD_TYPE_SETS, FieldRegistryService, FileFieldComponent, GroupFieldComponent, HOOK_REGISTRY, HookRegistryService, ImageFieldComponent, InMemoryEntityRefCacheStore, LOOKUP_REGISTRY, LookupRegistryService, MASKED_ROLES, MonthYearFieldComponent, MultiSelectFieldComponent, NumberFieldComponent, PasswordFieldComponent, RECORD_MIGRATIONS, RadioFieldComponent, RbacService, RulesEvaluationService, SYSTEM_DEFAULT_CAN_EDIT, TextFieldComponent, TextareaFieldComponent, TimeFieldComponent, UPLOAD_HANDLER, VALIDATION_MESSAGES, VALIDATOR_REGISTRY, ValidationMessagesService, ValidatorRegistryService, builtInFieldTypes, provideBuiltInFieldTypes, provideFieldTypes, provideNgxDynamicEntity, refreshChoiceOptions };
|
|
4979
5252
|
//# sourceMappingURL=ngx-dynamic-entity.mjs.map
|