@gitlab/duo-ui 8.6.0 → 8.7.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/chat/components/duo_chat_threads/duo_chat_threads.js +8 -4
- package/dist/components/chat/duo_chat.js +19 -1
- package/dist/utils/date.js +32 -9
- package/package.json +1 -1
- package/src/components/chat/components/duo_chat_threads/duo_chat_threads.vue +8 -4
- package/src/components/chat/duo_chat.vue +20 -0
- package/src/utils/date.js +41 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
# [8.7.0](https://gitlab.com/gitlab-org/duo-ui/compare/v8.6.0...v8.7.0) (2025-03-11)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* adapt default to BCP 47 ([7786272](https://gitlab.com/gitlab-org/duo-ui/commit/7786272f35b6447bda958ac01fc6922d0fb4c102))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* improve date formatting with localization support ([24c2548](https://gitlab.com/gitlab-org/duo-ui/commit/24c2548b2576fd70e5e93511f7107d965dd6361d))
|
|
12
|
+
|
|
1
13
|
# [8.6.0](https://gitlab.com/gitlab-org/duo-ui/compare/v8.5.0...v8.6.0) (2025-03-11)
|
|
2
14
|
|
|
3
15
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { GlButton, GlIcon } from '@gitlab/ui';
|
|
2
2
|
import { translate } from '../../../../utils/i18n';
|
|
3
|
-
import {
|
|
3
|
+
import { formatLocalizedDate } from '../../../../utils/date';
|
|
4
4
|
import DuoChatThreadsEmpty from './duo_chat_threads_empty';
|
|
5
5
|
import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
|
|
6
6
|
|
|
@@ -19,11 +19,15 @@ var script = {
|
|
|
19
19
|
threads: {
|
|
20
20
|
type: Array,
|
|
21
21
|
required: true
|
|
22
|
+
},
|
|
23
|
+
preferredLocale: {
|
|
24
|
+
type: Array,
|
|
25
|
+
required: true
|
|
22
26
|
}
|
|
23
27
|
},
|
|
24
28
|
computed: {
|
|
25
|
-
|
|
26
|
-
return
|
|
29
|
+
formattedLocalDate() {
|
|
30
|
+
return date => formatLocalizedDate(date, this.preferredLocale);
|
|
27
31
|
},
|
|
28
32
|
groupedThreads() {
|
|
29
33
|
return this.threads.reduce((threadsGroupedByDate, thread) => {
|
|
@@ -61,7 +65,7 @@ var script = {
|
|
|
61
65
|
const __vue_script__ = script;
|
|
62
66
|
|
|
63
67
|
/* template */
|
|
64
|
-
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:"gl-h-full gl-p-5"},[_c('div',{staticClass:"gl-bg-gray-50 gl-text-gray-500 gl-p-4 gl-mb-5 gl-rounded-base",attrs:{"data-testid":"chat-threads-info-banner"}},[_c('p',{staticClass:"gl-m-0 gl-flex"},[_c('gl-icon',{staticClass:"gl-mr-4",attrs:{"name":"bulb"}}),_vm._v(_vm._s(_vm.$options.i18n.CHAT_HISTORY_INFO)+"\n ")],1)]),_vm._v(" "),(_vm.hasThreads)?_vm._l((_vm.groupedThreads),function(threadsForDate,date){return _c('div',{key:date},[_c('div',{staticClass:"gl-font-bold gl-neutral-900 gl-mb-4",attrs:{"data-testid":"chat-threads-date-header"}},[_vm._v("\n "+_vm._s(_vm.
|
|
68
|
+
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:"gl-h-full gl-p-5"},[_c('div',{staticClass:"gl-bg-gray-50 gl-text-gray-500 gl-p-4 gl-mb-5 gl-rounded-base",attrs:{"data-testid":"chat-threads-info-banner"}},[_c('p',{staticClass:"gl-m-0 gl-flex"},[_c('gl-icon',{staticClass:"gl-mr-4",attrs:{"name":"bulb"}}),_vm._v(_vm._s(_vm.$options.i18n.CHAT_HISTORY_INFO)+"\n ")],1)]),_vm._v(" "),(_vm.hasThreads)?_vm._l((_vm.groupedThreads),function(threadsForDate,date){return _c('div',{key:date},[_c('div',{staticClass:"gl-font-bold gl-neutral-900 gl-mb-4",attrs:{"data-testid":"chat-threads-date-header"}},[_vm._v("\n "+_vm._s(_vm.formattedLocalDate(date))+"\n ")]),_vm._v(" "),_c('div',_vm._l((threadsForDate),function(thread){return _c('div',{key:thread.id,staticClass:"gl-flex gl-align-center gl-mb-4"},[_c('div',{staticClass:"thread-box hover:gl-bg-gray-50 focus:gl-bg-gray-50 gl-text-ellipsis gl-overflow-hidden gl-rounded-base gl-cursor-pointer gl-rounded-base gl-p-4 gl-w-full gl-whitespace-nowrap",attrs:{"tabindex":"0","data-testid":"chat-threads-thread-box"},on:{"click":function($event){return _vm.onSelectThread(thread)}}},[_vm._v("\n "+_vm._s(thread.title || 'Untitled Chat')+"\n ")]),_vm._v(" "),_c('gl-button',{staticClass:"gl-neutral-900 !gl-p-4",attrs:{"data-testid":"chat-threads-delete-thread-button","icon":"remove","category":"tertiary","size":"small","aria-label":_vm.$options.i18n.THREAD_DELETE_LABEL},on:{"click":function($event){return _vm.$emit('delete-thread', thread.id)}}})],1)}),0)])}):_c('duo-chat-threads-empty')],2)};
|
|
65
69
|
var __vue_staticRenderFns__ = [];
|
|
66
70
|
|
|
67
71
|
/* style */
|
|
@@ -33,6 +33,14 @@ const isThread = thread => typeof thread === 'object' && typeof thread.id === 's
|
|
|
33
33
|
|
|
34
34
|
// eslint-disable-next-line unicorn/no-array-callback-reference
|
|
35
35
|
const threadListValidator = threads => threads.every(isThread);
|
|
36
|
+
const localeValidator = value => {
|
|
37
|
+
try {
|
|
38
|
+
Intl.getCanonicalLocales(value);
|
|
39
|
+
return true;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
36
44
|
var script = {
|
|
37
45
|
name: 'DuoChat',
|
|
38
46
|
components: {
|
|
@@ -232,6 +240,16 @@ var script = {
|
|
|
232
240
|
type: Boolean,
|
|
233
241
|
required: false,
|
|
234
242
|
default: false
|
|
243
|
+
},
|
|
244
|
+
/**
|
|
245
|
+
* The preferred locale for the chat interface.
|
|
246
|
+
* Follows BCP 47 language tag format (e.g., 'en-US', 'fr-FR', 'es-ES').
|
|
247
|
+
*/
|
|
248
|
+
preferredLocale: {
|
|
249
|
+
type: Array,
|
|
250
|
+
required: false,
|
|
251
|
+
default: () => ['en-US', 'en'],
|
|
252
|
+
validator: localeValidator
|
|
235
253
|
}
|
|
236
254
|
},
|
|
237
255
|
data() {
|
|
@@ -574,7 +592,7 @@ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=
|
|
|
574
592
|
},attrs:{"width":_vm.shouldRenderResizable ? _vm.dimensions.width : null,"height":_vm.shouldRenderResizable ? _vm.dimensions.height : null,"max-width":_vm.shouldRenderResizable ? _vm.dimensions.maxWidth : null,"max-height":_vm.shouldRenderResizable ? _vm.dimensions.maxHeight : null,"min-width":_vm.shouldRenderResizable ? _vm.dimensions.minWidth : null,"left":_vm.shouldRenderResizable ? _vm.dimensions.left : null,"top":_vm.shouldRenderResizable ? _vm.dimensions.top : null,"fit-parent":true,"min-height":_vm.shouldRenderResizable ? _vm.dimensions.minHeight : null,"active":_vm.shouldRenderResizable ? ['l', 't', 'lt'] : null},on:{"resize:end":_vm.updateSize}},[(!_vm.isHidden)?_c('aside',{staticClass:"markdown-code-block duo-chat gl-bottom-0 gl-max-h-full",class:{
|
|
575
593
|
'resizable-content': _vm.shouldRenderResizable,
|
|
576
594
|
'duo-chat-drawer': !_vm.shouldRenderResizable,
|
|
577
|
-
},attrs:{"id":"chat-component","role":"complementary","data-testid":"chat-component"}},[(_vm.showHeader)?_c('duo-chat-header',{attrs:{"active-thread-id":_vm.activeThreadId,"title":_vm.isMultithreaded && _vm.currentView === 'list' ? _vm.$options.i18n.CHAT_HISTORY_TITLE : _vm.title,"error":_vm.error,"is-multithreaded":_vm.isMultithreaded,"current-view":_vm.currentView,"should-render-resizable":_vm.shouldRenderResizable,"badge-type":_vm.isMultithreaded ? null : _vm.badgeType},on:{"go-back":_vm.onGoBack,"new-chat":_vm.onNewChat,"close":_vm.hideChat},scopedSlots:_vm._u([{key:"subheader",fn:function(){return [_vm._t("subheader")]},proxy:true}],null,true)}):_vm._e(),_vm._v(" "),(_vm.shouldShowThreadList)?_c('div',{staticClass:"gl-h-full"},[_c('duo-chat-threads',{attrs:{"threads":_vm.threadList},on:{"new-chat":_vm.onNewChat,"select-thread":_vm.onSelectThread,"delete-thread":_vm.onDeleteThread,"close":_vm.hideChat}})],1):_c('span',{staticClass:"gl-h-full gl-flex gl-flex-col gl-justify-end"},[_c('div',{staticClass:"duo-chat-drawer-body gl-bg-default",attrs:{"data-testid":"chat-history"},on:{"scroll":_vm.handleScrollingTrottled}},[_c('transition-group',{staticClass:"duo-chat-history gl-flex gl-flex-col gl-justify-end",class:[
|
|
595
|
+
},attrs:{"id":"chat-component","role":"complementary","data-testid":"chat-component"}},[(_vm.showHeader)?_c('duo-chat-header',{attrs:{"active-thread-id":_vm.activeThreadId,"title":_vm.isMultithreaded && _vm.currentView === 'list' ? _vm.$options.i18n.CHAT_HISTORY_TITLE : _vm.title,"error":_vm.error,"is-multithreaded":_vm.isMultithreaded,"current-view":_vm.currentView,"should-render-resizable":_vm.shouldRenderResizable,"badge-type":_vm.isMultithreaded ? null : _vm.badgeType},on:{"go-back":_vm.onGoBack,"new-chat":_vm.onNewChat,"close":_vm.hideChat},scopedSlots:_vm._u([{key:"subheader",fn:function(){return [_vm._t("subheader")]},proxy:true}],null,true)}):_vm._e(),_vm._v(" "),(_vm.shouldShowThreadList)?_c('div',{staticClass:"gl-h-full"},[_c('duo-chat-threads',{attrs:{"threads":_vm.threadList,"preferred-locale":_vm.preferredLocale},on:{"new-chat":_vm.onNewChat,"select-thread":_vm.onSelectThread,"delete-thread":_vm.onDeleteThread,"close":_vm.hideChat}})],1):_c('span',{staticClass:"gl-h-full gl-flex gl-flex-col gl-justify-end"},[_c('div',{staticClass:"duo-chat-drawer-body gl-bg-default",attrs:{"data-testid":"chat-history"},on:{"scroll":_vm.handleScrollingTrottled}},[_c('transition-group',{staticClass:"duo-chat-history gl-flex gl-flex-col gl-justify-end",class:[
|
|
578
596
|
{
|
|
579
597
|
'gl-h-full': !_vm.hasMessages,
|
|
580
598
|
'force-scroll-bar': _vm.hasMessages,
|
package/dist/utils/date.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const DEFAULT_LOCALE = ['en-US'];
|
|
1
2
|
function getOrdinalSuffix(day) {
|
|
2
3
|
if (day > 3 && day < 21) return 'th';
|
|
3
4
|
switch (day % 10) {
|
|
@@ -11,17 +12,39 @@ function getOrdinalSuffix(day) {
|
|
|
11
12
|
return 'th';
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
+
const isValidDateString = dateStr => {
|
|
16
|
+
if (!dateStr) return false;
|
|
15
17
|
const date = new Date(dateStr);
|
|
18
|
+
return date.toString() !== 'Invalid Date';
|
|
19
|
+
};
|
|
20
|
+
const getValidLocale = preferredLocale => {
|
|
21
|
+
const locale = preferredLocale !== null && preferredLocale !== void 0 && preferredLocale.length ? preferredLocale : DEFAULT_LOCALE;
|
|
22
|
+
const supportedLocales = Intl.DateTimeFormat.supportedLocalesOf(locale);
|
|
23
|
+
return supportedLocales.length ? supportedLocales : DEFAULT_LOCALE;
|
|
24
|
+
};
|
|
25
|
+
const createDateFormatter = locale => {
|
|
26
|
+
return new Intl.DateTimeFormat(locale, {
|
|
27
|
+
month: 'long',
|
|
28
|
+
day: 'numeric',
|
|
29
|
+
year: 'numeric'
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
const formatEnglishDate = (date, formatter) => {
|
|
16
33
|
const day = date.getDate();
|
|
17
34
|
const suffix = getOrdinalSuffix(day);
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
35
|
+
const parts = formatter.formatToParts(date);
|
|
36
|
+
const formattedParts = parts.map(part => part.type === 'day' ? `${part.value}${suffix}` : part.value);
|
|
37
|
+
return formattedParts.join('');
|
|
38
|
+
};
|
|
39
|
+
function formatLocalizedDate(dateStr, preferredLocale) {
|
|
40
|
+
var _validLocale$;
|
|
41
|
+
if (!isValidDateString(dateStr)) {
|
|
42
|
+
return 'Invalid Date';
|
|
43
|
+
}
|
|
44
|
+
const date = new Date(dateStr);
|
|
45
|
+
const validLocale = getValidLocale(preferredLocale);
|
|
46
|
+
const formatter = createDateFormatter(validLocale);
|
|
47
|
+
return (_validLocale$ = validLocale[0]) !== null && _validLocale$ !== void 0 && _validLocale$.startsWith('en') ? formatEnglishDate(date, formatter) : formatter.format(date);
|
|
25
48
|
}
|
|
26
49
|
|
|
27
|
-
export {
|
|
50
|
+
export { formatLocalizedDate, getOrdinalSuffix };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { GlButton, GlIcon } from '@gitlab/ui';
|
|
3
3
|
import { translate } from '../../../../utils/i18n';
|
|
4
|
-
import {
|
|
4
|
+
import { formatLocalizedDate } from '../../../../utils/date';
|
|
5
5
|
import DuoChatThreadsEmpty from './duo_chat_threads_empty.vue';
|
|
6
6
|
|
|
7
7
|
const i18n = {
|
|
@@ -26,11 +26,15 @@ export default {
|
|
|
26
26
|
type: Array,
|
|
27
27
|
required: true,
|
|
28
28
|
},
|
|
29
|
+
preferredLocale: {
|
|
30
|
+
type: Array,
|
|
31
|
+
required: true,
|
|
32
|
+
},
|
|
29
33
|
},
|
|
30
34
|
|
|
31
35
|
computed: {
|
|
32
|
-
|
|
33
|
-
return
|
|
36
|
+
formattedLocalDate() {
|
|
37
|
+
return (date) => formatLocalizedDate(date, this.preferredLocale);
|
|
34
38
|
},
|
|
35
39
|
|
|
36
40
|
groupedThreads() {
|
|
@@ -86,7 +90,7 @@ export default {
|
|
|
86
90
|
<template v-if="hasThreads">
|
|
87
91
|
<div v-for="(threadsForDate, date) in groupedThreads" :key="date">
|
|
88
92
|
<div data-testid="chat-threads-date-header" class="gl-font-bold gl-neutral-900 gl-mb-4">
|
|
89
|
-
{{
|
|
93
|
+
{{ formattedLocalDate(date) }}
|
|
90
94
|
</div>
|
|
91
95
|
|
|
92
96
|
<div>
|
|
@@ -86,6 +86,15 @@ const isThread = (thread) =>
|
|
|
86
86
|
// eslint-disable-next-line unicorn/no-array-callback-reference
|
|
87
87
|
const threadListValidator = (threads) => threads.every(isThread);
|
|
88
88
|
|
|
89
|
+
const localeValidator = (value) => {
|
|
90
|
+
try {
|
|
91
|
+
Intl.getCanonicalLocales(value);
|
|
92
|
+
return true;
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
89
98
|
export default {
|
|
90
99
|
name: 'DuoChat',
|
|
91
100
|
components: {
|
|
@@ -287,6 +296,16 @@ export default {
|
|
|
287
296
|
required: false,
|
|
288
297
|
default: false,
|
|
289
298
|
},
|
|
299
|
+
/**
|
|
300
|
+
* The preferred locale for the chat interface.
|
|
301
|
+
* Follows BCP 47 language tag format (e.g., 'en-US', 'fr-FR', 'es-ES').
|
|
302
|
+
*/
|
|
303
|
+
preferredLocale: {
|
|
304
|
+
type: Array,
|
|
305
|
+
required: false,
|
|
306
|
+
default: () => ['en-US', 'en'],
|
|
307
|
+
validator: localeValidator,
|
|
308
|
+
},
|
|
290
309
|
},
|
|
291
310
|
data() {
|
|
292
311
|
return {
|
|
@@ -686,6 +705,7 @@ export default {
|
|
|
686
705
|
<div v-if="shouldShowThreadList" class="gl-h-full">
|
|
687
706
|
<duo-chat-threads
|
|
688
707
|
:threads="threadList"
|
|
708
|
+
:preferred-locale="preferredLocale"
|
|
689
709
|
@new-chat="onNewChat"
|
|
690
710
|
@select-thread="onSelectThread"
|
|
691
711
|
@delete-thread="onDeleteThread"
|
package/src/utils/date.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const DEFAULT_LOCALE = ['en-US'];
|
|
2
|
+
|
|
1
3
|
export function getOrdinalSuffix(day) {
|
|
2
4
|
if (day > 3 && day < 21) return 'th';
|
|
3
5
|
switch (day % 10) {
|
|
@@ -12,13 +14,48 @@ export function getOrdinalSuffix(day) {
|
|
|
12
14
|
}
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
const isValidDateString = (dateStr) => {
|
|
18
|
+
if (!dateStr) return false;
|
|
16
19
|
const date = new Date(dateStr);
|
|
20
|
+
return date.toString() !== 'Invalid Date';
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const getValidLocale = (preferredLocale) => {
|
|
24
|
+
const locale = preferredLocale?.length ? preferredLocale : DEFAULT_LOCALE;
|
|
25
|
+
const supportedLocales = Intl.DateTimeFormat.supportedLocalesOf(locale);
|
|
26
|
+
return supportedLocales.length ? supportedLocales : DEFAULT_LOCALE;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const createDateFormatter = (locale) => {
|
|
30
|
+
return new Intl.DateTimeFormat(locale, {
|
|
31
|
+
month: 'long',
|
|
32
|
+
day: 'numeric',
|
|
33
|
+
year: 'numeric',
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const formatEnglishDate = (date, formatter) => {
|
|
17
38
|
const day = date.getDate();
|
|
18
39
|
const suffix = getOrdinalSuffix(day);
|
|
19
40
|
|
|
20
|
-
const
|
|
21
|
-
const
|
|
41
|
+
const parts = formatter.formatToParts(date);
|
|
42
|
+
const formattedParts = parts.map((part) =>
|
|
43
|
+
part.type === 'day' ? `${part.value}${suffix}` : part.value
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return formattedParts.join('');
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function formatLocalizedDate(dateStr, preferredLocale) {
|
|
50
|
+
if (!isValidDateString(dateStr)) {
|
|
51
|
+
return 'Invalid Date';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const date = new Date(dateStr);
|
|
55
|
+
const validLocale = getValidLocale(preferredLocale);
|
|
56
|
+
const formatter = createDateFormatter(validLocale);
|
|
22
57
|
|
|
23
|
-
return
|
|
58
|
+
return validLocale[0]?.startsWith('en')
|
|
59
|
+
? formatEnglishDate(date, formatter)
|
|
60
|
+
: formatter.format(date);
|
|
24
61
|
}
|