@gitlab/ui 38.1.0 → 38.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.
Files changed (94) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/components/base/dropdown/dropdown.documentation.js +1 -5
  3. package/dist/components/base/dropdown/dropdown_item.documentation.js +2 -3
  4. package/dist/components/base/filtered_search/filtered_search.js +13 -20
  5. package/dist/components/base/filtered_search/filtered_search_suggestion.js +1 -1
  6. package/dist/components/base/filtered_search/filtered_search_token.js +31 -23
  7. package/dist/components/base/filtered_search/filtered_search_utils.js +42 -9
  8. package/dist/components/base/form/form_checkbox_tree/form_checkbox_tree.documentation.js +2 -27
  9. package/dist/components/base/form/form_checkbox_tree/form_checkbox_tree.js +16 -1
  10. package/dist/index.css +1 -1
  11. package/dist/index.css.map +1 -1
  12. package/dist/utility_classes.css +1 -1
  13. package/dist/utility_classes.css.map +1 -1
  14. package/dist/utils/use_mock_intersection_observer.js +2 -2
  15. package/documentation/components_documentation.js +0 -4
  16. package/documentation/documented_stories.js +4 -1
  17. package/package.json +12 -12
  18. package/src/components/base/avatar_link/avatar_link.stories.js +2 -2
  19. package/src/components/base/dropdown/dropdown.documentation.js +0 -3
  20. package/src/components/base/dropdown/dropdown.md +7 -2
  21. package/src/components/base/dropdown/dropdown.stories.js +487 -439
  22. package/src/components/base/dropdown/dropdown_item.documentation.js +0 -1
  23. package/src/components/base/dropdown/dropdown_item.md +0 -6
  24. package/src/components/base/dropdown/dropdown_item.stories.js +107 -35
  25. package/src/components/base/filtered_search/filtered_search.spec.js +37 -12
  26. package/src/components/base/filtered_search/filtered_search.stories.js +15 -7
  27. package/src/components/base/filtered_search/filtered_search.vue +12 -14
  28. package/src/components/base/filtered_search/filtered_search_suggestion.vue +1 -0
  29. package/src/components/base/filtered_search/filtered_search_token.spec.js +31 -1
  30. package/src/components/base/filtered_search/filtered_search_token.stories.js +1 -0
  31. package/src/components/base/filtered_search/filtered_search_token.vue +30 -21
  32. package/src/components/base/filtered_search/filtered_search_utils.js +38 -5
  33. package/src/components/base/form/form.stories.js +2 -0
  34. package/src/components/base/form/form_checkbox_tree/form_checkbox_tree.documentation.js +0 -26
  35. package/src/components/base/form/form_checkbox_tree/form_checkbox_tree.md +0 -4
  36. package/src/components/base/form/form_checkbox_tree/form_checkbox_tree.stories.js +123 -92
  37. package/src/components/base/form/form_checkbox_tree/form_checkbox_tree.vue +13 -1
  38. package/src/components/base/form/form_radio_group/form_radio_group.stories.js +2 -1
  39. package/src/components/base/markdown/markdown.scss +21 -0
  40. package/src/components/base/markdown/markdown_typescale_demo.html +17 -6
  41. package/src/components/base/navbar/navbar.stories.js +2 -1
  42. package/src/components/base/skeleton_loader/skeleton_loader.stories.js +67 -21
  43. package/src/components/base/tabs/tabs/tabs.stories.js +2 -2
  44. package/src/scss/typescale/typescale.md +0 -2
  45. package/src/scss/typescale/typescale.stories.js +17 -4
  46. package/src/scss/utilities.scss +24 -0
  47. package/src/scss/utility-mixins/display.scss +12 -0
  48. package/src/utils/use_mock_intersection_observer.js +3 -3
  49. package/dist/components/base/dropdown/dropdown_divider.documentation.js +0 -8
  50. package/dist/components/base/dropdown/dropdown_form.documentation.js +0 -17
  51. package/dist/components/base/dropdown/dropdown_section_header.documentation.js +0 -8
  52. package/dist/components/base/dropdown/dropdown_text.documentation.js +0 -8
  53. package/dist/components/base/dropdown/examples/dropdown.default.example.js +0 -38
  54. package/dist/components/base/dropdown/examples/dropdown.links.example.js +0 -38
  55. package/dist/components/base/dropdown/examples/dropdown.with_avatar_and_secondary_text.example.js +0 -38
  56. package/dist/components/base/dropdown/examples/dropdown.with_checked_items.example.js +0 -38
  57. package/dist/components/base/dropdown/examples/dropdown.with_clear_all.example.js +0 -38
  58. package/dist/components/base/dropdown/examples/dropdown.with_divider.example.js +0 -38
  59. package/dist/components/base/dropdown/examples/dropdown.with_form.example.js +0 -38
  60. package/dist/components/base/dropdown/examples/dropdown.with_header.example.js +0 -38
  61. package/dist/components/base/dropdown/examples/dropdown.with_highlighted_items.example.js +0 -38
  62. package/dist/components/base/dropdown/examples/dropdown.with_icons.example.js +0 -38
  63. package/dist/components/base/dropdown/examples/dropdown.with_right_align.example.js +0 -38
  64. package/dist/components/base/dropdown/examples/dropdown.with_search.example.js +0 -67
  65. package/dist/components/base/dropdown/examples/dropdown.with_section_headers.example.js +0 -38
  66. package/dist/components/base/dropdown/examples/index.js +0 -85
  67. package/dist/components/base/form/form_checkbox_tree/examples/form_checkbox_tree.basic.example.js +0 -103
  68. package/dist/components/base/form/form_checkbox_tree/examples/index.js +0 -13
  69. package/src/components/base/dropdown/dropdown_divider.documentation.js +0 -6
  70. package/src/components/base/dropdown/dropdown_divider.md +0 -7
  71. package/src/components/base/dropdown/dropdown_divider.stories.js +0 -16
  72. package/src/components/base/dropdown/dropdown_form.documentation.js +0 -9
  73. package/src/components/base/dropdown/dropdown_form.md +0 -4
  74. package/src/components/base/dropdown/dropdown_form.stories.js +0 -17
  75. package/src/components/base/dropdown/dropdown_section_header.documentation.js +0 -6
  76. package/src/components/base/dropdown/dropdown_section_header.stories.js +0 -17
  77. package/src/components/base/dropdown/dropdown_text.documentation.js +0 -6
  78. package/src/components/base/dropdown/dropdown_text.stories.js +0 -16
  79. package/src/components/base/dropdown/examples/dropdown.default.example.vue +0 -7
  80. package/src/components/base/dropdown/examples/dropdown.links.example.vue +0 -7
  81. package/src/components/base/dropdown/examples/dropdown.with_avatar_and_secondary_text.example.vue +0 -7
  82. package/src/components/base/dropdown/examples/dropdown.with_checked_items.example.vue +0 -6
  83. package/src/components/base/dropdown/examples/dropdown.with_clear_all.example.vue +0 -7
  84. package/src/components/base/dropdown/examples/dropdown.with_divider.example.vue +0 -9
  85. package/src/components/base/dropdown/examples/dropdown.with_form.example.vue +0 -10
  86. package/src/components/base/dropdown/examples/dropdown.with_header.example.vue +0 -7
  87. package/src/components/base/dropdown/examples/dropdown.with_highlighted_items.example.vue +0 -9
  88. package/src/components/base/dropdown/examples/dropdown.with_icons.example.vue +0 -43
  89. package/src/components/base/dropdown/examples/dropdown.with_right_align.example.vue +0 -7
  90. package/src/components/base/dropdown/examples/dropdown.with_search.example.vue +0 -38
  91. package/src/components/base/dropdown/examples/dropdown.with_section_headers.example.vue +0 -10
  92. package/src/components/base/dropdown/examples/index.js +0 -99
  93. package/src/components/base/form/form_checkbox_tree/examples/form_checkbox_tree.basic.example.vue +0 -77
  94. package/src/components/base/form/form_checkbox_tree/examples/index.js +0 -15
@@ -2,5 +2,4 @@ import description from './dropdown_item.md';
2
2
 
3
3
  export default {
4
4
  description,
5
- bootstrapComponent: 'b-dropdown',
6
5
  };
@@ -1,8 +1,2 @@
1
- # Dropdown Item
2
-
3
- <!-- STORY -->
4
-
5
- ## Usage
6
-
7
1
  The dropdown item component is meant to be used for clickable entries inside a dropdown component.
8
2
  If you provide the `href` attribute, it renders a link instead of a button.
@@ -1,42 +1,114 @@
1
- import { withKnobs } from '@storybook/addon-knobs';
2
- import { documentedStoriesOf } from '../../../../documentation/documented_stories';
1
+ import iconSpriteInfo from '@gitlab/svgs/dist/icons.json';
3
2
  import { GlDropdownItem } from '../../../index';
3
+ import { variantCssColorMap } from '../../../utils/constants';
4
4
  import readme from './dropdown_item.md';
5
5
 
6
6
  const components = {
7
7
  GlDropdownItem,
8
8
  };
9
9
 
10
- documentedStoriesOf('base/dropdown/dropdown-item', readme)
11
- .addDecorator(withKnobs)
12
- .add('default', () => ({
13
- props: {},
14
- components,
15
- template: '<ul class="list-unstyled"><gl-dropdown-item>Some item</gl-dropdown-item></ul>',
16
- }))
17
- .add('checked', () => ({
18
- props: {},
19
- components,
20
- template:
21
- '<ul class="list-unstyled"><gl-dropdown-item is-checked is-check-item>Some item</gl-dropdown-item></ul>',
22
- }))
23
- .add('checked with avatar', () => ({
24
- props: {},
25
- components,
26
- template: `<ul class="list-unstyled">
27
- <gl-dropdown-item
28
- is-checked
29
- is-check-item
30
- is-check-centered
31
- avatar-url="./img/avatar.png"
32
- secondary-text="@sytses"
33
- >
34
- Sid Sijbrandij
35
- </gl-dropdown-item></ul>`,
36
- }))
37
- .add('checked with secondary text', () => ({
38
- props: {},
39
- components,
40
- template:
41
- '<ul class="list-unstyled"><gl-dropdown-item is-checked is-check-item secondary-text="Lorem ipsum dolar sit amit...">Some item</gl-dropdown-item></ul>',
42
- }));
10
+ const wrap = (template) => `
11
+ <ul class="gl-list-style-none gl-pl-0">
12
+ <gl-dropdown-item
13
+ :avatar-url="avatarUrl"
14
+ :icon-color="iconColor"
15
+ :icon-name="iconName"
16
+ :icon-right-aria-label="iconRightAriaLabel"
17
+ :icon-right-name="iconRightName"
18
+ :is-checked="isChecked"
19
+ :is-check-item="isCheckItem"
20
+ :is-check-centered="isCheckCentered"
21
+ :secondary-text="secondaryText">
22
+ ${template}
23
+ </gl-dropdown-item>
24
+ </ul>
25
+ `;
26
+
27
+ const defaultValue = (prop) => GlDropdownItem.props[prop].default;
28
+
29
+ const generateProps = ({
30
+ avatarUrl = defaultValue('avatarUrl'),
31
+ iconColor = defaultValue('iconColor'),
32
+ iconName = defaultValue('iconName'),
33
+ iconRightAriaLabel = defaultValue('iconRightAriaLabel'),
34
+ iconRightName = defaultValue('iconRightName'),
35
+ isChecked = defaultValue('isChecked'),
36
+ isCheckItem = defaultValue('isCheckItem'),
37
+ isCheckCentered = defaultValue('isCheckCentered'),
38
+ secondaryText = defaultValue('secondaryText'),
39
+ } = {}) => ({
40
+ avatarUrl,
41
+ iconColor,
42
+ iconName,
43
+ iconRightAriaLabel,
44
+ iconRightName,
45
+ isChecked,
46
+ isCheckItem,
47
+ isCheckCentered,
48
+ secondaryText,
49
+ });
50
+
51
+ export const Default = (args, { argTypes }) => ({
52
+ props: Object.keys(argTypes),
53
+ components,
54
+ template: wrap('Some item'),
55
+ });
56
+ Default.args = generateProps();
57
+
58
+ export const Checked = (args, { argTypes }) => ({
59
+ props: Object.keys(argTypes),
60
+ components,
61
+ template: wrap('Some item'),
62
+ });
63
+ Checked.args = generateProps({ isChecked: true, isCheckItem: true });
64
+
65
+ export const CheckedWithAvatar = (args, { argTypes }) => ({
66
+ props: Object.keys(argTypes),
67
+ components,
68
+ template: wrap('Sid Sijbrandij'),
69
+ });
70
+ CheckedWithAvatar.args = generateProps({
71
+ isChecked: true,
72
+ isCheckItem: true,
73
+ isCheckCentered: true,
74
+ avatarUrl: './img/avatar.png',
75
+ secondaryText: '@sytses',
76
+ });
77
+
78
+ export const CheckedWithSecondaryText = (args, { argTypes }) => ({
79
+ props: Object.keys(argTypes),
80
+ components,
81
+ template: wrap('Some item'),
82
+ });
83
+ CheckedWithSecondaryText.args = generateProps({
84
+ isChecked: true,
85
+ isCheckItem: true,
86
+ secondaryText: 'Lorem ipsum dolar sit amit...',
87
+ });
88
+
89
+ export default {
90
+ title: 'base/dropdown/dropdown-item',
91
+ component: GlDropdownItem,
92
+ parameters: {
93
+ bootstrapComponent: 'b-dropdown-item',
94
+ docs: {
95
+ description: {
96
+ component: readme,
97
+ },
98
+ },
99
+ },
100
+ argTypes: {
101
+ iconColor: {
102
+ options: Object.keys(variantCssColorMap),
103
+ control: {
104
+ type: 'select',
105
+ },
106
+ },
107
+ iconName: {
108
+ options: iconSpriteInfo.icons,
109
+ control: {
110
+ type: 'select',
111
+ },
112
+ },
113
+ },
114
+ };
@@ -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
- }, 1000);
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
- }, 2000);
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
- }, 2000);
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
- }, 2000);
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: 'gl-filtered-search-token',
326
+ token: GlFilteredSearchToken,
326
327
  operators: [{ value: '=', description: 'is', default: 'true' }],
327
328
  options: [
328
329
  { icon: 'heart', title: 'heart', value: 1 },
@@ -374,14 +375,14 @@ export const WithFriendlyText = () => ({
374
375
  icon: 'weight',
375
376
  title: 'Weight',
376
377
  unique: true,
377
- token: 'gl-filtered-search-token',
378
+ token: GlFilteredSearchToken,
378
379
  },
379
380
  {
380
381
  type: 'confidential',
381
382
  icon: 'eye-slash',
382
383
  title: 'Confidential',
383
384
  unique: true,
384
- token: 'gl-filtered-search-token',
385
+ token: GlFilteredSearchToken,
385
386
  options: [
386
387
  { icon: 'eye-slash', value: 'Yes', title: 'Yes' },
387
388
  { icon: 'eye', value: 'No', title: 'No' },
@@ -406,6 +407,13 @@ export const WithFriendlyText = () => ({
406
407
  export const WithMultiSelect = () => {
407
408
  const MultiUserToken = {
408
409
  props: ['value', 'active', 'config'],
410
+ components: {
411
+ GlFilteredSearchToken,
412
+ GlFilteredSearchSuggestion,
413
+ GlLoadingIcon,
414
+ GlIcon,
415
+ GlAvatar,
416
+ },
409
417
  inheritAttrs: false,
410
418
  data() {
411
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="`${token.type}-${idx}`"
341
+ :key="token.id"
344
342
  v-model="token.value"
345
343
  :config="getTokenEntry(token.type)"
346
344
  :active="activeTokenIdx === idx"
@@ -51,6 +51,7 @@ export default {
51
51
  <gl-dropdown-item
52
52
  ref="item"
53
53
  class="gl-filtered-search-suggestion"
54
+ data-testid="filtered-search-suggestion"
54
55
  :class="{ 'gl-filtered-search-suggestion-active': isActive }"
55
56
  v-bind="$attrs"
56
57
  href="#"
@@ -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
 
@@ -247,6 +255,16 @@ describe('Filtered search token', () => {
247
255
  expect(wrapper.emitted().input[0][0].operator).toBe('=');
248
256
  expect(findDataSegment().props().active).toBe(true);
249
257
  });
258
+
259
+ it('does not mutate its value prop', async () => {
260
+ const originalValue = () => ({ operator: '', data: '' });
261
+ const value = observable(originalValue());
262
+ mountComponent({ active: true, value });
263
+
264
+ await wrapper.find('input').trigger('keydown', { key: 'q' });
265
+
266
+ expect(value).toEqual(originalValue());
267
+ });
250
268
  });
251
269
 
252
270
  describe('when multi select', () => {
@@ -270,5 +288,17 @@ describe('Filtered search token', () => {
270
288
 
271
289
  expect(wrapper.emitted('input')).toEqual([[{ data: '', operator: '=' }]]);
272
290
  });
291
+
292
+ it('passes down the value prop to the data segment if it changes', async () => {
293
+ createComponent({
294
+ value: { operator: '=', data: 'alpha' },
295
+ });
296
+
297
+ await wrapper.setProps({
298
+ value: { operator: '=', data: 'gamma' },
299
+ });
300
+
301
+ expect(findDataSegment().props('value')).toEqual('gamma');
302
+ });
273
303
  });
274
304
  });
@@ -63,6 +63,7 @@ export const WithCustomOperatorsOptions = (args, { argTypes }) => ({
63
63
  components: {
64
64
  GlFilteredSearchToken,
65
65
  GlFilteredSearchSuggestion,
66
+ GlIcon,
66
67
  },
67
68
  provide,
68
69
  props: ['active'],