@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.
- package/CHANGELOG.md +14 -0
- package/dist/components/base/filtered_search/common_story_options.js +2 -1
- package/dist/components/base/filtered_search/filtered_search.js +28 -4
- package/dist/components/base/filtered_search/filtered_search_suggestion_list.js +13 -5
- package/dist/components/base/filtered_search/filtered_search_term.js +51 -4
- package/dist/components/base/filtered_search/filtered_search_token.js +1 -2
- package/dist/components/base/filtered_search/filtered_search_token_segment.js +18 -5
- package/dist/components/base/filtered_search/filtered_search_utils.js +18 -7
- package/dist/utility_classes.css +1 -1
- package/dist/utility_classes.css.map +1 -1
- package/package.json +1 -1
- package/src/components/base/filtered_search/__snapshots__/filtered_search_term.spec.js.snap +2 -5
- package/src/components/base/filtered_search/common_story_options.js +1 -0
- package/src/components/base/filtered_search/filtered_search.md +10 -1
- package/src/components/base/filtered_search/filtered_search.spec.js +14 -2
- package/src/components/base/filtered_search/filtered_search.stories.js +17 -0
- package/src/components/base/filtered_search/filtered_search.vue +29 -1
- package/src/components/base/filtered_search/filtered_search_suggestion_list.spec.js +222 -75
- package/src/components/base/filtered_search/filtered_search_suggestion_list.vue +15 -6
- package/src/components/base/filtered_search/filtered_search_term.spec.js +73 -14
- package/src/components/base/filtered_search/filtered_search_term.vue +64 -6
- package/src/components/base/filtered_search/filtered_search_token.spec.js +1 -0
- package/src/components/base/filtered_search/filtered_search_token.vue +2 -2
- package/src/components/base/filtered_search/filtered_search_token_segment.spec.js +46 -2
- package/src/components/base/filtered_search/filtered_search_token_segment.vue +22 -5
- package/src/components/base/filtered_search/filtered_search_utils.js +20 -7
- package/src/scss/utilities.scss +32 -16
- 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: [
|
|
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
|
|
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'} | ${
|
|
68
|
-
${'submit'} | ${'submit'} | ${undefined}
|
|
69
|
-
${'
|
|
70
|
-
${'
|
|
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 {
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
.
|
|
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="
|
|
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="
|
|
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 }"
|
|
@@ -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
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
144
|
-
tokens.forEach((t) => {
|
|
145
|
+
return tokens.reduce((result, t) => {
|
|
145
146
|
if (typeof t === 'string') {
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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];
|
package/src/scss/utilities.scss
CHANGED
|
@@ -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
|
}
|