@platforma-sdk/ui-vue 1.10.0 → 1.10.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platforma-sdk/ui-vue",
3
- "version": "1.10.0",
3
+ "version": "1.10.2",
4
4
  "type": "module",
5
5
  "main": "dist/lib.umd.cjs",
6
6
  "module": "dist/lib.js",
@@ -38,7 +38,7 @@
38
38
  "@ag-grid-enterprise/side-bar": "^32.3.3",
39
39
  "@ag-grid-enterprise/column-tool-panel": "^32.3.3",
40
40
  "@milaboratories/uikit": "^2.2.3",
41
- "@platforma-sdk/model": "^1.10.0"
41
+ "@platforma-sdk/model": "^1.10.2"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@faker-js/faker": "^8.4.1",
@@ -9,6 +9,8 @@ import type {
9
9
  ManagedGridOptions,
10
10
  SortState,
11
11
  StateUpdatedEvent,
12
+ ColDef,
13
+ ColGroupDef,
12
14
  } from '@ag-grid-community/core';
13
15
  import { ModuleRegistry } from '@ag-grid-community/core';
14
16
  import { AgGridVue } from '@ag-grid-community/vue3';
@@ -18,7 +20,7 @@ import { RangeSelectionModule } from '@ag-grid-enterprise/range-selection';
18
20
  import { ServerSideRowModelModule } from '@ag-grid-enterprise/server-side-row-model';
19
21
  import { SideBarModule } from '@ag-grid-enterprise/side-bar';
20
22
  import { PlDropdownLine } from '@milaboratories/uikit';
21
- import type { AxisId, PlDataTableState, PTableRecordFilter, PTableSorting } from '@platforma-sdk/model';
23
+ import type { AxisId, PlDataTableState, PTableColumnSpec, PTableRecordFilter, PTableSorting } from '@platforma-sdk/model';
22
24
  import canonicalize from 'canonicalize';
23
25
  import * as lodash from 'lodash';
24
26
  import { computed, ref, shallowRef, toRefs, watch } from 'vue';
@@ -26,7 +28,8 @@ import { AgGridTheme, useWatchFetch } from '../../lib';
26
28
  import PlOverlayLoading from './PlAgOverlayLoading.vue';
27
29
  import PlOverlayNoRows from './PlAgOverlayNoRows.vue';
28
30
  import { updateXsvGridOptions } from './sources/file-source';
29
- import { enrichJoinWithLabelColumns, makeSheets, parseColId, PlAgDataTableRow, updatePFrameGridOptions } from './sources/table-source';
31
+ import type { PlAgDataTableRow } from './sources/table-source';
32
+ import { enrichJoinWithLabelColumns, makeSheets, parseColId, updatePFrameGridOptions } from './sources/table-source';
30
33
  import type { PlDataTableSettings, PlDataTableSheet } from './types';
31
34
 
32
35
  ModuleRegistry.registerModules([
@@ -38,15 +41,15 @@ ModuleRegistry.registerModules([
38
41
  ColumnsToolPanelModule,
39
42
  ]);
40
43
 
41
- const emit = defineEmits<{
42
- onRowDoubleClicked: [key: unknown[]]
43
- }>()
44
-
45
44
  const tableState = defineModel<PlDataTableState>({ default: { gridState: {} } });
46
45
  const props = defineProps<{
47
46
  settings: Readonly<PlDataTableSettings>;
48
47
  }>();
49
48
  const { settings } = toRefs(props);
49
+ const emit = defineEmits<{
50
+ onRowDoubleClicked: [key: unknown[]];
51
+ columnsChanged: [columns: PTableColumnSpec[]];
52
+ }>();
50
53
 
51
54
  watch(
52
55
  () => settings.value,
@@ -118,14 +121,15 @@ const sheets = useWatchFetch<PlDataTableSettings['sourceType'], PlDataTableSheet
118
121
  function makeSorting(state?: SortState): PTableSorting[] | undefined {
119
122
  if (settings.value.sourceType !== 'ptable' && settings.value.sourceType !== 'pframe') return undefined;
120
123
  return (
121
- state?.sortModel.map(
122
- (item) =>
123
- ({
124
- column: parseColId(item.colId),
125
- ascending: item.sort === 'asc',
126
- naAndAbsentAreLeastValues: true,
127
- }) as PTableSorting,
128
- ) ?? []
124
+ state?.sortModel.map((item) => {
125
+ const { spec, ...column } = parseColId(item.colId);
126
+ const _ = spec;
127
+ return {
128
+ column,
129
+ ascending: item.sort === 'asc',
130
+ naAndAbsentAreLeastValues: true,
131
+ } as PTableSorting;
132
+ }) ?? []
129
133
  );
130
134
  }
131
135
 
@@ -169,13 +173,13 @@ function makeFilters(sheetsState: Record<string, string | number>): PTableRecord
169
173
  type: 'bySingleColumn',
170
174
  column: sheet.column
171
175
  ? {
172
- type: 'column',
173
- id: sheet.column,
174
- }
176
+ type: 'column',
177
+ id: sheet.column,
178
+ }
175
179
  : {
176
- type: 'axis',
177
- id: sheet.axis,
178
- },
180
+ type: 'axis',
181
+ id: sheet.axis,
182
+ },
179
183
  predicate: {
180
184
  operator: 'Equal',
181
185
  reference: sheetsState[makeSheetId(sheet.axis)],
@@ -252,8 +256,7 @@ const gridOptions = ref<GridOptions<PlAgDataTableRow>>({
252
256
  event.api.autoSizeAllColumns();
253
257
  },
254
258
  onRowDoubleClicked: (value) => {
255
- if (value.data)
256
- emit("onRowDoubleClicked", value.data!.key)
259
+ if (value.data) emit('onRowDoubleClicked', value.data.key);
257
260
  },
258
261
  defaultColDef: {
259
262
  suppressHeaderMenuButton: true,
@@ -358,6 +361,15 @@ watch(
358
361
  (options, oldOptions) => {
359
362
  if (!oldOptions) return;
360
363
  if (options.rowModelType != oldOptions.rowModelType) ++reloadKey.value;
364
+ if (!lodash.isEqual(options.columnDefs, oldOptions.columnDefs) && options.columnDefs) {
365
+ const idColDef = (def: ColDef | ColGroupDef): def is ColDef => !('children' in def);
366
+ const colDefs: ColDef[] | undefined = options.columnDefs?.filter(idColDef);
367
+ const columns = colDefs
368
+ ?.map((def) => def.colId)
369
+ .filter((colId) => colId !== undefined)
370
+ .map((colId) => parseColId(colId));
371
+ emit('columnsChanged', columns ?? []);
372
+ }
361
373
  },
362
374
  );
363
375
 
@@ -438,13 +450,24 @@ watch(
438
450
  <div class="ap-ag-data-table-container">
439
451
  <Transition name="ap-ag-data-table-sheets-transition">
440
452
  <div v-if="sheets.value && sheets.value.length > 0" class="ap-ag-data-table-sheets">
441
- <PlDropdownLine v-for="(sheet, i) in sheets.value" :key="i" :model-value="sheetsState[makeSheetId(sheet.axis)]"
453
+ <PlDropdownLine
454
+ v-for="(sheet, i) in sheets.value"
455
+ :key="i"
456
+ :model-value="sheetsState[makeSheetId(sheet.axis)]"
442
457
  :options="sheet.options"
443
- @update:model-value="(newValue) => onSheetChanged(makeSheetId(sheet.axis), newValue)" />
458
+ @update:model-value="(newValue) => onSheetChanged(makeSheetId(sheet.axis), newValue)"
459
+ />
444
460
  </div>
445
461
  </Transition>
446
- <AgGridVue :key="reloadKey" :theme="AgGridTheme" class="ap-ag-data-table-grid" :grid-options="gridOptions"
447
- @grid-ready="onGridReady" @state-updated="onStateUpdated" @grid-pre-destroyed="onGridPreDestroyed" />
462
+ <AgGridVue
463
+ :key="reloadKey"
464
+ :theme="AgGridTheme"
465
+ class="ap-ag-data-table-grid"
466
+ :grid-options="gridOptions"
467
+ @grid-ready="onGridReady"
468
+ @state-updated="onStateUpdated"
469
+ @grid-pre-destroyed="onGridPreDestroyed"
470
+ />
448
471
  </div>
449
472
  </template>
450
473
 
@@ -0,0 +1,471 @@
1
+ <script lang="ts" setup>
2
+ import type { ListOption } from '@milaboratories/uikit';
3
+ import { PlCheckbox, PlDropdown, PlNumberField, 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
+ SingleValuePredicate,
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
+ const currentState = reactiveModel.state ?? {};
88
+ const newState: Record<string, PlTableFilter> = {};
89
+ for (const { id } of columnsWithIds) {
90
+ if (currentState[id] !== undefined) newState[id] = currentState[id];
91
+ }
92
+ reactiveModel.state = newState;
93
+ },
94
+ { immediate: true },
95
+ );
96
+
97
+ const getFilterLabel = (type: PlTableFilterType): string => {
98
+ switch (type) {
99
+ case 'isNotNA':
100
+ return 'Is not NA';
101
+ case 'isNA':
102
+ return 'Is NA';
103
+ case 'number_equals':
104
+ case 'string_equals':
105
+ return 'Equals';
106
+ case 'number_notEquals':
107
+ case 'string_notEquals':
108
+ return 'Not equals';
109
+ case 'number_greaterThen':
110
+ return 'Greater then';
111
+ case 'number_greaterThenOrEqualTo':
112
+ return 'Greater then or equal to';
113
+ case 'number_lessThen':
114
+ return 'Less then';
115
+ case 'number_lessThenOrEqualTo':
116
+ return 'Less then or equal to';
117
+ case 'number_between':
118
+ return 'Between';
119
+ case 'string_contains':
120
+ return 'Contains';
121
+ case 'string_doesNotContain':
122
+ return 'Does not contain';
123
+ case 'string_matches':
124
+ return 'Matches';
125
+ case 'string_doesNotMatch':
126
+ return 'Does not match';
127
+ case 'string_containsFuzzyMatch':
128
+ return 'Contains fuzzy match';
129
+ default:
130
+ throw Error(`unsupported filter type: ${type satisfies never}`);
131
+ }
132
+ };
133
+
134
+ const filterTypesNumber: PlTableFilterNumberType[] = [
135
+ 'isNotNA',
136
+ 'isNA',
137
+ 'number_equals',
138
+ 'number_notEquals',
139
+ 'number_greaterThen',
140
+ 'number_greaterThenOrEqualTo',
141
+ 'number_lessThen',
142
+ 'number_lessThenOrEqualTo',
143
+ 'number_between',
144
+ ] as const;
145
+ const filterTypesString: PlTableFilterStringType[] = [
146
+ 'isNotNA',
147
+ 'isNA',
148
+ 'string_equals',
149
+ // 'string_notEquals',
150
+ 'string_contains',
151
+ // 'string_doesNotContain',
152
+ 'string_matches',
153
+ // 'string_doesNotMatch',
154
+ 'string_containsFuzzyMatch',
155
+ ] as const;
156
+ const filterOptions = computed(() => {
157
+ const restrictionsMapValue = restrictionsMap.value;
158
+ const map: Record<string, ListOption<PlTableFilterType>[]> = {};
159
+ for (const { column, id } of columnsWithIds.value) {
160
+ const valueType = column.type === 'column' ? column.spec.valueType : column.spec.type;
161
+ let types: PlTableFilterType[] = valueType === 'String' ? filterTypesString : filterTypesNumber;
162
+
163
+ const restrictionsEntry = restrictionsMapValue[id];
164
+ if (restrictionsEntry !== undefined) {
165
+ types = types.filter((type) => restrictionsEntry.includes(type));
166
+ }
167
+
168
+ map[id] = types.map((type) => ({
169
+ value: type,
170
+ text: getFilterLabel(type),
171
+ }));
172
+ }
173
+ return map;
174
+ });
175
+
176
+ const getFilterReference = (filter: PlTableFilter): undefined | number | string => {
177
+ const type = filter.type;
178
+ switch (type) {
179
+ case 'isNotNA':
180
+ case 'isNA':
181
+ return undefined;
182
+ case 'number_equals':
183
+ case 'number_notEquals':
184
+ case 'number_greaterThen':
185
+ case 'number_greaterThenOrEqualTo':
186
+ case 'number_lessThen':
187
+ case 'number_lessThenOrEqualTo':
188
+ return filter.reference;
189
+ case 'number_between':
190
+ return filter.lowerBound;
191
+ case 'string_equals':
192
+ case 'string_notEquals':
193
+ case 'string_contains':
194
+ case 'string_doesNotContain':
195
+ case 'string_matches':
196
+ case 'string_doesNotMatch':
197
+ case 'string_containsFuzzyMatch':
198
+ return filter.reference;
199
+ default:
200
+ throw Error(`unsupported filter type: ${type satisfies never}`);
201
+ }
202
+ };
203
+ const getFilterDefault = (type: PlTableFilterType, reference?: undefined | number | string): PlTableFilter => {
204
+ switch (type) {
205
+ case 'isNotNA':
206
+ case 'isNA':
207
+ return { type };
208
+ case 'number_equals':
209
+ case 'number_notEquals':
210
+ case 'number_greaterThen':
211
+ case 'number_greaterThenOrEqualTo':
212
+ case 'number_lessThen':
213
+ case 'number_lessThenOrEqualTo':
214
+ return { type, reference: typeof reference === 'number' ? reference : 0 };
215
+ case 'number_between':
216
+ return {
217
+ type,
218
+ lowerBound: typeof reference === 'number' ? reference : 0,
219
+ includeLowerBound: true,
220
+ upperBound: 100,
221
+ includeUpperBound: false,
222
+ };
223
+ case 'string_equals':
224
+ case 'string_notEquals':
225
+ case 'string_contains':
226
+ case 'string_doesNotContain':
227
+ case 'string_matches':
228
+ case 'string_doesNotMatch':
229
+ return { type, reference: typeof reference === 'string' ? reference : '' };
230
+ case 'string_containsFuzzyMatch':
231
+ return {
232
+ type,
233
+ reference: typeof reference === 'string' ? reference : '',
234
+ maxEdits: 2,
235
+ substitutionsOnly: false,
236
+ wildcard: undefined,
237
+ };
238
+ default:
239
+ throw Error(`unsupported filter type: ${type satisfies never}`);
240
+ }
241
+ };
242
+ const updateColumnFilter = (columnId: string, type: PlTableFilterType): void => {
243
+ const prevFilter = reactiveModel.state![columnId];
244
+ reactiveModel.state![columnId] = getFilterDefault(type, getFilterReference(prevFilter));
245
+ };
246
+ const onFilterActiveChanged = (columnId: string, checked: boolean) => {
247
+ if (checked) {
248
+ reactiveModel.state![columnId] = defaultsMap.value[columnId] ?? getFilterDefault(filterOptions.value[columnId][0].value);
249
+ } else {
250
+ delete reactiveModel.state![columnId];
251
+ }
252
+ };
253
+
254
+ const makeWildcardOptions = (reference: string) => {
255
+ const chars = lodash.uniq(reference);
256
+ chars.sort();
257
+ return chars.map((char) => ({
258
+ value: char,
259
+ text: char,
260
+ }));
261
+ };
262
+
263
+ const makePredicate = (filter: PlTableFilter): SingleValuePredicate => {
264
+ const type = filter.type;
265
+ switch (type) {
266
+ case 'isNotNA':
267
+ return {
268
+ operator: 'Not',
269
+ operand: {
270
+ operator: 'IsNA',
271
+ },
272
+ };
273
+ case 'isNA':
274
+ return {
275
+ operator: 'IsNA',
276
+ };
277
+ case 'number_equals':
278
+ case 'string_equals':
279
+ return {
280
+ operator: 'Equal',
281
+ reference: filter.reference,
282
+ };
283
+ case 'number_notEquals':
284
+ case 'string_notEquals':
285
+ return {
286
+ operator: 'Not',
287
+ operand: {
288
+ operator: 'Equal',
289
+ reference: filter.reference,
290
+ },
291
+ };
292
+ case 'number_greaterThen':
293
+ return {
294
+ operator: 'Greater',
295
+ reference: filter.reference,
296
+ };
297
+ case 'number_greaterThenOrEqualTo':
298
+ return {
299
+ operator: 'GreaterOrEqual',
300
+ reference: filter.reference,
301
+ };
302
+ case 'number_lessThen':
303
+ return {
304
+ operator: 'Less',
305
+ reference: filter.reference,
306
+ };
307
+ case 'number_lessThenOrEqualTo':
308
+ return {
309
+ operator: 'LessOrEqual',
310
+ reference: filter.reference,
311
+ };
312
+ case 'number_between':
313
+ return {
314
+ operator: 'And',
315
+ operands: [
316
+ {
317
+ operator: filter.includeLowerBound ? 'GreaterOrEqual' : 'Greater',
318
+ reference: filter.lowerBound,
319
+ },
320
+ {
321
+ operator: filter.includeUpperBound ? 'LessOrEqual' : 'Less',
322
+ reference: filter.upperBound,
323
+ },
324
+ ],
325
+ };
326
+ case 'string_contains':
327
+ return {
328
+ operator: 'StringContains',
329
+ substring: filter.reference,
330
+ };
331
+ case 'string_doesNotContain':
332
+ return {
333
+ operator: 'Not',
334
+ operand: {
335
+ operator: 'StringContains',
336
+ substring: filter.reference,
337
+ },
338
+ };
339
+ case 'string_matches':
340
+ return {
341
+ operator: 'Matches',
342
+ regex: filter.reference,
343
+ };
344
+ case 'string_doesNotMatch':
345
+ return {
346
+ operator: 'Not',
347
+ operand: {
348
+ operator: 'Matches',
349
+ regex: filter.reference,
350
+ },
351
+ };
352
+ case 'string_containsFuzzyMatch':
353
+ return {
354
+ operator: 'StringContainsFuzzy',
355
+ reference: filter.reference,
356
+ maxEdits: filter.maxEdits,
357
+ substitutionsOnly: filter.substitutionsOnly,
358
+ wildcard: filter.wildcard,
359
+ };
360
+ default:
361
+ throw Error(`unsupported filter type: ${type satisfies never}`);
362
+ }
363
+ };
364
+ const makeFilters = (state: Record<string, PlTableFilter>): PTableRecordFilter[] => {
365
+ return columnsWithIds.value
366
+ .map(({ column, id }) => {
367
+ if (!(id in state)) return undefined;
368
+
369
+ const predicate = makePredicate(state[id]);
370
+ const { spec, ...columnId } = column;
371
+ const _ = spec;
372
+
373
+ return {
374
+ type: 'bySingleColumn',
375
+ column: columnId,
376
+ predicate,
377
+ } satisfies PTableRecordFilter;
378
+ })
379
+ .filter((entry) => entry !== undefined);
380
+ };
381
+
382
+ // Should this happen on Apply button click instead?
383
+ watch(
384
+ () => reactiveModel,
385
+ (reactiveModel) => {
386
+ if (!lodash.isEqual(reactiveModel.state, model.value.state)) {
387
+ model.value = {
388
+ state: lodash.cloneDeep(reactiveModel.state),
389
+ filters: makeFilters(reactiveModel.state),
390
+ };
391
+ }
392
+ },
393
+ {
394
+ immediate: true,
395
+ deep: true,
396
+ },
397
+ );
398
+ </script>
399
+
400
+ <template>
401
+ <div v-for="({ column, id }, i) in columnsWithIds" :key="id">
402
+ <form v-if="filterOptions[id].length > 0" class="d-flex gap-10 flex-column">
403
+ <PlCheckbox :model-value="!!reactiveModel.state[id]" @update:model-value="(checked) => onFilterActiveChanged(id, checked)">
404
+ {{ column.spec.annotations?.['pl7.app/label']?.trim() ?? 'Unlabeled ' + column.type + ' ' + i.toString() }}
405
+ </PlCheckbox>
406
+ <div class="controls d-flex gap-10 flex-column" :class="{ open: !!reactiveModel.state[id] }">
407
+ <PlDropdown
408
+ v-if="reactiveModel.state[id]"
409
+ :model-value="reactiveModel.state[id]!.type"
410
+ :options="filterOptions[id]"
411
+ label="Predicate"
412
+ @update:model-value="(type) => updateColumnFilter(id, type!)"
413
+ />
414
+ <template
415
+ v-if="
416
+ reactiveModel.state[id]?.type === 'number_equals' ||
417
+ reactiveModel.state[id]?.type === 'number_notEquals' ||
418
+ reactiveModel.state[id]?.type === 'number_lessThen' ||
419
+ reactiveModel.state[id]?.type === 'number_lessThenOrEqualTo' ||
420
+ reactiveModel.state[id]?.type === 'number_greaterThen' ||
421
+ reactiveModel.state[id]?.type === 'number_greaterThenOrEqualTo'
422
+ "
423
+ >
424
+ <PlNumberField v-model="reactiveModel.state[id].reference" label="Reference value" />
425
+ </template>
426
+ <template v-if="reactiveModel.state[id]?.type === 'number_between'">
427
+ <PlNumberField v-model="reactiveModel.state[id].lowerBound" label="Lower bound" />
428
+ <PlToggleSwitch v-model="reactiveModel.state[id].includeLowerBound" label="Include lower bound" />
429
+ <PlNumberField v-model="reactiveModel.state[id].upperBound" label="Upper bound" />
430
+ <PlToggleSwitch v-model="reactiveModel.state[id].includeUpperBound" label="Include upper bound" />
431
+ </template>
432
+ <template
433
+ v-if="
434
+ reactiveModel.state[id]?.type === 'string_equals' ||
435
+ reactiveModel.state[id]?.type === 'string_notEquals' ||
436
+ reactiveModel.state[id]?.type === 'string_contains' ||
437
+ reactiveModel.state[id]?.type === 'string_doesNotContain' ||
438
+ reactiveModel.state[id]?.type === 'string_matches' ||
439
+ reactiveModel.state[id]?.type === 'string_doesNotMatch'
440
+ "
441
+ >
442
+ <PlTextField v-model="reactiveModel.state[id].reference" label="Reference value" />
443
+ </template>
444
+ <template v-if="reactiveModel.state[id]?.type === 'string_containsFuzzyMatch'">
445
+ <PlTextField v-model="reactiveModel.state[id].reference" label="Reference value" />
446
+ <Slider v-model="reactiveModel.state[id].maxEdits" :max="5" breakpoints label="Maximum nuber of substitutions and indels" />
447
+ <PlToggleSwitch v-model="reactiveModel.state[id].substitutionsOnly" label="Substitutions only" />
448
+ <PlDropdown
449
+ v-model="reactiveModel.state[id].wildcard"
450
+ :options="makeWildcardOptions(reactiveModel.state[id].reference)"
451
+ clearable
452
+ label="Wildcard symbol"
453
+ />
454
+ </template>
455
+ </div>
456
+ </form>
457
+ </div>
458
+ </template>
459
+
460
+ <style lang="css" scoped>
461
+ .controls {
462
+ max-height: 0;
463
+ transition: max-height 0.15s ease-out;
464
+ overflow: hidden;
465
+ }
466
+ .controls.open {
467
+ max-height: 500px;
468
+ transition: max-height 0.25s ease-in;
469
+ overflow: visible;
470
+ }
471
+ </style>