@gitlab/ui 66.13.1 → 66.15.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 +14 -0
- package/dist/components/base/filtered_search/filtered_search_token.js +31 -21
- package/dist/components/base/filtered_search/filtered_search_token_segment.js +3 -2
- package/dist/components/experimental/experiment_badge/experiment_badge.js +83 -0
- package/dist/index.js +1 -0
- package/dist/tokens/css/tokens.css +1 -1
- package/dist/tokens/css/tokens.dark.css +1 -1
- package/dist/tokens/js/tokens.dark.js +1 -1
- package/dist/tokens/js/tokens.js +1 -1
- package/dist/tokens/scss/_tokens.dark.scss +1 -1
- package/dist/tokens/scss/_tokens.scss +1 -1
- package/package.json +2 -2
- package/src/components/base/filtered_search/filtered_search.stories.js +11 -4
- package/src/components/base/filtered_search/filtered_search_token.spec.js +25 -7
- package/src/components/base/filtered_search/filtered_search_token.vue +29 -18
- package/src/components/base/filtered_search/filtered_search_token_segment.spec.js +1 -1
- package/src/components/base/filtered_search/filtered_search_token_segment.vue +4 -2
- package/src/components/experimental/experiment_badge/experiment_badge.md +9 -0
- package/src/components/experimental/experiment_badge/experiment_badge.spec.js +66 -0
- package/src/components/experimental/experiment_badge/experiment_badge.stories.js +56 -0
- package/src/components/experimental/experiment_badge/experiment_badge.vue +84 -0
- package/src/index.js +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [66.15.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v66.14.0...v66.15.0) (2023-09-26)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **GLFilteredSearch:** Allow commas in multiSelect values ([160781a](https://gitlab.com/gitlab-org/gitlab-ui/commit/160781ad70105069163bc56631fb333a75764163))
|
|
7
|
+
|
|
8
|
+
# [66.14.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v66.13.1...v66.14.0) (2023-09-25)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **GlExperimentBadge:** Implement component ([de360ee](https://gitlab.com/gitlab-org/gitlab-ui/commit/de360eec00fb772cb3c10258f246975832d68f34))
|
|
14
|
+
|
|
1
15
|
## [66.13.1](https://gitlab.com/gitlab-org/gitlab-ui/compare/v66.13.0...v66.13.1) (2023-09-25)
|
|
2
16
|
|
|
3
17
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import cloneDeep from 'lodash/cloneDeep';
|
|
2
|
-
import
|
|
2
|
+
import isEqual from 'lodash/isEqual';
|
|
3
3
|
import GlToken from '../token/token';
|
|
4
4
|
import GlFilteredSearchTokenSegment from './filtered_search_token_segment';
|
|
5
5
|
import { tokenToOption, createTerm, TOKEN_CLOSE_SELECTOR } from './filtered_search_utils';
|
|
@@ -99,8 +99,13 @@ var script = {
|
|
|
99
99
|
operators() {
|
|
100
100
|
return this.config.operators || DEFAULT_OPERATORS;
|
|
101
101
|
},
|
|
102
|
+
tokenEmpty() {
|
|
103
|
+
var _this$tokenValue$data;
|
|
104
|
+
return ((_this$tokenValue$data = this.tokenValue.data) === null || _this$tokenValue$data === void 0 ? void 0 : _this$tokenValue$data.length) === 0;
|
|
105
|
+
},
|
|
102
106
|
hasDataOrDataSegmentIsCurrentlyActive() {
|
|
103
|
-
|
|
107
|
+
const hasData = !this.tokenEmpty;
|
|
108
|
+
return hasData || this.isSegmentActive(SEGMENT_DATA);
|
|
104
109
|
},
|
|
105
110
|
availableTokensWithSelf() {
|
|
106
111
|
return [this.config, ...this.availableTokens.filter(token => token !== this.config)].map(tokenToOption);
|
|
@@ -135,7 +140,7 @@ var script = {
|
|
|
135
140
|
},
|
|
136
141
|
value: {
|
|
137
142
|
handler(newValue, oldValue) {
|
|
138
|
-
if ((newValue === null || newValue === void 0 ? void 0 : newValue.data
|
|
143
|
+
if (isEqual(newValue === null || newValue === void 0 ? void 0 : newValue.data, oldValue === null || oldValue === void 0 ? void 0 : oldValue.data) && (newValue === null || newValue === void 0 ? void 0 : newValue.operator) === (oldValue === null || oldValue === void 0 ? void 0 : oldValue.operator)) {
|
|
139
144
|
return;
|
|
140
145
|
}
|
|
141
146
|
this.tokenValue = cloneDeep(newValue);
|
|
@@ -143,20 +148,31 @@ var script = {
|
|
|
143
148
|
},
|
|
144
149
|
active: {
|
|
145
150
|
immediate: true,
|
|
146
|
-
handler(
|
|
147
|
-
if (
|
|
151
|
+
handler(tokenIsActive) {
|
|
152
|
+
if (tokenIsActive) {
|
|
148
153
|
this.intendedCursorPosition = this.cursorPosition;
|
|
149
154
|
if (!this.activeSegment) {
|
|
150
|
-
this.activateSegment(this.
|
|
155
|
+
this.activateSegment(this.tokenEmpty ? SEGMENT_OPERATOR : SEGMENT_DATA);
|
|
151
156
|
}
|
|
152
|
-
} else
|
|
157
|
+
} else {
|
|
153
158
|
this.activeSegment = null;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
|
|
160
|
+
// restore multi select values if we have them
|
|
161
|
+
// otherwise destroy the token
|
|
162
|
+
if (this.config.multiSelect) {
|
|
163
|
+
this.$emit('input', {
|
|
164
|
+
...this.tokenValue,
|
|
165
|
+
data: this.multiSelectValues || ''
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (this.tokenEmpty && this.multiSelectValues.length === 0) {
|
|
169
|
+
/**
|
|
170
|
+
* Emitted when token is about to be destroyed.
|
|
171
|
+
*
|
|
172
|
+
* @event destroy
|
|
173
|
+
*/
|
|
174
|
+
this.$emit('destroy');
|
|
175
|
+
}
|
|
160
176
|
}
|
|
161
177
|
}
|
|
162
178
|
}
|
|
@@ -203,7 +219,7 @@ var script = {
|
|
|
203
219
|
return this.active && this.activeSegment === segment;
|
|
204
220
|
},
|
|
205
221
|
replaceWithTermIfEmpty() {
|
|
206
|
-
if (this.tokenValue.operator === '' && this.
|
|
222
|
+
if (this.tokenValue.operator === '' && this.tokenEmpty) {
|
|
207
223
|
/**
|
|
208
224
|
* Emitted when this token is converted to another type
|
|
209
225
|
* @property {object} token Replacement token configuration
|
|
@@ -259,7 +275,7 @@ var script = {
|
|
|
259
275
|
} = _ref3;
|
|
260
276
|
return value.startsWith(potentialValue);
|
|
261
277
|
})) {
|
|
262
|
-
if (this.
|
|
278
|
+
if (this.tokenEmpty) {
|
|
263
279
|
applySuggestion(suggestedValue);
|
|
264
280
|
} else {
|
|
265
281
|
evt.preventDefault();
|
|
@@ -292,12 +308,6 @@ var script = {
|
|
|
292
308
|
this.intendedCursorPosition = 'start';
|
|
293
309
|
},
|
|
294
310
|
handleComplete() {
|
|
295
|
-
if (this.config.multiSelect) {
|
|
296
|
-
this.$emit('input', {
|
|
297
|
-
...this.tokenValue,
|
|
298
|
-
data: this.multiSelectValues.join(COMMA)
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
311
|
/**
|
|
302
312
|
* Emitted when the token entry has been completed.
|
|
303
313
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import last from 'lodash/last';
|
|
2
2
|
import { Portal } from 'portal-vue';
|
|
3
|
-
import {
|
|
3
|
+
import { LEFT_MOUSE_BUTTON } from '../../../utils/constants';
|
|
4
4
|
import GlFilteredSearchSuggestion from './filtered_search_suggestion';
|
|
5
5
|
import GlFilteredSearchSuggestionList from './filtered_search_suggestion_list';
|
|
6
6
|
import { TERM_TOKEN_TYPE, splitOnQuotes, match, wrapTokenInQuotes } from './filtered_search_utils';
|
|
@@ -151,7 +151,7 @@ var script = {
|
|
|
151
151
|
return (_this$options = this.options) === null || _this$options === void 0 ? void 0 : _this$options.find(o => o.value === this.value);
|
|
152
152
|
},
|
|
153
153
|
nonMultipleValue() {
|
|
154
|
-
return this.
|
|
154
|
+
return Array.isArray(this.value) ? last(this.value) : this.value;
|
|
155
155
|
},
|
|
156
156
|
inputValue: {
|
|
157
157
|
get() {
|
|
@@ -210,6 +210,7 @@ var script = {
|
|
|
210
210
|
},
|
|
211
211
|
inputValue(newValue) {
|
|
212
212
|
if (this.termsAsTokens()) return;
|
|
213
|
+
if (this.multiSelect) return;
|
|
213
214
|
const hasUnclosedQuote = newValue.split('"').length % 2 === 0;
|
|
214
215
|
if (newValue.indexOf(' ') === -1 || hasUnclosedQuote) {
|
|
215
216
|
return;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import uniqueId from 'lodash/uniqueId';
|
|
2
|
+
import { GlBadge, GlPopover, GlLink } from '../../../index';
|
|
3
|
+
import GlSprintf from '../../utilities/sprintf/sprintf';
|
|
4
|
+
import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
|
|
5
|
+
|
|
6
|
+
const i18n = {
|
|
7
|
+
EXPERIMENT_BADGE: 'Experiment',
|
|
8
|
+
EXPERIMENT_POPOVER_TITLE: "What's an Experiment?",
|
|
9
|
+
EXPERIMENT_POPOVER_CONTENT: "An %{linkStart}Experiment%{linkEnd} is a feature that's in the process of being developed. It's not production-ready. We encourage users to try Experimental features and provide feedback. An Experiment: %{bullets}",
|
|
10
|
+
EXPERIMENT_POPOVER_BULLETS: ['May be unstable', 'Has no support and might not be documented', 'Can be removed at any time']
|
|
11
|
+
};
|
|
12
|
+
var script = {
|
|
13
|
+
name: 'GlExperimentBadge',
|
|
14
|
+
i18n,
|
|
15
|
+
components: {
|
|
16
|
+
GlBadge,
|
|
17
|
+
GlPopover,
|
|
18
|
+
GlSprintf,
|
|
19
|
+
GlLink
|
|
20
|
+
},
|
|
21
|
+
props: {
|
|
22
|
+
/**
|
|
23
|
+
* The URL of a page to provide more explanations on the experiment.
|
|
24
|
+
*/
|
|
25
|
+
experimentHelpPageUrl: {
|
|
26
|
+
type: String,
|
|
27
|
+
required: false,
|
|
28
|
+
default: ''
|
|
29
|
+
},
|
|
30
|
+
/**
|
|
31
|
+
* The placement of the popover in relation to the button.
|
|
32
|
+
*/
|
|
33
|
+
popoverPlacement: {
|
|
34
|
+
type: String,
|
|
35
|
+
required: false,
|
|
36
|
+
default: 'bottom'
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
created() {
|
|
40
|
+
this.triggerId = uniqueId('experiment-badge-');
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/* script */
|
|
45
|
+
const __vue_script__ = script;
|
|
46
|
+
|
|
47
|
+
/* template */
|
|
48
|
+
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('gl-badge',{staticClass:"gl-mx-4 gl-hover-cursor-pointer",attrs:{"id":_vm.triggerId,"variant":"neutral","size":"md"}},[_c('span',[_vm._v(_vm._s(_vm.$options.i18n.EXPERIMENT_BADGE))]),_vm._v(" "),_c('gl-popover',{attrs:{"triggers":"click","show-close-button":"","placement":_vm.popoverPlacement,"target":_vm.triggerId,"css-classes":['gl-z-index-9999!'],"title":_vm.$options.i18n.EXPERIMENT_POPOVER_TITLE}},[_c('gl-sprintf',{attrs:{"message":_vm.$options.i18n.EXPERIMENT_POPOVER_CONTENT},scopedSlots:_vm._u([{key:"link",fn:function(ref){
|
|
49
|
+
var content = ref.content;
|
|
50
|
+
return [(_vm.experimentHelpPageUrl)?_c('gl-link',{staticClass:"gl-font-sm!",attrs:{"href":_vm.experimentHelpPageUrl,"target":"_blank"}},[_vm._v("\n "+_vm._s(content)+"\n ")]):_c('span',[_vm._v(_vm._s(content))])]}},{key:"bullets",fn:function(){return [_c('ul',{staticClass:"gl-mb-0 gl-pl-5"},_vm._l((_vm.$options.i18n.EXPERIMENT_POPOVER_BULLETS),function(item,i){return _c('li',{key:("li-" + i)},[_vm._v("\n "+_vm._s(item)+"\n ")])}),0)]},proxy:true}])})],1)],1)};
|
|
51
|
+
var __vue_staticRenderFns__ = [];
|
|
52
|
+
|
|
53
|
+
/* style */
|
|
54
|
+
const __vue_inject_styles__ = undefined;
|
|
55
|
+
/* scoped */
|
|
56
|
+
const __vue_scope_id__ = undefined;
|
|
57
|
+
/* module identifier */
|
|
58
|
+
const __vue_module_identifier__ = undefined;
|
|
59
|
+
/* functional template */
|
|
60
|
+
const __vue_is_functional_template__ = false;
|
|
61
|
+
/* style inject */
|
|
62
|
+
|
|
63
|
+
/* style inject SSR */
|
|
64
|
+
|
|
65
|
+
/* style inject shadow dom */
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
const __vue_component__ = __vue_normalize__(
|
|
70
|
+
{ render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
|
|
71
|
+
__vue_inject_styles__,
|
|
72
|
+
__vue_script__,
|
|
73
|
+
__vue_scope_id__,
|
|
74
|
+
__vue_is_functional_template__,
|
|
75
|
+
__vue_module_identifier__,
|
|
76
|
+
false,
|
|
77
|
+
undefined,
|
|
78
|
+
undefined,
|
|
79
|
+
undefined
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
export default __vue_component__;
|
|
83
|
+
export { i18n };
|
package/dist/index.js
CHANGED
|
@@ -86,6 +86,7 @@ export { default as GlAccordion } from './components/base/accordion/accordion';
|
|
|
86
86
|
export { default as GlAccordionItem } from './components/base/accordion/accordion_item';
|
|
87
87
|
export { default as GlCarousel } from './components/base/carousel/carousel';
|
|
88
88
|
export { default as GlCarouselSlide } from './components/base/carousel/carousel_slide';
|
|
89
|
+
export { default as GlExperimentBadge } from './components/experimental/experiment_badge/experiment_badge';
|
|
89
90
|
export { default as GlAnimatedNumber } from './components/utilities/animated_number/animated_number';
|
|
90
91
|
export { default as GlFriendlyWrap } from './components/utilities/friendly_wrap/friendly_wrap';
|
|
91
92
|
export { default as GlIntersperse } from './components/utilities/intersperse/intersperse';
|
package/dist/tokens/js/tokens.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitlab/ui",
|
|
3
|
-
"version": "66.
|
|
3
|
+
"version": "66.15.0",
|
|
4
4
|
"description": "GitLab UI Components",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"cypress-axe": "^1.4.0",
|
|
127
127
|
"dompurify": "^3.0.0",
|
|
128
128
|
"emoji-regex": "^10.0.0",
|
|
129
|
-
"eslint": "8.
|
|
129
|
+
"eslint": "8.50.0",
|
|
130
130
|
"eslint-import-resolver-jest": "3.0.2",
|
|
131
131
|
"eslint-plugin-cypress": "2.15.1",
|
|
132
132
|
"eslint-plugin-storybook": "0.6.14",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import last from 'lodash/last';
|
|
1
2
|
import GlLoadingIcon from '../loading_icon/loading_icon.vue';
|
|
2
3
|
import GlIcon from '../icon/icon.vue';
|
|
3
4
|
import GlToken from '../token/token.vue';
|
|
@@ -525,13 +526,19 @@ export const WithMultiSelect = () => {
|
|
|
525
526
|
data() {
|
|
526
527
|
return {
|
|
527
528
|
users: fakeUsers,
|
|
528
|
-
selectedUsernames: this.value.data
|
|
529
|
+
selectedUsernames: this.value.data || [],
|
|
529
530
|
activeUser: null,
|
|
530
531
|
};
|
|
531
532
|
},
|
|
532
533
|
computed: {
|
|
533
534
|
filteredUsers() {
|
|
534
|
-
|
|
535
|
+
let term = this.value.data;
|
|
536
|
+
|
|
537
|
+
if (Array.isArray(this.value.data) && this.value.data.length > 1) {
|
|
538
|
+
term = last(this.value.data);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return this.users.filter((user) => user.username.includes(term));
|
|
535
542
|
},
|
|
536
543
|
selectedUsers() {
|
|
537
544
|
return this.config.multiSelect
|
|
@@ -593,7 +600,7 @@ export const WithMultiSelect = () => {
|
|
|
593
600
|
<template v-for="(user, index) in selectedUsers">
|
|
594
601
|
<gl-avatar :size="16" :entity-name="user.username" shape="circle" />
|
|
595
602
|
{{ user.name }}
|
|
596
|
-
<span v-if="!isLastUser(index)"
|
|
603
|
+
<span v-if="!isLastUser(index)">, </span>
|
|
597
604
|
</template>
|
|
598
605
|
</template>
|
|
599
606
|
<template #suggestions>
|
|
@@ -635,7 +642,7 @@ export const WithMultiSelect = () => {
|
|
|
635
642
|
multiSelect: true,
|
|
636
643
|
},
|
|
637
644
|
],
|
|
638
|
-
value: [{ type: 'assignee', value: { data: 'alpha,beta', operator: '=' } }],
|
|
645
|
+
value: [{ type: 'assignee', value: { data: ['alpha', 'beta'], operator: '=' } }],
|
|
639
646
|
};
|
|
640
647
|
},
|
|
641
648
|
template: `
|
|
@@ -334,14 +334,32 @@ describe('Filtered search token', () => {
|
|
|
334
334
|
active: true,
|
|
335
335
|
config: { multiSelect: true },
|
|
336
336
|
multiSelectValues: ['alpha', 'beta'],
|
|
337
|
-
value: { operator: '=', data: 'alpha' },
|
|
337
|
+
value: { operator: '=', data: ['alpha', 'beta'] },
|
|
338
338
|
});
|
|
339
339
|
});
|
|
340
340
|
|
|
341
|
-
it('emits input event when
|
|
342
|
-
|
|
341
|
+
it('emits input event when active is false', async () => {
|
|
342
|
+
wrapper.setProps({ value: { data: 'user', operator: '=' } });
|
|
343
|
+
wrapper.setProps({ active: false });
|
|
343
344
|
|
|
344
|
-
|
|
345
|
+
await nextTick();
|
|
346
|
+
|
|
347
|
+
expect(wrapper.emitted('input')).toEqual([
|
|
348
|
+
[{ data: 'user', operator: '=' }],
|
|
349
|
+
[{ data: ['alpha', 'beta'], operator: '=' }],
|
|
350
|
+
]);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('emits input event when active is false and search term empty', async () => {
|
|
354
|
+
wrapper.setProps({ value: { data: '', operator: '=' } });
|
|
355
|
+
wrapper.setProps({ active: false });
|
|
356
|
+
|
|
357
|
+
await nextTick();
|
|
358
|
+
|
|
359
|
+
expect(wrapper.emitted('input')).toEqual([
|
|
360
|
+
[{ data: '', operator: '=' }],
|
|
361
|
+
[{ data: ['alpha', 'beta'], operator: '=' }],
|
|
362
|
+
]);
|
|
345
363
|
});
|
|
346
364
|
|
|
347
365
|
it('emits empty input event when data segment is activated, so blank text input shows all suggestions', () => {
|
|
@@ -352,14 +370,14 @@ describe('Filtered search token', () => {
|
|
|
352
370
|
|
|
353
371
|
it('passes down the value prop to the data segment if it changes', async () => {
|
|
354
372
|
createComponent({
|
|
355
|
-
value: { operator: '=', data: 'alpha' },
|
|
373
|
+
value: { operator: '=', data: ['alpha'] },
|
|
356
374
|
});
|
|
357
375
|
|
|
358
376
|
await wrapper.setProps({
|
|
359
|
-
value: { operator: '=', data: 'gamma' },
|
|
377
|
+
value: { operator: '=', data: ['gamma'] },
|
|
360
378
|
});
|
|
361
379
|
|
|
362
|
-
expect(findDataSegment().props('value')).toEqual('gamma');
|
|
380
|
+
expect(findDataSegment().props('value')).toEqual(['gamma']);
|
|
363
381
|
});
|
|
364
382
|
});
|
|
365
383
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import cloneDeep from 'lodash/cloneDeep';
|
|
3
|
-
import
|
|
3
|
+
import isEqual from 'lodash/isEqual';
|
|
4
4
|
import GlToken from '../token/token.vue';
|
|
5
5
|
import GlFilteredSearchTokenSegment from './filtered_search_token_segment.vue';
|
|
6
6
|
import { createTerm, tokenToOption, TOKEN_CLOSE_SELECTOR } from './filtered_search_utils';
|
|
@@ -97,8 +97,13 @@ export default {
|
|
|
97
97
|
return this.config.operators || DEFAULT_OPERATORS;
|
|
98
98
|
},
|
|
99
99
|
|
|
100
|
+
tokenEmpty() {
|
|
101
|
+
return this.tokenValue.data?.length === 0;
|
|
102
|
+
},
|
|
103
|
+
|
|
100
104
|
hasDataOrDataSegmentIsCurrentlyActive() {
|
|
101
|
-
|
|
105
|
+
const hasData = !this.tokenEmpty;
|
|
106
|
+
return hasData || this.isSegmentActive(SEGMENT_DATA);
|
|
102
107
|
},
|
|
103
108
|
|
|
104
109
|
availableTokensWithSelf() {
|
|
@@ -137,7 +142,7 @@ export default {
|
|
|
137
142
|
|
|
138
143
|
value: {
|
|
139
144
|
handler(newValue, oldValue) {
|
|
140
|
-
if (newValue?.data
|
|
145
|
+
if (isEqual(newValue?.data, oldValue?.data) && newValue?.operator === oldValue?.operator) {
|
|
141
146
|
return;
|
|
142
147
|
}
|
|
143
148
|
|
|
@@ -147,20 +152,29 @@ export default {
|
|
|
147
152
|
|
|
148
153
|
active: {
|
|
149
154
|
immediate: true,
|
|
150
|
-
handler(
|
|
151
|
-
if (
|
|
155
|
+
handler(tokenIsActive) {
|
|
156
|
+
if (tokenIsActive) {
|
|
152
157
|
this.intendedCursorPosition = this.cursorPosition;
|
|
153
158
|
if (!this.activeSegment) {
|
|
154
|
-
this.activateSegment(this.
|
|
159
|
+
this.activateSegment(this.tokenEmpty ? SEGMENT_OPERATOR : SEGMENT_DATA);
|
|
155
160
|
}
|
|
156
|
-
} else
|
|
161
|
+
} else {
|
|
157
162
|
this.activeSegment = null;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
|
|
164
|
+
// restore multi select values if we have them
|
|
165
|
+
// otherwise destroy the token
|
|
166
|
+
if (this.config.multiSelect) {
|
|
167
|
+
this.$emit('input', { ...this.tokenValue, data: this.multiSelectValues || '' });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (this.tokenEmpty && this.multiSelectValues.length === 0) {
|
|
171
|
+
/**
|
|
172
|
+
* Emitted when token is about to be destroyed.
|
|
173
|
+
*
|
|
174
|
+
* @event destroy
|
|
175
|
+
*/
|
|
176
|
+
this.$emit('destroy');
|
|
177
|
+
}
|
|
164
178
|
}
|
|
165
179
|
},
|
|
166
180
|
},
|
|
@@ -206,7 +220,7 @@ export default {
|
|
|
206
220
|
},
|
|
207
221
|
|
|
208
222
|
replaceWithTermIfEmpty() {
|
|
209
|
-
if (this.tokenValue.operator === '' && this.
|
|
223
|
+
if (this.tokenValue.operator === '' && this.tokenEmpty) {
|
|
210
224
|
/**
|
|
211
225
|
* Emitted when this token is converted to another type
|
|
212
226
|
* @property {object} token Replacement token configuration
|
|
@@ -252,7 +266,7 @@ export default {
|
|
|
252
266
|
key.length === 1 &&
|
|
253
267
|
!this.operators.find(({ value }) => value.startsWith(potentialValue))
|
|
254
268
|
) {
|
|
255
|
-
if (this.
|
|
269
|
+
if (this.tokenEmpty) {
|
|
256
270
|
applySuggestion(suggestedValue);
|
|
257
271
|
} else {
|
|
258
272
|
evt.preventDefault();
|
|
@@ -288,9 +302,6 @@ export default {
|
|
|
288
302
|
},
|
|
289
303
|
|
|
290
304
|
handleComplete() {
|
|
291
|
-
if (this.config.multiSelect) {
|
|
292
|
-
this.$emit('input', { ...this.tokenValue, data: this.multiSelectValues.join(COMMA) });
|
|
293
|
-
}
|
|
294
305
|
/**
|
|
295
306
|
* Emitted when the token entry has been completed.
|
|
296
307
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import last from 'lodash/last';
|
|
3
3
|
import { Portal } from 'portal-vue';
|
|
4
|
-
import {
|
|
4
|
+
import { 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
7
|
import { splitOnQuotes, wrapTokenInQuotes, match, TERM_TOKEN_TYPE } from './filtered_search_utils';
|
|
@@ -152,7 +152,7 @@ export default {
|
|
|
152
152
|
},
|
|
153
153
|
|
|
154
154
|
nonMultipleValue() {
|
|
155
|
-
return this.
|
|
155
|
+
return Array.isArray(this.value) ? last(this.value) : this.value;
|
|
156
156
|
},
|
|
157
157
|
|
|
158
158
|
inputValue: {
|
|
@@ -228,6 +228,8 @@ export default {
|
|
|
228
228
|
inputValue(newValue) {
|
|
229
229
|
if (this.termsAsTokens()) return;
|
|
230
230
|
|
|
231
|
+
if (this.multiSelect) return;
|
|
232
|
+
|
|
231
233
|
const hasUnclosedQuote = newValue.split('"').length % 2 === 0;
|
|
232
234
|
if (newValue.indexOf(' ') === -1 || hasUnclosedQuote) {
|
|
233
235
|
return;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The component is representing a badge, marking the experimental features.
|
|
2
|
+
It is supposed to be used with the AI experiments, and comes with a popover explaining
|
|
3
|
+
what experiment means.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```html
|
|
8
|
+
<gl-experiment-badge experiment-help-page-url="https://gitlab.com" popover-placement="bottom" />
|
|
9
|
+
```
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { shallowMount } from '@vue/test-utils';
|
|
2
|
+
import GlBadge from '../../base/badge/badge.vue';
|
|
3
|
+
import GlPopover from '../../base/popover/popover.vue';
|
|
4
|
+
import GlLink from '../../base/link/link.vue';
|
|
5
|
+
import GlSprintf from '../../utilities/sprintf/sprintf.vue';
|
|
6
|
+
import GlExperimentBadge, { i18n } from './experiment_badge.vue';
|
|
7
|
+
|
|
8
|
+
jest.mock('lodash/uniqueId', () => () => 'fakeUniqueId');
|
|
9
|
+
|
|
10
|
+
describe('GlExperimentBadge', () => {
|
|
11
|
+
let wrapper;
|
|
12
|
+
|
|
13
|
+
const findBadge = () => wrapper.findComponent(GlBadge);
|
|
14
|
+
const findPopover = () => wrapper.findComponent(GlPopover);
|
|
15
|
+
const findHelpLink = () => wrapper.findComponent(GlLink);
|
|
16
|
+
|
|
17
|
+
const createComponent = (props = {}) => {
|
|
18
|
+
wrapper = shallowMount(GlExperimentBadge, {
|
|
19
|
+
propsData: {
|
|
20
|
+
...props,
|
|
21
|
+
},
|
|
22
|
+
stubs: {
|
|
23
|
+
GlSprintf,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
createComponent();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('renders main components', () => {
|
|
33
|
+
expect(findBadge().exists()).toBe(true);
|
|
34
|
+
expect(findPopover().exists()).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('sets correct props on the badge', () => {
|
|
38
|
+
const badgeType = 'neutral';
|
|
39
|
+
const badgeSize = 'md';
|
|
40
|
+
expect(findBadge().props('variant')).toBe(badgeType);
|
|
41
|
+
expect(findBadge().props('size')).toBe(badgeSize);
|
|
42
|
+
expect(findBadge().find('span').text()).toBe(i18n.EXPERIMENT_BADGE);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('sets correct props on the popover', () => {
|
|
46
|
+
expect(findPopover().props('triggers')).toBe('click');
|
|
47
|
+
expect(findPopover().props('title')).toBe(i18n.EXPERIMENT_POPOVER_TITLE);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('correctly sets the placement of the popover', () => {
|
|
51
|
+
const popoverPlacement = 'right';
|
|
52
|
+
createComponent({ popoverPlacement });
|
|
53
|
+
expect(findPopover().props('placement')).toBe(popoverPlacement);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('sets the link to the help page if passed', () => {
|
|
57
|
+
const experimentHelpPageUrl = 'https://gitlab.com';
|
|
58
|
+
createComponent({ experimentHelpPageUrl });
|
|
59
|
+
expect(findHelpLink().attributes('href')).toBe(experimentHelpPageUrl);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('generates the unique ID to connect the button and the popover', () => {
|
|
63
|
+
expect(findBadge().attributes('id')).toBe('fakeUniqueId');
|
|
64
|
+
expect(findPopover().attributes('target')).toBe('fakeUniqueId');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import GlExperimentBadge from './experiment_badge.vue';
|
|
2
|
+
import readme from './experiment_badge.md';
|
|
3
|
+
|
|
4
|
+
const defaultValue = (prop) => GlExperimentBadge.props[prop].default;
|
|
5
|
+
|
|
6
|
+
const generateProps = ({
|
|
7
|
+
experimentHelpPageUrl = defaultValue('experimentHelpPageUrl'),
|
|
8
|
+
popoverPlacement = defaultValue('popoverPlacement'),
|
|
9
|
+
} = {}) => ({
|
|
10
|
+
experimentHelpPageUrl,
|
|
11
|
+
popoverPlacement,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const Template = (args, { argTypes }) => ({
|
|
15
|
+
components: { GlExperimentBadge },
|
|
16
|
+
props: Object.keys(argTypes),
|
|
17
|
+
template: `
|
|
18
|
+
<div class='gl-h-13'>
|
|
19
|
+
<gl-experiment-badge
|
|
20
|
+
:experiment-help-page-url='experimentHelpPageUrl'
|
|
21
|
+
:popover-placement='popoverPlacement' />
|
|
22
|
+
</div>
|
|
23
|
+
`,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const Default = Template.bind({});
|
|
27
|
+
Default.args = generateProps();
|
|
28
|
+
|
|
29
|
+
export const WithHelpPageUrl = Template.bind({});
|
|
30
|
+
WithHelpPageUrl.args = {
|
|
31
|
+
...generateProps({
|
|
32
|
+
experimentHelpPageUrl:
|
|
33
|
+
'https://docs.gitlab.com/ee/policy/experiment-beta-support.html#experiment',
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const CustomPlacement = Template.bind({});
|
|
38
|
+
CustomPlacement.args = {
|
|
39
|
+
...generateProps({
|
|
40
|
+
popoverPlacement: 'right',
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default {
|
|
45
|
+
title: 'experimental/experiment_badge',
|
|
46
|
+
component: GlExperimentBadge,
|
|
47
|
+
parameters: {
|
|
48
|
+
storyshots: { disable: true },
|
|
49
|
+
docs: {
|
|
50
|
+
description: {
|
|
51
|
+
component: readme,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
argTypes: {},
|
|
56
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import uniqueId from 'lodash/uniqueId';
|
|
3
|
+
import { GlBadge, GlLink, GlPopover } from '../../../index';
|
|
4
|
+
import GlSprintf from '../../utilities/sprintf/sprintf.vue';
|
|
5
|
+
|
|
6
|
+
export const i18n = {
|
|
7
|
+
EXPERIMENT_BADGE: 'Experiment',
|
|
8
|
+
EXPERIMENT_POPOVER_TITLE: "What's an Experiment?",
|
|
9
|
+
EXPERIMENT_POPOVER_CONTENT:
|
|
10
|
+
"An %{linkStart}Experiment%{linkEnd} is a feature that's in the process of being developed. It's not production-ready. We encourage users to try Experimental features and provide feedback. An Experiment: %{bullets}",
|
|
11
|
+
EXPERIMENT_POPOVER_BULLETS: [
|
|
12
|
+
'May be unstable',
|
|
13
|
+
'Has no support and might not be documented',
|
|
14
|
+
'Can be removed at any time',
|
|
15
|
+
],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default {
|
|
19
|
+
name: 'GlExperimentBadge',
|
|
20
|
+
i18n,
|
|
21
|
+
components: {
|
|
22
|
+
GlBadge,
|
|
23
|
+
GlPopover,
|
|
24
|
+
GlSprintf,
|
|
25
|
+
GlLink,
|
|
26
|
+
},
|
|
27
|
+
props: {
|
|
28
|
+
/**
|
|
29
|
+
* The URL of a page to provide more explanations on the experiment.
|
|
30
|
+
*/
|
|
31
|
+
experimentHelpPageUrl: {
|
|
32
|
+
type: String,
|
|
33
|
+
required: false,
|
|
34
|
+
default: '',
|
|
35
|
+
},
|
|
36
|
+
/**
|
|
37
|
+
* The placement of the popover in relation to the button.
|
|
38
|
+
*/
|
|
39
|
+
popoverPlacement: {
|
|
40
|
+
type: String,
|
|
41
|
+
required: false,
|
|
42
|
+
default: 'bottom',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
created() {
|
|
46
|
+
this.triggerId = uniqueId('experiment-badge-');
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<gl-badge :id="triggerId" class="gl-mx-4 gl-hover-cursor-pointer" variant="neutral" size="md">
|
|
53
|
+
<span>{{ $options.i18n.EXPERIMENT_BADGE }}</span>
|
|
54
|
+
<gl-popover
|
|
55
|
+
triggers="click"
|
|
56
|
+
show-close-button
|
|
57
|
+
:placement="popoverPlacement"
|
|
58
|
+
:target="triggerId"
|
|
59
|
+
:css-classes="['gl-z-index-9999!']"
|
|
60
|
+
:title="$options.i18n.EXPERIMENT_POPOVER_TITLE"
|
|
61
|
+
>
|
|
62
|
+
<gl-sprintf :message="$options.i18n.EXPERIMENT_POPOVER_CONTENT">
|
|
63
|
+
<template #link="{ content }">
|
|
64
|
+
<gl-link
|
|
65
|
+
v-if="experimentHelpPageUrl"
|
|
66
|
+
:href="experimentHelpPageUrl"
|
|
67
|
+
target="_blank"
|
|
68
|
+
class="gl-font-sm!"
|
|
69
|
+
>
|
|
70
|
+
{{ content }}
|
|
71
|
+
</gl-link>
|
|
72
|
+
<span v-else>{{ content }}</span>
|
|
73
|
+
</template>
|
|
74
|
+
<template #bullets>
|
|
75
|
+
<ul class="gl-mb-0 gl-pl-5">
|
|
76
|
+
<li v-for="(item, i) in $options.i18n.EXPERIMENT_POPOVER_BULLETS" :key="`li-${i}`">
|
|
77
|
+
{{ item }}
|
|
78
|
+
</li>
|
|
79
|
+
</ul>
|
|
80
|
+
</template>
|
|
81
|
+
</gl-sprintf>
|
|
82
|
+
</gl-popover>
|
|
83
|
+
</gl-badge>
|
|
84
|
+
</template>
|
package/src/index.js
CHANGED
|
@@ -96,6 +96,9 @@ export { default as GlAccordionItem } from './components/base/accordion/accordio
|
|
|
96
96
|
export { default as GlCarousel } from './components/base/carousel/carousel.vue';
|
|
97
97
|
export { default as GlCarouselSlide } from './components/base/carousel/carousel_slide.vue';
|
|
98
98
|
|
|
99
|
+
// Experimental
|
|
100
|
+
export { default as GlExperimentBadge } from './components/experimental/experiment_badge/experiment_badge.vue';
|
|
101
|
+
|
|
99
102
|
// Utilities
|
|
100
103
|
export { default as GlAnimatedNumber } from './components/utilities/animated_number/animated_number.vue';
|
|
101
104
|
export { default as GlFriendlyWrap } from './components/utilities/friendly_wrap/friendly_wrap.vue';
|