@sd-angular/core 19.0.39 → 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.
- package/components/table/src/components/desktop-cell/desktop-cell.component.d.ts +1 -1
- package/components/table/src/services/field-value.util.d.ts +14 -0
- package/fesm2022/sd-angular-core-components-table.mjs +105 -11
- package/fesm2022/sd-angular-core-components-table.mjs.map +1 -1
- package/fesm2022/sd-angular-core-components-upload-file.mjs +5 -0
- package/fesm2022/sd-angular-core-components-upload-file.mjs.map +1 -1
- package/fesm2022/sd-angular-core-modules-auth.mjs.map +1 -1
- package/fesm2022/sd-angular-core-modules-layout.mjs.map +1 -1
- package/fesm2022/sd-angular-core-modules-permission.mjs.map +1 -1
- package/fesm2022/sd-angular-core-services-api.mjs +211 -8
- package/fesm2022/sd-angular-core-services-api.mjs.map +1 -1
- package/fesm2022/sd-angular-core-services-excel.mjs +5 -0
- package/fesm2022/sd-angular-core-services-excel.mjs.map +1 -1
- package/fesm2022/sd-angular-core-utilities-models.mjs.map +1 -1
- package/modules/auth/configurations/auth.configuration.d.ts +4 -3
- package/modules/layout/configurations/layout.configuration.d.ts +3 -2
- package/modules/permission/src/configurations/permission.configuration.d.ts +3 -2
- package/package.json +10 -10
- package/services/api/index.d.ts +1 -0
- package/services/api/src/api-handler-registry.d.ts +49 -0
- package/services/api/src/api-handler-selection.d.ts +10 -0
- package/services/api/src/api-route-configurations.d.ts +20 -0
- package/services/api/src/api.model.d.ts +7 -0
- 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<
|
|
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
|
|
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
|
|
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] =
|
|
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] =
|
|
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(
|
|
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 =
|
|
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 =>
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
3265
|
-
const nextVal =
|
|
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
|
}
|