@platforma-sdk/ui-vue 1.14.0 → 1.14.2

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 (29) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/lib.js +11921 -10921
  3. package/dist/lib.umd.cjs +37 -37
  4. package/dist/src/components/PlTableFilters/PlTableAddFilter.vue.d.ts +20 -0
  5. package/dist/src/components/PlTableFilters/PlTableAddFilter.vue.d.ts.map +1 -0
  6. package/dist/src/components/PlTableFilters/PlTableFilterEntry.vue.d.ts +16 -0
  7. package/dist/src/components/PlTableFilters/PlTableFilterEntry.vue.d.ts.map +1 -0
  8. package/dist/src/components/{PlAgDataTable → PlTableFilters}/PlTableFilters.vue.d.ts +2 -1
  9. package/dist/src/components/PlTableFilters/PlTableFilters.vue.d.ts.map +1 -0
  10. package/dist/src/components/PlTableFilters/filters_logic.d.ts +18 -0
  11. package/dist/src/components/PlTableFilters/filters_logic.d.ts.map +1 -0
  12. package/dist/src/components/PlTableFilters/index.d.ts +2 -0
  13. package/dist/src/components/PlTableFilters/index.d.ts.map +1 -0
  14. package/dist/src/lib.d.ts +2 -2
  15. package/dist/src/lib.d.ts.map +1 -1
  16. package/dist/style.css +1 -1
  17. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  18. package/package.json +5 -3
  19. package/src/components/PlAgDataTable/pl-table-filters.scss +98 -0
  20. package/src/components/PlAgGridColumnManager/PlAgGridColumnManager.vue +1 -1
  21. package/src/components/PlTableFilters/PlTableAddFilter.vue +60 -0
  22. package/src/components/PlTableFilters/PlTableFilterEntry.vue +61 -0
  23. package/src/components/PlTableFilters/PlTableFilters.vue +249 -0
  24. package/src/components/PlTableFilters/filters_logic.ts +347 -0
  25. package/src/components/PlTableFilters/index.ts +1 -0
  26. package/src/components/PlTableFilters/pl-table-filters.scss +98 -0
  27. package/src/lib.ts +3 -2
  28. package/dist/src/components/PlAgDataTable/PlTableFilters.vue.d.ts.map +0 -1
  29. package/src/components/PlAgDataTable/PlTableFilters.vue +0 -559
@@ -1,559 +0,0 @@
1
- <script lang="ts" setup>
2
- import type { ListOption } from '@milaboratories/uikit';
3
- import { PlCheckbox, PlDropdown, PlTextField, PlToggleSwitch, Slider } from '@milaboratories/uikit';
4
- import { computed, reactive, toRefs, watch } from 'vue';
5
- import canonicalize from 'canonicalize';
6
- import type {
7
- PlTableFiltersModel,
8
- PTableRecordFilter,
9
- SingleValuePredicateV2,
10
- PlTableFilterType,
11
- PlTableFilterNumberType,
12
- PlTableFilterStringType,
13
- PlTableFilter,
14
- PTableColumnSpec,
15
- PTableColumnId,
16
- } from '@platforma-sdk/model';
17
- import * as lodash from 'lodash';
18
- import type { PlTableFiltersDefault, PlTableFiltersRestriction } from './types';
19
-
20
- const model = defineModel<PlTableFiltersModel>({ required: true });
21
- const props = defineProps<{
22
- columns: Readonly<PTableColumnSpec[]>;
23
- restrictions?: Readonly<PlTableFiltersRestriction[]>;
24
- defaults?: Readonly<PlTableFiltersDefault[]>;
25
- }>();
26
- const { columns, restrictions, defaults } = toRefs(props);
27
-
28
- const makeColumnId = (column: PTableColumnId | PTableColumnSpec): string => canonicalize(column.id)!;
29
- const columnsWithIds = computed(() => {
30
- return columns.value
31
- .filter((column) => {
32
- const type = column.type;
33
- switch (type) {
34
- case 'axis':
35
- return column.spec.type !== 'Bytes';
36
- case 'column':
37
- return column.spec.valueType !== 'Bytes';
38
- default:
39
- throw Error(`unsupported data type: ${type satisfies never}`);
40
- }
41
- })
42
- .map((column) => ({
43
- column,
44
- id: makeColumnId(column),
45
- }));
46
- });
47
- const restrictionsMap = computed(() => {
48
- const restrictionsValue = restrictions.value ?? [];
49
- const map: Record<string, PlTableFilterType[]> = {};
50
- for (const { column, id } of columnsWithIds.value) {
51
- const entry = lodash.find(restrictionsValue, (entry) => lodash.isEqual(entry.column.id, column.id));
52
- if (entry !== undefined) {
53
- map[id] = entry.allowedFilterTypes;
54
- }
55
- }
56
- return map;
57
- });
58
- const defaultsMap = computed(() => {
59
- const defaultsValue = defaults.value ?? [];
60
- const map: Record<string, PlTableFilter> = {};
61
- for (const { column, id } of columnsWithIds.value) {
62
- const entry = lodash.find(defaultsValue, (entry) => lodash.isEqual(entry.column.id, column.id));
63
- if (entry !== undefined) {
64
- map[id] = entry.default;
65
- }
66
- }
67
- return map;
68
- });
69
-
70
- const makeState = (state?: Record<string, PlTableFilter>): Record<string, PlTableFilter> => {
71
- if (state !== undefined) return state;
72
- return defaultsMap.value;
73
- };
74
- const reactiveModel = reactive({ state: makeState(model.value.state) });
75
- watch(
76
- () => model.value,
77
- (model) => {
78
- if (!lodash.isEqual(reactiveModel.state, model.state)) {
79
- reactiveModel.state = makeState(model.state);
80
- }
81
- },
82
- );
83
-
84
- watch(
85
- () => columnsWithIds.value,
86
- (columnsWithIds) => {
87
- if (reactiveModel.state !== undefined && columnsWithIds.length === 0) return;
88
-
89
- const currentState = reactiveModel.state ?? {};
90
- const newState: Record<string, PlTableFilter> = {};
91
- for (const { id } of columnsWithIds) {
92
- if (currentState[id] !== undefined) newState[id] = currentState[id];
93
- }
94
- reactiveModel.state = newState;
95
- },
96
- { immediate: true },
97
- );
98
-
99
- const getFilterLabel = (type: PlTableFilterType): string => {
100
- switch (type) {
101
- case 'isNotNA':
102
- return 'Is not NA';
103
- case 'isNA':
104
- return 'Is NA';
105
- case 'number_equals':
106
- case 'string_equals':
107
- return 'Equals';
108
- case 'number_notEquals':
109
- case 'string_notEquals':
110
- return 'Not equals';
111
- case 'number_greaterThan':
112
- return 'Greater than';
113
- case 'number_greaterThanOrEqualTo':
114
- return 'Greater than or equal to';
115
- case 'number_lessThan':
116
- return 'Less than';
117
- case 'number_lessThanOrEqualTo':
118
- return 'Less than or equal to';
119
- case 'number_between':
120
- return 'Between';
121
- case 'string_contains':
122
- return 'Contains';
123
- case 'string_doesNotContain':
124
- return 'Does not contain';
125
- case 'string_matches':
126
- return 'Matches';
127
- case 'string_doesNotMatch':
128
- return 'Does not match';
129
- case 'string_containsFuzzyMatch':
130
- return 'Contains fuzzy match';
131
- default:
132
- throw Error(`unsupported filter type: ${type satisfies never}`);
133
- }
134
- };
135
-
136
- const filterTypesNumber: PlTableFilterNumberType[] = [
137
- 'isNotNA',
138
- 'isNA',
139
- 'number_equals',
140
- 'number_notEquals',
141
- 'number_greaterThan',
142
- 'number_greaterThanOrEqualTo',
143
- 'number_lessThan',
144
- 'number_lessThanOrEqualTo',
145
- 'number_between',
146
- ] as const;
147
- const filterTypesString: PlTableFilterStringType[] = [
148
- 'isNotNA',
149
- 'isNA',
150
- 'string_equals',
151
- 'string_notEquals',
152
- 'string_contains',
153
- 'string_doesNotContain',
154
- 'string_matches',
155
- 'string_doesNotMatch',
156
- 'string_containsFuzzyMatch',
157
- ] as const;
158
- const filterOptions = computed(() => {
159
- const restrictionsMapValue = restrictionsMap.value;
160
- const map: Record<string, ListOption<PlTableFilterType>[]> = {};
161
- for (const { column, id } of columnsWithIds.value) {
162
- const valueType = column.type === 'column' ? column.spec.valueType : column.spec.type;
163
- let types: PlTableFilterType[] = valueType === 'String' ? filterTypesString : filterTypesNumber;
164
-
165
- const restrictionsEntry = restrictionsMapValue[id];
166
- if (restrictionsEntry !== undefined) {
167
- types = types.filter((type) => restrictionsEntry.includes(type));
168
- }
169
-
170
- map[id] = types.map((type) => ({
171
- value: type,
172
- text: getFilterLabel(type),
173
- }));
174
- }
175
- return map;
176
- });
177
- const filterOptionsPresent = computed(() => {
178
- return lodash.some(Object.values(filterOptions.value), (options) => options.length > 0);
179
- });
180
-
181
- const getFilterReference = (filter: PlTableFilter): undefined | number | string => {
182
- const type = filter.type;
183
- switch (type) {
184
- case 'isNotNA':
185
- case 'isNA':
186
- return undefined;
187
- case 'number_equals':
188
- case 'number_notEquals':
189
- case 'number_greaterThan':
190
- case 'number_greaterThanOrEqualTo':
191
- case 'number_lessThan':
192
- case 'number_lessThanOrEqualTo':
193
- return filter.reference;
194
- case 'number_between':
195
- return filter.lowerBound;
196
- case 'string_equals':
197
- case 'string_notEquals':
198
- case 'string_contains':
199
- case 'string_doesNotContain':
200
- case 'string_matches':
201
- case 'string_doesNotMatch':
202
- case 'string_containsFuzzyMatch':
203
- return filter.reference;
204
- default:
205
- throw Error(`unsupported filter type: ${type satisfies never}`);
206
- }
207
- };
208
- const getFilterDefault = (type: PlTableFilterType, reference?: undefined | number | string): PlTableFilter => {
209
- switch (type) {
210
- case 'isNotNA':
211
- case 'isNA':
212
- return { type };
213
- case 'number_equals':
214
- case 'number_notEquals':
215
- case 'number_greaterThan':
216
- case 'number_greaterThanOrEqualTo':
217
- case 'number_lessThan':
218
- case 'number_lessThanOrEqualTo':
219
- return { type, reference: typeof reference === 'number' ? reference : 0 };
220
- case 'number_between':
221
- return {
222
- type,
223
- lowerBound: typeof reference === 'number' ? reference : 0,
224
- includeLowerBound: true,
225
- upperBound: 100,
226
- includeUpperBound: false,
227
- };
228
- case 'string_equals':
229
- case 'string_notEquals':
230
- case 'string_contains':
231
- case 'string_doesNotContain':
232
- case 'string_matches':
233
- case 'string_doesNotMatch':
234
- return { type, reference: typeof reference === 'string' ? reference : '' };
235
- case 'string_containsFuzzyMatch':
236
- return {
237
- type,
238
- reference: typeof reference === 'string' ? reference : '',
239
- maxEdits: 2,
240
- substitutionsOnly: false,
241
- wildcard: undefined,
242
- };
243
- default:
244
- throw Error(`unsupported filter type: ${type satisfies never}`);
245
- }
246
- };
247
- const updateColumnFilter = (columnId: string, type: PlTableFilterType): void => {
248
- const prevFilter = reactiveModel.state![columnId];
249
- reactiveModel.state![columnId] = getFilterDefault(type, getFilterReference(prevFilter));
250
- };
251
- const resetColumnFilter = (columnId: string) => {
252
- reactiveModel.state![columnId] = defaultsMap.value[columnId] ?? getFilterDefault(filterOptions.value[columnId][0].value);
253
- };
254
- const onFilterActiveChanged = (columnId: string, checked: boolean) => {
255
- if (checked) {
256
- resetColumnFilter(columnId);
257
- } else {
258
- delete reactiveModel.state![columnId];
259
- }
260
- };
261
-
262
- const parseNumber = (column: PTableColumnSpec, value: string): number => {
263
- const parsed = Number(value);
264
- if (!Number.isFinite(parsed)) throw Error('Model value is not a number.');
265
-
266
- const type = column.type === 'column' ? column.spec.valueType : column.spec.type;
267
- if ((type === 'Int' || type === 'Long') && !Number.isInteger(parsed)) throw Error('Model value is not an integer.');
268
-
269
- const min = column.spec.annotations?.['pl7.app/min'];
270
- if (min !== undefined) {
271
- const minValue = Number(min);
272
- if (Number.isFinite(minValue) && parsed < Number(min)) {
273
- throw Error('Model value is too low.');
274
- }
275
- }
276
-
277
- const max = column.spec.annotations?.['pl7.app/max'];
278
- if (max !== undefined) {
279
- const maxValue = Number(max);
280
- if (Number.isFinite(maxValue) && parsed > Number(max)) {
281
- throw Error('Model value is too high.');
282
- }
283
- }
284
-
285
- return parsed;
286
- };
287
- const parseString = (column: PTableColumnSpec, value: string): string => {
288
- const alphabet = column.spec.domain?.['pl7.app/alphabet'] ?? column.spec.annotations?.['pl7.app/alphabet'];
289
- if (alphabet === 'nucleotide' && !/^[AaTtGgCcNn]+$/.test(value)) throw Error('Model value is not a nucleotide.');
290
- if (alphabet === 'aminoacid' && !/^[AaCcDdEeFfGgHhIiKkLlMmNnPpQqRrSsTtVvWwYyXx]+$/.test(value)) throw Error('Model value is not an aminoacid.');
291
-
292
- return value;
293
- };
294
- const parseRegex = (value: string): string => {
295
- try {
296
- new RegExp(value);
297
- return value;
298
- } catch (err: unknown) {
299
- if (err instanceof SyntaxError) throw Error('Model value is not a regexp.');
300
- throw err;
301
- }
302
- };
303
- const makeWildcardOptions = (column: PTableColumnSpec, reference: string) => {
304
- const alphabet = column.spec.domain?.['pl7.app/alphabet'] ?? column.spec.annotations?.['pl7.app/alphabet'];
305
- if (alphabet === 'nucleotide') {
306
- return [
307
- {
308
- value: 'N',
309
- text: 'N',
310
- },
311
- ];
312
- }
313
- if (alphabet === 'aminoacid') {
314
- return [
315
- {
316
- value: 'X',
317
- text: 'X',
318
- },
319
- ];
320
- }
321
-
322
- const chars = lodash.uniq(reference);
323
- chars.sort();
324
- return chars.map((char) => ({
325
- value: char,
326
- text: char,
327
- }));
328
- };
329
-
330
- const makePredicate = (filter: PlTableFilter): SingleValuePredicateV2 => {
331
- const type = filter.type;
332
- switch (type) {
333
- case 'isNotNA':
334
- return {
335
- operator: 'Not',
336
- operand: {
337
- operator: 'IsNA',
338
- },
339
- };
340
- case 'isNA':
341
- return {
342
- operator: 'IsNA',
343
- };
344
- case 'number_equals':
345
- case 'string_equals':
346
- return {
347
- operator: 'Equal',
348
- reference: filter.reference,
349
- };
350
- case 'number_notEquals':
351
- case 'string_notEquals':
352
- return {
353
- operator: 'Not',
354
- operand: {
355
- operator: 'Equal',
356
- reference: filter.reference,
357
- },
358
- };
359
- case 'number_greaterThan':
360
- return {
361
- operator: 'Greater',
362
- reference: filter.reference,
363
- };
364
- case 'number_greaterThanOrEqualTo':
365
- return {
366
- operator: 'GreaterOrEqual',
367
- reference: filter.reference,
368
- };
369
- case 'number_lessThan':
370
- return {
371
- operator: 'Less',
372
- reference: filter.reference,
373
- };
374
- case 'number_lessThanOrEqualTo':
375
- return {
376
- operator: 'LessOrEqual',
377
- reference: filter.reference,
378
- };
379
- case 'number_between':
380
- return {
381
- operator: 'And',
382
- operands: [
383
- {
384
- operator: filter.includeLowerBound ? 'GreaterOrEqual' : 'Greater',
385
- reference: filter.lowerBound,
386
- },
387
- {
388
- operator: filter.includeUpperBound ? 'LessOrEqual' : 'Less',
389
- reference: filter.upperBound,
390
- },
391
- ],
392
- };
393
- case 'string_contains':
394
- return {
395
- operator: 'StringContains',
396
- substring: filter.reference,
397
- };
398
- case 'string_doesNotContain':
399
- return {
400
- operator: 'Not',
401
- operand: {
402
- operator: 'StringContains',
403
- substring: filter.reference,
404
- },
405
- };
406
- case 'string_matches':
407
- return {
408
- operator: 'Matches',
409
- regex: filter.reference,
410
- };
411
- case 'string_doesNotMatch':
412
- return {
413
- operator: 'Not',
414
- operand: {
415
- operator: 'Matches',
416
- regex: filter.reference,
417
- },
418
- };
419
- case 'string_containsFuzzyMatch':
420
- return {
421
- operator: 'StringContainsFuzzy',
422
- reference: filter.reference,
423
- maxEdits: filter.maxEdits,
424
- substitutionsOnly: filter.substitutionsOnly,
425
- wildcard: filter.wildcard,
426
- };
427
- default:
428
- throw Error(`unsupported filter type: ${type satisfies never}`);
429
- }
430
- };
431
- const makeFilters = (state: Record<string, PlTableFilter>): PTableRecordFilter[] => {
432
- return columnsWithIds.value
433
- .map(({ column, id }) => {
434
- if (!(id in state)) return undefined;
435
-
436
- const predicate = makePredicate(state[id]);
437
- const { spec, ...columnId } = column;
438
- const _ = spec;
439
-
440
- return {
441
- type: 'bySingleColumnV2',
442
- column: columnId,
443
- predicate,
444
- } satisfies PTableRecordFilter;
445
- })
446
- .filter((entry) => entry !== undefined);
447
- };
448
-
449
- watch(
450
- () => reactiveModel,
451
- (reactiveModel) => {
452
- if (!lodash.isEqual(reactiveModel.state, model.value.state)) {
453
- model.value = {
454
- state: lodash.cloneDeep(reactiveModel.state),
455
- filters: makeFilters(reactiveModel.state),
456
- };
457
- }
458
- },
459
- {
460
- immediate: true,
461
- deep: true,
462
- },
463
- );
464
- </script>
465
-
466
- <template>
467
- <div v-for="({ column, id }, i) in columnsWithIds" :key="id">
468
- <form v-if="filterOptions[id].length > 0" class="d-flex gap-10 flex-column">
469
- <PlCheckbox :model-value="!!reactiveModel.state[id]" @update:model-value="(checked) => onFilterActiveChanged(id, checked)">
470
- {{ column.spec.annotations?.['pl7.app/label']?.trim() ?? 'Unlabeled ' + column.type + ' ' + i.toString() }}
471
- </PlCheckbox>
472
- <div class="controls d-flex gap-10 flex-column" :class="{ open: !!reactiveModel.state[id] }">
473
- <PlDropdown
474
- v-if="reactiveModel.state[id]"
475
- :model-value="reactiveModel.state[id]!.type"
476
- :options="filterOptions[id]"
477
- label="Predicate"
478
- @update:model-value="(type) => updateColumnFilter(id, type!)"
479
- />
480
- <template
481
- v-if="
482
- reactiveModel.state[id]?.type === 'number_equals' ||
483
- reactiveModel.state[id]?.type === 'number_notEquals' ||
484
- reactiveModel.state[id]?.type === 'number_lessThan' ||
485
- reactiveModel.state[id]?.type === 'number_lessThanOrEqualTo' ||
486
- reactiveModel.state[id]?.type === 'number_greaterThan' ||
487
- reactiveModel.state[id]?.type === 'number_greaterThanOrEqualTo'
488
- "
489
- >
490
- <PlTextField
491
- v-model="reactiveModel.state[id].reference"
492
- :parse="(value: string): number => parseNumber(column, value)"
493
- label="Reference value"
494
- />
495
- </template>
496
- <template v-if="reactiveModel.state[id]?.type === 'number_between'">
497
- <PlTextField
498
- v-model="reactiveModel.state[id].lowerBound"
499
- :parse="(value: string): number => parseNumber(column, value)"
500
- label="Lower bound"
501
- />
502
- <PlToggleSwitch v-model="reactiveModel.state[id].includeLowerBound" label="Include lower bound" />
503
- <PlTextField
504
- v-model="reactiveModel.state[id].upperBound"
505
- :parse="(value: string): number => parseNumber(column, value)"
506
- label="Upper bound"
507
- />
508
- <PlToggleSwitch v-model="reactiveModel.state[id].includeUpperBound" label="Include upper bound" />
509
- </template>
510
- <template
511
- v-if="
512
- reactiveModel.state[id]?.type === 'string_equals' ||
513
- reactiveModel.state[id]?.type === 'string_notEquals' ||
514
- reactiveModel.state[id]?.type === 'string_contains' ||
515
- reactiveModel.state[id]?.type === 'string_doesNotContain'
516
- "
517
- >
518
- <PlTextField
519
- v-model="reactiveModel.state[id].reference"
520
- :parse="(value: string): string => parseString(column, value)"
521
- label="Reference value"
522
- />
523
- </template>
524
- <template v-if="reactiveModel.state[id]?.type === 'string_matches' || reactiveModel.state[id]?.type === 'string_doesNotMatch'">
525
- <PlTextField v-model="reactiveModel.state[id].reference" :parse="parseRegex" label="Reference value" />
526
- </template>
527
- <template v-if="reactiveModel.state[id]?.type === 'string_containsFuzzyMatch'">
528
- <PlTextField
529
- v-model="reactiveModel.state[id].reference"
530
- :parse="(value: string): string => parseString(column, value)"
531
- label="Reference value"
532
- />
533
- <Slider v-model="reactiveModel.state[id].maxEdits" :max="5" breakpoints label="Maximum nuber of substitutions and indels" />
534
- <PlToggleSwitch v-model="reactiveModel.state[id].substitutionsOnly" label="Substitutions only" />
535
- <PlDropdown
536
- v-model="reactiveModel.state[id].wildcard"
537
- :options="makeWildcardOptions(column, reactiveModel.state[id].reference)"
538
- clearable
539
- label="Wildcard symbol"
540
- />
541
- </template>
542
- </div>
543
- </form>
544
- </div>
545
- <div v-if="!filterOptionsPresent">No filters applicable</div>
546
- </template>
547
-
548
- <style lang="css" scoped>
549
- .controls {
550
- max-height: 0;
551
- transition: max-height 0.15s ease-out;
552
- overflow: hidden;
553
- }
554
- .controls.open {
555
- max-height: 500px;
556
- transition: max-height 0.25s ease-in;
557
- overflow: visible;
558
- }
559
- </style>