@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
|
@@ -3,12 +3,11 @@ 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, tokenToOption } from './filtered_search_utils';
|
|
6
|
+
import { createTerm, tokenToOption, TOKEN_CLOSE_SELECTOR } from './filtered_search_utils';
|
|
7
7
|
|
|
8
8
|
const SEGMENT_TITLE = 'TYPE';
|
|
9
9
|
const SEGMENT_OPERATOR = 'OPERATOR';
|
|
10
10
|
const SEGMENT_DATA = 'DATA';
|
|
11
|
-
const TOKEN_CLOSE_SELECTOR = '.gl-token-close';
|
|
12
11
|
|
|
13
12
|
const DEFAULT_OPERATORS = [
|
|
14
13
|
{ value: '=', description: 'is', default: true },
|
|
@@ -392,6 +391,7 @@ export default {
|
|
|
392
391
|
|
|
393
392
|
<!--
|
|
394
393
|
Emitted when Space is pressed in-between term text.
|
|
394
|
+
Not emitted when termsAsTokens is true.
|
|
395
395
|
@event split
|
|
396
396
|
@property {array} newTokens Token configurations
|
|
397
397
|
-->
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { shallowMount } from '@vue/test-utils';
|
|
2
2
|
import GlFilteredSearchTokenSegment from './filtered_search_token_segment.vue';
|
|
3
|
+
import { TERM_TOKEN_TYPE } from './filtered_search_utils';
|
|
3
4
|
|
|
4
5
|
const OPTIONS = [
|
|
5
6
|
{ value: '=', title: 'is' },
|
|
@@ -26,7 +27,7 @@ describe('Filtered search token segment', () => {
|
|
|
26
27
|
let alignSuggestionsMock;
|
|
27
28
|
let suggestionsMock;
|
|
28
29
|
|
|
29
|
-
const createComponent = (props) => {
|
|
30
|
+
const createComponent = ({ termsAsTokens = false, ...props } = {}) => {
|
|
30
31
|
alignSuggestionsMock = jest.fn();
|
|
31
32
|
suggestionsMock = {
|
|
32
33
|
methods: {
|
|
@@ -42,6 +43,7 @@ describe('Filtered search token segment', () => {
|
|
|
42
43
|
provide: {
|
|
43
44
|
portalName: 'fakePortal',
|
|
44
45
|
alignSuggestions: alignSuggestionsMock,
|
|
46
|
+
termsAsTokens: () => termsAsTokens,
|
|
45
47
|
},
|
|
46
48
|
stubs: {
|
|
47
49
|
Portal: { template: '<div><slot></slot></div>' },
|
|
@@ -50,7 +52,7 @@ describe('Filtered search token segment', () => {
|
|
|
50
52
|
});
|
|
51
53
|
};
|
|
52
54
|
|
|
53
|
-
const createWrappedComponent = ({ value, ...otherProps } = {}) => {
|
|
55
|
+
const createWrappedComponent = ({ value, termsAsTokens = false, ...otherProps } = {}) => {
|
|
54
56
|
// We need to create fake parent due to https://github.com/vuejs/vue-test-utils/issues/1140
|
|
55
57
|
const fakeParent = {
|
|
56
58
|
inheritAttrs: false,
|
|
@@ -68,6 +70,9 @@ describe('Filtered search token segment', () => {
|
|
|
68
70
|
|
|
69
71
|
wrapper = shallowMount(fakeParent, {
|
|
70
72
|
propsData: { ...otherProps, cursorPosition: 'end' },
|
|
73
|
+
provide: {
|
|
74
|
+
termsAsTokens: () => termsAsTokens,
|
|
75
|
+
},
|
|
71
76
|
stubs: { GlFilteredSearchTokenSegment },
|
|
72
77
|
});
|
|
73
78
|
};
|
|
@@ -184,6 +189,22 @@ describe('Filtered search token segment', () => {
|
|
|
184
189
|
);
|
|
185
190
|
});
|
|
186
191
|
|
|
192
|
+
it('leaves value as-is if options are provided and isTerm=true', async () => {
|
|
193
|
+
const originalValue = '!=';
|
|
194
|
+
createWrappedComponent({
|
|
195
|
+
value: originalValue,
|
|
196
|
+
title: 'Test',
|
|
197
|
+
options: OPTIONS,
|
|
198
|
+
isTerm: true,
|
|
199
|
+
active: true,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await wrapper.setData({ value: 'invalid' });
|
|
203
|
+
await wrapper.setProps({ active: false });
|
|
204
|
+
|
|
205
|
+
expect(wrapper.findComponent(GlFilteredSearchTokenSegment).emitted().input).toBe(undefined);
|
|
206
|
+
});
|
|
207
|
+
|
|
187
208
|
describe('applySuggestion', () => {
|
|
188
209
|
it('emits original token when no spaces are present', () => {
|
|
189
210
|
createComponent({ value: '' });
|
|
@@ -209,6 +230,29 @@ describe('Filtered search token segment', () => {
|
|
|
209
230
|
expect(wrapper.emitted('complete')[0][0]).toBe(formattedToken);
|
|
210
231
|
});
|
|
211
232
|
|
|
233
|
+
it('emits token as-is when spaces are present and termsAsTokens=true', () => {
|
|
234
|
+
createComponent({ value: '', termsAsTokens: true });
|
|
235
|
+
|
|
236
|
+
const token = 'token with spaces';
|
|
237
|
+
|
|
238
|
+
wrapper.vm.applySuggestion(token);
|
|
239
|
+
|
|
240
|
+
expect(wrapper.emitted('input')[0][0]).toBe(token);
|
|
241
|
+
expect(wrapper.emitted('select')[0][0]).toBe(token);
|
|
242
|
+
expect(wrapper.emitted('complete')[0][0]).toBe(token);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('emits input value when term token suggestion is chosen and termsAsTokens=true', () => {
|
|
246
|
+
const value = `some text with 'spaces and' "quotes"`;
|
|
247
|
+
createComponent({ value, termsAsTokens: true });
|
|
248
|
+
|
|
249
|
+
wrapper.vm.applySuggestion(TERM_TOKEN_TYPE);
|
|
250
|
+
|
|
251
|
+
expect(wrapper.emitted('input')[0][0]).toBe(value);
|
|
252
|
+
expect(wrapper.emitted('select')[0][0]).toBe(TERM_TOKEN_TYPE);
|
|
253
|
+
expect(wrapper.emitted('complete')[0][0]).toBe(TERM_TOKEN_TYPE);
|
|
254
|
+
});
|
|
255
|
+
|
|
212
256
|
it('selects suggestion on press Enter', () => {
|
|
213
257
|
createComponent({ active: true, options: OPTIONS, value: false });
|
|
214
258
|
wrapper.find('input').trigger('keydown', { key: 'ArrowDown' });
|
|
@@ -4,7 +4,7 @@ import { Portal } from 'portal-vue';
|
|
|
4
4
|
import { COMMA, LEFT_MOUSE_BUTTON } from '../../../utils/constants';
|
|
5
5
|
import GlFilteredSearchSuggestion from './filtered_search_suggestion.vue';
|
|
6
6
|
import GlFilteredSearchSuggestionList from './filtered_search_suggestion_list.vue';
|
|
7
|
-
import { splitOnQuotes, wrapTokenInQuotes, match } from './filtered_search_utils';
|
|
7
|
+
import { splitOnQuotes, wrapTokenInQuotes, match, TERM_TOKEN_TYPE } from './filtered_search_utils';
|
|
8
8
|
|
|
9
9
|
// We need some helpers to ensure @vue/compat compatibility
|
|
10
10
|
// @vue/compat will render comment nodes for v-if and comments in HTML
|
|
@@ -51,7 +51,7 @@ export default {
|
|
|
51
51
|
GlFilteredSearchSuggestionList,
|
|
52
52
|
GlFilteredSearchSuggestion,
|
|
53
53
|
},
|
|
54
|
-
inject: ['portalName', 'alignSuggestions'],
|
|
54
|
+
inject: ['portalName', 'alignSuggestions', 'termsAsTokens'],
|
|
55
55
|
inheritAttrs: false,
|
|
56
56
|
props: {
|
|
57
57
|
/**
|
|
@@ -140,6 +140,13 @@ export default {
|
|
|
140
140
|
},
|
|
141
141
|
|
|
142
142
|
computed: {
|
|
143
|
+
hasTermSuggestion() {
|
|
144
|
+
if (!this.termsAsTokens()) return false;
|
|
145
|
+
if (!this.options) return false;
|
|
146
|
+
|
|
147
|
+
return this.options.some(({ value }) => value === TERM_TOKEN_TYPE);
|
|
148
|
+
},
|
|
149
|
+
|
|
143
150
|
matchingOption() {
|
|
144
151
|
return this.options?.find((o) => o.value === this.value);
|
|
145
152
|
},
|
|
@@ -182,7 +189,10 @@ export default {
|
|
|
182
189
|
const option =
|
|
183
190
|
this.getMatchingOptionForInputValue(this.inputValue) ||
|
|
184
191
|
this.getMatchingOptionForInputValue(this.inputValue, { loose: true });
|
|
185
|
-
|
|
192
|
+
|
|
193
|
+
if (option) return option.value;
|
|
194
|
+
if (this.hasTermSuggestion) return TERM_TOKEN_TYPE;
|
|
195
|
+
return null;
|
|
186
196
|
}
|
|
187
197
|
|
|
188
198
|
const defaultOption = this.options.find((op) => op.default);
|
|
@@ -216,6 +226,8 @@ export default {
|
|
|
216
226
|
},
|
|
217
227
|
|
|
218
228
|
inputValue(newValue) {
|
|
229
|
+
if (this.termsAsTokens()) return;
|
|
230
|
+
|
|
219
231
|
const hasUnclosedQuote = newValue.split('"').length % 2 === 0;
|
|
220
232
|
if (newValue.indexOf(' ') === -1 || hasUnclosedQuote) {
|
|
221
233
|
return;
|
|
@@ -282,7 +294,9 @@ export default {
|
|
|
282
294
|
},
|
|
283
295
|
|
|
284
296
|
applySuggestion(suggestedValue) {
|
|
285
|
-
const formattedSuggestedValue =
|
|
297
|
+
const formattedSuggestedValue = this.termsAsTokens()
|
|
298
|
+
? suggestedValue
|
|
299
|
+
: wrapTokenInQuotes(suggestedValue);
|
|
286
300
|
|
|
287
301
|
/**
|
|
288
302
|
* Emitted when autocomplete entry is selected.
|
|
@@ -292,7 +306,10 @@ export default {
|
|
|
292
306
|
this.$emit('select', formattedSuggestedValue);
|
|
293
307
|
|
|
294
308
|
if (!this.multiSelect) {
|
|
295
|
-
this.$emit(
|
|
309
|
+
this.$emit(
|
|
310
|
+
'input',
|
|
311
|
+
formattedSuggestedValue === TERM_TOKEN_TYPE ? this.inputValue : formattedSuggestedValue
|
|
312
|
+
);
|
|
296
313
|
this.$emit('complete', formattedSuggestedValue);
|
|
297
314
|
}
|
|
298
315
|
},
|
|
@@ -7,6 +7,8 @@ export const TERM_TOKEN_TYPE = 'filtered-search-term';
|
|
|
7
7
|
|
|
8
8
|
export const INTENT_ACTIVATE_PREVIOUS = 'intent-activate-previous';
|
|
9
9
|
|
|
10
|
+
export const TOKEN_CLOSE_SELECTOR = '.gl-token-close';
|
|
11
|
+
|
|
10
12
|
export function isEmptyTerm(token) {
|
|
11
13
|
return token.type === TERM_TOKEN_TYPE && token.value.data.trim() === '';
|
|
12
14
|
}
|
|
@@ -135,21 +137,26 @@ export function createTerm(data = '') {
|
|
|
135
137
|
};
|
|
136
138
|
}
|
|
137
139
|
|
|
138
|
-
export function denormalizeTokens(inputTokens) {
|
|
140
|
+
export function denormalizeTokens(inputTokens, termsAsTokens = false) {
|
|
139
141
|
assertValidTokens(inputTokens);
|
|
140
142
|
|
|
141
143
|
const tokens = Array.isArray(inputTokens) ? inputTokens : [inputTokens];
|
|
142
144
|
|
|
143
|
-
|
|
144
|
-
tokens.forEach((t) => {
|
|
145
|
+
return tokens.reduce((result, t) => {
|
|
145
146
|
if (typeof t === 'string') {
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
if (termsAsTokens) {
|
|
148
|
+
const trimmedText = t.trim();
|
|
149
|
+
if (trimmedText) result.push(createTerm(trimmedText));
|
|
150
|
+
} else {
|
|
151
|
+
const stringTokens = t.split(' ').filter(Boolean);
|
|
152
|
+
stringTokens.forEach((strToken) => result.push(createTerm(strToken)));
|
|
153
|
+
}
|
|
148
154
|
} else {
|
|
149
155
|
result.push(ensureTokenId(t));
|
|
150
156
|
}
|
|
151
|
-
|
|
152
|
-
|
|
157
|
+
|
|
158
|
+
return result;
|
|
159
|
+
}, []);
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
/**
|
|
@@ -165,6 +172,12 @@ export function match(text, query) {
|
|
|
165
172
|
return text.toLowerCase().includes(query.toLowerCase());
|
|
166
173
|
}
|
|
167
174
|
|
|
175
|
+
export const termTokenDefinition = {
|
|
176
|
+
type: TERM_TOKEN_TYPE,
|
|
177
|
+
icon: 'title',
|
|
178
|
+
title: 'Search for this text',
|
|
179
|
+
};
|
|
180
|
+
|
|
168
181
|
export function splitOnQuotes(str) {
|
|
169
182
|
if (first(str) === "'" && last(str) === "'") {
|
|
170
183
|
return [str];
|