@sd-angular/core 19.0.44 → 19.0.46
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 +6 -0
- package/assets/scss/core/form.scss +21 -11
- package/assets/scss/core/view.scss +24 -0
- package/assets/scss/sd-core.scss +1 -0
- package/assets/scss/themes/_material-tokens.scss +56 -0
- package/assets/scss/themes/default.scss +10 -0
- package/components/form-generic/src/components/form-builder/components/expression-builder/expression-builder.component.d.ts +1 -0
- package/components/form-generic/src/models/form-generic-expression.model.d.ts +10 -4
- package/components/form-generic/src/pipes/component-viewed.pipe.d.ts +1 -0
- package/components/import-excel/src/pipes/column-transform.pipe.d.ts +1 -0
- package/components/inform/src/inform.component.d.ts +12 -3
- package/components/table/src/services/table-export/table-export.service.d.ts +1 -1
- package/components/table/src/services/table-filter/table-filter.model.d.ts +7 -0
- package/components/table/src/services/table-local/table-local-sort.util.d.ts +8 -2
- package/components/table/src/services/table-local/table-local.util.d.ts +6 -1
- package/components/table/src/table.component.d.ts +1 -1
- package/components/timeline/pipes/view-date.pipe.d.ts +1 -0
- package/configurations/src/sd-core.configuration.d.ts +7 -0
- package/fesm2022/sd-angular-core-components-form-generic.mjs +44 -31
- package/fesm2022/sd-angular-core-components-form-generic.mjs.map +1 -1
- package/fesm2022/sd-angular-core-components-import-excel.mjs +19 -5
- package/fesm2022/sd-angular-core-components-import-excel.mjs.map +1 -1
- package/fesm2022/sd-angular-core-components-inform.mjs +16 -2
- package/fesm2022/sd-angular-core-components-inform.mjs.map +1 -1
- package/fesm2022/sd-angular-core-components-section.mjs +2 -2
- package/fesm2022/sd-angular-core-components-section.mjs.map +1 -1
- package/fesm2022/sd-angular-core-components-table.mjs +174 -66
- package/fesm2022/sd-angular-core-components-table.mjs.map +1 -1
- package/fesm2022/sd-angular-core-components-timeline.mjs +4 -1
- package/fesm2022/sd-angular-core-components-timeline.mjs.map +1 -1
- package/fesm2022/sd-angular-core-components-view.mjs +2 -2
- package/fesm2022/sd-angular-core-components-view.mjs.map +1 -1
- package/fesm2022/sd-angular-core-configurations.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-checkbox.mjs +2 -2
- package/fesm2022/sd-angular-core-forms-checkbox.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-date-range.mjs +15 -2
- package/fesm2022/sd-angular-core-forms-date-range.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-date.mjs +11 -1
- package/fesm2022/sd-angular-core-forms-date.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-datetime.mjs +43 -9
- package/fesm2022/sd-angular-core-forms-datetime.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-models.mjs +6 -0
- package/fesm2022/sd-angular-core-forms-models.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-radio.mjs +2 -2
- package/fesm2022/sd-angular-core-forms-radio.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-switch.mjs +2 -2
- package/fesm2022/sd-angular-core-forms-switch.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-textarea.mjs +2 -2
- package/fesm2022/sd-angular-core-forms-textarea.mjs.map +1 -1
- package/fesm2022/sd-angular-core-pipes.mjs +26 -10
- package/fesm2022/sd-angular-core-pipes.mjs.map +1 -1
- package/fesm2022/sd-angular-core-services-date-time.mjs +143 -0
- package/fesm2022/sd-angular-core-services-date-time.mjs.map +1 -0
- package/fesm2022/sd-angular-core-services.mjs +1 -0
- package/fesm2022/sd-angular-core-services.mjs.map +1 -1
- package/fesm2022/sd-angular-core-utilities-models.mjs.map +1 -1
- package/forms/datetime/src/datetime-time-zone.adapter.d.ts +15 -0
- package/forms/models/src/sd-strict-date-adapter.d.ts +3 -0
- package/package.json +17 -13
- package/pipes/src/time-different.pipe.d.ts +1 -0
- package/services/date-time/index.d.ts +1 -0
- package/services/date-time/src/date-time.service.d.ts +54 -0
- package/services/date-time/src/public-api.d.ts +1 -0
- package/services/index.d.ts +1 -0
- package/utilities/extensions/src/string.extension.d.ts +1 -0
- package/utilities/models/src/icon.model.d.ts +2 -0
|
@@ -7,8 +7,8 @@ import * as i7 from '@angular/material/sort';
|
|
|
7
7
|
import { MatSort, MatSortModule } from '@angular/material/sort';
|
|
8
8
|
import * as i6 from '@angular/material/table';
|
|
9
9
|
import { MatTable, MatTableModule } from '@angular/material/table';
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
10
|
+
import { ɵSD_LOCAL_DATE_TIME as _SD_LOCAL_DATE_TIME, SdDateTimeService } from '@sd-angular/core/services/date-time';
|
|
11
|
+
import { SdNotifyService } from '@sd-angular/core/services/notify';
|
|
12
12
|
import { Subject, fromEvent, Subscription, isObservable, firstValueFrom } from 'rxjs';
|
|
13
13
|
import { debounceTime, startWith, map, switchMap, filter } from 'rxjs/operators';
|
|
14
14
|
import { DateUtilities, StringUtilities } from '@sd-angular/core/utilities';
|
|
@@ -35,8 +35,8 @@ import { SdReadChannel, cloneReadRequest } from '@sd-angular/core/utilities/read
|
|
|
35
35
|
import { SdQuickAction } from '@sd-angular/core/components/quick-action';
|
|
36
36
|
import { SdTooltipDirective, SdScrollDirective, SdDesktopDirective, SdMobileDirective, SdHoverCopyDirective } from '@sd-angular/core/directives';
|
|
37
37
|
import { SdSafeHtmlPipe, SdFormatNumberPipe } from '@sd-angular/core/pipes';
|
|
38
|
-
import { Utilities, BrowserUtilities } from '@sdcorejs/utils/fns';
|
|
39
|
-
import {
|
|
38
|
+
import { Utilities, BrowserUtilities, NumberUtilities as NumberUtilities$1 } from '@sdcorejs/utils/fns';
|
|
39
|
+
import { ArrayUtilities, NumberUtilities } from '@sd-angular/core/utilities/extensions';
|
|
40
40
|
import { SdBadge } from '@sd-angular/core/components/badge';
|
|
41
41
|
import { SdOperator } from '@sd-angular/core/components/operator';
|
|
42
42
|
import { SdInput, SdInputNumber, SdSelect, SdDate, SdDatetime, SdDateRange as SdDateRange$1, SdCheckbox, SdSwitch } from '@sd-angular/core/forms';
|
|
@@ -51,10 +51,13 @@ import { MatButtonModule } from '@angular/material/button';
|
|
|
51
51
|
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
|
52
52
|
import { SdModal } from '@sd-angular/core/components/modal';
|
|
53
53
|
import { SdConfirmService } from '@sd-angular/core/services/confirm';
|
|
54
|
+
import * as i1$1 from '@sd-angular/core/services';
|
|
55
|
+
import { SdStorageService } from '@sd-angular/core/services';
|
|
54
56
|
import * as i2$1 from '@angular/material/tooltip';
|
|
55
57
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
56
58
|
import * as i5$1 from '@angular/material/chips';
|
|
57
59
|
import { MatChipsModule } from '@angular/material/chips';
|
|
60
|
+
import { SdExcelService } from '@sd-angular/core/services/excel';
|
|
58
61
|
|
|
59
62
|
class SdTableCellDefDirective {
|
|
60
63
|
static ngTemplateContextGuard(_dir, _ctx) {
|
|
@@ -164,8 +167,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImpo
|
|
|
164
167
|
}]
|
|
165
168
|
}] });
|
|
166
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Machine-zone date math, exactly the payload sent before time zones: the local bounds of
|
|
172
|
+
* `SdDateTimeService`, and a datetime still read by `new Date(value)`.
|
|
173
|
+
*/
|
|
174
|
+
const LOCAL_FILTER_DATES = { ..._SD_LOCAL_DATE_TIME, parse: value => new Date(value) };
|
|
175
|
+
const hasFilterValue = (value) => value !== undefined && value !== null && value !== '';
|
|
167
176
|
const SdConvertToPagingReq = (filterRequest, args) => {
|
|
168
177
|
const { externalFilters, columns, fieldMapping } = args;
|
|
178
|
+
const dates = args.dateTime && args.dateTime.timeZone !== 'local' ? args.dateTime : LOCAL_FILTER_DATES;
|
|
169
179
|
const req = {
|
|
170
180
|
filters: [],
|
|
171
181
|
orders: args?.orders || [],
|
|
@@ -209,14 +219,14 @@ const SdConvertToPagingReq = (filterRequest, args) => {
|
|
|
209
219
|
filters.push({
|
|
210
220
|
field,
|
|
211
221
|
operator: 'GREATER_OR_EQUAL',
|
|
212
|
-
data:
|
|
222
|
+
data: dates.begin(value?.from).toISOString(),
|
|
213
223
|
});
|
|
214
224
|
}
|
|
215
225
|
if (value?.to) {
|
|
216
226
|
filters.push({
|
|
217
227
|
field,
|
|
218
228
|
operator: 'LESS_THAN',
|
|
219
|
-
data:
|
|
229
|
+
data: dates.beginOfNextDay(value?.to).toISOString(),
|
|
220
230
|
});
|
|
221
231
|
}
|
|
222
232
|
}
|
|
@@ -227,14 +237,14 @@ const SdConvertToPagingReq = (filterRequest, args) => {
|
|
|
227
237
|
filters.push({
|
|
228
238
|
field,
|
|
229
239
|
operator: 'GREATER_OR_EQUAL',
|
|
230
|
-
data:
|
|
240
|
+
data: dates.begin(value?.from).toISOString(),
|
|
231
241
|
});
|
|
232
242
|
}
|
|
233
243
|
if (value?.to) {
|
|
234
244
|
filters.push({
|
|
235
245
|
field,
|
|
236
246
|
operator: 'LESS_THAN',
|
|
237
|
-
data:
|
|
247
|
+
data: dates.beginOfNextDay(value?.to).toISOString(),
|
|
238
248
|
});
|
|
239
249
|
}
|
|
240
250
|
}
|
|
@@ -244,14 +254,14 @@ const SdConvertToPagingReq = (filterRequest, args) => {
|
|
|
244
254
|
filters.push({
|
|
245
255
|
field,
|
|
246
256
|
operator: 'GREATER_OR_EQUAL',
|
|
247
|
-
data:
|
|
257
|
+
data: dates.begin(value).toISOString(),
|
|
248
258
|
});
|
|
249
259
|
}
|
|
250
260
|
if (externalFilter.defaultOperator === 'LESS_OR_EQUAL') {
|
|
251
261
|
filters.push({
|
|
252
262
|
field,
|
|
253
263
|
operator: 'LESS_THAN',
|
|
254
|
-
data:
|
|
264
|
+
data: dates.beginOfNextDay(value).toISOString(),
|
|
255
265
|
});
|
|
256
266
|
}
|
|
257
267
|
}
|
|
@@ -260,14 +270,14 @@ const SdConvertToPagingReq = (filterRequest, args) => {
|
|
|
260
270
|
filters.push({
|
|
261
271
|
field,
|
|
262
272
|
operator: 'GREATER_OR_EQUAL',
|
|
263
|
-
data:
|
|
273
|
+
data: dates.parse(value).toISOString(),
|
|
264
274
|
});
|
|
265
275
|
}
|
|
266
276
|
if (externalFilter.defaultOperator === 'LESS_OR_EQUAL') {
|
|
267
277
|
filters.push({
|
|
268
278
|
field,
|
|
269
279
|
operator: 'LESS_OR_EQUAL',
|
|
270
|
-
data:
|
|
280
|
+
data: dates.parse(value).toISOString(),
|
|
271
281
|
});
|
|
272
282
|
}
|
|
273
283
|
}
|
|
@@ -288,14 +298,14 @@ const SdConvertToPagingReq = (filterRequest, args) => {
|
|
|
288
298
|
filters.push({
|
|
289
299
|
field,
|
|
290
300
|
operator: 'GREATER_OR_EQUAL',
|
|
291
|
-
data:
|
|
301
|
+
data: dates.begin(value?.from).toISOString(),
|
|
292
302
|
});
|
|
293
303
|
}
|
|
294
304
|
if (value?.to) {
|
|
295
305
|
filters.push({
|
|
296
306
|
field,
|
|
297
307
|
operator: 'LESS_THAN',
|
|
298
|
-
data:
|
|
308
|
+
data: dates.beginOfNextDay(value?.to).toISOString(),
|
|
299
309
|
});
|
|
300
310
|
}
|
|
301
311
|
}
|
|
@@ -337,8 +347,8 @@ const SdConvertToPagingReq = (filterRequest, args) => {
|
|
|
337
347
|
field,
|
|
338
348
|
operator: 'BETWEEN',
|
|
339
349
|
data: {
|
|
340
|
-
from:
|
|
341
|
-
to:
|
|
350
|
+
from: dates.begin(value?.from).toISOString(),
|
|
351
|
+
to: dates.end(value?.to).toISOString(),
|
|
342
352
|
},
|
|
343
353
|
});
|
|
344
354
|
}
|
|
@@ -346,14 +356,14 @@ const SdConvertToPagingReq = (filterRequest, args) => {
|
|
|
346
356
|
filters.push({
|
|
347
357
|
field,
|
|
348
358
|
operator: 'GREATER_OR_EQUAL',
|
|
349
|
-
data:
|
|
359
|
+
data: dates.begin(value?.from).toISOString(),
|
|
350
360
|
});
|
|
351
361
|
}
|
|
352
362
|
else if (value?.to) {
|
|
353
363
|
filters.push({
|
|
354
364
|
field,
|
|
355
365
|
operator: 'LESS_THAN',
|
|
356
|
-
data:
|
|
366
|
+
data: dates.beginOfNextDay(value?.to).toISOString(),
|
|
357
367
|
});
|
|
358
368
|
}
|
|
359
369
|
}
|
|
@@ -363,13 +373,39 @@ const SdConvertToPagingReq = (filterRequest, args) => {
|
|
|
363
373
|
field,
|
|
364
374
|
operator: 'BETWEEN',
|
|
365
375
|
data: {
|
|
366
|
-
from:
|
|
367
|
-
to:
|
|
376
|
+
from: dates.begin(value).toISOString(),
|
|
377
|
+
to: dates.end(value).toISOString(),
|
|
368
378
|
},
|
|
369
379
|
});
|
|
370
380
|
}
|
|
371
381
|
}
|
|
372
382
|
}
|
|
383
|
+
else if (column.type === 'number' && typeof value === 'object' && !Array.isArray(value) && ('from' in value || 'to' in value)) {
|
|
384
|
+
// why: the split-number filter emits an inclusive `{ from, to }`; a missing end leaves that side open, and 0 is a bound.
|
|
385
|
+
const hasFrom = hasFilterValue(value.from);
|
|
386
|
+
const hasTo = hasFilterValue(value.to);
|
|
387
|
+
if (hasFrom && hasTo) {
|
|
388
|
+
filters.push({
|
|
389
|
+
field,
|
|
390
|
+
operator: 'BETWEEN',
|
|
391
|
+
data: { from: value.from, to: value.to },
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
else if (hasFrom) {
|
|
395
|
+
filters.push({
|
|
396
|
+
field,
|
|
397
|
+
operator: 'GREATER_OR_EQUAL',
|
|
398
|
+
data: value.from,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
else if (hasTo) {
|
|
402
|
+
filters.push({
|
|
403
|
+
field,
|
|
404
|
+
operator: 'LESS_OR_EQUAL',
|
|
405
|
+
data: value.to,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
373
409
|
else {
|
|
374
410
|
if (Array.isArray(value)) {
|
|
375
411
|
if (value.length) {
|
|
@@ -2658,6 +2694,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImpo
|
|
|
2658
2694
|
class TableExportService {
|
|
2659
2695
|
#excelService = inject(SdExcelService);
|
|
2660
2696
|
#i18n = inject(I18nService);
|
|
2697
|
+
// why: date cells are written as the wall clock of ISdCoreConfiguration.timeZone, like the table shows them.
|
|
2698
|
+
#dateTime = inject(SdDateTimeService);
|
|
2661
2699
|
// ==========================================
|
|
2662
2700
|
// SIGNAL STATE (Component sẽ bind trực tiếp vào đây)
|
|
2663
2701
|
// ==========================================
|
|
@@ -2788,13 +2826,13 @@ class TableExportService {
|
|
|
2788
2826
|
}
|
|
2789
2827
|
}
|
|
2790
2828
|
else if (column.type === 'date') {
|
|
2791
|
-
obj[fieldStr] =
|
|
2829
|
+
obj[fieldStr] = this.#dateTime.toFormat(itemValue, 'yyyy/MM/dd');
|
|
2792
2830
|
}
|
|
2793
2831
|
else if (column.type === 'datetime') {
|
|
2794
|
-
obj[fieldStr] =
|
|
2832
|
+
obj[fieldStr] = this.#dateTime.toFormat(itemValue, 'yyyy/MM/dd HH:mm:ss');
|
|
2795
2833
|
}
|
|
2796
2834
|
else if (column.type === 'time') {
|
|
2797
|
-
obj[fieldStr] =
|
|
2835
|
+
obj[fieldStr] = this.#dateTime.toFormat(itemValue, 'HH:mm:ss');
|
|
2798
2836
|
}
|
|
2799
2837
|
else if (column.type === 'values' || column.type === 'lazy-values') {
|
|
2800
2838
|
const vals = (Array.isArray(itemValue) ? itemValue : [itemValue]).filter(e => !!e?.toString());
|
|
@@ -2930,6 +2968,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImpo
|
|
|
2930
2968
|
class TableFormatService {
|
|
2931
2969
|
// Inject các pipe/service dùng để format dữ liệu
|
|
2932
2970
|
#formatNumberPipe = inject(SdFormatNumberPipe);
|
|
2971
|
+
// why: date cells show the wall clock of ISdCoreConfiguration.timeZone.
|
|
2972
|
+
#dateTime = inject(SdDateTimeService);
|
|
2933
2973
|
// ==========================================
|
|
2934
2974
|
// PUBLIC METHODS
|
|
2935
2975
|
// ==========================================
|
|
@@ -3157,8 +3197,8 @@ class TableFormatService {
|
|
|
3157
3197
|
return text || EMPTY_STR;
|
|
3158
3198
|
}
|
|
3159
3199
|
#formatDateDisplay(value, type) {
|
|
3160
|
-
const date =
|
|
3161
|
-
const time =
|
|
3200
|
+
const date = this.#dateTime.toFormat(value, 'dd/MM/yyyy');
|
|
3201
|
+
const time = this.#dateTime.toFormat(value, 'HH:mm:ss');
|
|
3162
3202
|
if (type === 'datetime') {
|
|
3163
3203
|
return time && date ? `<div class="T14R">${date}<span class="T14R text-black400 ml-4">${time}</span></div>` : '';
|
|
3164
3204
|
}
|
|
@@ -3219,7 +3259,7 @@ const buildColumnWidthMap = (column) => {
|
|
|
3219
3259
|
return result;
|
|
3220
3260
|
};
|
|
3221
3261
|
|
|
3222
|
-
const sortValue = (value, type) => {
|
|
3262
|
+
const sortValue = (value, type, dateTime) => {
|
|
3223
3263
|
if (value === null || value === undefined || value === '')
|
|
3224
3264
|
return null;
|
|
3225
3265
|
if (type === 'number') {
|
|
@@ -3227,15 +3267,21 @@ const sortValue = (value, type) => {
|
|
|
3227
3267
|
return Number.isNaN(number) ? null : number;
|
|
3228
3268
|
}
|
|
3229
3269
|
if (type === 'date' || type === 'datetime' || type === 'time') {
|
|
3230
|
-
|
|
3270
|
+
// why: a time of day has no zone, so only date/datetime values are read in the configured zone.
|
|
3271
|
+
const zone = type !== 'time' && dateTime && dateTime.timeZone !== 'local' ? dateTime : undefined;
|
|
3272
|
+
const parse = (input) => (zone ? (zone.parse(input)?.getTime() ?? NaN) : new Date(input).getTime());
|
|
3273
|
+
const time = value instanceof Date ? value.getTime() : typeof value === 'string' || typeof value === 'number' ? parse(value) : NaN;
|
|
3231
3274
|
return Number.isNaN(time) ? null : time;
|
|
3232
3275
|
}
|
|
3233
3276
|
return String(value);
|
|
3234
3277
|
};
|
|
3235
|
-
/**
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3278
|
+
/**
|
|
3279
|
+
* Internal local-sort comparison; intentionally not exported from the table entry point.
|
|
3280
|
+
* `dateTime` reads zone-less dates in `ISdCoreConfiguration.timeZone`; omitted or `'local'` keeps the machine zone.
|
|
3281
|
+
*/
|
|
3282
|
+
const compareLocalValues = (a, b, type, direction, dateTime) => {
|
|
3283
|
+
const first = sortValue(a, type, dateTime);
|
|
3284
|
+
const second = sortValue(b, type, dateTime);
|
|
3239
3285
|
// why: xếp rỗng trước khi xét chiều để DESC vẫn giữ rỗng ở cuối và không đảo các dòng bằng nhau.
|
|
3240
3286
|
if (first === null)
|
|
3241
3287
|
return second === null ? 0 : 1;
|
|
@@ -3248,26 +3294,100 @@ const compareLocalValues = (a, b, type, direction) => {
|
|
|
3248
3294
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3249
3295
|
const isTableItem = (item) => !!item && typeof item === 'object' && 'data' in item && 'meta' in item;
|
|
3250
3296
|
const resolveItemData = (item) => (isTableItem(item) ? item.data : item);
|
|
3297
|
+
const hasValue = (value) => value !== undefined && value !== null && value !== '';
|
|
3251
3298
|
const hasActiveColumnFilter = (rawColumnFilter = {}, columns = []) => columns.some(col => {
|
|
3252
3299
|
const v = rawColumnFilter[col.field];
|
|
3253
3300
|
if (Array.isArray(v))
|
|
3254
3301
|
return v.length > 0;
|
|
3302
|
+
// why: a split-number bound of 0 is a value, not an empty end.
|
|
3255
3303
|
if (v && typeof v === 'object')
|
|
3256
|
-
return
|
|
3257
|
-
return v
|
|
3304
|
+
return hasValue(v.from) || hasValue(v.to);
|
|
3305
|
+
return hasValue(v);
|
|
3258
3306
|
});
|
|
3259
|
-
|
|
3307
|
+
// why: the same fallback SdConvertToPagingReq uses, so a local table filters like its server twin.
|
|
3308
|
+
const resolveOperator = (column, columnOperator) => columnOperator[column.field] || column.filter?.operator?.default;
|
|
3309
|
+
// Both sides are already trimmed and lowercased; no operator keeps the historical CONTAIN.
|
|
3310
|
+
const matchesText = (value, operator, operand) => {
|
|
3311
|
+
switch (operator) {
|
|
3312
|
+
case 'EQUAL':
|
|
3313
|
+
return value === operand;
|
|
3314
|
+
case 'NOT_EQUAL':
|
|
3315
|
+
return value !== operand;
|
|
3316
|
+
case 'NOT_CONTAIN':
|
|
3317
|
+
return !value.includes(operand);
|
|
3318
|
+
case 'START_WITH':
|
|
3319
|
+
return value.startsWith(operand);
|
|
3320
|
+
case 'NOT_START_WITH':
|
|
3321
|
+
return !value.startsWith(operand);
|
|
3322
|
+
case 'END_WITH':
|
|
3323
|
+
return value.endsWith(operand);
|
|
3324
|
+
case 'NOT_END_WITH':
|
|
3325
|
+
return !value.endsWith(operand);
|
|
3326
|
+
default:
|
|
3327
|
+
return value.includes(operand);
|
|
3328
|
+
}
|
|
3329
|
+
};
|
|
3330
|
+
const NUMBER_PREFIX_OPERATORS = {
|
|
3331
|
+
'>=': 'GREATER_OR_EQUAL',
|
|
3332
|
+
'<=': 'LESS_OR_EQUAL',
|
|
3333
|
+
'>': 'GREATER_THAN',
|
|
3334
|
+
'<': 'LESS_THAN',
|
|
3335
|
+
};
|
|
3336
|
+
const compareNumber = (value, operator, operand) => {
|
|
3337
|
+
switch (operator) {
|
|
3338
|
+
case 'NOT_EQUAL':
|
|
3339
|
+
return value !== operand;
|
|
3340
|
+
case 'GREATER_THAN':
|
|
3341
|
+
return value > operand;
|
|
3342
|
+
case 'GREATER_OR_EQUAL':
|
|
3343
|
+
return value >= operand;
|
|
3344
|
+
case 'LESS_THAN':
|
|
3345
|
+
return value < operand;
|
|
3346
|
+
case 'LESS_OR_EQUAL':
|
|
3347
|
+
return value <= operand;
|
|
3348
|
+
default:
|
|
3349
|
+
return value === operand;
|
|
3350
|
+
}
|
|
3351
|
+
};
|
|
3352
|
+
const toFiniteNumber = (value) => NumberUtilities$1.parseFiniteNumber(value, { trim: true });
|
|
3353
|
+
const matchesNumberFilter = (rawValue, filter, operator) => {
|
|
3354
|
+
// why: the split-number filter emits `{ from, to }`; a missing end leaves that side open.
|
|
3355
|
+
if (filter && typeof filter === 'object' && !Array.isArray(filter)) {
|
|
3356
|
+
const from = toFiniteNumber(filter.from);
|
|
3357
|
+
const to = toFiniteNumber(filter.to);
|
|
3358
|
+
if (from === null && to === null)
|
|
3359
|
+
return true;
|
|
3360
|
+
const value = toFiniteNumber(rawValue);
|
|
3361
|
+
return value !== null && (from === null || value >= from) && (to === null || value <= to);
|
|
3362
|
+
}
|
|
3363
|
+
// why: a typed prefix such as `>=5` is explicit, so it wins over the operator dropdown.
|
|
3364
|
+
const prefixed = typeof filter === 'string' ? /^\s*(>=|<=|>|<)(.*)$/.exec(filter) : null;
|
|
3365
|
+
const operand = toFiniteNumber(prefixed ? prefixed[2] : filter);
|
|
3366
|
+
if (operand === null)
|
|
3367
|
+
return true;
|
|
3368
|
+
const value = toFiniteNumber(rawValue);
|
|
3369
|
+
return value !== null && compareNumber(value, prefixed ? NUMBER_PREFIX_OPERATORS[prefixed[1]] : operator, operand);
|
|
3370
|
+
};
|
|
3371
|
+
const matchesColumnFilter = (data, columns = [], rawColumnFilter = {}, dateTime, columnOperator = {}) => {
|
|
3372
|
+
const zoned = dateTime && dateTime.timeZone !== 'local' ? dateTime : undefined;
|
|
3260
3373
|
for (const column of columns) {
|
|
3261
3374
|
const { field, type } = column;
|
|
3262
|
-
const filterValue = (rawColumnFilter[field] || '').toString().trim().toLowerCase();
|
|
3263
3375
|
const rawColVal = resolveFieldValue(data, field);
|
|
3376
|
+
// why: numbers bypass the text normalisation below, which reads a filter or row value of 0 as empty.
|
|
3377
|
+
if (type === 'number') {
|
|
3378
|
+
if (!matchesNumberFilter(rawColVal, rawColumnFilter[field], resolveOperator(column, columnOperator))) {
|
|
3379
|
+
return false;
|
|
3380
|
+
}
|
|
3381
|
+
continue;
|
|
3382
|
+
}
|
|
3383
|
+
const filterValue = (rawColumnFilter[field] || '').toString().trim().toLowerCase();
|
|
3264
3384
|
const columnValue = (rawColVal || '').toString().trim().toLowerCase();
|
|
3265
3385
|
if (filterValue) {
|
|
3266
3386
|
if (!columnValue && type !== 'datetime' && type !== 'date' && type !== 'time') {
|
|
3267
3387
|
return false;
|
|
3268
3388
|
}
|
|
3269
3389
|
if (type === 'string') {
|
|
3270
|
-
if (columnValue
|
|
3390
|
+
if (!matchesText(columnValue, resolveOperator(column, columnOperator), filterValue)) {
|
|
3271
3391
|
return false;
|
|
3272
3392
|
}
|
|
3273
3393
|
}
|
|
@@ -3289,30 +3409,6 @@ const matchesColumnFilter = (data, columns = [], rawColumnFilter = {}) => {
|
|
|
3289
3409
|
return false;
|
|
3290
3410
|
}
|
|
3291
3411
|
}
|
|
3292
|
-
else if (type === 'number') {
|
|
3293
|
-
const fValue = +filterValue.replace('>=', '').replace('<=', '').replace('>', '').replace('<', '');
|
|
3294
|
-
const cValue = +columnValue;
|
|
3295
|
-
if (fValue || fValue === 0) {
|
|
3296
|
-
if (!cValue && cValue !== 0) {
|
|
3297
|
-
return false;
|
|
3298
|
-
}
|
|
3299
|
-
if (filterValue.indexOf('>=') > -1 && cValue < fValue) {
|
|
3300
|
-
return false;
|
|
3301
|
-
}
|
|
3302
|
-
else if (filterValue.indexOf('<=') > -1 && cValue > fValue) {
|
|
3303
|
-
return false;
|
|
3304
|
-
}
|
|
3305
|
-
else if (filterValue.indexOf('<') > -1 && cValue >= fValue) {
|
|
3306
|
-
return false;
|
|
3307
|
-
}
|
|
3308
|
-
else if (filterValue.indexOf('>') > -1 && cValue <= fValue) {
|
|
3309
|
-
return false;
|
|
3310
|
-
}
|
|
3311
|
-
else if (cValue !== fValue) {
|
|
3312
|
-
return false;
|
|
3313
|
-
}
|
|
3314
|
-
}
|
|
3315
|
-
}
|
|
3316
3412
|
else if (type === 'boolean') {
|
|
3317
3413
|
if ((filterValue === '1' || filterValue === 'true') && columnValue !== '1' && columnValue !== 'true') {
|
|
3318
3414
|
return false;
|
|
@@ -3324,13 +3420,16 @@ const matchesColumnFilter = (data, columns = [], rawColumnFilter = {}) => {
|
|
|
3324
3420
|
else if (type === 'datetime' || type === 'date' || type === 'time') {
|
|
3325
3421
|
const from = rawColumnFilter[field]?.from ?? rawColumnFilter[field];
|
|
3326
3422
|
const to = rawColumnFilter[field]?.to ?? rawColumnFilter[field];
|
|
3327
|
-
|
|
3328
|
-
const
|
|
3423
|
+
// why: a time of day has no zone, so only date/datetime rows are read in the configured zone.
|
|
3424
|
+
const zone = type === 'time' ? undefined : zoned;
|
|
3425
|
+
const fromDate = (zone ?? _SD_LOCAL_DATE_TIME).begin(from);
|
|
3426
|
+
const toDate = (zone ?? _SD_LOCAL_DATE_TIME).end(to);
|
|
3329
3427
|
if (fromDate || toDate) {
|
|
3330
3428
|
if (!columnValue) {
|
|
3331
3429
|
return false;
|
|
3332
3430
|
}
|
|
3333
|
-
|
|
3431
|
+
// why: the zone branch parses the raw row value; `columnValue` is lowercased for text matching.
|
|
3432
|
+
const columnTime = zone ? (zone.parse(rawColVal)?.getTime() ?? NaN) : new Date(columnValue).getTime();
|
|
3334
3433
|
const fromDateTime = fromDate?.getTime() || null;
|
|
3335
3434
|
const toDateTime = toDate?.getTime() || null;
|
|
3336
3435
|
if (fromDateTime && fromDateTime > columnTime) {
|
|
@@ -3348,7 +3447,7 @@ const matchesColumnFilter = (data, columns = [], rawColumnFilter = {}) => {
|
|
|
3348
3447
|
const filterLocalItems = (localItems, option, filterInfo) => {
|
|
3349
3448
|
const { columns } = option;
|
|
3350
3449
|
const { rawColumnFilter, orderBy, orderDirection, pageSize, pageNumber } = filterInfo;
|
|
3351
|
-
const matchesData = (data) => matchesColumnFilter(data, columns, rawColumnFilter);
|
|
3450
|
+
const matchesData = (data) => matchesColumnFilter(data, columns, rawColumnFilter, option.dateTime, filterInfo.columnOperator);
|
|
3352
3451
|
const treeOpt = option.tree;
|
|
3353
3452
|
const treeSearch = option.type === 'local' && treeOpt?.loadType === 'static' && hasActiveColumnFilter(rawColumnFilter, columns);
|
|
3354
3453
|
const treeSearchPredicate = treeSearch ? matchesData : undefined;
|
|
@@ -3362,7 +3461,7 @@ const filterLocalItems = (localItems, option, filterInfo) => {
|
|
|
3362
3461
|
items.sort((tableItemCurrent, tableItemNext) => {
|
|
3363
3462
|
const dataVal = resolveFieldValue(resolveItemData(tableItemCurrent), field);
|
|
3364
3463
|
const nextVal = resolveFieldValue(resolveItemData(tableItemNext), field);
|
|
3365
|
-
return compareLocalValues(dataVal, nextVal, type, orderDirection);
|
|
3464
|
+
return compareLocalValues(dataVal, nextVal, type, orderDirection, option.dateTime);
|
|
3366
3465
|
});
|
|
3367
3466
|
}
|
|
3368
3467
|
}
|
|
@@ -3853,6 +3952,8 @@ class SdTable {
|
|
|
3853
3952
|
#i18n = inject(I18nService);
|
|
3854
3953
|
#tableFormatService = inject(TableFormatService);
|
|
3855
3954
|
#tableExportService = inject(TableExportService);
|
|
3955
|
+
// why: filters and sort read dates in ISdCoreConfiguration.timeZone; the pure helpers take it as an argument.
|
|
3956
|
+
#dateTime = inject(SdDateTimeService);
|
|
3856
3957
|
// Expose state signals từ TableExportService cho template binding.
|
|
3857
3958
|
exporting = this.#tableExportService.exporting;
|
|
3858
3959
|
exportTitle = this.#tableExportService.exportTitle;
|
|
@@ -4146,7 +4247,7 @@ class SdTable {
|
|
|
4146
4247
|
Object.assign(cur, next);
|
|
4147
4248
|
};
|
|
4148
4249
|
#filterLocal = (localItems, filterInfo) => {
|
|
4149
|
-
const result = filterLocalItems(localItems, this.tableOption(), filterInfo);
|
|
4250
|
+
const result = filterLocalItems(localItems, { ...this.tableOption(), dateTime: this.#dateTime }, filterInfo);
|
|
4150
4251
|
this.#treeSearchPredicate = result.treeSearchPredicate;
|
|
4151
4252
|
return {
|
|
4152
4253
|
items: result.items,
|
|
@@ -4240,6 +4341,7 @@ class SdTable {
|
|
|
4240
4341
|
const pagingReq = SdConvertToPagingReq(snapshot, {
|
|
4241
4342
|
columns: opt.columns,
|
|
4242
4343
|
externalFilters: opt.filter?.externalFilters,
|
|
4344
|
+
dateTime: this.#dateTime,
|
|
4243
4345
|
});
|
|
4244
4346
|
const optionRevision = this.#optionRevision;
|
|
4245
4347
|
const sourceOption = this.option();
|
|
@@ -4449,6 +4551,7 @@ class SdTable {
|
|
|
4449
4551
|
const pagingReq = SdConvertToPagingReq(filterReq, {
|
|
4450
4552
|
columns: opt.columns,
|
|
4451
4553
|
externalFilters: opt.filter?.externalFilters,
|
|
4554
|
+
dateTime: this.#dateTime,
|
|
4452
4555
|
});
|
|
4453
4556
|
const result = opt.items(filterReq, pagingReq);
|
|
4454
4557
|
return await result;
|
|
@@ -4827,6 +4930,11 @@ class SdTable {
|
|
|
4827
4930
|
if (operator == null)
|
|
4828
4931
|
return;
|
|
4829
4932
|
this.tableOption()?.filter?.operatorChange?.(column, operator);
|
|
4933
|
+
// why: a new operator changes what the current value means, so filter again; an empty cell only keeps the operator.
|
|
4934
|
+
if (hasActiveColumnFilter(this.columnFilter, [column]))
|
|
4935
|
+
this.onFilterChange();
|
|
4936
|
+
else
|
|
4937
|
+
this.onFilterCommit();
|
|
4830
4938
|
};
|
|
4831
4939
|
trackBy = (index, item) => {
|
|
4832
4940
|
return item.meta.id;
|