@gitlab/ui 61.1.3 → 61.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 (28) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/components/base/filtered_search/common_story_options.js +2 -1
  3. package/dist/components/base/filtered_search/filtered_search.js +28 -4
  4. package/dist/components/base/filtered_search/filtered_search_suggestion_list.js +13 -5
  5. package/dist/components/base/filtered_search/filtered_search_term.js +51 -4
  6. package/dist/components/base/filtered_search/filtered_search_token.js +1 -2
  7. package/dist/components/base/filtered_search/filtered_search_token_segment.js +18 -5
  8. package/dist/components/base/filtered_search/filtered_search_utils.js +18 -7
  9. package/dist/utility_classes.css +1 -1
  10. package/dist/utility_classes.css.map +1 -1
  11. package/package.json +1 -1
  12. package/src/components/base/filtered_search/__snapshots__/filtered_search_term.spec.js.snap +2 -5
  13. package/src/components/base/filtered_search/common_story_options.js +1 -0
  14. package/src/components/base/filtered_search/filtered_search.md +10 -1
  15. package/src/components/base/filtered_search/filtered_search.spec.js +14 -2
  16. package/src/components/base/filtered_search/filtered_search.stories.js +17 -0
  17. package/src/components/base/filtered_search/filtered_search.vue +29 -1
  18. package/src/components/base/filtered_search/filtered_search_suggestion_list.spec.js +222 -75
  19. package/src/components/base/filtered_search/filtered_search_suggestion_list.vue +15 -6
  20. package/src/components/base/filtered_search/filtered_search_term.spec.js +73 -14
  21. package/src/components/base/filtered_search/filtered_search_term.vue +64 -6
  22. package/src/components/base/filtered_search/filtered_search_token.spec.js +1 -0
  23. package/src/components/base/filtered_search/filtered_search_token.vue +2 -2
  24. package/src/components/base/filtered_search/filtered_search_token_segment.spec.js +46 -2
  25. package/src/components/base/filtered_search/filtered_search_token_segment.vue +22 -5
  26. package/src/components/base/filtered_search/filtered_search_utils.js +20 -7
  27. package/src/scss/utilities.scss +32 -16
  28. package/src/scss/utility-mixins/border.scss +16 -8
@@ -1,7 +1,8 @@
1
1
  import { nextTick } from 'vue';
2
2
  import { shallowMount } from '@vue/test-utils';
3
+ import GlToken from '../token/token.vue';
3
4
  import FilteredSearchTerm from './filtered_search_term.vue';
4
- import { INTENT_ACTIVATE_PREVIOUS } from './filtered_search_utils';
5
+ import { INTENT_ACTIVATE_PREVIOUS, TERM_TOKEN_TYPE } from './filtered_search_utils';
5
6
 
6
7
  const availableTokens = [
7
8
  { type: 'foo', title: 'test1-foo', token: 'stub', icon: 'eye' },
@@ -9,6 +10,8 @@ const availableTokens = [
9
10
  { type: 'baz', title: 'test1-baz', token: 'stub', icon: 'eye' },
10
11
  ];
11
12
 
13
+ const pointerClass = 'gl-cursor-pointer';
14
+
12
15
  describe('Filtered search term', () => {
13
16
  let wrapper;
14
17
 
@@ -21,13 +24,23 @@ describe('Filtered search term', () => {
21
24
 
22
25
  const segmentStub = {
23
26
  name: 'gl-filtered-search-token-segment-stub',
24
- template: '<div><slot name="view"></slot><slot name="suggestions"></slot></div>',
25
- props: ['searchInputAttributes', 'isLastToken', 'currentValue', 'viewOnly', 'options'],
27
+ template: '<div><slot v-if="!active" name="view"></slot><slot name="suggestions"></slot></div>',
28
+ props: [
29
+ 'searchInputAttributes',
30
+ 'isLastToken',
31
+ 'currentValue',
32
+ 'viewOnly',
33
+ 'options',
34
+ 'active',
35
+ ],
26
36
  };
27
37
 
28
- const createComponent = (props) => {
38
+ const createComponent = ({ termsAsTokens = false, ...props } = {}) => {
29
39
  wrapper = shallowMount(FilteredSearchTerm, {
30
40
  propsData: { ...defaultProps, ...props },
41
+ provide: {
42
+ termsAsTokens: () => termsAsTokens,
43
+ },
31
44
  stubs: {
32
45
  'gl-filtered-search-token-segment': segmentStub,
33
46
  },
@@ -36,14 +49,24 @@ describe('Filtered search term', () => {
36
49
 
37
50
  const findSearchInput = () => wrapper.find('input');
38
51
  const findTokenSegmentComponent = () => wrapper.findComponent(segmentStub);
52
+ const findToken = () => wrapper.findComponent(GlToken);
39
53
 
40
54
  it('renders value in inactive mode', () => {
41
55
  createComponent({ value: { data: 'test-value' } });
42
56
  expect(wrapper.html()).toMatchSnapshot();
43
57
  });
44
58
 
45
- it('renders input with value in active mode', () => {
59
+ it('renders token in inactive mode with termsAsTokens', () => {
60
+ createComponent({ value: { data: 'test value' }, termsAsTokens: true });
61
+ expect(findToken().exists()).toBe(true);
62
+ expect(findToken().props().viewOnly).toBe(false);
63
+ expect(findToken().classes()).toContain(pointerClass);
64
+ expect(findToken().text()).toBe('test value');
65
+ });
66
+
67
+ it('renders nothing with value in active mode (delegates to segment)', () => {
46
68
  createComponent({ value: { data: 'test-value' }, active: true });
69
+ expect(wrapper.text()).not.toContain('test-value');
47
70
  expect(wrapper.html()).toMatchSnapshot();
48
71
  });
49
72
 
@@ -60,20 +83,45 @@ describe('Filtered search term', () => {
60
83
  expect(findTokenSegmentComponent().props('options')).toHaveLength(2);
61
84
  });
62
85
 
86
+ it('suggests text search when termsAsTokens=true', async () => {
87
+ createComponent({
88
+ availableTokens,
89
+ active: true,
90
+ value: { data: 'foo' },
91
+ termsAsTokens: true,
92
+ });
93
+
94
+ await nextTick();
95
+
96
+ expect(findTokenSegmentComponent().props('options')).toEqual([
97
+ expect.objectContaining({
98
+ value: 'foo',
99
+ title: 'test1-foo',
100
+ }),
101
+ expect.objectContaining({
102
+ value: TERM_TOKEN_TYPE,
103
+ title: 'Search for this text',
104
+ }),
105
+ ]);
106
+ });
107
+
63
108
  it.each`
64
- originalEvent | emittedEvent | payload
65
- ${'activate'} | ${'activate'} | ${undefined}
66
- ${'deactivate'} | ${'deactivate'} | ${undefined}
67
- ${'split'} | ${'split'} | ${undefined}
68
- ${'submit'} | ${'submit'} | ${undefined}
69
- ${'complete'} | ${'replace'} | ${{ type: undefined }}
70
- ${'backspace'} | ${'destroy'} | ${{ intent: INTENT_ACTIVATE_PREVIOUS }}
109
+ originalEvent | originalPayload | emittedEvent | payload
110
+ ${'activate'} | ${undefined} | ${'activate'} | ${undefined}
111
+ ${'deactivate'} | ${undefined} | ${'deactivate'} | ${undefined}
112
+ ${'split'} | ${'foo'} | ${'split'} | ${'foo'}
113
+ ${'submit'} | ${undefined} | ${'submit'} | ${undefined}
114
+ ${'previous'} | ${undefined} | ${'previous'} | ${undefined}
115
+ ${'next'} | ${undefined} | ${'next'} | ${undefined}
116
+ ${'complete'} | ${'foo'} | ${'replace'} | ${{ type: 'foo' }}
117
+ ${'complete'} | ${TERM_TOKEN_TYPE} | ${'complete'} | ${undefined}
118
+ ${'backspace'} | ${undefined} | ${'destroy'} | ${{ intent: INTENT_ACTIVATE_PREVIOUS }}
71
119
  `(
72
120
  'emits $emittedEvent when token segment emits $originalEvent',
73
- async ({ originalEvent, emittedEvent, payload }) => {
121
+ async ({ originalEvent, originalPayload, emittedEvent, payload }) => {
74
122
  createComponent({ active: true, value: { data: 'something' } });
75
123
 
76
- findTokenSegmentComponent().vm.$emit(originalEvent);
124
+ findTokenSegmentComponent().vm.$emit(originalEvent, originalPayload);
77
125
 
78
126
  await nextTick();
79
127
 
@@ -117,6 +165,17 @@ describe('Filtered search term', () => {
117
165
  expect(findTokenSegmentComponent().props('viewOnly')).toBe(false);
118
166
  });
119
167
 
168
+ it('sets viewOnly prop and removes pointer class on token when termsAsTokens=true', () => {
169
+ createComponent({
170
+ value: { data: 'foo' },
171
+ viewOnly: true,
172
+ termsAsTokens: true,
173
+ });
174
+
175
+ expect(findToken().props().viewOnly).toBe(true);
176
+ expect(findToken().classes()).not.toContain(pointerClass);
177
+ });
178
+
120
179
  it('adds `searchInputAttributes` prop to search term input', () => {
121
180
  createComponent({
122
181
  placeholder: 'placeholder-stub',
@@ -1,12 +1,23 @@
1
1
  <script>
2
+ import GlToken from '../token/token.vue';
3
+ import { stopEvent } from '../../../utils/utils';
2
4
  import GlFilteredSearchTokenSegment from './filtered_search_token_segment.vue';
3
- import { INTENT_ACTIVATE_PREVIOUS, match, tokenToOption } from './filtered_search_utils';
5
+ import {
6
+ INTENT_ACTIVATE_PREVIOUS,
7
+ TERM_TOKEN_TYPE,
8
+ TOKEN_CLOSE_SELECTOR,
9
+ match,
10
+ tokenToOption,
11
+ termTokenDefinition,
12
+ } from './filtered_search_utils';
4
13
 
5
14
  export default {
6
15
  name: 'GlFilteredSearchTerm',
7
16
  components: {
8
17
  GlFilteredSearchTokenSegment,
18
+ GlToken,
9
19
  },
20
+ inject: ['termsAsTokens'],
10
21
  inheritAttrs: false,
11
22
  props: {
12
23
  /**
@@ -67,6 +78,14 @@ export default {
67
78
  default: 'end',
68
79
  validator: (value) => ['start', 'end'].includes(value),
69
80
  },
81
+ /**
82
+ * The title of the text search option. Ignored unless termsAsTokens is enabled.
83
+ */
84
+ searchTextOptionLabel: {
85
+ type: String,
86
+ required: false,
87
+ default: termTokenDefinition.title,
88
+ },
70
89
  viewOnly: {
71
90
  type: Boolean,
72
91
  required: false,
@@ -74,10 +93,22 @@ export default {
74
93
  },
75
94
  },
76
95
  computed: {
96
+ showInput() {
97
+ return this.termsAsTokens() || Boolean(this.placeholder);
98
+ },
99
+ showToken() {
100
+ return this.termsAsTokens() && Boolean(this.value.data);
101
+ },
77
102
  suggestedTokens() {
78
- return this.availableTokens
79
- .filter((token) => match(token.title, this.value.data))
80
- .map(tokenToOption);
103
+ const tokens = this.availableTokens.filter((token) => match(token.title, this.value.data));
104
+ if (this.termsAsTokens() && this.value.data) {
105
+ tokens.push({
106
+ ...termTokenDefinition,
107
+ title: this.searchTextOptionLabel,
108
+ });
109
+ }
110
+
111
+ return tokens.map(tokenToOption);
81
112
  },
82
113
  internalValue: {
83
114
  get() {
@@ -93,6 +124,9 @@ export default {
93
124
  this.$emit('input', { data });
94
125
  },
95
126
  },
127
+ eventListeners() {
128
+ return this.viewOnly ? {} : { mousedown: this.destroyByClose };
129
+ },
96
130
  },
97
131
  methods: {
98
132
  onBackspace() {
@@ -105,6 +139,21 @@ export default {
105
139
  */
106
140
  this.$emit('destroy', { intent: INTENT_ACTIVATE_PREVIOUS });
107
141
  },
142
+ destroyByClose(event) {
143
+ if (event.target.closest(TOKEN_CLOSE_SELECTOR)) {
144
+ stopEvent(event);
145
+ this.$emit('destroy');
146
+ }
147
+ },
148
+ onComplete(type) {
149
+ if (type === TERM_TOKEN_TYPE) {
150
+ // We've completed this term token
151
+ this.$emit('complete');
152
+ } else {
153
+ // We're changing the current token type
154
+ this.$emit('replace', { type });
155
+ }
156
+ },
108
157
  },
109
158
  };
110
159
  </script>
@@ -134,6 +183,7 @@ export default {
134
183
 
135
184
  <!--
136
185
  Emitted when Space is pressed in-between term text.
186
+ Not emitted when termsAsTokens is true.
137
187
  @event split
138
188
  @property {array} newTokens Token configurations
139
189
  -->
@@ -152,7 +202,7 @@ export default {
152
202
  :options="suggestedTokens"
153
203
  @activate="$emit('activate')"
154
204
  @deactivate="$emit('deactivate')"
155
- @complete="$emit('replace', { type: $event })"
205
+ @complete="onComplete"
156
206
  @backspace="onBackspace"
157
207
  @submit="$emit('submit')"
158
208
  @split="$emit('split', $event)"
@@ -160,8 +210,16 @@ export default {
160
210
  @next="$emit('next')"
161
211
  >
162
212
  <template #view>
213
+ <gl-token
214
+ v-if="showToken"
215
+ :class="{ 'gl-cursor-pointer': !viewOnly }"
216
+ :view-only="viewOnly"
217
+ v-on="eventListeners"
218
+ >{{ value.data }}</gl-token
219
+ >
220
+
163
221
  <input
164
- v-if="placeholder"
222
+ v-else-if="showInput"
165
223
  v-bind="searchInputAttributes"
166
224
  class="gl-filtered-search-term-input"
167
225
  :class="{ 'gl-bg-gray-10': viewOnly }"
@@ -44,6 +44,7 @@ describe('Filtered search token', () => {
44
44
  provide: {
45
45
  portalName: 'fake target',
46
46
  alignSuggestions: function fakeAlignSuggestions() {},
47
+ termsAsTokens: () => false,
47
48
  },
48
49
  stubs: {
49
50
  Portal: {
@@ -3,12 +3,11 @@ import cloneDeep from 'lodash/cloneDeep';
3
3
  import { COMMA } from '../../../utils/constants';
4
4
  import GlToken from '../token/token.vue';
5
5
  import GlFilteredSearchTokenSegment from './filtered_search_token_segment.vue';
6
- import { createTerm, tokenToOption } from './filtered_search_utils';
6
+ import { createTerm, tokenToOption, TOKEN_CLOSE_SELECTOR } from './filtered_search_utils';
7
7
 
8
8
  const SEGMENT_TITLE = 'TYPE';
9
9
  const SEGMENT_OPERATOR = 'OPERATOR';
10
10
  const SEGMENT_DATA = 'DATA';
11
- const TOKEN_CLOSE_SELECTOR = '.gl-token-close';
12
11
 
13
12
  const DEFAULT_OPERATORS = [
14
13
  { value: '=', description: 'is', default: true },
@@ -392,6 +391,7 @@ export default {
392
391
 
393
392
  <!--
394
393
  Emitted when Space is pressed in-between term text.
394
+ Not emitted when termsAsTokens is true.
395
395
  @event split
396
396
  @property {array} newTokens Token configurations
397
397
  -->
@@ -1,5 +1,6 @@
1
1
  import { shallowMount } from '@vue/test-utils';
2
2
  import GlFilteredSearchTokenSegment from './filtered_search_token_segment.vue';
3
+ import { TERM_TOKEN_TYPE } from './filtered_search_utils';
3
4
 
4
5
  const OPTIONS = [
5
6
  { value: '=', title: 'is' },
@@ -26,7 +27,7 @@ describe('Filtered search token segment', () => {
26
27
  let alignSuggestionsMock;
27
28
  let suggestionsMock;
28
29
 
29
- const createComponent = (props) => {
30
+ const createComponent = ({ termsAsTokens = false, ...props } = {}) => {
30
31
  alignSuggestionsMock = jest.fn();
31
32
  suggestionsMock = {
32
33
  methods: {
@@ -42,6 +43,7 @@ describe('Filtered search token segment', () => {
42
43
  provide: {
43
44
  portalName: 'fakePortal',
44
45
  alignSuggestions: alignSuggestionsMock,
46
+ termsAsTokens: () => termsAsTokens,
45
47
  },
46
48
  stubs: {
47
49
  Portal: { template: '<div><slot></slot></div>' },
@@ -50,7 +52,7 @@ describe('Filtered search token segment', () => {
50
52
  });
51
53
  };
52
54
 
53
- const createWrappedComponent = ({ value, ...otherProps } = {}) => {
55
+ const createWrappedComponent = ({ value, termsAsTokens = false, ...otherProps } = {}) => {
54
56
  // We need to create fake parent due to https://github.com/vuejs/vue-test-utils/issues/1140
55
57
  const fakeParent = {
56
58
  inheritAttrs: false,
@@ -68,6 +70,9 @@ describe('Filtered search token segment', () => {
68
70
 
69
71
  wrapper = shallowMount(fakeParent, {
70
72
  propsData: { ...otherProps, cursorPosition: 'end' },
73
+ provide: {
74
+ termsAsTokens: () => termsAsTokens,
75
+ },
71
76
  stubs: { GlFilteredSearchTokenSegment },
72
77
  });
73
78
  };
@@ -184,6 +189,22 @@ describe('Filtered search token segment', () => {
184
189
  );
185
190
  });
186
191
 
192
+ it('leaves value as-is if options are provided and isTerm=true', async () => {
193
+ const originalValue = '!=';
194
+ createWrappedComponent({
195
+ value: originalValue,
196
+ title: 'Test',
197
+ options: OPTIONS,
198
+ isTerm: true,
199
+ active: true,
200
+ });
201
+
202
+ await wrapper.setData({ value: 'invalid' });
203
+ await wrapper.setProps({ active: false });
204
+
205
+ expect(wrapper.findComponent(GlFilteredSearchTokenSegment).emitted().input).toBe(undefined);
206
+ });
207
+
187
208
  describe('applySuggestion', () => {
188
209
  it('emits original token when no spaces are present', () => {
189
210
  createComponent({ value: '' });
@@ -209,6 +230,29 @@ describe('Filtered search token segment', () => {
209
230
  expect(wrapper.emitted('complete')[0][0]).toBe(formattedToken);
210
231
  });
211
232
 
233
+ it('emits token as-is when spaces are present and termsAsTokens=true', () => {
234
+ createComponent({ value: '', termsAsTokens: true });
235
+
236
+ const token = 'token with spaces';
237
+
238
+ wrapper.vm.applySuggestion(token);
239
+
240
+ expect(wrapper.emitted('input')[0][0]).toBe(token);
241
+ expect(wrapper.emitted('select')[0][0]).toBe(token);
242
+ expect(wrapper.emitted('complete')[0][0]).toBe(token);
243
+ });
244
+
245
+ it('emits input value when term token suggestion is chosen and termsAsTokens=true', () => {
246
+ const value = `some text with 'spaces and' "quotes"`;
247
+ createComponent({ value, termsAsTokens: true });
248
+
249
+ wrapper.vm.applySuggestion(TERM_TOKEN_TYPE);
250
+
251
+ expect(wrapper.emitted('input')[0][0]).toBe(value);
252
+ expect(wrapper.emitted('select')[0][0]).toBe(TERM_TOKEN_TYPE);
253
+ expect(wrapper.emitted('complete')[0][0]).toBe(TERM_TOKEN_TYPE);
254
+ });
255
+
212
256
  it('selects suggestion on press Enter', () => {
213
257
  createComponent({ active: true, options: OPTIONS, value: false });
214
258
  wrapper.find('input').trigger('keydown', { key: 'ArrowDown' });
@@ -4,7 +4,7 @@ import { Portal } from 'portal-vue';
4
4
  import { COMMA, 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
- import { splitOnQuotes, wrapTokenInQuotes, match } from './filtered_search_utils';
7
+ import { splitOnQuotes, wrapTokenInQuotes, match, TERM_TOKEN_TYPE } from './filtered_search_utils';
8
8
 
9
9
  // We need some helpers to ensure @vue/compat compatibility
10
10
  // @vue/compat will render comment nodes for v-if and comments in HTML
@@ -51,7 +51,7 @@ export default {
51
51
  GlFilteredSearchSuggestionList,
52
52
  GlFilteredSearchSuggestion,
53
53
  },
54
- inject: ['portalName', 'alignSuggestions'],
54
+ inject: ['portalName', 'alignSuggestions', 'termsAsTokens'],
55
55
  inheritAttrs: false,
56
56
  props: {
57
57
  /**
@@ -140,6 +140,13 @@ export default {
140
140
  },
141
141
 
142
142
  computed: {
143
+ hasTermSuggestion() {
144
+ if (!this.termsAsTokens()) return false;
145
+ if (!this.options) return false;
146
+
147
+ return this.options.some(({ value }) => value === TERM_TOKEN_TYPE);
148
+ },
149
+
143
150
  matchingOption() {
144
151
  return this.options?.find((o) => o.value === this.value);
145
152
  },
@@ -182,7 +189,10 @@ export default {
182
189
  const option =
183
190
  this.getMatchingOptionForInputValue(this.inputValue) ||
184
191
  this.getMatchingOptionForInputValue(this.inputValue, { loose: true });
185
- return option?.value;
192
+
193
+ if (option) return option.value;
194
+ if (this.hasTermSuggestion) return TERM_TOKEN_TYPE;
195
+ return null;
186
196
  }
187
197
 
188
198
  const defaultOption = this.options.find((op) => op.default);
@@ -216,6 +226,8 @@ export default {
216
226
  },
217
227
 
218
228
  inputValue(newValue) {
229
+ if (this.termsAsTokens()) return;
230
+
219
231
  const hasUnclosedQuote = newValue.split('"').length % 2 === 0;
220
232
  if (newValue.indexOf(' ') === -1 || hasUnclosedQuote) {
221
233
  return;
@@ -282,7 +294,9 @@ export default {
282
294
  },
283
295
 
284
296
  applySuggestion(suggestedValue) {
285
- const formattedSuggestedValue = wrapTokenInQuotes(suggestedValue);
297
+ const formattedSuggestedValue = this.termsAsTokens()
298
+ ? suggestedValue
299
+ : wrapTokenInQuotes(suggestedValue);
286
300
 
287
301
  /**
288
302
  * Emitted when autocomplete entry is selected.
@@ -292,7 +306,10 @@ export default {
292
306
  this.$emit('select', formattedSuggestedValue);
293
307
 
294
308
  if (!this.multiSelect) {
295
- this.$emit('input', formattedSuggestedValue);
309
+ this.$emit(
310
+ 'input',
311
+ formattedSuggestedValue === TERM_TOKEN_TYPE ? this.inputValue : formattedSuggestedValue
312
+ );
296
313
  this.$emit('complete', formattedSuggestedValue);
297
314
  }
298
315
  },
@@ -7,6 +7,8 @@ export const TERM_TOKEN_TYPE = 'filtered-search-term';
7
7
 
8
8
  export const INTENT_ACTIVATE_PREVIOUS = 'intent-activate-previous';
9
9
 
10
+ export const TOKEN_CLOSE_SELECTOR = '.gl-token-close';
11
+
10
12
  export function isEmptyTerm(token) {
11
13
  return token.type === TERM_TOKEN_TYPE && token.value.data.trim() === '';
12
14
  }
@@ -135,21 +137,26 @@ export function createTerm(data = '') {
135
137
  };
136
138
  }
137
139
 
138
- export function denormalizeTokens(inputTokens) {
140
+ export function denormalizeTokens(inputTokens, termsAsTokens = false) {
139
141
  assertValidTokens(inputTokens);
140
142
 
141
143
  const tokens = Array.isArray(inputTokens) ? inputTokens : [inputTokens];
142
144
 
143
- const result = [];
144
- tokens.forEach((t) => {
145
+ return tokens.reduce((result, t) => {
145
146
  if (typeof t === 'string') {
146
- const stringTokens = t.split(' ').filter(Boolean);
147
- stringTokens.forEach((strToken) => result.push(createTerm(strToken)));
147
+ if (termsAsTokens) {
148
+ const trimmedText = t.trim();
149
+ if (trimmedText) result.push(createTerm(trimmedText));
150
+ } else {
151
+ const stringTokens = t.split(' ').filter(Boolean);
152
+ stringTokens.forEach((strToken) => result.push(createTerm(strToken)));
153
+ }
148
154
  } else {
149
155
  result.push(ensureTokenId(t));
150
156
  }
151
- });
152
- return result;
157
+
158
+ return result;
159
+ }, []);
153
160
  }
154
161
 
155
162
  /**
@@ -165,6 +172,12 @@ export function match(text, query) {
165
172
  return text.toLowerCase().includes(query.toLowerCase());
166
173
  }
167
174
 
175
+ export const termTokenDefinition = {
176
+ type: TERM_TOKEN_TYPE,
177
+ icon: 'title',
178
+ title: 'Search for this text',
179
+ };
180
+
168
181
  export function splitOnQuotes(str) {
169
182
  if (first(str) === "'" && last(str) === "'") {
170
183
  return [str];
@@ -1735,6 +1735,14 @@
1735
1735
  border-top-right-radius: $gl-border-radius-base !important;
1736
1736
  }
1737
1737
 
1738
+ .gl-rounded-bottom-left-small {
1739
+ border-bottom-left-radius: $gl-border-radius-small;
1740
+ }
1741
+
1742
+ .gl-rounded-bottom-left-small\! {
1743
+ border-bottom-left-radius: $gl-border-radius-small !important;
1744
+ }
1745
+
1738
1746
  .gl-rounded-bottom-left-base {
1739
1747
  border-bottom-left-radius: $gl-border-radius-base;
1740
1748
  }
@@ -1743,6 +1751,14 @@
1743
1751
  border-bottom-left-radius: $gl-border-radius-base !important;
1744
1752
  }
1745
1753
 
1754
+ .gl-rounded-bottom-left-large {
1755
+ border-bottom-left-radius: $gl-border-radius-large;
1756
+ }
1757
+
1758
+ .gl-rounded-bottom-left-large\! {
1759
+ border-bottom-left-radius: $gl-border-radius-large !important;
1760
+ }
1761
+
1746
1762
  .gl-rounded-bottom-left-none {
1747
1763
  border-bottom-left-radius: 0;
1748
1764
  }
@@ -1751,6 +1767,14 @@
1751
1767
  border-bottom-left-radius: 0 !important;
1752
1768
  }
1753
1769
 
1770
+ .gl-rounded-bottom-right-small {
1771
+ border-bottom-right-radius: $gl-border-radius-small;
1772
+ }
1773
+
1774
+ .gl-rounded-bottom-right-small\! {
1775
+ border-bottom-right-radius: $gl-border-radius-small !important;
1776
+ }
1777
+
1754
1778
  .gl-rounded-bottom-right-base {
1755
1779
  border-bottom-right-radius: $gl-border-radius-base;
1756
1780
  }
@@ -1759,6 +1783,14 @@
1759
1783
  border-bottom-right-radius: $gl-border-radius-base !important;
1760
1784
  }
1761
1785
 
1786
+ .gl-rounded-bottom-right-large {
1787
+ border-bottom-right-radius: $gl-border-radius-large;
1788
+ }
1789
+
1790
+ .gl-rounded-bottom-right-large\! {
1791
+ border-bottom-right-radius: $gl-border-radius-large !important;
1792
+ }
1793
+
1762
1794
  .gl-rounded-bottom-right-none {
1763
1795
  border-bottom-right-radius: 0;
1764
1796
  }
@@ -1793,22 +1825,6 @@
1793
1825
  border-top-right-radius: $gl-border-radius-small !important;
1794
1826
  }
1795
1827
 
1796
- .gl-rounded-bottom-left-small {
1797
- border-bottom-left-radius: $gl-border-radius-small;
1798
- }
1799
-
1800
- .gl-rounded-bottom-left-small\! {
1801
- border-bottom-left-radius: $gl-border-radius-small !important;
1802
- }
1803
-
1804
- .gl-rounded-bottom-right-small {
1805
- border-bottom-right-radius: $gl-border-radius-small;
1806
- }
1807
-
1808
- .gl-rounded-bottom-right-small\! {
1809
- border-bottom-right-radius: $gl-border-radius-small !important;
1810
- }
1811
-
1812
1828
  .gl-rounded-bottom-left-6 {
1813
1829
  border-bottom-left-radius: $gl-border-radius-6;
1814
1830
  }
@@ -405,18 +405,34 @@
405
405
  border-top-right-radius: $gl-border-radius-base;
406
406
  }
407
407
 
408
+ @mixin gl-rounded-bottom-left-small {
409
+ border-bottom-left-radius: $gl-border-radius-small;
410
+ }
411
+
408
412
  @mixin gl-rounded-bottom-left-base {
409
413
  border-bottom-left-radius: $gl-border-radius-base;
410
414
  }
411
415
 
416
+ @mixin gl-rounded-bottom-left-large {
417
+ border-bottom-left-radius: $gl-border-radius-large;
418
+ }
419
+
412
420
  @mixin gl-rounded-bottom-left-none {
413
421
  border-bottom-left-radius: 0;
414
422
  }
415
423
 
424
+ @mixin gl-rounded-bottom-right-small {
425
+ border-bottom-right-radius: $gl-border-radius-small;
426
+ }
427
+
416
428
  @mixin gl-rounded-bottom-right-base {
417
429
  border-bottom-right-radius: $gl-border-radius-base;
418
430
  }
419
431
 
432
+ @mixin gl-rounded-bottom-right-large {
433
+ border-bottom-right-radius: $gl-border-radius-large;
434
+ }
435
+
420
436
  @mixin gl-rounded-bottom-right-none {
421
437
  border-bottom-right-radius: 0;
422
438
  }
@@ -434,14 +450,6 @@
434
450
  border-top-right-radius: $gl-border-radius-small;
435
451
  }
436
452
 
437
- @mixin gl-rounded-bottom-left-small {
438
- border-bottom-left-radius: $gl-border-radius-small;
439
- }
440
-
441
- @mixin gl-rounded-bottom-right-small {
442
- border-bottom-right-radius: $gl-border-radius-small;
443
- }
444
-
445
453
  @mixin gl-rounded-bottom-left-6 {
446
454
  border-bottom-left-radius: $gl-border-radius-6;
447
455
  }