ngx-dynamic-entity 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +129 -139
  2. package/esm2022/lib/field-types/array-field.component.mjs +101 -101
  3. package/esm2022/lib/field-types/boolean-field.component.mjs +59 -59
  4. package/esm2022/lib/field-types/checkbox-field.component.mjs +45 -45
  5. package/esm2022/lib/field-types/currency-field.component.mjs +51 -51
  6. package/esm2022/lib/field-types/date-field.component.mjs +43 -43
  7. package/esm2022/lib/field-types/dropdown-field.component.mjs +81 -81
  8. package/esm2022/lib/field-types/email-field.component.mjs +61 -61
  9. package/esm2022/lib/field-types/entity-ref-field.component.mjs +67 -67
  10. package/esm2022/lib/field-types/file-field.component.mjs +85 -85
  11. package/esm2022/lib/field-types/group-field.component.mjs +47 -47
  12. package/esm2022/lib/field-types/image-field.component.mjs +89 -89
  13. package/esm2022/lib/field-types/month-year-field.component.mjs +77 -77
  14. package/esm2022/lib/field-types/multi-select-field.component.mjs +63 -63
  15. package/esm2022/lib/field-types/number-field.component.mjs +65 -65
  16. package/esm2022/lib/field-types/password-field.component.mjs +65 -65
  17. package/esm2022/lib/field-types/radio-field.component.mjs +63 -63
  18. package/esm2022/lib/field-types/text-field.component.mjs +59 -59
  19. package/esm2022/lib/field-types/textarea-field.component.mjs +45 -45
  20. package/esm2022/lib/form/dynamic-field/dynamic-field.component.mjs +1 -1
  21. package/esm2022/lib/form/dynamic-form.component.mjs +183 -10
  22. package/esm2022/lib/form/dynamic-record-form.component.mjs +36 -11
  23. package/esm2022/lib/providers/provide-field-types.mjs +1 -1
  24. package/esm2022/lib/providers/provide-ngx-dynamic-entity.mjs +3 -2
  25. package/esm2022/lib/services/cascade-data.service.mjs +1 -1
  26. package/esm2022/lib/services/entity-ref-cache.mjs +1 -1
  27. package/esm2022/lib/services/entity-ref-registry.service.mjs +1 -1
  28. package/esm2022/lib/services/entity-ref-selection.service.mjs +1 -1
  29. package/esm2022/lib/services/entity-reference.service.mjs +1 -1
  30. package/esm2022/lib/services/field-registry.service.mjs +1 -1
  31. package/esm2022/lib/services/file-upload.service.mjs +1 -1
  32. package/esm2022/lib/services/rbac.service.mjs +1 -1
  33. package/esm2022/lib/services/validator-registry.service.mjs +12 -1
  34. package/esm2022/lib/tokens/injection-tokens.mjs +17 -1
  35. package/esm2022/public-api.mjs +18 -2
  36. package/fesm2022/ngx-dynamic-entity.mjs +1392 -1166
  37. package/fesm2022/ngx-dynamic-entity.mjs.map +1 -1
  38. package/lib/field-types/dropdown-field.component.d.ts +1 -1
  39. package/lib/field-types/entity-ref-field.component.d.ts +1 -1
  40. package/lib/field-types/multi-select-field.component.d.ts +1 -1
  41. package/lib/field-types/radio-field.component.d.ts +1 -1
  42. package/lib/form/dynamic-form.component.d.ts +93 -6
  43. package/lib/form/dynamic-record-form.component.d.ts +13 -2
  44. package/lib/providers/provide-ngx-dynamic-entity.d.ts +8 -2
  45. package/lib/tokens/injection-tokens.d.ts +24 -2
  46. package/package.json +17 -18
  47. package/public-api.d.ts +10 -0
@@ -14,7 +14,7 @@ export declare class DropdownFieldComponent {
14
14
  readonly: boolean;
15
15
  masked: boolean;
16
16
  /** Inline `options`, or the field's named list resolved through the registry (§6.3). */
17
- readonly options: import("@angular/core").WritableSignal<import("@dynamic-entity/core").LocalizedText[]>;
17
+ readonly options: import("@angular/core").WritableSignal<DropdownOption[]>;
18
18
  readonly compareFn: (o1: any, o2: any) => boolean;
19
19
  isObjectVal(option: any): boolean;
20
20
  get label(): string;
@@ -1,6 +1,6 @@
1
1
  import { OnInit } from '@angular/core';
2
2
  import { AbstractControl } from '@angular/forms';
3
- import type { NestedFieldConfig, ReferenceOption } from '@dynamic-entity/core';
3
+ import type { NestedFieldConfig } from '@dynamic-entity/core';
4
4
  import * as i0 from "@angular/core";
5
5
  /**
6
6
  * EntityRefFieldComponent — a select populated from a consumer-registered loader.
@@ -14,7 +14,7 @@ export declare class MultiSelectFieldComponent {
14
14
  readonly: boolean;
15
15
  masked: boolean;
16
16
  /** Inline `options`, or the field's named list resolved through the registry (§6.3). */
17
- readonly options: import("@angular/core").WritableSignal<import("@dynamic-entity/core").LocalizedText[]>;
17
+ readonly options: import("@angular/core").WritableSignal<DropdownOption[]>;
18
18
  readonly compareFn: (o1: any, o2: any) => boolean;
19
19
  isObjectVal(option: any): boolean;
20
20
  get label(): string;
@@ -15,7 +15,7 @@ export declare class RadioFieldComponent {
15
15
  readonly: boolean;
16
16
  masked: boolean;
17
17
  /** Inline `options`, or the field's named list resolved through the registry (§6.3). */
18
- readonly options: import("@angular/core").WritableSignal<import("@dynamic-entity/core").LocalizedText[]>;
18
+ readonly options: import("@angular/core").WritableSignal<DropdownOption[]>;
19
19
  get label(): string;
20
20
  getOptStoredVal(option: any): any;
21
21
  /**
@@ -1,6 +1,7 @@
1
1
  import { EventEmitter, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
2
2
  import { AbstractControl, FormArray, FormGroup } from '@angular/forms';
3
3
  import type { EntityFormConfig, FormRule, NestedFieldConfig, NestedTabConfig } from '@dynamic-entity/core';
4
+ import { RbacService } from '../services/rbac.service';
4
5
  import * as i0 from "@angular/core";
5
6
  /**
6
7
  * DynamicFormComponent — the main form component.
@@ -49,6 +50,7 @@ export declare class DynamicFormComponent implements OnInit, OnChanges, OnDestro
49
50
  private readonly rulesEvaluation;
50
51
  private readonly entityRefSelection;
51
52
  private readonly commonModulesRegistry;
53
+ private readonly migrations;
52
54
  protected readonly Object: ObjectConstructor;
53
55
  readonly activeTab: import("@angular/core").WritableSignal<string>;
54
56
  readonly activeSubTab: import("@angular/core").WritableSignal<string>;
@@ -64,7 +66,7 @@ export declare class DynamicFormComponent implements OnInit, OnChanges, OnDestro
64
66
  /** Previous values, for detecting `patchOnTrue` false→true transitions. */
65
67
  private previousValues;
66
68
  handleKeyboardEvent(event: KeyboardEvent): void;
67
- readonly ruleResult: import("@angular/core").Signal<import("@dynamic-entity/core").RuleEvaluationResult>;
69
+ readonly ruleResult: import("@angular/core").Signal<RuleEvaluationResult>;
68
70
  /**
69
71
  * Critical fields whose value differs from the session baseline.
70
72
  * These drive the deferred `VALUE_CHANGED` banner — a critical edit is announced once,
@@ -78,12 +80,49 @@ export declare class DynamicFormComponent implements OnInit, OnChanges, OnDestro
78
80
  get activeSubTabConfig(): NestedTabConfig | null;
79
81
  get activeTabModuleComponent(): any | null;
80
82
  get fieldsForActiveTab(): NestedFieldConfig[];
81
- get permissions(): {
82
- canView: boolean;
83
- canEdit: boolean;
84
- canDelete: boolean;
85
- };
83
+ /**
84
+ * Entity-level permissions for the current roles.
85
+ *
86
+ * Cached rather than recomputed: this is read from the template several times per change
87
+ * detection pass, and a fresh object each time is both wasted allocation and a new
88
+ * identity for anything comparing by reference. Invalidated in `ngOnChanges` when
89
+ * `config` or `userRoles` change, which are its only inputs.
90
+ */
91
+ get permissions(): ReturnType<RbacService['getPermissions']>;
92
+ private permissionsCache;
93
+ /**
94
+ * Whether the current roles may see this record at all.
95
+ *
96
+ * `permissions.view` used to be computed and thrown away — a user whose roles failed it
97
+ * still received the complete form with every value in the DOM. It is honoured now.
98
+ *
99
+ * As with masking, this is presentational: it stops the browser rendering data, it does
100
+ * not stop the data reaching the browser. Authorize on the server.
101
+ */
102
+ get canView(): boolean;
103
+ /**
104
+ * Whether the current roles may delete this record.
105
+ *
106
+ * The library ships no delete affordance, so this is surfaced for the consumer to gate
107
+ * their own — exposed rather than dropped, because a permission the schema declares
108
+ * should be answerable.
109
+ */
110
+ get canDelete(): boolean;
86
111
  get canSubmit(): boolean;
112
+ /**
113
+ * Errors raised by `validation` rules, keyed by the target they point at.
114
+ *
115
+ * A rule can declare the record invalid independently of Angular's validators, so form
116
+ * validity alone is not the whole picture. `DynamicRecordFormComponent.saveSection()`
117
+ * has always honoured these; `submit()` did not, which meant the same rule blocked one
118
+ * save path and merely painted a banner on the other — and anything wiring
119
+ * `(formSubmit)` to persistence wrote records the rules engine had already rejected.
120
+ */
121
+ get ruleValidationErrors(): Record<string, string>;
122
+ /** True when a `validation` rule is currently blocking submission. */
123
+ get hasRuleErrors(): boolean;
124
+ /** Single source of truth for both the submit guard and the button's disabled state. */
125
+ get submitBlocked(): boolean;
87
126
  ngOnInit(): void;
88
127
  ngOnChanges(changes: SimpleChanges): void;
89
128
  ngOnDestroy(): void;
@@ -113,7 +152,31 @@ export declare class DynamicFormComponent implements OnInit, OnChanges, OnDestro
113
152
  */
114
153
  createArrayRow(field: NestedFieldConfig, value?: unknown): AbstractControl;
115
154
  private buildArrayRow;
155
+ /**
156
+ * Bring a record up to the config's `version` before it reaches the form.
157
+ *
158
+ * Done here because this is the one place a record enters the renderer, so a consumer
159
+ * cannot forget it. Migration is a no-op unless the consumer registered steps *and* the
160
+ * record is behind — an unstamped record is deliberately left alone (see `migrateRecord`).
161
+ *
162
+ * A missing or broken step throws from core. That is not swallowed: rendering a
163
+ * half-understood record would write it back in a shape matching neither schema.
164
+ */
165
+ private upgradeRecord;
116
166
  private patchForm;
167
+ /**
168
+ * A record is nested by tab id (`{ tabId: { fieldId: value } }`) unless the tab sets
169
+ * `flatData: true`. Handing a flat record to a nested tab finds nothing, so the fields
170
+ * stay empty — with no error and no clue. That silent miss is the most expensive way to
171
+ * lose an hour with this library, so name it in dev builds.
172
+ *
173
+ * Only top-level keys that match a known field id are reported: anything else is assumed
174
+ * to be the consumer's own record metadata (ids, timestamps, `_configVersion`) and is not
175
+ * our business.
176
+ */
177
+ private warnUnconsumedInitialData;
178
+ /** Warn once per key per form instance — patchForm re-runs on every initialData change. */
179
+ private readonly warnedUnconsumedKeys;
117
180
  /** Assemble full nested record from per-tab FormGroups, respecting flatData, refererField & arrays. */
118
181
  extractRecord(): Record<string, any>;
119
182
  private flattenFormValues;
@@ -130,6 +193,30 @@ export declare class DynamicFormComponent implements OnInit, OnChanges, OnDestro
130
193
  private runPatchOnTrue;
131
194
  /** Prefer a control declared on the configured target tab; fall back to a top-level control. */
132
195
  private resolveTargetControl;
196
+ /**
197
+ * The fields the render filter operates on — a tab's own fields, at every tab depth.
198
+ * Deliberately not `allFields()`: `group` and `array` children are shown or hidden with
199
+ * their parent, and toggling a child's state independently would fight the parent's.
200
+ */
201
+ private tabFields;
202
+ /**
203
+ * Keep a hidden field's control out of form validity.
204
+ *
205
+ * A field hidden by a rule or a `showWhen` condition is filtered out of the render, but
206
+ * its validators stay attached — so a hidden *required* field holds `form.invalid` true
207
+ * forever, permanently disabling Save with nothing on screen to explain why.
208
+ *
209
+ * Disabling is the fix rather than stripping validators: Angular excludes disabled
210
+ * controls from validity, and the field's own validators survive intact for when it
211
+ * comes back. Values are untouched — `flattenFormValues` and `extractRecord` both read
212
+ * `control.value`, which a disabled control still carries, so rule evaluation and the
213
+ * emitted record see exactly what they saw before. That also keeps this from feeding
214
+ * back on itself: hiding a field cannot change the values the rules are evaluated over.
215
+ *
216
+ * The visibility predicate is the same one `fieldsForActiveTab` filters with, so what is
217
+ * rendered and what counts toward validity cannot drift apart.
218
+ */
219
+ private syncHiddenFieldState;
133
220
  /** Every field in the config, including `group`/`array` children. */
134
221
  private allFields;
135
222
  /** An empty `originalData` counts as "not supplied" — fall back to the captured baseline. */
@@ -70,7 +70,7 @@ export declare class DynamicRecordFormComponent implements OnChanges {
70
70
  * building a row's FormGroup inside the tab view races the tab's own initialisation, so
71
71
  * the row form is kept detached and only pushed into the FormArray on save.
72
72
  */
73
- readonly inlineRowField: import("@angular/core").WritableSignal<NestedFieldConfig>;
73
+ readonly inlineRowField: import("@angular/core").WritableSignal<any>;
74
74
  readonly inlineRowIndex: import("@angular/core").WritableSignal<number>;
75
75
  inlineRowForm: FormGroup | null;
76
76
  /** `array` fields on the tab currently shown, so the template can list their rows. */
@@ -153,7 +153,18 @@ export declare class DynamicRecordFormComponent implements OnChanges {
153
153
  toggleHeaderStatus(): void;
154
154
  /** Locate a field's control wherever it lives, since header fields may be on any tab. */
155
155
  private controlFor;
156
- /** A field's current value, read from the record so it is stable during render. */
156
+ /**
157
+ * A field's current value, read from the record so it is stable during render.
158
+ *
159
+ * Resolved through `getTabData` so it honours exactly the nesting contract the form
160
+ * patches with — `{ tabId: { fieldId } }`, or the record root when the tab sets
161
+ * `flatData`. Reading the record flat instead is not a shortcut: it makes the summary
162
+ * display values the form never loaded, which reads as "the data is there" while every
163
+ * control behind it is empty.
164
+ *
165
+ * Walks sub-tabs as well: a summary, header, or profile field may sit on any tab at any
166
+ * depth.
167
+ */
157
168
  private fieldValue;
158
169
  readonly isModified: import("@angular/core").Signal<boolean>;
159
170
  readonly summaryFields: import("@angular/core").Signal<NestedFieldConfig[]>;
@@ -1,5 +1,6 @@
1
1
  import { EnvironmentProviders, Type } from '@angular/core';
2
- import type { EntityReferenceLoader, LookupListSource } from '@dynamic-entity/core';
2
+ import type { EntityReferenceLoader, LookupListSource, RecordMigration } from '@dynamic-entity/core';
3
+ import { type HookFn } from '../tokens/injection-tokens';
3
4
  export interface NgxDynamicEntityConfig {
4
5
  /** Roles that see masked field values as XXXXXXXXX */
5
6
  maskedRoles?: string[];
@@ -19,7 +20,12 @@ export interface NgxDynamicEntityConfig {
19
20
  /** Custom validator functions keyed by validator name */
20
21
  validators?: Record<string, any>;
21
22
  /** Hook functions keyed by hook name */
22
- hooks?: Record<string, Function>;
23
+ hooks?: Record<string, HookFn>;
24
+ /**
25
+ * Ordered steps that upgrade a saved record when `EntityFormConfig.version` moves ahead
26
+ * of the record's `_configVersion`. Applied to `initialData` before the form is patched.
27
+ */
28
+ migrations?: RecordMigration[];
23
29
  }
24
30
  /**
25
31
  * Provider function for ngx-dynamic-entity.
@@ -1,6 +1,5 @@
1
1
  import { InjectionToken } from '@angular/core';
2
2
  import type { Type } from '@angular/core';
3
- import type { CommonModuleEntry, EntityReferenceLoader, FileUploadHandler, LookupListSource } from '@dynamic-entity/core';
4
3
  import { type EntityRefCacheStore } from '../services/entity-ref-cache';
5
4
  /** Roles that see XXXXXXXXX for masked fields (ADR-003) */
6
5
  export declare const MASKED_ROLES: InjectionToken<string[]>;
@@ -43,10 +42,33 @@ export declare const ENTITY_REF_REGISTRY: InjectionToken<Map<string, EntityRefer
43
42
  * })
44
43
  */
45
44
  export declare const LOOKUP_REGISTRY: InjectionToken<Map<string, LookupListSource>>;
45
+ /**
46
+ * Ordered steps that move a saved record forward when a config's `version` increases.
47
+ *
48
+ * Without these, `EntityFormConfig.version` and `VersionedRecord._configVersion` are
49
+ * declarations nothing acts on: a schema can be edited freely while records keep their old
50
+ * shape, and nothing reconciles the two. Registering migrations makes the version numbers
51
+ * mean something.
52
+ *
53
+ * @example
54
+ * provideNgxDynamicEntity({
55
+ * migrations: [
56
+ * { from: 1, to: 2, description: 'split name', migrate: r => ({ ...r, firstName: r['name'] }) },
57
+ * ],
58
+ * })
59
+ */
60
+ export declare const RECORD_MIGRATIONS: InjectionToken<RecordMigration[]>;
46
61
  /** Registry: validator key → ValidatorFn */
47
62
  export declare const VALIDATOR_REGISTRY: InjectionToken<Map<string, any>>;
63
+ /**
64
+ * A registered hook: receives the payload, returns it (or a replacement), sync or async.
65
+ *
66
+ * Spelled out rather than typed as `Function`, which accepts class declarations and any
67
+ * arity and gives a consumer no signature to write against.
68
+ */
69
+ export type HookFn = (data: any, context?: unknown) => unknown | Promise<unknown>;
48
70
  /** Registry: hook key → async (data, context) => data */
49
- export declare const HOOK_REGISTRY: InjectionToken<Map<string, Function>>;
71
+ export declare const HOOK_REGISTRY: InjectionToken<Map<string, HookFn>>;
50
72
  /**
51
73
  * Consumer-registered list of common module tabs (documents, audit, tasks, etc.).
52
74
  * The library ships no built-in implementations. Consumers provide their own list
package/package.json CHANGED
@@ -1,35 +1,34 @@
1
1
  {
2
2
  "name": "ngx-dynamic-entity",
3
- "version": "1.0.0",
4
- "description": "Angular 17 standalone UI library for dynamic entity forms and tables.",
3
+ "version": "1.1.0",
4
+ "description": "Angular standalone UI library for rendering dynamic entity forms.",
5
+ "//typings": "Local workspace resolution only: ng-packagr overwrites this with the correct in-tarball path ('index.d.ts') when generating dist/package.json. Do NOT add a 'main' field here — ng-packagr does not generate one, so it would be copied verbatim and resolve to dist/dist/... in the published package.",
5
6
  "typings": "index.d.ts",
6
- "main": "./dist/fesm2022/ngx-dynamic-entity.mjs",
7
7
  "peerDependencies": {
8
- "@angular/cdk": "^17.0.0",
9
- "@angular/common": "^17.0.0",
10
- "@angular/core": "^17.0.0",
11
- "@angular/forms": "^17.0.0",
12
- "@angular/material": "^17.0.0",
13
- "@dynamic-entity/core": "^1.0.0",
8
+ "@angular/common": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
9
+ "@angular/core": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
10
+ "@angular/forms": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
11
+ "@dynamic-entity/core": "^1.1.0",
14
12
  "rxjs": "^7.0.0"
15
13
  },
16
- "peerDependenciesMeta": {
17
- "@angular/cdk": {
18
- "optional": true
19
- },
20
- "@angular/material": {
21
- "optional": true
22
- }
23
- },
24
14
  "keywords": [
25
15
  "angular",
26
16
  "dynamic-entity",
27
17
  "dynamic-forms",
28
- "dynamic-table",
29
18
  "rbac"
30
19
  ],
31
20
  "license": "MIT",
32
21
  "sideEffects": false,
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/berserker5619/Dynamic-Entity.git",
25
+ "directory": "packages/ngx-dynamic-entity"
26
+ },
27
+ "homepage": "https://github.com/berserker5619/Dynamic-Entity/tree/main/packages/ngx-dynamic-entity#readme",
28
+ "bugs": {
29
+ "url": "https://github.com/berserker5619/Dynamic-Entity/issues"
30
+ },
31
+ "author": "Nizamudeen",
33
32
  "module": "fesm2022/ngx-dynamic-entity.mjs",
34
33
  "exports": {
35
34
  "./package.json": {
package/public-api.d.ts CHANGED
@@ -19,10 +19,20 @@ export * from './lib/form/dynamic-field/dynamic-field.component';
19
19
  export * from './lib/field-types/text-field.component';
20
20
  export * from './lib/field-types/textarea-field.component';
21
21
  export * from './lib/field-types/number-field.component';
22
+ export * from './lib/field-types/currency-field.component';
23
+ export * from './lib/field-types/email-field.component';
24
+ export * from './lib/field-types/password-field.component';
22
25
  export * from './lib/field-types/checkbox-field.component';
26
+ export * from './lib/field-types/boolean-field.component';
23
27
  export * from './lib/field-types/date-field.component';
28
+ export * from './lib/field-types/month-year-field.component';
24
29
  export * from './lib/field-types/dropdown-field.component';
30
+ export * from './lib/field-types/radio-field.component';
25
31
  export * from './lib/field-types/multi-select-field.component';
26
32
  export * from './lib/field-types/entity-ref-field.component';
33
+ export * from './lib/field-types/group-field.component';
34
+ export * from './lib/field-types/array-field.component';
35
+ export * from './lib/field-types/image-field.component';
36
+ export * from './lib/field-types/file-field.component';
27
37
  export { resolveLabel, resolveOptionLabel, resolveOptionValue, formatDisplayValue, valuesMatch, normalizeOption, normalizeConfig, } from '@dynamic-entity/core';
28
38
  export type { EntityFormConfig, NestedTabConfig, NestedFieldConfig, FieldTableConfig, RichFieldType, LocalizedText, DropdownOption, VersionedRecord, EntityPermissions, RbacContext, FieldValidators, EntityReferenceConfig, EntityReferenceLoader, ReferenceOption, ReferenceLoaderContext, AutoPatchConfig, PatchOnTrueMapping, FileRef, FormRule, RuleCondition, RuleAction, RuleEvaluationResult, CommonModuleEntry, } from '@dynamic-entity/core';