@gitlab/ui 61.2.0 → 61.3.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 +7 -0
- package/dist/components/base/filtered_search/common_story_options.js +2 -1
- package/dist/components/base/filtered_search/filtered_search.js +28 -4
- package/dist/components/base/filtered_search/filtered_search_suggestion_list.js +13 -5
- package/dist/components/base/filtered_search/filtered_search_term.js +51 -4
- package/dist/components/base/filtered_search/filtered_search_token.js +1 -2
- package/dist/components/base/filtered_search/filtered_search_token_segment.js +18 -5
- package/dist/components/base/filtered_search/filtered_search_utils.js +18 -7
- package/package.json +1 -1
- package/src/components/base/filtered_search/__snapshots__/filtered_search_term.spec.js.snap +2 -5
- package/src/components/base/filtered_search/common_story_options.js +1 -0
- package/src/components/base/filtered_search/filtered_search.md +10 -1
- package/src/components/base/filtered_search/filtered_search.spec.js +14 -2
- package/src/components/base/filtered_search/filtered_search.stories.js +17 -0
- package/src/components/base/filtered_search/filtered_search.vue +29 -1
- package/src/components/base/filtered_search/filtered_search_suggestion_list.spec.js +222 -75
- package/src/components/base/filtered_search/filtered_search_suggestion_list.vue +15 -6
- package/src/components/base/filtered_search/filtered_search_term.spec.js +73 -14
- package/src/components/base/filtered_search/filtered_search_term.vue +64 -6
- package/src/components/base/filtered_search/filtered_search_token.spec.js +1 -0
- package/src/components/base/filtered_search/filtered_search_token.vue +2 -2
- package/src/components/base/filtered_search/filtered_search_token_segment.spec.js +46 -2
- package/src/components/base/filtered_search/filtered_search_token_segment.vue +22 -5
- package/src/components/base/filtered_search/filtered_search_utils.js +20 -7
|
@@ -7,13 +7,14 @@ describe('Filtered search suggestion list component', () => {
|
|
|
7
7
|
let wrapper;
|
|
8
8
|
|
|
9
9
|
describe('suggestions API', () => {
|
|
10
|
-
|
|
10
|
+
const createComponent = ({ termsAsTokens = false } = {}) => {
|
|
11
11
|
wrapper = shallowMount(FilteredSearchSuggestionList, {
|
|
12
|
-
provide: { suggestionsListClass: () => 'custom-class' },
|
|
12
|
+
provide: { suggestionsListClass: () => 'custom-class', termsAsTokens: () => termsAsTokens },
|
|
13
13
|
});
|
|
14
|
-
}
|
|
14
|
+
};
|
|
15
15
|
|
|
16
16
|
it('has expected public methods', () => {
|
|
17
|
+
createComponent();
|
|
17
18
|
expect(wrapper.vm.register).toBeInstanceOf(Function);
|
|
18
19
|
expect(wrapper.vm.unregister).toBeInstanceOf(Function);
|
|
19
20
|
expect(wrapper.vm.getValue).toBeInstanceOf(Function);
|
|
@@ -24,6 +25,7 @@ describe('Filtered search suggestion list component', () => {
|
|
|
24
25
|
describe('navigation', () => {
|
|
25
26
|
const stubs = [{ value: 'stub1' }, { value: 'stub2' }, { value: 'stub3' }];
|
|
26
27
|
beforeEach(() => {
|
|
28
|
+
createComponent();
|
|
27
29
|
stubs.forEach((s) => {
|
|
28
30
|
wrapper.vm.register(s);
|
|
29
31
|
});
|
|
@@ -71,6 +73,58 @@ describe('Filtered search suggestion list component', () => {
|
|
|
71
73
|
expect(wrapper.vm.getValue()).toBe('stub2');
|
|
72
74
|
});
|
|
73
75
|
});
|
|
76
|
+
|
|
77
|
+
describe('navigation with termsAsTokens', () => {
|
|
78
|
+
const stubs = [{ value: 'stub1' }, { value: 'stub2' }, { value: 'stub3' }];
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
createComponent({ termsAsTokens: true });
|
|
81
|
+
stubs.forEach((s) => {
|
|
82
|
+
wrapper.vm.register(s);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('does not select item by default', () => {
|
|
87
|
+
expect(wrapper.vm.getValue()).toBe(null);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('selects first item on nextItem call', async () => {
|
|
91
|
+
wrapper.vm.nextItem();
|
|
92
|
+
await nextTick();
|
|
93
|
+
expect(wrapper.vm.getValue()).toBe(stubs[0].value);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('wraps to last item on prevItem call', async () => {
|
|
97
|
+
wrapper.vm.nextItem();
|
|
98
|
+
wrapper.vm.prevItem();
|
|
99
|
+
await nextTick();
|
|
100
|
+
expect(wrapper.vm.getValue()).toBe(stubs[2].value);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('wraps to first item on nextItem call', async () => {
|
|
104
|
+
stubs.forEach(() => wrapper.vm.nextItem());
|
|
105
|
+
wrapper.vm.nextItem();
|
|
106
|
+
await nextTick();
|
|
107
|
+
expect(wrapper.vm.getValue()).toBe(stubs[0].value);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('remove selection if suggestion is unregistered', async () => {
|
|
111
|
+
wrapper.vm.nextItem();
|
|
112
|
+
await nextTick();
|
|
113
|
+
wrapper.vm.unregister(stubs[0]);
|
|
114
|
+
await nextTick();
|
|
115
|
+
expect(wrapper.vm.getValue()).toBe(null);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('selects correct suggestion when item (un)registration is late', async () => {
|
|
119
|
+
// Initially stub2 is at index 1.
|
|
120
|
+
await wrapper.setProps({ initialValue: 'stub2' });
|
|
121
|
+
// Remove item at index 0, so stub2 moves to index 0
|
|
122
|
+
wrapper.vm.unregister(stubs[0]);
|
|
123
|
+
await nextTick();
|
|
124
|
+
// stub2 should still be selected
|
|
125
|
+
expect(wrapper.vm.getValue()).toBe('stub2');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
74
128
|
});
|
|
75
129
|
|
|
76
130
|
describe('integration tests', () => {
|
|
@@ -87,6 +141,17 @@ describe('Filtered search suggestion list component', () => {
|
|
|
87
141
|
`,
|
|
88
142
|
};
|
|
89
143
|
|
|
144
|
+
const createComponent = ({ termsAsTokens = false } = {}) => {
|
|
145
|
+
wrapper = mount(FilteredSearchSuggestionList, {
|
|
146
|
+
provide: { suggestionsListClass: () => 'custom-class', termsAsTokens: () => termsAsTokens },
|
|
147
|
+
slots: {
|
|
148
|
+
default: list,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const findActiveSuggestion = () => wrapper.find('.gl-filtered-search-suggestion-active');
|
|
154
|
+
|
|
90
155
|
beforeAll(() => {
|
|
91
156
|
if (!HTMLElement.prototype.scrollIntoView) {
|
|
92
157
|
HTMLElement.prototype.scrollIntoView = jest.fn();
|
|
@@ -99,92 +164,174 @@ describe('Filtered search suggestion list component', () => {
|
|
|
99
164
|
}
|
|
100
165
|
});
|
|
101
166
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
slots: {
|
|
106
|
-
default: list,
|
|
107
|
-
},
|
|
167
|
+
describe('with termsAsTokens = false', () => {
|
|
168
|
+
beforeEach(() => {
|
|
169
|
+
createComponent();
|
|
108
170
|
});
|
|
109
|
-
});
|
|
110
171
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
172
|
+
it('selects first suggestion', async () => {
|
|
173
|
+
wrapper.vm.nextItem();
|
|
174
|
+
await nextTick();
|
|
175
|
+
expect(wrapper.vm.getValue()).toBe('One');
|
|
176
|
+
});
|
|
116
177
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
178
|
+
it('selects second suggestion', async () => {
|
|
179
|
+
wrapper.vm.nextItem();
|
|
180
|
+
wrapper.vm.nextItem();
|
|
181
|
+
await nextTick();
|
|
182
|
+
expect(wrapper.vm.getValue()).toBe('Two');
|
|
183
|
+
});
|
|
123
184
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
185
|
+
it('deselects first suggestion after list end', async () => {
|
|
186
|
+
wrapper.vm.nextItem();
|
|
187
|
+
wrapper.vm.nextItem();
|
|
188
|
+
wrapper.vm.nextItem();
|
|
189
|
+
wrapper.vm.nextItem();
|
|
190
|
+
await nextTick();
|
|
191
|
+
expect(wrapper.vm.getValue()).toBe(null);
|
|
192
|
+
});
|
|
132
193
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
194
|
+
it('deselects first suggestion after list start', async () => {
|
|
195
|
+
wrapper.vm.nextItem();
|
|
196
|
+
wrapper.vm.prevItem();
|
|
197
|
+
await nextTick();
|
|
198
|
+
expect(wrapper.vm.getValue()).toBe(null);
|
|
199
|
+
});
|
|
139
200
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
201
|
+
it('selects last suggestion in circle when selecting previous item', async () => {
|
|
202
|
+
wrapper.vm.nextItem();
|
|
203
|
+
wrapper.vm.prevItem();
|
|
204
|
+
wrapper.vm.prevItem();
|
|
205
|
+
await nextTick();
|
|
206
|
+
expect(wrapper.vm.getValue()).toBe(false);
|
|
207
|
+
});
|
|
147
208
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
209
|
+
it('selects first suggestion in circle when selecting next item', async () => {
|
|
210
|
+
wrapper.vm.nextItem();
|
|
211
|
+
wrapper.vm.nextItem();
|
|
212
|
+
wrapper.vm.nextItem();
|
|
213
|
+
wrapper.vm.nextItem();
|
|
214
|
+
wrapper.vm.nextItem();
|
|
215
|
+
await nextTick();
|
|
216
|
+
expect(wrapper.vm.getValue()).toBe('One');
|
|
217
|
+
});
|
|
157
218
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
219
|
+
it('highlights suggestion if initial-value is provided', async () => {
|
|
220
|
+
await wrapper.setProps({ initialValue: 'Two' });
|
|
221
|
+
expect(findActiveSuggestion().text()).toBe('Two');
|
|
222
|
+
});
|
|
162
223
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
224
|
+
it('highlights suggestion if initial-value is provided, regardless of case sensitivity', async () => {
|
|
225
|
+
await wrapper.setProps({ initialValue: 'two' });
|
|
226
|
+
expect(findActiveSuggestion().text()).toBe('Two');
|
|
227
|
+
});
|
|
167
228
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
229
|
+
it('highlights suggestion if initial-value is provided, regardless of falsiness', async () => {
|
|
230
|
+
await wrapper.setProps({ initialValue: false });
|
|
231
|
+
expect(findActiveSuggestion().text()).toBe('Three');
|
|
232
|
+
});
|
|
172
233
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
234
|
+
it('highlights first suggestion if initial-value is provided, deselected then selected', async () => {
|
|
235
|
+
await wrapper.setProps({ initialValue: 'One' });
|
|
236
|
+
wrapper.vm.prevItem();
|
|
237
|
+
wrapper.vm.nextItem();
|
|
238
|
+
await nextTick();
|
|
239
|
+
expect(findActiveSuggestion().text()).toBe('One');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('does not highlight anything if initial-value matches nothing', async () => {
|
|
243
|
+
await wrapper.setProps({ initialValue: 'missing' });
|
|
244
|
+
expect(findActiveSuggestion().exists()).toBe(false);
|
|
245
|
+
});
|
|
180
246
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
247
|
+
it('applies the injected suggestion-list-class to the dropdown', () => {
|
|
248
|
+
expect(wrapper.classes()).toContain('custom-class');
|
|
249
|
+
});
|
|
184
250
|
});
|
|
185
251
|
|
|
186
|
-
|
|
187
|
-
|
|
252
|
+
describe('with termsAsTokens = true', () => {
|
|
253
|
+
beforeEach(() => {
|
|
254
|
+
createComponent({ termsAsTokens: true });
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('selects first suggestion', async () => {
|
|
258
|
+
wrapper.vm.nextItem();
|
|
259
|
+
await nextTick();
|
|
260
|
+
expect(wrapper.vm.getValue()).toBe('One');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('selects second suggestion', async () => {
|
|
264
|
+
wrapper.vm.nextItem();
|
|
265
|
+
wrapper.vm.nextItem();
|
|
266
|
+
await nextTick();
|
|
267
|
+
expect(wrapper.vm.getValue()).toBe('Two');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('wraps to first suggestion after list end', async () => {
|
|
271
|
+
wrapper.vm.nextItem();
|
|
272
|
+
wrapper.vm.nextItem();
|
|
273
|
+
wrapper.vm.nextItem();
|
|
274
|
+
wrapper.vm.nextItem();
|
|
275
|
+
await nextTick();
|
|
276
|
+
expect(wrapper.vm.getValue()).toBe('One');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('wraps to last suggestion before list start', async () => {
|
|
280
|
+
wrapper.vm.nextItem();
|
|
281
|
+
wrapper.vm.prevItem();
|
|
282
|
+
await nextTick();
|
|
283
|
+
expect(wrapper.vm.getValue()).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('selects second-to-last suggestion in circle when selecting previous item', async () => {
|
|
287
|
+
wrapper.vm.nextItem();
|
|
288
|
+
wrapper.vm.prevItem();
|
|
289
|
+
wrapper.vm.prevItem();
|
|
290
|
+
await nextTick();
|
|
291
|
+
expect(wrapper.vm.getValue()).toBe('Two');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('selects second suggestion in circle when selecting next item', async () => {
|
|
295
|
+
wrapper.vm.nextItem();
|
|
296
|
+
wrapper.vm.nextItem();
|
|
297
|
+
wrapper.vm.nextItem();
|
|
298
|
+
wrapper.vm.nextItem();
|
|
299
|
+
wrapper.vm.nextItem();
|
|
300
|
+
await nextTick();
|
|
301
|
+
expect(wrapper.vm.getValue()).toBe('Two');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('highlights suggestion if initial-value is provided', async () => {
|
|
305
|
+
await wrapper.setProps({ initialValue: 'Two' });
|
|
306
|
+
expect(findActiveSuggestion().text()).toBe('Two');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('highlights suggestion if initial-value is provided, regardless of case sensitivity', async () => {
|
|
310
|
+
await wrapper.setProps({ initialValue: 'two' });
|
|
311
|
+
expect(findActiveSuggestion().text()).toBe('Two');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('highlights suggestion if initial-value is provided, regardless of falsiness', async () => {
|
|
315
|
+
await wrapper.setProps({ initialValue: false });
|
|
316
|
+
expect(findActiveSuggestion().text()).toBe('Three');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('highlights first suggestion if initial-value is provided, deselected then selected', async () => {
|
|
320
|
+
await wrapper.setProps({ initialValue: 'One' });
|
|
321
|
+
wrapper.vm.prevItem();
|
|
322
|
+
wrapper.vm.nextItem();
|
|
323
|
+
await nextTick();
|
|
324
|
+
expect(findActiveSuggestion().text()).toBe('One');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('does not highlight anything if initial-value matches nothing', async () => {
|
|
328
|
+
await wrapper.setProps({ initialValue: 'missing' });
|
|
329
|
+
expect(findActiveSuggestion().exists()).toBe(false);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('applies the injected suggestion-list-class to the dropdown', () => {
|
|
333
|
+
expect(wrapper.classes()).toContain('custom-class');
|
|
334
|
+
});
|
|
188
335
|
});
|
|
189
336
|
});
|
|
190
337
|
});
|
|
@@ -6,7 +6,7 @@ const NO_ACTIVE_ITEM = -2;
|
|
|
6
6
|
|
|
7
7
|
export default {
|
|
8
8
|
name: 'GlFilteredSearchSuggestionList',
|
|
9
|
-
inject: ['suggestionsListClass'],
|
|
9
|
+
inject: ['suggestionsListClass', 'termsAsTokens'],
|
|
10
10
|
provide() {
|
|
11
11
|
return {
|
|
12
12
|
filteredSearchSuggestionListInstance: this,
|
|
@@ -40,7 +40,7 @@ export default {
|
|
|
40
40
|
return this.registeredItems[this.initialActiveIdx];
|
|
41
41
|
},
|
|
42
42
|
activeItem() {
|
|
43
|
-
if (this.activeIdx === NO_ACTIVE_ITEM) return null;
|
|
43
|
+
if (!this.termsAsTokens() && this.activeIdx === NO_ACTIVE_ITEM) return null;
|
|
44
44
|
if (this.activeIdx === DEFER_TO_INITIAL_VALUE) return this.initialActiveItem;
|
|
45
45
|
return this.registeredItems[this.activeIdx];
|
|
46
46
|
},
|
|
@@ -76,15 +76,24 @@ export default {
|
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
78
|
nextItem() {
|
|
79
|
-
this.
|
|
79
|
+
if (this.termsAsTokens()) {
|
|
80
|
+
this.stepItem(1);
|
|
81
|
+
} else {
|
|
82
|
+
this.stepItem(1, this.registeredItems.length - 1);
|
|
83
|
+
}
|
|
80
84
|
},
|
|
81
85
|
prevItem() {
|
|
82
|
-
this.
|
|
86
|
+
if (this.termsAsTokens()) {
|
|
87
|
+
this.stepItem(-1);
|
|
88
|
+
} else {
|
|
89
|
+
this.stepItem(-1, 0);
|
|
90
|
+
}
|
|
83
91
|
},
|
|
84
92
|
stepItem(direction, endIdx) {
|
|
85
93
|
if (
|
|
86
|
-
this.
|
|
87
|
-
(this.activeIdx ===
|
|
94
|
+
!this.termsAsTokens() &&
|
|
95
|
+
(this.activeIdx === endIdx ||
|
|
96
|
+
(this.activeIdx === DEFER_TO_INITIAL_VALUE && this.initialActiveIdx === endIdx))
|
|
88
97
|
) {
|
|
89
98
|
// The user wants to move past the end of the list, so ensure nothing is selected.
|
|
90
99
|
this.activeIdx = NO_ACTIVE_ITEM;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { nextTick } from 'vue';
|
|
2
2
|
import { shallowMount } from '@vue/test-utils';
|
|
3
|
+
import GlToken from '../token/token.vue';
|
|
3
4
|
import FilteredSearchTerm from './filtered_search_term.vue';
|
|
4
|
-
import { INTENT_ACTIVATE_PREVIOUS } from './filtered_search_utils';
|
|
5
|
+
import { INTENT_ACTIVATE_PREVIOUS, TERM_TOKEN_TYPE } from './filtered_search_utils';
|
|
5
6
|
|
|
6
7
|
const availableTokens = [
|
|
7
8
|
{ type: 'foo', title: 'test1-foo', token: 'stub', icon: 'eye' },
|
|
@@ -9,6 +10,8 @@ const availableTokens = [
|
|
|
9
10
|
{ type: 'baz', title: 'test1-baz', token: 'stub', icon: 'eye' },
|
|
10
11
|
];
|
|
11
12
|
|
|
13
|
+
const pointerClass = 'gl-cursor-pointer';
|
|
14
|
+
|
|
12
15
|
describe('Filtered search term', () => {
|
|
13
16
|
let wrapper;
|
|
14
17
|
|
|
@@ -21,13 +24,23 @@ describe('Filtered search term', () => {
|
|
|
21
24
|
|
|
22
25
|
const segmentStub = {
|
|
23
26
|
name: 'gl-filtered-search-token-segment-stub',
|
|
24
|
-
template: '<div><slot name="view"></slot><slot name="suggestions"></slot></div>',
|
|
25
|
-
props: [
|
|
27
|
+
template: '<div><slot v-if="!active" name="view"></slot><slot name="suggestions"></slot></div>',
|
|
28
|
+
props: [
|
|
29
|
+
'searchInputAttributes',
|
|
30
|
+
'isLastToken',
|
|
31
|
+
'currentValue',
|
|
32
|
+
'viewOnly',
|
|
33
|
+
'options',
|
|
34
|
+
'active',
|
|
35
|
+
],
|
|
26
36
|
};
|
|
27
37
|
|
|
28
|
-
const createComponent = (props) => {
|
|
38
|
+
const createComponent = ({ termsAsTokens = false, ...props } = {}) => {
|
|
29
39
|
wrapper = shallowMount(FilteredSearchTerm, {
|
|
30
40
|
propsData: { ...defaultProps, ...props },
|
|
41
|
+
provide: {
|
|
42
|
+
termsAsTokens: () => termsAsTokens,
|
|
43
|
+
},
|
|
31
44
|
stubs: {
|
|
32
45
|
'gl-filtered-search-token-segment': segmentStub,
|
|
33
46
|
},
|
|
@@ -36,14 +49,24 @@ describe('Filtered search term', () => {
|
|
|
36
49
|
|
|
37
50
|
const findSearchInput = () => wrapper.find('input');
|
|
38
51
|
const findTokenSegmentComponent = () => wrapper.findComponent(segmentStub);
|
|
52
|
+
const findToken = () => wrapper.findComponent(GlToken);
|
|
39
53
|
|
|
40
54
|
it('renders value in inactive mode', () => {
|
|
41
55
|
createComponent({ value: { data: 'test-value' } });
|
|
42
56
|
expect(wrapper.html()).toMatchSnapshot();
|
|
43
57
|
});
|
|
44
58
|
|
|
45
|
-
it('renders
|
|
59
|
+
it('renders token in inactive mode with termsAsTokens', () => {
|
|
60
|
+
createComponent({ value: { data: 'test value' }, termsAsTokens: true });
|
|
61
|
+
expect(findToken().exists()).toBe(true);
|
|
62
|
+
expect(findToken().props().viewOnly).toBe(false);
|
|
63
|
+
expect(findToken().classes()).toContain(pointerClass);
|
|
64
|
+
expect(findToken().text()).toBe('test value');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('renders nothing with value in active mode (delegates to segment)', () => {
|
|
46
68
|
createComponent({ value: { data: 'test-value' }, active: true });
|
|
69
|
+
expect(wrapper.text()).not.toContain('test-value');
|
|
47
70
|
expect(wrapper.html()).toMatchSnapshot();
|
|
48
71
|
});
|
|
49
72
|
|
|
@@ -60,20 +83,45 @@ describe('Filtered search term', () => {
|
|
|
60
83
|
expect(findTokenSegmentComponent().props('options')).toHaveLength(2);
|
|
61
84
|
});
|
|
62
85
|
|
|
86
|
+
it('suggests text search when termsAsTokens=true', async () => {
|
|
87
|
+
createComponent({
|
|
88
|
+
availableTokens,
|
|
89
|
+
active: true,
|
|
90
|
+
value: { data: 'foo' },
|
|
91
|
+
termsAsTokens: true,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await nextTick();
|
|
95
|
+
|
|
96
|
+
expect(findTokenSegmentComponent().props('options')).toEqual([
|
|
97
|
+
expect.objectContaining({
|
|
98
|
+
value: 'foo',
|
|
99
|
+
title: 'test1-foo',
|
|
100
|
+
}),
|
|
101
|
+
expect.objectContaining({
|
|
102
|
+
value: TERM_TOKEN_TYPE,
|
|
103
|
+
title: 'Search for this text',
|
|
104
|
+
}),
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
|
|
63
108
|
it.each`
|
|
64
|
-
originalEvent | emittedEvent | payload
|
|
65
|
-
${'activate'} | ${'activate'} | ${undefined}
|
|
66
|
-
${'deactivate'} | ${'deactivate'} | ${undefined}
|
|
67
|
-
${'split'} | ${'split'} | ${
|
|
68
|
-
${'submit'} | ${'submit'} | ${undefined}
|
|
69
|
-
${'
|
|
70
|
-
${'
|
|
109
|
+
originalEvent | originalPayload | emittedEvent | payload
|
|
110
|
+
${'activate'} | ${undefined} | ${'activate'} | ${undefined}
|
|
111
|
+
${'deactivate'} | ${undefined} | ${'deactivate'} | ${undefined}
|
|
112
|
+
${'split'} | ${'foo'} | ${'split'} | ${'foo'}
|
|
113
|
+
${'submit'} | ${undefined} | ${'submit'} | ${undefined}
|
|
114
|
+
${'previous'} | ${undefined} | ${'previous'} | ${undefined}
|
|
115
|
+
${'next'} | ${undefined} | ${'next'} | ${undefined}
|
|
116
|
+
${'complete'} | ${'foo'} | ${'replace'} | ${{ type: 'foo' }}
|
|
117
|
+
${'complete'} | ${TERM_TOKEN_TYPE} | ${'complete'} | ${undefined}
|
|
118
|
+
${'backspace'} | ${undefined} | ${'destroy'} | ${{ intent: INTENT_ACTIVATE_PREVIOUS }}
|
|
71
119
|
`(
|
|
72
120
|
'emits $emittedEvent when token segment emits $originalEvent',
|
|
73
|
-
async ({ originalEvent, emittedEvent, payload }) => {
|
|
121
|
+
async ({ originalEvent, originalPayload, emittedEvent, payload }) => {
|
|
74
122
|
createComponent({ active: true, value: { data: 'something' } });
|
|
75
123
|
|
|
76
|
-
findTokenSegmentComponent().vm.$emit(originalEvent);
|
|
124
|
+
findTokenSegmentComponent().vm.$emit(originalEvent, originalPayload);
|
|
77
125
|
|
|
78
126
|
await nextTick();
|
|
79
127
|
|
|
@@ -117,6 +165,17 @@ describe('Filtered search term', () => {
|
|
|
117
165
|
expect(findTokenSegmentComponent().props('viewOnly')).toBe(false);
|
|
118
166
|
});
|
|
119
167
|
|
|
168
|
+
it('sets viewOnly prop and removes pointer class on token when termsAsTokens=true', () => {
|
|
169
|
+
createComponent({
|
|
170
|
+
value: { data: 'foo' },
|
|
171
|
+
viewOnly: true,
|
|
172
|
+
termsAsTokens: true,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(findToken().props().viewOnly).toBe(true);
|
|
176
|
+
expect(findToken().classes()).not.toContain(pointerClass);
|
|
177
|
+
});
|
|
178
|
+
|
|
120
179
|
it('adds `searchInputAttributes` prop to search term input', () => {
|
|
121
180
|
createComponent({
|
|
122
181
|
placeholder: 'placeholder-stub',
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
import GlToken from '../token/token.vue';
|
|
3
|
+
import { stopEvent } from '../../../utils/utils';
|
|
2
4
|
import GlFilteredSearchTokenSegment from './filtered_search_token_segment.vue';
|
|
3
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
INTENT_ACTIVATE_PREVIOUS,
|
|
7
|
+
TERM_TOKEN_TYPE,
|
|
8
|
+
TOKEN_CLOSE_SELECTOR,
|
|
9
|
+
match,
|
|
10
|
+
tokenToOption,
|
|
11
|
+
termTokenDefinition,
|
|
12
|
+
} from './filtered_search_utils';
|
|
4
13
|
|
|
5
14
|
export default {
|
|
6
15
|
name: 'GlFilteredSearchTerm',
|
|
7
16
|
components: {
|
|
8
17
|
GlFilteredSearchTokenSegment,
|
|
18
|
+
GlToken,
|
|
9
19
|
},
|
|
20
|
+
inject: ['termsAsTokens'],
|
|
10
21
|
inheritAttrs: false,
|
|
11
22
|
props: {
|
|
12
23
|
/**
|
|
@@ -67,6 +78,14 @@ export default {
|
|
|
67
78
|
default: 'end',
|
|
68
79
|
validator: (value) => ['start', 'end'].includes(value),
|
|
69
80
|
},
|
|
81
|
+
/**
|
|
82
|
+
* The title of the text search option. Ignored unless termsAsTokens is enabled.
|
|
83
|
+
*/
|
|
84
|
+
searchTextOptionLabel: {
|
|
85
|
+
type: String,
|
|
86
|
+
required: false,
|
|
87
|
+
default: termTokenDefinition.title,
|
|
88
|
+
},
|
|
70
89
|
viewOnly: {
|
|
71
90
|
type: Boolean,
|
|
72
91
|
required: false,
|
|
@@ -74,10 +93,22 @@ export default {
|
|
|
74
93
|
},
|
|
75
94
|
},
|
|
76
95
|
computed: {
|
|
96
|
+
showInput() {
|
|
97
|
+
return this.termsAsTokens() || Boolean(this.placeholder);
|
|
98
|
+
},
|
|
99
|
+
showToken() {
|
|
100
|
+
return this.termsAsTokens() && Boolean(this.value.data);
|
|
101
|
+
},
|
|
77
102
|
suggestedTokens() {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.
|
|
103
|
+
const tokens = this.availableTokens.filter((token) => match(token.title, this.value.data));
|
|
104
|
+
if (this.termsAsTokens() && this.value.data) {
|
|
105
|
+
tokens.push({
|
|
106
|
+
...termTokenDefinition,
|
|
107
|
+
title: this.searchTextOptionLabel,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return tokens.map(tokenToOption);
|
|
81
112
|
},
|
|
82
113
|
internalValue: {
|
|
83
114
|
get() {
|
|
@@ -93,6 +124,9 @@ export default {
|
|
|
93
124
|
this.$emit('input', { data });
|
|
94
125
|
},
|
|
95
126
|
},
|
|
127
|
+
eventListeners() {
|
|
128
|
+
return this.viewOnly ? {} : { mousedown: this.destroyByClose };
|
|
129
|
+
},
|
|
96
130
|
},
|
|
97
131
|
methods: {
|
|
98
132
|
onBackspace() {
|
|
@@ -105,6 +139,21 @@ export default {
|
|
|
105
139
|
*/
|
|
106
140
|
this.$emit('destroy', { intent: INTENT_ACTIVATE_PREVIOUS });
|
|
107
141
|
},
|
|
142
|
+
destroyByClose(event) {
|
|
143
|
+
if (event.target.closest(TOKEN_CLOSE_SELECTOR)) {
|
|
144
|
+
stopEvent(event);
|
|
145
|
+
this.$emit('destroy');
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
onComplete(type) {
|
|
149
|
+
if (type === TERM_TOKEN_TYPE) {
|
|
150
|
+
// We've completed this term token
|
|
151
|
+
this.$emit('complete');
|
|
152
|
+
} else {
|
|
153
|
+
// We're changing the current token type
|
|
154
|
+
this.$emit('replace', { type });
|
|
155
|
+
}
|
|
156
|
+
},
|
|
108
157
|
},
|
|
109
158
|
};
|
|
110
159
|
</script>
|
|
@@ -134,6 +183,7 @@ export default {
|
|
|
134
183
|
|
|
135
184
|
<!--
|
|
136
185
|
Emitted when Space is pressed in-between term text.
|
|
186
|
+
Not emitted when termsAsTokens is true.
|
|
137
187
|
@event split
|
|
138
188
|
@property {array} newTokens Token configurations
|
|
139
189
|
-->
|
|
@@ -152,7 +202,7 @@ export default {
|
|
|
152
202
|
:options="suggestedTokens"
|
|
153
203
|
@activate="$emit('activate')"
|
|
154
204
|
@deactivate="$emit('deactivate')"
|
|
155
|
-
@complete="
|
|
205
|
+
@complete="onComplete"
|
|
156
206
|
@backspace="onBackspace"
|
|
157
207
|
@submit="$emit('submit')"
|
|
158
208
|
@split="$emit('split', $event)"
|
|
@@ -160,8 +210,16 @@ export default {
|
|
|
160
210
|
@next="$emit('next')"
|
|
161
211
|
>
|
|
162
212
|
<template #view>
|
|
213
|
+
<gl-token
|
|
214
|
+
v-if="showToken"
|
|
215
|
+
:class="{ 'gl-cursor-pointer': !viewOnly }"
|
|
216
|
+
:view-only="viewOnly"
|
|
217
|
+
v-on="eventListeners"
|
|
218
|
+
>{{ value.data }}</gl-token
|
|
219
|
+
>
|
|
220
|
+
|
|
163
221
|
<input
|
|
164
|
-
v-if="
|
|
222
|
+
v-else-if="showInput"
|
|
165
223
|
v-bind="searchInputAttributes"
|
|
166
224
|
class="gl-filtered-search-term-input"
|
|
167
225
|
:class="{ 'gl-bg-gray-10': viewOnly }"
|