@gitlab/ui 66.14.0 → 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,10 @@
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
+
1
8
  # [66.14.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v66.13.1...v66.14.0) (2023-09-25)
2
9
 
3
10
 
@@ -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;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Mon, 25 Sep 2023 20:40:39 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 20:40:39 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 20:40:39 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 20:40:39 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 20:40:39 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 20:40:39 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.14.0",
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;