@gitlab/ui 38.2.0 → 38.2.1
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/filtered_search.js +13 -20
- package/dist/components/base/filtered_search/filtered_search_suggestion.js +1 -1
- package/dist/components/base/filtered_search/filtered_search_token.js +3 -7
- package/dist/components/base/filtered_search/filtered_search_utils.js +42 -9
- package/package.json +1 -1
- package/src/components/base/filtered_search/filtered_search.spec.js +37 -12
- package/src/components/base/filtered_search/filtered_search.stories.js +13 -6
- package/src/components/base/filtered_search/filtered_search.vue +12 -14
- package/src/components/base/filtered_search/filtered_search_suggestion.vue +1 -0
- package/src/components/base/filtered_search/filtered_search_token.spec.js +9 -1
- package/src/components/base/filtered_search/filtered_search_token.vue +3 -2
- package/src/components/base/filtered_search/filtered_search_utils.js +38 -5
- package/src/components/base/skeleton_loader/skeleton_loader.stories.js +67 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [38.2.1](https://gitlab.com/gitlab-org/gitlab-ui/compare/v38.2.0...v38.2.1) (2022-04-01)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **GlFilteredSearch:** Generate stable token keys ([88dccde](https://gitlab.com/gitlab-org/gitlab-ui/commit/88dccde9f4df1352a7e06e831de8baced4a8d18f))
|
|
7
|
+
|
|
1
8
|
# [38.2.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v38.1.0...v38.2.0) (2022-03-31)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -5,22 +5,12 @@ import { GlTooltipDirective } from '../../../directives/tooltip';
|
|
|
5
5
|
import GlIcon from '../icon/icon';
|
|
6
6
|
import GlSearchBoxByClick from '../search_box_by_click/search_box_by_click';
|
|
7
7
|
import GlFilteredSearchTerm from './filtered_search_term';
|
|
8
|
-
import {
|
|
8
|
+
import { createTerm, needDenormalization, denormalizeTokens, isEmptyTerm, INTENT_ACTIVATE_PREVIOUS, ensureTokenId, normalizeTokens } from './filtered_search_utils';
|
|
9
9
|
import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
|
|
10
10
|
|
|
11
11
|
Vue.use(PortalVue);
|
|
12
12
|
let portalUuid = 0;
|
|
13
13
|
|
|
14
|
-
function createTerm() {
|
|
15
|
-
let data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
|
|
16
|
-
return {
|
|
17
|
-
type: TERM_TOKEN_TYPE,
|
|
18
|
-
value: {
|
|
19
|
-
data
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
14
|
function initialState() {
|
|
25
15
|
return [createTerm()];
|
|
26
16
|
}
|
|
@@ -172,6 +162,14 @@ var script = {
|
|
|
172
162
|
watch: {
|
|
173
163
|
tokens: {
|
|
174
164
|
handler() {
|
|
165
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
166
|
+
const invalidToken = this.tokens.find(token => !token.id);
|
|
167
|
+
|
|
168
|
+
if (invalidToken) {
|
|
169
|
+
throw new Error(`Token does not have an id:\n${JSON.stringify(invalidToken)}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
175
173
|
if (this.tokens.length === 0 || !this.isLastTokenEmpty()) {
|
|
176
174
|
this.tokens.push(createTerm());
|
|
177
175
|
}
|
|
@@ -278,12 +276,12 @@ var script = {
|
|
|
278
276
|
},
|
|
279
277
|
|
|
280
278
|
replaceToken(idx, token) {
|
|
281
|
-
this.$set(this.tokens, idx, { ...token,
|
|
279
|
+
this.$set(this.tokens, idx, ensureTokenId({ ...token,
|
|
282
280
|
value: {
|
|
283
281
|
data: '',
|
|
284
282
|
...token.value
|
|
285
283
|
}
|
|
286
|
-
});
|
|
284
|
+
}));
|
|
287
285
|
this.activeTokenIdx = idx;
|
|
288
286
|
},
|
|
289
287
|
|
|
@@ -295,12 +293,7 @@ var script = {
|
|
|
295
293
|
return;
|
|
296
294
|
}
|
|
297
295
|
|
|
298
|
-
const newTokens = newStrings.map(data => (
|
|
299
|
-
type: TERM_TOKEN_TYPE,
|
|
300
|
-
value: {
|
|
301
|
-
data
|
|
302
|
-
}
|
|
303
|
-
}));
|
|
296
|
+
const newTokens = newStrings.map(data => createTerm(data));
|
|
304
297
|
this.tokens.splice(idx + 1, 0, ...newTokens);
|
|
305
298
|
this.activeTokenIdx = idx + newStrings.length;
|
|
306
299
|
},
|
|
@@ -333,7 +326,7 @@ var script = {
|
|
|
333
326
|
const __vue_script__ = script;
|
|
334
327
|
|
|
335
328
|
/* template */
|
|
336
|
-
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('gl-search-box-by-click',_vm._b({attrs:{"value":_vm.tokens,"history-items":_vm.historyItems,"clearable":_vm.hasValue,"search-button-attributes":_vm.searchButtonAttributes,"data-testid":"filtered-search-input"},on:{"submit":_vm.submit,"input":_vm.applyNewValue,"history-item-selected":function($event){return _vm.$emit('history-item-selected', $event)},"clear":function($event){return _vm.$emit('clear')},"clear-history":function($event){return _vm.$emit('clear-history')}},scopedSlots:_vm._u([{key:"history-item",fn:function(slotScope){return [_vm._t("history-item",null,null,slotScope)]}},{key:"input",fn:function(){return [_c('div',{staticClass:"gl-filtered-search-scrollable"},[_vm._l((_vm.tokens),function(token,idx){return [_c(_vm.getTokenComponent(token.type),{key:
|
|
329
|
+
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('gl-search-box-by-click',_vm._b({attrs:{"value":_vm.tokens,"history-items":_vm.historyItems,"clearable":_vm.hasValue,"search-button-attributes":_vm.searchButtonAttributes,"data-testid":"filtered-search-input"},on:{"submit":_vm.submit,"input":_vm.applyNewValue,"history-item-selected":function($event){return _vm.$emit('history-item-selected', $event)},"clear":function($event){return _vm.$emit('clear')},"clear-history":function($event){return _vm.$emit('clear-history')}},scopedSlots:_vm._u([{key:"history-item",fn:function(slotScope){return [_vm._t("history-item",null,null,slotScope)]}},{key:"input",fn:function(){return [_c('div',{staticClass:"gl-filtered-search-scrollable"},[_vm._l((_vm.tokens),function(token,idx){return [_c(_vm.getTokenComponent(token.type),{key:token.id,ref:"tokens",refInFor:true,tag:"component",staticClass:"gl-filtered-search-item",class:{
|
|
337
330
|
'gl-filtered-search-last-item': _vm.isLastToken(idx),
|
|
338
331
|
},attrs:{"config":_vm.getTokenEntry(token.type),"active":_vm.activeTokenIdx === idx,"available-tokens":_vm.currentAvailableTokens,"current-value":_vm.tokens,"index":idx,"placeholder":_vm.termPlaceholder,"show-friendly-text":_vm.showFriendlyText,"search-input-attributes":_vm.searchInputAttributes,"is-last-token":_vm.isLastToken(idx)},on:{"activate":function($event){return _vm.activate(idx)},"deactivate":function($event){return _vm.deactivate(token)},"destroy":function($event){return _vm.destroyToken(idx, $event)},"replace":function($event){return _vm.replaceToken(idx, $event)},"complete":_vm.completeToken,"submit":_vm.submit,"split":function($event){return _vm.createTokens(idx, $event)}},model:{value:(token.value),callback:function ($$v) {_vm.$set(token, "value", $$v);},expression:"token.value"}})]})],2),_vm._v(" "),_c('portal-target',{key:_vm.activeTokenIdx,ref:"menu",style:(_vm.suggestionsStyle),attrs:{"name":_vm.portalName,"slim":""}})]},proxy:true}],null,true)},'gl-search-box-by-click',_vm.$attrs,false))};
|
|
339
332
|
var __vue_staticRenderFns__ = [];
|
|
@@ -59,7 +59,7 @@ var script = {
|
|
|
59
59
|
const __vue_script__ = script;
|
|
60
60
|
|
|
61
61
|
/* template */
|
|
62
|
-
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('gl-dropdown-item',_vm._b({ref:"item",staticClass:"gl-filtered-search-suggestion",class:{ 'gl-filtered-search-suggestion-active': _vm.isActive },attrs:{"href":"#"},nativeOn:{"mousedown":function($event){$event.preventDefault();return _vm.emitValue($event)}}},'gl-dropdown-item',_vm.$attrs,false),[_vm._t("default")],2)};
|
|
62
|
+
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('gl-dropdown-item',_vm._b({ref:"item",staticClass:"gl-filtered-search-suggestion",class:{ 'gl-filtered-search-suggestion-active': _vm.isActive },attrs:{"data-testid":"filtered-search-suggestion","href":"#"},nativeOn:{"mousedown":function($event){$event.preventDefault();return _vm.emitValue($event)}}},'gl-dropdown-item',_vm.$attrs,false),[_vm._t("default")],2)};
|
|
63
63
|
var __vue_staticRenderFns__ = [];
|
|
64
64
|
|
|
65
65
|
/* style */
|
|
@@ -2,7 +2,7 @@ import _cloneDeep from 'lodash/cloneDeep';
|
|
|
2
2
|
import { COMMA } from '../../../utils/constants';
|
|
3
3
|
import GlToken from '../token/token';
|
|
4
4
|
import GlFilteredSearchTokenSegment from './filtered_search_token_segment';
|
|
5
|
-
import {
|
|
5
|
+
import { createTerm } from './filtered_search_utils';
|
|
6
6
|
import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
|
|
7
7
|
|
|
8
8
|
const SEGMENT_TITLE = 'TYPE';
|
|
@@ -203,12 +203,7 @@ var script = {
|
|
|
203
203
|
* Emitted when this token is converted to another type
|
|
204
204
|
* @property {object} token Replacement token configuration
|
|
205
205
|
*/
|
|
206
|
-
this.$emit('replace',
|
|
207
|
-
type: TERM_TOKEN_TYPE,
|
|
208
|
-
value: {
|
|
209
|
-
data: this.config.title
|
|
210
|
-
}
|
|
211
|
-
});
|
|
206
|
+
this.$emit('replace', createTerm(this.config.title));
|
|
212
207
|
}
|
|
213
208
|
},
|
|
214
209
|
|
|
@@ -298,6 +293,7 @@ var script = {
|
|
|
298
293
|
destroyByClose(event) {
|
|
299
294
|
if (event.target.closest(TOKEN_CLOSE_SELECTOR)) {
|
|
300
295
|
event.preventDefault();
|
|
296
|
+
event.stopPropagation();
|
|
301
297
|
this.$emit('destroy');
|
|
302
298
|
}
|
|
303
299
|
}
|
|
@@ -38,7 +38,45 @@ function needDenormalization(tokens) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
assertValidTokens(tokens);
|
|
41
|
-
return tokens.some(t => typeof t === 'string');
|
|
41
|
+
return tokens.some(t => typeof t === 'string' || !t.id);
|
|
42
|
+
}
|
|
43
|
+
let tokenIdCounter = 0;
|
|
44
|
+
|
|
45
|
+
const getTokenId = () => {
|
|
46
|
+
const tokenId = `token-${tokenIdCounter}`;
|
|
47
|
+
tokenIdCounter += 1;
|
|
48
|
+
return tokenId;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Ensure the given token has an `id` property, which `GlFilteredSearch` relies
|
|
52
|
+
* on as a unique key for the token.
|
|
53
|
+
*
|
|
54
|
+
* If the given token does not have an `id`, it returns a shallow copy of the
|
|
55
|
+
* token with an `id`. Otherwise, it returns the given token.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} token The token to check.
|
|
58
|
+
* @returns {object} A token with an `id`.
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
function ensureTokenId(token) {
|
|
63
|
+
if (!token.id) {
|
|
64
|
+
return { ...token,
|
|
65
|
+
id: getTokenId()
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return token;
|
|
70
|
+
}
|
|
71
|
+
function createTerm() {
|
|
72
|
+
let data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
|
|
73
|
+
return {
|
|
74
|
+
id: getTokenId(),
|
|
75
|
+
type: TERM_TOKEN_TYPE,
|
|
76
|
+
value: {
|
|
77
|
+
data
|
|
78
|
+
}
|
|
79
|
+
};
|
|
42
80
|
}
|
|
43
81
|
function denormalizeTokens(inputTokens) {
|
|
44
82
|
assertValidTokens(inputTokens);
|
|
@@ -47,14 +85,9 @@ function denormalizeTokens(inputTokens) {
|
|
|
47
85
|
tokens.forEach(t => {
|
|
48
86
|
if (typeof t === 'string') {
|
|
49
87
|
const stringTokens = t.split(' ').filter(Boolean);
|
|
50
|
-
stringTokens.forEach(strToken => result.push(
|
|
51
|
-
type: TERM_TOKEN_TYPE,
|
|
52
|
-
value: {
|
|
53
|
-
data: strToken
|
|
54
|
-
}
|
|
55
|
-
}));
|
|
88
|
+
stringTokens.forEach(strToken => result.push(createTerm(strToken)));
|
|
56
89
|
} else {
|
|
57
|
-
result.push(t);
|
|
90
|
+
result.push(ensureTokenId(t));
|
|
58
91
|
}
|
|
59
92
|
});
|
|
60
93
|
return result;
|
|
@@ -135,4 +168,4 @@ function wrapTokenInQuotes(token) {
|
|
|
135
168
|
return `"${token}"`;
|
|
136
169
|
}
|
|
137
170
|
|
|
138
|
-
export { INTENT_ACTIVATE_PREVIOUS, TERM_TOKEN_TYPE, denormalizeTokens, isEmptyTerm, needDenormalization, normalizeTokens, splitOnQuotes, wrapTokenInQuotes };
|
|
171
|
+
export { INTENT_ACTIVATE_PREVIOUS, TERM_TOKEN_TYPE, createTerm, denormalizeTokens, ensureTokenId, isEmptyTerm, needDenormalization, normalizeTokens, splitOnQuotes, wrapTokenInQuotes };
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Vue, { nextTick } from 'vue';
|
|
2
|
+
import { omit } from 'lodash';
|
|
2
3
|
import { shallowMount, mount } from '@vue/test-utils';
|
|
3
4
|
import GlFilteredSearch from './filtered_search.vue';
|
|
4
5
|
import GlFilteredSearchSuggestion from './filtered_search_suggestion.vue';
|
|
@@ -17,6 +18,8 @@ const FakeToken = {
|
|
|
17
18
|
|
|
18
19
|
Vue.directive('GlTooltip', () => {});
|
|
19
20
|
|
|
21
|
+
const stripId = (token) => (typeof token === 'object' ? omit(token, 'id') : token);
|
|
22
|
+
|
|
20
23
|
let wrapper;
|
|
21
24
|
describe('Filtered search', () => {
|
|
22
25
|
const defaultProps = {
|
|
@@ -41,7 +44,7 @@ describe('Filtered search', () => {
|
|
|
41
44
|
describe('value manipulation', () => {
|
|
42
45
|
it('creates term when empty', () => {
|
|
43
46
|
createComponent();
|
|
44
|
-
expect(wrapper.emitted().input[0][0]).toStrictEqual([
|
|
47
|
+
expect(wrapper.emitted().input[0][0].map(stripId)).toStrictEqual([
|
|
45
48
|
{ type: TERM_TOKEN_TYPE, value: { data: '' } },
|
|
46
49
|
]);
|
|
47
50
|
});
|
|
@@ -56,7 +59,7 @@ describe('Filtered search', () => {
|
|
|
56
59
|
value: [{ type: 'faketoken', value: { data: '' } }],
|
|
57
60
|
});
|
|
58
61
|
|
|
59
|
-
expect(wrapper.emitted().input[0][0].pop()).toStrictEqual({
|
|
62
|
+
expect(stripId(wrapper.emitted().input[0][0].pop())).toStrictEqual({
|
|
60
63
|
type: TERM_TOKEN_TYPE,
|
|
61
64
|
value: { data: '' },
|
|
62
65
|
});
|
|
@@ -172,7 +175,7 @@ describe('Filtered search', () => {
|
|
|
172
175
|
|
|
173
176
|
await nextTick();
|
|
174
177
|
|
|
175
|
-
expect(wrapper.emitted().input.pop()[0]).toStrictEqual([
|
|
178
|
+
expect(wrapper.emitted().input.pop()[0].map(stripId)).toStrictEqual([
|
|
176
179
|
{ type: 'faketoken', value: { data: '' } },
|
|
177
180
|
{ type: TERM_TOKEN_TYPE, value: { data: 'one' } },
|
|
178
181
|
{ type: TERM_TOKEN_TYPE, value: { data: 'three' } },
|
|
@@ -190,7 +193,7 @@ describe('Filtered search', () => {
|
|
|
190
193
|
|
|
191
194
|
await nextTick();
|
|
192
195
|
|
|
193
|
-
expect(wrapper.emitted().input.pop()[0]).toStrictEqual([
|
|
196
|
+
expect(wrapper.emitted().input.pop()[0].map(stripId)).toStrictEqual([
|
|
194
197
|
{ type: TERM_TOKEN_TYPE, value: { data: 'one' } },
|
|
195
198
|
{ type: TERM_TOKEN_TYPE, value: { data: '' } },
|
|
196
199
|
]);
|
|
@@ -305,7 +308,7 @@ describe('Filtered search', () => {
|
|
|
305
308
|
|
|
306
309
|
await nextTick();
|
|
307
310
|
|
|
308
|
-
expect(wrapper.emitted().input.pop()[0]).toStrictEqual([
|
|
311
|
+
expect(wrapper.emitted().input.pop()[0].map(stripId)).toStrictEqual([
|
|
309
312
|
{ type: TERM_TOKEN_TYPE, value: { data: '' } },
|
|
310
313
|
]);
|
|
311
314
|
});
|
|
@@ -318,7 +321,7 @@ describe('Filtered search', () => {
|
|
|
318
321
|
|
|
319
322
|
await nextTick();
|
|
320
323
|
|
|
321
|
-
expect(wrapper.emitted().input.pop()[0]).toStrictEqual([
|
|
324
|
+
expect(wrapper.emitted().input.pop()[0].map(stripId)).toStrictEqual([
|
|
322
325
|
{ type: 'faketoken', value: { data: 'test' } },
|
|
323
326
|
{ type: TERM_TOKEN_TYPE, value: { data: '' } },
|
|
324
327
|
]);
|
|
@@ -339,7 +342,7 @@ describe('Filtered search', () => {
|
|
|
339
342
|
|
|
340
343
|
await nextTick();
|
|
341
344
|
|
|
342
|
-
expect(wrapper.emitted().input.pop()[0]).toStrictEqual([
|
|
345
|
+
expect(wrapper.emitted().input.pop()[0].map(stripId)).toStrictEqual([
|
|
343
346
|
{ type: 'faketoken', value: { data: 'test' } },
|
|
344
347
|
{ type: TERM_TOKEN_TYPE, value: { data: '' } },
|
|
345
348
|
]);
|
|
@@ -352,7 +355,7 @@ describe('Filtered search', () => {
|
|
|
352
355
|
|
|
353
356
|
await nextTick();
|
|
354
357
|
|
|
355
|
-
expect(wrapper.emitted().input.pop()[0]).toStrictEqual([
|
|
358
|
+
expect(wrapper.emitted().input.pop()[0].map(stripId)).toStrictEqual([
|
|
356
359
|
{ type: TERM_TOKEN_TYPE, value: { data: 'one' } },
|
|
357
360
|
{ type: TERM_TOKEN_TYPE, value: { data: '' } },
|
|
358
361
|
]);
|
|
@@ -368,7 +371,7 @@ describe('Filtered search', () => {
|
|
|
368
371
|
await nextTick();
|
|
369
372
|
|
|
370
373
|
expect(wrapper.findAllComponents(GlFilteredSearchTerm).at(2).props('active')).toBe(true);
|
|
371
|
-
expect(wrapper.emitted().input.pop()[0]).toStrictEqual([
|
|
374
|
+
expect(wrapper.emitted().input.pop()[0].map(stripId)).toStrictEqual([
|
|
372
375
|
{ type: TERM_TOKEN_TYPE, value: { data: 'one' } },
|
|
373
376
|
{ type: TERM_TOKEN_TYPE, value: { data: 'two' } },
|
|
374
377
|
{ type: TERM_TOKEN_TYPE, value: { data: '' } },
|
|
@@ -385,7 +388,7 @@ describe('Filtered search', () => {
|
|
|
385
388
|
|
|
386
389
|
await nextTick();
|
|
387
390
|
|
|
388
|
-
expect(wrapper.emitted().input.pop()[0]).toStrictEqual([
|
|
391
|
+
expect(wrapper.emitted().input.pop()[0].map(stripId)).toStrictEqual([
|
|
389
392
|
{ type: TERM_TOKEN_TYPE, value: { data: 'one' } },
|
|
390
393
|
{ type: TERM_TOKEN_TYPE, value: { data: 'foo' } },
|
|
391
394
|
{ type: TERM_TOKEN_TYPE, value: { data: 'bar' } },
|
|
@@ -415,7 +418,7 @@ describe('Filtered search', () => {
|
|
|
415
418
|
});
|
|
416
419
|
wrapper.findComponent(GlFilteredSearchTerm).vm.$emit('submit');
|
|
417
420
|
expect(wrapper.emitted().submit).toBeDefined();
|
|
418
|
-
expect(wrapper.emitted().submit[0][0]).toStrictEqual([
|
|
421
|
+
expect(wrapper.emitted().submit[0][0].map(stripId)).toStrictEqual([
|
|
419
422
|
'one two',
|
|
420
423
|
{ type: 'faketoken', value: { data: 'smth' } },
|
|
421
424
|
'four five',
|
|
@@ -475,7 +478,7 @@ describe('Filtered search', () => {
|
|
|
475
478
|
});
|
|
476
479
|
await nextTick();
|
|
477
480
|
|
|
478
|
-
expect(wrapper.findComponent(GlFilteredSearchTerm).props('currentValue')).toEqual([
|
|
481
|
+
expect(wrapper.findComponent(GlFilteredSearchTerm).props('currentValue').map(stripId)).toEqual([
|
|
479
482
|
{ type: 'filtered-search-term', value: { data: 'one' } },
|
|
480
483
|
{ type: 'filtered-search-term', value: { data: '' } },
|
|
481
484
|
]);
|
|
@@ -705,4 +708,26 @@ describe('Filtered search integration tests', () => {
|
|
|
705
708
|
|
|
706
709
|
expect(wrapper.findAllComponents(GlFilteredSearchTerm)).toHaveLength(1);
|
|
707
710
|
});
|
|
711
|
+
|
|
712
|
+
// Regression test for https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1761
|
|
713
|
+
it('does not incorrectly activate next token of the same type after token destruction', async () => {
|
|
714
|
+
mountComponent({
|
|
715
|
+
value: [
|
|
716
|
+
{ type: 'static', value: { data: 'first', operator: '=' } },
|
|
717
|
+
{ type: 'static', value: { data: 'second', operator: '=' } },
|
|
718
|
+
{ type: 'unique', value: { data: 'something' } },
|
|
719
|
+
],
|
|
720
|
+
});
|
|
721
|
+
await nextTick();
|
|
722
|
+
|
|
723
|
+
expect(
|
|
724
|
+
wrapper.findAllComponents(GlFilteredSearchToken).wrappers.map((cmp) => cmp.props('active'))
|
|
725
|
+
).toEqual([false, false, false]);
|
|
726
|
+
|
|
727
|
+
await wrapper.find('.gl-token-close').trigger('mousedown');
|
|
728
|
+
|
|
729
|
+
expect(
|
|
730
|
+
wrapper.findAllComponents(GlFilteredSearchToken).wrappers.map((cmp) => cmp.props('active'))
|
|
731
|
+
).toEqual([false, false]);
|
|
732
|
+
});
|
|
708
733
|
});
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
GlFilteredSearchTerm,
|
|
7
7
|
GlFilteredSearchTokenSegment,
|
|
8
8
|
GlLoadingIcon,
|
|
9
|
+
GlIcon,
|
|
9
10
|
GlToken,
|
|
10
11
|
GlAvatar,
|
|
11
12
|
} from '../../../index';
|
|
@@ -56,14 +57,14 @@ const UserToken = {
|
|
|
56
57
|
setStoryTimeout(() => {
|
|
57
58
|
this.loadingView = false;
|
|
58
59
|
this.activeUser = fakeUsers.find((u) => u.username === this.value.data);
|
|
59
|
-
},
|
|
60
|
+
}, 500);
|
|
60
61
|
},
|
|
61
62
|
loadSuggestions() {
|
|
62
63
|
this.loadingSuggestions = true;
|
|
63
64
|
setStoryTimeout(() => {
|
|
64
65
|
this.loadingSuggestions = false;
|
|
65
66
|
this.users = fakeUsers;
|
|
66
|
-
},
|
|
67
|
+
}, 500);
|
|
67
68
|
},
|
|
68
69
|
},
|
|
69
70
|
watch: {
|
|
@@ -132,7 +133,7 @@ const MilestoneToken = {
|
|
|
132
133
|
setStoryTimeout(() => {
|
|
133
134
|
this.loadingSuggestions = false;
|
|
134
135
|
this.milestones = fakeMilestones;
|
|
135
|
-
},
|
|
136
|
+
}, 500);
|
|
136
137
|
},
|
|
137
138
|
},
|
|
138
139
|
watch: {
|
|
@@ -214,7 +215,7 @@ const LabelToken = {
|
|
|
214
215
|
setStoryTimeout(() => {
|
|
215
216
|
this.loadingSuggestions = false;
|
|
216
217
|
this.labels = fakeLabels;
|
|
217
|
-
},
|
|
218
|
+
}, 500);
|
|
218
219
|
},
|
|
219
220
|
},
|
|
220
221
|
watch: {
|
|
@@ -322,7 +323,7 @@ export const WithHistoryItems = () => ({
|
|
|
322
323
|
type: 'demotoken',
|
|
323
324
|
title: 'Unique',
|
|
324
325
|
icon: 'document',
|
|
325
|
-
token:
|
|
326
|
+
token: GlFilteredSearchToken,
|
|
326
327
|
operators: [{ value: '=', description: 'is', default: 'true' }],
|
|
327
328
|
options: [
|
|
328
329
|
{ icon: 'heart', title: 'heart', value: 1 },
|
|
@@ -406,7 +407,13 @@ export const WithFriendlyText = () => ({
|
|
|
406
407
|
export const WithMultiSelect = () => {
|
|
407
408
|
const MultiUserToken = {
|
|
408
409
|
props: ['value', 'active', 'config'],
|
|
409
|
-
components: {
|
|
410
|
+
components: {
|
|
411
|
+
GlFilteredSearchToken,
|
|
412
|
+
GlFilteredSearchSuggestion,
|
|
413
|
+
GlLoadingIcon,
|
|
414
|
+
GlIcon,
|
|
415
|
+
GlAvatar,
|
|
416
|
+
},
|
|
410
417
|
inheritAttrs: false,
|
|
411
418
|
data() {
|
|
412
419
|
return {
|
|
@@ -8,8 +8,9 @@ import GlSearchBoxByClick from '../search_box_by_click/search_box_by_click.vue';
|
|
|
8
8
|
import GlFilteredSearchTerm from './filtered_search_term.vue';
|
|
9
9
|
import {
|
|
10
10
|
isEmptyTerm,
|
|
11
|
-
TERM_TOKEN_TYPE,
|
|
12
11
|
INTENT_ACTIVATE_PREVIOUS,
|
|
12
|
+
createTerm,
|
|
13
|
+
ensureTokenId,
|
|
13
14
|
normalizeTokens,
|
|
14
15
|
denormalizeTokens,
|
|
15
16
|
needDenormalization,
|
|
@@ -19,13 +20,6 @@ Vue.use(PortalVue);
|
|
|
19
20
|
|
|
20
21
|
let portalUuid = 0;
|
|
21
22
|
|
|
22
|
-
function createTerm(data = '') {
|
|
23
|
-
return {
|
|
24
|
-
type: TERM_TOKEN_TYPE,
|
|
25
|
-
value: { data },
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
23
|
function initialState() {
|
|
30
24
|
return [createTerm()];
|
|
31
25
|
}
|
|
@@ -160,6 +154,13 @@ export default {
|
|
|
160
154
|
watch: {
|
|
161
155
|
tokens: {
|
|
162
156
|
handler() {
|
|
157
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
158
|
+
const invalidToken = this.tokens.find((token) => !token.id);
|
|
159
|
+
if (invalidToken) {
|
|
160
|
+
throw new Error(`Token does not have an id:\n${JSON.stringify(invalidToken)}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
163
164
|
if (this.tokens.length === 0 || !this.isLastTokenEmpty()) {
|
|
164
165
|
this.tokens.push(createTerm());
|
|
165
166
|
}
|
|
@@ -255,7 +256,7 @@ export default {
|
|
|
255
256
|
},
|
|
256
257
|
|
|
257
258
|
replaceToken(idx, token) {
|
|
258
|
-
this.$set(this.tokens, idx, { ...token, value: { data: '', ...token.value } });
|
|
259
|
+
this.$set(this.tokens, idx, ensureTokenId({ ...token, value: { data: '', ...token.value } }));
|
|
259
260
|
this.activeTokenIdx = idx;
|
|
260
261
|
},
|
|
261
262
|
|
|
@@ -269,10 +270,7 @@ export default {
|
|
|
269
270
|
return;
|
|
270
271
|
}
|
|
271
272
|
|
|
272
|
-
const newTokens = newStrings.map((data) => (
|
|
273
|
-
type: TERM_TOKEN_TYPE,
|
|
274
|
-
value: { data },
|
|
275
|
-
}));
|
|
273
|
+
const newTokens = newStrings.map((data) => createTerm(data));
|
|
276
274
|
|
|
277
275
|
this.tokens.splice(idx + 1, 0, ...newTokens);
|
|
278
276
|
|
|
@@ -340,7 +338,7 @@ export default {
|
|
|
340
338
|
<component
|
|
341
339
|
:is="getTokenComponent(token.type)"
|
|
342
340
|
ref="tokens"
|
|
343
|
-
:key="
|
|
341
|
+
:key="token.id"
|
|
344
342
|
v-model="token.value"
|
|
345
343
|
:config="getTokenEntry(token.type)"
|
|
346
344
|
:active="activeTokenIdx === idx"
|
|
@@ -225,8 +225,16 @@ describe('Filtered search token', () => {
|
|
|
225
225
|
mountComponent({ value: { operator: '=', data: 'something' } });
|
|
226
226
|
const closeWrapper = wrapper.find('.gl-token-close');
|
|
227
227
|
closeWrapper.element.closest = () => closeWrapper.element;
|
|
228
|
-
closeWrapper.trigger('mousedown');
|
|
229
228
|
|
|
229
|
+
const preventDefaultSpy = jest.fn();
|
|
230
|
+
const stopPropagationSpy = jest.fn();
|
|
231
|
+
closeWrapper.trigger('mousedown', {
|
|
232
|
+
preventDefault: preventDefaultSpy,
|
|
233
|
+
stopPropagation: stopPropagationSpy,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(preventDefaultSpy).toHaveBeenCalled();
|
|
237
|
+
expect(stopPropagationSpy).toHaveBeenCalled();
|
|
230
238
|
expect(wrapper.emitted().destroy).toHaveLength(1);
|
|
231
239
|
});
|
|
232
240
|
|
|
@@ -3,7 +3,7 @@ import { cloneDeep } from 'lodash';
|
|
|
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 {
|
|
6
|
+
import { createTerm } from './filtered_search_utils';
|
|
7
7
|
|
|
8
8
|
const SEGMENT_TITLE = 'TYPE';
|
|
9
9
|
const SEGMENT_OPERATOR = 'OPERATOR';
|
|
@@ -183,7 +183,7 @@ export default {
|
|
|
183
183
|
* Emitted when this token is converted to another type
|
|
184
184
|
* @property {object} token Replacement token configuration
|
|
185
185
|
*/
|
|
186
|
-
this.$emit('replace',
|
|
186
|
+
this.$emit('replace', createTerm(this.config.title));
|
|
187
187
|
}
|
|
188
188
|
},
|
|
189
189
|
|
|
@@ -254,6 +254,7 @@ export default {
|
|
|
254
254
|
destroyByClose(event) {
|
|
255
255
|
if (event.target.closest(TOKEN_CLOSE_SELECTOR)) {
|
|
256
256
|
event.preventDefault();
|
|
257
|
+
event.stopPropagation();
|
|
257
258
|
this.$emit('destroy');
|
|
258
259
|
}
|
|
259
260
|
},
|
|
@@ -39,7 +39,42 @@ export function needDenormalization(tokens) {
|
|
|
39
39
|
|
|
40
40
|
assertValidTokens(tokens);
|
|
41
41
|
|
|
42
|
-
return tokens.some((t) => typeof t === 'string');
|
|
42
|
+
return tokens.some((t) => typeof t === 'string' || !t.id);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let tokenIdCounter = 0;
|
|
46
|
+
const getTokenId = () => {
|
|
47
|
+
const tokenId = `token-${tokenIdCounter}`;
|
|
48
|
+
tokenIdCounter += 1;
|
|
49
|
+
return tokenId;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Ensure the given token has an `id` property, which `GlFilteredSearch` relies
|
|
53
|
+
* on as a unique key for the token.
|
|
54
|
+
*
|
|
55
|
+
* If the given token does not have an `id`, it returns a shallow copy of the
|
|
56
|
+
* token with an `id`. Otherwise, it returns the given token.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} token The token to check.
|
|
59
|
+
* @returns {object} A token with an `id`.
|
|
60
|
+
*/
|
|
61
|
+
export function ensureTokenId(token) {
|
|
62
|
+
if (!token.id) {
|
|
63
|
+
return {
|
|
64
|
+
...token,
|
|
65
|
+
id: getTokenId(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return token;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createTerm(data = '') {
|
|
73
|
+
return {
|
|
74
|
+
id: getTokenId(),
|
|
75
|
+
type: TERM_TOKEN_TYPE,
|
|
76
|
+
value: { data },
|
|
77
|
+
};
|
|
43
78
|
}
|
|
44
79
|
|
|
45
80
|
export function denormalizeTokens(inputTokens) {
|
|
@@ -51,11 +86,9 @@ export function denormalizeTokens(inputTokens) {
|
|
|
51
86
|
tokens.forEach((t) => {
|
|
52
87
|
if (typeof t === 'string') {
|
|
53
88
|
const stringTokens = t.split(' ').filter(Boolean);
|
|
54
|
-
stringTokens.forEach((strToken) =>
|
|
55
|
-
result.push({ type: TERM_TOKEN_TYPE, value: { data: strToken } })
|
|
56
|
-
);
|
|
89
|
+
stringTokens.forEach((strToken) => result.push(createTerm(strToken)));
|
|
57
90
|
} else {
|
|
58
|
-
result.push(t);
|
|
91
|
+
result.push(ensureTokenId(t));
|
|
59
92
|
}
|
|
60
93
|
});
|
|
61
94
|
return result;
|
|
@@ -1,28 +1,77 @@
|
|
|
1
1
|
import { GlSkeletonLoader } from '../../../index';
|
|
2
|
+
import { makeContainer } from '../../../utils/story_decorators/container';
|
|
2
3
|
import readme from './skeleton_loader.md';
|
|
3
4
|
|
|
4
|
-
const
|
|
5
|
+
const defaultValue = (prop) => GlSkeletonLoader.props[prop].default;
|
|
6
|
+
|
|
7
|
+
const generateProps = ({
|
|
8
|
+
width = defaultValue('width'),
|
|
9
|
+
height = defaultValue('height'),
|
|
10
|
+
preserveAspectRatio = defaultValue('preserveAspectRatio'),
|
|
11
|
+
lines = defaultValue('lines'),
|
|
12
|
+
equalWidthLines = defaultValue('equalWidthLines'),
|
|
13
|
+
} = {}) => ({
|
|
14
|
+
width,
|
|
15
|
+
height,
|
|
16
|
+
preserveAspectRatio,
|
|
17
|
+
lines,
|
|
18
|
+
equalWidthLines,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const template = (slotContent = '') => `
|
|
22
|
+
<gl-skeleton-loader
|
|
23
|
+
:width="width"
|
|
24
|
+
:height="height"
|
|
25
|
+
:preserveAspectRatio="preserveAspectRatio"
|
|
26
|
+
:lines="lines"
|
|
27
|
+
:equalWidthLines="equalWidthLines"
|
|
28
|
+
>${slotContent}</gl-skeleton-loader>
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
export const Default = (args) => ({
|
|
5
32
|
components: { GlSkeletonLoader },
|
|
6
33
|
props: Object.keys(args),
|
|
7
|
-
template:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
<rect y="86" width="38" height="16" rx="4" />
|
|
19
|
-
</gl-skeleton-loader>
|
|
20
|
-
</div>
|
|
21
|
-
</div>`,
|
|
34
|
+
template: template(),
|
|
35
|
+
});
|
|
36
|
+
Object.assign(Default, {
|
|
37
|
+
args: generateProps(),
|
|
38
|
+
parameters: {
|
|
39
|
+
controls: {
|
|
40
|
+
// When not providing custom shapes in the default slot, these are the
|
|
41
|
+
// only props you're likely to want to modify.
|
|
42
|
+
include: ['width', 'lines', 'equalWidthLines'],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
22
45
|
});
|
|
23
46
|
|
|
24
|
-
export const
|
|
25
|
-
|
|
47
|
+
export const WithCustomShapes = (args) => ({
|
|
48
|
+
components: { GlSkeletonLoader },
|
|
49
|
+
props: Object.keys(args),
|
|
50
|
+
template: template(`
|
|
51
|
+
<rect width="276" height="16" rx="4" />
|
|
52
|
+
<rect y="18" width="237" height="16" rx="4" />
|
|
53
|
+
<rect y="42" width="118" height="16" rx="8" />
|
|
54
|
+
<rect x="122" y="42" width="130" height="16" rx="8" />
|
|
55
|
+
<rect y="62" width="106" height="16" rx="8" />
|
|
56
|
+
<rect x="110" y="62" width="56" height="16" rx="8" />
|
|
57
|
+
<rect x="256" y="42" width="71" height="16" rx="8" />
|
|
58
|
+
<rect y="86" width="38" height="16" rx="4" />
|
|
59
|
+
`),
|
|
60
|
+
});
|
|
61
|
+
Object.assign(WithCustomShapes, {
|
|
62
|
+
args: generateProps({
|
|
63
|
+
width: 327,
|
|
64
|
+
height: 102,
|
|
65
|
+
}),
|
|
66
|
+
parameters: {
|
|
67
|
+
controls: {
|
|
68
|
+
// With custom shapes, other props become useful, although they're a bit
|
|
69
|
+
// counterintuitive.
|
|
70
|
+
include: ['width', 'height', 'preserveAspectRatio'],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
decorators: [makeContainer({ width: '250px' })],
|
|
74
|
+
});
|
|
26
75
|
|
|
27
76
|
export default {
|
|
28
77
|
title: 'base/skeleton-loader',
|
|
@@ -34,8 +83,5 @@ export default {
|
|
|
34
83
|
component: readme,
|
|
35
84
|
},
|
|
36
85
|
},
|
|
37
|
-
controls: {
|
|
38
|
-
disable: true,
|
|
39
|
-
},
|
|
40
86
|
},
|
|
41
87
|
};
|