@praxisui/dynamic-form 9.0.5-rc.38 → 9.0.5-rc.39

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.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-08-19T02:54:26.311Z",
3
+ "generatedAt": "2026-08-19T11:10:58.413Z",
4
4
  "packageName": "@praxisui/dynamic-form",
5
- "packageVersion": "9.0.5-rc.38",
5
+ "packageVersion": "9.0.5-rc.39",
6
6
  "sourceRegistry": "praxis-component-registry-ingestion",
7
7
  "sourceRegistryVersion": "1.0.0",
8
8
  "componentCount": 2,
@@ -4,7 +4,7 @@ doc_type: "adr"
4
4
  component: "praxis-dynamic-form"
5
5
  status: "accepted"
6
6
  owner: "praxis-ui"
7
- last_updated: "2026-04-15"
7
+ last_updated: "2026-08-19"
8
8
  ---
9
9
 
10
10
  # Dynamic Form Authoring Document Semantics
@@ -39,6 +39,9 @@ Essa regra evita perda silenciosa do contrato canonico de Entity Lookup durante
39
39
  - `Apply` e `Save` vindos do editor operam em modo `replace-all`.
40
40
  - `reset` do editor limpa todos os modos persistidos do artefato.
41
41
  - adapters legados continuam suportados sem limpeza destrutiva de contexto ausente.
42
+ - editores subordinados de seção, linha e coluna devem aplicar alterações pela localização estrutural capturada (`section`, `row`, `column`), nunca por busca global do primeiro `id` coincidente;
43
+ - IDs de linha devem ser únicos dentro da seção e IDs de coluna devem ser únicos dentro da linha;
44
+ - o estado `dirty` desses editores é reversível: restaurar o snapshot inicial deve desabilitar novamente Apply/Save.
42
45
 
43
46
  ## Visual Blocks Inside Layout
44
47
 
@@ -17654,6 +17654,9 @@ class PraxisDynamicForm {
17654
17654
  openRowEditor(row) {
17655
17655
  if (!row)
17656
17656
  return;
17657
+ const location = this.resolveLayoutEditorLocation('row', row);
17658
+ if (!location)
17659
+ return;
17657
17660
  Promise.resolve().then(function () { return rowEditor_component; }).then((m) => {
17658
17661
  const { RowEditorComponent } = m;
17659
17662
  const ref = this.settingsPanel.openChild({
@@ -17662,13 +17665,16 @@ class PraxisDynamicForm {
17662
17665
  titleIcon: 'view_stream',
17663
17666
  content: { component: RowEditorComponent, inputs: { row } },
17664
17667
  });
17665
- ref.applied$.pipe(takeUntil(this.destroy$)).subscribe((value) => this.applyRowEditorValue(row, value));
17666
- ref.saved$.pipe(takeUntil(this.destroy$)).subscribe((value) => this.applyRowEditorValue(row, value));
17668
+ ref.applied$.pipe(takeUntil(this.destroy$)).subscribe((value) => this.applyRowEditorValue(location, value));
17669
+ ref.saved$.pipe(takeUntil(this.destroy$)).subscribe((value) => this.applyRowEditorValue(location, value));
17667
17670
  });
17668
17671
  }
17669
17672
  openColumnEditor(column) {
17670
17673
  if (!column)
17671
17674
  return;
17675
+ const location = this.resolveLayoutEditorLocation('column', column);
17676
+ if (!location)
17677
+ return;
17672
17678
  Promise.resolve().then(function () { return columnEditor_component; }).then((m) => {
17673
17679
  const { ColumnEditorComponent } = m;
17674
17680
  const ref = this.settingsPanel.openChild({
@@ -17677,8 +17683,8 @@ class PraxisDynamicForm {
17677
17683
  titleIcon: 'view_column',
17678
17684
  content: { component: ColumnEditorComponent, inputs: { column } },
17679
17685
  });
17680
- ref.applied$.pipe(takeUntil(this.destroy$)).subscribe((value) => this.applyColumnEditorValue(column, value));
17681
- ref.saved$.pipe(takeUntil(this.destroy$)).subscribe((value) => this.applyColumnEditorValue(column, value));
17686
+ ref.applied$.pipe(takeUntil(this.destroy$)).subscribe((value) => this.applyColumnEditorValue(location, value));
17687
+ ref.saved$.pipe(takeUntil(this.destroy$)).subscribe((value) => this.applyColumnEditorValue(location, value));
17682
17688
  });
17683
17689
  }
17684
17690
  openSectionEditor(section, focusTarget) {
@@ -17703,50 +17709,61 @@ class PraxisDynamicForm {
17703
17709
  });
17704
17710
  });
17705
17711
  }
17706
- applyRowEditorValue(row, value) {
17707
- this.applyNestedLayoutEditorValue('row', row, value);
17712
+ applyRowEditorValue(location, value) {
17713
+ this.applyNestedLayoutEditorValue('row', location, value);
17708
17714
  }
17709
- applyColumnEditorValue(column, value) {
17710
- this.applyNestedLayoutEditorValue('column', column, value);
17715
+ applyColumnEditorValue(location, value) {
17716
+ this.applyNestedLayoutEditorValue('column', location, value);
17711
17717
  }
17712
- applyNestedLayoutEditorValue(kind, target, value) {
17713
- if (!target || !value || typeof value !== 'object')
17714
- return;
17715
- const sourceSections = this.config?.sections || [];
17716
- let targetPosition;
17717
- for (let sectionIndex = 0; sectionIndex < sourceSections.length; sectionIndex++) {
17718
- const section = sourceSections[sectionIndex];
17718
+ resolveLayoutEditorLocation(kind, target) {
17719
+ const sections = this.config?.sections || [];
17720
+ for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
17721
+ const section = sections[sectionIndex];
17719
17722
  for (let rowIndex = 0; rowIndex < (section.rows || []).length; rowIndex++) {
17720
17723
  const row = section.rows[rowIndex];
17721
- if (kind === 'row' &&
17722
- (row === target || (!!row?.id && !!target?.id && row.id === target.id))) {
17723
- targetPosition = { sectionIndex, rowIndex };
17724
- break;
17724
+ if (kind === 'row' && row === target) {
17725
+ return { sectionIndex, rowIndex, sectionId: section.id, rowId: row.id };
17725
17726
  }
17726
17727
  if (kind === 'column') {
17727
- const columnIndex = (row.columns || []).findIndex((candidate) => candidate === target ||
17728
- (!!candidate?.id && !!target?.id && candidate.id === target.id));
17728
+ const columnIndex = (row.columns || []).findIndex((candidate) => candidate === target);
17729
17729
  if (columnIndex >= 0) {
17730
- targetPosition = { sectionIndex, rowIndex, columnIndex };
17731
- break;
17730
+ return {
17731
+ sectionIndex,
17732
+ rowIndex,
17733
+ columnIndex,
17734
+ sectionId: section.id,
17735
+ rowId: row.id,
17736
+ columnId: row.columns[columnIndex]?.id,
17737
+ };
17732
17738
  }
17733
17739
  }
17734
17740
  }
17735
- if (targetPosition)
17736
- break;
17737
17741
  }
17738
- if (!targetPosition)
17742
+ return undefined;
17743
+ }
17744
+ applyNestedLayoutEditorValue(kind, location, value) {
17745
+ if (!value || typeof value !== 'object')
17746
+ return;
17747
+ const sourceSections = this.config?.sections || [];
17748
+ const targetSection = sourceSections[location.sectionIndex];
17749
+ const targetRow = targetSection?.rows?.[location.rowIndex];
17750
+ const targetColumn = targetRow?.columns?.[location.columnIndex ?? -1];
17751
+ if (!targetSection ||
17752
+ targetSection.id !== location.sectionId ||
17753
+ !targetRow ||
17754
+ targetRow.id !== location.rowId ||
17755
+ (kind === 'column' && (!targetColumn || targetColumn.id !== location.columnId)))
17739
17756
  return;
17740
17757
  const sections = structuredClone(sourceSections);
17741
- const positionedRow = sections[targetPosition.sectionIndex].rows[targetPosition.rowIndex];
17758
+ const positionedRow = sections[location.sectionIndex].rows[location.rowIndex];
17742
17759
  if (kind === 'row') {
17743
- sections[targetPosition.sectionIndex].rows[targetPosition.rowIndex] = {
17760
+ sections[location.sectionIndex].rows[location.rowIndex] = {
17744
17761
  ...positionedRow,
17745
17762
  ...structuredClone(value),
17746
17763
  };
17747
17764
  }
17748
17765
  else {
17749
- const columnIndex = targetPosition.columnIndex;
17766
+ const columnIndex = location.columnIndex;
17750
17767
  positionedRow.columns[columnIndex] = {
17751
17768
  ...positionedRow.columns[columnIndex],
17752
17769
  ...structuredClone(value),
@@ -20586,6 +20603,30 @@ class FormConfigService {
20586
20603
  else {
20587
20604
  ids.add(section.id);
20588
20605
  }
20606
+ const rowIds = new Set();
20607
+ for (const row of section.rows || []) {
20608
+ if (!row.id) {
20609
+ errors.push(`Row id is required in section: ${section.id}`);
20610
+ }
20611
+ else if (rowIds.has(row.id)) {
20612
+ errors.push(`Duplicate row id in section ${section.id}: ${row.id}`);
20613
+ }
20614
+ else {
20615
+ rowIds.add(row.id);
20616
+ }
20617
+ const columnIds = new Set();
20618
+ for (const column of row.columns || []) {
20619
+ if (!column.id) {
20620
+ errors.push(`Column id is required in row: ${row.id}`);
20621
+ }
20622
+ else if (columnIds.has(column.id)) {
20623
+ errors.push(`Duplicate column id in row ${row.id}: ${column.id}`);
20624
+ }
20625
+ else {
20626
+ columnIds.add(column.id);
20627
+ }
20628
+ }
20629
+ }
20589
20630
  }
20590
20631
  return errors;
20591
20632
  }
@@ -31007,10 +31048,14 @@ class PraxisDynamicFormConfigEditor {
31007
31048
  onLayoutSelect(event) {
31008
31049
  if (event && event.type === 'column' && event.openEditor) {
31009
31050
  const column = withFormConfigSections(this.editedConfig).sections[event.sectionIndex]?.rows[event.rowIndex]?.columns[event.columnIndex];
31010
- this.openColumnEditor(column);
31051
+ this.openColumnEditor(column, {
31052
+ sectionIndex: event.sectionIndex,
31053
+ rowIndex: event.rowIndex,
31054
+ columnIndex: event.columnIndex,
31055
+ });
31011
31056
  }
31012
31057
  }
31013
- openColumnEditor(column) {
31058
+ openColumnEditor(column, location) {
31014
31059
  if (!column)
31015
31060
  return;
31016
31061
  Promise.resolve().then(function () { return columnEditor_component; }).then((m) => {
@@ -31021,34 +31066,22 @@ class PraxisDynamicFormConfigEditor {
31021
31066
  titleIcon: 'view_column',
31022
31067
  content: { component: ColumnEditorComponent, inputs: { column } },
31023
31068
  });
31024
- ref.applied$.pipe(takeUntil(this.destroy$)).subscribe((value) => this.applyColumnEditorValue(column, value));
31025
- ref.saved$.pipe(takeUntil(this.destroy$)).subscribe((value) => this.applyColumnEditorValue(column, value));
31069
+ ref.applied$.pipe(takeUntil(this.destroy$)).subscribe((value) => this.applyColumnEditorValue(column, location, value));
31070
+ ref.saved$.pipe(takeUntil(this.destroy$)).subscribe((value) => this.applyColumnEditorValue(column, location, value));
31026
31071
  });
31027
31072
  }
31028
- applyColumnEditorValue(column, value) {
31073
+ applyColumnEditorValue(column, location, value) {
31029
31074
  if (!column || !value || typeof value !== 'object')
31030
31075
  return;
31031
31076
  const sourceSections = this.editedConfig.sections || [];
31032
- let targetPosition;
31033
- for (let sectionIndex = 0; sectionIndex < sourceSections.length; sectionIndex++) {
31034
- const section = sourceSections[sectionIndex];
31035
- for (let rowIndex = 0; rowIndex < (section.rows || []).length; rowIndex++) {
31036
- const row = section.rows[rowIndex];
31037
- const index = (row.columns || []).findIndex((candidate) => candidate === column || (!!candidate?.id && !!column?.id && candidate.id === column.id));
31038
- if (index >= 0) {
31039
- targetPosition = { sectionIndex, rowIndex, columnIndex: index };
31040
- break;
31041
- }
31042
- }
31043
- if (targetPosition)
31044
- break;
31045
- }
31046
- if (!targetPosition)
31077
+ const targetColumn = sourceSections[location.sectionIndex]?.rows?.[location.rowIndex]
31078
+ ?.columns?.[location.columnIndex];
31079
+ if (!targetColumn || targetColumn.id !== column.id)
31047
31080
  return;
31048
31081
  const sections = structuredClone(sourceSections);
31049
- const row = sections[targetPosition.sectionIndex].rows[targetPosition.rowIndex];
31050
- row.columns[targetPosition.columnIndex] = {
31051
- ...row.columns[targetPosition.columnIndex],
31082
+ const row = sections[location.sectionIndex].rows[location.rowIndex];
31083
+ row.columns[location.columnIndex] = {
31084
+ ...row.columns[location.columnIndex],
31052
31085
  ...structuredClone(value),
31053
31086
  };
31054
31087
  this.onConfigChange({ ...this.editedConfig, sections });
@@ -33045,6 +33078,7 @@ class SectionEditorComponent {
33045
33078
  });
33046
33079
  customActionValue = '__custom__';
33047
33080
  destroy$ = new Subject();
33081
+ initialValueSig = '';
33048
33082
  titleInput;
33049
33083
  descriptionInput;
33050
33084
  constructor(fb, data, iconPicker) {
@@ -33100,9 +33134,11 @@ class SectionEditorComponent {
33100
33134
  headerActions: this.fb.array((this.normalizeHeaderActions(this.section.headerActions) ?? []).map((action) => this.createHeaderActionGroup(action))),
33101
33135
  });
33102
33136
  this.syncCollapsedControlState(!!this.form.get('collapsible')?.value);
33137
+ this.initialValueSig = JSON.stringify(this.normalizeSectionFormValue(this.form.getRawValue()));
33103
33138
  this.form.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
33104
33139
  this.applyNormalizedFormValue();
33105
- this.isDirty$.next(true);
33140
+ this.isDirty$.next(JSON.stringify(this.normalizeSectionFormValue(this.form.getRawValue())) !==
33141
+ this.initialValueSig);
33106
33142
  this.isValid$.next(this.form.valid);
33107
33143
  });
33108
33144
  this.form.get('collapsible')?.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((canCollapse) => {
@@ -33114,6 +33150,9 @@ class SectionEditorComponent {
33114
33150
  }
33115
33151
  ngAfterViewInit() {
33116
33152
  setTimeout(() => {
33153
+ if (!this.isDirty$.value) {
33154
+ this.initialValueSig = JSON.stringify(this.normalizeSectionFormValue(this.form.getRawValue()));
33155
+ }
33117
33156
  if (this.focusTarget === 'title') {
33118
33157
  this.titleInput?.nativeElement?.focus?.();
33119
33158
  }
@@ -33429,17 +33468,14 @@ class SectionEditorComponent {
33429
33468
  return;
33430
33469
  this.form.patchValue({ titleColor: v, descriptionColor: v });
33431
33470
  Object.assign(this.section, { titleColor: v, descriptionColor: v });
33432
- this.isDirty$.next(true);
33433
33471
  }
33434
33472
  onTitleStyleChange(style) {
33435
33473
  this.section.titleStyle = style;
33436
33474
  this.form.get('titleStyle')?.setValue(style);
33437
- this.isDirty$.next(true);
33438
33475
  }
33439
33476
  onDescriptionStyleChange(style) {
33440
33477
  this.section.descriptionStyle = style;
33441
33478
  this.form.get('descriptionStyle')?.setValue(style);
33442
- this.isDirty$.next(true);
33443
33479
  }
33444
33480
  trackFieldOption(index, field) {
33445
33481
  return field.value || String(index);
@@ -35378,6 +35414,7 @@ class ColumnEditorComponent {
35378
35414
  isBusy$ = new BehaviorSubject(false);
35379
35415
  destroy$ = new Subject();
35380
35416
  _previewTimer;
35417
+ initialValueSig = '';
35381
35418
  i18n = inject(PraxisI18nService);
35382
35419
  constructor(fb, data) {
35383
35420
  this.fb = fb;
@@ -35420,6 +35457,7 @@ class ColumnEditorComponent {
35420
35457
  className: [col.className ?? ''],
35421
35458
  testId: [col.testId ?? ''],
35422
35459
  });
35460
+ this.initialValueSig = JSON.stringify(this.form.getRawValue());
35423
35461
  this.form.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
35424
35462
  // Evita NG0100: agendar a mutação para o próximo macrotask,
35425
35463
  // fora do ciclo de detecção atual, mantendo o preview ao vivo.
@@ -35428,7 +35466,7 @@ class ColumnEditorComponent {
35428
35466
  }
35429
35467
  this._previewTimer = setTimeout(() => {
35430
35468
  this.applyChanges();
35431
- this.isDirty$.next(true);
35469
+ this.isDirty$.next(JSON.stringify(this.form.getRawValue()) !== this.initialValueSig);
35432
35470
  this.isValid$.next(this.form.valid);
35433
35471
  this._previewTimer = null;
35434
35472
  }, 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisui/dynamic-form",
3
- "version": "9.0.5-rc.38",
3
+ "version": "9.0.5-rc.39",
4
4
  "description": "Angular dynamic form engine for Praxis UI: metadata-driven forms, hooks, and services integrating @praxisui/* packages.",
5
5
  "peerDependencies": {
6
6
  "@angular/common": "^21.0.0",
@@ -9,13 +9,13 @@
9
9
  "@angular/forms": "^21.0.0",
10
10
  "@angular/material": "^21.0.0",
11
11
  "@angular/router": "^21.0.0",
12
- "@praxisui/ai": "^9.0.5-rc.38",
13
- "@praxisui/dynamic-fields": "^9.0.5-rc.38",
14
- "@praxisui/metadata-editor": "^9.0.5-rc.38",
15
- "@praxisui/rich-content": "^9.0.5-rc.38",
16
- "@praxisui/settings-panel": "^9.0.5-rc.38",
17
- "@praxisui/visual-builder": "^9.0.5-rc.38",
18
- "@praxisui/core": "^9.0.5-rc.38",
12
+ "@praxisui/ai": "^9.0.5-rc.39",
13
+ "@praxisui/dynamic-fields": "^9.0.5-rc.39",
14
+ "@praxisui/metadata-editor": "^9.0.5-rc.39",
15
+ "@praxisui/rich-content": "^9.0.5-rc.39",
16
+ "@praxisui/settings-panel": "^9.0.5-rc.39",
17
+ "@praxisui/visual-builder": "^9.0.5-rc.39",
18
+ "@praxisui/core": "^9.0.5-rc.39",
19
19
  "rxjs": "^7.8.0"
20
20
  },
21
21
  "dependencies": {
@@ -1360,6 +1360,7 @@ declare class PraxisDynamicForm implements OnInit, OnChanges, OnDestroy {
1360
1360
  openSectionEditor(section: any, focusTarget?: 'title' | 'description'): void;
1361
1361
  private applyRowEditorValue;
1362
1362
  private applyColumnEditorValue;
1363
+ private resolveLayoutEditorLocation;
1363
1364
  private applyNestedLayoutEditorValue;
1364
1365
  private applySectionEditorValue;
1365
1366
  private resolveControlTypeEditorial;
@@ -2474,6 +2475,7 @@ declare class SectionEditorComponent implements OnInit, AfterViewInit, OnDestroy
2474
2475
  readonly globalActionCatalog: ActionCatalogOption[];
2475
2476
  readonly customActionValue = "__custom__";
2476
2477
  private destroy$;
2478
+ private initialValueSig;
2477
2479
  titleInput?: ElementRef<HTMLInputElement>;
2478
2480
  descriptionInput?: ElementRef<HTMLTextAreaElement>;
2479
2481
  constructor(fb: FormBuilder, data: {
@@ -2597,6 +2599,7 @@ declare class ColumnEditorComponent implements OnInit, OnDestroy, SettingsValueP
2597
2599
  isBusy$: BehaviorSubject<boolean>;
2598
2600
  private destroy$;
2599
2601
  private _previewTimer;
2602
+ private initialValueSig;
2600
2603
  private readonly i18n;
2601
2604
  constructor(fb: FormBuilder, data: {
2602
2605
  column: FormColumn;