@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,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
- <div ref="content" v-safe-html:[$options.safeHtmlConfigExtension]="messageContent"></div>
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
- <template v-if="isAssistantMessage">
179
- <documentation-sources v-if="sources" :sources="sources" />
197
+ <template v-if="isAssistantMessage">
198
+ <documentation-sources v-if="sources" :sources="sources" />
180
199
 
181
- <div class="gl-display-flex gl-align-items-flex-end gl-mt-4 duo-chat-message-feedback">
182
- <gl-duo-user-feedback
183
- :feedback-received="hasFeedback"
184
- :modal-title="$options.i18n.MODAL.TITLE"
185
- :modal-alert="$options.i18n.MODAL.ALERT_TEXT"
186
- @feedback="logEvent"
187
- >
188
- <template #feedback-extra-fields>
189
- <gl-form-group :label="$options.i18n.MODAL.DID_WHAT" optional>
190
- <gl-form-textarea v-model="didWhat" :placeholder="$options.i18n.MODAL.INTERACTION" />
191
- </gl-form-group>
192
- <gl-form-group :label="$options.i18n.MODAL.IMPROVE_WHAT" optional>
193
- <gl-form-textarea
194
- v-model="improveWhat"
195
- :placeholder="$options.i18n.MODAL.BETTER_RESPONSE"
196
- />
197
- </gl-form-group>
198
- </template>
199
- </gl-duo-user-feedback>
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
- </template>
225
+ </div>
202
226
  </div>
203
227
  </template>
@@ -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
+ }
@@ -3,6 +3,7 @@ import {
3
3
  isElementTabbable,
4
4
  focusFirstFocusableElement,
5
5
  stopEvent,
6
+ getHorizontalBoundingClientRect,
6
7
  } from './utils';
7
8
 
8
9
  describe('isElementFocusable', () => {
@@ -153,4 +154,55 @@ describe('stopEvent', () => {
153
154
  expect(event.stopPropagation).not.toHaveBeenCalled();
154
155
  expect(event.stopImmediatePropagation).toHaveBeenCalledTimes(1);
155
156
  });
157
+
158
+ describe('getHorizontalBoundingClientRect', () => {
159
+ describe('when there is a reference element', () => {
160
+ let mainElement;
161
+
162
+ const [
163
+ MAIN_LEFT_BOUNDARY,
164
+ MAIN_WIDTH,
165
+ MAIN_TOP_BOUNDARY,
166
+ MAIN_HEIGHT,
167
+ DOCUMENT_TOP_BOUNDARY,
168
+ DOCUMENT_HEIGHT,
169
+ ] = [10, 20, 30, 40, 0, 50];
170
+
171
+ beforeEach(() => {
172
+ mainElement = document.createElement('main');
173
+ jest.spyOn(mainElement, 'getBoundingClientRect').mockImplementation(() => {
174
+ return {
175
+ x: MAIN_LEFT_BOUNDARY,
176
+ width: MAIN_WIDTH,
177
+ y: MAIN_TOP_BOUNDARY,
178
+ height: MAIN_HEIGHT,
179
+ };
180
+ });
181
+ Object.defineProperty(document.documentElement, 'clientHeight', {
182
+ get() {
183
+ return DOCUMENT_HEIGHT;
184
+ },
185
+ });
186
+ });
187
+
188
+ afterEach(() => {
189
+ mainElement = null;
190
+ });
191
+
192
+ it("returns the element's horizontal boundaries and the document's vertical boundaries", () => {
193
+ expect(getHorizontalBoundingClientRect(mainElement)).toEqual({
194
+ x: MAIN_LEFT_BOUNDARY,
195
+ width: MAIN_WIDTH,
196
+ y: DOCUMENT_TOP_BOUNDARY,
197
+ height: DOCUMENT_HEIGHT,
198
+ });
199
+ });
200
+ });
201
+
202
+ describe('when there is no reference element', () => {
203
+ it('returns `null`', () => {
204
+ expect(getHorizontalBoundingClientRect(null)).toEqual(null);
205
+ });
206
+ });
207
+ });
156
208
  });