@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 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 { COMMA } from '../../../utils/constants';
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
- return this.tokenValue.data !== '' || this.isSegmentActive(SEGMENT_DATA);
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) === (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)) {
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(newValue) {
147
- if (newValue) {
151
+ handler(tokenIsActive) {
152
+ if (tokenIsActive) {
148
153
  this.intendedCursorPosition = this.cursorPosition;
149
154
  if (!this.activeSegment) {
150
- this.activateSegment(this.tokenValue.data !== '' ? SEGMENT_DATA : SEGMENT_OPERATOR);
155
+ this.activateSegment(this.tokenEmpty ? SEGMENT_OPERATOR : SEGMENT_DATA);
151
156
  }
152
- } else if (this.tokenValue.data === '') {
157
+ } else {
153
158
  this.activeSegment = null;
154
- /**
155
- * Emitted when token is about to be destroyed.
156
- *
157
- * @event destroy
158
- */
159
- this.$emit('destroy');
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.tokenValue.data === '') {
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.tokenValue.data === '') {
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 { COMMA, LEFT_MOUSE_BUTTON } from '../../../utils/constants';
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.multiSelect ? last(this.value.split(COMMA)) : this.value;
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';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Mon, 25 Sep 2023 19:08:19 GMT
3
+ * Generated on Tue, 26 Sep 2023 11:34:10 GMT
4
4
  */
5
5
 
6
6
  :root {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Mon, 25 Sep 2023 19:08:19 GMT
3
+ * Generated on Tue, 26 Sep 2023 11:34:10 GMT
4
4
  */
5
5
 
6
6
  :root {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Mon, 25 Sep 2023 19:08:19 GMT
3
+ * Generated on Tue, 26 Sep 2023 11:34:10 GMT
4
4
  */
5
5
 
6
6
  export const BLACK = "#fff";
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Mon, 25 Sep 2023 19:08:19 GMT
3
+ * Generated on Tue, 26 Sep 2023 11:34:10 GMT
4
4
  */
5
5
 
6
6
  export const BLACK = "#000";
@@ -1,6 +1,6 @@
1
1
 
2
2
  // Do not edit directly
3
- // Generated on Mon, 25 Sep 2023 19:08:19 GMT
3
+ // Generated on Tue, 26 Sep 2023 11:34:10 GMT
4
4
 
5
5
  $red-950: #fff4f3;
6
6
  $red-900: #fcf1ef;
@@ -1,6 +1,6 @@
1
1
 
2
2
  // Do not edit directly
3
- // Generated on Mon, 25 Sep 2023 19:08:19 GMT
3
+ // Generated on Tue, 26 Sep 2023 11:34:10 GMT
4
4
 
5
5
  $gl-line-height-52: 3.25rem;
6
6
  $gl-line-height-44: 2.75rem;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "66.13.1",
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.49.0",
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 ? this.value.data.split(',') : [],
529
+ selectedUsernames: this.value.data || [],
529
530
  activeUser: null,
530
531
  };
531
532
  },
532
533
  computed: {
533
534
  filteredUsers() {
534
- return this.users.filter((user) => user.username.includes(this.value.data));
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)" class="gl-mx-2">,&nbsp;</span>
603
+ <span v-if="!isLastUser(index)">,&nbsp;</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 data segment is completed', () => {
342
- findDataSegment().vm.$emit('complete');
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
- expect(wrapper.emitted('input')).toEqual([[{ data: 'alpha,beta', operator: '=' }]]);
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 { COMMA } from '../../../utils/constants';
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
- return this.tokenValue.data !== '' || this.isSegmentActive(SEGMENT_DATA);
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 === oldValue?.data && newValue?.operator === oldValue?.operator) {
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(newValue) {
151
- if (newValue) {
155
+ handler(tokenIsActive) {
156
+ if (tokenIsActive) {
152
157
  this.intendedCursorPosition = this.cursorPosition;
153
158
  if (!this.activeSegment) {
154
- this.activateSegment(this.tokenValue.data !== '' ? SEGMENT_DATA : SEGMENT_OPERATOR);
159
+ this.activateSegment(this.tokenEmpty ? SEGMENT_OPERATOR : SEGMENT_DATA);
155
160
  }
156
- } else if (this.tokenValue.data === '') {
161
+ } else {
157
162
  this.activeSegment = null;
158
- /**
159
- * Emitted when token is about to be destroyed.
160
- *
161
- * @event destroy
162
- */
163
- this.$emit('destroy');
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.tokenValue.data === '') {
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.tokenValue.data === '') {
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
  *
@@ -275,7 +275,7 @@ describe('Filtered search token segment', () => {
275
275
  createComponent({
276
276
  active: true,
277
277
  multiSelect: true,
278
- value: 'beta',
278
+ value: ['alpha', 'beta'],
279
279
  });
280
280
  });
281
281
 
@@ -1,7 +1,7 @@
1
1
  <script>
2
2
  import last from 'lodash/last';
3
3
  import { Portal } from 'portal-vue';
4
- import { COMMA, LEFT_MOUSE_BUTTON } from '../../../utils/constants';
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.multiSelect ? last(this.value.split(COMMA)) : this.value;
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';