@sdcorejs/angular 19.0.1 → 19.0.4
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 +22 -4
- package/assets/scss/core/_inline-edit.scss +105 -0
- package/assets/scss/core/form.scss +15 -0
- package/components/autoid-inspector/src/models/autoid-element.model.d.ts +11 -0
- package/components/autoid-inspector/src/models/autoid-export-format.model.d.ts +19 -0
- package/components/autoid-inspector/src/services/autoid-export.service.d.ts +7 -1
- package/components/autoid-inspector/src/services/autoid-scanner.service.d.ts +15 -2
- package/components/button/src/button.component.d.ts +1 -2
- package/components/form-generic/src/components/form-builder/form-builder.component.d.ts +13 -15
- package/components/form-generic/src/components/form-render/form-render.component.d.ts +1 -2
- package/components/form-generic/src/components/sd-feel-expression/sd-feel-expression.component.d.ts +1 -2
- package/components/index.d.ts +1 -0
- package/components/modal-resizable/index.d.ts +1 -0
- package/components/modal-resizable/src/modal-resizable.component.d.ts +29 -0
- package/components/query-bar/src/components/inline-chip/inline-chip.component.d.ts +11 -22
- package/components/query-bar/src/components/inline-value-chip/inline-value-chip.component.d.ts +3 -7
- package/components/query-builder/index.d.ts +2 -0
- package/components/query-builder/src/query-builder.component.d.ts +98 -22
- package/components/query-builder/src/query-builder.model.d.ts +118 -0
- package/components/query-builder/src/query-builder.serializer.d.ts +32 -0
- package/components/section/src/section.component.d.ts +1 -2
- package/components/side-drawer/src/side-drawer.component.d.ts +1 -2
- package/components/stepper/src/stepper.component.d.ts +1 -2
- package/components/tab/src/tab-group.component.d.ts +1 -2
- package/components/table/src/components/config/config.component.d.ts +3 -1
- package/components/table/src/components/selector-action/selector-action.component.d.ts +3 -1
- package/components/table/src/models/table-option-tree.model.d.ts +96 -8
- package/components/table/src/services/tree/tree.util.d.ts +43 -2
- package/components/table/src/table.component.d.ts +1 -2
- package/components/upload-file/src/upload-file.component.d.ts +2 -0
- package/components/view/src/view.component.d.ts +33 -0
- package/fesm2022/sdcorejs-angular-components-autoid-inspector.mjs +97 -10
- package/fesm2022/sdcorejs-angular-components-autoid-inspector.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-avatar.mjs +2 -2
- package/fesm2022/sdcorejs-angular-components-avatar.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-button.mjs +3 -5
- package/fesm2022/sdcorejs-angular-components-button.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-form-generic.mjs +106 -197
- package/fesm2022/sdcorejs-angular-components-form-generic.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-modal-resizable.mjs +158 -0
- package/fesm2022/sdcorejs-angular-components-modal-resizable.mjs.map +1 -0
- package/fesm2022/sdcorejs-angular-components-operator.mjs +3 -1
- package/fesm2022/sdcorejs-angular-components-operator.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-preview.mjs +4 -4
- package/fesm2022/sdcorejs-angular-components-preview.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-query-bar.mjs +41 -87
- package/fesm2022/sdcorejs-angular-components-query-bar.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-query-builder.mjs +669 -57
- package/fesm2022/sdcorejs-angular-components-query-builder.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-section.mjs +2 -4
- package/fesm2022/sdcorejs-angular-components-section.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-side-drawer.mjs +3 -5
- package/fesm2022/sdcorejs-angular-components-side-drawer.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-stepper.mjs +2 -4
- package/fesm2022/sdcorejs-angular-components-stepper.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-tab.mjs +2 -4
- package/fesm2022/sdcorejs-angular-components-tab.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-table.mjs +235 -110
- package/fesm2022/sdcorejs-angular-components-table.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-upload-file.mjs +15 -4
- package/fesm2022/sdcorejs-angular-components-upload-file.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-view.mjs +43 -11
- package/fesm2022/sdcorejs-angular-components-view.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components.mjs +1 -0
- package/fesm2022/sdcorejs-angular-components.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-autocomplete.mjs +32 -7
- package/fesm2022/sdcorejs-angular-forms-autocomplete.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-checkbox.mjs +9 -4
- package/fesm2022/sdcorejs-angular-forms-checkbox.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-chip-calendar.mjs +10 -5
- package/fesm2022/sdcorejs-angular-forms-chip-calendar.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-chip.mjs +10 -5
- package/fesm2022/sdcorejs-angular-forms-chip.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-date-range.mjs +18 -9
- package/fesm2022/sdcorejs-angular-forms-date-range.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-date.mjs +20 -8
- package/fesm2022/sdcorejs-angular-forms-date.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-datetime.mjs +20 -8
- package/fesm2022/sdcorejs-angular-forms-datetime.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-inline-text.mjs +167 -0
- package/fesm2022/sdcorejs-angular-forms-inline-text.mjs.map +1 -0
- package/fesm2022/sdcorejs-angular-forms-input-color.mjs +5 -3
- package/fesm2022/sdcorejs-angular-forms-input-color.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-input-number.mjs +41 -8
- package/fesm2022/sdcorejs-angular-forms-input-number.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-input.mjs +35 -9
- package/fesm2022/sdcorejs-angular-forms-input.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-models.mjs +32 -2
- package/fesm2022/sdcorejs-angular-forms-models.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-radio.mjs +9 -4
- package/fesm2022/sdcorejs-angular-forms-radio.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-select.mjs +33 -9
- package/fesm2022/sdcorejs-angular-forms-select.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-switch.mjs +9 -4
- package/fesm2022/sdcorejs-angular-forms-switch.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-textarea.mjs +15 -5
- package/fesm2022/sdcorejs-angular-forms-textarea.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-i18n.mjs +20 -0
- package/fesm2022/sdcorejs-angular-i18n.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-confirm.mjs +1 -1
- package/fesm2022/sdcorejs-angular-services-confirm.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-notify.mjs +40 -9
- package/fesm2022/sdcorejs-angular-services-notify.mjs.map +1 -1
- package/forms/autocomplete/src/autocomplete.component.d.ts +16 -3
- package/forms/checkbox/src/checkbox.component.d.ts +5 -1
- package/forms/chip/src/chip.component.d.ts +5 -2
- package/forms/chip-calendar/src/chip-calendar.component.d.ts +5 -2
- package/forms/date/src/date.component.d.ts +14 -5
- package/forms/date-range/src/date-range.component.d.ts +12 -5
- package/forms/datetime/src/datetime.component.d.ts +14 -5
- package/forms/inline-text/index.d.ts +1 -0
- package/forms/inline-text/src/inline-text.component.d.ts +76 -0
- package/forms/input/src/input.component.d.ts +14 -2
- package/forms/input-color/src/input-color.component.d.ts +3 -1
- package/forms/input-number/src/input-number.component.d.ts +14 -2
- package/forms/models/index.d.ts +1 -0
- package/forms/models/src/sd-viewed.d.ts +38 -0
- package/forms/radio/src/radio.component.d.ts +5 -2
- package/forms/select/src/select.component.d.ts +23 -5
- package/forms/switch/src/switch.component.d.ts +5 -2
- package/forms/textarea/src/textarea.component.d.ts +9 -2
- package/i18n/src/en.d.ts +4 -0
- package/package.json +9 -1
- package/services/notify/src/components/toast/toast.component.d.ts +16 -1
- package/services/notify/src/notify.model.d.ts +17 -0
|
@@ -1,83 +1,695 @@
|
|
|
1
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
1
2
|
import * as i0 from '@angular/core';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
3
|
+
import { input, booleanAttribute, model, signal, computed, effect, untracked, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
4
|
+
import { SdDate } from '@sdcorejs/angular/forms/date';
|
|
5
|
+
import { SdDatetime } from '@sdcorejs/angular/forms/datetime';
|
|
6
|
+
import { SdInput } from '@sdcorejs/angular/forms/input';
|
|
7
|
+
import { SdSelect } from '@sdcorejs/angular/forms/select';
|
|
8
|
+
import { SdOperator } from '@sdcorejs/angular/components/operator';
|
|
7
9
|
|
|
10
|
+
/** Type guard — narrows a `QbNode` to a `QbGroup`. */
|
|
11
|
+
function isQbGroup(node) {
|
|
12
|
+
return node.kind === 'group';
|
|
13
|
+
}
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Operator vocabulary per field type. Mirrors query-bar's
|
|
16
|
+
// SD_QUERY_OPERATORS_BY_TYPE so the two components stay consistent, but is kept
|
|
17
|
+
// local so query-builder has no dependency on the query-bar package.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
/** Allowed operators per field type — used when `field.operators` is not set. */
|
|
20
|
+
const QB_OPERATORS_BY_TYPE = {
|
|
21
|
+
string: ['CONTAIN', 'NOT_CONTAIN', 'EQUAL', 'NOT_EQUAL', 'START_WITH', 'END_WITH', 'NULL', 'NOT_NULL'],
|
|
22
|
+
number: ['EQUAL', 'NOT_EQUAL', 'GREATER_THAN', 'GREATER_OR_EQUAL', 'LESS_THAN', 'LESS_OR_EQUAL', 'BETWEEN', 'NULL', 'NOT_NULL'],
|
|
23
|
+
boolean: ['EQUAL', 'NOT_EQUAL'],
|
|
24
|
+
date: ['EQUAL', 'NOT_EQUAL', 'GREATER_THAN', 'LESS_THAN', 'BETWEEN', 'NULL', 'NOT_NULL'],
|
|
25
|
+
datetime: ['EQUAL', 'NOT_EQUAL', 'GREATER_THAN', 'LESS_THAN', 'BETWEEN', 'NULL', 'NOT_NULL'],
|
|
26
|
+
values: ['IN', 'NOT_IN', 'EQUAL', 'NOT_EQUAL', 'NULL', 'NOT_NULL'],
|
|
27
|
+
};
|
|
28
|
+
/** Default operator when a rule is first created for a field. */
|
|
29
|
+
const QB_DEFAULT_OPERATOR_BY_TYPE = {
|
|
30
|
+
string: 'CONTAIN',
|
|
31
|
+
number: 'EQUAL',
|
|
32
|
+
boolean: 'EQUAL',
|
|
33
|
+
date: 'BETWEEN',
|
|
34
|
+
datetime: 'BETWEEN',
|
|
35
|
+
values: 'IN',
|
|
36
|
+
};
|
|
37
|
+
/** Operators with no value payload — the value editor is hidden for these. */
|
|
38
|
+
const QB_NO_DATA_OPERATORS = ['NULL', 'NOT_NULL'];
|
|
39
|
+
/** Operators whose value is an array — the value editor offers multi-select. */
|
|
40
|
+
const QB_MULTI_OPERATORS = ['IN', 'NOT_IN'];
|
|
41
|
+
/** Operators allowed for `field` — explicit override wins, else the per-type set. */
|
|
42
|
+
function qbAllowedOperators(field) {
|
|
43
|
+
if (!field)
|
|
44
|
+
return [];
|
|
45
|
+
if (field.operators?.length)
|
|
46
|
+
return field.operators;
|
|
47
|
+
return QB_OPERATORS_BY_TYPE[field.type] ?? [];
|
|
48
|
+
}
|
|
49
|
+
/** Starting operator for a new rule on `field` — falls back to the per-type default. */
|
|
50
|
+
function qbDefaultOperator(field) {
|
|
51
|
+
if (!field)
|
|
52
|
+
return undefined;
|
|
53
|
+
const allowed = qbAllowedOperators(field);
|
|
54
|
+
if (field.defaultOperator && allowed.includes(field.defaultOperator))
|
|
55
|
+
return field.defaultOperator;
|
|
56
|
+
const byType = QB_DEFAULT_OPERATOR_BY_TYPE[field.type];
|
|
57
|
+
return allowed.includes(byType) ? byType : allowed[0];
|
|
58
|
+
}
|
|
59
|
+
/** True when `op` is NULL / NOT_NULL (no value editor). */
|
|
60
|
+
function qbIsNoDataOperator(op) {
|
|
61
|
+
return !!op && QB_NO_DATA_OPERATORS.includes(op);
|
|
62
|
+
}
|
|
63
|
+
/** True when `op` is IN / NOT_IN (array value). */
|
|
64
|
+
function qbIsMultiOperator(op) {
|
|
65
|
+
return !!op && QB_MULTI_OPERATORS.includes(op);
|
|
66
|
+
}
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Node factories — monotonic counter for stable, render-safe ids (no Math.random
|
|
69
|
+
// / Date.now so SSR + tests stay deterministic).
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
let qbSeq = 0;
|
|
72
|
+
/** Next stable node id with the given prefix. */
|
|
73
|
+
function qbId(prefix = 'qb') {
|
|
74
|
+
return `${prefix}-${++qbSeq}`;
|
|
75
|
+
}
|
|
76
|
+
/** Build a fresh rule node (optionally pre-filled). */
|
|
77
|
+
function qbNewRule(field, operator, value) {
|
|
78
|
+
return { id: qbId('r'), kind: 'rule', field, operator, value };
|
|
79
|
+
}
|
|
80
|
+
/** Build a fresh group node. */
|
|
81
|
+
function qbNewGroup(logic = 'AND', children = []) {
|
|
82
|
+
return { id: qbId('g'), kind: 'group', logic, children };
|
|
83
|
+
}
|
|
84
|
+
/** Type guard — narrows an arbitrary value to a relative-date spec. */
|
|
85
|
+
function qbIsRelativeDate(v) {
|
|
86
|
+
return !!v && typeof v === 'object' && (v.rel === 'now' || v.rel === 'offset');
|
|
87
|
+
}
|
|
88
|
+
/** Starting relative value when a rule first switches to "relative" mode. */
|
|
89
|
+
function qbDefaultRelative() {
|
|
90
|
+
return { rel: 'offset', unit: 'day', amount: 1, direction: 'previous' };
|
|
91
|
+
}
|
|
92
|
+
/** Stable option list for the date-mode select (module ref — never reallocated). */
|
|
93
|
+
const QB_DATE_MODES = [
|
|
94
|
+
{ value: 'absolute', display: 'Ngày cụ thể' },
|
|
95
|
+
{ value: 'now', display: 'Hôm nay' },
|
|
96
|
+
{ value: 'relative', display: 'Tương đối' },
|
|
97
|
+
];
|
|
98
|
+
/** Stable combined direction×unit option list (token `'unit:direction'`). */
|
|
99
|
+
const QB_RELATIVE_UNIT_OPTIONS = [
|
|
100
|
+
{ value: 'day:previous', display: 'ngày trước' },
|
|
101
|
+
{ value: 'day:next', display: 'ngày tới' },
|
|
102
|
+
{ value: 'week:previous', display: 'tuần trước' },
|
|
103
|
+
{ value: 'week:next', display: 'tuần tới' },
|
|
104
|
+
{ value: 'month:previous', display: 'tháng trước' },
|
|
105
|
+
{ value: 'month:next', display: 'tháng tới' },
|
|
106
|
+
];
|
|
107
|
+
/** Shared empty option array — stable ref for fallbacks (avoids per-call allocation). */
|
|
108
|
+
const QB_EMPTY_OPTIONS = [];
|
|
109
|
+
|
|
110
|
+
// SQL symbol for each single-value comparison operator.
|
|
111
|
+
const SYMBOL = {
|
|
112
|
+
EQUAL: '=',
|
|
113
|
+
NOT_EQUAL: '!=',
|
|
114
|
+
GREATER_THAN: '>',
|
|
115
|
+
LESS_THAN: '<',
|
|
116
|
+
GREATER_OR_EQUAL: '>=',
|
|
117
|
+
LESS_OR_EQUAL: '<=',
|
|
118
|
+
};
|
|
119
|
+
const LIKE_OPERATORS = ['CONTAIN', 'NOT_CONTAIN', 'START_WITH', 'NOT_START_WITH', 'END_WITH', 'NOT_END_WITH'];
|
|
120
|
+
/** A `Filter` is a logical group when its operator is AND / OR and it carries a `data[]`. */
|
|
121
|
+
function isAndOr$1(filter) {
|
|
122
|
+
return !!filter && (filter.operator === 'AND' || filter.operator === 'OR') && Array.isArray(filter.data);
|
|
123
|
+
}
|
|
124
|
+
function isEmptyValue(v) {
|
|
125
|
+
return v === null || v === undefined || v === '';
|
|
126
|
+
}
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Internal tree → public Filter
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
function ruleToFilter(rule) {
|
|
131
|
+
if (!rule.field || !rule.operator)
|
|
132
|
+
return null;
|
|
133
|
+
const operator = rule.operator;
|
|
134
|
+
if (QB_NO_DATA_OPERATORS.includes(operator)) {
|
|
135
|
+
return { field: rule.field, operator };
|
|
136
|
+
}
|
|
137
|
+
if (operator === 'BETWEEN') {
|
|
138
|
+
const v = rule.value;
|
|
139
|
+
if (!v || typeof v !== 'object')
|
|
140
|
+
return null;
|
|
141
|
+
if (isEmptyValue(v.from) || isEmptyValue(v.to))
|
|
142
|
+
return null;
|
|
143
|
+
return { field: rule.field, operator: 'BETWEEN', data: { from: v.from, to: v.to } };
|
|
144
|
+
}
|
|
145
|
+
if (QB_MULTI_OPERATORS.includes(operator)) {
|
|
146
|
+
const arr = Array.isArray(rule.value) ? rule.value : isEmptyValue(rule.value) ? [] : [rule.value];
|
|
147
|
+
if (!arr.length)
|
|
148
|
+
return null;
|
|
149
|
+
return { field: rule.field, operator, data: arr };
|
|
150
|
+
}
|
|
151
|
+
if (qbIsRelativeDate(rule.value)) {
|
|
152
|
+
const v = rule.value;
|
|
153
|
+
if (v.rel === 'now')
|
|
154
|
+
return { field: rule.field, operator, data: { rel: 'now' } };
|
|
155
|
+
// offset — only emit when fully specified, else drop the incomplete rule
|
|
156
|
+
if (v.unit && v.direction && typeof v.amount === 'number' && v.amount >= 1) {
|
|
157
|
+
return {
|
|
158
|
+
field: rule.field,
|
|
159
|
+
operator,
|
|
160
|
+
data: { rel: 'offset', unit: v.unit, amount: v.amount, direction: v.direction },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
if (isEmptyValue(rule.value))
|
|
166
|
+
return null;
|
|
167
|
+
return { field: rule.field, operator, data: rule.value };
|
|
168
|
+
}
|
|
169
|
+
function nodeToFilter(node) {
|
|
170
|
+
if (isQbGroup(node)) {
|
|
171
|
+
const data = node.children.map(nodeToFilter).filter((f) => f != null);
|
|
172
|
+
if (!data.length)
|
|
173
|
+
return null;
|
|
174
|
+
return { operator: node.logic, data };
|
|
175
|
+
}
|
|
176
|
+
return ruleToFilter(node);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Map the internal builder tree to the public `Filter` (the root is always a
|
|
180
|
+
* `FilterAndOr`). Rules missing field / operator / value — and empty nested
|
|
181
|
+
* groups — are dropped so the result is always a valid `Filter`.
|
|
182
|
+
*
|
|
183
|
+
* @param group - the editable tree's root group.
|
|
184
|
+
* @returns the canonical nested `Filter`, or `null` when no complete rule survives.
|
|
185
|
+
*/
|
|
186
|
+
function treeToFilter(group) {
|
|
187
|
+
return nodeToFilter(group);
|
|
188
|
+
}
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Public Filter → internal tree (for seeding [value])
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
function ruleFromFilter(filter) {
|
|
193
|
+
const operator = filter.operator;
|
|
194
|
+
if (QB_NO_DATA_OPERATORS.includes(operator)) {
|
|
195
|
+
return qbNewRule(filter.field, operator);
|
|
196
|
+
}
|
|
197
|
+
return qbNewRule(filter.field, operator, filter.data);
|
|
198
|
+
}
|
|
199
|
+
function nodeFromFilter(filter) {
|
|
200
|
+
return isAndOr$1(filter) ? groupFromFilter(filter) : ruleFromFilter(filter);
|
|
201
|
+
}
|
|
202
|
+
function groupFromFilter(filter) {
|
|
203
|
+
return qbNewGroup(filter.operator, filter.data.map(nodeFromFilter));
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Inverse of {@link treeToFilter} — rebuild the editable tree from a `Filter` so a
|
|
207
|
+
* consumer can seed `[value]`. A non-group (bare) filter is wrapped in an AND root;
|
|
208
|
+
* `null` yields an empty AND root.
|
|
209
|
+
*
|
|
210
|
+
* @param filter - a `Filter` (group or leaf), or `null` / `undefined` for an empty tree.
|
|
211
|
+
* @returns a fresh root `QbGroup` mirroring the filter's structure.
|
|
212
|
+
*/
|
|
213
|
+
function filterToTree(filter) {
|
|
214
|
+
if (!filter)
|
|
215
|
+
return qbNewGroup('AND', []);
|
|
216
|
+
if (isAndOr$1(filter))
|
|
217
|
+
return groupFromFilter(filter);
|
|
218
|
+
return qbNewGroup('AND', [ruleFromFilter(filter)]);
|
|
219
|
+
}
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Public Filter → SQL-ish highlight tokens (view mode)
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
function escapeStr(s) {
|
|
224
|
+
return s.replace(/'/g, "''");
|
|
225
|
+
}
|
|
226
|
+
const REL_UNIT_VI = { day: 'ngày', week: 'tuần', month: 'tháng' };
|
|
227
|
+
/** Render a relative-date spec as readable Vietnamese for the view string. */
|
|
228
|
+
function formatRelative(v) {
|
|
229
|
+
if (v.rel === 'now')
|
|
230
|
+
return 'hôm nay';
|
|
231
|
+
const unit = REL_UNIT_VI[v.unit ?? 'day'];
|
|
232
|
+
const dir = v.direction === 'next' ? 'tới' : 'trước';
|
|
233
|
+
return `${v.amount ?? 1} ${unit} ${dir}`;
|
|
234
|
+
}
|
|
235
|
+
/** Format one scalar value for the view string — quoted/labelled per field type. */
|
|
236
|
+
function formatScalar(field, raw) {
|
|
237
|
+
if (qbIsRelativeDate(raw))
|
|
238
|
+
return formatRelative(raw);
|
|
239
|
+
if (raw === null || raw === undefined)
|
|
240
|
+
return '';
|
|
241
|
+
if (field?.type === 'boolean') {
|
|
242
|
+
return raw ? field.trueLabel ?? 'Có' : field.falseLabel ?? 'Không';
|
|
243
|
+
}
|
|
244
|
+
if (field?.type === 'values') {
|
|
245
|
+
const opt = field.values?.find(o => o.value === raw);
|
|
246
|
+
return `'${escapeStr(opt ? opt.display : String(raw))}'`;
|
|
247
|
+
}
|
|
248
|
+
if (field?.type === 'number' || typeof raw === 'number' || typeof raw === 'boolean') {
|
|
249
|
+
return String(raw);
|
|
250
|
+
}
|
|
251
|
+
// string / date / datetime / unknown → quoted
|
|
252
|
+
return `'${escapeStr(String(raw))}'`;
|
|
253
|
+
}
|
|
254
|
+
function likePattern(operator, raw) {
|
|
255
|
+
const esc = escapeStr(raw);
|
|
256
|
+
if (operator === 'CONTAIN' || operator === 'NOT_CONTAIN')
|
|
257
|
+
return `'%${esc}%'`;
|
|
258
|
+
if (operator === 'START_WITH' || operator === 'NOT_START_WITH')
|
|
259
|
+
return `'${esc}%'`;
|
|
260
|
+
return `'%${esc}'`; // END_WITH / NOT_END_WITH
|
|
261
|
+
}
|
|
262
|
+
function ruleTokens(filter, fields) {
|
|
263
|
+
const field = fields.find(f => f.key === filter.field);
|
|
264
|
+
const out = [{ kind: 'field', text: field?.label ?? String(filter.field) }];
|
|
265
|
+
const sp = () => out.push({ kind: 'plain', text: ' ' });
|
|
266
|
+
const operator = filter.operator;
|
|
267
|
+
if (QB_NO_DATA_OPERATORS.includes(operator)) {
|
|
268
|
+
sp();
|
|
269
|
+
out.push({ kind: 'op', text: operator === 'NULL' ? 'is null' : 'is not null' });
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
if (operator === 'BETWEEN') {
|
|
273
|
+
sp();
|
|
274
|
+
out.push({ kind: 'op', text: 'between' });
|
|
275
|
+
sp();
|
|
276
|
+
out.push({ kind: 'value', text: formatScalar(field, filter.data?.from) });
|
|
277
|
+
out.push({ kind: 'plain', text: ' and ' });
|
|
278
|
+
out.push({ kind: 'value', text: formatScalar(field, filter.data?.to) });
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
if (QB_MULTI_OPERATORS.includes(operator)) {
|
|
282
|
+
sp();
|
|
283
|
+
out.push({ kind: 'op', text: operator === 'IN' ? 'in' : 'not in' });
|
|
284
|
+
sp();
|
|
285
|
+
const arr = Array.isArray(filter.data) ? filter.data : [];
|
|
286
|
+
out.push({ kind: 'value', text: `(${arr.map((v) => formatScalar(field, v)).join(', ')})` });
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
if (LIKE_OPERATORS.includes(operator)) {
|
|
290
|
+
sp();
|
|
291
|
+
out.push({ kind: 'op', text: operator.startsWith('NOT_') ? 'not like' : 'like' });
|
|
292
|
+
sp();
|
|
293
|
+
out.push({ kind: 'value', text: likePattern(operator, String(filter.data ?? '')) });
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
sp();
|
|
297
|
+
out.push({ kind: 'op', text: SYMBOL[operator] ?? operator });
|
|
298
|
+
sp();
|
|
299
|
+
out.push({ kind: 'value', text: formatScalar(field, filter.data) });
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
function tokensFor(filter, fields) {
|
|
303
|
+
if (isAndOr$1(filter)) {
|
|
304
|
+
const out = [];
|
|
305
|
+
filter.data.forEach((child, i) => {
|
|
306
|
+
if (i > 0)
|
|
307
|
+
out.push({ kind: 'logic', text: ` ${filter.operator.toLowerCase()} ` });
|
|
308
|
+
const childTokens = tokensFor(child, fields);
|
|
309
|
+
// Wrap a nested multi-child group in parentheses; a single-child group needs none.
|
|
310
|
+
if (isAndOr$1(child) && child.data.length > 1) {
|
|
311
|
+
out.push({ kind: 'paren', text: '(' }, ...childTokens, { kind: 'paren', text: ')' });
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
out.push(...childTokens);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
return out;
|
|
318
|
+
}
|
|
319
|
+
return ruleTokens(filter, fields);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Render a `Filter` to a SQL-ish token stream for view mode. Field tokens use the
|
|
323
|
+
* matching field's `label`; operators map to SQL syntax (`= != like between is null …`);
|
|
324
|
+
* `and` / `or` are lowercase; nested multi-child groups are wrapped in parentheses.
|
|
325
|
+
* Returns a token stream (not a string) so the template can wrap each piece in a
|
|
326
|
+
* highlight `<span>` keyed by `QbToken.kind`.
|
|
327
|
+
*
|
|
328
|
+
* @param filter - the `Filter` to render, or `null` / `undefined`.
|
|
329
|
+
* @param fields - field metadata, used to resolve each field key to its display label + value formatting.
|
|
330
|
+
* @returns the ordered tokens, or `[]` when `filter` is empty.
|
|
331
|
+
*/
|
|
332
|
+
function filterToTokens(filter, fields) {
|
|
333
|
+
if (!filter)
|
|
334
|
+
return [];
|
|
335
|
+
return tokensFor(filter, fields);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
339
|
+
/** A logical `Filter` (AND / OR group) carries a `data[]` array. */
|
|
340
|
+
function isAndOr(filter) {
|
|
341
|
+
return !!filter && (filter.operator === 'AND' || filter.operator === 'OR') && Array.isArray(filter.data);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Visual filter / rule builder. Compose nested AND / OR groups of
|
|
345
|
+
* `field operator value` conditions; emits the canonical `Filter` (root is a
|
|
346
|
+
* `FilterAndOr` tree) via `[(value)]`, plus a flat `[(filters)]` mirror of the
|
|
347
|
+
* root group's direct children. `mode="view"` renders a read-only SQL-ish raw
|
|
348
|
+
* string with highlighted operators + values.
|
|
349
|
+
*/
|
|
8
350
|
class SdQueryBuilder {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
351
|
+
// -------------------------------------------------------------------------
|
|
352
|
+
// Inputs
|
|
353
|
+
// -------------------------------------------------------------------------
|
|
354
|
+
/** Filterable fields — each declares a `type` that drives the operator set + value editor. */
|
|
355
|
+
fields = input([]);
|
|
356
|
+
/** `'edit'` (default) = interactive tree · `'view'` = read-only raw-query string. */
|
|
357
|
+
mode = input('edit');
|
|
358
|
+
/** Disable all editing controls. */
|
|
359
|
+
disabled = input(false, { transform: booleanAttribute });
|
|
360
|
+
/** Prefix for `data-autoid` on inner controls (E2E selectors). */
|
|
361
|
+
autoId = input();
|
|
362
|
+
// -------------------------------------------------------------------------
|
|
363
|
+
// Two-way models (output contract)
|
|
364
|
+
// -------------------------------------------------------------------------
|
|
365
|
+
/** Canonical output — the root `FilterAndOr` tree, or `null` when empty. `[(value)]`. */
|
|
366
|
+
value = model(null);
|
|
367
|
+
/** Convenience mirror — the root group's direct children. `[(filters)]`. */
|
|
368
|
+
filters = model([]);
|
|
369
|
+
/** Root group's logical connector. `[(rootLogic)]`. */
|
|
370
|
+
rootLogic = model('AND');
|
|
371
|
+
// -------------------------------------------------------------------------
|
|
372
|
+
// Internal source of truth — the editable tree (carries UI state).
|
|
373
|
+
// -------------------------------------------------------------------------
|
|
374
|
+
#tree = signal(qbNewGroup('AND'));
|
|
375
|
+
tree = this.#tree.asReadonly();
|
|
376
|
+
/** Last filter WE emitted — used to recognise (and ignore) our own echo in the inbound effect. */
|
|
377
|
+
#lastEmitted = undefined;
|
|
378
|
+
// -------------------------------------------------------------------------
|
|
379
|
+
// Derived state
|
|
380
|
+
// -------------------------------------------------------------------------
|
|
381
|
+
/** `field.key` → field, for fast lookup from a rule. */
|
|
382
|
+
fieldByKey = computed(() => {
|
|
383
|
+
const map = {};
|
|
384
|
+
for (const f of this.fields())
|
|
385
|
+
map[f.key] = f;
|
|
386
|
+
return map;
|
|
387
|
+
});
|
|
388
|
+
/** Boolean fields → their stable `[true,false]` option list. Memoized so the template
|
|
389
|
+
* binding hands `sd-select` the SAME array ref each CD (else its `toObservable(items)`
|
|
390
|
+
* → `markForCheck()` loops → OOM). Recomputes only when `fields()` changes. */
|
|
391
|
+
#booleanOptionsByKey = computed(() => {
|
|
392
|
+
const map = new Map();
|
|
393
|
+
for (const f of this.fields()) {
|
|
394
|
+
if (f.type === 'boolean') {
|
|
395
|
+
map.set(f.key, [
|
|
396
|
+
{ value: true, display: f.trueLabel ?? 'Có' },
|
|
397
|
+
{ value: false, display: f.falseLabel ?? 'Không' },
|
|
398
|
+
]);
|
|
19
399
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
400
|
+
}
|
|
401
|
+
return map;
|
|
402
|
+
});
|
|
403
|
+
/** Stable option list for the date-mode select. */
|
|
404
|
+
dateModes = QB_DATE_MODES;
|
|
405
|
+
/** Stable combined unit×direction option list for the relative select. */
|
|
406
|
+
relativeUnitOptions = QB_RELATIVE_UNIT_OPTIONS;
|
|
407
|
+
/** View-mode token stream — recomputed from the emitted value. */
|
|
408
|
+
tokens = computed(() => filterToTokens(this.value(), this.fields()));
|
|
409
|
+
isView = computed(() => this.mode() === 'view');
|
|
410
|
+
/** Expose the type-guard to the template. */
|
|
411
|
+
isGroup = isQbGroup;
|
|
412
|
+
constructor() {
|
|
413
|
+
// Inbound seeding: an EXTERNAL write to value / filters rebuilds the tree.
|
|
414
|
+
// Our own emits are recognised via #lastEmitted and skipped (no echo loop).
|
|
415
|
+
effect(() => {
|
|
416
|
+
const v = this.value();
|
|
417
|
+
const fl = this.filters();
|
|
418
|
+
const incoming = this.#coalesce(v, fl);
|
|
419
|
+
if (this.#filterEq(incoming, this.#lastEmitted))
|
|
420
|
+
return;
|
|
421
|
+
this.#tree.set(filterToTree(incoming));
|
|
422
|
+
this.#commit();
|
|
423
|
+
});
|
|
25
424
|
}
|
|
26
|
-
|
|
27
|
-
|
|
425
|
+
#coalesce(v, fl) {
|
|
426
|
+
if (v)
|
|
427
|
+
return v;
|
|
428
|
+
if (fl && fl.length)
|
|
429
|
+
return { operator: untracked(() => this.rootLogic()), data: fl };
|
|
430
|
+
return null;
|
|
28
431
|
}
|
|
29
|
-
|
|
432
|
+
#filterEq(a, b) {
|
|
433
|
+
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
434
|
+
}
|
|
435
|
+
// -------------------------------------------------------------------------
|
|
436
|
+
// Tree mutation → emit
|
|
437
|
+
// -------------------------------------------------------------------------
|
|
438
|
+
/** New root ref so the template (which reads `tree()`) re-renders. */
|
|
439
|
+
#bumpTree() {
|
|
440
|
+
this.#tree.set({ ...this.#tree() });
|
|
441
|
+
}
|
|
442
|
+
/** Recompute the output `Filter` from the tree and push it to value / filters / rootLogic. */
|
|
443
|
+
#commit() {
|
|
444
|
+
const filter = treeToFilter(this.#tree());
|
|
445
|
+
this.#lastEmitted = filter;
|
|
446
|
+
this.value.set(filter);
|
|
447
|
+
this.filters.set(isAndOr(filter) ? filter.data : filter ? [filter] : []);
|
|
448
|
+
const logic = this.#tree().logic;
|
|
449
|
+
if (this.rootLogic() !== logic)
|
|
450
|
+
this.rootLogic.set(logic);
|
|
451
|
+
}
|
|
452
|
+
/** Mutate-in-place + re-render + emit. */
|
|
453
|
+
#apply() {
|
|
454
|
+
this.#bumpTree();
|
|
455
|
+
this.#commit();
|
|
456
|
+
}
|
|
457
|
+
// -------------------------------------------------------------------------
|
|
458
|
+
// Group / rule structural mutations
|
|
459
|
+
// -------------------------------------------------------------------------
|
|
460
|
+
/** Switch a group's AND/OR connector and re-emit. No-op when disabled or unchanged. */
|
|
461
|
+
setGroupLogic(group, logic) {
|
|
462
|
+
if (this.disabled() || group.logic === logic)
|
|
463
|
+
return;
|
|
464
|
+
group.logic = logic;
|
|
465
|
+
this.#apply();
|
|
466
|
+
}
|
|
467
|
+
/** Open this group's "+" menu (closing any other open one). UI-only — does not emit. */
|
|
30
468
|
toggleDropdown(group, event) {
|
|
469
|
+
if (this.disabled())
|
|
470
|
+
return;
|
|
31
471
|
event.stopPropagation();
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
group.isOpen = !group.isOpen;
|
|
472
|
+
const willOpen = !group.open;
|
|
473
|
+
this.#closeAllDropdowns(this.#tree());
|
|
474
|
+
group.open = willOpen;
|
|
475
|
+
this.#bumpTree(); // UI-only — no emit
|
|
37
476
|
}
|
|
38
|
-
|
|
39
|
-
this
|
|
477
|
+
closeAllDropdowns() {
|
|
478
|
+
if (!this.#hasOpenDropdown(this.#tree()))
|
|
479
|
+
return; // avoid a re-render on every container click
|
|
480
|
+
this.#closeAllDropdowns(this.#tree());
|
|
481
|
+
this.#bumpTree();
|
|
40
482
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
483
|
+
#hasOpenDropdown(node) {
|
|
484
|
+
if (!isQbGroup(node))
|
|
485
|
+
return false;
|
|
486
|
+
return node.open === true || node.children.some(c => this.#hasOpenDropdown(c));
|
|
487
|
+
}
|
|
488
|
+
#closeAllDropdowns(node) {
|
|
489
|
+
if (isQbGroup(node)) {
|
|
490
|
+
node.open = false;
|
|
491
|
+
node.children.forEach(c => this.#closeAllDropdowns(c));
|
|
492
|
+
}
|
|
47
493
|
}
|
|
48
|
-
|
|
494
|
+
/** Append a blank rule to `group`. */
|
|
49
495
|
addRule(group) {
|
|
50
|
-
|
|
51
|
-
|
|
496
|
+
if (this.disabled())
|
|
497
|
+
return;
|
|
498
|
+
group.children.push(qbNewRule());
|
|
499
|
+
group.open = false;
|
|
500
|
+
this.#apply();
|
|
52
501
|
}
|
|
502
|
+
/** Append a nested sub-group (seeded with one blank rule) to `group`. */
|
|
53
503
|
addGroup(group) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
504
|
+
if (this.disabled())
|
|
505
|
+
return;
|
|
506
|
+
group.children.push(qbNewGroup('AND', [qbNewRule()]));
|
|
507
|
+
group.open = false;
|
|
508
|
+
this.#apply();
|
|
509
|
+
}
|
|
510
|
+
/** Remove the child at `index` from `parent` (a rule or a whole sub-group). */
|
|
511
|
+
removeNode(parent, index) {
|
|
512
|
+
if (this.disabled())
|
|
513
|
+
return;
|
|
514
|
+
parent.children.splice(index, 1);
|
|
515
|
+
this.#apply();
|
|
516
|
+
}
|
|
517
|
+
// -------------------------------------------------------------------------
|
|
518
|
+
// Rule field / operator / value mutations
|
|
519
|
+
// -------------------------------------------------------------------------
|
|
520
|
+
/**
|
|
521
|
+
* Point `rule` at a new field. Resets operator to the field's default and the value
|
|
522
|
+
* to that operator's empty shape — the old operator/value rarely fit the new type.
|
|
523
|
+
*
|
|
524
|
+
* @param rule - the rule being edited.
|
|
525
|
+
* @param key - the chosen field key (from the field picker). Swap-only: a null / undefined
|
|
526
|
+
* key is a no-op — the field can be changed but never cleared (issue #2).
|
|
527
|
+
*/
|
|
528
|
+
setField(rule, key) {
|
|
529
|
+
if (this.disabled())
|
|
530
|
+
return;
|
|
531
|
+
if (key == null)
|
|
532
|
+
return; // swap-only: never clear the field (issue #2)
|
|
533
|
+
rule.field = key;
|
|
534
|
+
const field = this.fieldByKey()[rule.field];
|
|
535
|
+
const op = qbDefaultOperator(field);
|
|
536
|
+
rule.operator = op;
|
|
537
|
+
rule.value = this.#defaultValueFor(op);
|
|
538
|
+
this.#apply();
|
|
539
|
+
}
|
|
540
|
+
/** Change `rule`'s operator and reshape its value to fit the new operator (array / range / none). */
|
|
541
|
+
setOperator(rule, op) {
|
|
542
|
+
if (this.disabled())
|
|
543
|
+
return;
|
|
544
|
+
rule.operator = op;
|
|
545
|
+
rule.value = this.#reshapeValue(op, rule.value);
|
|
546
|
+
this.#apply();
|
|
547
|
+
}
|
|
548
|
+
/** Set a single-value rule's value (coercing numeric fields to `number`). */
|
|
549
|
+
setScalar(rule, raw) {
|
|
550
|
+
if (this.disabled())
|
|
551
|
+
return;
|
|
552
|
+
rule.value = this.#coerce(rule, raw);
|
|
553
|
+
this.#apply();
|
|
554
|
+
}
|
|
555
|
+
/** Set the lower bound of a BETWEEN rule's `{ from, to }` value. */
|
|
556
|
+
setBetweenFrom(rule, raw) {
|
|
557
|
+
if (this.disabled())
|
|
558
|
+
return;
|
|
559
|
+
rule.value = { ...(rule.value ?? {}), from: this.#coerce(rule, raw) };
|
|
560
|
+
this.#apply();
|
|
561
|
+
}
|
|
562
|
+
/** Set the upper bound of a BETWEEN rule's `{ from, to }` value. */
|
|
563
|
+
setBetweenTo(rule, raw) {
|
|
564
|
+
if (this.disabled())
|
|
565
|
+
return;
|
|
566
|
+
rule.value = { ...(rule.value ?? {}), to: this.#coerce(rule, raw) };
|
|
567
|
+
this.#apply();
|
|
568
|
+
}
|
|
569
|
+
/** Coerce numeric fields to `number` so the emitted Filter carries a real number. */
|
|
570
|
+
#coerce(rule, raw) {
|
|
571
|
+
const field = this.fieldByKey()[rule.field];
|
|
572
|
+
if (field?.type === 'number' && raw !== null && raw !== undefined && raw !== '') {
|
|
573
|
+
const n = Number(raw);
|
|
574
|
+
return Number.isNaN(n) ? raw : n;
|
|
575
|
+
}
|
|
576
|
+
return raw;
|
|
577
|
+
}
|
|
578
|
+
#defaultValueFor(op) {
|
|
579
|
+
if (qbIsMultiOperator(op))
|
|
580
|
+
return [];
|
|
581
|
+
if (op === 'BETWEEN')
|
|
582
|
+
return { from: null, to: null };
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
#reshapeValue(op, current) {
|
|
586
|
+
if (qbIsNoDataOperator(op))
|
|
587
|
+
return null;
|
|
588
|
+
if (qbIsMultiOperator(op))
|
|
589
|
+
return Array.isArray(current) ? current : current == null ? [] : [current];
|
|
590
|
+
if (op === 'BETWEEN') {
|
|
591
|
+
// a relative spec can't be a BETWEEN endpoint — reset to a fresh range
|
|
592
|
+
return current && typeof current === 'object' && !Array.isArray(current) && !qbIsRelativeDate(current)
|
|
593
|
+
? current
|
|
594
|
+
: { from: null, to: null };
|
|
595
|
+
}
|
|
596
|
+
// single value — keep a relative spec; otherwise drop array/range remnants
|
|
597
|
+
if (qbIsRelativeDate(current))
|
|
598
|
+
return current;
|
|
599
|
+
return Array.isArray(current) || (current && typeof current === 'object') ? null : current;
|
|
600
|
+
}
|
|
601
|
+
// -------------------------------------------------------------------------
|
|
602
|
+
// Template helpers
|
|
603
|
+
// -------------------------------------------------------------------------
|
|
604
|
+
/** The field metadata for a rule's current `field` key (or undefined if unset). */
|
|
605
|
+
fieldOf(rule) {
|
|
606
|
+
return rule.field ? this.fieldByKey()[rule.field] : undefined;
|
|
607
|
+
}
|
|
608
|
+
/** Operators allowed for a rule, derived from its field's type / override. */
|
|
609
|
+
allowedOperators(rule) {
|
|
610
|
+
return qbAllowedOperators(this.fieldOf(rule));
|
|
611
|
+
}
|
|
612
|
+
/** Whether to render the operator picker — hidden when only one operator is allowed. */
|
|
613
|
+
showOperatorSelector(rule) {
|
|
614
|
+
return this.allowedOperators(rule).length > 1;
|
|
615
|
+
}
|
|
616
|
+
/** True for NULL / NOT_NULL — the template then renders no value editor. */
|
|
617
|
+
isNoData(op) {
|
|
618
|
+
return qbIsNoDataOperator(op);
|
|
619
|
+
}
|
|
620
|
+
/** True for IN / NOT_IN — the template then renders a multi-select value editor. */
|
|
621
|
+
isMulti(op) {
|
|
622
|
+
return qbIsMultiOperator(op);
|
|
623
|
+
}
|
|
624
|
+
/** `[true/false]` option list for a boolean field's value select (stable reference). */
|
|
625
|
+
booleanItems(field) {
|
|
626
|
+
return this.#booleanOptionsByKey().get(field.key) ?? QB_EMPTY_OPTIONS;
|
|
627
|
+
}
|
|
628
|
+
/** Current date-value mode of a rule, derived from its value (no separate state). */
|
|
629
|
+
dateMode(rule) {
|
|
630
|
+
const v = rule.value;
|
|
631
|
+
if (qbIsRelativeDate(v))
|
|
632
|
+
return v.rel === 'now' ? 'now' : 'relative';
|
|
633
|
+
return 'absolute';
|
|
634
|
+
}
|
|
635
|
+
/** Switch a date/datetime rule's value mode, reseeding the value for the new mode. */
|
|
636
|
+
setDateMode(rule, mode) {
|
|
637
|
+
if (this.disabled())
|
|
638
|
+
return;
|
|
639
|
+
if (mode === 'now')
|
|
640
|
+
rule.value = { rel: 'now' };
|
|
641
|
+
else if (mode === 'relative')
|
|
642
|
+
rule.value = qbDefaultRelative();
|
|
643
|
+
else
|
|
644
|
+
rule.value = null; // absolute — pick a concrete date
|
|
645
|
+
this.#apply();
|
|
646
|
+
}
|
|
647
|
+
/** Read the offset amount of a relative rule (default 1). */
|
|
648
|
+
relativeAmount(rule) {
|
|
649
|
+
const v = rule.value;
|
|
650
|
+
return qbIsRelativeDate(v) && v.rel === 'offset' ? v.amount ?? 1 : 1;
|
|
651
|
+
}
|
|
652
|
+
/** Set the offset amount (clamped to an integer >= 1). */
|
|
653
|
+
setRelativeAmount(rule, raw) {
|
|
654
|
+
if (this.disabled())
|
|
655
|
+
return;
|
|
656
|
+
const n = Math.floor(Number(raw));
|
|
657
|
+
const amount = Number.isNaN(n) || n < 1 ? 1 : n;
|
|
658
|
+
const cur = qbIsRelativeDate(rule.value) ? rule.value : qbDefaultRelative();
|
|
659
|
+
rule.value = { rel: 'offset', unit: cur.unit ?? 'day', amount, direction: cur.direction ?? 'previous' };
|
|
660
|
+
this.#apply();
|
|
661
|
+
}
|
|
662
|
+
/** Read the `'unit:direction'` token of a relative rule (default `'day:previous'`). */
|
|
663
|
+
relativeUnitDirValue(rule) {
|
|
664
|
+
const v = rule.value;
|
|
665
|
+
if (qbIsRelativeDate(v) && v.rel === 'offset')
|
|
666
|
+
return `${v.unit ?? 'day'}:${v.direction ?? 'previous'}`;
|
|
667
|
+
return 'day:previous';
|
|
668
|
+
}
|
|
669
|
+
/** Set the offset unit + direction from a `'unit:direction'` token. */
|
|
670
|
+
setRelativeUnitDir(rule, token) {
|
|
671
|
+
if (this.disabled())
|
|
672
|
+
return;
|
|
673
|
+
const [unit, direction] = (token ?? 'day:previous').split(':');
|
|
674
|
+
const cur = qbIsRelativeDate(rule.value) ? rule.value : qbDefaultRelative();
|
|
675
|
+
rule.value = { rel: 'offset', unit, amount: cur.amount ?? 1, direction };
|
|
676
|
+
this.#apply();
|
|
59
677
|
}
|
|
60
|
-
|
|
61
|
-
|
|
678
|
+
/** Build a `data-autoid` for an inner control under this builder's `autoId` prefix. */
|
|
679
|
+
autoIdFor(suffix) {
|
|
680
|
+
return `${this.autoId() ?? 'qb'}-${suffix}`;
|
|
62
681
|
}
|
|
63
682
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdQueryBuilder, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
64
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.21", type: SdQueryBuilder, isStandalone: true, selector: "sd-query-builder", inputs: { group: "group" }, outputs: { groupChange: "groupChange" }, host: { listeners: { "document:click": "closeAll()" } }, ngImport: i0, template: "<div class=\"qb-container\">\n <ng-container *ngTemplateOutlet=\"groupTemplate; context: { $implicit: group, isRoot: true }\"></ng-container>\n</div>\n\n<ng-template #groupTemplate let-group let-isRoot=\"isRoot\" let-parent=\"parent\" let-index=\"index\">\n <div class=\"qb-group\">\n \n <div class=\"qb-header\" [class.is-open]=\"group.isOpen\">\n <div class=\"qb-condition-switch\">\n <button type=\"button\" \n [class.active]=\"group.condition === 'AND'\" \n (click)=\"toggleCondition(group, 'AND')\">AND</button>\n <button type=\"button\" \n [class.active]=\"group.condition === 'OR'\" \n (click)=\"toggleCondition(group, 'OR')\">OR</button>\n </div>\n\n <div class=\"qb-actions\">\n <button type=\"button\" class=\"btn-icon btn-add\" (click)=\"toggleDropdown(group, $event)\">+</button>\n \n <div class=\"qb-dropdown\" *ngIf=\"group.isOpen\" (click)=\"$event.stopPropagation()\" aria-hidden=\"true\">\n <button (click)=\"addGroup(group)\">+ Add Group</button>\n <button (click)=\"addRule(group)\">+ Add Condition</button>\n </div>\n\n <button *ngIf=\"!isRoot\" type=\"button\" class=\"btn-icon btn-remove\" (click)=\"removeItem(parent, index)\">\u2715</button>\n </div>\n </div>\n\n <div class=\"qb-body\">\n <div class=\"qb-list\">\n <div *ngFor=\"let item of group.rules; let i = index\" \n class=\"qb-treenode\"\n [class.is-group]=\"isGroup(item)\"\n [class.is-rule]=\"!isGroup(item)\">\n \n <ng-container *ngIf=\"!isGroup(item)\">\n <div class=\"qb-rule-card\">\n <div class=\"form-row\">\n <input class=\"form-input field-input\" [(ngModel)]=\"item.field\" placeholder=\"Select Field\">\n <ng-container *ngIf=\"item.field\">\n <div class=\"divider\"></div>\n <select class=\"form-input\" [(ngModel)]=\"item.operator\">\n <option value=\"Equal\">Equal</option>\n <option value=\"Not Equal\">Not Equal</option>\n </select>\n <div class=\"divider\"></div>\n <input class=\"form-input\" [(ngModel)]=\"item.value\" placeholder=\"Value\">\n </ng-container>\n <button type=\"button\" class=\"btn-icon btn-remove-rule\" (click)=\"removeItem(group, i)\">\u2715</button>\n </div>\n </div>\n </ng-container>\n <ng-container *ngIf=\"isGroup(item)\">\n <ng-container *ngTemplateOutlet=\"groupTemplate; context: { $implicit: item, isRoot: false, parent: group, index: i }\"></ng-container>\n </ng-container>\n </div>\n </div>\n </div>\n\n </div>\n</ng-template>", styles: [":host{display:block;font-family:Segoe UI,Roboto,sans-serif;font-size:14px;color:#333}.qb-container{padding:20px;background:#fff}.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-condition-switch{display:inline-flex;border-radius:4px;overflow:hidden;background:#fff;border:1px solid #e0e0e0;height:32px}.qb-condition-switch button{border:none;background:transparent;padding:0 16px;font-weight:600;font-size:13px;cursor:pointer;color:#666;height:100%}.qb-condition-switch button.active{background:#6246a8;color:#fff}.qb-condition-switch button:hover:not(.active){background:#f5f5f5}.qb-actions{position:relative;display:flex;gap:4px}.qb-body{padding-left:28px}.qb-list{position:relative}.qb-treenode{position:relative;padding-left:24px;margin-bottom:12px}.qb-treenode:before{content:\"\";position:absolute;left:0;top:-12px;bottom:0;width:1px;border-left:1px dashed #c4c4c4}.qb-treenode:after{content:\"\";position:absolute;left:0;width:24px;height:1px;border-top:1px dashed #c4c4c4}.qb-treenode:first-child:before{top:-14px;height:calc(100% + 14px)}.qb-treenode.is-group:after{top:16px}.qb-treenode.is-group:last-child:before{height:auto;bottom:auto;top:-14px;height:30px}.qb-treenode.is-rule:after{top:26px}.qb-treenode.is-rule:last-child:before{height:auto;bottom:auto;top:-14px;height:40px}.qb-rule-card{background:#fafafa;border:1px solid #e0e0e0;border-radius:4px;padding:10px 12px;transition:.2s}.qb-rule-card:hover{border-color:#ccc}.form-row{display:flex;align-items:center;gap:12px;height:32px}.form-input{border:none;background:transparent;font-size:14px;color:#333;outline:none;height:100%}.form-input::placeholder{color:#aaa}.divider{width:1px;height:20px;background:#ddd}.btn-icon{background:transparent;border:none;cursor:pointer;font-size:18px;color:#666;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:4px}.btn-icon:hover{background:#f0f0f0;color:#6246a8}.btn-remove{color:#999}.btn-remove:hover{color:#d32f2f;background:#ffebee}.btn-remove-rule{margin-left:auto;color:#999}.btn-remove-rule:hover{color:#d32f2f;background:none}.qb-dropdown{position:absolute;top:100%;left:0;margin-top:4px;background:#fff;border:1px solid #e0e0e0;box-shadow:0 4px 12px #00000026;border-radius:4px;min-width:160px;z-index:100}.qb-dropdown button{display:block;width:100%;text-align:left;background:none;border:none;padding:10px 14px;cursor:pointer;font-size:13px;color:#333}.qb-dropdown button:hover{background:#f3f0fa;color:#6246a8}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
|
|
683
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.21", type: SdQueryBuilder, isStandalone: true, selector: "sd-query-builder", inputs: { fields: { classPropertyName: "fields", publicName: "fields", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", 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]=\"disabled()\" (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]=\"disabled()\" (click)=\"setGroupLogic(group, 'AND')\">AND</button>\n <button type=\"button\" [class.active]=\"group.logic === 'OR'\" [disabled]=\"disabled()\" (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]=\"disabled()\" (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]=\"disabled()\" (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]=\"fields()\"\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]=\"disabled()\"\n [clearable]=\"false\"\n hideInlineError\n [autoId]=\"autoIdFor('field-' + index)\"\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]=\"disabled()\"\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 (rule.operator === 'BETWEEN') {\n <div class=\"qb-between\">\n @switch (fieldOf(rule)?.type) {\n @case ('number') {\n <sd-input class=\"qb-val\" type=\"number\" placeholder=\"T\u1EEB\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [disabled]=\"disabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-input>\n <span class=\"qb-dash\">\u2014</span>\n <sd-input class=\"qb-val\" type=\"number\" placeholder=\"\u0110\u1EBFn\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [disabled]=\"disabled()\" hideInlineError [autoId]=\"autoIdFor('to-' + index)\"></sd-input>\n }\n @case ('date') {\n <sd-date class=\"qb-val\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [disabled]=\"disabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-date>\n <span class=\"qb-dash\">\u2014</span>\n <sd-date class=\"qb-val\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [disabled]=\"disabled()\" 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]=\"disabled()\" 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]=\"disabled()\" 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]=\"disabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-input>\n }\n @case ('number') {\n <sd-input class=\"qb-val\" type=\"number\" placeholder=\"Gi\u00E1 tr\u1ECB\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"disabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-input>\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]=\"disabled()\" [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]=\"disabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('datemode-' + index)\"></sd-select>\n @switch (_dateMode) {\n @case ('absolute') {\n <sd-date class=\"qb-val\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"disabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-date>\n }\n @case ('relative') {\n <sd-input class=\"qb-rel-amount\" type=\"number\" size=\"sm\" [model]=\"relativeAmount(rule)\" (modelChange)=\"setRelativeAmount(rule, $event)\" [disabled]=\"disabled()\" hideInlineError [autoId]=\"autoIdFor('rel-amount-' + index)\"></sd-input>\n <sd-select class=\"qb-rel-unit\" [items]=\"relativeUnitOptions\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"relativeUnitDirValue(rule)\" (modelChange)=\"setRelativeUnitDir(rule, $event)\" [disabled]=\"disabled()\" [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]=\"disabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('datemode-' + index)\"></sd-select>\n @switch (_dtMode) {\n @case ('absolute') {\n <sd-datetime class=\"qb-val\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"disabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-datetime>\n }\n @case ('relative') {\n <sd-input class=\"qb-rel-amount\" type=\"number\" size=\"sm\" [model]=\"relativeAmount(rule)\" (modelChange)=\"setRelativeAmount(rule, $event)\" [disabled]=\"disabled()\" hideInlineError [autoId]=\"autoIdFor('rel-amount-' + index)\"></sd-input>\n <sd-select class=\"qb-rel-unit\" [items]=\"relativeUnitOptions\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"relativeUnitDirValue(rule)\" (modelChange)=\"setRelativeUnitDir(rule, $event)\" [disabled]=\"disabled()\" [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]=\"disabled()\" [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]=\"disabled()\" (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, #2a66f4);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, #2a66f4);color:#fff}.qb-logic-switch button:hover:not(.active):not([disabled]){background:var(--sd-primary-light, #eaf1ff);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, #eaf1ff);color:var(--sd-primary, #2a66f4)}.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, #eaf1ff);color:var(--sd-primary, #2a66f4)}.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:50%;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:150px}.qb-val{min-width:140px}.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, #2a66f4);border-radius:6px;padding:12px;background:#fff}.qb-date-mode{min-width:120px}.qb-rel-amount{width:72px}.qb-rel-unit{min-width:110px}\n"], dependencies: [{ 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", "hideInlineError", "blurOnEnter", "required", "readonly", "disabled", "viewed", "minlength", "maxlength", "pattern", "patternErrorMessage", "validator", "inlineError", "hyperlink", "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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
65
684
|
}
|
|
66
685
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdQueryBuilder, decorators: [{
|
|
67
686
|
type: Component,
|
|
68
|
-
args: [{ selector: 'sd-query-builder', standalone: true, imports: [CommonModule, FormsModule], template: "<div class=\"qb-container\">\n <ng-container *ngTemplateOutlet=\"groupTemplate; context: { $implicit: group, isRoot: true }\"></ng-container>\n</div>\n\n<ng-template #groupTemplate let-group let-isRoot=\"isRoot\" let-parent=\"parent\" let-index=\"index\">\n <div class=\"qb-group\">\n \n <div class=\"qb-header\" [class.is-open]=\"group.isOpen\">\n <div class=\"qb-condition-switch\">\n <button type=\"button\" \n [class.active]=\"group.condition === 'AND'\" \n (click)=\"toggleCondition(group, 'AND')\">AND</button>\n <button type=\"button\" \n [class.active]=\"group.condition === 'OR'\" \n (click)=\"toggleCondition(group, 'OR')\">OR</button>\n </div>\n\n <div class=\"qb-actions\">\n <button type=\"button\" class=\"btn-icon btn-add\" (click)=\"toggleDropdown(group, $event)\">+</button>\n \n <div class=\"qb-dropdown\" *ngIf=\"group.isOpen\" (click)=\"$event.stopPropagation()\" aria-hidden=\"true\">\n <button (click)=\"addGroup(group)\">+ Add Group</button>\n <button (click)=\"addRule(group)\">+ Add Condition</button>\n </div>\n\n <button *ngIf=\"!isRoot\" type=\"button\" class=\"btn-icon btn-remove\" (click)=\"removeItem(parent, index)\">\u2715</button>\n </div>\n </div>\n\n <div class=\"qb-body\">\n <div class=\"qb-list\">\n <div *ngFor=\"let item of group.rules; let i = index\" \n class=\"qb-treenode\"\n [class.is-group]=\"isGroup(item)\"\n [class.is-rule]=\"!isGroup(item)\">\n \n <ng-container *ngIf=\"!isGroup(item)\">\n <div class=\"qb-rule-card\">\n <div class=\"form-row\">\n <input class=\"form-input field-input\" [(ngModel)]=\"item.field\" placeholder=\"Select Field\">\n <ng-container *ngIf=\"item.field\">\n <div class=\"divider\"></div>\n <select class=\"form-input\" [(ngModel)]=\"item.operator\">\n <option value=\"Equal\">Equal</option>\n <option value=\"Not Equal\">Not Equal</option>\n </select>\n <div class=\"divider\"></div>\n <input class=\"form-input\" [(ngModel)]=\"item.value\" placeholder=\"Value\">\n </ng-container>\n <button type=\"button\" class=\"btn-icon btn-remove-rule\" (click)=\"removeItem(group, i)\">\u2715</button>\n </div>\n </div>\n </ng-container>\n <ng-container *ngIf=\"isGroup(item)\">\n <ng-container *ngTemplateOutlet=\"groupTemplate; context: { $implicit: item, isRoot: false, parent: group, index: i }\"></ng-container>\n </ng-container>\n </div>\n </div>\n </div>\n\n </div>\n</ng-template>", styles: [":host{display:block;font-family:Segoe UI,Roboto,sans-serif;font-size:14px;color:#333}.qb-container{padding:20px;background:#fff}.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-condition-switch{display:inline-flex;border-radius:4px;overflow:hidden;background:#fff;border:1px solid #e0e0e0;height:32px}.qb-condition-switch button{border:none;background:transparent;padding:0 16px;font-weight:600;font-size:13px;cursor:pointer;color:#666;height:100%}.qb-condition-switch button.active{background:#6246a8;color:#fff}.qb-condition-switch button:hover:not(.active){background:#f5f5f5}.qb-actions{position:relative;display:flex;gap:4px}.qb-body{padding-left:28px}.qb-list{position:relative}.qb-treenode{position:relative;padding-left:24px;margin-bottom:12px}.qb-treenode:before{content:\"\";position:absolute;left:0;top:-12px;bottom:0;width:1px;border-left:1px dashed #c4c4c4}.qb-treenode:after{content:\"\";position:absolute;left:0;width:24px;height:1px;border-top:1px dashed #c4c4c4}.qb-treenode:first-child:before{top:-14px;height:calc(100% + 14px)}.qb-treenode.is-group:after{top:16px}.qb-treenode.is-group:last-child:before{height:auto;bottom:auto;top:-14px;height:30px}.qb-treenode.is-rule:after{top:26px}.qb-treenode.is-rule:last-child:before{height:auto;bottom:auto;top:-14px;height:40px}.qb-rule-card{background:#fafafa;border:1px solid #e0e0e0;border-radius:4px;padding:10px 12px;transition:.2s}.qb-rule-card:hover{border-color:#ccc}.form-row{display:flex;align-items:center;gap:12px;height:32px}.form-input{border:none;background:transparent;font-size:14px;color:#333;outline:none;height:100%}.form-input::placeholder{color:#aaa}.divider{width:1px;height:20px;background:#ddd}.btn-icon{background:transparent;border:none;cursor:pointer;font-size:18px;color:#666;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:4px}.btn-icon:hover{background:#f0f0f0;color:#6246a8}.btn-remove{color:#999}.btn-remove:hover{color:#d32f2f;background:#ffebee}.btn-remove-rule{margin-left:auto;color:#999}.btn-remove-rule:hover{color:#d32f2f;background:none}.qb-dropdown{position:absolute;top:100%;left:0;margin-top:4px;background:#fff;border:1px solid #e0e0e0;box-shadow:0 4px 12px #00000026;border-radius:4px;min-width:160px;z-index:100}.qb-dropdown button{display:block;width:100%;text-align:left;background:none;border:none;padding:10px 14px;cursor:pointer;font-size:13px;color:#333}.qb-dropdown button:hover{background:#f3f0fa;color:#6246a8}\n"] }]
|
|
69
|
-
}],
|
|
70
|
-
type: Input
|
|
71
|
-
}], groupChange: [{
|
|
72
|
-
type: Output
|
|
73
|
-
}], closeAll: [{
|
|
74
|
-
type: HostListener,
|
|
75
|
-
args: ['document:click']
|
|
76
|
-
}] } });
|
|
687
|
+
args: [{ selector: 'sd-query-builder', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgTemplateOutlet, SdOperator, SdSelect, SdInput, SdDate, SdDatetime], 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]=\"disabled()\" (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]=\"disabled()\" (click)=\"setGroupLogic(group, 'AND')\">AND</button>\n <button type=\"button\" [class.active]=\"group.logic === 'OR'\" [disabled]=\"disabled()\" (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]=\"disabled()\" (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]=\"disabled()\" (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]=\"fields()\"\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]=\"disabled()\"\n [clearable]=\"false\"\n hideInlineError\n [autoId]=\"autoIdFor('field-' + index)\"\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]=\"disabled()\"\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 (rule.operator === 'BETWEEN') {\n <div class=\"qb-between\">\n @switch (fieldOf(rule)?.type) {\n @case ('number') {\n <sd-input class=\"qb-val\" type=\"number\" placeholder=\"T\u1EEB\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [disabled]=\"disabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-input>\n <span class=\"qb-dash\">\u2014</span>\n <sd-input class=\"qb-val\" type=\"number\" placeholder=\"\u0110\u1EBFn\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [disabled]=\"disabled()\" hideInlineError [autoId]=\"autoIdFor('to-' + index)\"></sd-input>\n }\n @case ('date') {\n <sd-date class=\"qb-val\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [disabled]=\"disabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-date>\n <span class=\"qb-dash\">\u2014</span>\n <sd-date class=\"qb-val\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [disabled]=\"disabled()\" 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]=\"disabled()\" 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]=\"disabled()\" 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]=\"disabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-input>\n }\n @case ('number') {\n <sd-input class=\"qb-val\" type=\"number\" placeholder=\"Gi\u00E1 tr\u1ECB\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"disabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-input>\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]=\"disabled()\" [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]=\"disabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('datemode-' + index)\"></sd-select>\n @switch (_dateMode) {\n @case ('absolute') {\n <sd-date class=\"qb-val\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"disabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-date>\n }\n @case ('relative') {\n <sd-input class=\"qb-rel-amount\" type=\"number\" size=\"sm\" [model]=\"relativeAmount(rule)\" (modelChange)=\"setRelativeAmount(rule, $event)\" [disabled]=\"disabled()\" hideInlineError [autoId]=\"autoIdFor('rel-amount-' + index)\"></sd-input>\n <sd-select class=\"qb-rel-unit\" [items]=\"relativeUnitOptions\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"relativeUnitDirValue(rule)\" (modelChange)=\"setRelativeUnitDir(rule, $event)\" [disabled]=\"disabled()\" [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]=\"disabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('datemode-' + index)\"></sd-select>\n @switch (_dtMode) {\n @case ('absolute') {\n <sd-datetime class=\"qb-val\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"disabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-datetime>\n }\n @case ('relative') {\n <sd-input class=\"qb-rel-amount\" type=\"number\" size=\"sm\" [model]=\"relativeAmount(rule)\" (modelChange)=\"setRelativeAmount(rule, $event)\" [disabled]=\"disabled()\" hideInlineError [autoId]=\"autoIdFor('rel-amount-' + index)\"></sd-input>\n <sd-select class=\"qb-rel-unit\" [items]=\"relativeUnitOptions\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"relativeUnitDirValue(rule)\" (modelChange)=\"setRelativeUnitDir(rule, $event)\" [disabled]=\"disabled()\" [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]=\"disabled()\" [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]=\"disabled()\" (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, #2a66f4);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, #2a66f4);color:#fff}.qb-logic-switch button:hover:not(.active):not([disabled]){background:var(--sd-primary-light, #eaf1ff);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, #eaf1ff);color:var(--sd-primary, #2a66f4)}.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, #eaf1ff);color:var(--sd-primary, #2a66f4)}.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:50%;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:150px}.qb-val{min-width:140px}.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, #2a66f4);border-radius:6px;padding:12px;background:#fff}.qb-date-mode{min-width:120px}.qb-rel-amount{width:72px}.qb-rel-unit{min-width:110px}\n"] }]
|
|
688
|
+
}], ctorParameters: () => [] });
|
|
77
689
|
|
|
78
690
|
/**
|
|
79
691
|
* Generated bundle index. Do not edit.
|
|
80
692
|
*/
|
|
81
693
|
|
|
82
|
-
export { SdQueryBuilder };
|
|
694
|
+
export { QB_DATE_MODES, QB_DEFAULT_OPERATOR_BY_TYPE, QB_EMPTY_OPTIONS, QB_MULTI_OPERATORS, QB_NO_DATA_OPERATORS, QB_OPERATORS_BY_TYPE, QB_RELATIVE_UNIT_OPTIONS, SdQueryBuilder, filterToTokens, filterToTree, isQbGroup, qbAllowedOperators, qbDefaultOperator, qbDefaultRelative, qbId, qbIsMultiOperator, qbIsNoDataOperator, qbIsRelativeDate, qbNewGroup, qbNewRule, treeToFilter };
|
|
83
695
|
//# sourceMappingURL=sdcorejs-angular-components-query-builder.mjs.map
|