@gitlab/ui 79.1.0 → 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.
Files changed (26) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/components/base/form/form_textarea/form_textarea.js +6 -3
  3. package/dist/components/base/new_dropdowns/base_dropdown/base_dropdown.js +9 -6
  4. package/dist/components/base/new_dropdowns/constants.js +2 -2
  5. package/dist/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.js +17 -8
  6. package/dist/index.css +1 -1
  7. package/dist/index.css.map +1 -1
  8. package/dist/tokens/css/tokens.css +1 -1
  9. package/dist/tokens/css/tokens.dark.css +1 -1
  10. package/dist/tokens/js/tokens.dark.js +1 -1
  11. package/dist/tokens/js/tokens.js +1 -1
  12. package/dist/tokens/scss/_tokens.dark.scss +1 -1
  13. package/dist/tokens/scss/_tokens.scss +1 -1
  14. package/dist/utils/utils.js +19 -1
  15. package/package.json +1 -1
  16. package/src/components/base/form/form_textarea/form_textarea.spec.js +37 -0
  17. package/src/components/base/form/form_textarea/form_textarea.vue +6 -3
  18. package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.spec.js +31 -26
  19. package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.vue +16 -6
  20. package/src/components/base/new_dropdowns/constants.js +1 -1
  21. package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.scss +15 -1
  22. package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.spec.js +51 -45
  23. package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.stories.js +8 -0
  24. package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.vue +52 -28
  25. package/src/utils/utils.js +18 -0
  26. package/src/utils/utils.spec.js +52 -0
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Wed, 24 Apr 2024 11:54:45 GMT
3
+ * Generated on Wed, 24 Apr 2024 16:36:49 GMT
4
4
  */
5
5
 
6
6
  :root {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Wed, 24 Apr 2024 11:54:45 GMT
3
+ * Generated on Wed, 24 Apr 2024 16:36:49 GMT
4
4
  */
5
5
 
6
6
  :root.gl-dark {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Wed, 24 Apr 2024 11:54:45 GMT
3
+ * Generated on Wed, 24 Apr 2024 16:36:49 GMT
4
4
  */
5
5
 
6
6
  export const DATA_VIZ_GREEN_50 = "#133a03";
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Wed, 24 Apr 2024 11:54:45 GMT
3
+ * Generated on Wed, 24 Apr 2024 16:36:49 GMT
4
4
  */
5
5
 
6
6
  export const DATA_VIZ_GREEN_50 = "#ddfab7";
@@ -1,6 +1,6 @@
1
1
 
2
2
  // Do not edit directly
3
- // Generated on Wed, 24 Apr 2024 11:54:45 GMT
3
+ // Generated on Wed, 24 Apr 2024 16:36:49 GMT
4
4
 
5
5
  $gl-text-tertiary: #737278 !default;
6
6
  $gl-text-secondary: #89888d !default;
@@ -1,6 +1,6 @@
1
1
 
2
2
  // Do not edit directly
3
- // Generated on Wed, 24 Apr 2024 11:54:45 GMT
3
+ // Generated on Wed, 24 Apr 2024 16:36:49 GMT
4
4
 
5
5
  $gl-text-tertiary: #89888d !default;
6
6
  $gl-text-secondary: #737278 !default;
@@ -194,4 +194,22 @@ function filterVisible(els) {
194
194
  return (els || []).filter(el => isVisible(el));
195
195
  }
196
196
 
197
- export { colorFromBackground, debounceByAnimationFrame, filterVisible, focusFirstFocusableElement, getColorContrast, hexToRgba, isDev, isElementFocusable, isElementTabbable, logWarning, relativeLuminance, rgbFromHex, rgbFromString, stopEvent, throttle, toSrgb, uid };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "79.1.0",
3
+ "version": "79.2.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -201,5 +201,42 @@ describe('GlFormTextArea', () => {
201
201
 
202
202
  itUpdatesDebouncedScreenReaderText(expectedText);
203
203
  });
204
+
205
+ describe('when `value` prop is `null`', () => {
206
+ const expectedText = `${characterCount} characters remaining`;
207
+
208
+ beforeEach(() => {
209
+ createComponent({
210
+ value: null,
211
+ characterCount,
212
+ });
213
+ });
214
+
215
+ it('displays remaining characters', () => {
216
+ expect(wrapper.text()).toContain(expectedText);
217
+ });
218
+
219
+ itUpdatesDebouncedScreenReaderText(expectedText);
220
+ });
221
+
222
+ describe('when `value` prop is updated to `null`', () => {
223
+ const textareaCharacterCount = 5;
224
+ const expectedText = `${characterCount} characters remaining`;
225
+
226
+ beforeEach(() => {
227
+ createComponent({
228
+ value: 'a'.repeat(textareaCharacterCount),
229
+ characterCount,
230
+ });
231
+
232
+ wrapper.setProps({ value: null });
233
+ });
234
+
235
+ it('updates character count text', () => {
236
+ expect(wrapper.text()).toContain(expectedText);
237
+ });
238
+
239
+ itUpdatesDebouncedScreenReaderText(expectedText);
240
+ });
204
241
  });
205
242
  });
@@ -98,7 +98,7 @@ export default {
98
98
  return;
99
99
  }
100
100
 
101
- this.remainingCharacterCount = this.characterCount - newValue.length;
101
+ this.remainingCharacterCount = this.characterCount - this.valueLength(newValue);
102
102
  this.debouncedUpdateRemainingCharacterCountSrOnly(newValue);
103
103
  },
104
104
  },
@@ -111,16 +111,19 @@ export default {
111
111
  );
112
112
  },
113
113
  methods: {
114
+ valueLength(value) {
115
+ return value?.length || 0;
116
+ },
114
117
  handleKeyPress(e) {
115
118
  if (e.keyCode === 13 && (e.metaKey || e.ctrlKey)) {
116
119
  this.$emit('submit');
117
120
  }
118
121
  },
119
122
  updateRemainingCharacterCountSrOnly(newValue) {
120
- this.remainingCharacterCountSrOnly = this.characterCount - newValue.length;
123
+ this.remainingCharacterCountSrOnly = this.characterCount - this.valueLength(newValue);
121
124
  },
122
125
  initialRemainingCharacterCount() {
123
- return this.characterCount - this.value.length;
126
+ return this.characterCount - this.valueLength(this.value);
124
127
  },
125
128
  },
126
129
  };
@@ -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(Object)), shift()],
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
- GL_DROPDOWN_BOUNDARY_SELECTOR,
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 { logWarning, isElementTabbable, isElementFocusable } from '../../../../utils/utils';
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
- alignment,
276
- boundary: this.$el.closest(GL_DROPDOWN_BOUNDARY_SELECTOR) || 'clippingAncestors',
277
- allowedPlacements: dropdownAllowedAutoPlacements[this.placement],
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 GL_DROPDOWN_BOUNDARY_SELECTOR = 'main';
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';
@@ -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
+ }
@@ -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('outputs errors if they are present', async () => {
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 contentText = findContent().text();
156
- expect(contentText).toContain(errors[0]);
157
- expect(contentText).toContain(errors[1]);
158
- expect(contentText).toContain(errors[2]);
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
- expect(findContent().text()).not.toContain(newContent);
343
- expect(findContent().text()).not.toContain(MOCK_USER_PROMPT_MESSAGE.content);
344
- expect(findContent().text()).toContain('error');
345
- });
364
+ await nextTick();
365
+ expect(findContent().exists()).toBe(false);
346
366
 
347
- it('merges all the errors for output', async () => {
348
- const errors = ['foo', 'bar', 'baz'];
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/development/fe_guide/style/vue.html#setting-component-state
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
- expect(renderGFM).toHaveBeenCalled();
523
- expect(findContent().text()).toBe(expectedContent);
528
+
529
+ expect(findContentWrapper().text()).toBe(expectedContent);
524
530
  }
525
531
  );
526
532
  });
@@ -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,