@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.
Files changed (56) hide show
  1. package/.turbo/turbo-build.log +21 -21
  2. package/.turbo/turbo-formatter$colon$check.log +2 -2
  3. package/.turbo/turbo-linter$colon$check.log +2 -2
  4. package/.turbo/turbo-types$colon$check.log +1 -1
  5. package/CHANGELOG.md +17 -0
  6. package/dist/components/PlAdvancedFilter/FilterEditor.js.map +1 -1
  7. package/dist/components/PlAdvancedFilter/FilterEditor.style.js.map +1 -1
  8. package/dist/components/PlAdvancedFilter/FilterEditor.test.d.ts +2 -0
  9. package/dist/components/PlAdvancedFilter/FilterEditor.test.d.ts.map +1 -0
  10. package/dist/components/PlAdvancedFilter/FilterEditor.vue.d.ts.map +1 -1
  11. package/dist/components/PlAdvancedFilter/FilterEditor.vue2.js +142 -145
  12. package/dist/components/PlAdvancedFilter/FilterEditor.vue2.js.map +1 -1
  13. package/dist/components/PlAdvancedFilter/PlAdvancedFilter.js.map +1 -1
  14. package/dist/components/PlAdvancedFilter/PlAdvancedFilter.style.js.map +1 -1
  15. package/dist/components/PlAdvancedFilter/PlAdvancedFilter.vue.d.ts.map +1 -1
  16. package/dist/components/PlAdvancedFilter/PlAdvancedFilter.vue2.js.map +1 -1
  17. package/dist/components/PlAdvancedFilter/types.d.ts +5 -6
  18. package/dist/components/PlAdvancedFilter/types.d.ts.map +1 -1
  19. package/dist/components/PlAdvancedFilter/utils.d.ts +1 -0
  20. package/dist/components/PlAdvancedFilter/utils.d.ts.map +1 -1
  21. package/dist/components/PlAdvancedFilter/utils.js +10 -1
  22. package/dist/components/PlAdvancedFilter/utils.js.map +1 -1
  23. package/dist/components/PlAnnotations/components/AnnotationsSidebar.js.map +1 -1
  24. package/dist/components/PlAnnotations/components/AnnotationsSidebar.style.js.map +1 -1
  25. package/dist/components/PlAnnotations/components/AnnotationsSidebar.vue.d.ts +7 -10
  26. package/dist/components/PlAnnotations/components/AnnotationsSidebar.vue.d.ts.map +1 -1
  27. package/dist/components/PlAnnotations/components/AnnotationsSidebar.vue2.js +71 -50
  28. package/dist/components/PlAnnotations/components/AnnotationsSidebar.vue2.js.map +1 -1
  29. package/dist/components/PlAnnotations/components/FilterSidebar.js.map +1 -1
  30. package/dist/components/PlAnnotations/components/FilterSidebar.style.js.map +1 -1
  31. package/dist/components/PlAnnotations/components/FilterSidebar.vue.d.ts +5 -7
  32. package/dist/components/PlAnnotations/components/FilterSidebar.vue.d.ts.map +1 -1
  33. package/dist/components/PlAnnotations/components/FilterSidebar.vue2.js +81 -67
  34. package/dist/components/PlAnnotations/components/FilterSidebar.vue2.js.map +1 -1
  35. package/dist/components/PlAnnotations/components/PlAnnotations.js.map +1 -1
  36. package/dist/components/PlAnnotations/components/PlAnnotations.style.js.map +1 -1
  37. package/dist/components/PlAnnotations/components/PlAnnotations.vue.d.ts +4 -14
  38. package/dist/components/PlAnnotations/components/PlAnnotations.vue.d.ts.map +1 -1
  39. package/dist/components/PlAnnotations/components/PlAnnotations.vue2.js +43 -38
  40. package/dist/components/PlAnnotations/components/PlAnnotations.vue2.js.map +1 -1
  41. package/dist/components/PlAnnotations/components/PlAnnotationsModal.js.map +1 -1
  42. package/dist/components/PlAnnotations/components/PlAnnotationsModal.style.js.map +1 -1
  43. package/dist/components/PlAnnotations/components/PlAnnotationsModal.vue.d.ts +5 -13
  44. package/dist/components/PlAnnotations/components/PlAnnotationsModal.vue.d.ts.map +1 -1
  45. package/dist/components/PlAnnotations/components/PlAnnotationsModal.vue2.js +37 -40
  46. package/dist/components/PlAnnotations/components/PlAnnotationsModal.vue2.js.map +1 -1
  47. package/package.json +7 -7
  48. package/src/components/PlAdvancedFilter/FilterEditor.test.ts +315 -0
  49. package/src/components/PlAdvancedFilter/FilterEditor.vue +12 -18
  50. package/src/components/PlAdvancedFilter/PlAdvancedFilter.vue +1 -6
  51. package/src/components/PlAdvancedFilter/types.ts +6 -8
  52. package/src/components/PlAdvancedFilter/utils.ts +20 -0
  53. package/src/components/PlAnnotations/components/AnnotationsSidebar.vue +59 -30
  54. package/src/components/PlAnnotations/components/FilterSidebar.vue +65 -40
  55. package/src/components/PlAnnotations/components/PlAnnotations.vue +35 -19
  56. 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 { getFilterInfo, getNormalizedSpec, isNumericFilter, isPositionFilter } from "./utils";
30
- import { Entries } from "@milaboratories/helpers";
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: EditableFilter["type"]) {
91
- const defaultFilter = DEFAULT_FILTERS[newType];
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="(v) => changeFilterType(v!)"
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
- | RequireFields<
73
+ | RequiredBy<
76
74
  Extract<FilterLeafContent, { type: "patternFuzzyContainSubsequence" }>,
77
75
  "maxEdits" | "substitutionsOnly"
78
76
  >
79
- | OptionalFields<Extract<FilterLeafContent, { type: NumericalWithOptionalN }>, "n">
80
- | OptionalFields<Extract<FilterLeafContent, { type: NumericalWithOptionalX }>, "x">
81
- | OptionalFields<Extract<FilterLeafContent, { type: StringWithOptionalValue }>, "value">;
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
- // Models
18
- const annotation = defineModel<Annotation>("annotation", { required: true });
19
- const selectedStepId = defineModel<undefined | number>("selectedStepId");
20
- // Emits
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
- // Actions
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
- annotation.value.steps.push({
28
- id,
29
- label: "",
30
- filter: {
31
- id: randomInt(),
32
- type: "and",
33
- filters: [],
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
- selectedStepId.value = id;
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
- v-model="annotation.title"
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
- >Above annotations override the ones below. Rearrange them by dragging.</span
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
- v-model:items="annotation.steps"
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
- @item-click="(item) => (selectedStepId = item.id)"
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="annotation.defaultValue = $event === '' ? undefined : $event"
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