@gitlab/ui 66.14.0 → 66.16.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "66.14.0",
3
+ "version": "66.16.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;
@@ -123,9 +123,14 @@
123
123
  }
124
124
  }
125
125
 
126
- // Helper functions to aid maintenance of heading styles
127
126
 
128
- // Because we can't interpolate into a variable name we have to do some stuff
127
+ /**
128
+ * Helper function to resolve font-size value from $gl-font-sizes and
129
+ * $gl-font-sizes-fixed maps.
130
+ *
131
+ * @param $size Number font-size scale
132
+ * @param $fixed Boolean toggle default and fixed font size scales
133
+ */
129
134
  @function get-font-size-variable($size, $fixed) {
130
135
  @if $fixed == true {
131
136
  @if map-has-key($gl-font-sizes-fixed, $size) {
@@ -144,17 +149,22 @@
144
149
  }
145
150
  }
146
151
 
147
- // Set fixed as false by default, matching desired usage in the product
148
- // Headings do not have margin included, margin is determined by the context
149
- @mixin gl-heading($size, $fixed: false) {
150
- // These settings apply to all headings
152
+ /**
153
+ * Defines default properties for heading typography based on font-size
154
+ * scale value and default or fixed sizing.
155
+ *
156
+ * Note: overrides Bootstrap margin-top, other margin is determined by
157
+ * individual context
158
+ *
159
+ * @param $size Number font-size scale
160
+ * @param $fixed Boolean toggle default and fixed font size scales
161
+ */
162
+ @mixin gl-heading-scale($size, $fixed: false) {
151
163
  font-weight: $gl-font-weight-heading;
152
164
  margin-top: 0; // override bootstrap reset in GitLab
153
-
154
- // Because we can't interpolate into a variable name we have to do some stuff
155
165
  font-size: get-font-size-variable($size, $fixed);
156
166
 
157
- // Larger headings have less letter spacing
167
+ // Larger headings have reduced letter spacing
158
168
  @if ($size <= 500) {
159
169
  letter-spacing: $gl-letter-spacing-heading;
160
170
  } @else {