@sd-angular/core 19.0.40 → 19.0.41

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 (24) hide show
  1. package/components/table/src/components/desktop-cell/desktop-cell.component.d.ts +1 -1
  2. package/components/table/src/services/field-value.util.d.ts +14 -0
  3. package/fesm2022/sd-angular-core-components-table.mjs +105 -11
  4. package/fesm2022/sd-angular-core-components-table.mjs.map +1 -1
  5. package/fesm2022/sd-angular-core-components-upload-file.mjs +5 -0
  6. package/fesm2022/sd-angular-core-components-upload-file.mjs.map +1 -1
  7. package/fesm2022/sd-angular-core-modules-auth.mjs.map +1 -1
  8. package/fesm2022/sd-angular-core-modules-layout.mjs.map +1 -1
  9. package/fesm2022/sd-angular-core-modules-permission.mjs.map +1 -1
  10. package/fesm2022/sd-angular-core-services-api.mjs +211 -8
  11. package/fesm2022/sd-angular-core-services-api.mjs.map +1 -1
  12. package/fesm2022/sd-angular-core-services-excel.mjs +5 -0
  13. package/fesm2022/sd-angular-core-services-excel.mjs.map +1 -1
  14. package/fesm2022/sd-angular-core-utilities-models.mjs.map +1 -1
  15. package/modules/auth/configurations/auth.configuration.d.ts +4 -3
  16. package/modules/layout/configurations/layout.configuration.d.ts +3 -2
  17. package/modules/permission/src/configurations/permission.configuration.d.ts +3 -2
  18. package/package.json +14 -14
  19. package/services/api/index.d.ts +1 -0
  20. package/services/api/src/api-handler-registry.d.ts +49 -0
  21. package/services/api/src/api-handler-selection.d.ts +10 -0
  22. package/services/api/src/api-route-configurations.d.ts +20 -0
  23. package/services/api/src/api.model.d.ts +7 -0
  24. package/utilities/models/src/maybe-async.model.d.ts +2 -1
@@ -9,7 +9,7 @@ export declare class DesktopCellComponent {
9
9
  column: import("@angular/core").InputSignal<SdTableColumn>;
10
10
  item: import("@angular/core").InputSignal<SdTableItem<any>>;
11
11
  cellDef: import("@angular/core").InputSignal<Record<string, SdTableCellDefDirective<any>>>;
12
- value: import("@angular/core").Signal<unknown>;
12
+ value: import("@angular/core").Signal<any>;
13
13
  key: import("@angular/core").Signal<any>;
14
14
  autoId: import("@angular/core").Signal<string>;
15
15
  truncateEnable: import("@angular/core").Signal<boolean | undefined>;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Reads a row value by table field (`column.field`, group field) for every table read path.
3
+ *
4
+ * why (NSP-5745): `@sdcorejs/utils` 1.2 parses property paths strictly and throws
5
+ * `UnsafePropertyPathError` for a segment with whitespace or a path with leading/trailing
6
+ * whitespace. Tables whose columns come from backend data (for example import headers such as
7
+ * "Số phòng ngủ" or "Loại sản phẩm*") then failed the whole read with "Không thể tải dữ liệu".
8
+ *
9
+ * A path that utils accepts keeps the exact `getNestedValue` result. A path that utils rejects
10
+ * falls back to the 1.1.x dot-split lookup, restricted to own data properties (no inherited
11
+ * values, no getters), never through a prototype-sensitive segment and never into a prototype
12
+ * object.
13
+ */
14
+ export declare function resolveFieldValue<T = any>(source: unknown, field: unknown): T | undefined;
@@ -1477,6 +1477,91 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImpo
1477
1477
  ], providers: [ConfigService], template: "@let _configuration = configuration();\n@let _autoId = autoId();\n\n<sd-modal width=\"sm\" [title]=\"'core.component.table.setup' | translate\" #modal>\n @if (_configuration) {\n <div class=\"c-container\">\n <div class=\"c-table\" style=\"max-height: 70vh\">\n <table\n mat-table\n [dataSource]=\"_configuration.columns!\"\n cdkDropList\n [cdkDropListData]=\"_configuration.columns\"\n (cdkDropListDropped)=\"dropTable($event)\">\n <ng-container matColumnDef=\"invisible\">\n <th mat-header-cell class=\"c-th text-center px-4\" *matHeaderCellDef style=\"width: 96px\">\n {{ 'core.component.form-builder.display' | translate }}\n </th>\n <td class=\"text-center px-4\" mat-cell *matCellDef=\"let item\">\n <div class=\"d-flex\" style=\"gap: 8px\">\n <div class=\"c-handle\" (mousedown)=\"handleMouseDown()\" (mouseup)=\"handleMouseUp()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 0 24 24\" width=\"24px\" fill=\"#5f6368\">\n <path d=\"M0 0h24v24H0V0z\" fill=\"none\" />\n <path\n d=\"M11 18c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm-2-8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z\" />\n </svg>\n </div>\n <sd-switch [model]=\"!item.invisible\" (modelChange)=\"item.invisible = !item.invisible\" />\n </div>\n </td>\n </ng-container>\n <ng-container matColumnDef=\"title\">\n <th mat-header-cell class=\"c-th px-4\" *matHeaderCellDef>\n {{ 'core.component.table.column-header' | translate }}\n </th>\n <td class=\"px-4\" mat-cell *matCellDef=\"let item\">\n <sd-input [clearable]=\"true\" class=\"py-4\" size=\"sm\" [placeholder]=\"item?.origin?.title\" [(model)]=\"item.title\" hideInlineError></sd-input>\n </td>\n </ng-container>\n <ng-container matColumnDef=\"width\">\n <th mat-header-cell class=\"c-th px-4\" *matHeaderCellDef style=\"width: 80px\">\n {{ 'core.component.table.width' | translate }}\n </th>\n <td class=\"px-4\" mat-cell *matCellDef=\"let item\">\n <sd-input class=\"py-4\" size=\"sm\" [placeholder]=\"item?.origin?.width\" [(model)]=\"item.width\" hideInlineError></sd-input>\n </td>\n </ng-container>\n <ng-container matColumnDef=\"fixed\">\n <th mat-header-cell class=\"c-th text-center px-4\" *matHeaderCellDef style=\"width: 80px\">\n {{ 'core.component.table.fixed' | translate }}\n </th>\n <td class=\"text-center px-4\" mat-cell *matCellDef=\"let item\">\n <sd-switch [(model)]=\"item.fixed\" />\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"truncate\">\n <th mat-header-cell class=\"c-th text-center px-4\" *matHeaderCellDef style=\"width: 100px\">\n {{ 'core.component.table.char-limit' | translate }}\n </th>\n <td class=\"text-center px-4\" mat-cell *matCellDef=\"let item\">\n <sd-switch [(model)]=\"item.truncate\" />\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"['invisible', 'title', 'width', 'fixed', 'truncate']; sticky: true\"></tr>\n <tr\n mat-row\n *matRowDef=\"let row; columns: ['invisible', 'title', 'width', 'fixed', 'truncate']\"\n cdkDrag\n [cdkDragData]=\"row\"\n cdkDragLockAxis=\"y\"\n [cdkDragDisabled]=\"dragDisabled\">\n </tr>\n </table>\n </div>\n </div>\n }\n <ng-container sdFooter>\n <sd-button [autoId]=\"_autoId ? _autoId + '-config-skip' : null\" class=\"mr-8\" (click)=\"close()\" [title]=\"'core.component.table.skip' | translate\" color=\"secondary\"> </sd-button>\n <sd-button [autoId]=\"_autoId ? _autoId + '-config-reset' : null\" class=\"mr-8\" (click)=\"onReset()\" [title]=\"'core.component.form-builder.default' | translate\" color=\"primary\"> </sd-button>\n <sd-button [autoId]=\"_autoId ? _autoId + '-config-apply' : null\" (click)=\"onSave()\" [title]=\"'core.component.table.apply' | translate\" type=\"fill\" color=\"primary\"> </sd-button>\n </ng-container>\n</sd-modal>\n", styles: [".c-table{position:relative;overflow:auto}.c-table table{width:100%}.c-table .c-th{font-weight:500;font-size:14px;line-height:20px;color:#000}.c-container{position:relative;min-height:200px}.c-handle{color:#ccc;cursor:move}\n"] }]
1478
1478
  }], ctorParameters: () => [] });
1479
1479
 
1480
+ /** Segments that must never be traversed, matching the `@sdcorejs/utils` 1.2 safe-key rule. */
1481
+ const PROTOTYPE_SEGMENTS = new Set(['__proto__', 'prototype', 'constructor']);
1482
+ /** Upper bound for the per-field cache; column/group fields are a small, stable set in practice. */
1483
+ const MAX_CACHED_FIELDS = 1000;
1484
+ /** `true` when `@sdcorejs/utils` rejects the field as a property path. */
1485
+ const rejectedFields = new Map();
1486
+ /**
1487
+ * Reads a row value by table field (`column.field`, group field) for every table read path.
1488
+ *
1489
+ * why (NSP-5745): `@sdcorejs/utils` 1.2 parses property paths strictly and throws
1490
+ * `UnsafePropertyPathError` for a segment with whitespace or a path with leading/trailing
1491
+ * whitespace. Tables whose columns come from backend data (for example import headers such as
1492
+ * "Số phòng ngủ" or "Loại sản phẩm*") then failed the whole read with "Không thể tải dữ liệu".
1493
+ *
1494
+ * A path that utils accepts keeps the exact `getNestedValue` result. A path that utils rejects
1495
+ * falls back to the 1.1.x dot-split lookup, restricted to own data properties (no inherited
1496
+ * values, no getters), never through a prototype-sensitive segment and never into a prototype
1497
+ * object.
1498
+ */
1499
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1500
+ function resolveFieldValue(source, field) {
1501
+ if (!isTraversable(source))
1502
+ return undefined;
1503
+ if (typeof field !== 'string' || field === '')
1504
+ return undefined;
1505
+ if (isRejectedPath(field))
1506
+ return readOwnDotPath(source, field);
1507
+ try {
1508
+ return Utilities.getNestedValue(source, field);
1509
+ }
1510
+ catch {
1511
+ // why: for an accepted path utils 1.2.4 only throws when the traversal meets a prototype
1512
+ // object, which this resolver never reads (same result as the fallback).
1513
+ return undefined;
1514
+ }
1515
+ }
1516
+ /**
1517
+ * Detects a path that `@sdcorejs/utils` rejects, once per field.
1518
+ *
1519
+ * why: matching `instanceof UnsafePropertyPathError` is unreliable. `@sdcorejs/utils/fns` and
1520
+ * `@sdcorejs/utils/errors` each ship their own copy of the class, and an application bundler
1521
+ * renames one of them, so the brand check fails in bundled apps. An empty own-property root can
1522
+ * only make `getNestedValue` throw for the path itself, so the probe needs no error type. The
1523
+ * cache also keeps sort/filter loops from paying for a thrown exception on every row.
1524
+ */
1525
+ function isRejectedPath(field) {
1526
+ const cached = rejectedFields.get(field);
1527
+ if (cached !== undefined)
1528
+ return cached;
1529
+ let rejected = false;
1530
+ try {
1531
+ Utilities.getNestedValue({}, field);
1532
+ }
1533
+ catch {
1534
+ rejected = true;
1535
+ }
1536
+ if (rejectedFields.size >= MAX_CACHED_FIELDS)
1537
+ rejectedFields.clear();
1538
+ rejectedFields.set(field, rejected);
1539
+ return rejected;
1540
+ }
1541
+ function readOwnDotPath(source, field) {
1542
+ let current = source;
1543
+ for (const segment of field.split('.')) {
1544
+ if (PROTOTYPE_SEGMENTS.has(segment))
1545
+ return undefined;
1546
+ if (!isTraversable(current))
1547
+ return undefined;
1548
+ const descriptor = Object.getOwnPropertyDescriptor(current, segment);
1549
+ if (!descriptor || !('value' in descriptor))
1550
+ return undefined;
1551
+ current = descriptor.value;
1552
+ }
1553
+ return isPrototypeObject(current) ? undefined : current;
1554
+ }
1555
+ /** Non-null object or function that is not a built-in prototype object. */
1556
+ function isTraversable(value) {
1557
+ if (value === null || (typeof value !== 'object' && typeof value !== 'function'))
1558
+ return false;
1559
+ return !isPrototypeObject(value);
1560
+ }
1561
+ function isPrototypeObject(value) {
1562
+ return value === Object.prototype || value === Function.prototype;
1563
+ }
1564
+
1480
1565
  class ToStringPipe {
1481
1566
  transform(value) {
1482
1567
  return value !== null && value !== undefined ? String(value) : '';
@@ -1547,7 +1632,7 @@ class DesktopCellComponent {
1547
1632
  item = input.required();
1548
1633
  cellDef = input.required({});
1549
1634
  value = computed(() => {
1550
- return Utilities.getNestedValue(this.item()?.data, this.column()?.field);
1635
+ return resolveFieldValue(this.item()?.data, this.column()?.field);
1551
1636
  });
1552
1637
  key = computed(() => {
1553
1638
  const data = this.item()?.data;
@@ -2344,12 +2429,12 @@ class SdGroupPipe {
2344
2429
  const collapsible = !!group.collapsible;
2345
2430
  const defaultExpanded = !group.defaultCollapsed;
2346
2431
  // Bucket items theo combined field-value hash.
2347
- // Field hỗ trợ dot-notation (vd 'customer.id') — resolve qua getNestedValue.
2432
+ // Field hỗ trợ dot-notation (vd 'customer.id') và key có khoảng trắng (NSP-5745) — resolve qua resolveFieldValue.
2348
2433
  const buckets = new Map();
2349
2434
  for (const item of items) {
2350
2435
  const values = {};
2351
2436
  for (const f of fields)
2352
- values[f] = Utilities.getNestedValue(item.data, f);
2437
+ values[f] = resolveFieldValue(item.data, f);
2353
2438
  const key = Utilities.hash(values);
2354
2439
  let bucket = buckets.get(key);
2355
2440
  if (!bucket) {
@@ -2657,11 +2742,11 @@ class TableExportService {
2657
2742
  for (const item of exportItems) {
2658
2743
  const obj = {};
2659
2744
  const handle = async (exportColumn) => {
2660
- obj[exportColumn.field] = Utilities.getNestedValue(item, exportColumn.field);
2745
+ obj[exportColumn.field] = resolveFieldValue(item, exportColumn.field);
2661
2746
  const column = allColumns.find(e => e.field === exportColumn.field);
2662
2747
  const exportedColumn = allExportedColumns.find(e => e.field === exportColumn.field);
2663
2748
  if (exportedColumn?.transform) {
2664
- obj[exportedColumn.field] = exportedColumn.transform(Utilities.getNestedValue(item, exportedColumn.field), item);
2749
+ obj[exportedColumn.field] = exportedColumn.transform(resolveFieldValue(item, exportedColumn.field), item);
2665
2750
  return;
2666
2751
  }
2667
2752
  if (!column)
@@ -2676,7 +2761,7 @@ class TableExportService {
2676
2761
  }
2677
2762
  if (!columns.some(e => e.field === column.field))
2678
2763
  return;
2679
- const itemValue = Utilities.getNestedValue(item, column.field);
2764
+ const itemValue = resolveFieldValue(item, column.field);
2680
2765
  const fieldStr = column.field;
2681
2766
  if (column.transform) {
2682
2767
  const transform = column.transform(itemValue, item, { isExport: true });
@@ -2934,7 +3019,7 @@ class TableFormatService {
2934
3019
  const { option: { views, valueField, displayField }, } = column;
2935
3020
  cacheObjValues[fieldStr] = cacheObjValues[fieldStr] || {};
2936
3021
  const values = ArrayUtilities.distinct(items
2937
- .map(item => Utilities.getNestedValue(item.data, fieldStr))
3022
+ .map(item => resolveFieldValue(item.data, fieldStr))
2938
3023
  .filter(val => val?.toString())
2939
3024
  .reduce((current, next) => [...current, ...(Array.isArray(next) ? next : [next])], [])
2940
3025
  .filter(val => !Object.keys(cacheObjValues[fieldStr]).includes(val)));
@@ -2958,7 +3043,16 @@ class TableFormatService {
2958
3043
  // Format dữ liệu cho từng hàng
2959
3044
  for (const item of items) {
2960
3045
  const rowData = item.data;
2961
- const value = Utilities.getNestedValue(rowData, fieldStr);
3046
+ // why: đọc giá trị cũng thuộc phạm vi một ô (NSP-5745) — lỗi bất ngờ khi đọc field chỉ làm ô
3047
+ // hiện rỗng, không được biến lượt đọc thành công thành "Không thể tải dữ liệu".
3048
+ let value;
3049
+ try {
3050
+ value = resolveFieldValue(rowData, fieldStr);
3051
+ }
3052
+ catch (error) {
3053
+ report(error);
3054
+ value = undefined;
3055
+ }
2962
3056
  item.meta.display[fieldStr] = {
2963
3057
  badge: undefined,
2964
3058
  cellStyle: column.align === 'right' ? { 'text-align': 'right!important' } : undefined,
@@ -3161,7 +3255,7 @@ const matchesColumnFilter = (data, columns = [], rawColumnFilter = {}) => {
3161
3255
  for (const column of columns) {
3162
3256
  const { field, type } = column;
3163
3257
  const filterValue = (rawColumnFilter[field] || '').toString().trim().toLowerCase();
3164
- const rawColVal = Utilities.getNestedValue(data, field);
3258
+ const rawColVal = resolveFieldValue(data, field);
3165
3259
  const columnValue = (rawColVal || '').toString().trim().toLowerCase();
3166
3260
  if (filterValue) {
3167
3261
  if (!columnValue && type !== 'datetime' && type !== 'date' && type !== 'time') {
@@ -3261,8 +3355,8 @@ const filterLocalItems = (localItems, option, filterInfo) => {
3261
3355
  if (column) {
3262
3356
  const { type, field } = column;
3263
3357
  items.sort((tableItemCurrent, tableItemNext) => {
3264
- const dataVal = Utilities.getNestedValue(resolveItemData(tableItemCurrent), field);
3265
- const nextVal = Utilities.getNestedValue(resolveItemData(tableItemNext), field);
3358
+ const dataVal = resolveFieldValue(resolveItemData(tableItemCurrent), field);
3359
+ const nextVal = resolveFieldValue(resolveItemData(tableItemNext), field);
3266
3360
  return compareLocalValues(dataVal, nextVal, type, orderDirection);
3267
3361
  });
3268
3362
  }