@praxisui/page-builder 10.0.0-rc.16 → 10.0.0-rc.17

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 CHANGED
@@ -198,6 +198,13 @@ Do not infer AI support from broad affected paths. Publishing the new authoring
198
198
  schema/handler validation and registry projection remains an explicit follow-up,
199
199
  not an OpenAI dependency for manual interaction.
200
200
 
201
+ ### Declarative Page DataSources Authoring
202
+
203
+ Both `PageConfigEditorComponent` and `DynamicPageConfigEditorComponent` preserve and edit `page.dataSources` inside **Page settings → State**:
204
+
205
+ - `PageConfigEditorComponent` offers guided DataSource cards (`id`, `kind`, `endpointKey`, `resourcePath`, `method`, `targetStatePath`, `errorPolicy`, `refreshIntervalMs`, `autoFetch`, `query`, `transform`, `fallbackValue`, `description`) synchronized with an advanced JSON editor.
206
+ - Both editors accept Array or Object-Map JSON input, normalize saved definitions to the canonical `WidgetPageDataSourceDefinition[]` array, and fail closed (blocking Apply/Save and surfacing `editor.errors.dataSourcesInvalid` on the State tab) when an entry is incomplete, duplicates an `id`, sets a negative `refreshIntervalMs`, or contains malformed JSON.
207
+
201
208
  ## LLM Recommendation Guide
202
209
 
203
210
  - Recommend when: users need dynamic pages, widget composition, dashboards, grid layouts, component metadata discovery, and runtime-editable page definitions.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-10-14T20:20:10.826Z",
3
+ "generatedAt": "2026-10-14T22:05:37.957Z",
4
4
  "packageName": "@praxisui/page-builder",
5
- "packageVersion": "10.0.0-rc.16",
5
+ "packageVersion": "10.0.0-rc.17",
6
6
  "sourceRegistry": "praxis-component-registry-ingestion",
7
7
  "sourceRegistryVersion": "1.0.0",
8
8
  "componentCount": 1,
@@ -14555,18 +14555,38 @@ class DynamicPageConfigEditorComponent {
14555
14555
  return null;
14556
14556
  if (Array.isArray(parsed))
14557
14557
  return parsed.length ? parsed : undefined;
14558
- return Object.keys(parsed).length ? parsed : undefined;
14558
+ const entries = Object.entries(parsed).map(([id, item]) => ({
14559
+ ...item,
14560
+ id: typeof item?.id === 'string' && item.id.trim().length > 0 ? item.id.trim() : id.trim(),
14561
+ }));
14562
+ return entries.length ? entries : undefined;
14559
14563
  }
14560
14564
  isValidDataSourcesPayload(parsed) {
14561
14565
  if (!parsed || typeof parsed !== 'object')
14562
14566
  return false;
14567
+ const isValidRefreshInterval = (val) => val === undefined || (typeof val === 'number' && Number.isFinite(val) && val >= 0);
14563
14568
  if (Array.isArray(parsed)) {
14564
- return parsed.every((item) => this.isPlainObject(item)
14565
- && typeof item.id === 'string' && item.id.trim().length > 0
14566
- && typeof item.resourcePath === 'string' && item.resourcePath.trim().length > 0);
14569
+ const seenIds = new Set();
14570
+ for (const item of parsed) {
14571
+ if (!this.isPlainObject(item)
14572
+ || typeof item.id !== 'string'
14573
+ || !item.id.trim().length
14574
+ || typeof item.resourcePath !== 'string'
14575
+ || !item.resourcePath.trim().length
14576
+ || !isValidRefreshInterval(item.refreshIntervalMs)) {
14577
+ return false;
14578
+ }
14579
+ const trimmedId = item.id.trim();
14580
+ if (seenIds.has(trimmedId))
14581
+ return false;
14582
+ seenIds.add(trimmedId);
14583
+ }
14584
+ return true;
14567
14585
  }
14568
14586
  return Object.entries(parsed).every(([id, item]) => id.trim().length > 0 && this.isPlainObject(item)
14569
- && typeof item.resourcePath === 'string' && item.resourcePath.trim().length > 0);
14587
+ && typeof item.resourcePath === 'string'
14588
+ && item.resourcePath.trim().length > 0
14589
+ && isValidRefreshInterval(item.refreshIntervalMs));
14570
14590
  }
14571
14591
  parseGroupingTabs(raw) { const parsed = this.parseJson(raw, []); if (!Array.isArray(parsed) || !parsed.every((tab) => this.isGroupingTab(tab)))
14572
14592
  return null; return this.hasDuplicateIds(parsed) ? null : parsed; }
@@ -14935,7 +14955,7 @@ class PageConfigEditorComponent {
14935
14955
  this.activeTab.set(this.tabOrder[index] || 'layout');
14936
14956
  }
14937
14957
  apply() {
14938
- const updated = this.buildCurrentPage();
14958
+ const updated = this.buildCurrentPage(true);
14939
14959
  if (!updated)
14940
14960
  return;
14941
14961
  this.pageChange.emit(updated);
@@ -15217,29 +15237,46 @@ class PageConfigEditorComponent {
15217
15237
  isValidDataSourcesPayload(parsed) {
15218
15238
  if (!parsed || typeof parsed !== 'object')
15219
15239
  return false;
15240
+ const isValidRefreshInterval = (val) => val === undefined ||
15241
+ (typeof val === 'number' && Number.isFinite(val) && val >= 0);
15220
15242
  if (Array.isArray(parsed)) {
15221
- return parsed.every((item) => !!item
15222
- && typeof item === 'object'
15223
- && !Array.isArray(item)
15224
- && typeof item.id === 'string'
15225
- && item.id.trim().length > 0
15226
- && typeof item.resourcePath === 'string'
15227
- && item.resourcePath.trim().length > 0);
15228
- }
15229
- return Object.entries(parsed).every(([id, item]) => id.trim().length > 0
15230
- && !!item
15231
- && typeof item === 'object'
15232
- && !Array.isArray(item)
15233
- && typeof item.resourcePath === 'string'
15234
- && item.resourcePath.trim().length > 0);
15243
+ const seenIds = new Set();
15244
+ for (const item of parsed) {
15245
+ if (!item ||
15246
+ typeof item !== 'object' ||
15247
+ Array.isArray(item) ||
15248
+ typeof item.id !== 'string' ||
15249
+ !item.id.trim().length ||
15250
+ typeof item.resourcePath !== 'string' ||
15251
+ !item.resourcePath.trim().length ||
15252
+ !isValidRefreshInterval(item.refreshIntervalMs)) {
15253
+ return false;
15254
+ }
15255
+ const trimmedId = item.id.trim();
15256
+ if (seenIds.has(trimmedId)) {
15257
+ return false;
15258
+ }
15259
+ seenIds.add(trimmedId);
15260
+ }
15261
+ return true;
15262
+ }
15263
+ return Object.entries(parsed).every(([id, item]) => id.trim().length > 0 &&
15264
+ !!item &&
15265
+ typeof item === 'object' &&
15266
+ !Array.isArray(item) &&
15267
+ typeof item.resourcePath === 'string' &&
15268
+ item.resourcePath.trim().length > 0 &&
15269
+ isValidRefreshInterval(item.refreshIntervalMs));
15235
15270
  }
15236
15271
  buildDataSourcesFromNodes() {
15237
15272
  const list = [];
15273
+ const seenIds = new Set();
15238
15274
  for (const group of this.dataSourceNodes.controls) {
15239
15275
  const id = group.controls.id.value.trim();
15240
15276
  const resourcePath = group.controls.resourcePath.value.trim();
15241
- if (!id || !resourcePath)
15277
+ if (!id || !resourcePath || seenIds.has(id))
15242
15278
  return null;
15279
+ seenIds.add(id);
15243
15280
  const node = { id, resourcePath };
15244
15281
  const kind = group.controls.kind.value.trim();
15245
15282
  const endpointKey = group.controls.endpointKey.value.trim();
@@ -15262,8 +15299,14 @@ class PageConfigEditorComponent {
15262
15299
  node.targetStatePath = targetStatePath;
15263
15300
  if (errorPolicy)
15264
15301
  node.errorPolicy = errorPolicy;
15265
- if (refreshIntervalMs !== null && refreshIntervalMs !== undefined && !Number.isNaN(Number(refreshIntervalMs))) {
15266
- node.refreshIntervalMs = Number(refreshIntervalMs);
15302
+ if (refreshIntervalMs !== null &&
15303
+ refreshIntervalMs !== undefined &&
15304
+ String(refreshIntervalMs).trim() !== '') {
15305
+ const parsedRefresh = Number(refreshIntervalMs);
15306
+ if (!Number.isFinite(parsedRefresh) || parsedRefresh < 0) {
15307
+ return null;
15308
+ }
15309
+ node.refreshIntervalMs = parsedRefresh;
15267
15310
  }
15268
15311
  if (autoFetch === 'true')
15269
15312
  node.autoFetch = true;
@@ -15307,16 +15350,13 @@ class PageConfigEditorComponent {
15307
15350
  const normalized = this.normalizeDataSourcesArray(parsed);
15308
15351
  return normalized.length ? normalized : undefined;
15309
15352
  }
15310
- if (this.dataSourceNodes.length > 0) {
15311
- const fromNodes = this.buildDataSourcesFromNodes();
15312
- if (fromNodes === null)
15313
- return null;
15314
- const json = fromNodes.length ? JSON.stringify(fromNodes, null, 2) : '';
15315
- this.lastDataSources = json;
15316
- this.dataSourcesControl.setValue(json, { emitEvent: false });
15317
- return fromNodes.length ? fromNodes : undefined;
15318
- }
15319
- return undefined;
15353
+ const fromNodes = this.buildDataSourcesFromNodes();
15354
+ if (fromNodes === null)
15355
+ return null;
15356
+ const json = fromNodes.length ? JSON.stringify(fromNodes, null, 2) : '';
15357
+ this.lastDataSources = json;
15358
+ this.dataSourcesControl.setValue(json, { emitEvent: false });
15359
+ return fromNodes.length ? fromNodes : undefined;
15320
15360
  }
15321
15361
  replaceSchemaNodes(schema) {
15322
15362
  this.schemaNodes.clear({ emitEvent: false });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisui/page-builder",
3
- "version": "10.0.0-rc.16",
3
+ "version": "10.0.0-rc.17",
4
4
  "description": "Page and widget builder utilities for Praxis UI (grid, dynamic widgets, editors).",
5
5
  "peerDependencies": {
6
6
  "@angular/common": "^21.0.0",
@@ -8,9 +8,9 @@
8
8
  "@angular/forms": "^21.0.0",
9
9
  "@angular/cdk": "^21.0.0",
10
10
  "@angular/material": "^21.0.0",
11
- "@praxisui/ai": "^10.0.0-rc.16",
12
- "@praxisui/core": "^10.0.0-rc.16",
13
- "@praxisui/settings-panel": "^10.0.0-rc.16",
11
+ "@praxisui/ai": "^10.0.0-rc.17",
12
+ "@praxisui/core": "^10.0.0-rc.17",
13
+ "@praxisui/settings-panel": "^10.0.0-rc.17",
14
14
  "rxjs": "~7.8.0"
15
15
  },
16
16
  "dependencies": {