@platforma-sdk/ui-vue 1.65.9 → 1.66.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/.turbo/turbo-build.log +21 -21
- package/.turbo/turbo-formatter$colon$check.log +2 -2
- package/.turbo/turbo-linter$colon$check.log +2 -2
- package/.turbo/turbo-types$colon$check.log +1 -1
- package/CHANGELOG.md +17 -0
- package/dist/components/PlAdvancedFilter/FilterEditor.js.map +1 -1
- package/dist/components/PlAdvancedFilter/FilterEditor.style.js.map +1 -1
- package/dist/components/PlAdvancedFilter/FilterEditor.test.d.ts +2 -0
- package/dist/components/PlAdvancedFilter/FilterEditor.test.d.ts.map +1 -0
- package/dist/components/PlAdvancedFilter/FilterEditor.vue.d.ts.map +1 -1
- package/dist/components/PlAdvancedFilter/FilterEditor.vue2.js +142 -145
- package/dist/components/PlAdvancedFilter/FilterEditor.vue2.js.map +1 -1
- package/dist/components/PlAdvancedFilter/PlAdvancedFilter.js.map +1 -1
- package/dist/components/PlAdvancedFilter/PlAdvancedFilter.style.js.map +1 -1
- package/dist/components/PlAdvancedFilter/PlAdvancedFilter.vue.d.ts.map +1 -1
- package/dist/components/PlAdvancedFilter/PlAdvancedFilter.vue2.js.map +1 -1
- package/dist/components/PlAdvancedFilter/types.d.ts +5 -6
- package/dist/components/PlAdvancedFilter/types.d.ts.map +1 -1
- package/dist/components/PlAdvancedFilter/utils.d.ts +1 -0
- package/dist/components/PlAdvancedFilter/utils.d.ts.map +1 -1
- package/dist/components/PlAdvancedFilter/utils.js +10 -1
- package/dist/components/PlAdvancedFilter/utils.js.map +1 -1
- package/dist/components/PlAnnotations/components/AnnotationsSidebar.js.map +1 -1
- package/dist/components/PlAnnotations/components/AnnotationsSidebar.style.js.map +1 -1
- package/dist/components/PlAnnotations/components/AnnotationsSidebar.vue.d.ts +7 -10
- package/dist/components/PlAnnotations/components/AnnotationsSidebar.vue.d.ts.map +1 -1
- package/dist/components/PlAnnotations/components/AnnotationsSidebar.vue2.js +71 -50
- package/dist/components/PlAnnotations/components/AnnotationsSidebar.vue2.js.map +1 -1
- package/dist/components/PlAnnotations/components/FilterSidebar.js.map +1 -1
- package/dist/components/PlAnnotations/components/FilterSidebar.style.js.map +1 -1
- package/dist/components/PlAnnotations/components/FilterSidebar.vue.d.ts +5 -7
- package/dist/components/PlAnnotations/components/FilterSidebar.vue.d.ts.map +1 -1
- package/dist/components/PlAnnotations/components/FilterSidebar.vue2.js +81 -67
- package/dist/components/PlAnnotations/components/FilterSidebar.vue2.js.map +1 -1
- package/dist/components/PlAnnotations/components/PlAnnotations.js.map +1 -1
- package/dist/components/PlAnnotations/components/PlAnnotations.style.js.map +1 -1
- package/dist/components/PlAnnotations/components/PlAnnotations.vue.d.ts +4 -14
- package/dist/components/PlAnnotations/components/PlAnnotations.vue.d.ts.map +1 -1
- package/dist/components/PlAnnotations/components/PlAnnotations.vue2.js +43 -38
- package/dist/components/PlAnnotations/components/PlAnnotations.vue2.js.map +1 -1
- package/dist/components/PlAnnotations/components/PlAnnotationsModal.js.map +1 -1
- package/dist/components/PlAnnotations/components/PlAnnotationsModal.style.js.map +1 -1
- package/dist/components/PlAnnotations/components/PlAnnotationsModal.vue.d.ts +5 -13
- package/dist/components/PlAnnotations/components/PlAnnotationsModal.vue.d.ts.map +1 -1
- package/dist/components/PlAnnotations/components/PlAnnotationsModal.vue2.js +37 -40
- package/dist/components/PlAnnotations/components/PlAnnotationsModal.vue2.js.map +1 -1
- package/package.json +7 -7
- package/src/components/PlAdvancedFilter/FilterEditor.test.ts +315 -0
- package/src/components/PlAdvancedFilter/FilterEditor.vue +12 -18
- package/src/components/PlAdvancedFilter/PlAdvancedFilter.vue +1 -6
- package/src/components/PlAdvancedFilter/types.ts +6 -8
- package/src/components/PlAdvancedFilter/utils.ts +20 -0
- package/src/components/PlAnnotations/components/AnnotationsSidebar.vue +59 -30
- package/src/components/PlAnnotations/components/FilterSidebar.vue +65 -40
- package/src/components/PlAnnotations/components/PlAnnotations.vue +35 -19
- package/src/components/PlAnnotations/components/PlAnnotationsModal.vue +18 -21
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { mount } from "@vue/test-utils";
|
|
3
|
+
import { defineComponent, h } from "vue";
|
|
4
|
+
import { beforeAll, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
function makeStub(name: string) {
|
|
7
|
+
return defineComponent({
|
|
8
|
+
name,
|
|
9
|
+
props: ["modelValue", "options", "optionsSearch", "error", "errorStatus"],
|
|
10
|
+
emits: ["update:modelValue"],
|
|
11
|
+
setup(_, { slots }) {
|
|
12
|
+
return () => h("div", { "data-stub": name }, slots.default?.());
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
vi.mock("@milaboratories/uikit", () => ({
|
|
18
|
+
PlAutocomplete: makeStub("PlAutocomplete"),
|
|
19
|
+
PlAutocompleteMulti: makeStub("PlAutocompleteMulti"),
|
|
20
|
+
PlDropdown: makeStub("PlDropdown"),
|
|
21
|
+
PlIcon16: makeStub("PlIcon16"),
|
|
22
|
+
PlNumberField: makeStub("PlNumberField"),
|
|
23
|
+
PlTextField: makeStub("PlTextField"),
|
|
24
|
+
PlToggleSwitch: makeStub("PlToggleSwitch"),
|
|
25
|
+
Slider: makeStub("Slider"),
|
|
26
|
+
filterUiMetadata: new Proxy(
|
|
27
|
+
{},
|
|
28
|
+
{
|
|
29
|
+
get: (_t, key: string) => ({
|
|
30
|
+
label: key,
|
|
31
|
+
supportedFor: () => true,
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
let FilterEditor: typeof import("./FilterEditor.vue").default;
|
|
38
|
+
let DEFAULT_FILTERS: typeof import("./constants").DEFAULT_FILTERS;
|
|
39
|
+
let SUPPORTED_FILTER_TYPES: typeof import("./constants").SUPPORTED_FILTER_TYPES;
|
|
40
|
+
|
|
41
|
+
type EditableFilter = import("./types").EditableFilter;
|
|
42
|
+
type PlAdvancedFilterColumnId = import("./types").PlAdvancedFilterColumnId;
|
|
43
|
+
type SourceOptionInfo = import("./types").SourceOptionInfo;
|
|
44
|
+
|
|
45
|
+
beforeAll(async () => {
|
|
46
|
+
FilterEditor = (await import("./FilterEditor.vue")).default;
|
|
47
|
+
const constants = await import("./constants");
|
|
48
|
+
DEFAULT_FILTERS = constants.DEFAULT_FILTERS;
|
|
49
|
+
SUPPORTED_FILTER_TYPES = constants.SUPPORTED_FILTER_TYPES;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const columnA = "colA" as PlAdvancedFilterColumnId;
|
|
53
|
+
const columnB = "colB" as PlAdvancedFilterColumnId;
|
|
54
|
+
|
|
55
|
+
const columnOptions: SourceOptionInfo[] = [
|
|
56
|
+
{
|
|
57
|
+
id: columnA,
|
|
58
|
+
label: "Column A",
|
|
59
|
+
spec: { kind: "PColumn", name: "a", valueType: "String", axesSpec: [] },
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: columnB,
|
|
63
|
+
label: "Column B",
|
|
64
|
+
spec: { kind: "PColumn", name: "b", valueType: "Int", axesSpec: [] },
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
function mountEditor(
|
|
69
|
+
filter: EditableFilter,
|
|
70
|
+
handlers: {
|
|
71
|
+
onUpdateFilter?: (f: EditableFilter) => void;
|
|
72
|
+
onDelete?: (c: PlAdvancedFilterColumnId) => void;
|
|
73
|
+
onChangeOperand?: () => void;
|
|
74
|
+
} = {},
|
|
75
|
+
) {
|
|
76
|
+
return mount(FilterEditor, {
|
|
77
|
+
props: {
|
|
78
|
+
filter,
|
|
79
|
+
isLast: false,
|
|
80
|
+
operand: "and",
|
|
81
|
+
enableDnd: false,
|
|
82
|
+
columnOptions,
|
|
83
|
+
supportedFilters: SUPPORTED_FILTER_TYPES,
|
|
84
|
+
getSuggestOptions: () => [],
|
|
85
|
+
onDelete: handlers.onDelete ?? (() => {}),
|
|
86
|
+
onUpdateFilter: handlers.onUpdateFilter ?? (() => {}),
|
|
87
|
+
onChangeOperand: handlers.onChangeOperand ?? (() => {}),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function findDropdowns(wrapper: ReturnType<typeof mount>) {
|
|
93
|
+
return wrapper.findAllComponents({ name: "PlDropdown" });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe("FilterEditor.vue: changeFilterType", () => {
|
|
97
|
+
it("changing type from isNA to greaterThan emits filter with new type and default x", () => {
|
|
98
|
+
const onUpdateFilter = vi.fn();
|
|
99
|
+
const wrapper = mountEditor({ type: "isNA", column: columnA }, { onUpdateFilter });
|
|
100
|
+
|
|
101
|
+
// second PlDropdown (index 1) is the filter-type selector
|
|
102
|
+
const typeDropdown = findDropdowns(wrapper)[1];
|
|
103
|
+
typeDropdown.vm.$emit("update:modelValue", "greaterThan");
|
|
104
|
+
|
|
105
|
+
expect(onUpdateFilter).toHaveBeenCalledTimes(1);
|
|
106
|
+
expect(onUpdateFilter).toHaveBeenCalledWith({
|
|
107
|
+
type: "greaterThan",
|
|
108
|
+
column: columnA,
|
|
109
|
+
x: 0,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("changing type from patternEquals to greaterThan drops stale `value`", () => {
|
|
114
|
+
const onUpdateFilter = vi.fn();
|
|
115
|
+
const wrapper = mountEditor(
|
|
116
|
+
{ type: "patternEquals", column: columnA, value: "hello" },
|
|
117
|
+
{ onUpdateFilter },
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
findDropdowns(wrapper)[1].vm.$emit("update:modelValue", "greaterThan");
|
|
121
|
+
|
|
122
|
+
expect(onUpdateFilter).toHaveBeenCalledWith({
|
|
123
|
+
type: "greaterThan",
|
|
124
|
+
column: columnA,
|
|
125
|
+
x: 0,
|
|
126
|
+
});
|
|
127
|
+
expect(onUpdateFilter.mock.calls[0][0]).not.toHaveProperty("value");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("changing between numeric filters resets `x` to default (data fields are not preserved)", () => {
|
|
131
|
+
const onUpdateFilter = vi.fn();
|
|
132
|
+
const wrapper = mountEditor(
|
|
133
|
+
{ type: "greaterThan", column: columnA, x: 42 },
|
|
134
|
+
{ onUpdateFilter },
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
findDropdowns(wrapper)[1].vm.$emit("update:modelValue", "lessThan");
|
|
138
|
+
|
|
139
|
+
expect(onUpdateFilter).toHaveBeenCalledWith({
|
|
140
|
+
type: "lessThan",
|
|
141
|
+
column: columnA,
|
|
142
|
+
x: 0,
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("changing to patternFuzzyContainSubsequence populates all default fields", () => {
|
|
147
|
+
const onUpdateFilter = vi.fn();
|
|
148
|
+
const wrapper = mountEditor({ type: "isNA", column: columnA }, { onUpdateFilter });
|
|
149
|
+
|
|
150
|
+
findDropdowns(wrapper)[1].vm.$emit("update:modelValue", "patternFuzzyContainSubsequence");
|
|
151
|
+
|
|
152
|
+
expect(onUpdateFilter).toHaveBeenCalledWith({
|
|
153
|
+
...DEFAULT_FILTERS.patternFuzzyContainSubsequence,
|
|
154
|
+
column: columnA,
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("undefined newType is ignored (no emit)", () => {
|
|
159
|
+
const onUpdateFilter = vi.fn();
|
|
160
|
+
const wrapper = mountEditor({ type: "isNA", column: columnA }, { onUpdateFilter });
|
|
161
|
+
|
|
162
|
+
findDropdowns(wrapper)[1].vm.$emit("update:modelValue", undefined);
|
|
163
|
+
|
|
164
|
+
expect(onUpdateFilter).not.toHaveBeenCalled();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("preserves meta fields (id, isExpanded, isSuppressed, source) across type change", () => {
|
|
168
|
+
const onUpdateFilter = vi.fn();
|
|
169
|
+
const wrapper = mountEditor(
|
|
170
|
+
{
|
|
171
|
+
type: "patternEquals",
|
|
172
|
+
column: columnA,
|
|
173
|
+
value: "foo",
|
|
174
|
+
id: 777,
|
|
175
|
+
isExpanded: true,
|
|
176
|
+
isSuppressed: false,
|
|
177
|
+
source: "abc",
|
|
178
|
+
} as unknown as EditableFilter,
|
|
179
|
+
{ onUpdateFilter },
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
findDropdowns(wrapper)[1].vm.$emit("update:modelValue", "greaterThan");
|
|
183
|
+
|
|
184
|
+
expect(onUpdateFilter).toHaveBeenCalledWith({
|
|
185
|
+
type: "greaterThan",
|
|
186
|
+
column: columnA,
|
|
187
|
+
x: 0,
|
|
188
|
+
id: 777,
|
|
189
|
+
isExpanded: true,
|
|
190
|
+
isSuppressed: false,
|
|
191
|
+
source: "abc",
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("does not carry over unknown data fields from the old filter", () => {
|
|
196
|
+
const onUpdateFilter = vi.fn();
|
|
197
|
+
const wrapper = mountEditor({ type: "topN", column: columnA, n: 7 }, { onUpdateFilter });
|
|
198
|
+
|
|
199
|
+
findDropdowns(wrapper)[1].vm.$emit("update:modelValue", "isNA");
|
|
200
|
+
|
|
201
|
+
const emitted = onUpdateFilter.mock.calls[0][0];
|
|
202
|
+
expect(emitted).toEqual({ type: "isNA", column: columnA });
|
|
203
|
+
expect(emitted).not.toHaveProperty("n");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("FilterEditor.vue: updateFilterProp", () => {
|
|
208
|
+
it("editing `x` on greaterThan filter emits updated filter preserving type and column", () => {
|
|
209
|
+
const onUpdateFilter = vi.fn();
|
|
210
|
+
const wrapper = mountEditor({ type: "greaterThan", column: columnA, x: 0 }, { onUpdateFilter });
|
|
211
|
+
|
|
212
|
+
const numberField = wrapper.findComponent({ name: "PlNumberField" });
|
|
213
|
+
numberField.vm.$emit("update:modelValue", 99);
|
|
214
|
+
|
|
215
|
+
expect(onUpdateFilter).toHaveBeenCalledWith({
|
|
216
|
+
type: "greaterThan",
|
|
217
|
+
column: columnA,
|
|
218
|
+
x: 99,
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("editing `value` on patternEquals emits updated value", () => {
|
|
223
|
+
const onUpdateFilter = vi.fn();
|
|
224
|
+
const wrapper = mountEditor(
|
|
225
|
+
{ type: "patternEquals", column: columnA, value: "" },
|
|
226
|
+
{ onUpdateFilter },
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const autocomplete = wrapper.findComponent({ name: "PlAutocomplete" });
|
|
230
|
+
autocomplete.vm.$emit("update:modelValue", "abc");
|
|
231
|
+
|
|
232
|
+
expect(onUpdateFilter).toHaveBeenCalledWith({
|
|
233
|
+
type: "patternEquals",
|
|
234
|
+
column: columnA,
|
|
235
|
+
value: "abc",
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("editing substring `value` on patternContainSubsequence emits updated value", () => {
|
|
240
|
+
const onUpdateFilter = vi.fn();
|
|
241
|
+
const wrapper = mountEditor(
|
|
242
|
+
{ type: "patternContainSubsequence", column: columnA, value: "" },
|
|
243
|
+
{ onUpdateFilter },
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const textField = wrapper.findComponent({ name: "PlTextField" });
|
|
247
|
+
textField.vm.$emit("update:modelValue", "xyz");
|
|
248
|
+
|
|
249
|
+
expect(onUpdateFilter).toHaveBeenCalledWith({
|
|
250
|
+
type: "patternContainSubsequence",
|
|
251
|
+
column: columnA,
|
|
252
|
+
value: "xyz",
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("editing `n` on topN emits updated value", () => {
|
|
257
|
+
const onUpdateFilter = vi.fn();
|
|
258
|
+
const wrapper = mountEditor({ type: "topN", column: columnA, n: 10 }, { onUpdateFilter });
|
|
259
|
+
|
|
260
|
+
const numberField = wrapper.findComponent({ name: "PlNumberField" });
|
|
261
|
+
numberField.vm.$emit("update:modelValue", 5);
|
|
262
|
+
|
|
263
|
+
expect(onUpdateFilter).toHaveBeenCalledWith({
|
|
264
|
+
type: "topN",
|
|
265
|
+
column: columnA,
|
|
266
|
+
n: 5,
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe("FilterEditor.vue: changeSourceId", () => {
|
|
272
|
+
it("changing source to a column supporting the current filter keeps the filter type", () => {
|
|
273
|
+
const onUpdateFilter = vi.fn();
|
|
274
|
+
const wrapper = mountEditor({ type: "isNA", column: columnA }, { onUpdateFilter });
|
|
275
|
+
|
|
276
|
+
// first PlDropdown is the source selector
|
|
277
|
+
findDropdowns(wrapper)[0].vm.$emit("update:modelValue", columnB);
|
|
278
|
+
|
|
279
|
+
expect(onUpdateFilter).toHaveBeenCalledWith({
|
|
280
|
+
type: "isNA",
|
|
281
|
+
column: columnB,
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("undefined newSourceId is ignored", () => {
|
|
286
|
+
const onUpdateFilter = vi.fn();
|
|
287
|
+
const wrapper = mountEditor({ type: "isNA", column: columnA }, { onUpdateFilter });
|
|
288
|
+
|
|
289
|
+
findDropdowns(wrapper)[0].vm.$emit("update:modelValue", undefined);
|
|
290
|
+
|
|
291
|
+
expect(onUpdateFilter).not.toHaveBeenCalled();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("unknown source id is ignored", () => {
|
|
295
|
+
const onUpdateFilter = vi.fn();
|
|
296
|
+
const wrapper = mountEditor({ type: "isNA", column: columnA }, { onUpdateFilter });
|
|
297
|
+
|
|
298
|
+
findDropdowns(wrapper)[0].vm.$emit("update:modelValue", "unknown" as PlAdvancedFilterColumnId);
|
|
299
|
+
|
|
300
|
+
expect(onUpdateFilter).not.toHaveBeenCalled();
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe("FilterEditor.vue: onDelete", () => {
|
|
305
|
+
it("clicking the close icon invokes onDelete with the current column id", async () => {
|
|
306
|
+
const onDelete = vi.fn();
|
|
307
|
+
const wrapper = mountEditor({ type: "isNA", column: columnA }, { onDelete });
|
|
308
|
+
|
|
309
|
+
// the close button wraps a PlIcon16 with name="close"
|
|
310
|
+
const closeBtn = wrapper.find('[class*="closeButton"]');
|
|
311
|
+
await closeBtn.trigger("click");
|
|
312
|
+
|
|
313
|
+
expect(onDelete).toHaveBeenCalledWith(columnA);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
@@ -26,8 +26,14 @@ import type { SUPPORTED_FILTER_TYPES } from "./constants";
|
|
|
26
26
|
import { DEFAULT_FILTER_TYPE, DEFAULT_FILTERS } from "./constants";
|
|
27
27
|
import OperandButton from "./OperandButton.vue";
|
|
28
28
|
import type { EditableFilter, Operand, PlAdvancedFilterColumnId, SourceOptionInfo } from "./types";
|
|
29
|
-
import {
|
|
30
|
-
|
|
29
|
+
import {
|
|
30
|
+
getFilterInfo,
|
|
31
|
+
getNormalizedSpec,
|
|
32
|
+
isNumericFilter,
|
|
33
|
+
isPositionFilter,
|
|
34
|
+
mergeFilterForTypeChange,
|
|
35
|
+
} from "./utils";
|
|
36
|
+
import { isNil } from "es-toolkit";
|
|
31
37
|
|
|
32
38
|
const props = defineProps<{
|
|
33
39
|
filter: EditableFilter;
|
|
@@ -87,21 +93,9 @@ async function getMultiSuggestOptionsFn(
|
|
|
87
93
|
throw new Error("Invalid arguments combination");
|
|
88
94
|
}
|
|
89
95
|
|
|
90
|
-
function changeFilterType(newType
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
props.onUpdateFilter(
|
|
94
|
-
(Object.entries(defaultFilter) as Entries<EditableFilter>).reduce(
|
|
95
|
-
(res, [key, val]) => {
|
|
96
|
-
res[key] = props.filter[key] ?? val;
|
|
97
|
-
return res;
|
|
98
|
-
},
|
|
99
|
-
{ ...props.filter, type: newType } as Record<
|
|
100
|
-
keyof EditableFilter,
|
|
101
|
-
EditableFilter[keyof EditableFilter]
|
|
102
|
-
>,
|
|
103
|
-
) as EditableFilter,
|
|
104
|
-
);
|
|
96
|
+
function changeFilterType(newType?: EditableFilter["type"]) {
|
|
97
|
+
if (isNil(newType)) return;
|
|
98
|
+
props.onUpdateFilter(mergeFilterForTypeChange(props.filter, newType));
|
|
105
99
|
}
|
|
106
100
|
|
|
107
101
|
function changeSourceId(newSourceId?: PlAdvancedFilterColumnId) {
|
|
@@ -333,7 +327,7 @@ const stringMatchesError = computed(() => {
|
|
|
333
327
|
:group-position="
|
|
334
328
|
props.filter.type === 'isNA' || props.filter.type === 'isNotNA' ? 'bottom' : 'middle'
|
|
335
329
|
"
|
|
336
|
-
@update:model-value="
|
|
330
|
+
@update:model-value="changeFilterType"
|
|
337
331
|
/>
|
|
338
332
|
</div>
|
|
339
333
|
|
|
@@ -252,12 +252,7 @@ function validateFilter<T extends CommonFilter>(item: T): EditableFilter {
|
|
|
252
252
|
</template>
|
|
253
253
|
<template #item-content="{ item, index }">
|
|
254
254
|
<div
|
|
255
|
-
:class="[
|
|
256
|
-
$style.groupContent,
|
|
257
|
-
{
|
|
258
|
-
[$style.suppressedLabel]: item.isSuppressed,
|
|
259
|
-
},
|
|
260
|
-
]"
|
|
255
|
+
:class="[$style.groupContent, { [$style.suppressedLabel]: item.isSuppressed }]"
|
|
261
256
|
dropzone="true"
|
|
262
257
|
@drop="(event) => handleDropToExistingGroup(index, event)"
|
|
263
258
|
@dragover="dragOver"
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
SUniversalPColumnId,
|
|
10
10
|
} from "@platforma-sdk/model";
|
|
11
11
|
import type { SUPPORTED_FILTER_TYPES } from "./constants";
|
|
12
|
+
import { PartialBy, RequiredBy } from "@milaboratories/helpers";
|
|
12
13
|
|
|
13
14
|
export type Operand = "or" | "and";
|
|
14
15
|
|
|
@@ -52,9 +53,6 @@ export type RootFilter<Meta extends RequiredMeta = RequiredMeta> = Omit<
|
|
|
52
53
|
// or, and, not - in groups
|
|
53
54
|
export type SupportedFilterTypes = (typeof SUPPORTED_FILTER_TYPES)[number];
|
|
54
55
|
|
|
55
|
-
type RequireFields<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
|
|
56
|
-
type OptionalFields<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|
57
|
-
|
|
58
56
|
type NumericalWithOptionalN = "topN" | "bottomN";
|
|
59
57
|
type NumericalWithOptionalX =
|
|
60
58
|
| "lessThan"
|
|
@@ -64,21 +62,21 @@ type NumericalWithOptionalX =
|
|
|
64
62
|
| "equal"
|
|
65
63
|
| "notEqual";
|
|
66
64
|
type StringWithOptionalValue = "patternEquals" | "patternNotEquals";
|
|
67
|
-
// types from ui with some changed by optionality fields
|
|
68
65
|
type EditedTypes =
|
|
69
66
|
| "patternFuzzyContainSubsequence"
|
|
70
67
|
| NumericalWithOptionalX
|
|
71
68
|
| StringWithOptionalValue
|
|
72
69
|
| NumericalWithOptionalN;
|
|
70
|
+
|
|
73
71
|
export type EditableFilter =
|
|
74
72
|
| Exclude<FilterLeafContent, { type: EditedTypes }>
|
|
75
|
-
|
|
|
73
|
+
| RequiredBy<
|
|
76
74
|
Extract<FilterLeafContent, { type: "patternFuzzyContainSubsequence" }>,
|
|
77
75
|
"maxEdits" | "substitutionsOnly"
|
|
78
76
|
>
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
|
|
|
77
|
+
| PartialBy<Extract<FilterLeafContent, { type: NumericalWithOptionalN }>, "n">
|
|
78
|
+
| PartialBy<Extract<FilterLeafContent, { type: NumericalWithOptionalX }>, "x">
|
|
79
|
+
| PartialBy<Extract<FilterLeafContent, { type: StringWithOptionalValue }>, "value">;
|
|
82
80
|
|
|
83
81
|
export type FixedAxisInfo = {
|
|
84
82
|
idx: number;
|
|
@@ -148,3 +148,23 @@ export function isValidColumnId(id: unknown): id is PlAdvancedFilterColumnId {
|
|
|
148
148
|
return false;
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
+
|
|
152
|
+
export function mergeFilterForTypeChange(
|
|
153
|
+
oldFilter: EditableFilter,
|
|
154
|
+
newType: SupportedFilterTypes,
|
|
155
|
+
): EditableFilter {
|
|
156
|
+
const oldDefault = DEFAULT_FILTERS[oldFilter.type] as Record<string, unknown>;
|
|
157
|
+
const newDefault = DEFAULT_FILTERS[newType];
|
|
158
|
+
|
|
159
|
+
const stripped = { ...oldFilter } as Record<string, unknown>;
|
|
160
|
+
for (const key of Object.keys(oldDefault)) {
|
|
161
|
+
if (key === "type" || key === "column") continue;
|
|
162
|
+
delete stripped[key];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
...newDefault,
|
|
167
|
+
...stripped,
|
|
168
|
+
type: newType,
|
|
169
|
+
} as EditableFilter;
|
|
170
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import $commonStyle from "./style.module.css";
|
|
3
3
|
|
|
4
|
+
import { produce } from "immer";
|
|
4
5
|
import { randomInt } from "@milaboratories/helpers";
|
|
5
6
|
import {
|
|
6
7
|
PlBtnGhost,
|
|
@@ -14,26 +15,53 @@ import type { Annotation } from "../types";
|
|
|
14
15
|
import { validateTitle } from "../utils";
|
|
15
16
|
import { isEmpty } from "es-toolkit/compat";
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
annotation: Annotation;
|
|
20
|
+
selectedStepId: undefined | number;
|
|
21
|
+
onUpdateAnnotation: (annotation: Annotation) => void;
|
|
22
|
+
onUpdateSelectedStepId: (id: undefined | number) => void;
|
|
23
|
+
}>();
|
|
24
|
+
|
|
21
25
|
const emits = defineEmits<{
|
|
22
26
|
(e: "delete-schema"): void;
|
|
23
27
|
}>();
|
|
24
|
-
|
|
28
|
+
|
|
29
|
+
function produceAnnotationUpdate(updater: (draft: Annotation) => void) {
|
|
30
|
+
props.onUpdateAnnotation(produce(props.annotation, updater));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function updateTitle(title: string) {
|
|
34
|
+
produceAnnotationUpdate((draft) => {
|
|
35
|
+
draft.title = title;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function updateSteps(steps: Annotation["steps"]) {
|
|
40
|
+
produceAnnotationUpdate((draft) => {
|
|
41
|
+
draft.steps = steps;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function updateDefaultValue(value: string) {
|
|
46
|
+
produceAnnotationUpdate((draft) => {
|
|
47
|
+
draft.defaultValue = value === "" ? undefined : value;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
25
51
|
function handleAddStep() {
|
|
26
52
|
const id = randomInt();
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
53
|
+
produceAnnotationUpdate((draft) => {
|
|
54
|
+
draft.steps.push({
|
|
55
|
+
id,
|
|
56
|
+
label: "",
|
|
57
|
+
filter: {
|
|
58
|
+
id: randomInt(),
|
|
59
|
+
type: "and",
|
|
60
|
+
filters: [],
|
|
61
|
+
},
|
|
62
|
+
});
|
|
35
63
|
});
|
|
36
|
-
|
|
64
|
+
props.onUpdateSelectedStepId(id);
|
|
37
65
|
}
|
|
38
66
|
</script>
|
|
39
67
|
|
|
@@ -41,28 +69,29 @@ function handleAddStep() {
|
|
|
41
69
|
<PlSidebarItem>
|
|
42
70
|
<template #header-content>
|
|
43
71
|
<PlEditableTitle
|
|
44
|
-
|
|
45
|
-
:class="{ [$commonStyle.flashing]: annotation.title.length === 0 }"
|
|
72
|
+
:model-value="props.annotation.title"
|
|
73
|
+
:class="{ [$commonStyle.flashing]: props.annotation.title.length === 0 }"
|
|
46
74
|
:max-length="40"
|
|
47
75
|
max-width="600px"
|
|
48
76
|
placeholder="Annotation Title"
|
|
49
|
-
:autofocus="annotation.title.length === 0"
|
|
77
|
+
:autofocus="props.annotation.title.length === 0"
|
|
50
78
|
:validate="validateTitle"
|
|
79
|
+
@update:model-value="updateTitle"
|
|
51
80
|
/>
|
|
52
81
|
</template>
|
|
53
|
-
<template v-if="annotation" #body-content>
|
|
54
|
-
<div :class="[$style.root, { [$commonStyle.disabled]: annotation.title.length === 0 }]">
|
|
55
|
-
<span :class="$style.tip"
|
|
56
|
-
|
|
57
|
-
>
|
|
58
|
-
|
|
82
|
+
<template v-if="props.annotation" #body-content>
|
|
83
|
+
<div :class="[$style.root, { [$commonStyle.disabled]: props.annotation.title.length === 0 }]">
|
|
84
|
+
<span :class="$style.tip">
|
|
85
|
+
Above annotations override the ones below. Rearrange them by dragging.
|
|
86
|
+
</span>
|
|
59
87
|
<PlElementList
|
|
60
|
-
|
|
88
|
+
:items="props.annotation.steps"
|
|
61
89
|
:get-item-key="(item) => item.id"
|
|
62
|
-
:is-active="(item) => item.id === selectedStepId"
|
|
90
|
+
:is-active="(item) => item.id === props.selectedStepId"
|
|
63
91
|
:item-class="$style.stepItem"
|
|
64
92
|
:class="$style.steps"
|
|
65
|
-
@
|
|
93
|
+
@update:items="updateSteps"
|
|
94
|
+
@item-click="(item) => props.onUpdateSelectedStepId(item.id)"
|
|
66
95
|
>
|
|
67
96
|
<template #item-title="{ item }">
|
|
68
97
|
{{ item.label }}
|
|
@@ -74,15 +103,15 @@ function handleAddStep() {
|
|
|
74
103
|
<PlTextField
|
|
75
104
|
:class="[
|
|
76
105
|
$style.defaultValue,
|
|
77
|
-
{ [$style.emptyDefaultValue]: isEmpty(annotation.defaultValue) },
|
|
106
|
+
{ [$style.emptyDefaultValue]: isEmpty(props.annotation.defaultValue) },
|
|
78
107
|
]"
|
|
79
|
-
:model-value="annotation.defaultValue ?? ''"
|
|
108
|
+
:model-value="props.annotation.defaultValue ?? ''"
|
|
80
109
|
label="Label remaining with"
|
|
81
110
|
placeholder="No label"
|
|
82
111
|
clearable
|
|
83
112
|
helper="This label will be applied to the remaining rows, after all other filters are applied."
|
|
84
113
|
@click.stop
|
|
85
|
-
@update:model-value="
|
|
114
|
+
@update:model-value="updateDefaultValue"
|
|
86
115
|
/>
|
|
87
116
|
</div>
|
|
88
117
|
</template>
|
|
@@ -90,7 +119,7 @@ function handleAddStep() {
|
|
|
90
119
|
<PlBtnGhost
|
|
91
120
|
icon="delete-bin"
|
|
92
121
|
reverse
|
|
93
|
-
:disabled="annotation.steps.length === 0"
|
|
122
|
+
:disabled="props.annotation.steps.length === 0"
|
|
94
123
|
@click.stop="emits('delete-schema')"
|
|
95
124
|
>
|
|
96
125
|
Delete Schema
|