@gitlab/ui 78.2.0 → 78.2.2

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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Thu, 14 Mar 2024 18:11:11 GMT
3
+ * Generated on Tue, 19 Mar 2024 16:19:47 GMT
4
4
  */
5
5
 
6
6
  :root {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Thu, 14 Mar 2024 18:11:11 GMT
3
+ * Generated on Tue, 19 Mar 2024 16:19:47 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 Thu, 14 Mar 2024 18:11:11 GMT
3
+ * Generated on Tue, 19 Mar 2024 16:19:47 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 Thu, 14 Mar 2024 18:11:11 GMT
3
+ * Generated on Tue, 19 Mar 2024 16:19:47 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 Thu, 14 Mar 2024 18:11:11 GMT
3
+ // Generated on Tue, 19 Mar 2024 16:19:47 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 Thu, 14 Mar 2024 18:11:11 GMT
3
+ // Generated on Tue, 19 Mar 2024 16:19:47 GMT
4
4
 
5
5
  $gl-text-tertiary: #89888d !default;
6
6
  $gl-text-secondary: #737278 !default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "78.2.0",
3
+ "version": "78.2.2",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -102,7 +102,7 @@
102
102
  "@gitlab/eslint-plugin": "19.4.0",
103
103
  "@gitlab/fonts": "^1.3.0",
104
104
  "@gitlab/stylelint-config": "6.1.0",
105
- "@gitlab/svgs": "3.89.0",
105
+ "@gitlab/svgs": "3.90.0",
106
106
  "@rollup/plugin-commonjs": "^11.1.0",
107
107
  "@rollup/plugin-node-resolve": "^7.1.3",
108
108
  "@rollup/plugin-replace": "^2.3.2",
@@ -113,7 +113,7 @@
113
113
  "@storybook/addon-viewport": "^7.6.17",
114
114
  "@storybook/builder-webpack5": "^7.6.17",
115
115
  "@storybook/test": "^7.6.17",
116
- "@storybook/test-runner": "0.16.0",
116
+ "@storybook/test-runner": "0.17.0",
117
117
  "@storybook/theming": "^7.6.17",
118
118
  "@storybook/vue": "^7.6.17",
119
119
  "@storybook/vue-webpack5": "^7.6.17",
@@ -131,7 +131,7 @@
131
131
  "babel-jest": "29.0.1",
132
132
  "babel-loader": "^8.0.5",
133
133
  "bootstrap": "4.6.2",
134
- "cypress": "13.6.6",
134
+ "cypress": "13.7.0",
135
135
  "cypress-axe": "^1.4.0",
136
136
  "cypress-real-events": "^1.11.0",
137
137
  "dompurify": "^3.0.0",
@@ -171,7 +171,7 @@
171
171
  "sass-true": "^6.1.0",
172
172
  "start-server-and-test": "^1.10.6",
173
173
  "storybook": "^7.6.17",
174
- "storybook-dark-mode": "3.0.3",
174
+ "storybook-dark-mode": "4.0.0",
175
175
  "style-dictionary": "^3.8.0",
176
176
  "stylelint": "15.10.2",
177
177
  "tailwind-config-viewer": "1.7.3",
@@ -349,7 +349,7 @@ const tokens = [
349
349
  token: UserToken,
350
350
  },
351
351
  { type: 'user', icon: 'user', title: 'Assignee', dataType: 'user', token: UserToken },
352
- { type: 'milestone', icon: 'clock', title: 'Milestone', unique: true, token: MilestoneToken },
352
+ { type: 'milestone', icon: 'milestone', title: 'Milestone', unique: true, token: MilestoneToken },
353
353
  { type: 'label', icon: 'labels', title: 'Label', token: LabelToken },
354
354
  { type: 'weight', icon: 'weight', title: 'Weight', unique: true, token: GlFilteredSearchToken },
355
355
  {
@@ -18,13 +18,12 @@ table.gl-table {
18
18
  @include gl-font-weight-bold;
19
19
  @include gl-text-gray-900;
20
20
 
21
- &.gl-text-right > span {
22
- @include gl-display-flex;
21
+ &.gl-text-right > div {
23
22
  flex-direction: row-reverse;
24
23
 
25
- span:nth-of-type(2) {
24
+ div.gl-ml-2 {
26
25
  @include gl-ml-0;
27
- @include gl-mr-3;
26
+ @include gl-mr-2;
28
27
  }
29
28
  }
30
29
 
@@ -129,6 +128,14 @@ table.gl-table {
129
128
  &.table-hover td.table-secondary:hover {
130
129
  background-color: $gray-10;
131
130
  }
131
+
132
+ thead th:hover {
133
+ background-color: transparent !important;
134
+
135
+ [name='sort-icon'] {
136
+ display: flex !important;
137
+ }
138
+ }
132
139
  }
133
140
 
134
141
  .table.b-table > thead > tr > th,
@@ -27,7 +27,10 @@ describe('GlTable', () => {
27
27
  };
28
28
 
29
29
  const findBTable = () => wrapper.findComponent(BTable);
30
- const findFirstColHeader = () => findBTable().find('thead').findAll('th').at(0);
30
+ const findColHeaderAt = (index) => findBTable().find('thead').findAll('th').at(index);
31
+ const findFirstColHeader = () => findColHeaderAt(0);
32
+ const findSortIconForHeaderAt = (index) =>
33
+ findColHeaderAt(index).find('[data-testid="sort-icon"]');
31
34
 
32
35
  afterEach(() => {
33
36
  logWarning.mockClear();
@@ -80,19 +83,31 @@ describe('GlTable', () => {
80
83
  });
81
84
 
82
85
  describe('sortable columns', () => {
83
- const field = {
84
- key: 'name',
85
- label: 'Name column',
86
- sortable: true,
87
- };
86
+ const fields = [
87
+ {
88
+ key: 'name',
89
+ label: 'Name column',
90
+ sortable: true,
91
+ },
92
+ {
93
+ key: 'age',
94
+ label: 'Age column',
95
+ sortable: true,
96
+ },
97
+ {
98
+ key: 'email',
99
+ label: 'Email column',
100
+ sortable: false,
101
+ },
102
+ ];
88
103
 
89
104
  describe('without custom slots', () => {
90
105
  beforeEach(() => {
91
- factory({ mountFn: mount, props: { fields: [field] } });
106
+ factory({ mountFn: mount, props: { fields } });
92
107
  });
93
108
 
94
109
  it('sets the correct column label', () => {
95
- expect(findFirstColHeader().text()).toMatch(field.label);
110
+ expect(findFirstColHeader().text()).toMatch(fields[0].label);
96
111
  });
97
112
 
98
113
  it('renders the ascending sort icon', async () => {
@@ -112,6 +127,40 @@ describe('GlTable', () => {
112
127
 
113
128
  expect(headerText).toContain('↓');
114
129
  });
130
+
131
+ it('sets initial sorting column using the sortBy property', () => {
132
+ factory({ mountFn: mount, props: { fields, sortBy: 'age' } });
133
+
134
+ expect(findSortIconForHeaderAt(0).classes()).toContain('gl-display-none');
135
+ expect(findSortIconForHeaderAt(1).classes()).not.toContain('gl-display-none');
136
+ });
137
+
138
+ it('sets initial sorting direction using the sortDesc property', () => {
139
+ factory({
140
+ mountFn: mount,
141
+ props: { fields, sortBy: 'age', sortDesc: true },
142
+ });
143
+
144
+ expect(findColHeaderAt(1).text()).toContain('↓');
145
+ });
146
+
147
+ it('does not render sort icon for non-sortable columns', () => {
148
+ expect(findSortIconForHeaderAt(2).exists()).toBe(false);
149
+ });
150
+
151
+ describe('changing the active sort column', () => {
152
+ it('hides sorting icon in previous active sort column', async () => {
153
+ await findColHeaderAt(0).trigger('click');
154
+
155
+ expect(findSortIconForHeaderAt(0).classes()).not.toContain('gl-display-none');
156
+ expect(findSortIconForHeaderAt(1).classes()).toContain('gl-display-none');
157
+
158
+ await findColHeaderAt(1).trigger('click');
159
+
160
+ expect(findSortIconForHeaderAt(0).classes()).toContain('gl-display-none');
161
+ expect(findSortIconForHeaderAt(1).classes()).not.toContain('gl-display-none');
162
+ });
163
+ });
115
164
  });
116
165
 
117
166
  describe('when headers are customized via slots', () => {
@@ -121,7 +170,7 @@ describe('GlTable', () => {
121
170
  factory({
122
171
  mountFn: mount,
123
172
  props: {
124
- fields: [field],
173
+ fields,
125
174
  },
126
175
  scopedSlots: {
127
176
  'head(name)': `<div>${customSlotContent}</div>`,
@@ -50,6 +50,7 @@ export const Default = (args, { argTypes }) => ({
50
50
  sort-by="col_three"
51
51
  sort-desc
52
52
  no-sort-reset
53
+ sort-direction="desc"
53
54
  hover
54
55
  selectable
55
56
  selected-variant="primary"
@@ -73,11 +73,22 @@ export default {
73
73
  isSortable({ field }) {
74
74
  return field?.sortable;
75
75
  },
76
+ activeSortingColumn({ field }) {
77
+ return this.localSortBy === field?.key;
78
+ },
76
79
  getSortingIcon({ field }) {
77
- if (this.localSortBy !== field?.key) {
78
- return null;
80
+ if (this.activeSortingColumn({ field })) {
81
+ if (this.localSortDesc) {
82
+ return '↓';
83
+ }
84
+ return '↑';
85
+ }
86
+
87
+ if (this.$attrs['sort-direction'] === 'desc') {
88
+ return '↓';
79
89
  }
80
- return this.localSortDesc ? 'arrow-down' : 'arrow-up';
90
+
91
+ return '↑';
81
92
  },
82
93
  },
83
94
  };
@@ -96,27 +107,21 @@ export default {
96
107
  <slot :name="slotName" v-bind="scope"></slot>
97
108
  </template>
98
109
  <template v-for="headSlotName in headSlots" #[headSlotName]="scope">
99
- <span :key="headSlotName">
110
+ <div :key="headSlotName" class="gl-display-flex">
100
111
  <slot :name="headSlotName" v-bind="scope"
101
112
  ><span>{{ scope.label }}</span></slot
102
113
  ><template v-if="isSortable(scope)">
103
- <span
104
- v-if="getSortingIcon(scope) && getSortingIcon(scope) === 'arrow-up'"
105
- class="gl-ml-3 gl-min-w-5 gl-text-gray-900 gl-text-center"
106
- name="sort-icon"
107
- >
108
-
109
- </span>
110
- <span
111
- v-else-if="getSortingIcon(scope) && getSortingIcon(scope) === 'arrow-down'"
112
- class="gl-ml-3 gl-min-w-5 gl-text-gray-900 gl-text-center"
113
- name="sort-icon"
114
- >
115
-
116
- </span>
117
- <span v-else class="gl-display-inline-block gl-w-5 gl-h-3 gl-ml-3"></span>
114
+ <div class="gl-ml-2 gl-w-5 gl-text-gray-900 gl-display-flex gl-justify-content-center">
115
+ <span
116
+ name="sort-icon"
117
+ data-testid="sort-icon"
118
+ :class="{ 'gl-display-none': !activeSortingColumn(scope) }"
119
+ >
120
+ {{ getSortingIcon(scope) }}
121
+ </span>
122
+ </div>
118
123
  </template>
119
- </span>
124
+ </div>
120
125
  </template>
121
126
  </b-table>
122
127
  </template>
@@ -1,4 +1,5 @@
1
1
  export const CHAT_RESET_MESSAGE = '/reset';
2
+ export const CHAT_CLEAN_MESSAGE = '/clean';
2
3
 
3
4
  export const LOADING_TRANSITION_DURATION = 7500;
4
5
 
@@ -8,30 +8,14 @@ import DuoChatLoader from './components/duo_chat_loader/duo_chat_loader.vue';
8
8
  import DuoChatPredefinedPrompts from './components/duo_chat_predefined_prompts/duo_chat_predefined_prompts.vue';
9
9
  import DuoChatConversation from './components/duo_chat_conversation/duo_chat_conversation.vue';
10
10
  import GlDuoChat from './duo_chat.vue';
11
- import { MOCK_RESPONSE_MESSAGE, MOCK_USER_PROMPT_MESSAGE } from './mock_data';
11
+ import {
12
+ MOCK_RESPONSE_MESSAGE,
13
+ MOCK_USER_PROMPT_MESSAGE,
14
+ SLASH_COMMANDS as slashCommands,
15
+ } from './mock_data';
12
16
 
13
17
  import { MESSAGE_MODEL_ROLES, CHAT_RESET_MESSAGE } from './constants';
14
18
 
15
- const slashCommands = [
16
- {
17
- name: '/reset',
18
- shouldSubmit: true,
19
- description: 'Reset conversation, ignore the previous messages.',
20
- },
21
- {
22
- name: '/tests',
23
- description: 'Write tests for the selected snippet.',
24
- },
25
- {
26
- name: '/refactor',
27
- description: 'Refactor the selected snippet.',
28
- },
29
- {
30
- name: '/explain',
31
- description: 'Explain the selected snippet.',
32
- },
33
- ];
34
-
35
19
  const invalidSlashCommands = [
36
20
  {
37
21
  name: '/foo',
@@ -373,6 +357,28 @@ describe('GlDuoChat', () => {
373
357
  clickSubmit();
374
358
  expect(wrapper.emitted('send-chat-prompt')).toBe(undefined);
375
359
  });
360
+
361
+ it('resets the prompt after form submission', async () => {
362
+ const prompt = 'foo';
363
+ createComponent({ data: { prompt } });
364
+ expect(findChatInput().props('value')).toBe(prompt);
365
+ clickSubmit();
366
+ await nextTick();
367
+ expect(findChatInput().props('value')).toBe('');
368
+ });
369
+
370
+ it('focuses on prompt after form submission', async () => {
371
+ const focusSpy = jest.fn();
372
+ jest.spyOn(HTMLElement.prototype, 'focus').mockImplementation(function focusMockImpl() {
373
+ focusSpy(this);
374
+ });
375
+ createComponent({ data: { prompt: 'TEST!' } });
376
+
377
+ clickSubmit();
378
+ await nextTick();
379
+
380
+ expect(focusSpy).toHaveBeenCalledWith(findChatInput().element);
381
+ });
376
382
  });
377
383
 
378
384
  describe('reset', () => {
@@ -456,20 +462,6 @@ describe('GlDuoChat', () => {
456
462
  expect(findChatComponent().exists()).toBe(true);
457
463
  });
458
464
 
459
- it('resets the prompt when a message is loaded', async () => {
460
- const prompt = 'foo';
461
- createComponent({ data: { prompt } });
462
- expect(findChatInput().props('value')).toBe(prompt);
463
- // setProps is justified here because we are testing the component's
464
- // reactive behavior which consistutes an exception
465
- // See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
466
- wrapper.setProps({
467
- isLoading: true,
468
- });
469
- await nextTick();
470
- expect(findChatInput().props('value')).toBe('');
471
- });
472
-
473
465
  it('renders custom loader when isLoading', () => {
474
466
  createComponent({ propsData: { isLoading: true } });
475
467
  expect(findCustomLoader().exists()).toBe(true);
@@ -773,7 +765,6 @@ describe('GlDuoChat', () => {
773
765
  expect(findSelectedSlashCommand().text()).toContain(command);
774
766
  findChatInput().trigger('keyup', { key: 'Enter' });
775
767
  await nextTick();
776
- expect(findChatInput().props('value')).toBe(`${command}`);
777
768
  expect(wrapper.emitted('send-chat-prompt')).toEqual([[command]]);
778
769
  });
779
770
  });
@@ -827,7 +818,6 @@ describe('GlDuoChat', () => {
827
818
  findSelectedSlashCommand().vm.$emit('click');
828
819
  await nextTick();
829
820
 
830
- expect(findChatInput().props('value')).toBe(slashCommandsNames[commandIndex]);
831
821
  expect(wrapper.emitted('send-chat-prompt')).toEqual([
832
822
  [slashCommandsNames[commandIndex]],
833
823
  ]);
@@ -3,33 +3,15 @@ import GlAlert from '../../../base/alert/alert.vue';
3
3
  import { makeContainer } from '../../../../utils/story_decorators/container';
4
4
  import GlDuoChat from './duo_chat.vue';
5
5
  import readme from './duo_chat.md';
6
+ import { CHAT_CLEAN_MESSAGE } from './constants';
6
7
  import {
7
8
  MOCK_RESPONSE_MESSAGE,
8
9
  MOCK_USER_PROMPT_MESSAGE,
10
+ SLASH_COMMANDS as slashCommands,
9
11
  generateMockResponseChunks,
10
12
  renderGFM,
11
13
  } from './mock_data';
12
14
 
13
- const slashCommands = [
14
- {
15
- name: '/reset',
16
- shouldSubmit: true,
17
- description: 'Reset conversation, ignore the previous messages.',
18
- },
19
- {
20
- name: '/tests',
21
- description: 'Write tests for the selected snippet.',
22
- },
23
- {
24
- name: '/refactor',
25
- description: 'Refactor the selected snippet.',
26
- },
27
- {
28
- name: '/explain',
29
- description: 'Explain the selected snippet.',
30
- },
31
- ];
32
-
33
15
  const defaultValue = (prop) =>
34
16
  typeof GlDuoChat.props[prop].default === 'function'
35
17
  ? GlDuoChat.props[prop].default()
@@ -120,8 +102,12 @@ export const Interactive = (args, { argTypes }) => ({
120
102
  requestId: this.requestId,
121
103
  };
122
104
  this.loggerInfo += `New prompt: ${JSON.stringify(newPrompt)}\n\n`;
123
- this.msgs.push(newPrompt);
124
- this.promptInFlight = true;
105
+ if (prompt === CHAT_CLEAN_MESSAGE) {
106
+ this.msgs = [];
107
+ } else {
108
+ this.msgs.push(newPrompt);
109
+ this.promptInFlight = true;
110
+ }
125
111
  },
126
112
  onChatHidden() {
127
113
  this.isHidden = true;
@@ -204,6 +190,8 @@ export const Interactive = (args, { argTypes }) => ({
204
190
  :empty-state-title="emptyStateTitle"
205
191
  :empty-state-description="emptyStateDescription"
206
192
  :chat-prompt-placeholder="chatPromptPlaceholder"
193
+ :slash-commands="slashCommands"
194
+ class="gl-drawer-default"
207
195
  @send-chat-prompt="onSendChatPrompt"
208
196
  @chat-hidden="onChatHidden"
209
197
  />
@@ -253,6 +241,7 @@ export const Slots = (args, { argTypes }) => ({
253
241
  </div>
254
242
  `,
255
243
  });
244
+ Slots.args = generateProps();
256
245
  Slots.decorators = [makeContainer({ height: '800px' })];
257
246
 
258
247
  export default {
@@ -253,13 +253,9 @@ export default {
253
253
  },
254
254
  },
255
255
  watch: {
256
- isLoading(newVal) {
256
+ isLoading() {
257
257
  this.isHidden = false;
258
258
  this.scrollToBottom();
259
- if (newVal) {
260
- // We reset the prompt when we start getting the response and focus in the prompt field
261
- this.setPromptAndFocus();
262
- }
263
259
  },
264
260
  },
265
261
  created() {
@@ -290,6 +286,7 @@ export default {
290
286
  * @param {String} prompt The user prompt to send.
291
287
  */
292
288
  this.$emit('send-chat-prompt', this.prompt);
289
+ this.setPromptAndFocus();
293
290
  }
294
291
  },
295
292
  sendPredefinedPrompt(prompt) {
@@ -1,5 +1,10 @@
1
1
  import { setStoryTimeout } from '../../../../utils/test_utils';
2
- import { DOCUMENTATION_SOURCE_TYPES, MESSAGE_MODEL_ROLES } from './constants';
2
+ import {
3
+ DOCUMENTATION_SOURCE_TYPES,
4
+ MESSAGE_MODEL_ROLES,
5
+ CHAT_RESET_MESSAGE,
6
+ CHAT_CLEAN_MESSAGE,
7
+ } from './constants';
3
8
 
4
9
  const MOCK_SOURCES = [
5
10
  {
@@ -119,3 +124,28 @@ export const renderGFM = (el) => {
119
124
  block?.classList.add('gl-markdown', 'gl-compact-markdown');
120
125
  });
121
126
  };
127
+
128
+ export const SLASH_COMMANDS = [
129
+ {
130
+ name: CHAT_RESET_MESSAGE,
131
+ shouldSubmit: true,
132
+ description: 'Reset conversation, ignore the previous messages.',
133
+ },
134
+ {
135
+ name: CHAT_CLEAN_MESSAGE,
136
+ shouldSubmit: true,
137
+ description: 'Delete all messages in this conversation.',
138
+ },
139
+ {
140
+ name: '/tests',
141
+ description: 'Write tests for the selected snippet.',
142
+ },
143
+ {
144
+ name: '/refactor',
145
+ description: 'Refactor the selected snippet.',
146
+ },
147
+ {
148
+ name: '/explain',
149
+ description: 'Explain the selected snippet.',
150
+ },
151
+ ];