@gitlab/ui 79.1.1 → 79.2.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 +12 -0
- package/dist/components/base/new_dropdowns/base_dropdown/base_dropdown.js +9 -6
- package/dist/components/base/new_dropdowns/constants.js +2 -2
- package/dist/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.js +17 -8
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/tokens/css/tokens.css +1 -1
- package/dist/tokens/css/tokens.dark.css +1 -1
- package/dist/tokens/js/tokens.dark.js +1 -1
- package/dist/tokens/js/tokens.js +1 -1
- package/dist/tokens/scss/_tokens.dark.scss +1 -1
- package/dist/tokens/scss/_tokens.scss +1 -1
- package/dist/utils/utils.js +19 -1
- package/package.json +1 -1
- package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.spec.js +31 -26
- package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.vue +16 -6
- package/src/components/base/new_dropdowns/constants.js +1 -1
- package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.scss +15 -1
- package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.spec.js +51 -45
- package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.stories.js +8 -0
- package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.vue +52 -28
- package/src/utils/utils.js +18 -0
- package/src/utils/utils.spec.js +52 -0
package/dist/tokens/js/tokens.js
CHANGED
package/dist/utils/utils.js
CHANGED
|
@@ -194,4 +194,22 @@ function filterVisible(els) {
|
|
|
194
194
|
return (els || []).filter(el => isVisible(el));
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Given an element, returns a Rect object
|
|
199
|
+
* with top and bottom boundaries removed.
|
|
200
|
+
*/
|
|
201
|
+
function getHorizontalBoundingClientRect(el) {
|
|
202
|
+
const rect = el === null || el === void 0 ? void 0 : el.getBoundingClientRect();
|
|
203
|
+
if (rect) {
|
|
204
|
+
return {
|
|
205
|
+
x: rect.x,
|
|
206
|
+
width: rect.width,
|
|
207
|
+
y: 0,
|
|
208
|
+
// top of the document
|
|
209
|
+
height: document.documentElement.clientHeight // bottom of the document
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export { colorFromBackground, debounceByAnimationFrame, filterVisible, focusFirstFocusableElement, getColorContrast, getHorizontalBoundingClientRect, hexToRgba, isDev, isElementFocusable, isElementTabbable, logWarning, relativeLuminance, rgbFromHex, rgbFromString, stopEvent, throttle, toSrgb, uid };
|
package/package.json
CHANGED
|
@@ -116,15 +116,16 @@ describe('base dropdown', () => {
|
|
|
116
116
|
strategy: 'absolute',
|
|
117
117
|
middleware: [
|
|
118
118
|
offset({ mainAxis: DEFAULT_OFFSET }),
|
|
119
|
-
autoPlacement(
|
|
120
|
-
alignment: 'start',
|
|
121
|
-
boundary: document.querySelector('main'),
|
|
122
|
-
allowedPlacements: ['bottom-start', 'top-start', 'bottom-end', 'top-end'],
|
|
123
|
-
}),
|
|
119
|
+
autoPlacement(expect.any(Function)),
|
|
124
120
|
shift(),
|
|
125
121
|
],
|
|
126
122
|
}
|
|
127
123
|
);
|
|
124
|
+
expect(autoPlacement.mock.calls[0][0]()).toEqual({
|
|
125
|
+
alignment: 'start',
|
|
126
|
+
boundary: { x: 0, y: 0, width: 0, height: 0 },
|
|
127
|
+
allowedPlacements: ['bottom-start', 'top-start', 'bottom-end', 'top-end'],
|
|
128
|
+
});
|
|
128
129
|
|
|
129
130
|
document.body.innerHTML = '';
|
|
130
131
|
});
|
|
@@ -141,15 +142,16 @@ describe('base dropdown', () => {
|
|
|
141
142
|
strategy: 'absolute',
|
|
142
143
|
middleware: [
|
|
143
144
|
offset({ mainAxis: DEFAULT_OFFSET }),
|
|
144
|
-
autoPlacement(
|
|
145
|
-
alignment: 'start',
|
|
146
|
-
boundary: 'clippingAncestors',
|
|
147
|
-
allowedPlacements: ['bottom-start', 'top-start', 'bottom-end', 'top-end'],
|
|
148
|
-
}),
|
|
145
|
+
autoPlacement(expect.any(Function)),
|
|
149
146
|
shift(),
|
|
150
147
|
],
|
|
151
148
|
}
|
|
152
149
|
);
|
|
150
|
+
expect(autoPlacement.mock.calls[0][0]()).toEqual({
|
|
151
|
+
alignment: 'start',
|
|
152
|
+
boundary: 'clippingAncestors',
|
|
153
|
+
allowedPlacements: ['bottom-start', 'top-start', 'bottom-end', 'top-end'],
|
|
154
|
+
});
|
|
153
155
|
});
|
|
154
156
|
|
|
155
157
|
it('initializes Floating UI with reference and floating elements and config for center-aligned menu', async () => {
|
|
@@ -164,15 +166,16 @@ describe('base dropdown', () => {
|
|
|
164
166
|
strategy: 'absolute',
|
|
165
167
|
middleware: [
|
|
166
168
|
offset({ mainAxis: DEFAULT_OFFSET }),
|
|
167
|
-
autoPlacement(
|
|
168
|
-
alignment: undefined,
|
|
169
|
-
boundary: 'clippingAncestors',
|
|
170
|
-
allowedPlacements: ['bottom', 'top'],
|
|
171
|
-
}),
|
|
169
|
+
autoPlacement(expect.any(Function)),
|
|
172
170
|
shift(),
|
|
173
171
|
],
|
|
174
172
|
}
|
|
175
173
|
);
|
|
174
|
+
expect(autoPlacement.mock.calls[0][0]()).toEqual({
|
|
175
|
+
alignment: undefined,
|
|
176
|
+
boundary: 'clippingAncestors',
|
|
177
|
+
allowedPlacements: ['bottom', 'top'],
|
|
178
|
+
});
|
|
176
179
|
});
|
|
177
180
|
|
|
178
181
|
it('initializes Floating UI with reference and floating elements and config for right-aligned menu', async () => {
|
|
@@ -187,15 +190,16 @@ describe('base dropdown', () => {
|
|
|
187
190
|
strategy: 'absolute',
|
|
188
191
|
middleware: [
|
|
189
192
|
offset({ mainAxis: DEFAULT_OFFSET }),
|
|
190
|
-
autoPlacement(
|
|
191
|
-
alignment: 'end',
|
|
192
|
-
boundary: 'clippingAncestors',
|
|
193
|
-
allowedPlacements: ['bottom-start', 'top-start', 'bottom-end', 'top-end'],
|
|
194
|
-
}),
|
|
193
|
+
autoPlacement(expect.any(Function)),
|
|
195
194
|
shift(),
|
|
196
195
|
],
|
|
197
196
|
}
|
|
198
197
|
);
|
|
198
|
+
expect(autoPlacement.mock.calls[0][0]()).toEqual({
|
|
199
|
+
alignment: 'end',
|
|
200
|
+
boundary: 'clippingAncestors',
|
|
201
|
+
allowedPlacements: ['bottom-start', 'top-start', 'bottom-end', 'top-end'],
|
|
202
|
+
});
|
|
199
203
|
});
|
|
200
204
|
|
|
201
205
|
it('initializes Floating UI with reference and floating elements and config for `right-start` aligned menu', async () => {
|
|
@@ -210,15 +214,16 @@ describe('base dropdown', () => {
|
|
|
210
214
|
strategy: 'absolute',
|
|
211
215
|
middleware: [
|
|
212
216
|
offset({ mainAxis: DEFAULT_OFFSET }),
|
|
213
|
-
autoPlacement(
|
|
214
|
-
alignment: 'start',
|
|
215
|
-
boundary: 'clippingAncestors',
|
|
216
|
-
allowedPlacements: ['right-start', 'right-end', 'left-start', 'left-end'],
|
|
217
|
-
}),
|
|
217
|
+
autoPlacement(expect.any(Function)),
|
|
218
218
|
shift(),
|
|
219
219
|
],
|
|
220
220
|
}
|
|
221
221
|
);
|
|
222
|
+
expect(autoPlacement.mock.calls[0][0]()).toEqual({
|
|
223
|
+
alignment: 'start',
|
|
224
|
+
boundary: 'clippingAncestors',
|
|
225
|
+
allowedPlacements: ['right-start', 'right-end', 'left-start', 'left-end'],
|
|
226
|
+
});
|
|
222
227
|
});
|
|
223
228
|
|
|
224
229
|
it("passes custom offset to Floating UI's middleware", async () => {
|
|
@@ -235,7 +240,7 @@ describe('base dropdown', () => {
|
|
|
235
240
|
{
|
|
236
241
|
placement: 'bottom-end',
|
|
237
242
|
strategy: 'absolute',
|
|
238
|
-
middleware: [offset(customOffset), autoPlacement(expect.any(
|
|
243
|
+
middleware: [offset(customOffset), autoPlacement(expect.any(Function)), shift()],
|
|
239
244
|
}
|
|
240
245
|
);
|
|
241
246
|
});
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
dropdownVariantOptions,
|
|
10
10
|
} from '../../../../utils/constants';
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
GL_DROPDOWN_HORIZONTAL_BOUNDARY_SELECTOR,
|
|
13
13
|
GL_DROPDOWN_SHOWN,
|
|
14
14
|
GL_DROPDOWN_HIDDEN,
|
|
15
15
|
GL_DROPDOWN_BEFORE_CLOSE,
|
|
@@ -21,7 +21,12 @@ import {
|
|
|
21
21
|
POSITION_ABSOLUTE,
|
|
22
22
|
POSITION_FIXED,
|
|
23
23
|
} from '../constants';
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
logWarning,
|
|
26
|
+
isElementTabbable,
|
|
27
|
+
isElementFocusable,
|
|
28
|
+
getHorizontalBoundingClientRect,
|
|
29
|
+
} from '../../../../utils/utils';
|
|
25
30
|
|
|
26
31
|
import GlButton from '../../button/button.vue';
|
|
27
32
|
import GlIcon from '../../icon/icon.vue';
|
|
@@ -271,10 +276,15 @@ export default {
|
|
|
271
276
|
strategy: this.positioningStrategy,
|
|
272
277
|
middleware: [
|
|
273
278
|
offset(this.offset),
|
|
274
|
-
autoPlacement({
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
279
|
+
autoPlacement(() => {
|
|
280
|
+
const autoHorizontalBoundary = getHorizontalBoundingClientRect(
|
|
281
|
+
this.$el.closest(GL_DROPDOWN_HORIZONTAL_BOUNDARY_SELECTOR)
|
|
282
|
+
);
|
|
283
|
+
return {
|
|
284
|
+
alignment,
|
|
285
|
+
boundary: autoHorizontalBoundary || 'clippingAncestors',
|
|
286
|
+
allowedPlacements: dropdownAllowedAutoPlacements[this.placement],
|
|
287
|
+
};
|
|
278
288
|
}),
|
|
279
289
|
shift(),
|
|
280
290
|
size({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// base dropdown events
|
|
2
|
-
export const
|
|
2
|
+
export const GL_DROPDOWN_HORIZONTAL_BOUNDARY_SELECTOR = 'main';
|
|
3
3
|
export const GL_DROPDOWN_SHOWN = 'shown';
|
|
4
4
|
export const GL_DROPDOWN_HIDDEN = 'hidden';
|
|
5
5
|
export const GL_DROPDOWN_BEFORE_CLOSE = 'beforeClose';
|
package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.scss
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
.duo-chat-message {
|
|
2
2
|
max-width: 90%;
|
|
3
|
+
position: relative;
|
|
3
4
|
|
|
4
5
|
code {
|
|
5
6
|
@include gl-bg-gray-100;
|
|
6
7
|
}
|
|
7
8
|
|
|
9
|
+
pre.code {
|
|
10
|
+
@include gl-bg-white;
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
pre code {
|
|
9
14
|
@include gl-font-sm;
|
|
10
15
|
@include gl-line-height-1;
|
|
@@ -30,4 +35,13 @@
|
|
|
30
35
|
@include gl-opacity-10;
|
|
31
36
|
}
|
|
32
37
|
}
|
|
33
|
-
|
|
38
|
+
|
|
39
|
+
.has-error {
|
|
40
|
+
margin-left: $gl-spacing-scale-6;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.error-icon {
|
|
44
|
+
top: 14px;
|
|
45
|
+
position: absolute;
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.spec.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { nextTick } from 'vue';
|
|
2
2
|
import { shallowMount } from '@vue/test-utils';
|
|
3
3
|
import GlDuoUserFeedback from '../../../user_feedback/user_feedback.vue';
|
|
4
|
+
import GlIcon from '../../../../../base/icon/icon.vue';
|
|
4
5
|
import {
|
|
5
6
|
MOCK_USER_PROMPT_MESSAGE,
|
|
6
7
|
MOCK_RESPONSE_MESSAGE,
|
|
@@ -12,10 +13,13 @@ import GlDuoChatMessage from './duo_chat_message.vue';
|
|
|
12
13
|
describe('DuoChatMessage', () => {
|
|
13
14
|
let wrapper;
|
|
14
15
|
|
|
16
|
+
const findContentWrapper = () => wrapper.findComponent({ ref: 'content-wrapper' });
|
|
15
17
|
const findContent = () => wrapper.findComponent({ ref: 'content' });
|
|
18
|
+
const findErrorMessage = () => wrapper.findComponent({ ref: 'error-message' });
|
|
16
19
|
const findDocumentSources = () => wrapper.findComponent(DocumentationSources);
|
|
17
20
|
const findUserFeedback = () => wrapper.findComponent(GlDuoUserFeedback);
|
|
18
21
|
const findCopyCodeButton = () => wrapper.find('copy-code');
|
|
22
|
+
const findErrorIcon = () => wrapper.findComponent(GlIcon);
|
|
19
23
|
const mockMarkdownContent = 'foo **bar**';
|
|
20
24
|
|
|
21
25
|
let renderMarkdown;
|
|
@@ -82,6 +86,10 @@ describe('DuoChatMessage', () => {
|
|
|
82
86
|
it('does not render the user feedback component', () => {
|
|
83
87
|
expect(findUserFeedback().exists()).toBe(false);
|
|
84
88
|
});
|
|
89
|
+
|
|
90
|
+
it('does not render the error icon', () => {
|
|
91
|
+
expect(findErrorIcon().exists()).toBe(false);
|
|
92
|
+
});
|
|
85
93
|
});
|
|
86
94
|
|
|
87
95
|
describe('rendering with assistant message', () => {
|
|
@@ -100,6 +108,10 @@ describe('DuoChatMessage', () => {
|
|
|
100
108
|
expect(findDocumentSources().props('sources')).toEqual(MOCK_RESPONSE_MESSAGE.extras.sources);
|
|
101
109
|
});
|
|
102
110
|
|
|
111
|
+
it('does not render the error icon', () => {
|
|
112
|
+
expect(findErrorIcon().exists()).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
103
115
|
it.each([null, undefined, ''])(
|
|
104
116
|
'does not render sources component when `sources` is %s',
|
|
105
117
|
(sources) => {
|
|
@@ -139,7 +151,19 @@ describe('DuoChatMessage', () => {
|
|
|
139
151
|
});
|
|
140
152
|
|
|
141
153
|
describe('message output', () => {
|
|
142
|
-
it('
|
|
154
|
+
it('renders the warning icon when message has errors', () => {
|
|
155
|
+
createComponent({
|
|
156
|
+
message: {
|
|
157
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
158
|
+
errors: ['foo'],
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
expect(findErrorIcon().exists()).toBe(true);
|
|
162
|
+
expect(findErrorMessage().text()).toBe('foo');
|
|
163
|
+
expect(findContentWrapper().classes()).toContain('has-error');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('outputs errors as icon if they are present', async () => {
|
|
143
167
|
const errors = ['error1', 'error2', 'error3'];
|
|
144
168
|
|
|
145
169
|
createComponent({
|
|
@@ -147,15 +171,30 @@ describe('DuoChatMessage', () => {
|
|
|
147
171
|
...MOCK_USER_PROMPT_MESSAGE,
|
|
148
172
|
errors,
|
|
149
173
|
contentHtml: 'fooHtml barHtml',
|
|
174
|
+
content: 'foo bar',
|
|
175
|
+
chunks: ['a', 'b', 'c'],
|
|
150
176
|
},
|
|
151
177
|
});
|
|
152
178
|
|
|
153
179
|
await nextTick();
|
|
154
180
|
|
|
155
|
-
const
|
|
156
|
-
expect(
|
|
157
|
-
expect(
|
|
158
|
-
expect(
|
|
181
|
+
const errorMessage = findErrorMessage().text();
|
|
182
|
+
expect(errorMessage).toContain(errors[0]);
|
|
183
|
+
expect(errorMessage).toContain(errors[1]);
|
|
184
|
+
expect(errorMessage).toContain(errors[2]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('outputs errors if message has no content', async () => {
|
|
188
|
+
createComponent({
|
|
189
|
+
message: {
|
|
190
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
191
|
+
contentHtml: '',
|
|
192
|
+
content: '',
|
|
193
|
+
errors: ['error'],
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
await nextTick();
|
|
197
|
+
expect(findErrorMessage().text()).toBe('error');
|
|
159
198
|
});
|
|
160
199
|
|
|
161
200
|
it('outputs contentHtml if it is present', async () => {
|
|
@@ -216,23 +255,6 @@ describe('DuoChatMessage', () => {
|
|
|
216
255
|
expect(renderGFM).toHaveBeenCalled();
|
|
217
256
|
});
|
|
218
257
|
|
|
219
|
-
it('sanitizes html produced by errors', async () => {
|
|
220
|
-
createComponent({
|
|
221
|
-
options: {
|
|
222
|
-
provide: null,
|
|
223
|
-
},
|
|
224
|
-
message: {
|
|
225
|
-
...MOCK_USER_PROMPT_MESSAGE,
|
|
226
|
-
errors: ['[click here](javascript:prompt(1))'],
|
|
227
|
-
contentHtml: undefined,
|
|
228
|
-
content: '',
|
|
229
|
-
},
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
await nextTick();
|
|
233
|
-
expect(findContent().html()).toContain('<p><a>click here</a></p>');
|
|
234
|
-
});
|
|
235
|
-
|
|
236
258
|
it('sanitizes html produced by content', async () => {
|
|
237
259
|
createComponent({
|
|
238
260
|
options: {
|
|
@@ -339,27 +361,11 @@ describe('DuoChatMessage', () => {
|
|
|
339
361
|
errors: ['error'],
|
|
340
362
|
},
|
|
341
363
|
});
|
|
342
|
-
|
|
343
|
-
expect(findContent().
|
|
344
|
-
expect(findContent().text()).toContain('error');
|
|
345
|
-
});
|
|
364
|
+
await nextTick();
|
|
365
|
+
expect(findContent().exists()).toBe(false);
|
|
346
366
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
// setProps is justified here because we are testing the component's
|
|
350
|
-
// reactive behavior which consistutes an exception
|
|
351
|
-
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
352
|
-
await wrapper.setProps({
|
|
353
|
-
message: {
|
|
354
|
-
...MOCK_USER_PROMPT_MESSAGE,
|
|
355
|
-
contentHtml: '',
|
|
356
|
-
content: '',
|
|
357
|
-
errors,
|
|
358
|
-
},
|
|
359
|
-
});
|
|
360
|
-
expect(findContent().text()).toContain(errors[0]);
|
|
361
|
-
expect(findContent().text()).toContain(errors[1]);
|
|
362
|
-
expect(findContent().text()).toContain(errors[2]);
|
|
367
|
+
expect(findErrorMessage().text()).toBe('error');
|
|
368
|
+
expect(findErrorIcon().exists()).toBe(true);
|
|
363
369
|
});
|
|
364
370
|
|
|
365
371
|
it('hydrates the output message with GLFM if its not a chunk', async () => {
|
|
@@ -479,7 +485,7 @@ describe('DuoChatMessage', () => {
|
|
|
479
485
|
|
|
480
486
|
// setProps is justified here because we are testing the component's
|
|
481
487
|
// reactive behavior which consistutes an exception
|
|
482
|
-
// See https://docs.gitlab.com/ee/
|
|
488
|
+
// See https://docs.gitlab.com/ee/devoutputs errors if message has no contentelopment/fe_guide/style/vue.html#setting-component-state
|
|
483
489
|
await wrapper.setProps({
|
|
484
490
|
message: CHUNK2,
|
|
485
491
|
});
|
|
@@ -519,8 +525,8 @@ describe('DuoChatMessage', () => {
|
|
|
519
525
|
errors,
|
|
520
526
|
},
|
|
521
527
|
});
|
|
522
|
-
|
|
523
|
-
expect(
|
|
528
|
+
|
|
529
|
+
expect(findContentWrapper().text()).toBe(expectedContent);
|
|
524
530
|
}
|
|
525
531
|
);
|
|
526
532
|
});
|
package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.stories.js
CHANGED
|
@@ -33,6 +33,14 @@ Response.args = generateProps({
|
|
|
33
33
|
message: MOCK_RESPONSE_MESSAGE,
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
export const ErrorResponse = Template.bind({});
|
|
37
|
+
ErrorResponse.args = generateProps({
|
|
38
|
+
message: {
|
|
39
|
+
...MOCK_RESPONSE_MESSAGE,
|
|
40
|
+
errors: ['Error: Whatever you see is wrong'],
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
36
44
|
export default {
|
|
37
45
|
title: 'experimental/duo/chat/duo-chat-message',
|
|
38
46
|
component: GlDuoChatMessage,
|
package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.vue
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
import GlIcon from '../../../../../base/icon/icon.vue';
|
|
3
|
+
import { GlTooltipDirective } from '../../../../../../directives/tooltip';
|
|
2
4
|
import GlDuoUserFeedback from '../../../user_feedback/user_feedback.vue';
|
|
3
5
|
import GlFormGroup from '../../../../../base/form/form_group/form_group.vue';
|
|
4
6
|
import GlFormTextarea from '../../../../../base/form/form_textarea/form_textarea.vue';
|
|
@@ -19,11 +21,13 @@ export const i18n = {
|
|
|
19
21
|
INTERACTION: 'The situation in which you interacted with GitLab Duo Chat.',
|
|
20
22
|
IMPROVE_WHAT: 'How could the response be improved?',
|
|
21
23
|
BETTER_RESPONSE: 'How the response might better meet your needs.',
|
|
24
|
+
MESSAGE_ERROR: 'Error sending the message',
|
|
22
25
|
},
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
export default {
|
|
26
29
|
name: 'GlDuoChatMessage',
|
|
30
|
+
i18n,
|
|
27
31
|
safeHtmlConfigExtension: {
|
|
28
32
|
ADD_TAGS: ['copy-code'],
|
|
29
33
|
},
|
|
@@ -32,9 +36,11 @@ export default {
|
|
|
32
36
|
GlDuoUserFeedback,
|
|
33
37
|
GlFormGroup,
|
|
34
38
|
GlFormTextarea,
|
|
39
|
+
GlIcon,
|
|
35
40
|
},
|
|
36
41
|
directives: {
|
|
37
42
|
SafeHtml,
|
|
43
|
+
GlTooltip: GlTooltipDirective,
|
|
38
44
|
},
|
|
39
45
|
provide() {
|
|
40
46
|
return {
|
|
@@ -92,9 +98,6 @@ export default {
|
|
|
92
98
|
return this.message.extras?.hasFeedback;
|
|
93
99
|
},
|
|
94
100
|
defaultContent() {
|
|
95
|
-
if (this.message.errors.length > 0)
|
|
96
|
-
return this.renderMarkdown(this.message.errors.join('; '));
|
|
97
|
-
|
|
98
101
|
if (this.message.contentHtml) {
|
|
99
102
|
return this.message.contentHtml;
|
|
100
103
|
}
|
|
@@ -105,8 +108,13 @@ export default {
|
|
|
105
108
|
if (this.isAssistantMessage && this.isChunk) {
|
|
106
109
|
return this.renderMarkdown(concatUntilEmpty(this.messageChunks));
|
|
107
110
|
}
|
|
111
|
+
|
|
108
112
|
return this.defaultContent || this.renderMarkdown(concatUntilEmpty(this.message.chunks));
|
|
109
113
|
},
|
|
114
|
+
|
|
115
|
+
error() {
|
|
116
|
+
return Boolean(this.message?.errors?.length) && this.message.errors.join('; ');
|
|
117
|
+
},
|
|
110
118
|
},
|
|
111
119
|
beforeCreate() {
|
|
112
120
|
if (!customElements.get('copy-code')) {
|
|
@@ -142,7 +150,7 @@ export default {
|
|
|
142
150
|
}
|
|
143
151
|
},
|
|
144
152
|
hydrateContentWithGFM() {
|
|
145
|
-
if (!this.isChunk) {
|
|
153
|
+
if (!this.isChunk && this.$refs.content) {
|
|
146
154
|
this.$nextTick(this.renderGFM(this.$refs.content));
|
|
147
155
|
}
|
|
148
156
|
},
|
|
@@ -161,7 +169,6 @@ export default {
|
|
|
161
169
|
}
|
|
162
170
|
},
|
|
163
171
|
},
|
|
164
|
-
i18n,
|
|
165
172
|
};
|
|
166
173
|
</script>
|
|
167
174
|
<template>
|
|
@@ -171,33 +178,50 @@ export default {
|
|
|
171
178
|
'gl-ml-auto gl-bg-blue-100 gl-text-blue-900 gl-rounded-bottom-right-none': isUserMessage,
|
|
172
179
|
'gl-rounded-bottom-left-none gl-text-gray-900 gl-bg-white gl-border-1 gl-border-solid gl-border-gray-50':
|
|
173
180
|
isAssistantMessage,
|
|
181
|
+
'gl-bg-red-50 gl-border-none!': error,
|
|
174
182
|
}"
|
|
175
183
|
>
|
|
176
|
-
<
|
|
184
|
+
<gl-icon
|
|
185
|
+
v-if="error"
|
|
186
|
+
:aria-label="$options.i18n.MESSAGE_ERROR"
|
|
187
|
+
name="status_warning_borderless"
|
|
188
|
+
:size="16"
|
|
189
|
+
class="gl-text-red-600 gl-border gl-border-red-500 gl-rounded-full gl-mr-3 gl-flex-shrink-0 error-icon"
|
|
190
|
+
data-testid="error"
|
|
191
|
+
/>
|
|
192
|
+
<div ref="content-wrapper" :class="{ 'has-error': error }">
|
|
193
|
+
<div v-if="error" ref="error-message" class="error-message">{{ error }}</div>
|
|
194
|
+
<div v-else>
|
|
195
|
+
<div ref="content" v-safe-html:[$options.safeHtmlConfigExtension]="messageContent"></div>
|
|
177
196
|
|
|
178
|
-
|
|
179
|
-
|
|
197
|
+
<template v-if="isAssistantMessage">
|
|
198
|
+
<documentation-sources v-if="sources" :sources="sources" />
|
|
180
199
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
:
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
+
<div class="gl-display-flex gl-align-items-flex-end gl-mt-4 duo-chat-message-feedback">
|
|
201
|
+
<gl-duo-user-feedback
|
|
202
|
+
:feedback-received="hasFeedback"
|
|
203
|
+
:modal-title="$options.i18n.MODAL.TITLE"
|
|
204
|
+
:modal-alert="$options.i18n.MODAL.ALERT_TEXT"
|
|
205
|
+
@feedback="logEvent"
|
|
206
|
+
>
|
|
207
|
+
<template #feedback-extra-fields>
|
|
208
|
+
<gl-form-group :label="$options.i18n.MODAL.DID_WHAT" optional>
|
|
209
|
+
<gl-form-textarea
|
|
210
|
+
v-model="didWhat"
|
|
211
|
+
:placeholder="$options.i18n.MODAL.INTERACTION"
|
|
212
|
+
/>
|
|
213
|
+
</gl-form-group>
|
|
214
|
+
<gl-form-group :label="$options.i18n.MODAL.IMPROVE_WHAT" optional>
|
|
215
|
+
<gl-form-textarea
|
|
216
|
+
v-model="improveWhat"
|
|
217
|
+
:placeholder="$options.i18n.MODAL.BETTER_RESPONSE"
|
|
218
|
+
/>
|
|
219
|
+
</gl-form-group>
|
|
220
|
+
</template>
|
|
221
|
+
</gl-duo-user-feedback>
|
|
222
|
+
</div>
|
|
223
|
+
</template>
|
|
200
224
|
</div>
|
|
201
|
-
</
|
|
225
|
+
</div>
|
|
202
226
|
</div>
|
|
203
227
|
</template>
|
package/src/utils/utils.js
CHANGED
|
@@ -204,3 +204,21 @@ export function stopEvent(
|
|
|
204
204
|
export function filterVisible(els) {
|
|
205
205
|
return (els || []).filter((el) => isVisible(el));
|
|
206
206
|
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Given an element, returns a Rect object
|
|
210
|
+
* with top and bottom boundaries removed.
|
|
211
|
+
*/
|
|
212
|
+
export function getHorizontalBoundingClientRect(el) {
|
|
213
|
+
const rect = el?.getBoundingClientRect();
|
|
214
|
+
|
|
215
|
+
if (rect) {
|
|
216
|
+
return {
|
|
217
|
+
x: rect.x,
|
|
218
|
+
width: rect.width,
|
|
219
|
+
y: 0, // top of the document
|
|
220
|
+
height: document.documentElement.clientHeight, // bottom of the document
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|