@platforma-sdk/ui-vue 1.45.34 → 1.45.36

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 (150) hide show
  1. package/.turbo/turbo-build.log +216 -222
  2. package/.turbo/turbo-type-check.log +1 -1
  3. package/CHANGELOG.md +16 -0
  4. package/dist/AgGridVue/useAgGridOptions.js +2 -3
  5. package/dist/AgGridVue/useAgGridOptions.js.map +1 -1
  6. package/dist/components/{PlMultiSequenceAlignment/Legend.vue.d.ts → PlAdvancedFilter/OperandButton.vue.d.ts} +4 -2
  7. package/dist/components/{PlMultiSequenceAlignment/Toolbar.vue.js → PlAdvancedFilter/OperandButton.vue.js} +3 -3
  8. package/dist/components/PlAdvancedFilter/OperandButton.vue.js.map +1 -0
  9. package/dist/components/PlAdvancedFilter/OperandButton.vue2.js +25 -0
  10. package/dist/components/PlAdvancedFilter/OperandButton.vue2.js.map +1 -0
  11. package/dist/components/PlAdvancedFilter/OperandButton.vue3.js +13 -0
  12. package/dist/components/PlAdvancedFilter/OperandButton.vue3.js.map +1 -0
  13. package/dist/components/PlAdvancedFilter/PlAdvancedFilter.vue.d.ts +39 -0
  14. package/dist/components/PlAdvancedFilter/PlAdvancedFilter.vue.js +10 -0
  15. package/dist/components/PlAdvancedFilter/PlAdvancedFilter.vue.js.map +1 -0
  16. package/dist/components/PlAdvancedFilter/PlAdvancedFilter.vue2.js +199 -0
  17. package/dist/components/PlAdvancedFilter/PlAdvancedFilter.vue2.js.map +1 -0
  18. package/dist/components/PlAdvancedFilter/PlAdvancedFilter.vue3.js +17 -0
  19. package/dist/components/PlAdvancedFilter/PlAdvancedFilter.vue3.js.map +1 -0
  20. package/dist/components/PlAdvancedFilter/SingleFilter.vue.d.ts +37 -0
  21. package/dist/components/{PlMultiSequenceAlignment/PlMultiSequenceAlignment.vue.js → PlAdvancedFilter/SingleFilter.vue.js} +3 -3
  22. package/dist/components/PlAdvancedFilter/SingleFilter.vue.js.map +1 -0
  23. package/dist/components/PlAdvancedFilter/SingleFilter.vue2.js +306 -0
  24. package/dist/components/PlAdvancedFilter/SingleFilter.vue2.js.map +1 -0
  25. package/dist/components/PlAdvancedFilter/SingleFilter.vue3.js +35 -0
  26. package/dist/components/PlAdvancedFilter/SingleFilter.vue3.js.map +1 -0
  27. package/dist/components/PlAdvancedFilter/constants.d.ts +4 -0
  28. package/dist/components/PlAdvancedFilter/constants.js +41 -0
  29. package/dist/components/PlAdvancedFilter/constants.js.map +1 -0
  30. package/dist/components/PlAdvancedFilter/index.d.ts +1 -0
  31. package/dist/components/PlAdvancedFilter/types.d.ts +57 -0
  32. package/dist/components/PlAdvancedFilter/types.js +8 -0
  33. package/dist/components/PlAdvancedFilter/types.js.map +1 -0
  34. package/dist/components/PlAdvancedFilter/utils.js +150 -0
  35. package/dist/components/PlAdvancedFilter/utils.js.map +1 -0
  36. package/dist/components/PlAgDataTable/PlAgRowCount.vue.js +7 -8
  37. package/dist/components/PlAgDataTable/PlAgRowCount.vue.js.map +1 -1
  38. package/dist/components/PlAgRowNumCheckbox/PlAgRowNumCheckbox.vue.js +9 -10
  39. package/dist/components/PlAgRowNumCheckbox/PlAgRowNumCheckbox.vue.js.map +1 -1
  40. package/dist/components/PlAgRowNumHeader.vue.js +2 -3
  41. package/dist/components/PlAgRowNumHeader.vue.js.map +1 -1
  42. package/dist/index.js +32 -32
  43. package/dist/lib.d.ts +1 -1
  44. package/package.json +6 -7
  45. package/src/components/PlAdvancedFilter/OperandButton.vue +53 -0
  46. package/src/components/PlAdvancedFilter/PlAdvancedFilter.vue +209 -0
  47. package/src/components/PlAdvancedFilter/SingleFilter.vue +425 -0
  48. package/src/components/PlAdvancedFilter/constants.ts +42 -0
  49. package/src/components/PlAdvancedFilter/index.ts +1 -0
  50. package/src/components/PlAdvancedFilter/types.ts +77 -0
  51. package/src/components/PlAdvancedFilter/utils.ts +215 -0
  52. package/src/lib.ts +2 -2
  53. package/dist/assets/multi-sequence-alignment.worker-Cm0gZp19.js +0 -6
  54. package/dist/assets/multi-sequence-alignment.worker-Cm0gZp19.js.map +0 -1
  55. package/dist/assets/phylogenetic-tree.worker-4CrExYEo.js +0 -5
  56. package/dist/assets/phylogenetic-tree.worker-4CrExYEo.js.map +0 -1
  57. package/dist/components/PlMultiSequenceAlignment/Consensus.vue.d.ts +0 -9
  58. package/dist/components/PlMultiSequenceAlignment/Consensus.vue.js +0 -10
  59. package/dist/components/PlMultiSequenceAlignment/Consensus.vue.js.map +0 -1
  60. package/dist/components/PlMultiSequenceAlignment/Consensus.vue2.js +0 -122
  61. package/dist/components/PlMultiSequenceAlignment/Consensus.vue2.js.map +0 -1
  62. package/dist/components/PlMultiSequenceAlignment/Consensus.vue3.js +0 -9
  63. package/dist/components/PlMultiSequenceAlignment/Consensus.vue3.js.map +0 -1
  64. package/dist/components/PlMultiSequenceAlignment/Legend.vue.js +0 -10
  65. package/dist/components/PlMultiSequenceAlignment/Legend.vue.js.map +0 -1
  66. package/dist/components/PlMultiSequenceAlignment/Legend.vue2.js +0 -28
  67. package/dist/components/PlMultiSequenceAlignment/Legend.vue2.js.map +0 -1
  68. package/dist/components/PlMultiSequenceAlignment/Legend.vue3.js +0 -13
  69. package/dist/components/PlMultiSequenceAlignment/Legend.vue3.js.map +0 -1
  70. package/dist/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue.d.ts +0 -25
  71. package/dist/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue.js +0 -10
  72. package/dist/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue.js.map +0 -1
  73. package/dist/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue2.js +0 -138
  74. package/dist/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue2.js.map +0 -1
  75. package/dist/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue3.js +0 -31
  76. package/dist/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue3.js.map +0 -1
  77. package/dist/components/PlMultiSequenceAlignment/PhylogeneticTree.vue.d.ts +0 -8
  78. package/dist/components/PlMultiSequenceAlignment/PhylogeneticTree.vue.js +0 -10
  79. package/dist/components/PlMultiSequenceAlignment/PhylogeneticTree.vue.js.map +0 -1
  80. package/dist/components/PlMultiSequenceAlignment/PhylogeneticTree.vue2.js +0 -77
  81. package/dist/components/PlMultiSequenceAlignment/PhylogeneticTree.vue2.js.map +0 -1
  82. package/dist/components/PlMultiSequenceAlignment/PhylogeneticTree.vue3.js +0 -9
  83. package/dist/components/PlMultiSequenceAlignment/PhylogeneticTree.vue3.js.map +0 -1
  84. package/dist/components/PlMultiSequenceAlignment/PlMultiSequenceAlignment.vue.d.ts +0 -71
  85. package/dist/components/PlMultiSequenceAlignment/PlMultiSequenceAlignment.vue.js.map +0 -1
  86. package/dist/components/PlMultiSequenceAlignment/PlMultiSequenceAlignment.vue2.js +0 -224
  87. package/dist/components/PlMultiSequenceAlignment/PlMultiSequenceAlignment.vue2.js.map +0 -1
  88. package/dist/components/PlMultiSequenceAlignment/PlMultiSequenceAlignment.vue3.js +0 -9
  89. package/dist/components/PlMultiSequenceAlignment/PlMultiSequenceAlignment.vue3.js.map +0 -1
  90. package/dist/components/PlMultiSequenceAlignment/SeqLogo.vue.d.ts +0 -8
  91. package/dist/components/PlMultiSequenceAlignment/SeqLogo.vue.js +0 -10
  92. package/dist/components/PlMultiSequenceAlignment/SeqLogo.vue.js.map +0 -1
  93. package/dist/components/PlMultiSequenceAlignment/SeqLogo.vue2.js +0 -127
  94. package/dist/components/PlMultiSequenceAlignment/SeqLogo.vue2.js.map +0 -1
  95. package/dist/components/PlMultiSequenceAlignment/SeqLogo.vue3.js +0 -9
  96. package/dist/components/PlMultiSequenceAlignment/SeqLogo.vue3.js.map +0 -1
  97. package/dist/components/PlMultiSequenceAlignment/Toolbar.vue.d.ts +0 -16
  98. package/dist/components/PlMultiSequenceAlignment/Toolbar.vue.js.map +0 -1
  99. package/dist/components/PlMultiSequenceAlignment/Toolbar.vue2.js +0 -228
  100. package/dist/components/PlMultiSequenceAlignment/Toolbar.vue2.js.map +0 -1
  101. package/dist/components/PlMultiSequenceAlignment/Toolbar.vue3.js +0 -19
  102. package/dist/components/PlMultiSequenceAlignment/Toolbar.vue3.js.map +0 -1
  103. package/dist/components/PlMultiSequenceAlignment/cell-size.d.ts +0 -4
  104. package/dist/components/PlMultiSequenceAlignment/cell-size.js +0 -8
  105. package/dist/components/PlMultiSequenceAlignment/cell-size.js.map +0 -1
  106. package/dist/components/PlMultiSequenceAlignment/chemical-properties.d.ts +0 -44
  107. package/dist/components/PlMultiSequenceAlignment/chemical-properties.js +0 -132
  108. package/dist/components/PlMultiSequenceAlignment/chemical-properties.js.map +0 -1
  109. package/dist/components/PlMultiSequenceAlignment/data.d.ts +0 -61
  110. package/dist/components/PlMultiSequenceAlignment/data.js +0 -370
  111. package/dist/components/PlMultiSequenceAlignment/data.js.map +0 -1
  112. package/dist/components/PlMultiSequenceAlignment/index.d.ts +0 -1
  113. package/dist/components/PlMultiSequenceAlignment/markup.d.ts +0 -16
  114. package/dist/components/PlMultiSequenceAlignment/markup.js +0 -84
  115. package/dist/components/PlMultiSequenceAlignment/markup.js.map +0 -1
  116. package/dist/components/PlMultiSequenceAlignment/migrations.d.ts +0 -3
  117. package/dist/components/PlMultiSequenceAlignment/migrations.js +0 -24
  118. package/dist/components/PlMultiSequenceAlignment/migrations.js.map +0 -1
  119. package/dist/components/PlMultiSequenceAlignment/multi-sequence-alignment.worker.d.ts +0 -6
  120. package/dist/components/PlMultiSequenceAlignment/phylogenetic-tree.worker.d.ts +0 -7
  121. package/dist/components/PlMultiSequenceAlignment/residue-counts.d.ts +0 -2
  122. package/dist/components/PlMultiSequenceAlignment/residue-counts.js +0 -13
  123. package/dist/components/PlMultiSequenceAlignment/residue-counts.js.map +0 -1
  124. package/dist/components/PlMultiSequenceAlignment/settings.d.ts +0 -2
  125. package/dist/components/PlMultiSequenceAlignment/settings.js +0 -9
  126. package/dist/components/PlMultiSequenceAlignment/settings.js.map +0 -1
  127. package/dist/components/PlMultiSequenceAlignment/types.d.ts +0 -5
  128. package/dist/components/PlMultiSequenceAlignment/useMiPlots.d.ts +0 -4
  129. package/dist/components/PlMultiSequenceAlignment/useMiPlots.js +0 -19
  130. package/dist/components/PlMultiSequenceAlignment/useMiPlots.js.map +0 -1
  131. package/src/components/PlMultiSequenceAlignment/Consensus.vue +0 -165
  132. package/src/components/PlMultiSequenceAlignment/Legend.vue +0 -44
  133. package/src/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue +0 -299
  134. package/src/components/PlMultiSequenceAlignment/PhylogeneticTree.vue +0 -110
  135. package/src/components/PlMultiSequenceAlignment/PlMultiSequenceAlignment.vue +0 -314
  136. package/src/components/PlMultiSequenceAlignment/README.md +0 -216
  137. package/src/components/PlMultiSequenceAlignment/SeqLogo.vue +0 -166
  138. package/src/components/PlMultiSequenceAlignment/Toolbar.vue +0 -228
  139. package/src/components/PlMultiSequenceAlignment/cell-size.ts +0 -4
  140. package/src/components/PlMultiSequenceAlignment/chemical-properties.ts +0 -199
  141. package/src/components/PlMultiSequenceAlignment/data.ts +0 -661
  142. package/src/components/PlMultiSequenceAlignment/index.ts +0 -1
  143. package/src/components/PlMultiSequenceAlignment/markup.ts +0 -141
  144. package/src/components/PlMultiSequenceAlignment/migrations.ts +0 -46
  145. package/src/components/PlMultiSequenceAlignment/multi-sequence-alignment.worker.ts +0 -54
  146. package/src/components/PlMultiSequenceAlignment/phylogenetic-tree.worker.ts +0 -89
  147. package/src/components/PlMultiSequenceAlignment/residue-counts.ts +0 -124
  148. package/src/components/PlMultiSequenceAlignment/settings.ts +0 -7
  149. package/src/components/PlMultiSequenceAlignment/types.ts +0 -3
  150. package/src/components/PlMultiSequenceAlignment/useMiPlots.ts +0 -23
@@ -0,0 +1,209 @@
1
+ <script lang="ts" setup>
2
+ import SingleFilter from './SingleFilter.vue';
3
+ import { PlBtnSecondary, PlElementList, PlCheckbox, PlIcon16 } from '@milaboratories/uikit';
4
+ import type { PlAdvancedFilterColumnId, CommonFilterSpec, Group, PlAdvancedFilterUI, SourceOptionInfo } from './types';
5
+ import { computed } from 'vue';
6
+ import OperandButton from './OperandButton.vue';
7
+ import { DEFAULT_FILTER_TYPE, DEFAULT_FILTERS } from './constants';
8
+ import type { ListOptionBase } from '@platforma-sdk/model';
9
+ import { createNewGroup, isValidColumnId, toInnerModel, toOuterModel, useInnerModel } from './utils';
10
+
11
+ const props = withDefaults(defineProps<{
12
+ /** List of ids of sources (columns, axes) that can be selected in filters */
13
+ items: SourceOptionInfo[];
14
+ /** If true - new filter can be added by droppind element into filter group; else new column is added by button click */
15
+ enableDnd?: boolean;
16
+ /** Loading function for unique values for Equal/InSet filters and fixed axes options. */
17
+ getSuggestOptions: (params: { columnId: PlAdvancedFilterColumnId; searchStr: string; axisIdx?: number }) => (Promise<ListOptionBase<string | number>[]>) |
18
+ ((params: { columnId: PlAdvancedFilterColumnId; searchStr: string; axisIdx?: number }) => ListOptionBase<string | number>[]);
19
+ /** Loading function for label of selected value for Equal/InSet filters and fixed axes options. */
20
+ getSuggestModel: (params: { columnId: PlAdvancedFilterColumnId; searchStr: string; axisIdx?: number }) => (Promise<ListOptionBase<string | number>>) |
21
+ ((params: { columnId: PlAdvancedFilterColumnId; searchStr: string; axisIdx?: number }) => ListOptionBase<string | number>);
22
+ }>(), { enableDnd: false });
23
+
24
+ const model = defineModel<CommonFilterSpec>({ required: true });
25
+ const innerModel = useInnerModel<CommonFilterSpec, PlAdvancedFilterUI>(toInnerModel, toOuterModel, model);
26
+
27
+ const firstColumnId = computed(() => props.items[0]?.id);
28
+ const emptyGroup: Group[] = [{
29
+ id: 'empty',
30
+ not: false,
31
+ operand: 'and',
32
+ filters: [],
33
+ expanded: true,
34
+ }];
35
+
36
+ function addColumnToGroup(groupIdx: number, selectedSourceId: PlAdvancedFilterColumnId) {
37
+ innerModel.value.groups[groupIdx].filters.push({
38
+ ...DEFAULT_FILTERS[DEFAULT_FILTER_TYPE],
39
+ column: selectedSourceId,
40
+ });
41
+ }
42
+
43
+ function removeFilterFromGroup(groupIdx: number, filterIdx: number) {
44
+ if (innerModel.value.groups[groupIdx].filters.length === 1 && filterIdx === 0) {
45
+ removeGroup(groupIdx);
46
+ } else {
47
+ innerModel.value.groups[groupIdx].filters = innerModel.value.groups[groupIdx].filters.filter((_v, idx) => idx !== filterIdx);
48
+ }
49
+ }
50
+
51
+ function removeGroup(groupIdx: number) {
52
+ innerModel.value.groups = innerModel.value.groups.filter((v, idx) => idx !== groupIdx);
53
+ }
54
+ function addGroup(selectedSourceId: PlAdvancedFilterColumnId) {
55
+ const newGroup = createNewGroup(selectedSourceId);
56
+ innerModel.value.groups.push(newGroup);
57
+ }
58
+
59
+ function handleDropToExistingGroup(groupIdx: number, event: DragEvent) {
60
+ const dataTransfer = event.dataTransfer;
61
+ if (dataTransfer?.getData('text/plain')) {
62
+ const draggedId = dataTransfer.getData('text/plain');
63
+ if (isValidColumnId(draggedId)) {
64
+ addColumnToGroup(groupIdx, draggedId);
65
+ }
66
+ }
67
+ }
68
+ function handleDropToNewGroup(event: DragEvent) {
69
+ const dataTransfer = event.dataTransfer;
70
+ if (dataTransfer?.getData('text/plain')) {
71
+ const draggedId = dataTransfer.getData('text/plain');
72
+ if (isValidColumnId(draggedId)) {
73
+ addGroup(draggedId);
74
+ }
75
+ }
76
+ }
77
+ function dragOver(event: DragEvent) {
78
+ event.preventDefault();
79
+ }
80
+ </script>
81
+ <template>
82
+ <div>
83
+ <PlElementList
84
+ v-model:items="innerModel.groups"
85
+ :get-item-key="(group) => group.id"
86
+
87
+ :item-class="$style.filterGroup"
88
+ :item-class-content="$style.filterGroupContent"
89
+ :item-class-title="$style.filterGroupTitle"
90
+
91
+ :is-expanded="(group) => group.expanded"
92
+
93
+ :disableDragging="false"
94
+ :disableRemoving="false"
95
+ :disableToggling="true"
96
+ :disablePinning="true"
97
+
98
+ :on-expand="(group) => { group.expanded = !group.expanded}"
99
+ >
100
+ <template #item-title>
101
+ Filter group
102
+ </template>
103
+ <template #item-content="{ item, index }">
104
+ <div
105
+ :class="$style.groupContent" dropzone="true"
106
+ @drop="(event) => handleDropToExistingGroup(index, event)"
107
+ @dragover="dragOver"
108
+ >
109
+ <PlCheckbox v-model="item.not">NOT</PlCheckbox>
110
+ <SingleFilter
111
+ v-for="(filter, filterIdx) in item.filters"
112
+ :key="filterIdx"
113
+ v-model="item.filters[filterIdx]"
114
+ :operand="item.operand"
115
+ :column-options="items"
116
+ :get-suggest-model="getSuggestModel"
117
+ :get-suggest-options="getSuggestOptions"
118
+ :enable-dnd="enableDnd"
119
+ :is-last="filterIdx === item.filters.length - 1"
120
+ :on-change-operand="(v) => item.operand = v"
121
+ :on-delete="() => removeFilterFromGroup(index, filterIdx)"
122
+ />
123
+ <div v-if="enableDnd" :class="$style.dropzone">
124
+ <div>Drop dimensions here</div>
125
+ </div>
126
+ <PlBtnSecondary v-else @click="addColumnToGroup(index, firstColumnId)">
127
+ <PlIcon16 name="add" style="margin-right: 8px"/>Add column
128
+ </PlBtnSecondary>
129
+ </div>
130
+ </template>
131
+ <template #item-after="{ index }">
132
+ <OperandButton
133
+ :class="$style.buttonWrapper"
134
+ :active="innerModel.operand"
135
+ :disabled="index === innerModel.groups.length - 1"
136
+ :on-select="(v) => innerModel.operand = v"
137
+ />
138
+ </template>
139
+ </PlElementList>
140
+
141
+ <!-- Last group - always exists, always empty, just for adding new groups -->
142
+ <PlElementList
143
+ v-model:items="emptyGroup"
144
+ :get-item-key="(group) => group.id"
145
+ :item-class="$style.filterGroup"
146
+ :item-class-content="$style.filterGroupContent"
147
+ :item-class-title="$style.filterGroupTitle"
148
+
149
+ :is-expanded="() => true"
150
+
151
+ :disableDragging="true"
152
+ :disableRemoving="true"
153
+ :disableToggling="true"
154
+ :disablePinning="true"
155
+ dropzone="true"
156
+ @drop="handleDropToNewGroup"
157
+ @dragover="dragOver"
158
+ >
159
+ <template #item-title>Filter group</template>
160
+ <template #item-content="{item}">
161
+ <PlCheckbox v-model="item.not" disabled >NOT</PlCheckbox>
162
+ <div v-if="enableDnd" :class="$style.dropzone">
163
+ <div>Drop dimensions here</div>
164
+ </div>
165
+ <PlBtnSecondary v-else @click="addGroup(firstColumnId)">
166
+ <PlIcon16 name="add" style="margin-right: 8px"/>Add column
167
+ </PlBtnSecondary>
168
+ </template>
169
+ </PlElementList>
170
+ </div>
171
+ </template>
172
+ <style module>
173
+ .filterGroup {
174
+ background: var(--bg-base-light);
175
+ }
176
+ .filterGroup:hover {
177
+ background: rgba(99, 224, 36, 0.12);
178
+ }
179
+ .filterGroupTitle {
180
+ background: none;
181
+ }
182
+ .groupContent {
183
+ display: flex;
184
+ flex-direction: column;
185
+ gap: 12px;
186
+ }
187
+ .dropzone {
188
+ border-radius: 6px;
189
+ border: 1.5px dashed var(--color-div-grey);
190
+ color: var(--txt-03);
191
+ font-family: Manrope;
192
+ font-size: 14px;
193
+ font-style: normal;
194
+ font-weight: 500;
195
+ height: 40px;
196
+ cursor: default;
197
+ display: flex;
198
+ justify-content: center;
199
+ align-items: center;
200
+ }
201
+ .buttonWrapper {
202
+ height: 72px;
203
+ display: flex;
204
+ align-items: center;
205
+ }
206
+ :global(.sortable-chosen) .buttonWrapper {
207
+ visibility: hidden;
208
+ }
209
+ </style>
@@ -0,0 +1,425 @@
1
+ <script lang="ts" setup>
2
+ import type { PlAdvancedFilterColumnId, Filter, Operand, SourceOptionInfo } from './types';
3
+ import { PlIcon16, PlDropdown, PlAutocomplete, PlAutocompleteMulti, PlTextField, PlNumberField, Slider, PlToggleSwitch } from '@milaboratories/uikit';
4
+ import { computed } from 'vue';
5
+ import { SUPPORTED_FILTER_TYPES, DEFAULT_FILTER_TYPE, DEFAULT_FILTERS } from './constants';
6
+ import type { AnchoredPColumnId, AxisFilterByIdx, AxisFilterValue, SUniversalPColumnId } from '@platforma-sdk/model';
7
+ import { isFilteredPColumn, parseColumnId, stringifyColumnId, type ListOptionBase } from '@platforma-sdk/model';
8
+ import OperandButton from './OperandButton.vue';
9
+ import { getFilterInfo, getNormalizedSpec, isNumericFilter, isStringFilter } from './utils';
10
+
11
+ const props = defineProps<{
12
+ operand: Operand;
13
+ columnOptions: SourceOptionInfo[];
14
+ enableDnd: boolean;
15
+ isLast: boolean;
16
+ getSuggestOptions: (params: { columnId: PlAdvancedFilterColumnId; searchStr: string; axisIdx?: number }) => (Promise<ListOptionBase<string | number>[]>) |
17
+ ((params: { columnId: SUniversalPColumnId; searchStr: string; axisIdx?: number }) => ListOptionBase<string | number>[]);
18
+ getSuggestModel: (params: { columnId: PlAdvancedFilterColumnId; searchStr: string; axisIdx?: number }) => (Promise<ListOptionBase<string | number>>) |
19
+ ((params: { columnId: PlAdvancedFilterColumnId; searchStr: string; axisIdx?: number }) => ListOptionBase<string | number>);
20
+ onDelete: (columnId: PlAdvancedFilterColumnId) => void;
21
+ onChangeOperand: (op: Operand) => void;
22
+ }>();
23
+
24
+ const filter = defineModel<Filter>({ required: true });
25
+
26
+ async function getSuggestModelMultiFn(id: PlAdvancedFilterColumnId, v: string[], axisIdx?: number): Promise<ListOptionBase<string>[]> {
27
+ return Promise.all(v.map((v) => props.getSuggestModel({ columnId: id, searchStr: v, axisIdx }) as Promise<ListOptionBase<string>>));
28
+ }
29
+ async function getSuggestModelSingleFn(id: PlAdvancedFilterColumnId, v: string, axisIdx?: number): Promise<ListOptionBase<string>> {
30
+ return props.getSuggestModel({ columnId: id, searchStr: v, axisIdx }) as Promise<ListOptionBase<string>>;
31
+ }
32
+ async function getSuggestOptionsFn(id: PlAdvancedFilterColumnId, str: string, axisIdx?: number): Promise<ListOptionBase<string>[]> {
33
+ return props.getSuggestOptions({ columnId: id, searchStr: str, axisIdx }) as Promise<ListOptionBase<string>[]>;
34
+ }
35
+
36
+ function changeFilterType() {
37
+ const nextFilterInfo = getFilterInfo(filter.value.type);
38
+ if (currentSpec.value && nextFilterInfo.supportedFor(currentSpec.value) && isNumericFilter(filter.value)) {
39
+ // no extra changes, previous filter is compatible with new filter type
40
+ return;
41
+ } else if (currentSpec.value && nextFilterInfo.supportedFor(currentSpec.value) && isStringFilter(filter.value)) {
42
+ // erase extra settings for string filter types, save only value and column (for example regex)
43
+ filter.value = {
44
+ ...DEFAULT_FILTERS[filter.value.type],
45
+ value: filter.value.value,
46
+ column: filter.value.column,
47
+ } as Filter;
48
+ } else {
49
+ filter.value = {
50
+ ...DEFAULT_FILTERS[filter.value.type],
51
+ column: filter.value.column,
52
+ };
53
+ }
54
+ }
55
+
56
+ function changeSourceId(newSourceId?: PlAdvancedFilterColumnId) {
57
+ if (!newSourceId) {
58
+ return;
59
+ }
60
+ const newSourceInfo = props.columnOptions.find((v) => v.id === getSourceId(newSourceId));
61
+ if (!newSourceInfo) {
62
+ return;
63
+ }
64
+ const filterInfo = getFilterInfo(filter.value.type);
65
+ const newSourceSpec = getNormalizedSpec(newSourceInfo?.spec);
66
+ if (filterInfo.supportedFor(newSourceSpec)) { // don't do anything except update source id
67
+ filter.value.column = newSourceId;
68
+ } else { // reset to default filter which fits to any column
69
+ filter.value = {
70
+ ...DEFAULT_FILTERS[DEFAULT_FILTER_TYPE],
71
+ column: newSourceId,
72
+ };
73
+ }
74
+ }
75
+
76
+ const inconsistentSourceSelected = computed(() => {
77
+ const selectedOption = props.columnOptions.find((op) => op.id === getSourceId(filter.value.column));
78
+ return selectedOption === undefined;
79
+ });
80
+ const sourceOptions = computed(() => {
81
+ const options = props.columnOptions.map((v) => ({ value: v.id, label: v.label ?? v }));
82
+ if (inconsistentSourceSelected.value) {
83
+ options.unshift({ value: filter.value.column, label: 'Inconsistent value' });
84
+ }
85
+ return options;
86
+ });
87
+
88
+ function getSourceId(column: PlAdvancedFilterColumnId): PlAdvancedFilterColumnId {
89
+ try {
90
+ const parsedColumnId = parseColumnId(column as SUniversalPColumnId);
91
+ if (isFilteredPColumn(parsedColumnId)) {
92
+ return stringifyColumnId(parsedColumnId.source);
93
+ } else {
94
+ return column;
95
+ }
96
+ } catch {
97
+ return column;
98
+ }
99
+ }
100
+
101
+ // similar to FilteredPColumnId but source is stringified and axis filters can be undefined
102
+ type ColumnAsSourceAndFixedAxes = { source: PlAdvancedFilterColumnId; axisFiltersByIndex: Record<number, AxisFilterValue | undefined> };
103
+ function getColumnAsSourceAndFixedAxes(column: PlAdvancedFilterColumnId): ColumnAsSourceAndFixedAxes {
104
+ const sourceId = getSourceId(column);
105
+ const option = props.columnOptions.find((op) => op.id === sourceId);
106
+ const axesToBeFixed = (option?.axesToBeFixed ?? []).reduce((res, item) => {
107
+ res[item.idx] = undefined;
108
+ return res;
109
+ }, {} as Record<number, AxisFilterValue | undefined>);
110
+ try {
111
+ const parsedColumnId = parseColumnId(column as SUniversalPColumnId);
112
+ if (isFilteredPColumn(parsedColumnId)) {
113
+ return {
114
+ source: sourceId,
115
+ axisFiltersByIndex: parsedColumnId.axisFilters.reduce((res, item) => {
116
+ res[item[0]] = item[1];
117
+ return res;
118
+ }, axesToBeFixed),
119
+ };
120
+ }
121
+ } catch {
122
+ return { source: column, axisFiltersByIndex: axesToBeFixed };
123
+ }
124
+ return { source: column, axisFiltersByIndex: axesToBeFixed };
125
+ }
126
+
127
+ function stringifyColumn(value: ColumnAsSourceAndFixedAxes): PlAdvancedFilterColumnId {
128
+ if (Object.keys(value.axisFiltersByIndex).length === 0) {
129
+ return value.source;
130
+ }
131
+ return stringifyColumnId({
132
+ source: parseColumnId(value.source as SUniversalPColumnId) as AnchoredPColumnId,
133
+ axisFilters: Object.entries(value.axisFiltersByIndex).map(([idx, value]) => [Number(idx), value] as AxisFilterByIdx),
134
+ });
135
+ }
136
+
137
+ const columnAsSourceAndFixedAxes = computed({
138
+ get: () => {
139
+ return getColumnAsSourceAndFixedAxes(filter.value.column);
140
+ },
141
+ set: (value) => {
142
+ filter.value.column = stringifyColumn(value);
143
+ },
144
+ });
145
+ function updateAxisFilterValue(idx: number, value: AxisFilterValue | undefined) {
146
+ columnAsSourceAndFixedAxes.value = {
147
+ ...columnAsSourceAndFixedAxes.value,
148
+ axisFiltersByIndex: { ...columnAsSourceAndFixedAxes.value.axisFiltersByIndex, [idx]: value } };
149
+ }
150
+
151
+ const currentOption = computed(() => props.columnOptions.find((op) => op.id === columnAsSourceAndFixedAxes.value.source));
152
+ const currentSpec = computed(() => currentOption.value?.spec ? getNormalizedSpec(currentOption.value.spec) : null);
153
+ const currentType = computed(() => currentSpec.value?.valueType);
154
+ const currentError = computed(() => currentOption.value?.error || inconsistentSourceSelected.value);
155
+
156
+ const filterTypesOptions = computed(() => [...SUPPORTED_FILTER_TYPES].filter((v) =>
157
+ filter.value.type === v || (currentSpec.value ? getFilterInfo(v).supportedFor(currentSpec.value) : true),
158
+ ).map((v) => ({ value: v, label: getFilterInfo(v).label })),
159
+ );
160
+
161
+ const wildcardOptions = computed(() => {
162
+ if (filter.value.type === 'patternFuzzyContainSubsequence') {
163
+ if (currentOption.value?.alphabet === 'nucleotide') {
164
+ return [{ label: 'N', value: 'N' }];
165
+ }
166
+ if (currentOption.value?.alphabet === 'aminoacid') {
167
+ return [{ label: 'X', value: 'X' }];
168
+ }
169
+ return [...new Set(filter.value.value.split(''))].sort().map((v) => ({ value: v, label: v }));
170
+ }
171
+ return [];
172
+ });
173
+
174
+ const stringMatchesError = computed(() => {
175
+ if (filter.value.type !== 'patternMatchesRegularExpression') {
176
+ return false;
177
+ }
178
+ try {
179
+ new RegExp(filter.value.value);
180
+ return false;
181
+ } catch {
182
+ return true;
183
+ }
184
+ });
185
+
186
+ </script>
187
+ <template>
188
+ <div :class="$style.filterWrapper">
189
+ <!-- top element - column selector / column label - for all filter types-->
190
+ <div v-if="enableDnd" :class="[$style.top, $style.columnChip, {[$style.error]: currentError}]">
191
+ <div :class="[$style.typeIcon, {[$style.error]: currentError}]">
192
+ <PlIcon16 v-if="currentError" name="warning"/>
193
+ <PlIcon16 v-else :name="currentType === 'String' || currentType === undefined ? 'cell-type-txt' : 'cell-type-num'"/>
194
+ </div>
195
+ <div :class="$style.titleWrapper" :title="currentOption?.label ?? ''">
196
+ <div :class="$style.title">
197
+ {{ inconsistentSourceSelected ? 'Inconsistent value' : currentOption?.label ?? filter.column }}
198
+ </div>
199
+ </div>
200
+ <div :class="$style.closeIcon" @click="onDelete(filter.column)">
201
+ <PlIcon16 name="close"/>
202
+ </div>
203
+ </div>
204
+ <div v-else :class="$style.top" >
205
+ <PlDropdown
206
+ v-model="columnAsSourceAndFixedAxes.source"
207
+ :errorStatus="currentError"
208
+ :options="sourceOptions"
209
+ :style="{width: '100%'}"
210
+ group-position="top-left"
211
+ @update:model-value="changeSourceId"
212
+ />
213
+ <div :class="$style.closeButton" @click="onDelete(filter.column)">
214
+ <PlIcon16 name="close"/>
215
+ </div>
216
+ </div>
217
+
218
+ <div v-if="currentOption?.axesToBeFixed?.length" :class="$style.fixedAxesBlock">
219
+ <template v-for="value in currentOption?.axesToBeFixed" :key="value.idx">
220
+ <PlAutocomplete
221
+ v-model="columnAsSourceAndFixedAxes.axisFiltersByIndex[value.idx]"
222
+ :label="value.label"
223
+ :options-search="(str) => getSuggestOptionsFn(columnAsSourceAndFixedAxes.source, str, value.idx)"
224
+ :model-search="(v) => getSuggestModelSingleFn(columnAsSourceAndFixedAxes.source, v as string, value.idx)"
225
+ :disabled="inconsistentSourceSelected"
226
+ :clearable="true"
227
+ @update:model-value="(v) => updateAxisFilterValue(value.idx, v)"
228
+ />
229
+ </template>
230
+ </div>
231
+
232
+ <!-- middle - filter type selector - for all filter types -->
233
+ <div :class="filter.type === 'isNA' || filter.type === 'isNotNA' ? $style.bottom : $style.middle">
234
+ <PlDropdown
235
+ v-model="filter.type"
236
+ :options="filterTypesOptions"
237
+ :group-position="filter.type === 'isNA' || filter.type === 'isNotNA' ? 'bottom' : 'middle'"
238
+ @update:model-value="changeFilterType"
239
+ />
240
+ </div>
241
+
242
+ <!-- middle - for fuzzy contains filter -->
243
+ <template v-if="filter.type === 'patternFuzzyContainSubsequence'">
244
+ <div :class="$style.middle">
245
+ <PlTextField
246
+ v-model="filter.value"
247
+ placeholder="Substring"
248
+ group-position="middle"
249
+ />
250
+ </div>
251
+ <div :class="$style.innerSection">
252
+ <Slider
253
+ v-model="filter.maxEdits"
254
+ :max="5"
255
+ breakpoints label="Maximum number of substitutions and indels"
256
+ />
257
+ <PlToggleSwitch
258
+ v-model="filter.substitutionsOnly"
259
+ label="Substitutions only"
260
+ />
261
+ </div>
262
+ </template>
263
+
264
+ <!-- bottom element - individual settings for every filter type -->
265
+ <div :class="$style.bottom">
266
+ <template v-if="filter.type === 'patternEquals' || filter.type === 'patternNotEquals'" >
267
+ <PlAutocomplete
268
+ v-model="filter.value"
269
+ :options-search="(str) => getSuggestOptionsFn(columnAsSourceAndFixedAxes.source, str)"
270
+ :model-search="(v) => getSuggestModelSingleFn(columnAsSourceAndFixedAxes.source, v as string)"
271
+ :disabled="inconsistentSourceSelected"
272
+ :clearable="true"
273
+ group-position="bottom"
274
+ />
275
+ </template>
276
+ <template v-if="filter.type === 'inSet' || filter.type === 'notInSet'" >
277
+ <PlAutocompleteMulti
278
+ v-model="filter.value"
279
+ :options-search="(str) => getSuggestOptionsFn(columnAsSourceAndFixedAxes.source, str)"
280
+ :model-search="(v) => getSuggestModelMultiFn(columnAsSourceAndFixedAxes.source, v as string[])"
281
+ :disabled="inconsistentSourceSelected"
282
+ group-position="bottom"
283
+ />
284
+ </template>
285
+ <PlNumberField
286
+ v-if="isNumericFilter(filter)"
287
+ v-model="filter.x"
288
+ group-position="bottom"
289
+ />
290
+ <PlTextField
291
+ v-if="filter.type === 'patternContainSubsequence' || filter.type === 'patternNotContainSubsequence'"
292
+ v-model="filter.value"
293
+ placeholder="Substring"
294
+ group-position="bottom"
295
+ />
296
+ <PlTextField
297
+ v-if="filter.type === 'patternMatchesRegularExpression'"
298
+ v-model="filter.value"
299
+ :error="stringMatchesError ? 'Regular expression is not valid' : undefined"
300
+ placeholder="Regular expression"
301
+ group-position="bottom"
302
+ />
303
+ <PlDropdown
304
+ v-if="filter.type === 'patternFuzzyContainSubsequence'"
305
+ v-model="filter.wildcard"
306
+ clearable
307
+ placeholder="Wildcard value"
308
+ :options="wildcardOptions"
309
+ group-position="bottom"
310
+ />
311
+ </div>
312
+ </div>
313
+ <OperandButton
314
+ :class="$style.buttonWrapper"
315
+ :active="operand"
316
+ :disabled="isLast"
317
+ :on-select="onChangeOperand"
318
+ />
319
+ </template>
320
+
321
+ <style module>
322
+ .filterWrapper {
323
+ position: relative;
324
+ display: flex;
325
+ flex-direction: column;
326
+ margin-bottom: 8px;
327
+ width: 100%;
328
+ cursor: default;
329
+ }
330
+
331
+ .typeIcon {
332
+ display: inline-flex;
333
+ margin-right: 8px;
334
+ }
335
+
336
+ .typeIcon.error {
337
+ --icon-color: var(--txt-error);
338
+ }
339
+
340
+ .closeIcon {
341
+ display: inline-flex;
342
+ margin-left: 12px;
343
+ cursor: pointer;
344
+ }
345
+
346
+ .titleWrapper {
347
+ flex-grow: 1;
348
+ overflow: hidden;
349
+ }
350
+ .title {
351
+ overflow: hidden;
352
+ color: var(--txt-01);
353
+ text-overflow: ellipsis;
354
+ white-space: nowrap;
355
+ font-size: 14px;
356
+ font-weight: 500;
357
+ line-height: 20px;
358
+ }
359
+
360
+ .columnChip {
361
+ width: 100%;
362
+ display: flex;
363
+ padding: 10px 12px;
364
+ align-items: center;
365
+ border-radius: 6px;
366
+ border: 1px solid var(--txt-01);
367
+ border-bottom-right-radius: 0;
368
+ border-bottom-left-radius: 0;
369
+
370
+ &.error {
371
+ border-color: var(--txt-error);
372
+ }
373
+ }
374
+
375
+ .innerSection {
376
+ border: 1px solid var(--txt-01);
377
+ border-top: none;
378
+ padding: 16px 12px;
379
+ }
380
+
381
+ .closeButton {
382
+ border: 1px solid var(--txt-01);
383
+ border-top-right-radius: 6px;
384
+ border-left: none;
385
+ width: 40px;
386
+ height: 40px;
387
+ display: flex;
388
+ justify-content: center;
389
+ align-items: center;
390
+ flex-shrink: 0;
391
+ cursor: pointer;
392
+ }
393
+
394
+ .top {
395
+ position: relative;
396
+ display: flex;
397
+ width: 100%;
398
+ z-index: 1;
399
+ background: #fff;
400
+ }
401
+
402
+ .fixedAxesBlock {
403
+ position: relative;
404
+ display: flex;
405
+ flex-direction: column;
406
+ padding: 12px 8px;
407
+ gap: 12px;
408
+ border-left: 1px solid var(--txt-01);
409
+ border-right: 1px solid var(--txt-01);
410
+ }
411
+
412
+ .fixedAxesBlock > * {
413
+ background: #fff;
414
+ }
415
+
416
+ .middle, .bottom {
417
+ position: relative;
418
+ margin-top: -1px;
419
+ background: #fff;
420
+ }
421
+
422
+ .buttonWrapper {
423
+ margin-bottom: 8px;
424
+ }
425
+ </style>
@@ -0,0 +1,42 @@
1
+ import type { PlAdvancedFilterColumnId, Filter, FilterType, SupportedFilterTypes } from './types';
2
+
3
+ export const SUPPORTED_FILTER_TYPES = new Set<SupportedFilterTypes>([
4
+ 'isNA',
5
+ 'isNotNA',
6
+ 'greaterThan',
7
+ 'greaterThanOrEqual',
8
+ 'lessThan',
9
+ 'lessThanOrEqual',
10
+ 'patternEquals',
11
+ 'patternNotEquals',
12
+ 'patternContainSubsequence',
13
+ 'patternNotContainSubsequence',
14
+ 'equal',
15
+ 'notEqual',
16
+ 'patternFuzzyContainSubsequence',
17
+ 'patternMatchesRegularExpression',
18
+ 'inSet',
19
+ 'notInSet',
20
+ ]);
21
+
22
+ export const DEFAULT_FILTER_TYPE: FilterType = 'isNA';
23
+
24
+ const emptyCommonPart = { column: '' as PlAdvancedFilterColumnId };
25
+ export const DEFAULT_FILTERS: Record<SupportedFilterTypes, Filter> = {
26
+ isNA: { type: 'isNA', ...emptyCommonPart },
27
+ isNotNA: { type: 'isNotNA', ...emptyCommonPart },
28
+ lessThan: { type: 'lessThan', x: undefined, ...emptyCommonPart },
29
+ lessThanOrEqual: { type: 'lessThanOrEqual', x: undefined, ...emptyCommonPart },
30
+ patternEquals: { type: 'patternEquals', value: undefined, ...emptyCommonPart },
31
+ patternNotEquals: { type: 'patternNotEquals', value: undefined, ...emptyCommonPart },
32
+ greaterThan: { type: 'greaterThan', x: undefined, ...emptyCommonPart },
33
+ greaterThanOrEqual: { type: 'greaterThanOrEqual', x: undefined, ...emptyCommonPart },
34
+ patternContainSubsequence: { type: 'patternContainSubsequence', value: '', ...emptyCommonPart },
35
+ patternNotContainSubsequence: { type: 'patternNotContainSubsequence', value: '', ...emptyCommonPart },
36
+ patternFuzzyContainSubsequence: { type: 'patternFuzzyContainSubsequence', maxEdits: 2, substitutionsOnly: false, wildcard: undefined, value: '', ...emptyCommonPart },
37
+ patternMatchesRegularExpression: { type: 'patternMatchesRegularExpression', value: '', ...emptyCommonPart },
38
+ equal: { type: 'equal', x: undefined, ...emptyCommonPart },
39
+ notEqual: { type: 'notEqual', x: undefined, ...emptyCommonPart },
40
+ inSet: { type: 'inSet', value: [], ...emptyCommonPart },
41
+ notInSet: { type: 'notInSet', value: [], ...emptyCommonPart },
42
+ };
@@ -0,0 +1 @@
1
+ export { default as PlAdvancedFilter } from './PlAdvancedFilter.vue';