@gitlab/ui 60.2.0 → 61.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/components/base/filtered_search/filtered_search.js +1 -4
- package/dist/components/base/filtered_search/filtered_search_suggestion.js +1 -1
- package/dist/components/base/filtered_search/filtered_search_suggestion_list.js +30 -16
- package/dist/components/base/filtered_search/filtered_search_term.js +4 -6
- package/dist/components/base/filtered_search/filtered_search_token.js +16 -17
- package/dist/components/base/filtered_search/filtered_search_token_segment.js +23 -14
- package/dist/components/base/filtered_search/filtered_search_utils.js +78 -1
- package/dist/utils/number_utils.js +22 -1
- package/package.json +1 -1
- package/src/components/base/filtered_search/__snapshots__/filtered_search_term.spec.js.snap +2 -0
- package/src/components/base/filtered_search/filtered_search.md +5 -3
- package/src/components/base/filtered_search/filtered_search.stories.js +8 -5
- package/src/components/base/filtered_search/filtered_search.vue +1 -11
- package/src/components/base/filtered_search/filtered_search_suggestion.md +8 -2
- package/src/components/base/filtered_search/filtered_search_suggestion.vue +1 -0
- package/src/components/base/filtered_search/filtered_search_suggestion_list.spec.js +61 -64
- package/src/components/base/filtered_search/filtered_search_suggestion_list.vue +39 -20
- package/src/components/base/filtered_search/filtered_search_term.spec.js +11 -28
- package/src/components/base/filtered_search/filtered_search_term.vue +6 -17
- package/src/components/base/filtered_search/filtered_search_token.spec.js +3 -22
- package/src/components/base/filtered_search/filtered_search_token.vue +8 -16
- package/src/components/base/filtered_search/filtered_search_token_segment.spec.js +18 -1
- package/src/components/base/filtered_search/filtered_search_token_segment.stories.js +9 -0
- package/src/components/base/filtered_search/filtered_search_token_segment.vue +35 -12
- package/src/components/base/filtered_search/filtered_search_utils.js +69 -0
- package/src/components/base/filtered_search/filtered_search_utils.spec.js +32 -1
- package/src/utils/number_utils.js +21 -0
- package/src/utils/number_utils.spec.js +42 -0
|
@@ -304,8 +304,8 @@ const tokens = [
|
|
|
304
304
|
unique: true,
|
|
305
305
|
token: GlFilteredSearchToken,
|
|
306
306
|
options: [
|
|
307
|
-
{ icon: 'eye-slash', value: '
|
|
308
|
-
{ icon: 'eye', value: '
|
|
307
|
+
{ icon: 'eye-slash', value: 'true', title: 'Yes' },
|
|
308
|
+
{ icon: 'eye', value: 'false', title: 'No' },
|
|
309
309
|
],
|
|
310
310
|
},
|
|
311
311
|
];
|
|
@@ -410,8 +410,8 @@ export const WithFriendlyText = () => ({
|
|
|
410
410
|
unique: true,
|
|
411
411
|
token: GlFilteredSearchToken,
|
|
412
412
|
options: [
|
|
413
|
-
{ icon: 'eye-slash', value: '
|
|
414
|
-
{ icon: 'eye', value: '
|
|
413
|
+
{ icon: 'eye-slash', value: 'true', title: 'Yes' },
|
|
414
|
+
{ icon: 'eye', value: 'false', title: 'No' },
|
|
415
415
|
],
|
|
416
416
|
},
|
|
417
417
|
],
|
|
@@ -480,6 +480,9 @@ export const WithMultiSelect = () => {
|
|
|
480
480
|
isLastUser(index) {
|
|
481
481
|
return index === this.selectedUsers.length - 1;
|
|
482
482
|
},
|
|
483
|
+
key(user, index) {
|
|
484
|
+
return `${user.id}-${index}`;
|
|
485
|
+
},
|
|
483
486
|
},
|
|
484
487
|
watch: {
|
|
485
488
|
// eslint-disable-next-line func-names
|
|
@@ -514,7 +517,7 @@ export const WithMultiSelect = () => {
|
|
|
514
517
|
</template>
|
|
515
518
|
</template>
|
|
516
519
|
<template #suggestions>
|
|
517
|
-
<gl-filtered-search-suggestion :key="user
|
|
520
|
+
<gl-filtered-search-suggestion :key="key(user, index)" v-for="(user, index) in filteredUsers" :value="user.username">
|
|
518
521
|
<div class="gl-display-flex gl-align-items-center">
|
|
519
522
|
<gl-icon
|
|
520
523
|
v-if="config.multiSelect"
|
|
@@ -345,9 +345,6 @@ export default {
|
|
|
345
345
|
*/
|
|
346
346
|
this.$emit('submit', normalizeTokens(cloneDeep(this.tokens)));
|
|
347
347
|
},
|
|
348
|
-
hasTitleSlot() {
|
|
349
|
-
return Boolean(this.$scopedSlots.title);
|
|
350
|
-
},
|
|
351
348
|
},
|
|
352
349
|
};
|
|
353
350
|
</script>
|
|
@@ -403,14 +400,7 @@ export default {
|
|
|
403
400
|
@split="createTokens(idx, $event)"
|
|
404
401
|
@previous="activatePreviousToken"
|
|
405
402
|
@next="activateNextToken"
|
|
406
|
-
|
|
407
|
-
<template
|
|
408
|
-
v-if="hasTitleSlot() && getTokenComponent(token.type).name === 'GlFilteredSearchTerm'"
|
|
409
|
-
#title="title"
|
|
410
|
-
>
|
|
411
|
-
<slot name="title" v-bind="title"></slot>
|
|
412
|
-
</template>
|
|
413
|
-
</component>
|
|
403
|
+
/>
|
|
414
404
|
</div>
|
|
415
405
|
<portal-target
|
|
416
406
|
ref="menu"
|
|
@@ -3,7 +3,13 @@ suggestions in a top-level suggestion list:
|
|
|
3
3
|
|
|
4
4
|
```html
|
|
5
5
|
<gl-filtered-search-suggestion-list>
|
|
6
|
-
<gl-filtered-search-suggestion value="foo">Example suggestion</gl-filtered-search-suggestion>
|
|
7
|
-
<gl-filtered-search-suggestion value="bar">Example suggestion 2</gl-filtered-search-suggestion>
|
|
6
|
+
<gl-filtered-search-suggestion value="foo" key="foo-0">Example suggestion</gl-filtered-search-suggestion>
|
|
7
|
+
<gl-filtered-search-suggestion value="bar" key="bar-1">Example suggestion 2</gl-filtered-search-suggestion>
|
|
8
8
|
</gl-filtered-search-suggestion-list>
|
|
9
9
|
```
|
|
10
|
+
|
|
11
|
+
> NOTE: Provide a `key` to suggestions of the form `${value}-${index}` (or
|
|
12
|
+
> similar). While using the index in keys is usually frowned upon for
|
|
13
|
+
> performance reasons, the current implementation relies on all suggestions
|
|
14
|
+
> getting destroyed and recreated to keep rendering order in sync with
|
|
15
|
+
> <kbd>Up</kbd>/<kbd>Down</kbd> keyboard interaction.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nextTick } from 'vue';
|
|
1
2
|
import { shallowMount, mount } from '@vue/test-utils';
|
|
2
3
|
import FilteredSearchSuggestion from './filtered_search_suggestion.vue';
|
|
3
4
|
import FilteredSearchSuggestionList from './filtered_search_suggestion_list.vue';
|
|
@@ -32,40 +33,42 @@ describe('Filtered search suggestion list component', () => {
|
|
|
32
33
|
expect(wrapper.vm.getValue()).toBe(null);
|
|
33
34
|
});
|
|
34
35
|
|
|
35
|
-
it('selects first item on nextItem call', () => {
|
|
36
|
+
it('selects first item on nextItem call', async () => {
|
|
36
37
|
wrapper.vm.nextItem();
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
38
|
+
await nextTick();
|
|
39
|
+
expect(wrapper.vm.getValue()).toBe(stubs[0].value);
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
it('deselects first item on prevItem call', () => {
|
|
42
|
+
it('deselects first item on prevItem call', async () => {
|
|
43
43
|
wrapper.vm.nextItem();
|
|
44
44
|
wrapper.vm.prevItem();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
});
|
|
45
|
+
await nextTick();
|
|
46
|
+
expect(wrapper.vm.getValue()).toBe(null);
|
|
48
47
|
});
|
|
49
48
|
|
|
50
|
-
it('deselects last item on nextItem call', () => {
|
|
49
|
+
it('deselects last item on nextItem call', async () => {
|
|
51
50
|
stubs.forEach(() => wrapper.vm.nextItem());
|
|
52
51
|
wrapper.vm.nextItem();
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
});
|
|
52
|
+
await nextTick();
|
|
53
|
+
expect(wrapper.vm.getValue()).toBe(null);
|
|
56
54
|
});
|
|
57
55
|
|
|
58
|
-
it('remove selection if suggestion is unregistered', () => {
|
|
56
|
+
it('remove selection if suggestion is unregistered', async () => {
|
|
59
57
|
wrapper.vm.nextItem();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
58
|
+
await nextTick();
|
|
59
|
+
wrapper.vm.unregister(stubs[0]);
|
|
60
|
+
await nextTick();
|
|
61
|
+
expect(wrapper.vm.getValue()).toBe(null);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('selects correct suggestion when item (un)registration is late', async () => {
|
|
65
|
+
// Initially stub2 is at index 1.
|
|
66
|
+
await wrapper.setProps({ initialValue: 'stub2' });
|
|
67
|
+
// Remove item at index 0, so stub2 moves to index 0
|
|
68
|
+
wrapper.vm.unregister(stubs[0]);
|
|
69
|
+
await nextTick();
|
|
70
|
+
// stub2 should still be selected
|
|
71
|
+
expect(wrapper.vm.getValue()).toBe('stub2');
|
|
69
72
|
});
|
|
70
73
|
});
|
|
71
74
|
});
|
|
@@ -105,85 +108,79 @@ describe('Filtered search suggestion list component', () => {
|
|
|
105
108
|
});
|
|
106
109
|
});
|
|
107
110
|
|
|
108
|
-
it('selects first suggestion', () => {
|
|
111
|
+
it('selects first suggestion', async () => {
|
|
109
112
|
wrapper.vm.nextItem();
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
});
|
|
113
|
+
await nextTick();
|
|
114
|
+
expect(wrapper.vm.getValue()).toBe('One');
|
|
113
115
|
});
|
|
114
116
|
|
|
115
|
-
it('selects second suggestion', () => {
|
|
117
|
+
it('selects second suggestion', async () => {
|
|
116
118
|
wrapper.vm.nextItem();
|
|
117
119
|
wrapper.vm.nextItem();
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
});
|
|
120
|
+
await nextTick();
|
|
121
|
+
expect(wrapper.vm.getValue()).toBe('Two');
|
|
121
122
|
});
|
|
122
123
|
|
|
123
|
-
it('deselects first suggestion after list end', () => {
|
|
124
|
+
it('deselects first suggestion after list end', async () => {
|
|
124
125
|
wrapper.vm.nextItem();
|
|
125
126
|
wrapper.vm.nextItem();
|
|
126
127
|
wrapper.vm.nextItem();
|
|
127
128
|
wrapper.vm.nextItem();
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
});
|
|
129
|
+
await nextTick();
|
|
130
|
+
expect(wrapper.vm.getValue()).toBe(null);
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
-
it('deselects first suggestion after list start', () => {
|
|
133
|
+
it('deselects first suggestion after list start', async () => {
|
|
134
134
|
wrapper.vm.nextItem();
|
|
135
135
|
wrapper.vm.prevItem();
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
});
|
|
136
|
+
await nextTick();
|
|
137
|
+
expect(wrapper.vm.getValue()).toBe(null);
|
|
139
138
|
});
|
|
140
139
|
|
|
141
|
-
it('selects last suggestion in circle when selecting previous item', () => {
|
|
140
|
+
it('selects last suggestion in circle when selecting previous item', async () => {
|
|
142
141
|
wrapper.vm.nextItem();
|
|
143
142
|
wrapper.vm.prevItem();
|
|
144
143
|
wrapper.vm.prevItem();
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
});
|
|
144
|
+
await nextTick();
|
|
145
|
+
expect(wrapper.vm.getValue()).toBe(false);
|
|
148
146
|
});
|
|
149
147
|
|
|
150
|
-
it('selects first suggestion in circle when selecting next item', () => {
|
|
148
|
+
it('selects first suggestion in circle when selecting next item', async () => {
|
|
151
149
|
wrapper.vm.nextItem();
|
|
152
150
|
wrapper.vm.nextItem();
|
|
153
151
|
wrapper.vm.nextItem();
|
|
154
152
|
wrapper.vm.nextItem();
|
|
155
153
|
wrapper.vm.nextItem();
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
});
|
|
154
|
+
await nextTick();
|
|
155
|
+
expect(wrapper.vm.getValue()).toBe('One');
|
|
159
156
|
});
|
|
160
157
|
|
|
161
|
-
it('highlights suggestion if initial-value is provided', () => {
|
|
162
|
-
wrapper.setProps({ initialValue: 'Two' });
|
|
163
|
-
|
|
164
|
-
expect(wrapper.find('.gl-filtered-search-suggestion-active').text()).toBe('Two');
|
|
165
|
-
});
|
|
158
|
+
it('highlights suggestion if initial-value is provided', async () => {
|
|
159
|
+
await wrapper.setProps({ initialValue: 'Two' });
|
|
160
|
+
expect(wrapper.find('.gl-filtered-search-suggestion-active').text()).toBe('Two');
|
|
166
161
|
});
|
|
167
162
|
|
|
168
163
|
it('highlights suggestion if initial-value is provided, regardless of case sensitivity', async () => {
|
|
169
|
-
wrapper.setProps({ initialValue: 'two' });
|
|
170
|
-
|
|
171
|
-
expect(wrapper.find('.gl-filtered-search-suggestion-active').text()).toBe('Two');
|
|
172
|
-
});
|
|
164
|
+
await wrapper.setProps({ initialValue: 'two' });
|
|
165
|
+
expect(wrapper.find('.gl-filtered-search-suggestion-active').text()).toBe('Two');
|
|
173
166
|
});
|
|
174
167
|
|
|
175
168
|
it('highlights suggestion if initial-value is provided, regardless of falsiness', async () => {
|
|
176
|
-
wrapper.setProps({ initialValue: false });
|
|
177
|
-
|
|
178
|
-
expect(wrapper.find('.gl-filtered-search-suggestion-active').text()).toBe('Three');
|
|
179
|
-
});
|
|
169
|
+
await wrapper.setProps({ initialValue: false });
|
|
170
|
+
expect(wrapper.find('.gl-filtered-search-suggestion-active').text()).toBe('Three');
|
|
180
171
|
});
|
|
181
172
|
|
|
182
|
-
it('
|
|
183
|
-
wrapper.setProps({ initialValue: '
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
173
|
+
it('highlights first suggestion if initial-value is provided, deselected then selected', async () => {
|
|
174
|
+
await wrapper.setProps({ initialValue: 'One' });
|
|
175
|
+
wrapper.vm.prevItem();
|
|
176
|
+
wrapper.vm.nextItem();
|
|
177
|
+
await nextTick();
|
|
178
|
+
expect(wrapper.find('.gl-filtered-search-suggestion-active').text()).toBe('One');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('does not highlight anything if initial-value matches nothing', async () => {
|
|
182
|
+
await wrapper.setProps({ initialValue: 'missing' });
|
|
183
|
+
expect(wrapper.find('.gl-filtered-search-suggestion-active').exists()).toBe(false);
|
|
187
184
|
});
|
|
188
185
|
|
|
189
186
|
it('applies the injected suggestion-list-class to the dropdown', () => {
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
import { stepIndexAndWrap } from './filtered_search_utils';
|
|
3
|
+
|
|
4
|
+
const DEFER_TO_INITIAL_VALUE = -1;
|
|
5
|
+
const NO_ACTIVE_ITEM = -2;
|
|
6
|
+
|
|
2
7
|
export default {
|
|
3
8
|
name: 'GlFilteredSearchSuggestionList',
|
|
4
9
|
inject: ['suggestionsListClass'],
|
|
@@ -17,18 +22,27 @@ export default {
|
|
|
17
22
|
default: null,
|
|
18
23
|
},
|
|
19
24
|
},
|
|
25
|
+
|
|
20
26
|
data() {
|
|
21
27
|
return {
|
|
22
|
-
activeIdx:
|
|
28
|
+
activeIdx: DEFER_TO_INITIAL_VALUE,
|
|
23
29
|
registeredItems: [],
|
|
24
30
|
};
|
|
25
31
|
},
|
|
26
32
|
|
|
27
33
|
computed: {
|
|
34
|
+
initialActiveIdx() {
|
|
35
|
+
return this.registeredItems.findIndex((item) =>
|
|
36
|
+
this.valuesMatch(item.value, this.initialValue)
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
initialActiveItem() {
|
|
40
|
+
return this.registeredItems[this.initialActiveIdx];
|
|
41
|
+
},
|
|
28
42
|
activeItem() {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
43
|
+
if (this.activeIdx === NO_ACTIVE_ITEM) return null;
|
|
44
|
+
if (this.activeIdx === DEFER_TO_INITIAL_VALUE) return this.initialActiveItem;
|
|
45
|
+
return this.registeredItems[this.activeIdx];
|
|
32
46
|
},
|
|
33
47
|
listClasses() {
|
|
34
48
|
return [this.suggestionsListClass(), 'dropdown-menu gl-filtered-search-suggestion-list'];
|
|
@@ -36,10 +50,8 @@ export default {
|
|
|
36
50
|
},
|
|
37
51
|
|
|
38
52
|
watch: {
|
|
39
|
-
initialValue(
|
|
40
|
-
this.activeIdx =
|
|
41
|
-
this.valuesMatch(item.value, newValue)
|
|
42
|
-
);
|
|
53
|
+
initialValue() {
|
|
54
|
+
this.activeIdx = DEFER_TO_INITIAL_VALUE;
|
|
43
55
|
},
|
|
44
56
|
},
|
|
45
57
|
|
|
@@ -53,31 +65,38 @@ export default {
|
|
|
53
65
|
},
|
|
54
66
|
register(item) {
|
|
55
67
|
this.registeredItems.push(item);
|
|
56
|
-
if (this.valuesMatch(item.value, this.initialValue)) {
|
|
57
|
-
this.activeIdx = this.registeredItems.length - 1;
|
|
58
|
-
}
|
|
59
68
|
},
|
|
60
69
|
unregister(item) {
|
|
61
70
|
const idx = this.registeredItems.indexOf(item);
|
|
62
71
|
if (idx !== -1) {
|
|
63
72
|
this.registeredItems.splice(idx, 1);
|
|
64
73
|
if (idx === this.activeIdx) {
|
|
65
|
-
this.activeIdx =
|
|
74
|
+
this.activeIdx = DEFER_TO_INITIAL_VALUE;
|
|
66
75
|
}
|
|
67
76
|
}
|
|
68
77
|
},
|
|
69
78
|
nextItem() {
|
|
70
|
-
|
|
71
|
-
this.activeIdx += 1;
|
|
72
|
-
} else {
|
|
73
|
-
this.activeIdx = 0;
|
|
74
|
-
}
|
|
79
|
+
this.stepItem(1, this.registeredItems.length - 1);
|
|
75
80
|
},
|
|
76
81
|
prevItem() {
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
this.stepItem(-1, 0);
|
|
83
|
+
},
|
|
84
|
+
stepItem(direction, endIdx) {
|
|
85
|
+
if (
|
|
86
|
+
this.activeIdx === endIdx ||
|
|
87
|
+
(this.activeIdx === DEFER_TO_INITIAL_VALUE && this.initialActiveIdx === endIdx)
|
|
88
|
+
) {
|
|
89
|
+
// The user wants to move past the end of the list, so ensure nothing is selected.
|
|
90
|
+
this.activeIdx = NO_ACTIVE_ITEM;
|
|
79
91
|
} else {
|
|
80
|
-
|
|
92
|
+
const index =
|
|
93
|
+
this.activeIdx === DEFER_TO_INITIAL_VALUE
|
|
94
|
+
? // Currently active item is set by initialValue (i.e., text input matching),
|
|
95
|
+
// so step relative to that.
|
|
96
|
+
this.initialActiveIdx
|
|
97
|
+
: // Otherwise, step relative to the explicitly (via up/down arrows) activated item.
|
|
98
|
+
this.activeIdx;
|
|
99
|
+
this.activeIdx = stepIndexAndWrap(index, direction, this.registeredItems.length);
|
|
81
100
|
}
|
|
82
101
|
},
|
|
83
102
|
getValue() {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { nextTick } from 'vue';
|
|
2
2
|
import { shallowMount } from '@vue/test-utils';
|
|
3
|
-
import GlFilteredSearchSuggestion from './filtered_search_suggestion.vue';
|
|
4
3
|
import FilteredSearchTerm from './filtered_search_term.vue';
|
|
5
4
|
import { INTENT_ACTIVATE_PREVIOUS } from './filtered_search_utils';
|
|
6
5
|
|
|
@@ -23,39 +22,21 @@ describe('Filtered search term', () => {
|
|
|
23
22
|
const segmentStub = {
|
|
24
23
|
name: 'gl-filtered-search-token-segment-stub',
|
|
25
24
|
template: '<div><slot name="view"></slot><slot name="suggestions"></slot></div>',
|
|
26
|
-
props: ['searchInputAttributes', 'isLastToken', 'currentValue', 'viewOnly'],
|
|
25
|
+
props: ['searchInputAttributes', 'isLastToken', 'currentValue', 'viewOnly', 'options'],
|
|
27
26
|
};
|
|
28
27
|
|
|
29
|
-
const createComponent = (props
|
|
28
|
+
const createComponent = (props) => {
|
|
30
29
|
wrapper = shallowMount(FilteredSearchTerm, {
|
|
31
30
|
propsData: { ...defaultProps, ...props },
|
|
32
31
|
stubs: {
|
|
33
32
|
'gl-filtered-search-token-segment': segmentStub,
|
|
34
33
|
},
|
|
35
|
-
...options,
|
|
36
34
|
});
|
|
37
35
|
};
|
|
38
36
|
|
|
39
37
|
const findSearchInput = () => wrapper.find('input');
|
|
40
38
|
const findTokenSegmentComponent = () => wrapper.findComponent(segmentStub);
|
|
41
39
|
|
|
42
|
-
it('renders title slot', async () => {
|
|
43
|
-
createComponent(
|
|
44
|
-
{ availableTokens, active: true, value: { data: 'test1' } },
|
|
45
|
-
{
|
|
46
|
-
scopedSlots: {
|
|
47
|
-
title: '<div slot-scope="{ value }">New {{value}}</div>',
|
|
48
|
-
},
|
|
49
|
-
}
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
await nextTick();
|
|
53
|
-
|
|
54
|
-
expect(wrapper.findAllComponents(GlFilteredSearchSuggestion).at(0).text()).toBe(
|
|
55
|
-
'New test1-foo'
|
|
56
|
-
);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
40
|
it('renders value in inactive mode', () => {
|
|
60
41
|
createComponent({ value: { data: 'test-value' } });
|
|
61
42
|
expect(wrapper.html()).toMatchSnapshot();
|
|
@@ -76,7 +57,7 @@ describe('Filtered search term', () => {
|
|
|
76
57
|
|
|
77
58
|
await nextTick();
|
|
78
59
|
|
|
79
|
-
expect(
|
|
60
|
+
expect(findTokenSegmentComponent().props('options')).toHaveLength(2);
|
|
80
61
|
});
|
|
81
62
|
|
|
82
63
|
it.each`
|
|
@@ -120,12 +101,14 @@ describe('Filtered search term', () => {
|
|
|
120
101
|
viewOnly,
|
|
121
102
|
});
|
|
122
103
|
|
|
123
|
-
expect(findTokenSegmentComponent().props()).toEqual(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
104
|
+
expect(findTokenSegmentComponent().props()).toEqual(
|
|
105
|
+
expect.objectContaining({
|
|
106
|
+
searchInputAttributes,
|
|
107
|
+
isLastToken,
|
|
108
|
+
currentValue,
|
|
109
|
+
viewOnly,
|
|
110
|
+
})
|
|
111
|
+
);
|
|
129
112
|
});
|
|
130
113
|
|
|
131
114
|
it('by default sets `viewOnly` to false on `GlFilteredSearchTokenSegment`', () => {
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import GlFilteredSearchSuggestion from './filtered_search_suggestion.vue';
|
|
3
2
|
import GlFilteredSearchTokenSegment from './filtered_search_token_segment.vue';
|
|
4
|
-
import { INTENT_ACTIVATE_PREVIOUS } from './filtered_search_utils';
|
|
3
|
+
import { INTENT_ACTIVATE_PREVIOUS, match, tokenToOption } from './filtered_search_utils';
|
|
5
4
|
|
|
6
5
|
export default {
|
|
7
6
|
name: 'GlFilteredSearchTerm',
|
|
8
7
|
components: {
|
|
9
8
|
GlFilteredSearchTokenSegment,
|
|
10
|
-
GlFilteredSearchSuggestion,
|
|
11
9
|
},
|
|
12
10
|
inheritAttrs: false,
|
|
13
11
|
props: {
|
|
@@ -77,9 +75,9 @@ export default {
|
|
|
77
75
|
},
|
|
78
76
|
computed: {
|
|
79
77
|
suggestedTokens() {
|
|
80
|
-
return this.availableTokens
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
return this.availableTokens
|
|
79
|
+
.filter((token) => match(token.title, this.value.data))
|
|
80
|
+
.map(tokenToOption);
|
|
83
81
|
},
|
|
84
82
|
internalValue: {
|
|
85
83
|
get() {
|
|
@@ -143,6 +141,7 @@ export default {
|
|
|
143
141
|
<gl-filtered-search-token-segment
|
|
144
142
|
ref="segment"
|
|
145
143
|
v-model="internalValue"
|
|
144
|
+
is-term
|
|
146
145
|
class="gl-filtered-search-term-token"
|
|
147
146
|
:active="active"
|
|
148
147
|
:cursor-position="cursorPosition"
|
|
@@ -150,6 +149,7 @@ export default {
|
|
|
150
149
|
:is-last-token="isLastToken"
|
|
151
150
|
:current-value="currentValue"
|
|
152
151
|
:view-only="viewOnly"
|
|
152
|
+
:options="suggestedTokens"
|
|
153
153
|
@activate="$emit('activate')"
|
|
154
154
|
@deactivate="$emit('deactivate')"
|
|
155
155
|
@complete="$emit('replace', { type: $event })"
|
|
@@ -159,17 +159,6 @@ export default {
|
|
|
159
159
|
@previous="$emit('previous')"
|
|
160
160
|
@next="$emit('next')"
|
|
161
161
|
>
|
|
162
|
-
<template #suggestions>
|
|
163
|
-
<gl-filtered-search-suggestion
|
|
164
|
-
v-for="(item, idx) in suggestedTokens"
|
|
165
|
-
:key="idx"
|
|
166
|
-
:value="item.type"
|
|
167
|
-
:icon-name="item.icon"
|
|
168
|
-
>
|
|
169
|
-
<slot name="title" v-bind="{ value: item.title }"> {{ item.title }} </slot>
|
|
170
|
-
</gl-filtered-search-suggestion>
|
|
171
|
-
</template>
|
|
172
|
-
|
|
173
162
|
<template #view>
|
|
174
163
|
<input
|
|
175
164
|
v-if="placeholder"
|
|
@@ -33,10 +33,9 @@ describe('Filtered search token', () => {
|
|
|
33
33
|
cursorPosition: 'end',
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
const createComponent = (props
|
|
36
|
+
const createComponent = (props) => {
|
|
37
37
|
wrapper = shallowMount(GlFilteredSearchToken, {
|
|
38
38
|
propsData: { ...defaultProps, ...props },
|
|
39
|
-
...options,
|
|
40
39
|
});
|
|
41
40
|
};
|
|
42
41
|
|
|
@@ -190,7 +189,7 @@ describe('Filtered search token', () => {
|
|
|
190
189
|
config: availableTokens[0],
|
|
191
190
|
});
|
|
192
191
|
|
|
193
|
-
findTitleSegment().vm.$emit('complete', availableTokens[1].
|
|
192
|
+
findTitleSegment().vm.$emit('complete', availableTokens[1].type);
|
|
194
193
|
|
|
195
194
|
expect(wrapper.emitted('replace')).toHaveLength(1);
|
|
196
195
|
expect(wrapper.emitted().replace[0][0].value).toStrictEqual(originalValue);
|
|
@@ -204,7 +203,7 @@ describe('Filtered search token', () => {
|
|
|
204
203
|
config: availableTokens[0],
|
|
205
204
|
});
|
|
206
205
|
|
|
207
|
-
findTitleSegment().vm.$emit('complete', availableTokens[2].
|
|
206
|
+
findTitleSegment().vm.$emit('complete', availableTokens[2].type);
|
|
208
207
|
|
|
209
208
|
expect(wrapper.emitted('replace')).toHaveLength(1);
|
|
210
209
|
expect(wrapper.emitted().replace[0][0].value).toStrictEqual({ data: '' });
|
|
@@ -373,22 +372,4 @@ describe('Filtered search token', () => {
|
|
|
373
372
|
}
|
|
374
373
|
);
|
|
375
374
|
});
|
|
376
|
-
|
|
377
|
-
it('renders title options passed via slot', async () => {
|
|
378
|
-
createComponent(
|
|
379
|
-
{},
|
|
380
|
-
{
|
|
381
|
-
slots: {
|
|
382
|
-
'title-option': '<div>new title</div>',
|
|
383
|
-
},
|
|
384
|
-
stubs: {
|
|
385
|
-
GlFilteredSearchTokenSegment: {
|
|
386
|
-
template: `<div><slot name="option" v-bind='{ option: { value: "default title"} }'></slot></div>`,
|
|
387
|
-
},
|
|
388
|
-
},
|
|
389
|
-
}
|
|
390
|
-
);
|
|
391
|
-
|
|
392
|
-
expect(findTitleSegment().text()).toBe('new title');
|
|
393
|
-
});
|
|
394
375
|
});
|
|
@@ -3,7 +3,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
|
|
3
3
|
import { COMMA } from '../../../utils/constants';
|
|
4
4
|
import GlToken from '../token/token.vue';
|
|
5
5
|
import GlFilteredSearchTokenSegment from './filtered_search_token_segment.vue';
|
|
6
|
-
import { createTerm } from './filtered_search_utils';
|
|
6
|
+
import { createTerm, tokenToOption } from './filtered_search_utils';
|
|
7
7
|
|
|
8
8
|
const SEGMENT_TITLE = 'TYPE';
|
|
9
9
|
const SEGMENT_OPERATOR = 'OPERATOR';
|
|
@@ -11,7 +11,7 @@ const SEGMENT_DATA = 'DATA';
|
|
|
11
11
|
const TOKEN_CLOSE_SELECTOR = '.gl-token-close';
|
|
12
12
|
|
|
13
13
|
const DEFAULT_OPERATORS = [
|
|
14
|
-
{ value: '=', description: 'is', default:
|
|
14
|
+
{ value: '=', description: 'is', default: true },
|
|
15
15
|
{ value: '!=', description: 'is not' },
|
|
16
16
|
];
|
|
17
17
|
|
|
@@ -98,10 +98,9 @@ export default {
|
|
|
98
98
|
},
|
|
99
99
|
|
|
100
100
|
availableTokensWithSelf() {
|
|
101
|
-
return [this.config, ...this.availableTokens.filter((
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}));
|
|
101
|
+
return [this.config, ...this.availableTokens.filter((token) => token !== this.config)].map(
|
|
102
|
+
tokenToOption
|
|
103
|
+
);
|
|
105
104
|
},
|
|
106
105
|
|
|
107
106
|
operatorDescription() {
|
|
@@ -212,8 +211,8 @@ export default {
|
|
|
212
211
|
}
|
|
213
212
|
},
|
|
214
213
|
|
|
215
|
-
replaceToken(
|
|
216
|
-
const newTokenConfig = this.availableTokens.find((
|
|
214
|
+
replaceToken(newType) {
|
|
215
|
+
const newTokenConfig = this.availableTokens.find(({ type }) => type === newType);
|
|
217
216
|
|
|
218
217
|
if (newTokenConfig === this.config) {
|
|
219
218
|
this.$nextTick(() => {
|
|
@@ -303,10 +302,6 @@ export default {
|
|
|
303
302
|
this.$emit('destroy');
|
|
304
303
|
}
|
|
305
304
|
},
|
|
306
|
-
|
|
307
|
-
hasTitleOptionSlot() {
|
|
308
|
-
return Boolean(this.$scopedSlots['title-option']);
|
|
309
|
-
},
|
|
310
305
|
},
|
|
311
306
|
};
|
|
312
307
|
</script>
|
|
@@ -350,9 +345,6 @@ export default {
|
|
|
350
345
|
{{ inputValue }}
|
|
351
346
|
</gl-token>
|
|
352
347
|
</template>
|
|
353
|
-
<template v-if="hasTitleOptionSlot()" #option>
|
|
354
|
-
<slot name="title-option"></slot>
|
|
355
|
-
</template>
|
|
356
348
|
</gl-filtered-search-token-segment>
|
|
357
349
|
|
|
358
350
|
<gl-filtered-search-token-segment
|
|
@@ -361,6 +353,7 @@ export default {
|
|
|
361
353
|
:active="isSegmentActive($options.segments.SEGMENT_OPERATOR)"
|
|
362
354
|
:cursor-position="intendedCursorPosition"
|
|
363
355
|
:options="operators"
|
|
356
|
+
option-text-field="value"
|
|
364
357
|
:custom-input-keydown-handler="handleOperatorKeydown"
|
|
365
358
|
:view-only="viewOnly"
|
|
366
359
|
@activate="activateSegment($options.segments.SEGMENT_OPERATOR)"
|
|
@@ -412,7 +405,6 @@ export default {
|
|
|
412
405
|
:multi-select="config.multiSelect"
|
|
413
406
|
:options="config.options"
|
|
414
407
|
:view-only="viewOnly"
|
|
415
|
-
option-text-field="title"
|
|
416
408
|
@activate="activateDataSegment"
|
|
417
409
|
@backspace="activateSegment($options.segments.SEGMENT_OPERATOR)"
|
|
418
410
|
@complete="handleComplete"
|