@sdcorejs/angular 21.1.5 → 21.2.0
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 +12 -4
- package/assets/scss/core/menu.scss +18 -0
- package/assets/scss/core/utilities/_misc.scss +15 -0
- package/assets/scss/sd-core.scss +1 -0
- package/fesm2022/sdcorejs-angular-components-anchor.mjs +37 -13
- package/fesm2022/sdcorejs-angular-components-anchor.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-autoid-inspector.mjs +2 -2
- package/fesm2022/sdcorejs-angular-components-autoid-inspector.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-badge.mjs +15 -2
- package/fesm2022/sdcorejs-angular-components-badge.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-button.mjs +2 -2
- package/fesm2022/sdcorejs-angular-components-button.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-ckeditor-styles.mjs +1 -1
- package/fesm2022/sdcorejs-angular-components-ckeditor-styles.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-code-editor.mjs +3 -3
- package/fesm2022/sdcorejs-angular-components-code-editor.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-data-state.mjs +3 -3
- package/fesm2022/sdcorejs-angular-components-data-state.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-editor.mjs +2 -2
- package/fesm2022/sdcorejs-angular-components-editor.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-form-generic.mjs +324 -192
- package/fesm2022/sdcorejs-angular-components-form-generic.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-history.mjs +4 -4
- package/fesm2022/sdcorejs-angular-components-history.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-import-excel.mjs +13 -5
- package/fesm2022/sdcorejs-angular-components-import-excel.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-inform.mjs +3 -3
- package/fesm2022/sdcorejs-angular-components-inform.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-modal-resizable.mjs +164 -8
- package/fesm2022/sdcorejs-angular-components-modal-resizable.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-modal.mjs +30 -18
- package/fesm2022/sdcorejs-angular-components-modal.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-org-chart.mjs +60 -13
- package/fesm2022/sdcorejs-angular-components-org-chart.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-preview.mjs +58 -19
- package/fesm2022/sdcorejs-angular-components-preview.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-query-bar.mjs +192 -30
- package/fesm2022/sdcorejs-angular-components-query-bar.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-query-builder.mjs +187 -127
- package/fesm2022/sdcorejs-angular-components-query-builder.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-section.mjs +16 -2
- package/fesm2022/sdcorejs-angular-components-section.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-side-drawer.mjs +83 -24
- package/fesm2022/sdcorejs-angular-components-side-drawer.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-splitter.mjs +57 -21
- package/fesm2022/sdcorejs-angular-components-splitter.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-stepper.mjs +2 -2
- package/fesm2022/sdcorejs-angular-components-stepper.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-tab-router.mjs +68 -20
- package/fesm2022/sdcorejs-angular-components-tab-router.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-tab.mjs +3 -2
- package/fesm2022/sdcorejs-angular-components-tab.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-table.mjs +557 -255
- package/fesm2022/sdcorejs-angular-components-table.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-tree.mjs +80 -17
- package/fesm2022/sdcorejs-angular-components-tree.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-upload-file.mjs +48 -13
- package/fesm2022/sdcorejs-angular-components-upload-file.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components.mjs +6 -1
- package/fesm2022/sdcorejs-angular-components.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-configurations.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-directives.mjs +45 -8
- package/fesm2022/sdcorejs-angular-directives.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-autocomplete.mjs +40 -29
- package/fesm2022/sdcorejs-angular-forms-autocomplete.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-checkbox.mjs +5 -5
- package/fesm2022/sdcorejs-angular-forms-checkbox.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-chip-calendar.mjs +67 -50
- package/fesm2022/sdcorejs-angular-forms-chip-calendar.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-chip.mjs +57 -43
- package/fesm2022/sdcorejs-angular-forms-chip.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-date-range.mjs +234 -64
- package/fesm2022/sdcorejs-angular-forms-date-range.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-date.mjs +209 -42
- package/fesm2022/sdcorejs-angular-forms-date.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-datetime.mjs +131 -27
- package/fesm2022/sdcorejs-angular-forms-datetime.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-entity-picker.mjs +40 -16
- package/fesm2022/sdcorejs-angular-forms-entity-picker.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-input-color.mjs +16 -5
- package/fesm2022/sdcorejs-angular-forms-input-color.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-input-number.mjs +24 -9
- package/fesm2022/sdcorejs-angular-forms-input-number.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-input.mjs +58 -10
- package/fesm2022/sdcorejs-angular-forms-input.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-models.mjs +279 -6
- package/fesm2022/sdcorejs-angular-forms-models.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-radio.mjs +4 -4
- package/fesm2022/sdcorejs-angular-forms-radio.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-select.mjs +136 -38
- package/fesm2022/sdcorejs-angular-forms-select.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-switch.mjs +18 -3
- package/fesm2022/sdcorejs-angular-forms-switch.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-textarea.mjs +51 -62
- package/fesm2022/sdcorejs-angular-forms-textarea.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-time-range.mjs +57 -12
- package/fesm2022/sdcorejs-angular-forms-time-range.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-time.mjs +16 -5
- package/fesm2022/sdcorejs-angular-forms-time.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-tree-select.mjs +100 -9
- package/fesm2022/sdcorejs-angular-forms-tree-select.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms.mjs +91 -46
- package/fesm2022/sdcorejs-angular-forms.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-handlers.mjs +18 -3
- package/fesm2022/sdcorejs-angular-handlers.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-i18n.mjs +510 -68
- package/fesm2022/sdcorejs-angular-i18n.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-interceptors.mjs +87 -29
- package/fesm2022/sdcorejs-angular-interceptors.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-modules-auth.mjs +25 -15
- package/fesm2022/sdcorejs-angular-modules-auth.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-modules-icon.mjs +2 -2
- package/fesm2022/sdcorejs-angular-modules-icon.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-modules-keycloak.mjs +32 -3
- package/fesm2022/sdcorejs-angular-modules-keycloak.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-modules-layout.mjs +441 -135
- package/fesm2022/sdcorejs-angular-modules-layout.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-modules-permission.mjs +177 -40
- package/fesm2022/sdcorejs-angular-modules-permission.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-pipes.mjs +62 -28
- package/fesm2022/sdcorejs-angular-pipes.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-api.mjs +123 -13
- package/fesm2022/sdcorejs-angular-services-api.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-cache.mjs +53 -3
- package/fesm2022/sdcorejs-angular-services-cache.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-confirm.mjs +25 -73
- package/fesm2022/sdcorejs-angular-services-confirm.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-excel.mjs +12 -10
- package/fesm2022/sdcorejs-angular-services-excel.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-loading.mjs +75 -84
- package/fesm2022/sdcorejs-angular-services-loading.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-notify.mjs +46 -5
- package/fesm2022/sdcorejs-angular-services-notify.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-storage.mjs +4 -0
- package/fesm2022/sdcorejs-angular-services-storage.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services.mjs +0 -1
- package/fesm2022/sdcorejs-angular-services.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-utilities-extensions.mjs +225 -80
- package/fesm2022/sdcorejs-angular-utilities-extensions.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-utilities-models.mjs +1 -63
- package/fesm2022/sdcorejs-angular-utilities-models.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular.mjs +17 -10
- package/fesm2022/sdcorejs-angular.mjs.map +1 -1
- package/package.json +40 -25
- package/types/sdcorejs-angular-components-anchor.d.ts +3 -1
- package/types/sdcorejs-angular-components-badge.d.ts +1 -0
- package/types/sdcorejs-angular-components-ckeditor-styles.d.ts +1 -1
- package/types/sdcorejs-angular-components-form-generic.d.ts +44 -33
- package/types/sdcorejs-angular-components-import-excel.d.ts +1 -0
- package/types/sdcorejs-angular-components-mini-editor.d.ts +1 -1
- package/types/sdcorejs-angular-components-modal-resizable.d.ts +92 -0
- package/types/sdcorejs-angular-components-modal.d.ts +3 -2
- package/types/sdcorejs-angular-components-org-chart.d.ts +6 -0
- package/types/sdcorejs-angular-components-preview.d.ts +1 -0
- package/types/sdcorejs-angular-components-query-bar.d.ts +55 -17
- package/types/sdcorejs-angular-components-query-builder.d.ts +150 -98
- package/types/sdcorejs-angular-components-section.d.ts +3 -0
- package/types/sdcorejs-angular-components-tab-router.d.ts +19 -19
- package/types/sdcorejs-angular-components-table.d.ts +252 -110
- package/types/sdcorejs-angular-components-tree.d.ts +48 -30
- package/types/sdcorejs-angular-components-upload-file.d.ts +6 -3
- package/types/sdcorejs-angular-components.d.ts +6 -1
- package/types/sdcorejs-angular-configurations.d.ts +1 -2
- package/types/sdcorejs-angular-forms-autocomplete.d.ts +5 -2
- package/types/sdcorejs-angular-forms-chip-calendar.d.ts +10 -0
- package/types/sdcorejs-angular-forms-date-range.d.ts +14 -2
- package/types/sdcorejs-angular-forms-date.d.ts +21 -3
- package/types/sdcorejs-angular-forms-datetime.d.ts +13 -2
- package/types/sdcorejs-angular-forms-entity-picker.d.ts +9 -3
- package/types/sdcorejs-angular-forms-input-color.d.ts +4 -0
- package/types/sdcorejs-angular-forms-input-number.d.ts +3 -0
- package/types/sdcorejs-angular-forms-input.d.ts +3 -0
- package/types/sdcorejs-angular-forms-models.d.ts +146 -5
- package/types/sdcorejs-angular-forms-select.d.ts +27 -4
- package/types/sdcorejs-angular-forms-switch.d.ts +7 -1
- package/types/sdcorejs-angular-forms-textarea.d.ts +5 -3
- package/types/sdcorejs-angular-forms-time-range.d.ts +22 -1
- package/types/sdcorejs-angular-forms-tree-select.d.ts +36 -5
- package/types/sdcorejs-angular-forms.d.ts +22 -13
- package/types/sdcorejs-angular-i18n.d.ts +99 -16
- package/types/sdcorejs-angular-interceptors.d.ts +13 -3
- package/types/sdcorejs-angular-modules-auth.d.ts +1 -1
- package/types/sdcorejs-angular-modules-keycloak.d.ts +1 -0
- package/types/sdcorejs-angular-modules-layout.d.ts +103 -28
- package/types/sdcorejs-angular-modules-permission.d.ts +66 -4
- package/types/sdcorejs-angular-pipes.d.ts +27 -5
- package/types/sdcorejs-angular-services-api.d.ts +21 -1
- package/types/sdcorejs-angular-services-cache.d.ts +11 -1
- package/types/sdcorejs-angular-services-notify.d.ts +1 -1
- package/types/sdcorejs-angular-services.d.ts +0 -1
- package/types/sdcorejs-angular-utilities-extensions.d.ts +83 -61
- package/types/sdcorejs-angular-utilities-models.d.ts +2 -76
- package/types/sdcorejs-angular.d.ts +11 -2
- package/fesm2022/sdcorejs-angular-components-base.mjs +0 -23
- package/fesm2022/sdcorejs-angular-components-base.mjs.map +0 -1
- package/fesm2022/sdcorejs-angular-components-document-builder.mjs +0 -5038
- package/fesm2022/sdcorejs-angular-components-document-builder.mjs.map +0 -1
- package/fesm2022/sdcorejs-angular-models.mjs +0 -6
- package/fesm2022/sdcorejs-angular-models.mjs.map +0 -1
- package/fesm2022/sdcorejs-angular-services-docx.mjs +0 -291
- package/fesm2022/sdcorejs-angular-services-docx.mjs.map +0 -1
- package/fesm2022/sdcorejs-angular-services-license.mjs +0 -97
- package/fesm2022/sdcorejs-angular-services-license.mjs.map +0 -1
- package/types/sdcorejs-angular-components-base.d.ts +0 -11
- package/types/sdcorejs-angular-components-document-builder.d.ts +0 -252
- package/types/sdcorejs-angular-models.d.ts +0 -2
- package/types/sdcorejs-angular-services-docx.d.ts +0 -27
- package/types/sdcorejs-angular-services-license.d.ts +0 -12
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NgTemplateOutlet } from '@angular/common';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { input, booleanAttribute, model, signal, computed, effect, untracked, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { inject, input, booleanAttribute, model, signal, computed, effect, untracked, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
4
|
+
import { I18nService } from '@sdcorejs/angular/i18n';
|
|
4
5
|
import { SdDate } from '@sdcorejs/angular/forms/date';
|
|
5
6
|
import { SdDatetime } from '@sdcorejs/angular/forms/datetime';
|
|
6
7
|
import { SdInput } from '@sdcorejs/angular/forms/input';
|
|
@@ -11,8 +12,8 @@ import { SdOperator } from '@sdcorejs/angular/components/operator';
|
|
|
11
12
|
import { FilterUtilities } from '@sdcorejs/utils/fns';
|
|
12
13
|
import { SdIcon } from '@sdcorejs/angular/modules/icon';
|
|
13
14
|
|
|
14
|
-
/** Type guard — narrows a `
|
|
15
|
-
function
|
|
15
|
+
/** Type guard — narrows a `SdQbNode` to a `SdQbGroup`. */
|
|
16
|
+
function sdIsQbGroup(node) {
|
|
16
17
|
return node.kind === 'group';
|
|
17
18
|
}
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
@@ -21,7 +22,7 @@ function isQbGroup(node) {
|
|
|
21
22
|
// local so query-builder has no dependency on the query-bar package.
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
/** Allowed operators per field type — used when `field.operators` is not set. */
|
|
24
|
-
const
|
|
25
|
+
const SD_QB_OPERATORS_BY_TYPE = {
|
|
25
26
|
string: ['CONTAIN', 'NOT_CONTAIN', 'EQUAL', 'NOT_EQUAL', 'START_WITH', 'END_WITH', 'NULL', 'NOT_NULL'],
|
|
26
27
|
number: ['EQUAL', 'NOT_EQUAL', 'GREATER_THAN', 'GREATER_OR_EQUAL', 'LESS_THAN', 'LESS_OR_EQUAL', 'BETWEEN', 'NULL', 'NOT_NULL'],
|
|
27
28
|
boolean: ['EQUAL', 'NOT_EQUAL'],
|
|
@@ -30,7 +31,7 @@ const QB_OPERATORS_BY_TYPE = {
|
|
|
30
31
|
values: ['IN', 'NOT_IN', 'EQUAL', 'NOT_EQUAL', 'NULL', 'NOT_NULL'],
|
|
31
32
|
};
|
|
32
33
|
/** Default operator when a rule is first created for a field. */
|
|
33
|
-
const
|
|
34
|
+
const SD_QB_DEFAULT_OPERATOR_BY_TYPE = {
|
|
34
35
|
string: 'CONTAIN',
|
|
35
36
|
number: 'EQUAL',
|
|
36
37
|
boolean: 'EQUAL',
|
|
@@ -39,45 +40,45 @@ const QB_DEFAULT_OPERATOR_BY_TYPE = {
|
|
|
39
40
|
values: 'IN',
|
|
40
41
|
};
|
|
41
42
|
/** Operators with no value payload — the value editor is hidden for these. */
|
|
42
|
-
const
|
|
43
|
+
const SD_QB_NO_DATA_OPERATORS = ['NULL', 'NOT_NULL'];
|
|
43
44
|
/** Operators whose value is an array — the value editor offers multi-select. */
|
|
44
|
-
const
|
|
45
|
+
const SD_QB_MULTI_OPERATORS = ['IN', 'NOT_IN'];
|
|
45
46
|
/** Operators allowed for `field` — explicit override wins, else the per-type set. */
|
|
46
|
-
function
|
|
47
|
+
function sdQbAllowedOperators(field) {
|
|
47
48
|
if (!field)
|
|
48
49
|
return [];
|
|
49
50
|
if (field.operators?.length)
|
|
50
51
|
return field.operators;
|
|
51
|
-
return
|
|
52
|
+
return SD_QB_OPERATORS_BY_TYPE[field.type] ?? [];
|
|
52
53
|
}
|
|
53
54
|
/** Starting operator for a new rule on `field` — falls back to the per-type default. */
|
|
54
|
-
function
|
|
55
|
+
function sdQbDefaultOperator(field) {
|
|
55
56
|
if (!field)
|
|
56
57
|
return undefined;
|
|
57
|
-
const allowed =
|
|
58
|
+
const allowed = sdQbAllowedOperators(field);
|
|
58
59
|
if (field.defaultOperator && allowed.includes(field.defaultOperator))
|
|
59
60
|
return field.defaultOperator;
|
|
60
|
-
const byType =
|
|
61
|
+
const byType = SD_QB_DEFAULT_OPERATOR_BY_TYPE[field.type];
|
|
61
62
|
return allowed.includes(byType) ? byType : allowed[0];
|
|
62
63
|
}
|
|
63
64
|
/** True when `op` is NULL / NOT_NULL (no value editor). */
|
|
64
|
-
function
|
|
65
|
-
return !!op &&
|
|
65
|
+
function sdQbIsNoDataOperator(op) {
|
|
66
|
+
return !!op && SD_QB_NO_DATA_OPERATORS.includes(op);
|
|
66
67
|
}
|
|
67
68
|
/** True when `op` is IN / NOT_IN (array value). */
|
|
68
|
-
function
|
|
69
|
-
return !!op &&
|
|
69
|
+
function sdQbIsMultiOperator(op) {
|
|
70
|
+
return !!op && SD_QB_MULTI_OPERATORS.includes(op);
|
|
70
71
|
}
|
|
71
72
|
/** True when the operator accepts one right-hand operand, including a field reference. */
|
|
72
|
-
function
|
|
73
|
-
return !!op && !
|
|
73
|
+
function sdQbSupportsFieldCompareOperator(op) {
|
|
74
|
+
return !!op && !sdQbIsNoDataOperator(op) && !sdQbIsMultiOperator(op) && op !== 'BETWEEN';
|
|
74
75
|
}
|
|
75
76
|
/**
|
|
76
77
|
* Icon fallback per field type when `SdQueryBuilderField.icon` is not set.
|
|
77
78
|
* Mirrors query-bar's `SD_QUERY_TYPE_ICON` so the two components read consistently,
|
|
78
79
|
* but kept local so query-builder has no dependency on the query-bar package.
|
|
79
80
|
*/
|
|
80
|
-
const
|
|
81
|
+
const SD_QB_TYPE_ICON = {
|
|
81
82
|
string: 'text_fields',
|
|
82
83
|
number: 'tag',
|
|
83
84
|
boolean: 'toggle_on',
|
|
@@ -87,12 +88,12 @@ const QB_TYPE_ICON = {
|
|
|
87
88
|
};
|
|
88
89
|
/**
|
|
89
90
|
* Icon shown before a field in the field picker / selected trigger.
|
|
90
|
-
* Priority: `field.icon` → `
|
|
91
|
+
* Priority: `field.icon` → `SD_QB_TYPE_ICON[field.type]` → `'tune'` (default, like query-bar).
|
|
91
92
|
*/
|
|
92
|
-
function
|
|
93
|
+
function sdQbFieldIcon(field) {
|
|
93
94
|
if (!field)
|
|
94
95
|
return 'tune';
|
|
95
|
-
return field.icon ??
|
|
96
|
+
return field.icon ?? SD_QB_TYPE_ICON[field.type] ?? 'tune';
|
|
96
97
|
}
|
|
97
98
|
// ---------------------------------------------------------------------------
|
|
98
99
|
// Node factories — monotonic counter for stable, render-safe ids (no Math.random
|
|
@@ -100,52 +101,58 @@ function qbFieldIcon(field) {
|
|
|
100
101
|
// ---------------------------------------------------------------------------
|
|
101
102
|
let qbSeq = 0;
|
|
102
103
|
/** Next stable node id with the given prefix. */
|
|
103
|
-
function
|
|
104
|
+
function sdQbId(prefix = 'qb') {
|
|
104
105
|
return `${prefix}-${++qbSeq}`;
|
|
105
106
|
}
|
|
106
107
|
/** Build a fresh rule node (optionally pre-filled). */
|
|
107
|
-
function
|
|
108
|
-
return { id:
|
|
108
|
+
function sdQbNewRule(field, operator, value, valueSource, compareField) {
|
|
109
|
+
return { id: sdQbId('r'), kind: 'rule', field, operator, value, valueSource, compareField };
|
|
109
110
|
}
|
|
110
111
|
/** Build a fresh group node. */
|
|
111
|
-
function
|
|
112
|
-
return { id:
|
|
112
|
+
function sdQbNewGroup(logic = 'AND', children = []) {
|
|
113
|
+
return { id: sdQbId('g'), kind: 'group', logic, children };
|
|
113
114
|
}
|
|
114
115
|
/** Sentinel held as a rule value (and emitted as `data`) for the "today" date mode. */
|
|
115
|
-
const
|
|
116
|
+
const SD_QB_TODAY = 'TODAY';
|
|
116
117
|
/** Type guard — narrows a rule value to a utils `DateRelative` (offset spec). */
|
|
117
|
-
const
|
|
118
|
+
const sdQbIsRelativeDate = FilterUtilities.isDateRelative;
|
|
118
119
|
/** Type guard — narrows a rule value to the `'TODAY'` sentinel ("now"/today mode). */
|
|
119
|
-
function
|
|
120
|
-
return v ===
|
|
120
|
+
function sdQbIsToday(v) {
|
|
121
|
+
return v === SD_QB_TODAY;
|
|
121
122
|
}
|
|
122
123
|
/** Starting relative value when a rule first switches to "relative" mode. */
|
|
123
|
-
function
|
|
124
|
+
function sdQbDefaultRelative() {
|
|
124
125
|
return { amount: 1, direction: 'previous', unit: 'day' };
|
|
125
126
|
}
|
|
126
127
|
/** Stable option list for the date-mode select (module ref — never reallocated). */
|
|
127
|
-
const
|
|
128
|
-
{ value: 'absolute',
|
|
129
|
-
{ value: 'now',
|
|
130
|
-
{ value: 'relative',
|
|
128
|
+
const SD_QB_DATE_MODES = [
|
|
129
|
+
{ value: 'absolute', labelKey: 'core.component.query-builder.date-mode.absolute', icon: 'event' },
|
|
130
|
+
{ value: 'now', labelKey: 'core.component.query-builder.date-mode.now', icon: 'today' },
|
|
131
|
+
{ value: 'relative', labelKey: 'core.component.query-builder.date-mode.relative', icon: 'history' },
|
|
131
132
|
];
|
|
132
133
|
/** Stable option list for choosing between a literal value and another field. */
|
|
133
|
-
const
|
|
134
|
-
{ value: 'literal',
|
|
135
|
-
{ value: 'field',
|
|
134
|
+
const SD_QB_VALUE_SOURCE_OPTIONS = [
|
|
135
|
+
{ value: 'literal', labelKey: 'core.component.query-builder.value-source.literal', icon: 'edit_note' },
|
|
136
|
+
{ value: 'field', labelKey: 'core.component.query-builder.value-source.field', icon: 'view_column' },
|
|
136
137
|
];
|
|
137
138
|
/** Stable combined direction×unit option list (token `'unit:direction'`). */
|
|
138
|
-
const
|
|
139
|
-
{ value: 'day:previous',
|
|
140
|
-
{ value: 'day:next',
|
|
141
|
-
{ value: 'week:previous',
|
|
142
|
-
{ value: 'week:next',
|
|
143
|
-
{ value: 'month:previous',
|
|
144
|
-
{ value: 'month:next',
|
|
139
|
+
const SD_QB_RELATIVE_UNIT_OPTIONS = [
|
|
140
|
+
{ value: 'day:previous', labelKey: 'core.component.query-builder.relative.day-previous' },
|
|
141
|
+
{ value: 'day:next', labelKey: 'core.component.query-builder.relative.day-next' },
|
|
142
|
+
{ value: 'week:previous', labelKey: 'core.component.query-builder.relative.week-previous' },
|
|
143
|
+
{ value: 'week:next', labelKey: 'core.component.query-builder.relative.week-next' },
|
|
144
|
+
{ value: 'month:previous', labelKey: 'core.component.query-builder.relative.month-previous' },
|
|
145
|
+
{ value: 'month:next', labelKey: 'core.component.query-builder.relative.month-next' },
|
|
145
146
|
];
|
|
147
|
+
/** i18n key of the combined `unit`×`direction` phrase for a relative-date offset. */
|
|
148
|
+
function qbRelativeLabelKey(unit, direction) {
|
|
149
|
+
return `core.component.query-builder.relative.${unit}-${direction}`;
|
|
150
|
+
}
|
|
146
151
|
/** Shared empty option array — stable ref for fallbacks (avoids per-call allocation). */
|
|
147
|
-
const
|
|
152
|
+
const SD_QB_EMPTY_OPTIONS = [];
|
|
148
153
|
|
|
154
|
+
/** Fallback khi caller không truyền translator: trả về chính key để thiếu i18n lộ ra, không im lặng. */
|
|
155
|
+
const IDENTITY_TRANSLATE = key => key;
|
|
149
156
|
// SQL symbol for each single-value comparison operator.
|
|
150
157
|
const SYMBOL = {
|
|
151
158
|
EQUAL: '=',
|
|
@@ -170,11 +177,11 @@ function ruleToFilter(rule) {
|
|
|
170
177
|
if (!rule.field || !rule.operator)
|
|
171
178
|
return null;
|
|
172
179
|
const operator = rule.operator;
|
|
173
|
-
if (
|
|
180
|
+
if (SD_QB_NO_DATA_OPERATORS.includes(operator)) {
|
|
174
181
|
return { field: rule.field, operator };
|
|
175
182
|
}
|
|
176
183
|
if (rule.valueSource === 'field') {
|
|
177
|
-
if (!
|
|
184
|
+
if (!sdQbSupportsFieldCompareOperator(operator) || isEmptyValue(rule.compareField))
|
|
178
185
|
return null;
|
|
179
186
|
return { field: rule.field, operator, dataType: 'field', data: rule.compareField };
|
|
180
187
|
}
|
|
@@ -186,16 +193,16 @@ function ruleToFilter(rule) {
|
|
|
186
193
|
return null;
|
|
187
194
|
return { field: rule.field, operator: 'BETWEEN', data: { from: v.from, to: v.to } };
|
|
188
195
|
}
|
|
189
|
-
if (
|
|
196
|
+
if (SD_QB_MULTI_OPERATORS.includes(operator)) {
|
|
190
197
|
const arr = Array.isArray(rule.value) ? rule.value : isEmptyValue(rule.value) ? [] : [rule.value];
|
|
191
198
|
if (!arr.length)
|
|
192
199
|
return null;
|
|
193
200
|
return { field: rule.field, operator, data: arr };
|
|
194
201
|
}
|
|
195
|
-
if (
|
|
196
|
-
return { field: rule.field, operator, dataType: 'date-today', data:
|
|
202
|
+
if (sdQbIsToday(rule.value)) {
|
|
203
|
+
return { field: rule.field, operator, dataType: 'date-today', data: SD_QB_TODAY };
|
|
197
204
|
}
|
|
198
|
-
if (
|
|
205
|
+
if (sdQbIsRelativeDate(rule.value)) {
|
|
199
206
|
const v = rule.value;
|
|
200
207
|
// emit only when fully specified, else drop the incomplete rule
|
|
201
208
|
if (v.unit && v.direction && typeof v.amount === 'number' && v.amount >= 1) {
|
|
@@ -216,7 +223,7 @@ function ruleToFilter(rule) {
|
|
|
216
223
|
return { field: rule.field, operator, data: rule.value };
|
|
217
224
|
}
|
|
218
225
|
function nodeToFilter(node) {
|
|
219
|
-
if (
|
|
226
|
+
if (sdIsQbGroup(node)) {
|
|
220
227
|
const data = node.children.map(nodeToFilter).filter((f) => f != null);
|
|
221
228
|
if (!data.length)
|
|
222
229
|
return null;
|
|
@@ -240,33 +247,33 @@ function treeToFilter(group) {
|
|
|
240
247
|
// ---------------------------------------------------------------------------
|
|
241
248
|
function ruleFromFilter(filter) {
|
|
242
249
|
const operator = filter.operator;
|
|
243
|
-
if (
|
|
244
|
-
return
|
|
250
|
+
if (SD_QB_NO_DATA_OPERATORS.includes(operator)) {
|
|
251
|
+
return sdQbNewRule(filter.field, operator);
|
|
245
252
|
}
|
|
246
253
|
if (filter.dataType === 'field') {
|
|
247
|
-
return
|
|
254
|
+
return sdQbNewRule(filter.field, operator, undefined, 'field', filter.data);
|
|
248
255
|
}
|
|
249
256
|
// Relative dates: read the utils `dataType` discriminator back into the internal value.
|
|
250
257
|
if (filter.dataType === 'date-today') {
|
|
251
|
-
return
|
|
258
|
+
return sdQbNewRule(filter.field, operator, SD_QB_TODAY);
|
|
252
259
|
}
|
|
253
260
|
if (filter.dataType === 'date-relative') {
|
|
254
|
-
return
|
|
261
|
+
return sdQbNewRule(filter.field, operator, filter.data);
|
|
255
262
|
}
|
|
256
263
|
// Back-compat: legacy `{ rel: 'now' | 'offset' }` payloads (pre-dataType migration)
|
|
257
264
|
// still seed the editor correctly.
|
|
258
265
|
const legacy = readLegacyRelative(filter.data);
|
|
259
266
|
if (legacy !== undefined) {
|
|
260
|
-
return
|
|
267
|
+
return sdQbNewRule(filter.field, operator, legacy);
|
|
261
268
|
}
|
|
262
|
-
return
|
|
269
|
+
return sdQbNewRule(filter.field, operator, filter.data);
|
|
263
270
|
}
|
|
264
271
|
/** Map a pre-migration `{ rel }` payload to the new internal value, or `undefined` if not legacy. */
|
|
265
272
|
function readLegacyRelative(data) {
|
|
266
273
|
if (!data || typeof data !== 'object')
|
|
267
274
|
return undefined;
|
|
268
275
|
if (data.rel === 'now')
|
|
269
|
-
return
|
|
276
|
+
return SD_QB_TODAY;
|
|
270
277
|
if (data.rel === 'offset' && data.unit && data.direction && typeof data.amount === 'number') {
|
|
271
278
|
return { amount: data.amount, direction: data.direction, unit: data.unit };
|
|
272
279
|
}
|
|
@@ -276,7 +283,7 @@ function nodeFromFilter(filter) {
|
|
|
276
283
|
return isAndOr$1(filter) ? groupFromFilter(filter) : ruleFromFilter(filter);
|
|
277
284
|
}
|
|
278
285
|
function groupFromFilter(filter) {
|
|
279
|
-
return
|
|
286
|
+
return sdQbNewGroup(filter.operator, filter.data.map(nodeFromFilter));
|
|
280
287
|
}
|
|
281
288
|
/**
|
|
282
289
|
* Inverse of {@link treeToFilter} — rebuild the editable tree from a `Filter` so a
|
|
@@ -284,14 +291,14 @@ function groupFromFilter(filter) {
|
|
|
284
291
|
* `null` yields an empty AND root.
|
|
285
292
|
*
|
|
286
293
|
* @param filter - a `Filter` (group or leaf), or `null` / `undefined` for an empty tree.
|
|
287
|
-
* @returns a fresh root `
|
|
294
|
+
* @returns a fresh root `SdQbGroup` mirroring the filter's structure.
|
|
288
295
|
*/
|
|
289
296
|
function filterToTree(filter) {
|
|
290
297
|
if (!filter)
|
|
291
|
-
return
|
|
298
|
+
return sdQbNewGroup('AND', []);
|
|
292
299
|
if (isAndOr$1(filter))
|
|
293
300
|
return groupFromFilter(filter);
|
|
294
|
-
return
|
|
301
|
+
return sdQbNewGroup('AND', [ruleFromFilter(filter)]);
|
|
295
302
|
}
|
|
296
303
|
// ---------------------------------------------------------------------------
|
|
297
304
|
// Public Filter → SQL-ish highlight tokens (view mode)
|
|
@@ -299,23 +306,27 @@ function filterToTree(filter) {
|
|
|
299
306
|
function escapeStr(s) {
|
|
300
307
|
return s.replace(/'/g, "''");
|
|
301
308
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
309
|
+
/** Render a relative-date offset as a readable phrase in the active language. */
|
|
310
|
+
function formatRelative(v, t) {
|
|
311
|
+
// why: mỗi ngôn ngữ ghép `amount` với cụm đơn vị+hướng theo trật tự khác nhau ('3 ngày trước' /
|
|
312
|
+
// '3 days ago' / '3日前'), nên để catalog quyết định trật tự thay vì nối chuỗi cứng ở đây.
|
|
313
|
+
return t('core.component.query-builder.relative.format', {
|
|
314
|
+
amount: v.amount,
|
|
315
|
+
phrase: t(qbRelativeLabelKey(v.unit, v.direction)),
|
|
316
|
+
});
|
|
308
317
|
}
|
|
309
318
|
/** Format one scalar value for the view string — quoted/labelled per field type. */
|
|
310
|
-
function formatScalar(field, raw) {
|
|
311
|
-
if (
|
|
312
|
-
return '
|
|
313
|
-
if (
|
|
314
|
-
return formatRelative(raw);
|
|
319
|
+
function formatScalar(field, raw, t) {
|
|
320
|
+
if (sdQbIsToday(raw))
|
|
321
|
+
return t('core.component.query-builder.date-mode.now');
|
|
322
|
+
if (sdQbIsRelativeDate(raw))
|
|
323
|
+
return formatRelative(raw, t);
|
|
315
324
|
if (raw === null || raw === undefined)
|
|
316
325
|
return '';
|
|
317
326
|
if (field?.type === 'boolean') {
|
|
318
|
-
return raw
|
|
327
|
+
return raw
|
|
328
|
+
? (field.trueLabel ?? t('core.component.query-builder.boolean.true'))
|
|
329
|
+
: (field.falseLabel ?? t('core.component.query-builder.boolean.false'));
|
|
319
330
|
}
|
|
320
331
|
if (field?.type === 'values') {
|
|
321
332
|
const opt = field.values?.find(o => o.value === raw);
|
|
@@ -340,12 +351,12 @@ function operatorToken(operator) {
|
|
|
340
351
|
return operator.startsWith('NOT_') ? 'not like' : 'like';
|
|
341
352
|
return SYMBOL[operator] ?? operator;
|
|
342
353
|
}
|
|
343
|
-
function ruleTokens(filter, fields) {
|
|
354
|
+
function ruleTokens(filter, fields, t) {
|
|
344
355
|
const field = fields.find(f => f.key === filter.field);
|
|
345
356
|
const out = [{ kind: 'field', text: field?.label ?? String(filter.field) }];
|
|
346
357
|
const sp = () => out.push({ kind: 'plain', text: ' ' });
|
|
347
358
|
const operator = filter.operator;
|
|
348
|
-
if (
|
|
359
|
+
if (SD_QB_NO_DATA_OPERATORS.includes(operator)) {
|
|
349
360
|
sp();
|
|
350
361
|
out.push({ kind: 'op', text: operator === 'NULL' ? 'is null' : 'is not null' });
|
|
351
362
|
return out;
|
|
@@ -354,17 +365,17 @@ function ruleTokens(filter, fields) {
|
|
|
354
365
|
sp();
|
|
355
366
|
out.push({ kind: 'op', text: 'between' });
|
|
356
367
|
sp();
|
|
357
|
-
out.push({ kind: 'value', text: formatScalar(field, filter.data?.from) });
|
|
368
|
+
out.push({ kind: 'value', text: formatScalar(field, filter.data?.from, t) });
|
|
358
369
|
out.push({ kind: 'plain', text: ' and ' });
|
|
359
|
-
out.push({ kind: 'value', text: formatScalar(field, filter.data?.to) });
|
|
370
|
+
out.push({ kind: 'value', text: formatScalar(field, filter.data?.to, t) });
|
|
360
371
|
return out;
|
|
361
372
|
}
|
|
362
|
-
if (
|
|
373
|
+
if (SD_QB_MULTI_OPERATORS.includes(operator)) {
|
|
363
374
|
sp();
|
|
364
375
|
out.push({ kind: 'op', text: operator === 'IN' ? 'in' : 'not in' });
|
|
365
376
|
sp();
|
|
366
377
|
const arr = Array.isArray(filter.data) ? filter.data : [];
|
|
367
|
-
out.push({ kind: 'value', text: `(${arr.map((v) => formatScalar(field, v)).join(', ')})` });
|
|
378
|
+
out.push({ kind: 'value', text: `(${arr.map((v) => formatScalar(field, v, t)).join(', ')})` });
|
|
368
379
|
return out;
|
|
369
380
|
}
|
|
370
381
|
if (filter.dataType === 'field') {
|
|
@@ -385,16 +396,16 @@ function ruleTokens(filter, fields) {
|
|
|
385
396
|
sp();
|
|
386
397
|
out.push({ kind: 'op', text: operatorToken(operator) });
|
|
387
398
|
sp();
|
|
388
|
-
out.push({ kind: 'value', text: formatScalar(field, filter.data) });
|
|
399
|
+
out.push({ kind: 'value', text: formatScalar(field, filter.data, t) });
|
|
389
400
|
return out;
|
|
390
401
|
}
|
|
391
|
-
function tokensFor(filter, fields) {
|
|
402
|
+
function tokensFor(filter, fields, t) {
|
|
392
403
|
if (isAndOr$1(filter)) {
|
|
393
404
|
const out = [];
|
|
394
405
|
filter.data.forEach((child, i) => {
|
|
395
406
|
if (i > 0)
|
|
396
407
|
out.push({ kind: 'logic', text: ` ${filter.operator.toLowerCase()} ` });
|
|
397
|
-
const childTokens = tokensFor(child, fields);
|
|
408
|
+
const childTokens = tokensFor(child, fields, t);
|
|
398
409
|
// Wrap a nested multi-child group in parentheses; a single-child group needs none.
|
|
399
410
|
if (isAndOr$1(child) && child.data.length > 1) {
|
|
400
411
|
out.push({ kind: 'paren', text: '(' }, ...childTokens, { kind: 'paren', text: ')' });
|
|
@@ -405,23 +416,26 @@ function tokensFor(filter, fields) {
|
|
|
405
416
|
});
|
|
406
417
|
return out;
|
|
407
418
|
}
|
|
408
|
-
return ruleTokens(filter, fields);
|
|
419
|
+
return ruleTokens(filter, fields, t);
|
|
409
420
|
}
|
|
410
421
|
/**
|
|
411
422
|
* Render a `Filter` to a SQL-ish token stream for view mode. Field tokens use the
|
|
412
423
|
* matching field's `label`; operators map to SQL syntax (`= != like between is null …`);
|
|
413
424
|
* `and` / `or` are lowercase; nested multi-child groups are wrapped in parentheses.
|
|
414
425
|
* Returns a token stream (not a string) so the template can wrap each piece in a
|
|
415
|
-
* highlight `<span>` keyed by `
|
|
426
|
+
* highlight `<span>` keyed by `SdQbToken.kind`.
|
|
416
427
|
*
|
|
417
428
|
* @param filter - the `Filter` to render, or `null` / `undefined`.
|
|
418
429
|
* @param fields - field metadata, used to resolve each field key to its display label + value formatting.
|
|
430
|
+
* @param translate - i18n lookup for language-dependent value tokens (today / relative offsets /
|
|
431
|
+
* boolean labels). Optional so the serializer stays callable outside an Angular injection context;
|
|
432
|
+
* omitting it renders the raw i18n keys.
|
|
419
433
|
* @returns the ordered tokens, or `[]` when `filter` is empty.
|
|
420
434
|
*/
|
|
421
|
-
function filterToTokens(filter, fields) {
|
|
435
|
+
function filterToTokens(filter, fields, translate = IDENTITY_TRANSLATE) {
|
|
422
436
|
if (!filter)
|
|
423
437
|
return [];
|
|
424
|
-
return tokensFor(filter, fields);
|
|
438
|
+
return tokensFor(filter, fields, translate);
|
|
425
439
|
}
|
|
426
440
|
|
|
427
441
|
/** A logical `Filter` (AND / OR group) carries a `data[]` array. */
|
|
@@ -437,6 +451,7 @@ const QB_EMPTY_FIELDS = [];
|
|
|
437
451
|
* string with highlighted operators + values.
|
|
438
452
|
*/
|
|
439
453
|
class SdQueryBuilder {
|
|
454
|
+
#i18n = inject(I18nService);
|
|
440
455
|
// -------------------------------------------------------------------------
|
|
441
456
|
// Inputs
|
|
442
457
|
// -------------------------------------------------------------------------
|
|
@@ -464,7 +479,7 @@ class SdQueryBuilder {
|
|
|
464
479
|
// -------------------------------------------------------------------------
|
|
465
480
|
// Internal source of truth — the editable tree (carries UI state).
|
|
466
481
|
// -------------------------------------------------------------------------
|
|
467
|
-
#tree = signal(
|
|
482
|
+
#tree = signal(sdQbNewGroup('AND'), ...(ngDevMode ? [{ debugName: "#tree" }] : /* istanbul ignore next */ []));
|
|
468
483
|
tree = this.#tree.asReadonly();
|
|
469
484
|
/** Last filter WE emitted — used to recognise (and ignore) our own echo in the inbound effect. */
|
|
470
485
|
#lastEmitted = undefined;
|
|
@@ -491,19 +506,40 @@ class SdQueryBuilder {
|
|
|
491
506
|
for (const f of this.resolvedFields()) {
|
|
492
507
|
if (f.type === 'boolean') {
|
|
493
508
|
map.set(f.key, [
|
|
494
|
-
{ value: true, display: f.trueLabel ?? '
|
|
495
|
-
{ value: false, display: f.falseLabel ?? '
|
|
509
|
+
{ value: true, display: f.trueLabel ?? this.#i18n.t('core.component.query-builder.boolean.true') },
|
|
510
|
+
{ value: false, display: f.falseLabel ?? this.#i18n.t('core.component.query-builder.boolean.false') },
|
|
496
511
|
]);
|
|
497
512
|
}
|
|
498
513
|
}
|
|
499
514
|
return map;
|
|
500
515
|
}, ...(ngDevMode ? [{ debugName: "#booleanOptionsByKey" }] : /* istanbul ignore next */ []));
|
|
516
|
+
// -------------------------------------------------------------------------
|
|
517
|
+
// Option lists — dịch lúc ĐỌC, không phải lúc module được đánh giá.
|
|
518
|
+
//
|
|
519
|
+
// why: bảng hằng số trong `query-builder.model.ts` chỉ giữ `labelKey`; `computed` ở đây mới sinh
|
|
520
|
+
// ra `display`. Nhờ vậy đổi ngôn ngữ là nhãn đổi theo (computed phụ thuộc signal `messages()` bên
|
|
521
|
+
// trong `I18nService.t`), mà tham chiếu mảng vẫn ỔN ĐỊNH giữa các chu kỳ change detection vì
|
|
522
|
+
// computed chỉ tính lại khi ngôn ngữ đổi — bắt buộc, nếu không `sd-select` sẽ rơi lại vào vòng lặp
|
|
523
|
+
// `toObservable(items)` → `markForCheck()` → CD mới → mảng mới (xem `#booleanOptionsByKey`).
|
|
524
|
+
// -------------------------------------------------------------------------
|
|
501
525
|
/** Stable option list for the date-mode select. */
|
|
502
|
-
dateModes =
|
|
526
|
+
dateModes = computed(() => SD_QB_DATE_MODES.map(o => ({ ...o, display: this.#i18n.t(o.labelKey) })), ...(ngDevMode ? [{ debugName: "dateModes" }] : /* istanbul ignore next */ []));
|
|
503
527
|
/** Stable option list for the literal-vs-field operand select. */
|
|
504
|
-
valueSourceOptions =
|
|
528
|
+
valueSourceOptions = computed(() => SD_QB_VALUE_SOURCE_OPTIONS.map(o => ({ ...o, display: this.#i18n.t(o.labelKey) })), ...(ngDevMode ? [{ debugName: "valueSourceOptions" }] : /* istanbul ignore next */ []));
|
|
505
529
|
/** Stable combined unit×direction option list for the relative select. */
|
|
506
|
-
relativeUnitOptions =
|
|
530
|
+
relativeUnitOptions = computed(() => SD_QB_RELATIVE_UNIT_OPTIONS.map(o => ({ ...o, display: this.#i18n.t(o.labelKey) })), ...(ngDevMode ? [{ debugName: "relativeUnitOptions" }] : /* istanbul ignore next */ []));
|
|
531
|
+
// Nhãn i18n tĩnh của template (placeholder / title / aria-label / trạng thái rỗng).
|
|
532
|
+
selectFieldLabel = computed(() => this.#i18n.t('core.component.query-builder.select-field'), ...(ngDevMode ? [{ debugName: "selectFieldLabel" }] : /* istanbul ignore next */ []));
|
|
533
|
+
valueLabel = computed(() => this.#i18n.t('core.component.query-builder.value'), ...(ngDevMode ? [{ debugName: "valueLabel" }] : /* istanbul ignore next */ []));
|
|
534
|
+
fromLabel = computed(() => this.#i18n.t('core.component.query-builder.from'), ...(ngDevMode ? [{ debugName: "fromLabel" }] : /* istanbul ignore next */ []));
|
|
535
|
+
toLabel = computed(() => this.#i18n.t('core.component.query-builder.to'), ...(ngDevMode ? [{ debugName: "toLabel" }] : /* istanbul ignore next */ []));
|
|
536
|
+
addLabel = computed(() => this.#i18n.t('core.component.query-builder.add'), ...(ngDevMode ? [{ debugName: "addLabel" }] : /* istanbul ignore next */ []));
|
|
537
|
+
addNodeLabel = computed(() => this.#i18n.t('core.component.query-builder.add-node'), ...(ngDevMode ? [{ debugName: "addNodeLabel" }] : /* istanbul ignore next */ []));
|
|
538
|
+
conditionLabel = computed(() => this.#i18n.t('core.component.query-builder.condition'), ...(ngDevMode ? [{ debugName: "conditionLabel" }] : /* istanbul ignore next */ []));
|
|
539
|
+
groupLabel = computed(() => this.#i18n.t('core.component.query-builder.group'), ...(ngDevMode ? [{ debugName: "groupLabel" }] : /* istanbul ignore next */ []));
|
|
540
|
+
removeGroupLabel = computed(() => this.#i18n.t('core.component.query-builder.remove-group'), ...(ngDevMode ? [{ debugName: "removeGroupLabel" }] : /* istanbul ignore next */ []));
|
|
541
|
+
removeConditionLabel = computed(() => this.#i18n.t('core.component.query-builder.remove-condition'), ...(ngDevMode ? [{ debugName: "removeConditionLabel" }] : /* istanbul ignore next */ []));
|
|
542
|
+
emptyLabel = computed(() => this.#i18n.t('core.component.query-builder.empty'), ...(ngDevMode ? [{ debugName: "emptyLabel" }] : /* istanbul ignore next */ []));
|
|
507
543
|
#compareFieldsByKey = computed(() => {
|
|
508
544
|
const fields = this.resolvedFields();
|
|
509
545
|
const map = new Map();
|
|
@@ -526,13 +562,20 @@ class SdQueryBuilder {
|
|
|
526
562
|
}
|
|
527
563
|
return map;
|
|
528
564
|
}, ...(ngDevMode ? [{ debugName: "#compareFieldsByKey" }] : /* istanbul ignore next */ []));
|
|
529
|
-
/**
|
|
530
|
-
|
|
565
|
+
/**
|
|
566
|
+
* Translator handed to the serializer.
|
|
567
|
+
* why: `filterToTokens` là hàm thuần cấp module, không inject được. Truyền hàm dịch vào để chuỗi
|
|
568
|
+
* view-mode ("hôm nay", "3 ngày trước", "Có"/"Không") không còn hardcode tiếng Việt. Trường ổn
|
|
569
|
+
* định (không tạo mới mỗi lần gọi) để `computed` bên dưới không phụ thuộc tham chiếu mới.
|
|
570
|
+
*/
|
|
571
|
+
#translate = (key, params) => this.#i18n.t(key, params);
|
|
572
|
+
/** View-mode token stream — recomputed from the emitted value (and from the active language). */
|
|
573
|
+
tokens = computed(() => filterToTokens(this.option()?.value ?? this.value(), this.resolvedFields(), this.#translate), ...(ngDevMode ? [{ debugName: "tokens" }] : /* istanbul ignore next */ []));
|
|
531
574
|
isView = computed(() => this.resolvedMode() === 'view', ...(ngDevMode ? [{ debugName: "isView" }] : /* istanbul ignore next */ []));
|
|
532
575
|
/** Expose the type-guard to the template. */
|
|
533
|
-
isGroup =
|
|
576
|
+
isGroup = sdIsQbGroup;
|
|
534
577
|
/** Resolve the leading icon for a field (own `icon` → per-type → `'tune'`) — used by the field picker. */
|
|
535
|
-
fieldIcon =
|
|
578
|
+
fieldIcon = sdQbFieldIcon;
|
|
536
579
|
constructor() {
|
|
537
580
|
// Inbound seeding: an EXTERNAL write to value / filters rebuilds the tree.
|
|
538
581
|
// Our own emits are recognised via #lastEmitted and skipped (no echo loop).
|
|
@@ -610,12 +653,12 @@ class SdQueryBuilder {
|
|
|
610
653
|
this.#bumpTree();
|
|
611
654
|
}
|
|
612
655
|
#hasOpenDropdown(node) {
|
|
613
|
-
if (!
|
|
656
|
+
if (!sdIsQbGroup(node))
|
|
614
657
|
return false;
|
|
615
658
|
return node.open === true || node.children.some(c => this.#hasOpenDropdown(c));
|
|
616
659
|
}
|
|
617
660
|
#closeAllDropdowns(node) {
|
|
618
|
-
if (
|
|
661
|
+
if (sdIsQbGroup(node)) {
|
|
619
662
|
node.open = false;
|
|
620
663
|
node.children.forEach(c => this.#closeAllDropdowns(c));
|
|
621
664
|
}
|
|
@@ -624,7 +667,7 @@ class SdQueryBuilder {
|
|
|
624
667
|
addRule(group) {
|
|
625
668
|
if (this.resolvedDisabled())
|
|
626
669
|
return;
|
|
627
|
-
group.children.push(
|
|
670
|
+
group.children.push(sdQbNewRule());
|
|
628
671
|
group.open = false;
|
|
629
672
|
this.#apply();
|
|
630
673
|
}
|
|
@@ -632,7 +675,7 @@ class SdQueryBuilder {
|
|
|
632
675
|
addGroup(group) {
|
|
633
676
|
if (this.resolvedDisabled())
|
|
634
677
|
return;
|
|
635
|
-
group.children.push(
|
|
678
|
+
group.children.push(sdQbNewGroup('AND', [sdQbNewRule()]));
|
|
636
679
|
group.open = false;
|
|
637
680
|
this.#apply();
|
|
638
681
|
}
|
|
@@ -661,7 +704,7 @@ class SdQueryBuilder {
|
|
|
661
704
|
return; // swap-only: never clear the field (issue #2)
|
|
662
705
|
rule.field = key;
|
|
663
706
|
const field = this.fieldByKey()[rule.field];
|
|
664
|
-
const op =
|
|
707
|
+
const op = sdQbDefaultOperator(field);
|
|
665
708
|
rule.operator = op;
|
|
666
709
|
rule.value = this.#defaultValueFor(op);
|
|
667
710
|
this.#useLiteralSource(rule);
|
|
@@ -742,25 +785,25 @@ class SdQueryBuilder {
|
|
|
742
785
|
return raw;
|
|
743
786
|
}
|
|
744
787
|
#defaultValueFor(op) {
|
|
745
|
-
if (
|
|
788
|
+
if (sdQbIsMultiOperator(op))
|
|
746
789
|
return [];
|
|
747
790
|
if (op === 'BETWEEN')
|
|
748
791
|
return { from: null, to: null };
|
|
749
792
|
return null;
|
|
750
793
|
}
|
|
751
794
|
#reshapeValue(op, current) {
|
|
752
|
-
if (
|
|
795
|
+
if (sdQbIsNoDataOperator(op))
|
|
753
796
|
return null;
|
|
754
|
-
if (
|
|
797
|
+
if (sdQbIsMultiOperator(op))
|
|
755
798
|
return Array.isArray(current) ? current : current == null ? [] : [current];
|
|
756
799
|
if (op === 'BETWEEN') {
|
|
757
800
|
// a relative spec / 'TODAY' can't be a BETWEEN endpoint — reset to a fresh range
|
|
758
|
-
return current && typeof current === 'object' && !Array.isArray(current) && !
|
|
801
|
+
return current && typeof current === 'object' && !Array.isArray(current) && !sdQbIsRelativeDate(current)
|
|
759
802
|
? current
|
|
760
803
|
: { from: null, to: null };
|
|
761
804
|
}
|
|
762
805
|
// single value — keep a relative spec or the 'TODAY' sentinel; else drop array/range remnants
|
|
763
|
-
if (
|
|
806
|
+
if (sdQbIsToday(current) || sdQbIsRelativeDate(current))
|
|
764
807
|
return current;
|
|
765
808
|
return Array.isArray(current) || (current && typeof current === 'object') ? null : current;
|
|
766
809
|
}
|
|
@@ -780,7 +823,7 @@ class SdQueryBuilder {
|
|
|
780
823
|
}
|
|
781
824
|
/** Operators allowed for a rule, derived from its field's type / override. */
|
|
782
825
|
allowedOperators(rule) {
|
|
783
|
-
return
|
|
826
|
+
return sdQbAllowedOperators(this.fieldOf(rule));
|
|
784
827
|
}
|
|
785
828
|
/** Whether to render the operator picker — hidden when only one operator is allowed. */
|
|
786
829
|
showOperatorSelector(rule) {
|
|
@@ -793,7 +836,7 @@ class SdQueryBuilder {
|
|
|
793
836
|
return this.#compareFieldsByKey().get(field.key) ?? QB_EMPTY_FIELDS;
|
|
794
837
|
}
|
|
795
838
|
canCompareWithField(rule) {
|
|
796
|
-
return
|
|
839
|
+
return sdQbSupportsFieldCompareOperator(rule.operator) && this.compareFields(rule).length > 0;
|
|
797
840
|
}
|
|
798
841
|
showValueSourceSelector(rule) {
|
|
799
842
|
return this.canCompareWithField(rule) && (this.resolvedComparisonMode() === 'value-or-field' || rule.valueSource === 'field');
|
|
@@ -806,22 +849,39 @@ class SdQueryBuilder {
|
|
|
806
849
|
}
|
|
807
850
|
/** True for NULL / NOT_NULL — the template then renders no value editor. */
|
|
808
851
|
isNoData(op) {
|
|
809
|
-
return
|
|
852
|
+
return sdQbIsNoDataOperator(op);
|
|
810
853
|
}
|
|
811
854
|
/** True for IN / NOT_IN — the template then renders a multi-select value editor. */
|
|
812
855
|
isMulti(op) {
|
|
813
|
-
return
|
|
856
|
+
return sdQbIsMultiOperator(op);
|
|
814
857
|
}
|
|
815
858
|
/** `[true/false]` option list for a boolean field's value select (stable reference). */
|
|
816
859
|
booleanItems(field) {
|
|
817
|
-
return this.#booleanOptionsByKey().get(field.key) ??
|
|
860
|
+
return this.#booleanOptionsByKey().get(field.key) ?? SD_QB_EMPTY_OPTIONS;
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Option list cho field `type:'values'`.
|
|
864
|
+
*
|
|
865
|
+
* why: template cũ bind thẳng `[items]="fieldOf(rule)!.values || []"`. `values` là OPTIONAL trên
|
|
866
|
+
* `SdQueryBuilderField`, nên với field khai thiếu `values`, nhánh `|| []` cấp phát một mảng MỚI
|
|
867
|
+
* mỗi lần change detection chạy — đúng nguyên nhân OOM đã ghi ở `#booleanOptionsByKey`:
|
|
868
|
+
* `sd-select` có `toObservable(items)` → `markForCheck()` → CD mới → mảng mới → lặp vô hạn.
|
|
869
|
+
*
|
|
870
|
+
* why: KHÔNG memo hoá qua `computed<Map>` snapshot. `field.values` vốn ĐÃ là mảng của consumer
|
|
871
|
+
* nên tự nó ổn định; snapshot vào Map chỉ thêm một tầng cache không bao giờ invalidate khi
|
|
872
|
+
* consumer nạp option bất đồng bộ bằng cách gán tại chỗ (`fields[0].values = loaded`) — reference
|
|
873
|
+
* `fields` không đổi nên `computed` giữ nguyên list cũ vĩnh viễn. Chỉ nhánh RỖNG cần hằng số
|
|
874
|
+
* dùng chung để tránh literal mới mỗi CD.
|
|
875
|
+
*/
|
|
876
|
+
valueItems(field) {
|
|
877
|
+
return field.values?.length ? field.values : SD_QB_EMPTY_OPTIONS;
|
|
818
878
|
}
|
|
819
879
|
/** Current date-value mode of a rule, derived from its value (no separate state). */
|
|
820
880
|
dateMode(rule) {
|
|
821
881
|
const v = rule.value;
|
|
822
|
-
if (
|
|
882
|
+
if (sdQbIsToday(v))
|
|
823
883
|
return 'now';
|
|
824
|
-
if (
|
|
884
|
+
if (sdQbIsRelativeDate(v))
|
|
825
885
|
return 'relative';
|
|
826
886
|
return 'absolute';
|
|
827
887
|
}
|
|
@@ -831,9 +891,9 @@ class SdQueryBuilder {
|
|
|
831
891
|
return;
|
|
832
892
|
this.#useLiteralSource(rule);
|
|
833
893
|
if (mode === 'now')
|
|
834
|
-
rule.value =
|
|
894
|
+
rule.value = SD_QB_TODAY;
|
|
835
895
|
else if (mode === 'relative')
|
|
836
|
-
rule.value =
|
|
896
|
+
rule.value = sdQbDefaultRelative();
|
|
837
897
|
else
|
|
838
898
|
rule.value = null; // absolute — pick a concrete date
|
|
839
899
|
this.#apply();
|
|
@@ -841,7 +901,7 @@ class SdQueryBuilder {
|
|
|
841
901
|
/** Read the offset amount of a relative rule (default 1). */
|
|
842
902
|
relativeAmount(rule) {
|
|
843
903
|
const v = rule.value;
|
|
844
|
-
return
|
|
904
|
+
return sdQbIsRelativeDate(v) ? (v.amount ?? 1) : 1;
|
|
845
905
|
}
|
|
846
906
|
/** Set the offset amount (clamped to an integer >= 1). */
|
|
847
907
|
setRelativeAmount(rule, raw) {
|
|
@@ -850,14 +910,14 @@ class SdQueryBuilder {
|
|
|
850
910
|
this.#useLiteralSource(rule);
|
|
851
911
|
const n = Math.floor(Number(raw));
|
|
852
912
|
const amount = Number.isNaN(n) || n < 1 ? 1 : n;
|
|
853
|
-
const cur =
|
|
913
|
+
const cur = sdQbIsRelativeDate(rule.value) ? rule.value : sdQbDefaultRelative();
|
|
854
914
|
rule.value = { amount, direction: cur.direction, unit: cur.unit };
|
|
855
915
|
this.#apply();
|
|
856
916
|
}
|
|
857
917
|
/** Read the `'unit:direction'` token of a relative rule (default `'day:previous'`). */
|
|
858
918
|
relativeUnitDirValue(rule) {
|
|
859
919
|
const v = rule.value;
|
|
860
|
-
if (
|
|
920
|
+
if (sdQbIsRelativeDate(v))
|
|
861
921
|
return `${v.unit}:${v.direction}`;
|
|
862
922
|
return 'day:previous';
|
|
863
923
|
}
|
|
@@ -868,7 +928,7 @@ class SdQueryBuilder {
|
|
|
868
928
|
this.#useLiteralSource(rule);
|
|
869
929
|
const normalizedToken = typeof token === 'string' ? token : 'day:previous';
|
|
870
930
|
const [unit, direction] = normalizedToken.split(':');
|
|
871
|
-
const cur =
|
|
931
|
+
const cur = sdQbIsRelativeDate(rule.value) ? rule.value : sdQbDefaultRelative();
|
|
872
932
|
rule.value = { amount: cur.amount, direction, unit };
|
|
873
933
|
this.#apply();
|
|
874
934
|
}
|
|
@@ -877,16 +937,16 @@ class SdQueryBuilder {
|
|
|
877
937
|
return `${this.resolvedAutoId() ?? 'qb'}-${suffix}`;
|
|
878
938
|
}
|
|
879
939
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SdQueryBuilder, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
880
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: SdQueryBuilder, isStandalone: true, selector: "sd-query-builder", inputs: { option: { classPropertyName: "option", publicName: "option", isSignal: true, isRequired: false, transformFunction: null }, fields: { classPropertyName: "fields", publicName: "fields", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, comparisonMode: { classPropertyName: "comparisonMode", publicName: "comparisonMode", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, autoId: { classPropertyName: "autoId", publicName: "autoId", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, filters: { classPropertyName: "filters", publicName: "filters", isSignal: true, isRequired: false, transformFunction: null }, rootLogic: { classPropertyName: "rootLogic", publicName: "rootLogic", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", filters: "filtersChange", rootLogic: "rootLogicChange" }, ngImport: i0, template: "@if (isView()) {\n <div class=\"qb-view\" [attr.data-autoid]=\"autoIdFor('view')\" aria-readonly=\"true\">\n @if (tokens().length) {\n @for (t of tokens(); track $index) {\n <span class=\"qb-tok qb-tok-{{ t.kind }}\">{{ t.text }}</span>\n }\n } @else {\n <span class=\"qb-view-empty\">\u2014</span>\n }\n </div>\n} @else {\n <div class=\"qb-container\" [class.is-disabled]=\"resolvedDisabled()\" (click)=\"closeAllDropdowns()\">\n <ng-container [ngTemplateOutlet]=\"groupTpl\" [ngTemplateOutletContext]=\"{ $implicit: tree(), isRoot: true }\"></ng-container>\n </div>\n}\n\n<!-- Recursive group node -->\n<ng-template #groupTpl let-group let-isRoot=\"isRoot\" let-parent=\"parent\" let-index=\"index\">\n <div class=\"qb-group\" [class.is-root]=\"isRoot\">\n <div class=\"qb-header\" [class.is-open]=\"group.open\">\n <div class=\"qb-logic-switch\">\n <button type=\"button\" [class.active]=\"group.logic === 'AND'\" [disabled]=\"resolvedDisabled()\" (click)=\"setGroupLogic(group, 'AND')\">AND</button>\n <button type=\"button\" [class.active]=\"group.logic === 'OR'\" [disabled]=\"resolvedDisabled()\" (click)=\"setGroupLogic(group, 'OR')\">OR</button>\n </div>\n\n <div class=\"qb-actions\">\n <button type=\"button\" class=\"qb-btn qb-btn-add\" title=\"Th\u00EAm\" aria-label=\"Th\u00EAm \u0111i\u1EC1u ki\u1EC7n ho\u1EB7c nh\u00F3m\" [disabled]=\"resolvedDisabled()\" (click)=\"toggleDropdown(group, $event)\">+</button>\n @if (group.open) {\n <div class=\"qb-dropdown\" (click)=\"$event.stopPropagation()\">\n <button type=\"button\" (click)=\"addRule(group)\">+ \u0110i\u1EC1u ki\u1EC7n</button>\n <button type=\"button\" (click)=\"addGroup(group)\">+ Nh\u00F3m</button>\n </div>\n }\n @if (!isRoot) {\n <button type=\"button\" class=\"qb-btn qb-btn-remove\" title=\"Xo\u00E1 nh\u00F3m\" aria-label=\"Xo\u00E1 nh\u00F3m\" [disabled]=\"resolvedDisabled()\" (click)=\"removeNode(parent, index)\">\u2715</button>\n }\n </div>\n </div>\n\n <div class=\"qb-body\">\n @for (node of group.children; track node.id; let i = $index) {\n <div class=\"qb-treenode\" [class.is-group]=\"isGroup(node)\" [class.is-rule]=\"!isGroup(node)\">\n @if (isGroup(node)) {\n <ng-container\n [ngTemplateOutlet]=\"groupTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: node, isRoot: false, parent: group, index: i }\"\n ></ng-container>\n } @else {\n <ng-container [ngTemplateOutlet]=\"ruleTpl\" [ngTemplateOutletContext]=\"{ $implicit: node, parent: group, index: i }\"></ng-container>\n }\n </div>\n } @empty {\n <div class=\"qb-empty\">Ch\u01B0a c\u00F3 \u0111i\u1EC1u ki\u1EC7n</div>\n }\n </div>\n </div>\n</ng-template>\n\n<!-- Single rule node: field \u2192 operator \u2192 value -->\n<ng-template #ruleTpl let-rule let-parent=\"parent\" let-index=\"index\">\n <div class=\"qb-rule\">\n <sd-select\n class=\"qb-field\"\n [items]=\"resolvedFields()\"\n valueField=\"key\"\n displayField=\"label\"\n placeholder=\"Ch\u1ECDn tr\u01B0\u1EDDng\"\n size=\"sm\"\n [model]=\"rule.field\"\n (modelChange)=\"setField(rule, $event)\"\n [disabled]=\"resolvedDisabled()\"\n [clearable]=\"false\"\n hideInlineError\n [autoId]=\"autoIdFor('field-' + index)\"\n >\n <!-- Option trong dropdown: icon th\u1EC3 hi\u1EC7n type + label. -->\n <ng-template sdItemDef let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"fieldIcon($any(item))\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item).label }}</span>\n </span>\n </ng-template>\n <!-- Gi\u00E1 tr\u1ECB \u0111\u00E3 ch\u1ECDn \u1EDF trigger: icon m\u1EB7c \u0111\u1ECBnh theo type khi field ch\u01B0a c\u00F3 icon ri\u00EAng. -->\n <ng-template #sdSelected let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"fieldIcon($any(item))\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item)?.label }}</span>\n </span>\n </ng-template>\n </sd-select>\n\n @if (rule.field) {\n <div class=\"qb-divider\"></div>\n\n @if (showOperatorSelector(rule)) {\n <sd-operator\n class=\"qb-op\"\n [operators]=\"allowedOperators(rule)\"\n [model]=\"rule.operator\"\n (modelChange)=\"setOperator(rule, $event)\"\n [disabled]=\"resolvedDisabled()\"\n [autoId]=\"autoIdFor('op-' + index)\"\n ></sd-operator>\n }\n\n @if (rule.operator && !isNoData(rule.operator)) {\n <div class=\"qb-divider\"></div>\n\n @if (showValueSourceSelector(rule)) {\n <sd-select\n class=\"qb-value-source\"\n [items]=\"valueSourceOptions\"\n valueField=\"value\"\n displayField=\"display\"\n size=\"sm\"\n [model]=\"valueSourceOf(rule)\"\n (modelChange)=\"setValueSource(rule, $event)\"\n [disabled]=\"resolvedDisabled()\"\n [clearable]=\"false\"\n hideInlineError\n [autoId]=\"autoIdFor('valuesource-' + index)\"\n >\n <ng-template sdItemDef let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item).icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item).display }}</span>\n </span>\n </ng-template>\n <ng-template #sdSelected let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item)?.icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item)?.display }}</span>\n </span>\n </ng-template>\n </sd-select>\n <div class=\"qb-divider\"></div>\n }\n\n @if (isFieldSource(rule)) {\n <sd-select\n class=\"qb-val qb-compare-field\"\n [items]=\"compareFields(rule)\"\n valueField=\"key\"\n displayField=\"label\"\n placeholder=\"Ch\u1ECDn tr\u01B0\u1EDDng\"\n size=\"sm\"\n [model]=\"rule.compareField\"\n (modelChange)=\"setCompareField(rule, $event)\"\n [disabled]=\"resolvedDisabled()\"\n [clearable]=\"false\"\n hideInlineError\n [autoId]=\"autoIdFor('compare-field-' + index)\"\n >\n <ng-template sdItemDef let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"fieldIcon($any(item))\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item).label }}</span>\n </span>\n </ng-template>\n <ng-template #sdSelected let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"fieldIcon($any(item))\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item)?.label }}</span>\n </span>\n </ng-template>\n </sd-select>\n } @else if (rule.operator === 'BETWEEN') {\n <div class=\"qb-between\">\n @switch (fieldOf(rule)?.type) {\n @case ('number') {\n <sd-input-number class=\"qb-val\" placeholder=\"T\u1EEB\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [min]=\"fieldOf(rule)?.min\" [max]=\"fieldOf(rule)?.max\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-input-number>\n <span class=\"qb-dash\">\u2014</span>\n <sd-input-number class=\"qb-val\" placeholder=\"\u0110\u1EBFn\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [min]=\"fieldOf(rule)?.min\" [max]=\"fieldOf(rule)?.max\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('to-' + index)\"></sd-input-number>\n }\n @case ('date') {\n <sd-date class=\"qb-val qb-date-val\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-date>\n <span class=\"qb-dash\">\u2014</span>\n <sd-date class=\"qb-val qb-date-val\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('to-' + index)\"></sd-date>\n }\n @case ('datetime') {\n <sd-datetime class=\"qb-val\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-datetime>\n <span class=\"qb-dash\">\u2014</span>\n <sd-datetime class=\"qb-val\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('to-' + index)\"></sd-datetime>\n }\n }\n </div>\n } @else {\n @switch (fieldOf(rule)?.type) {\n @case ('string') {\n <sd-input class=\"qb-val\" placeholder=\"Gi\u00E1 tr\u1ECB\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-input>\n }\n @case ('number') {\n <sd-input-number class=\"qb-val\" placeholder=\"Gi\u00E1 tr\u1ECB\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [min]=\"fieldOf(rule)?.min\" [max]=\"fieldOf(rule)?.max\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-input-number>\n }\n @case ('boolean') {\n <sd-select class=\"qb-val\" [items]=\"booleanItems(fieldOf(rule)!)\" valueField=\"value\" displayField=\"display\" placeholder=\"Gi\u00E1 tr\u1ECB\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-select>\n }\n @case ('date') {\n @let _dateMode = dateMode(rule);\n <sd-select class=\"qb-date-mode\" [items]=\"dateModes\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"_dateMode\" (modelChange)=\"setDateMode(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('datemode-' + index)\">\n <ng-template sdItemDef let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item).icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item).display }}</span>\n </span>\n </ng-template>\n <ng-template #sdSelected let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item)?.icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item)?.display }}</span>\n </span>\n </ng-template>\n </sd-select>\n @switch (_dateMode) {\n @case ('absolute') {\n <sd-date class=\"qb-val qb-date-val\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-date>\n }\n @case ('relative') {\n <sd-input-number class=\"qb-rel-amount\" type=\"positive\" [precision]=\"0\" size=\"sm\" [model]=\"relativeAmount(rule)\" (modelChange)=\"setRelativeAmount(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('rel-amount-' + index)\"></sd-input-number>\n <sd-select class=\"qb-rel-unit\" [items]=\"relativeUnitOptions\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"relativeUnitDirValue(rule)\" (modelChange)=\"setRelativeUnitDir(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('rel-unit-' + index)\"></sd-select>\n }\n }\n }\n @case ('datetime') {\n @let _dtMode = dateMode(rule);\n <sd-select class=\"qb-date-mode\" [items]=\"dateModes\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"_dtMode\" (modelChange)=\"setDateMode(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('datemode-' + index)\">\n <ng-template sdItemDef let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item).icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item).display }}</span>\n </span>\n </ng-template>\n <ng-template #sdSelected let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item)?.icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item)?.display }}</span>\n </span>\n </ng-template>\n </sd-select>\n @switch (_dtMode) {\n @case ('absolute') {\n <sd-datetime class=\"qb-val\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-datetime>\n }\n @case ('relative') {\n <sd-input-number class=\"qb-rel-amount\" type=\"positive\" [precision]=\"0\" size=\"sm\" [model]=\"relativeAmount(rule)\" (modelChange)=\"setRelativeAmount(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('rel-amount-' + index)\"></sd-input-number>\n <sd-select class=\"qb-rel-unit\" [items]=\"relativeUnitOptions\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"relativeUnitDirValue(rule)\" (modelChange)=\"setRelativeUnitDir(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('rel-unit-' + index)\"></sd-select>\n }\n }\n }\n @case ('values') {\n <sd-select class=\"qb-val\" [items]=\"fieldOf(rule)!.values || []\" valueField=\"value\" displayField=\"display\" [multiple]=\"isMulti(rule.operator)\" placeholder=\"Gi\u00E1 tr\u1ECB\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-select>\n }\n }\n }\n }\n }\n\n <button type=\"button\" class=\"qb-btn qb-btn-remove-rule\" title=\"Xo\u00E1 \u0111i\u1EC1u ki\u1EC7n\" aria-label=\"Xo\u00E1 \u0111i\u1EC1u ki\u1EC7n\" [disabled]=\"resolvedDisabled()\" (click)=\"removeNode(parent, index)\">\u2715</button>\n </div>\n</ng-template>\n", styles: [":host{display:block;font-size:14px;color:var(--sd-text, #333)}.qb-view{display:block;width:100%;min-height:38px;padding:8px 12px;border:1px solid #d0d5dd;border-radius:6px;background:var(--sd-disabled-bg, #f6f7f9);color:var(--sd-text, #333);font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:13px;line-height:1.7;cursor:default;-webkit-user-select:text;user-select:text;white-space:pre-wrap;word-break:break-word}.qb-view-empty{color:#98a2b3}.qb-tok{white-space:pre}.qb-tok.qb-tok-field{color:var(--sd-text, #1d2939);font-weight:600}.qb-tok.qb-tok-op{color:var(--sd-primary, #005cbb);font-weight:600}.qb-tok.qb-tok-value{color:var(--sd-success, #1a7f37)}.qb-tok.qb-tok-logic{color:#98a2b3;font-weight:700;text-transform:uppercase}.qb-tok.qb-tok-paren{color:#667085}.qb-container{padding:16px;background:#fff;border:1px solid #e0e0e0;border-radius:6px}.qb-container.is-disabled{opacity:.7;pointer-events:none}.qb-header{display:flex;align-items:center;gap:8px;margin-bottom:12px;position:relative;z-index:1}.qb-header.is-open{z-index:1000}.qb-logic-switch{display:inline-flex;border:1px solid #e0e0e0;border-radius:4px;overflow:hidden;height:30px}.qb-logic-switch button{border:none;background:transparent;padding:0 14px;font-weight:600;font-size:12px;cursor:pointer;color:#667085;height:100%}.qb-logic-switch button.active{background:var(--sd-primary, #005cbb);color:#fff}.qb-logic-switch button:hover:not(.active):not([disabled]){background:var(--sd-primary-light, #d7e3ff);color:var(--sd-primary-dark, #1c4ad9)}.qb-logic-switch button[disabled]{cursor:default}.qb-actions{position:relative;display:flex;align-items:center;gap:4px}.qb-btn{background:transparent;border:none;cursor:pointer;font-size:16px;color:#667085;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;border-radius:4px}.qb-btn:hover:not([disabled]){background:var(--sd-primary-light, #d7e3ff);color:var(--sd-primary, #005cbb)}.qb-btn[disabled]{cursor:default;color:#cbd2dc}.qb-btn-remove:hover:not([disabled]),.qb-btn-remove-rule:hover:not([disabled]){background:#fee4e2;color:#d92d20}.qb-btn-remove-rule{margin-left:auto}.qb-dropdown{position:absolute;top:100%;left:0;margin-top:4px;background:#fff;border:1px solid #e0e0e0;box-shadow:0 4px 12px #1018281f;border-radius:6px;min-width:150px;z-index:100;overflow:hidden}.qb-dropdown button{display:block;width:100%;text-align:left;background:none;border:none;padding:9px 14px;cursor:pointer;font-size:13px;color:#344054}.qb-dropdown button:hover{background:var(--sd-primary-light, #d7e3ff);color:var(--sd-primary, #005cbb)}.qb-body{padding-left:24px}.qb-treenode{position:relative;padding-left:22px;margin-bottom:12px}.qb-treenode:last-child{margin-bottom:0}.qb-treenode:before{content:\"\";position:absolute;left:0;top:-12px;bottom:0;border-left:1px dashed #d0d5dd}.qb-treenode:after{content:\"\";position:absolute;left:0;top:50%;width:18px;border-top:1px dashed #d0d5dd}.qb-treenode:last-child:before{bottom:auto;height:calc(50% + 12px)}.qb-empty{color:#98a2b3;font-style:italic;font-size:13px;padding:4px 0}.qb-rule{display:flex;align-items:center;gap:8px;background:#fafbfc;border:1px solid #e0e0e0;border-radius:6px;padding:6px 10px}.qb-rule:hover{border-color:#c2c8d0}.qb-field{min-width:170px}.qb-val{min-width:140px}.qb-value-source{min-width:132px}.qb-compare-field{min-width:170px}.qb-date-val{min-width:0;width:132px;flex:0 0 auto}::ng-deep .qb-opt{display:flex;align-items:center;gap:6px;min-width:0}::ng-deep .qb-opt .qb-opt-iconsd-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;margin:0;font-size:18px;width:18px;height:18px;line-height:1;color:#667085}::ng-deep .qb-opt .qb-opt-label{min-width:0}.qb-between{display:flex;align-items:center;gap:6px}.qb-dash{color:#98a2b3}.qb-divider{width:1px;align-self:stretch;background:#e0e0e0}.qb-treenode.is-group>.qb-group{border:1px solid #e0e0e0;border-left:2px solid var(--sd-primary, #005cbb);border-radius:6px;padding:12px;background:#fff}.qb-date-mode{min-width:160px}.qb-rel-amount{width:72px}.qb-rel-unit{min-width:110px}\n"], dependencies: [{ kind: "component", type: SdIcon, selector: "sd-icon", inputs: ["name", "fontIcon", "color", "set", "fontSet", "size", "strokeWidth", "absoluteStrokeWidth", "ariaLabel"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: SdOperator, selector: "sd-operator", inputs: ["model", "operators", "disabled", "autoId"], outputs: ["modelChange"] }, { kind: "component", type: SdSelect, selector: "sd-select", inputs: ["autoId", "name", "size", "form", "label", "helperText", "placeholder", "valueField", "displayField", "disabledField", "cacheChecksum", "limit", "hyperlink", "minWidthPanel", "hideInlineError", "required", "disabled", "viewed", "multiple", "clearable", "validator", "inlineError", "appearance", "floatLabel", "items", "model"], outputs: ["modelChange", "sdChange", "sdSelection"] }, { kind: "component", type: SdInput, selector: "sd-input", inputs: ["autoId", "name", "appearance", "floatLabel", "size", "form", "label", "helperText", "placeholder", "type", "mask", "hideInlineError", "blurOnEnter", "clearable", "required", "readonly", "disabled", "viewed", "minlength", "maxlength", "pattern", "patternErrorMessage", "validator", "inlineError", "hyperlink", "model"], outputs: ["modelChange", "sdChange", "sdFocus", "sdBlur", "keyupEnter", "cleared", "sdFocusForceBlur"] }, { kind: "component", type: SdInputNumber, selector: "sd-input-number", inputs: ["autoId", "name", "size", "form", "label", "helperText", "placeholder", "hideInlineError", "blurOnEnter", "clearable", "required", "readonly", "disabled", "viewed", "type", "precision", "format", "min", "max", "validator", "inlineError", "hyperlink", "appearance", "floatLabel", "model"], outputs: ["modelChange", "sdChange", "sdFocus", "sdBlur", "keyupEnter", "cleared", "sdFocusForceBlur"] }, { kind: "component", type: SdDate, selector: "sd-date", inputs: ["autoId", "name", "size", "form", "label", "helperText", "placeholder", "hideInlineError", "required", "disabled", "viewed", "clearable", "inlineError", "hyperlink", "appearance", "floatLabel", "min", "minDate", "max", "maxDate", "model"], outputs: ["modelChange", "sdChange", "sdFocus"] }, { kind: "component", type: SdDatetime, selector: "sd-datetime", inputs: ["autoId", "name", "size", "form", "label", "helperText", "placeholder", "hideInlineError", "required", "disabled", "viewed", "showSeconds", "clearable", "inlineError", "hyperlink", "appearance", "floatLabel", "min", "minDate", "max", "maxDate", "model"], outputs: ["modelChange", "sdChange", "sdFocus"] }, { kind: "directive", type: SdItemDefDefDirective, selector: "[sdItemDef]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
940
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: SdQueryBuilder, isStandalone: true, selector: "sd-query-builder", inputs: { option: { classPropertyName: "option", publicName: "option", isSignal: true, isRequired: false, transformFunction: null }, fields: { classPropertyName: "fields", publicName: "fields", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, comparisonMode: { classPropertyName: "comparisonMode", publicName: "comparisonMode", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, autoId: { classPropertyName: "autoId", publicName: "autoId", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, filters: { classPropertyName: "filters", publicName: "filters", isSignal: true, isRequired: false, transformFunction: null }, rootLogic: { classPropertyName: "rootLogic", publicName: "rootLogic", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", filters: "filtersChange", rootLogic: "rootLogicChange" }, ngImport: i0, template: "@if (isView()) {\n <div class=\"qb-view\" [attr.data-autoid]=\"autoIdFor('view')\" aria-readonly=\"true\">\n @if (tokens().length) {\n @for (t of tokens(); track $index) {\n <span class=\"qb-tok qb-tok-{{ t.kind }}\">{{ t.text }}</span>\n }\n } @else {\n <span class=\"qb-view-empty\">\u2014</span>\n }\n </div>\n} @else {\n <!-- why: \"click ra v\u00F9ng tr\u1ED1ng \u0111\u1EC3 \u0111\u00F3ng dropdown\" l\u00E0 h\u00E0nh vi CH\u1EC8 c\u00F3 \u1EDF chu\u1ED9t \u2014 ng\u01B0\u1EDDi d\u00F9ng b\u00E0n ph\u00EDm\n kh\u00F4ng c\u00F3 c\u00E1ch n\u00E0o \u0111\u00F3ng. Th\u00EAm Escape l\u00E0m t\u01B0\u01A1ng \u0111\u01B0\u01A1ng. role=\"group\": container l\u00E0 m\u1ED9t nh\u00F3m\n control n\u00EAn KH\u00D4NG nh\u1EADn tabindex, tab \u0111i th\u1EB3ng v\u00E0o c\u00E1c n\u00FAt/select b\u00EAn trong. -->\n <div\n class=\"qb-container\"\n role=\"group\"\n [class.is-disabled]=\"resolvedDisabled()\"\n (click)=\"closeAllDropdowns()\"\n (keydown.escape)=\"closeAllDropdowns()\">\n <ng-container [ngTemplateOutlet]=\"groupTpl\" [ngTemplateOutletContext]=\"{ $implicit: tree(), isRoot: true }\"></ng-container>\n </div>\n}\n\n<!-- Recursive group node -->\n<ng-template #groupTpl let-group let-isRoot=\"isRoot\" let-parent=\"parent\" let-index=\"index\">\n @let _removeGroupLabel = removeGroupLabel();\n <div class=\"qb-group\" [class.is-root]=\"isRoot\">\n <div class=\"qb-header\" [class.is-open]=\"group.open\">\n <div class=\"qb-logic-switch\">\n <button type=\"button\" [class.active]=\"group.logic === 'AND'\" [disabled]=\"resolvedDisabled()\" (click)=\"setGroupLogic(group, 'AND')\">AND</button>\n <button type=\"button\" [class.active]=\"group.logic === 'OR'\" [disabled]=\"resolvedDisabled()\" (click)=\"setGroupLogic(group, 'OR')\">OR</button>\n </div>\n\n <div class=\"qb-actions\">\n <button type=\"button\" class=\"qb-btn qb-btn-add\" [title]=\"addLabel()\" [attr.aria-label]=\"addNodeLabel()\" [disabled]=\"resolvedDisabled()\" (click)=\"toggleDropdown(group, $event)\">+</button>\n @if (group.open) {\n <!-- why: dropdown l\u00E0 NH\u00D3M hai n\u00FAt (role=\"group\"), kh\u00F4ng ph\u1EA3i n\u00FAt \u2014 kh\u00F4ng nh\u1EADn tabindex,\n tab \u0111i th\u1EB3ng v\u00E0o hai n\u00FAt. stopPropagation cho c\u1EA3 click l\u1EABn Enter \u0111\u1EC3 thao t\u00E1c b\u00EAn\n trong dropdown kh\u00F4ng k\u00EDch ho\u1EA1t handler \"\u0111\u00F3ng t\u1EA5t c\u1EA3\" c\u1EE7a container. -->\n <div class=\"qb-dropdown\" role=\"group\" (click)=\"$event.stopPropagation()\" (keydown.enter)=\"$event.stopPropagation()\">\n <button type=\"button\" (click)=\"addRule(group)\">+ {{ conditionLabel() }}</button>\n <button type=\"button\" (click)=\"addGroup(group)\">+ {{ groupLabel() }}</button>\n </div>\n }\n @if (!isRoot) {\n <button type=\"button\" class=\"qb-btn qb-btn-remove\" [title]=\"_removeGroupLabel\" [attr.aria-label]=\"_removeGroupLabel\" [disabled]=\"resolvedDisabled()\" (click)=\"removeNode(parent, index)\">\u2715</button>\n }\n </div>\n </div>\n\n <div class=\"qb-body\">\n @for (node of group.children; track node.id; let i = $index) {\n <div class=\"qb-treenode\" [class.is-group]=\"isGroup(node)\" [class.is-rule]=\"!isGroup(node)\">\n @if (isGroup(node)) {\n <ng-container\n [ngTemplateOutlet]=\"groupTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: node, isRoot: false, parent: group, index: i }\"\n ></ng-container>\n } @else {\n <ng-container [ngTemplateOutlet]=\"ruleTpl\" [ngTemplateOutletContext]=\"{ $implicit: node, parent: group, index: i }\"></ng-container>\n }\n </div>\n } @empty {\n <div class=\"qb-empty\">{{ emptyLabel() }}</div>\n }\n </div>\n </div>\n</ng-template>\n\n<!-- Single rule node: field \u2192 operator \u2192 value -->\n<ng-template #ruleTpl let-rule let-parent=\"parent\" let-index=\"index\">\n @let _selectFieldLabel = selectFieldLabel();\n @let _valueLabel = valueLabel();\n @let _removeConditionLabel = removeConditionLabel();\n @let _dateModes = dateModes();\n @let _relativeUnitOptions = relativeUnitOptions();\n <div class=\"qb-rule\">\n <sd-select\n class=\"qb-field\"\n [items]=\"resolvedFields()\"\n valueField=\"key\"\n displayField=\"label\"\n [placeholder]=\"_selectFieldLabel\"\n size=\"sm\"\n [model]=\"rule.field\"\n (modelChange)=\"setField(rule, $event)\"\n [disabled]=\"resolvedDisabled()\"\n [clearable]=\"false\"\n hideInlineError\n [autoId]=\"autoIdFor('field-' + index)\"\n >\n <!-- Option trong dropdown: icon th\u1EC3 hi\u1EC7n type + label. -->\n <ng-template sdItemDef let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"fieldIcon($any(item))\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item).label }}</span>\n </span>\n </ng-template>\n <!-- Gi\u00E1 tr\u1ECB \u0111\u00E3 ch\u1ECDn \u1EDF trigger: icon m\u1EB7c \u0111\u1ECBnh theo type khi field ch\u01B0a c\u00F3 icon ri\u00EAng. -->\n <ng-template #sdSelected let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"fieldIcon($any(item))\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item)?.label }}</span>\n </span>\n </ng-template>\n </sd-select>\n\n @if (rule.field) {\n <div class=\"qb-divider\"></div>\n\n @if (showOperatorSelector(rule)) {\n <sd-operator\n class=\"qb-op\"\n [operators]=\"allowedOperators(rule)\"\n [model]=\"rule.operator\"\n (modelChange)=\"setOperator(rule, $event)\"\n [disabled]=\"resolvedDisabled()\"\n [autoId]=\"autoIdFor('op-' + index)\"\n ></sd-operator>\n }\n\n @if (rule.operator && !isNoData(rule.operator)) {\n <div class=\"qb-divider\"></div>\n\n @if (showValueSourceSelector(rule)) {\n <sd-select\n class=\"qb-value-source\"\n [items]=\"valueSourceOptions()\"\n valueField=\"value\"\n displayField=\"display\"\n size=\"sm\"\n [model]=\"valueSourceOf(rule)\"\n (modelChange)=\"setValueSource(rule, $event)\"\n [disabled]=\"resolvedDisabled()\"\n [clearable]=\"false\"\n hideInlineError\n [autoId]=\"autoIdFor('valuesource-' + index)\"\n >\n <ng-template sdItemDef let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item).icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item).display }}</span>\n </span>\n </ng-template>\n <ng-template #sdSelected let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item)?.icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item)?.display }}</span>\n </span>\n </ng-template>\n </sd-select>\n <div class=\"qb-divider\"></div>\n }\n\n @if (isFieldSource(rule)) {\n <sd-select\n class=\"qb-val qb-compare-field\"\n [items]=\"compareFields(rule)\"\n valueField=\"key\"\n displayField=\"label\"\n [placeholder]=\"_selectFieldLabel\"\n size=\"sm\"\n [model]=\"rule.compareField\"\n (modelChange)=\"setCompareField(rule, $event)\"\n [disabled]=\"resolvedDisabled()\"\n [clearable]=\"false\"\n hideInlineError\n [autoId]=\"autoIdFor('compare-field-' + index)\"\n >\n <ng-template sdItemDef let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"fieldIcon($any(item))\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item).label }}</span>\n </span>\n </ng-template>\n <ng-template #sdSelected let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"fieldIcon($any(item))\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item)?.label }}</span>\n </span>\n </ng-template>\n </sd-select>\n } @else if (rule.operator === 'BETWEEN') {\n <div class=\"qb-between\">\n @switch (fieldOf(rule)?.type) {\n @case ('number') {\n <sd-input-number class=\"qb-val\" [placeholder]=\"fromLabel()\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [min]=\"fieldOf(rule)?.min\" [max]=\"fieldOf(rule)?.max\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-input-number>\n <span class=\"qb-dash\">\u2014</span>\n <sd-input-number class=\"qb-val\" [placeholder]=\"toLabel()\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [min]=\"fieldOf(rule)?.min\" [max]=\"fieldOf(rule)?.max\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('to-' + index)\"></sd-input-number>\n }\n @case ('date') {\n <sd-date class=\"qb-val qb-date-val\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-date>\n <span class=\"qb-dash\">\u2014</span>\n <sd-date class=\"qb-val qb-date-val\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('to-' + index)\"></sd-date>\n }\n @case ('datetime') {\n <sd-datetime class=\"qb-val\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-datetime>\n <span class=\"qb-dash\">\u2014</span>\n <sd-datetime class=\"qb-val\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('to-' + index)\"></sd-datetime>\n }\n }\n </div>\n } @else {\n @switch (fieldOf(rule)?.type) {\n @case ('string') {\n <sd-input class=\"qb-val\" [placeholder]=\"_valueLabel\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-input>\n }\n @case ('number') {\n <sd-input-number class=\"qb-val\" [placeholder]=\"_valueLabel\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [min]=\"fieldOf(rule)?.min\" [max]=\"fieldOf(rule)?.max\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-input-number>\n }\n @case ('boolean') {\n <sd-select class=\"qb-val\" [items]=\"booleanItems(fieldOf(rule)!)\" valueField=\"value\" displayField=\"display\" [placeholder]=\"_valueLabel\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-select>\n }\n @case ('date') {\n @let _dateMode = dateMode(rule);\n <sd-select class=\"qb-date-mode\" [items]=\"_dateModes\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"_dateMode\" (modelChange)=\"setDateMode(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('datemode-' + index)\">\n <ng-template sdItemDef let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item).icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item).display }}</span>\n </span>\n </ng-template>\n <ng-template #sdSelected let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item)?.icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item)?.display }}</span>\n </span>\n </ng-template>\n </sd-select>\n @switch (_dateMode) {\n @case ('absolute') {\n <sd-date class=\"qb-val qb-date-val\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-date>\n }\n @case ('relative') {\n <sd-input-number class=\"qb-rel-amount\" type=\"positive\" [precision]=\"0\" size=\"sm\" [model]=\"relativeAmount(rule)\" (modelChange)=\"setRelativeAmount(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('rel-amount-' + index)\"></sd-input-number>\n <sd-select class=\"qb-rel-unit\" [items]=\"_relativeUnitOptions\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"relativeUnitDirValue(rule)\" (modelChange)=\"setRelativeUnitDir(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('rel-unit-' + index)\"></sd-select>\n }\n }\n }\n @case ('datetime') {\n @let _dtMode = dateMode(rule);\n <sd-select class=\"qb-date-mode\" [items]=\"_dateModes\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"_dtMode\" (modelChange)=\"setDateMode(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('datemode-' + index)\">\n <ng-template sdItemDef let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item).icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item).display }}</span>\n </span>\n </ng-template>\n <ng-template #sdSelected let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item)?.icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item)?.display }}</span>\n </span>\n </ng-template>\n </sd-select>\n @switch (_dtMode) {\n @case ('absolute') {\n <sd-datetime class=\"qb-val\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-datetime>\n }\n @case ('relative') {\n <sd-input-number class=\"qb-rel-amount\" type=\"positive\" [precision]=\"0\" size=\"sm\" [model]=\"relativeAmount(rule)\" (modelChange)=\"setRelativeAmount(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('rel-amount-' + index)\"></sd-input-number>\n <sd-select class=\"qb-rel-unit\" [items]=\"_relativeUnitOptions\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"relativeUnitDirValue(rule)\" (modelChange)=\"setRelativeUnitDir(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('rel-unit-' + index)\"></sd-select>\n }\n }\n }\n @case ('values') {\n <sd-select class=\"qb-val\" [items]=\"valueItems(fieldOf(rule)!)\" valueField=\"value\" displayField=\"display\" [multiple]=\"isMulti(rule.operator)\" [placeholder]=\"_valueLabel\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-select>\n }\n }\n }\n }\n }\n\n <button type=\"button\" class=\"qb-btn qb-btn-remove-rule\" [title]=\"_removeConditionLabel\" [attr.aria-label]=\"_removeConditionLabel\" [disabled]=\"resolvedDisabled()\" (click)=\"removeNode(parent, index)\">\u2715</button>\n </div>\n</ng-template>\n", styles: [":host{display:block;font-size:14px;color:var(--sd-text, #333)}.qb-view{display:block;width:100%;min-height:38px;padding:8px 12px;border:1px solid #d0d5dd;border-radius:6px;background:var(--sd-disabled-bg, #f6f7f9);color:var(--sd-text, #333);font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:13px;line-height:1.7;cursor:default;-webkit-user-select:text;user-select:text;white-space:pre-wrap;word-break:break-word}.qb-view-empty{color:#98a2b3}.qb-tok{white-space:pre}.qb-tok.qb-tok-field{color:var(--sd-text, #1d2939);font-weight:600}.qb-tok.qb-tok-op{color:var(--sd-primary, #005cbb);font-weight:600}.qb-tok.qb-tok-value{color:var(--sd-success, #1a7f37)}.qb-tok.qb-tok-logic{color:#98a2b3;font-weight:700;text-transform:uppercase}.qb-tok.qb-tok-paren{color:#667085}.qb-container{padding:16px;background:#fff;border:1px solid #e0e0e0;border-radius:6px}.qb-container.is-disabled{opacity:.7;pointer-events:none}.qb-header{display:flex;align-items:center;gap:8px;margin-bottom:12px;position:relative;z-index:1}.qb-header.is-open{z-index:1000}.qb-logic-switch{display:inline-flex;border:1px solid #e0e0e0;border-radius:4px;overflow:hidden;height:30px}.qb-logic-switch button{border:none;background:transparent;padding:0 14px;font-weight:600;font-size:12px;cursor:pointer;color:#667085;height:100%}.qb-logic-switch button.active{background:var(--sd-primary, #005cbb);color:#fff}.qb-logic-switch button:hover:not(.active):not([disabled]){background:var(--sd-primary-light, #d7e3ff);color:var(--sd-primary-dark, #1c4ad9)}.qb-logic-switch button[disabled]{cursor:default}.qb-actions{position:relative;display:flex;align-items:center;gap:4px}.qb-btn{background:transparent;border:none;cursor:pointer;font-size:16px;color:#667085;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;border-radius:4px}.qb-btn:hover:not([disabled]){background:var(--sd-primary-light, #d7e3ff);color:var(--sd-primary, #005cbb)}.qb-btn[disabled]{cursor:default;color:#cbd2dc}.qb-btn-remove:hover:not([disabled]),.qb-btn-remove-rule:hover:not([disabled]){background:#fee4e2;color:#d92d20}.qb-btn-remove-rule{margin-left:auto}.qb-dropdown{position:absolute;top:100%;left:0;margin-top:4px;background:#fff;border:1px solid #e0e0e0;box-shadow:0 4px 12px #1018281f;border-radius:6px;min-width:150px;z-index:100;overflow:hidden}.qb-dropdown button{display:block;width:100%;text-align:left;background:none;border:none;padding:9px 14px;cursor:pointer;font-size:13px;color:#344054}.qb-dropdown button:hover{background:var(--sd-primary-light, #d7e3ff);color:var(--sd-primary, #005cbb)}.qb-body{padding-left:24px}.qb-treenode{position:relative;padding-left:22px;margin-bottom:12px}.qb-treenode:last-child{margin-bottom:0}.qb-treenode:before{content:\"\";position:absolute;left:0;top:-12px;bottom:0;border-left:1px dashed #d0d5dd}.qb-treenode:after{content:\"\";position:absolute;left:0;top:50%;width:18px;border-top:1px dashed #d0d5dd}.qb-treenode:last-child:before{bottom:auto;height:calc(50% + 12px)}.qb-empty{color:#98a2b3;font-style:italic;font-size:13px;padding:4px 0}.qb-rule{display:flex;align-items:center;gap:8px;background:#fafbfc;border:1px solid #e0e0e0;border-radius:6px;padding:6px 10px}.qb-rule:hover{border-color:#c2c8d0}.qb-field{min-width:170px}.qb-val{min-width:140px}.qb-value-source{min-width:132px}.qb-compare-field{min-width:170px}.qb-date-val{min-width:0;width:132px;flex:0 0 auto}::ng-deep .qb-opt{display:flex;align-items:center;gap:6px;min-width:0}::ng-deep .qb-opt .qb-opt-iconsd-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;margin:0;font-size:18px;width:18px;height:18px;line-height:1;color:#667085}::ng-deep .qb-opt .qb-opt-label{min-width:0}.qb-between{display:flex;align-items:center;gap:6px}.qb-dash{color:#98a2b3}.qb-divider{width:1px;align-self:stretch;background:#e0e0e0}.qb-treenode.is-group>.qb-group{border:1px solid #e0e0e0;border-left:2px solid var(--sd-primary, #005cbb);border-radius:6px;padding:12px;background:#fff}.qb-date-mode{min-width:160px}.qb-rel-amount{width:72px}.qb-rel-unit{min-width:110px}\n"], dependencies: [{ kind: "component", type: SdIcon, selector: "sd-icon", inputs: ["name", "fontIcon", "color", "set", "fontSet", "size", "strokeWidth", "absoluteStrokeWidth", "ariaLabel"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: SdOperator, selector: "sd-operator", inputs: ["model", "operators", "disabled", "autoId"], outputs: ["modelChange"] }, { kind: "component", type: SdSelect, selector: "sd-select", inputs: ["autoId", "name", "size", "form", "label", "helperText", "placeholder", "valueField", "displayField", "disabledField", "cacheChecksum", "limit", "hyperlink", "minWidthPanel", "hideInlineError", "required", "disabled", "viewed", "multiple", "showSelectAll", "clearable", "validator", "inlineError", "appearance", "floatLabel", "items", "model"], outputs: ["modelChange", "sdChange", "sdSelection"] }, { kind: "component", type: SdInput, selector: "sd-input", inputs: ["autoId", "name", "appearance", "floatLabel", "size", "form", "label", "helperText", "placeholder", "type", "mask", "hideInlineError", "blurOnEnter", "clearable", "required", "readonly", "disabled", "viewed", "minlength", "maxlength", "pattern", "patternErrorMessage", "validator", "inlineError", "hyperlink", "model"], outputs: ["modelChange", "sdChange", "sdFocus", "sdBlur", "keyupEnter", "cleared", "sdFocusForceBlur"] }, { kind: "component", type: SdInputNumber, selector: "sd-input-number", inputs: ["autoId", "name", "size", "form", "label", "helperText", "placeholder", "hideInlineError", "blurOnEnter", "clearable", "required", "readonly", "disabled", "viewed", "type", "precision", "format", "min", "max", "validator", "inlineError", "hyperlink", "appearance", "floatLabel", "model"], outputs: ["modelChange", "sdChange", "sdFocus", "sdBlur", "keyupEnter", "cleared", "sdFocusForceBlur"] }, { kind: "component", type: SdDate, selector: "sd-date", inputs: ["autoId", "name", "size", "form", "label", "helperText", "placeholder", "hideInlineError", "required", "disabled", "viewed", "clearable", "inlineError", "hyperlink", "appearance", "floatLabel", "min", "minDate", "max", "maxDate", "transform", "model"], outputs: ["modelChange", "sdChange", "sdFocus"] }, { kind: "component", type: SdDatetime, selector: "sd-datetime", inputs: ["autoId", "name", "size", "form", "label", "helperText", "placeholder", "hideInlineError", "required", "disabled", "viewed", "showSeconds", "clearable", "inlineError", "hyperlink", "appearance", "floatLabel", "min", "minDate", "max", "maxDate", "transform", "model"], outputs: ["modelChange", "sdChange", "sdFocus"] }, { kind: "directive", type: SdItemDefDefDirective, selector: "[sdItemDef]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
881
941
|
}
|
|
882
942
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SdQueryBuilder, decorators: [{
|
|
883
943
|
type: Component,
|
|
884
|
-
args: [{ selector: 'sd-query-builder', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [SdIcon, NgTemplateOutlet, SdOperator, SdSelect, SdInput, SdInputNumber, SdDate, SdDatetime, SdItemDefDefDirective], template: "@if (isView()) {\n <div class=\"qb-view\" [attr.data-autoid]=\"autoIdFor('view')\" aria-readonly=\"true\">\n @if (tokens().length) {\n @for (t of tokens(); track $index) {\n <span class=\"qb-tok qb-tok-{{ t.kind }}\">{{ t.text }}</span>\n }\n } @else {\n <span class=\"qb-view-empty\">\u2014</span>\n }\n </div>\n} @else {\n <div class=\"qb-container\" [class.is-disabled]=\"resolvedDisabled()\" (click)=\"closeAllDropdowns()\">\n <ng-container [ngTemplateOutlet]=\"groupTpl\" [ngTemplateOutletContext]=\"{ $implicit: tree(), isRoot: true }\"></ng-container>\n </div>\n}\n\n<!-- Recursive group node -->\n<ng-template #groupTpl let-group let-isRoot=\"isRoot\" let-parent=\"parent\" let-index=\"index\">\n <div class=\"qb-group\" [class.is-root]=\"isRoot\">\n <div class=\"qb-header\" [class.is-open]=\"group.open\">\n <div class=\"qb-logic-switch\">\n <button type=\"button\" [class.active]=\"group.logic === 'AND'\" [disabled]=\"resolvedDisabled()\" (click)=\"setGroupLogic(group, 'AND')\">AND</button>\n <button type=\"button\" [class.active]=\"group.logic === 'OR'\" [disabled]=\"resolvedDisabled()\" (click)=\"setGroupLogic(group, 'OR')\">OR</button>\n </div>\n\n <div class=\"qb-actions\">\n <button type=\"button\" class=\"qb-btn qb-btn-add\" title=\"Th\u00EAm\" aria-label=\"Th\u00EAm \u0111i\u1EC1u ki\u1EC7n ho\u1EB7c nh\u00F3m\" [disabled]=\"resolvedDisabled()\" (click)=\"toggleDropdown(group, $event)\">+</button>\n @if (group.open) {\n <div class=\"qb-dropdown\" (click)=\"$event.stopPropagation()\">\n <button type=\"button\" (click)=\"addRule(group)\">+ \u0110i\u1EC1u ki\u1EC7n</button>\n <button type=\"button\" (click)=\"addGroup(group)\">+ Nh\u00F3m</button>\n </div>\n }\n @if (!isRoot) {\n <button type=\"button\" class=\"qb-btn qb-btn-remove\" title=\"Xo\u00E1 nh\u00F3m\" aria-label=\"Xo\u00E1 nh\u00F3m\" [disabled]=\"resolvedDisabled()\" (click)=\"removeNode(parent, index)\">\u2715</button>\n }\n </div>\n </div>\n\n <div class=\"qb-body\">\n @for (node of group.children; track node.id; let i = $index) {\n <div class=\"qb-treenode\" [class.is-group]=\"isGroup(node)\" [class.is-rule]=\"!isGroup(node)\">\n @if (isGroup(node)) {\n <ng-container\n [ngTemplateOutlet]=\"groupTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: node, isRoot: false, parent: group, index: i }\"\n ></ng-container>\n } @else {\n <ng-container [ngTemplateOutlet]=\"ruleTpl\" [ngTemplateOutletContext]=\"{ $implicit: node, parent: group, index: i }\"></ng-container>\n }\n </div>\n } @empty {\n <div class=\"qb-empty\">Ch\u01B0a c\u00F3 \u0111i\u1EC1u ki\u1EC7n</div>\n }\n </div>\n </div>\n</ng-template>\n\n<!-- Single rule node: field \u2192 operator \u2192 value -->\n<ng-template #ruleTpl let-rule let-parent=\"parent\" let-index=\"index\">\n <div class=\"qb-rule\">\n <sd-select\n class=\"qb-field\"\n [items]=\"resolvedFields()\"\n valueField=\"key\"\n displayField=\"label\"\n placeholder=\"Ch\u1ECDn tr\u01B0\u1EDDng\"\n size=\"sm\"\n [model]=\"rule.field\"\n (modelChange)=\"setField(rule, $event)\"\n [disabled]=\"resolvedDisabled()\"\n [clearable]=\"false\"\n hideInlineError\n [autoId]=\"autoIdFor('field-' + index)\"\n >\n <!-- Option trong dropdown: icon th\u1EC3 hi\u1EC7n type + label. -->\n <ng-template sdItemDef let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"fieldIcon($any(item))\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item).label }}</span>\n </span>\n </ng-template>\n <!-- Gi\u00E1 tr\u1ECB \u0111\u00E3 ch\u1ECDn \u1EDF trigger: icon m\u1EB7c \u0111\u1ECBnh theo type khi field ch\u01B0a c\u00F3 icon ri\u00EAng. -->\n <ng-template #sdSelected let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"fieldIcon($any(item))\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item)?.label }}</span>\n </span>\n </ng-template>\n </sd-select>\n\n @if (rule.field) {\n <div class=\"qb-divider\"></div>\n\n @if (showOperatorSelector(rule)) {\n <sd-operator\n class=\"qb-op\"\n [operators]=\"allowedOperators(rule)\"\n [model]=\"rule.operator\"\n (modelChange)=\"setOperator(rule, $event)\"\n [disabled]=\"resolvedDisabled()\"\n [autoId]=\"autoIdFor('op-' + index)\"\n ></sd-operator>\n }\n\n @if (rule.operator && !isNoData(rule.operator)) {\n <div class=\"qb-divider\"></div>\n\n @if (showValueSourceSelector(rule)) {\n <sd-select\n class=\"qb-value-source\"\n [items]=\"valueSourceOptions\"\n valueField=\"value\"\n displayField=\"display\"\n size=\"sm\"\n [model]=\"valueSourceOf(rule)\"\n (modelChange)=\"setValueSource(rule, $event)\"\n [disabled]=\"resolvedDisabled()\"\n [clearable]=\"false\"\n hideInlineError\n [autoId]=\"autoIdFor('valuesource-' + index)\"\n >\n <ng-template sdItemDef let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item).icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item).display }}</span>\n </span>\n </ng-template>\n <ng-template #sdSelected let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item)?.icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item)?.display }}</span>\n </span>\n </ng-template>\n </sd-select>\n <div class=\"qb-divider\"></div>\n }\n\n @if (isFieldSource(rule)) {\n <sd-select\n class=\"qb-val qb-compare-field\"\n [items]=\"compareFields(rule)\"\n valueField=\"key\"\n displayField=\"label\"\n placeholder=\"Ch\u1ECDn tr\u01B0\u1EDDng\"\n size=\"sm\"\n [model]=\"rule.compareField\"\n (modelChange)=\"setCompareField(rule, $event)\"\n [disabled]=\"resolvedDisabled()\"\n [clearable]=\"false\"\n hideInlineError\n [autoId]=\"autoIdFor('compare-field-' + index)\"\n >\n <ng-template sdItemDef let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"fieldIcon($any(item))\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item).label }}</span>\n </span>\n </ng-template>\n <ng-template #sdSelected let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"fieldIcon($any(item))\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item)?.label }}</span>\n </span>\n </ng-template>\n </sd-select>\n } @else if (rule.operator === 'BETWEEN') {\n <div class=\"qb-between\">\n @switch (fieldOf(rule)?.type) {\n @case ('number') {\n <sd-input-number class=\"qb-val\" placeholder=\"T\u1EEB\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [min]=\"fieldOf(rule)?.min\" [max]=\"fieldOf(rule)?.max\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-input-number>\n <span class=\"qb-dash\">\u2014</span>\n <sd-input-number class=\"qb-val\" placeholder=\"\u0110\u1EBFn\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [min]=\"fieldOf(rule)?.min\" [max]=\"fieldOf(rule)?.max\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('to-' + index)\"></sd-input-number>\n }\n @case ('date') {\n <sd-date class=\"qb-val qb-date-val\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-date>\n <span class=\"qb-dash\">\u2014</span>\n <sd-date class=\"qb-val qb-date-val\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('to-' + index)\"></sd-date>\n }\n @case ('datetime') {\n <sd-datetime class=\"qb-val\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-datetime>\n <span class=\"qb-dash\">\u2014</span>\n <sd-datetime class=\"qb-val\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('to-' + index)\"></sd-datetime>\n }\n }\n </div>\n } @else {\n @switch (fieldOf(rule)?.type) {\n @case ('string') {\n <sd-input class=\"qb-val\" placeholder=\"Gi\u00E1 tr\u1ECB\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-input>\n }\n @case ('number') {\n <sd-input-number class=\"qb-val\" placeholder=\"Gi\u00E1 tr\u1ECB\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [min]=\"fieldOf(rule)?.min\" [max]=\"fieldOf(rule)?.max\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-input-number>\n }\n @case ('boolean') {\n <sd-select class=\"qb-val\" [items]=\"booleanItems(fieldOf(rule)!)\" valueField=\"value\" displayField=\"display\" placeholder=\"Gi\u00E1 tr\u1ECB\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-select>\n }\n @case ('date') {\n @let _dateMode = dateMode(rule);\n <sd-select class=\"qb-date-mode\" [items]=\"dateModes\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"_dateMode\" (modelChange)=\"setDateMode(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('datemode-' + index)\">\n <ng-template sdItemDef let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item).icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item).display }}</span>\n </span>\n </ng-template>\n <ng-template #sdSelected let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item)?.icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item)?.display }}</span>\n </span>\n </ng-template>\n </sd-select>\n @switch (_dateMode) {\n @case ('absolute') {\n <sd-date class=\"qb-val qb-date-val\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-date>\n }\n @case ('relative') {\n <sd-input-number class=\"qb-rel-amount\" type=\"positive\" [precision]=\"0\" size=\"sm\" [model]=\"relativeAmount(rule)\" (modelChange)=\"setRelativeAmount(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('rel-amount-' + index)\"></sd-input-number>\n <sd-select class=\"qb-rel-unit\" [items]=\"relativeUnitOptions\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"relativeUnitDirValue(rule)\" (modelChange)=\"setRelativeUnitDir(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('rel-unit-' + index)\"></sd-select>\n }\n }\n }\n @case ('datetime') {\n @let _dtMode = dateMode(rule);\n <sd-select class=\"qb-date-mode\" [items]=\"dateModes\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"_dtMode\" (modelChange)=\"setDateMode(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('datemode-' + index)\">\n <ng-template sdItemDef let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item).icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item).display }}</span>\n </span>\n </ng-template>\n <ng-template #sdSelected let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item)?.icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item)?.display }}</span>\n </span>\n </ng-template>\n </sd-select>\n @switch (_dtMode) {\n @case ('absolute') {\n <sd-datetime class=\"qb-val\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-datetime>\n }\n @case ('relative') {\n <sd-input-number class=\"qb-rel-amount\" type=\"positive\" [precision]=\"0\" size=\"sm\" [model]=\"relativeAmount(rule)\" (modelChange)=\"setRelativeAmount(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('rel-amount-' + index)\"></sd-input-number>\n <sd-select class=\"qb-rel-unit\" [items]=\"relativeUnitOptions\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"relativeUnitDirValue(rule)\" (modelChange)=\"setRelativeUnitDir(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('rel-unit-' + index)\"></sd-select>\n }\n }\n }\n @case ('values') {\n <sd-select class=\"qb-val\" [items]=\"fieldOf(rule)!.values || []\" valueField=\"value\" displayField=\"display\" [multiple]=\"isMulti(rule.operator)\" placeholder=\"Gi\u00E1 tr\u1ECB\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-select>\n }\n }\n }\n }\n }\n\n <button type=\"button\" class=\"qb-btn qb-btn-remove-rule\" title=\"Xo\u00E1 \u0111i\u1EC1u ki\u1EC7n\" aria-label=\"Xo\u00E1 \u0111i\u1EC1u ki\u1EC7n\" [disabled]=\"resolvedDisabled()\" (click)=\"removeNode(parent, index)\">\u2715</button>\n </div>\n</ng-template>\n", styles: [":host{display:block;font-size:14px;color:var(--sd-text, #333)}.qb-view{display:block;width:100%;min-height:38px;padding:8px 12px;border:1px solid #d0d5dd;border-radius:6px;background:var(--sd-disabled-bg, #f6f7f9);color:var(--sd-text, #333);font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:13px;line-height:1.7;cursor:default;-webkit-user-select:text;user-select:text;white-space:pre-wrap;word-break:break-word}.qb-view-empty{color:#98a2b3}.qb-tok{white-space:pre}.qb-tok.qb-tok-field{color:var(--sd-text, #1d2939);font-weight:600}.qb-tok.qb-tok-op{color:var(--sd-primary, #005cbb);font-weight:600}.qb-tok.qb-tok-value{color:var(--sd-success, #1a7f37)}.qb-tok.qb-tok-logic{color:#98a2b3;font-weight:700;text-transform:uppercase}.qb-tok.qb-tok-paren{color:#667085}.qb-container{padding:16px;background:#fff;border:1px solid #e0e0e0;border-radius:6px}.qb-container.is-disabled{opacity:.7;pointer-events:none}.qb-header{display:flex;align-items:center;gap:8px;margin-bottom:12px;position:relative;z-index:1}.qb-header.is-open{z-index:1000}.qb-logic-switch{display:inline-flex;border:1px solid #e0e0e0;border-radius:4px;overflow:hidden;height:30px}.qb-logic-switch button{border:none;background:transparent;padding:0 14px;font-weight:600;font-size:12px;cursor:pointer;color:#667085;height:100%}.qb-logic-switch button.active{background:var(--sd-primary, #005cbb);color:#fff}.qb-logic-switch button:hover:not(.active):not([disabled]){background:var(--sd-primary-light, #d7e3ff);color:var(--sd-primary-dark, #1c4ad9)}.qb-logic-switch button[disabled]{cursor:default}.qb-actions{position:relative;display:flex;align-items:center;gap:4px}.qb-btn{background:transparent;border:none;cursor:pointer;font-size:16px;color:#667085;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;border-radius:4px}.qb-btn:hover:not([disabled]){background:var(--sd-primary-light, #d7e3ff);color:var(--sd-primary, #005cbb)}.qb-btn[disabled]{cursor:default;color:#cbd2dc}.qb-btn-remove:hover:not([disabled]),.qb-btn-remove-rule:hover:not([disabled]){background:#fee4e2;color:#d92d20}.qb-btn-remove-rule{margin-left:auto}.qb-dropdown{position:absolute;top:100%;left:0;margin-top:4px;background:#fff;border:1px solid #e0e0e0;box-shadow:0 4px 12px #1018281f;border-radius:6px;min-width:150px;z-index:100;overflow:hidden}.qb-dropdown button{display:block;width:100%;text-align:left;background:none;border:none;padding:9px 14px;cursor:pointer;font-size:13px;color:#344054}.qb-dropdown button:hover{background:var(--sd-primary-light, #d7e3ff);color:var(--sd-primary, #005cbb)}.qb-body{padding-left:24px}.qb-treenode{position:relative;padding-left:22px;margin-bottom:12px}.qb-treenode:last-child{margin-bottom:0}.qb-treenode:before{content:\"\";position:absolute;left:0;top:-12px;bottom:0;border-left:1px dashed #d0d5dd}.qb-treenode:after{content:\"\";position:absolute;left:0;top:50%;width:18px;border-top:1px dashed #d0d5dd}.qb-treenode:last-child:before{bottom:auto;height:calc(50% + 12px)}.qb-empty{color:#98a2b3;font-style:italic;font-size:13px;padding:4px 0}.qb-rule{display:flex;align-items:center;gap:8px;background:#fafbfc;border:1px solid #e0e0e0;border-radius:6px;padding:6px 10px}.qb-rule:hover{border-color:#c2c8d0}.qb-field{min-width:170px}.qb-val{min-width:140px}.qb-value-source{min-width:132px}.qb-compare-field{min-width:170px}.qb-date-val{min-width:0;width:132px;flex:0 0 auto}::ng-deep .qb-opt{display:flex;align-items:center;gap:6px;min-width:0}::ng-deep .qb-opt .qb-opt-iconsd-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;margin:0;font-size:18px;width:18px;height:18px;line-height:1;color:#667085}::ng-deep .qb-opt .qb-opt-label{min-width:0}.qb-between{display:flex;align-items:center;gap:6px}.qb-dash{color:#98a2b3}.qb-divider{width:1px;align-self:stretch;background:#e0e0e0}.qb-treenode.is-group>.qb-group{border:1px solid #e0e0e0;border-left:2px solid var(--sd-primary, #005cbb);border-radius:6px;padding:12px;background:#fff}.qb-date-mode{min-width:160px}.qb-rel-amount{width:72px}.qb-rel-unit{min-width:110px}\n"] }]
|
|
944
|
+
args: [{ selector: 'sd-query-builder', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [SdIcon, NgTemplateOutlet, SdOperator, SdSelect, SdInput, SdInputNumber, SdDate, SdDatetime, SdItemDefDefDirective], template: "@if (isView()) {\n <div class=\"qb-view\" [attr.data-autoid]=\"autoIdFor('view')\" aria-readonly=\"true\">\n @if (tokens().length) {\n @for (t of tokens(); track $index) {\n <span class=\"qb-tok qb-tok-{{ t.kind }}\">{{ t.text }}</span>\n }\n } @else {\n <span class=\"qb-view-empty\">\u2014</span>\n }\n </div>\n} @else {\n <!-- why: \"click ra v\u00F9ng tr\u1ED1ng \u0111\u1EC3 \u0111\u00F3ng dropdown\" l\u00E0 h\u00E0nh vi CH\u1EC8 c\u00F3 \u1EDF chu\u1ED9t \u2014 ng\u01B0\u1EDDi d\u00F9ng b\u00E0n ph\u00EDm\n kh\u00F4ng c\u00F3 c\u00E1ch n\u00E0o \u0111\u00F3ng. Th\u00EAm Escape l\u00E0m t\u01B0\u01A1ng \u0111\u01B0\u01A1ng. role=\"group\": container l\u00E0 m\u1ED9t nh\u00F3m\n control n\u00EAn KH\u00D4NG nh\u1EADn tabindex, tab \u0111i th\u1EB3ng v\u00E0o c\u00E1c n\u00FAt/select b\u00EAn trong. -->\n <div\n class=\"qb-container\"\n role=\"group\"\n [class.is-disabled]=\"resolvedDisabled()\"\n (click)=\"closeAllDropdowns()\"\n (keydown.escape)=\"closeAllDropdowns()\">\n <ng-container [ngTemplateOutlet]=\"groupTpl\" [ngTemplateOutletContext]=\"{ $implicit: tree(), isRoot: true }\"></ng-container>\n </div>\n}\n\n<!-- Recursive group node -->\n<ng-template #groupTpl let-group let-isRoot=\"isRoot\" let-parent=\"parent\" let-index=\"index\">\n @let _removeGroupLabel = removeGroupLabel();\n <div class=\"qb-group\" [class.is-root]=\"isRoot\">\n <div class=\"qb-header\" [class.is-open]=\"group.open\">\n <div class=\"qb-logic-switch\">\n <button type=\"button\" [class.active]=\"group.logic === 'AND'\" [disabled]=\"resolvedDisabled()\" (click)=\"setGroupLogic(group, 'AND')\">AND</button>\n <button type=\"button\" [class.active]=\"group.logic === 'OR'\" [disabled]=\"resolvedDisabled()\" (click)=\"setGroupLogic(group, 'OR')\">OR</button>\n </div>\n\n <div class=\"qb-actions\">\n <button type=\"button\" class=\"qb-btn qb-btn-add\" [title]=\"addLabel()\" [attr.aria-label]=\"addNodeLabel()\" [disabled]=\"resolvedDisabled()\" (click)=\"toggleDropdown(group, $event)\">+</button>\n @if (group.open) {\n <!-- why: dropdown l\u00E0 NH\u00D3M hai n\u00FAt (role=\"group\"), kh\u00F4ng ph\u1EA3i n\u00FAt \u2014 kh\u00F4ng nh\u1EADn tabindex,\n tab \u0111i th\u1EB3ng v\u00E0o hai n\u00FAt. stopPropagation cho c\u1EA3 click l\u1EABn Enter \u0111\u1EC3 thao t\u00E1c b\u00EAn\n trong dropdown kh\u00F4ng k\u00EDch ho\u1EA1t handler \"\u0111\u00F3ng t\u1EA5t c\u1EA3\" c\u1EE7a container. -->\n <div class=\"qb-dropdown\" role=\"group\" (click)=\"$event.stopPropagation()\" (keydown.enter)=\"$event.stopPropagation()\">\n <button type=\"button\" (click)=\"addRule(group)\">+ {{ conditionLabel() }}</button>\n <button type=\"button\" (click)=\"addGroup(group)\">+ {{ groupLabel() }}</button>\n </div>\n }\n @if (!isRoot) {\n <button type=\"button\" class=\"qb-btn qb-btn-remove\" [title]=\"_removeGroupLabel\" [attr.aria-label]=\"_removeGroupLabel\" [disabled]=\"resolvedDisabled()\" (click)=\"removeNode(parent, index)\">\u2715</button>\n }\n </div>\n </div>\n\n <div class=\"qb-body\">\n @for (node of group.children; track node.id; let i = $index) {\n <div class=\"qb-treenode\" [class.is-group]=\"isGroup(node)\" [class.is-rule]=\"!isGroup(node)\">\n @if (isGroup(node)) {\n <ng-container\n [ngTemplateOutlet]=\"groupTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: node, isRoot: false, parent: group, index: i }\"\n ></ng-container>\n } @else {\n <ng-container [ngTemplateOutlet]=\"ruleTpl\" [ngTemplateOutletContext]=\"{ $implicit: node, parent: group, index: i }\"></ng-container>\n }\n </div>\n } @empty {\n <div class=\"qb-empty\">{{ emptyLabel() }}</div>\n }\n </div>\n </div>\n</ng-template>\n\n<!-- Single rule node: field \u2192 operator \u2192 value -->\n<ng-template #ruleTpl let-rule let-parent=\"parent\" let-index=\"index\">\n @let _selectFieldLabel = selectFieldLabel();\n @let _valueLabel = valueLabel();\n @let _removeConditionLabel = removeConditionLabel();\n @let _dateModes = dateModes();\n @let _relativeUnitOptions = relativeUnitOptions();\n <div class=\"qb-rule\">\n <sd-select\n class=\"qb-field\"\n [items]=\"resolvedFields()\"\n valueField=\"key\"\n displayField=\"label\"\n [placeholder]=\"_selectFieldLabel\"\n size=\"sm\"\n [model]=\"rule.field\"\n (modelChange)=\"setField(rule, $event)\"\n [disabled]=\"resolvedDisabled()\"\n [clearable]=\"false\"\n hideInlineError\n [autoId]=\"autoIdFor('field-' + index)\"\n >\n <!-- Option trong dropdown: icon th\u1EC3 hi\u1EC7n type + label. -->\n <ng-template sdItemDef let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"fieldIcon($any(item))\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item).label }}</span>\n </span>\n </ng-template>\n <!-- Gi\u00E1 tr\u1ECB \u0111\u00E3 ch\u1ECDn \u1EDF trigger: icon m\u1EB7c \u0111\u1ECBnh theo type khi field ch\u01B0a c\u00F3 icon ri\u00EAng. -->\n <ng-template #sdSelected let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"fieldIcon($any(item))\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item)?.label }}</span>\n </span>\n </ng-template>\n </sd-select>\n\n @if (rule.field) {\n <div class=\"qb-divider\"></div>\n\n @if (showOperatorSelector(rule)) {\n <sd-operator\n class=\"qb-op\"\n [operators]=\"allowedOperators(rule)\"\n [model]=\"rule.operator\"\n (modelChange)=\"setOperator(rule, $event)\"\n [disabled]=\"resolvedDisabled()\"\n [autoId]=\"autoIdFor('op-' + index)\"\n ></sd-operator>\n }\n\n @if (rule.operator && !isNoData(rule.operator)) {\n <div class=\"qb-divider\"></div>\n\n @if (showValueSourceSelector(rule)) {\n <sd-select\n class=\"qb-value-source\"\n [items]=\"valueSourceOptions()\"\n valueField=\"value\"\n displayField=\"display\"\n size=\"sm\"\n [model]=\"valueSourceOf(rule)\"\n (modelChange)=\"setValueSource(rule, $event)\"\n [disabled]=\"resolvedDisabled()\"\n [clearable]=\"false\"\n hideInlineError\n [autoId]=\"autoIdFor('valuesource-' + index)\"\n >\n <ng-template sdItemDef let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item).icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item).display }}</span>\n </span>\n </ng-template>\n <ng-template #sdSelected let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item)?.icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item)?.display }}</span>\n </span>\n </ng-template>\n </sd-select>\n <div class=\"qb-divider\"></div>\n }\n\n @if (isFieldSource(rule)) {\n <sd-select\n class=\"qb-val qb-compare-field\"\n [items]=\"compareFields(rule)\"\n valueField=\"key\"\n displayField=\"label\"\n [placeholder]=\"_selectFieldLabel\"\n size=\"sm\"\n [model]=\"rule.compareField\"\n (modelChange)=\"setCompareField(rule, $event)\"\n [disabled]=\"resolvedDisabled()\"\n [clearable]=\"false\"\n hideInlineError\n [autoId]=\"autoIdFor('compare-field-' + index)\"\n >\n <ng-template sdItemDef let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"fieldIcon($any(item))\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item).label }}</span>\n </span>\n </ng-template>\n <ng-template #sdSelected let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"fieldIcon($any(item))\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item)?.label }}</span>\n </span>\n </ng-template>\n </sd-select>\n } @else if (rule.operator === 'BETWEEN') {\n <div class=\"qb-between\">\n @switch (fieldOf(rule)?.type) {\n @case ('number') {\n <sd-input-number class=\"qb-val\" [placeholder]=\"fromLabel()\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [min]=\"fieldOf(rule)?.min\" [max]=\"fieldOf(rule)?.max\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-input-number>\n <span class=\"qb-dash\">\u2014</span>\n <sd-input-number class=\"qb-val\" [placeholder]=\"toLabel()\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [min]=\"fieldOf(rule)?.min\" [max]=\"fieldOf(rule)?.max\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('to-' + index)\"></sd-input-number>\n }\n @case ('date') {\n <sd-date class=\"qb-val qb-date-val\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-date>\n <span class=\"qb-dash\">\u2014</span>\n <sd-date class=\"qb-val qb-date-val\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('to-' + index)\"></sd-date>\n }\n @case ('datetime') {\n <sd-datetime class=\"qb-val\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-datetime>\n <span class=\"qb-dash\">\u2014</span>\n <sd-datetime class=\"qb-val\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('to-' + index)\"></sd-datetime>\n }\n }\n </div>\n } @else {\n @switch (fieldOf(rule)?.type) {\n @case ('string') {\n <sd-input class=\"qb-val\" [placeholder]=\"_valueLabel\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-input>\n }\n @case ('number') {\n <sd-input-number class=\"qb-val\" [placeholder]=\"_valueLabel\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [min]=\"fieldOf(rule)?.min\" [max]=\"fieldOf(rule)?.max\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-input-number>\n }\n @case ('boolean') {\n <sd-select class=\"qb-val\" [items]=\"booleanItems(fieldOf(rule)!)\" valueField=\"value\" displayField=\"display\" [placeholder]=\"_valueLabel\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-select>\n }\n @case ('date') {\n @let _dateMode = dateMode(rule);\n <sd-select class=\"qb-date-mode\" [items]=\"_dateModes\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"_dateMode\" (modelChange)=\"setDateMode(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('datemode-' + index)\">\n <ng-template sdItemDef let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item).icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item).display }}</span>\n </span>\n </ng-template>\n <ng-template #sdSelected let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item)?.icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item)?.display }}</span>\n </span>\n </ng-template>\n </sd-select>\n @switch (_dateMode) {\n @case ('absolute') {\n <sd-date class=\"qb-val qb-date-val\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-date>\n }\n @case ('relative') {\n <sd-input-number class=\"qb-rel-amount\" type=\"positive\" [precision]=\"0\" size=\"sm\" [model]=\"relativeAmount(rule)\" (modelChange)=\"setRelativeAmount(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('rel-amount-' + index)\"></sd-input-number>\n <sd-select class=\"qb-rel-unit\" [items]=\"_relativeUnitOptions\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"relativeUnitDirValue(rule)\" (modelChange)=\"setRelativeUnitDir(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('rel-unit-' + index)\"></sd-select>\n }\n }\n }\n @case ('datetime') {\n @let _dtMode = dateMode(rule);\n <sd-select class=\"qb-date-mode\" [items]=\"_dateModes\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"_dtMode\" (modelChange)=\"setDateMode(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('datemode-' + index)\">\n <ng-template sdItemDef let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item).icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item).display }}</span>\n </span>\n </ng-template>\n <ng-template #sdSelected let-item=\"item\">\n <span class=\"qb-opt\">\n <sd-icon class=\"qb-opt-icon\" [name]=\"$any(item)?.icon\"></sd-icon>\n <span class=\"qb-opt-label\">{{ $any(item)?.display }}</span>\n </span>\n </ng-template>\n </sd-select>\n @switch (_dtMode) {\n @case ('absolute') {\n <sd-datetime class=\"qb-val\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-datetime>\n }\n @case ('relative') {\n <sd-input-number class=\"qb-rel-amount\" type=\"positive\" [precision]=\"0\" size=\"sm\" [model]=\"relativeAmount(rule)\" (modelChange)=\"setRelativeAmount(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('rel-amount-' + index)\"></sd-input-number>\n <sd-select class=\"qb-rel-unit\" [items]=\"_relativeUnitOptions\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"relativeUnitDirValue(rule)\" (modelChange)=\"setRelativeUnitDir(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('rel-unit-' + index)\"></sd-select>\n }\n }\n }\n @case ('values') {\n <sd-select class=\"qb-val\" [items]=\"valueItems(fieldOf(rule)!)\" valueField=\"value\" displayField=\"display\" [multiple]=\"isMulti(rule.operator)\" [placeholder]=\"_valueLabel\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-select>\n }\n }\n }\n }\n }\n\n <button type=\"button\" class=\"qb-btn qb-btn-remove-rule\" [title]=\"_removeConditionLabel\" [attr.aria-label]=\"_removeConditionLabel\" [disabled]=\"resolvedDisabled()\" (click)=\"removeNode(parent, index)\">\u2715</button>\n </div>\n</ng-template>\n", styles: [":host{display:block;font-size:14px;color:var(--sd-text, #333)}.qb-view{display:block;width:100%;min-height:38px;padding:8px 12px;border:1px solid #d0d5dd;border-radius:6px;background:var(--sd-disabled-bg, #f6f7f9);color:var(--sd-text, #333);font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:13px;line-height:1.7;cursor:default;-webkit-user-select:text;user-select:text;white-space:pre-wrap;word-break:break-word}.qb-view-empty{color:#98a2b3}.qb-tok{white-space:pre}.qb-tok.qb-tok-field{color:var(--sd-text, #1d2939);font-weight:600}.qb-tok.qb-tok-op{color:var(--sd-primary, #005cbb);font-weight:600}.qb-tok.qb-tok-value{color:var(--sd-success, #1a7f37)}.qb-tok.qb-tok-logic{color:#98a2b3;font-weight:700;text-transform:uppercase}.qb-tok.qb-tok-paren{color:#667085}.qb-container{padding:16px;background:#fff;border:1px solid #e0e0e0;border-radius:6px}.qb-container.is-disabled{opacity:.7;pointer-events:none}.qb-header{display:flex;align-items:center;gap:8px;margin-bottom:12px;position:relative;z-index:1}.qb-header.is-open{z-index:1000}.qb-logic-switch{display:inline-flex;border:1px solid #e0e0e0;border-radius:4px;overflow:hidden;height:30px}.qb-logic-switch button{border:none;background:transparent;padding:0 14px;font-weight:600;font-size:12px;cursor:pointer;color:#667085;height:100%}.qb-logic-switch button.active{background:var(--sd-primary, #005cbb);color:#fff}.qb-logic-switch button:hover:not(.active):not([disabled]){background:var(--sd-primary-light, #d7e3ff);color:var(--sd-primary-dark, #1c4ad9)}.qb-logic-switch button[disabled]{cursor:default}.qb-actions{position:relative;display:flex;align-items:center;gap:4px}.qb-btn{background:transparent;border:none;cursor:pointer;font-size:16px;color:#667085;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;border-radius:4px}.qb-btn:hover:not([disabled]){background:var(--sd-primary-light, #d7e3ff);color:var(--sd-primary, #005cbb)}.qb-btn[disabled]{cursor:default;color:#cbd2dc}.qb-btn-remove:hover:not([disabled]),.qb-btn-remove-rule:hover:not([disabled]){background:#fee4e2;color:#d92d20}.qb-btn-remove-rule{margin-left:auto}.qb-dropdown{position:absolute;top:100%;left:0;margin-top:4px;background:#fff;border:1px solid #e0e0e0;box-shadow:0 4px 12px #1018281f;border-radius:6px;min-width:150px;z-index:100;overflow:hidden}.qb-dropdown button{display:block;width:100%;text-align:left;background:none;border:none;padding:9px 14px;cursor:pointer;font-size:13px;color:#344054}.qb-dropdown button:hover{background:var(--sd-primary-light, #d7e3ff);color:var(--sd-primary, #005cbb)}.qb-body{padding-left:24px}.qb-treenode{position:relative;padding-left:22px;margin-bottom:12px}.qb-treenode:last-child{margin-bottom:0}.qb-treenode:before{content:\"\";position:absolute;left:0;top:-12px;bottom:0;border-left:1px dashed #d0d5dd}.qb-treenode:after{content:\"\";position:absolute;left:0;top:50%;width:18px;border-top:1px dashed #d0d5dd}.qb-treenode:last-child:before{bottom:auto;height:calc(50% + 12px)}.qb-empty{color:#98a2b3;font-style:italic;font-size:13px;padding:4px 0}.qb-rule{display:flex;align-items:center;gap:8px;background:#fafbfc;border:1px solid #e0e0e0;border-radius:6px;padding:6px 10px}.qb-rule:hover{border-color:#c2c8d0}.qb-field{min-width:170px}.qb-val{min-width:140px}.qb-value-source{min-width:132px}.qb-compare-field{min-width:170px}.qb-date-val{min-width:0;width:132px;flex:0 0 auto}::ng-deep .qb-opt{display:flex;align-items:center;gap:6px;min-width:0}::ng-deep .qb-opt .qb-opt-iconsd-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;margin:0;font-size:18px;width:18px;height:18px;line-height:1;color:#667085}::ng-deep .qb-opt .qb-opt-label{min-width:0}.qb-between{display:flex;align-items:center;gap:6px}.qb-dash{color:#98a2b3}.qb-divider{width:1px;align-self:stretch;background:#e0e0e0}.qb-treenode.is-group>.qb-group{border:1px solid #e0e0e0;border-left:2px solid var(--sd-primary, #005cbb);border-radius:6px;padding:12px;background:#fff}.qb-date-mode{min-width:160px}.qb-rel-amount{width:72px}.qb-rel-unit{min-width:110px}\n"] }]
|
|
885
945
|
}], ctorParameters: () => [], propDecorators: { option: [{ type: i0.Input, args: [{ isSignal: true, alias: "option", required: false }] }], fields: [{ type: i0.Input, args: [{ isSignal: true, alias: "fields", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], comparisonMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "comparisonMode", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], autoId: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoId", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], filters: [{ type: i0.Input, args: [{ isSignal: true, alias: "filters", required: false }] }, { type: i0.Output, args: ["filtersChange"] }], rootLogic: [{ type: i0.Input, args: [{ isSignal: true, alias: "rootLogic", required: false }] }, { type: i0.Output, args: ["rootLogicChange"] }] } });
|
|
886
946
|
|
|
887
947
|
/**
|
|
888
948
|
* Generated bundle index. Do not edit.
|
|
889
949
|
*/
|
|
890
950
|
|
|
891
|
-
export {
|
|
951
|
+
export { SD_QB_DATE_MODES, SD_QB_DEFAULT_OPERATOR_BY_TYPE, SD_QB_EMPTY_OPTIONS, SD_QB_MULTI_OPERATORS, SD_QB_NO_DATA_OPERATORS, SD_QB_OPERATORS_BY_TYPE, SD_QB_RELATIVE_UNIT_OPTIONS, SD_QB_TODAY, SD_QB_TYPE_ICON, SD_QB_VALUE_SOURCE_OPTIONS, SdQueryBuilder, filterToTokens, filterToTree, qbRelativeLabelKey, sdIsQbGroup, sdQbAllowedOperators, sdQbDefaultOperator, sdQbDefaultRelative, sdQbFieldIcon, sdQbId, sdQbIsMultiOperator, sdQbIsNoDataOperator, sdQbIsRelativeDate, sdQbIsToday, sdQbNewGroup, sdQbNewRule, sdQbSupportsFieldCompareOperator, treeToFilter };
|
|
892
952
|
//# sourceMappingURL=sdcorejs-angular-components-query-builder.mjs.map
|