feat: Move timeMixin to a helper (#9799)
# Pull Request Template ## Description This PR will replace the usage of `timeMixin` with `timeHelper` Fixes https://linear.app/chatwoot/issue/CW-3451/move-time-mixin-to-a-helper ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? Please refer to this issue description. https://linear.app/chatwoot/issue/CW-3451/move-time-mixin-to-a-helper ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
parent
84c380c8c4
commit
79381b08cc
@ -122,7 +122,6 @@ import ChatListHeader from './ChatListHeader.vue';
|
|||||||
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
|
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
|
||||||
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
|
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
|
||||||
import ConversationItem from './ConversationItem.vue';
|
import ConversationItem from './ConversationItem.vue';
|
||||||
import timeMixin from '../mixins/time';
|
|
||||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import conversationMixin from '../mixins/conversations';
|
import conversationMixin from '../mixins/conversations';
|
||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
@ -159,7 +158,6 @@ export default {
|
|||||||
VirtualList,
|
VirtualList,
|
||||||
},
|
},
|
||||||
mixins: [
|
mixins: [
|
||||||
timeMixin,
|
|
||||||
conversationMixin,
|
conversationMixin,
|
||||||
keyboardEventListenerMixins,
|
keyboardEventListenerMixins,
|
||||||
alertMixin,
|
alertMixin,
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
delay: { show: 1500, hide: 0 },
|
delay: { show: 1500, hide: 0 },
|
||||||
hideOnClick: true,
|
hideOnClick: true,
|
||||||
}"
|
}"
|
||||||
class="text-xxs text-slate-500 dark:text-slate-500 leading-4 ml-auto hover:text-slate-900 dark:hover:text-slate-100"
|
class="ml-auto leading-4 text-xxs text-slate-500 dark:text-slate-500 hover:text-slate-900 dark:hover:text-slate-100"
|
||||||
>
|
>
|
||||||
<span>{{ `${createdAtTime} • ${lastActivityTime}` }}</span>
|
<span>{{ `${createdAtTime} • ${lastActivityTime}` }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -16,11 +16,14 @@ const MINUTE_IN_MILLI_SECONDS = 60000;
|
|||||||
const HOUR_IN_MILLI_SECONDS = MINUTE_IN_MILLI_SECONDS * 60;
|
const HOUR_IN_MILLI_SECONDS = MINUTE_IN_MILLI_SECONDS * 60;
|
||||||
const DAY_IN_MILLI_SECONDS = HOUR_IN_MILLI_SECONDS * 24;
|
const DAY_IN_MILLI_SECONDS = HOUR_IN_MILLI_SECONDS * 24;
|
||||||
|
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import {
|
||||||
|
dynamicTime,
|
||||||
|
dateFormat,
|
||||||
|
shortTimestamp,
|
||||||
|
} from 'shared/helpers/timeHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TimeAgo',
|
name: 'TimeAgo',
|
||||||
mixins: [timeMixin],
|
|
||||||
props: {
|
props: {
|
||||||
isAutoRefreshEnabled: {
|
isAutoRefreshEnabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -37,17 +40,17 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
lastActivityAtTimeAgo: this.dynamicTime(this.lastActivityTimestamp),
|
lastActivityAtTimeAgo: dynamicTime(this.lastActivityTimestamp),
|
||||||
createdAtTimeAgo: this.dynamicTime(this.createdAtTimestamp),
|
createdAtTimeAgo: dynamicTime(this.createdAtTimestamp),
|
||||||
timer: null,
|
timer: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
lastActivityTime() {
|
lastActivityTime() {
|
||||||
return this.shortTimestamp(this.lastActivityAtTimeAgo);
|
return shortTimestamp(this.lastActivityAtTimeAgo);
|
||||||
},
|
},
|
||||||
createdAtTime() {
|
createdAtTime() {
|
||||||
return this.shortTimestamp(this.createdAtTimeAgo);
|
return shortTimestamp(this.createdAtTimeAgo);
|
||||||
},
|
},
|
||||||
createdAt() {
|
createdAt() {
|
||||||
const createdTimeDiff = Date.now() - this.createdAtTimestamp * 1000;
|
const createdTimeDiff = Date.now() - this.createdAtTimestamp * 1000;
|
||||||
@ -56,9 +59,9 @@ export default {
|
|||||||
? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.LATEST')} ${
|
? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.LATEST')} ${
|
||||||
this.createdAtTimeAgo
|
this.createdAtTimeAgo
|
||||||
}`
|
}`
|
||||||
: `${this.$t(
|
: `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST')} ${dateFormat(
|
||||||
'CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST'
|
this.createdAtTimestamp
|
||||||
)} ${this.dateFormat(this.createdAtTimestamp)}`;
|
)}`;
|
||||||
},
|
},
|
||||||
lastActivity() {
|
lastActivity() {
|
||||||
const lastActivityTimeDiff =
|
const lastActivityTimeDiff =
|
||||||
@ -70,7 +73,7 @@ export default {
|
|||||||
}`
|
}`
|
||||||
: `${this.$t(
|
: `${this.$t(
|
||||||
'CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.NOT_ACTIVE'
|
'CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.NOT_ACTIVE'
|
||||||
)} ${this.dateFormat(this.lastActivityTimestamp)}`;
|
)} ${dateFormat(this.lastActivityTimestamp)}`;
|
||||||
},
|
},
|
||||||
tooltipText() {
|
tooltipText() {
|
||||||
return `${this.createdAt}
|
return `${this.createdAt}
|
||||||
@ -79,10 +82,10 @@ export default {
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
lastActivityTimestamp() {
|
lastActivityTimestamp() {
|
||||||
this.lastActivityAtTimeAgo = this.dynamicTime(this.lastActivityTimestamp);
|
this.lastActivityAtTimeAgo = dynamicTime(this.lastActivityTimestamp);
|
||||||
},
|
},
|
||||||
createdAtTimestamp() {
|
createdAtTimestamp() {
|
||||||
this.createdAtTimeAgo = this.dynamicTime(this.createdAtTimestamp);
|
this.createdAtTimeAgo = dynamicTime(this.createdAtTimestamp);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -96,10 +99,8 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
createTimer() {
|
createTimer() {
|
||||||
this.timer = setTimeout(() => {
|
this.timer = setTimeout(() => {
|
||||||
this.lastActivityAtTimeAgo = this.dynamicTime(
|
this.lastActivityAtTimeAgo = dynamicTime(this.lastActivityTimestamp);
|
||||||
this.lastActivityTimestamp
|
this.createdAtTimeAgo = dynamicTime(this.createdAtTimestamp);
|
||||||
);
|
|
||||||
this.createdAtTimeAgo = this.dynamicTime(this.createdAtTimestamp);
|
|
||||||
this.createTimer();
|
this.createTimer();
|
||||||
}, this.refreshTime());
|
}, this.refreshTime());
|
||||||
},
|
},
|
||||||
|
|||||||
@ -120,7 +120,6 @@ import { mapGetters } from 'vuex';
|
|||||||
import Thumbnail from '../Thumbnail.vue';
|
import Thumbnail from '../Thumbnail.vue';
|
||||||
import MessagePreview from './MessagePreview.vue';
|
import MessagePreview from './MessagePreview.vue';
|
||||||
import conversationMixin from '../../../mixins/conversations';
|
import conversationMixin from '../../../mixins/conversations';
|
||||||
import timeMixin from '../../../mixins/time';
|
|
||||||
import router from '../../../routes';
|
import router from '../../../routes';
|
||||||
import { frontendURL, conversationUrl } from '../../../helper/URLHelper';
|
import { frontendURL, conversationUrl } from '../../../helper/URLHelper';
|
||||||
import InboxName from '../InboxName.vue';
|
import InboxName from '../InboxName.vue';
|
||||||
@ -144,7 +143,7 @@ export default {
|
|||||||
SLACardLabel,
|
SLACardLabel,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [inboxMixin, timeMixin, conversationMixin, alertMixin],
|
mixins: [inboxMixin, conversationMixin, alertMixin],
|
||||||
props: {
|
props: {
|
||||||
activeLabel: {
|
activeLabel: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@ -77,10 +77,10 @@
|
|||||||
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
||||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import timeMixin from '../../../../mixins/time';
|
import { messageTimestamp } from 'shared/helpers/timeHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [inboxMixin, timeMixin],
|
mixins: [inboxMixin],
|
||||||
props: {
|
props: {
|
||||||
sender: {
|
sender: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -159,7 +159,7 @@ export default {
|
|||||||
return MESSAGE_STATUS.SENT === this.messageStatus;
|
return MESSAGE_STATUS.SENT === this.messageStatus;
|
||||||
},
|
},
|
||||||
readableTime() {
|
readableTime() {
|
||||||
return this.messageTimestamp(this.createdAt, 'LLL d, h:mm a');
|
return messageTimestamp(this.createdAt, 'LLL d, h:mm a');
|
||||||
},
|
},
|
||||||
screenName() {
|
screenName() {
|
||||||
const { additional_attributes: additionalAttributes = {} } =
|
const { additional_attributes: additionalAttributes = {} } =
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
@click="onClose"
|
@click="onClose"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="bg-white dark:bg-slate-900 z-10 flex items-center justify-between w-full h-16 px-6 py-2"
|
class="z-10 flex items-center justify-between w-full h-16 px-6 py-2 bg-white dark:bg-slate-900"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -167,7 +167,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-center w-full h-16 px-6 py-2 z-10">
|
<div class="z-10 flex items-center justify-center w-full h-16 px-6 py-2">
|
||||||
<div
|
<div
|
||||||
class="items-center rounded-sm flex font-semibold justify-center min-w-[5rem] p-1 bg-slate-25 dark:bg-slate-800 text-slate-600 dark:text-slate-200 text-sm"
|
class="items-center rounded-sm flex font-semibold justify-center min-w-[5rem] p-1 bg-slate-25 dark:bg-slate-800 text-slate-600 dark:text-slate-200 text-sm"
|
||||||
>
|
>
|
||||||
@ -182,7 +182,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import { messageTimestamp } from 'shared/helpers/timeHelper';
|
||||||
|
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
|
|
||||||
@ -199,7 +199,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
},
|
},
|
||||||
mixins: [keyboardEventListenerMixins, timeMixin],
|
mixins: [keyboardEventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
show: {
|
show: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -236,7 +236,7 @@ export default {
|
|||||||
readableTime() {
|
readableTime() {
|
||||||
const { created_at: createdAt } = this.activeAttachment;
|
const { created_at: createdAt } = this.activeAttachment;
|
||||||
if (!createdAt) return '';
|
if (!createdAt) return '';
|
||||||
return this.messageTimestamp(createdAt, 'LLL d yyyy, h:mm a') || '';
|
return messageTimestamp(createdAt, 'LLL d yyyy, h:mm a') || '';
|
||||||
},
|
},
|
||||||
isImage() {
|
isImage() {
|
||||||
return this.activeFileType === ALLOWED_FILE_TYPES.IMAGE;
|
return this.activeFileType === ALLOWED_FILE_TYPES.IMAGE;
|
||||||
|
|||||||
@ -1,128 +0,0 @@
|
|||||||
import TimeMixin from '../time';
|
|
||||||
|
|
||||||
describe('#messageStamp', () => {
|
|
||||||
it('returns correct value', () => {
|
|
||||||
expect(TimeMixin.methods.messageStamp(1612971343)).toEqual('3:35 PM');
|
|
||||||
expect(TimeMixin.methods.messageStamp(1612971343, 'LLL d, h:mm a')).toEqual(
|
|
||||||
'Feb 10, 3:35 PM'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#messageTimestamp', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.useFakeTimers('modern');
|
|
||||||
|
|
||||||
const mockDate = new Date(2023, 4, 5);
|
|
||||||
vi.setSystemTime(mockDate);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the message date in the specified format if the message was sent in the current year', () => {
|
|
||||||
expect(TimeMixin.methods.messageTimestamp(1680777464)).toEqual(
|
|
||||||
'Apr 6, 2023'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it('should return the message date and time in a different format if the message was sent in a different year', () => {
|
|
||||||
expect(TimeMixin.methods.messageTimestamp(1612971343)).toEqual(
|
|
||||||
'Feb 10 2021, 3:35 PM'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#dynamicTime', () => {
|
|
||||||
it('returns correct value', () => {
|
|
||||||
Date.now = vi.fn(() => new Date(Date.UTC(2023, 1, 14)).valueOf());
|
|
||||||
expect(TimeMixin.methods.dynamicTime(1612971343)).toEqual(
|
|
||||||
'about 2 years ago'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#dateFormat', () => {
|
|
||||||
it('returns correct value', () => {
|
|
||||||
expect(TimeMixin.methods.dateFormat(1612971343)).toEqual('Feb 10, 2021');
|
|
||||||
expect(TimeMixin.methods.dateFormat(1612971343, 'LLL d, yyyy')).toEqual(
|
|
||||||
'Feb 10, 2021'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#shortTimestamp', () => {
|
|
||||||
// Test cases when withAgo is false or not provided
|
|
||||||
it('returns correct value without ago', () => {
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('less than a minute ago')).toEqual(
|
|
||||||
'now'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('1 minute ago')).toEqual('1m');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('12 minutes ago')).toEqual('12m');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('a minute ago')).toEqual('1m');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('an hour ago')).toEqual('1h');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('1 hour ago')).toEqual('1h');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('2 hours ago')).toEqual('2h');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('1 day ago')).toEqual('1d');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('a day ago')).toEqual('1d');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('3 days ago')).toEqual('3d');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('a month ago')).toEqual('1mo');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('1 month ago')).toEqual('1mo');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('2 months ago')).toEqual('2mo');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('a year ago')).toEqual('1y');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('1 year ago')).toEqual('1y');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('4 years ago')).toEqual('4y');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test cases when withAgo is true
|
|
||||||
it('returns correct value with ago', () => {
|
|
||||||
expect(
|
|
||||||
TimeMixin.methods.shortTimestamp('less than a minute ago', true)
|
|
||||||
).toEqual('now');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('1 minute ago', true)).toEqual(
|
|
||||||
'1m ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('12 minutes ago', true)).toEqual(
|
|
||||||
'12m ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('a minute ago', true)).toEqual(
|
|
||||||
'1m ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('an hour ago', true)).toEqual(
|
|
||||||
'1h ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('1 hour ago', true)).toEqual(
|
|
||||||
'1h ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('2 hours ago', true)).toEqual(
|
|
||||||
'2h ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('1 day ago', true)).toEqual(
|
|
||||||
'1d ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('a day ago', true)).toEqual(
|
|
||||||
'1d ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('3 days ago', true)).toEqual(
|
|
||||||
'3d ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('a month ago', true)).toEqual(
|
|
||||||
'1mo ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('1 month ago', true)).toEqual(
|
|
||||||
'1mo ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('2 months ago', true)).toEqual(
|
|
||||||
'2mo ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('a year ago', true)).toEqual(
|
|
||||||
'1y ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('1 year ago', true)).toEqual(
|
|
||||||
'1y ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('4 years ago', true)).toEqual(
|
|
||||||
'4y ago'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
import {
|
|
||||||
format,
|
|
||||||
isSameYear,
|
|
||||||
fromUnixTime,
|
|
||||||
formatDistanceToNow,
|
|
||||||
} from 'date-fns';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
methods: {
|
|
||||||
messageStamp(time, dateFormat = 'h:mm a') {
|
|
||||||
const unixTime = fromUnixTime(time);
|
|
||||||
return format(unixTime, dateFormat);
|
|
||||||
},
|
|
||||||
messageTimestamp(time, dateFormat = 'MMM d, yyyy') {
|
|
||||||
const messageTime = fromUnixTime(time);
|
|
||||||
const now = new Date();
|
|
||||||
const messageDate = format(messageTime, dateFormat);
|
|
||||||
if (!isSameYear(messageTime, now)) {
|
|
||||||
return format(messageTime, 'LLL d y, h:mm a');
|
|
||||||
}
|
|
||||||
return messageDate;
|
|
||||||
},
|
|
||||||
dynamicTime(time) {
|
|
||||||
const unixTime = fromUnixTime(time);
|
|
||||||
return formatDistanceToNow(unixTime, { addSuffix: true });
|
|
||||||
},
|
|
||||||
dateFormat(time, dateFormat = 'MMM d, yyyy') {
|
|
||||||
const unixTime = fromUnixTime(time);
|
|
||||||
return format(unixTime, dateFormat);
|
|
||||||
},
|
|
||||||
shortTimestamp(time, withAgo = false) {
|
|
||||||
// This function takes a time string and converts it to a short time string
|
|
||||||
// with the following format: 1m, 1h, 1d, 1mo, 1y
|
|
||||||
// The function also takes an optional boolean parameter withAgo
|
|
||||||
// which will add the word "ago" to the end of the time string
|
|
||||||
const suffix = withAgo ? ' ago' : '';
|
|
||||||
const timeMappings = {
|
|
||||||
'less than a minute ago': 'now',
|
|
||||||
'a minute ago': `1m${suffix}`,
|
|
||||||
'an hour ago': `1h${suffix}`,
|
|
||||||
'a day ago': `1d${suffix}`,
|
|
||||||
'a month ago': `1mo${suffix}`,
|
|
||||||
'a year ago': `1y${suffix}`,
|
|
||||||
};
|
|
||||||
// Check if the time string is one of the specific cases
|
|
||||||
if (timeMappings[time]) {
|
|
||||||
return timeMappings[time];
|
|
||||||
}
|
|
||||||
const convertToShortTime = time
|
|
||||||
.replace(/about|over|almost|/g, '')
|
|
||||||
.replace(' minute ago', `m${suffix}`)
|
|
||||||
.replace(' minutes ago', `m${suffix}`)
|
|
||||||
.replace(' hour ago', `h${suffix}`)
|
|
||||||
.replace(' hours ago', `h${suffix}`)
|
|
||||||
.replace(' day ago', `d${suffix}`)
|
|
||||||
.replace(' days ago', `d${suffix}`)
|
|
||||||
.replace(' month ago', `mo${suffix}`)
|
|
||||||
.replace(' months ago', `mo${suffix}`)
|
|
||||||
.replace(' year ago', `y${suffix}`)
|
|
||||||
.replace(' years ago', `y${suffix}`);
|
|
||||||
return convertToShortTime;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col mb-2 p-4 border border-solid border-slate-75 dark:border-slate-700 overflow-hidden rounded-md flex-grow shadow-sm bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-100 note-wrap"
|
class="flex flex-col flex-grow p-4 mb-2 overflow-hidden bg-white border border-solid rounded-md shadow-sm border-slate-75 dark:border-slate-700 dark:bg-slate-900 text-slate-700 dark:text-slate-100 note-wrap"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between items-end text-xs gap-1">
|
<div class="flex items-end justify-between gap-1 text-xs">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<thumbnail
|
<thumbnail
|
||||||
:title="noteAuthorName"
|
:title="noteAuthorName"
|
||||||
@ -22,7 +22,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions flex invisible">
|
<div class="flex invisible actions">
|
||||||
<woot-button
|
<woot-button
|
||||||
v-tooltip="$t('NOTES.CONTENT_HEADER.DELETE')"
|
v-tooltip="$t('NOTES.CONTENT_HEADER.DELETE')"
|
||||||
variant="smooth"
|
variant="smooth"
|
||||||
@ -45,22 +45,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
v-dompurify-html="formatMessage(note || '')"
|
v-dompurify-html="formatMessage(note || '')"
|
||||||
class="note__content mt-4"
|
class="mt-4 note__content"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
|
||||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
|
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [timeMixin, messageFormatterMixin],
|
mixins: [messageFormatterMixin],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
id: {
|
id: {
|
||||||
@ -87,7 +87,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
readableTime() {
|
readableTime() {
|
||||||
return this.dynamicTime(this.createdAt);
|
return dynamicTime(this.createdAt);
|
||||||
},
|
},
|
||||||
noteAuthor() {
|
noteAuthor() {
|
||||||
return this.user || {};
|
return this.user || {};
|
||||||
|
|||||||
@ -27,7 +27,7 @@
|
|||||||
</h5>
|
</h5>
|
||||||
<h5
|
<h5
|
||||||
v-if="email"
|
v-if="email"
|
||||||
class="text-sm email text-slate-700 dark:text-slate-200 overflow-hidden whitespace-nowrap text-ellipsis"
|
class="overflow-hidden text-sm email text-slate-700 dark:text-slate-200 whitespace-nowrap text-ellipsis"
|
||||||
>
|
>
|
||||||
<span class="pre-text">{{ $t('SEARCH.EMAIL') }}:</span>
|
<span class="pre-text">{{ $t('SEARCH.EMAIL') }}:</span>
|
||||||
{{ email }}
|
{{ email }}
|
||||||
@ -40,14 +40,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { frontendURL } from 'dashboard/helper/URLHelper.js';
|
import { frontendURL } from 'dashboard/helper/URLHelper.js';
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||||
import InboxName from 'dashboard/components/widgets/InboxName.vue';
|
import InboxName from 'dashboard/components/widgets/InboxName.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
InboxName,
|
InboxName,
|
||||||
},
|
},
|
||||||
mixins: [timeMixin],
|
|
||||||
props: {
|
props: {
|
||||||
id: {
|
id: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@ -90,7 +89,7 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
createdAtTime() {
|
createdAtTime() {
|
||||||
return this.dynamicTime(this.createdAt);
|
return dynamicTime(this.createdAt);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<section
|
<section
|
||||||
class="contacts-table-wrap bg-white dark:bg-slate-900 flex-1 h-full overflow-hidden -mt-1"
|
class="flex-1 h-full -mt-1 overflow-hidden bg-white contacts-table-wrap dark:bg-slate-900"
|
||||||
>
|
>
|
||||||
<ve-table
|
<ve-table
|
||||||
:fixed-header="true"
|
:fixed-header="true"
|
||||||
@ -20,7 +20,7 @@
|
|||||||
v-else-if="!isLoading && !contacts.length"
|
v-else-if="!isLoading && !contacts.length"
|
||||||
:title="$t('CONTACTS_PAGE.LIST.NO_CONTACTS')"
|
:title="$t('CONTACTS_PAGE.LIST.NO_CONTACTS')"
|
||||||
/>
|
/>
|
||||||
<div v-if="isLoading" class="items-center flex text-base justify-center">
|
<div v-if="isLoading" class="flex items-center justify-center text-base">
|
||||||
<spinner />
|
<spinner />
|
||||||
<span>{{ $t('CONTACTS_PAGE.LIST.LOADING_MESSAGE') }}</span>
|
<span>{{ $t('CONTACTS_PAGE.LIST.LOADING_MESSAGE') }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -34,7 +34,7 @@ import { getCountryFlag } from 'dashboard/helper/flag';
|
|||||||
import Spinner from 'shared/components/Spinner.vue';
|
import Spinner from 'shared/components/Spinner.vue';
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ export default {
|
|||||||
Spinner,
|
Spinner,
|
||||||
VeTable,
|
VeTable,
|
||||||
},
|
},
|
||||||
mixins: [timeMixin, rtlMixin],
|
mixins: [rtlMixin],
|
||||||
props: {
|
props: {
|
||||||
contacts: {
|
contacts: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@ -105,9 +105,9 @@ export default {
|
|||||||
countryCode: additional.country_code,
|
countryCode: additional.country_code,
|
||||||
conversationsCount: item.conversations_count || '---',
|
conversationsCount: item.conversations_count || '---',
|
||||||
last_activity_at: lastActivityAt
|
last_activity_at: lastActivityAt
|
||||||
? this.dynamicTime(lastActivityAt)
|
? dynamicTime(lastActivityAt)
|
||||||
: '---',
|
: '---',
|
||||||
created_at: createdAt ? this.dynamicTime(createdAt) : '---',
|
created_at: createdAt ? dynamicTime(createdAt) : '---',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -134,7 +134,7 @@ export default {
|
|||||||
status={row.availability_status}
|
status={row.availability_status}
|
||||||
/>
|
/>
|
||||||
<div class="user-block">
|
<div class="user-block">
|
||||||
<h6 class="text-base overflow-hidden whitespace-nowrap text-ellipsis">
|
<h6 class="overflow-hidden text-base whitespace-nowrap text-ellipsis">
|
||||||
<router-link
|
<router-link
|
||||||
to={`/app/accounts/${this.$route.params.accountId}/contacts/${row.id}`}
|
to={`/app/accounts/${this.$route.params.accountId}/contacts/${row.id}`}
|
||||||
class="user-name"
|
class="user-name"
|
||||||
|
|||||||
@ -28,10 +28,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||||
export default {
|
export default {
|
||||||
mixins: [timeMixin],
|
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
eventType: {
|
eventType: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -53,7 +51,7 @@ export default {
|
|||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
readableTime() {
|
readableTime() {
|
||||||
return this.dynamicTime(this.timeStamp);
|
return dynamicTime(this.timeStamp);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative items-center p-4 bg-white dark:bg-slate-900 w-full">
|
<div class="relative items-center w-full p-4 bg-white dark:bg-slate-900">
|
||||||
<div class="text-left rtl:text-right flex flex-col gap-2 w-full">
|
<div class="flex flex-col w-full gap-2 text-left rtl:text-right">
|
||||||
<div class="flex justify-between flex-row">
|
<div class="flex flex-row justify-between">
|
||||||
<thumbnail
|
<thumbnail
|
||||||
v-if="showAvatar"
|
v-if="showAvatar"
|
||||||
:src="contact.thumbnail"
|
:src="contact.thumbnail"
|
||||||
@ -18,9 +18,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col items-start gap-1.5 min-w-0 w-full">
|
<div class="flex flex-col items-start gap-1.5 min-w-0 w-full">
|
||||||
<div v-if="showAvatar" class="flex items-start gap-2 min-w-0 w-full">
|
<div v-if="showAvatar" class="flex items-start w-full min-w-0 gap-2">
|
||||||
<h3
|
<h3
|
||||||
class="flex-shrink min-w-0 text-base text-slate-800 dark:text-slate-100 capitalize my-0 max-w-full break-words"
|
class="flex-shrink max-w-full min-w-0 my-0 text-base capitalize break-words text-slate-800 dark:text-slate-100"
|
||||||
>
|
>
|
||||||
{{ contact.name }}
|
{{ contact.name }}
|
||||||
</h3>
|
</h3>
|
||||||
@ -55,7 +55,7 @@
|
|||||||
<p v-if="additionalAttributes.description" class="break-words mb-0.5">
|
<p v-if="additionalAttributes.description" class="break-words mb-0.5">
|
||||||
{{ additionalAttributes.description }}
|
{{ additionalAttributes.description }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-col gap-2 items-start w-full">
|
<div class="flex flex-col items-start w-full gap-2">
|
||||||
<contact-info-row
|
<contact-info-row
|
||||||
:href="contact.email ? `mailto:${contact.email}` : ''"
|
:href="contact.email ? `mailto:${contact.email}` : ''"
|
||||||
:value="contact.email"
|
:value="contact.email"
|
||||||
@ -166,7 +166,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||||
import ContactInfoRow from './ContactInfoRow.vue';
|
import ContactInfoRow from './ContactInfoRow.vue';
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
import SocialIcons from './SocialIcons.vue';
|
import SocialIcons from './SocialIcons.vue';
|
||||||
@ -194,7 +194,7 @@ export default {
|
|||||||
NewConversation,
|
NewConversation,
|
||||||
ContactMergeModal,
|
ContactMergeModal,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin, adminMixin, timeMixin],
|
mixins: [alertMixin, adminMixin],
|
||||||
props: {
|
props: {
|
||||||
contact: {
|
contact: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -260,6 +260,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
dynamicTime,
|
||||||
toggleMergeModal() {
|
toggleMergeModal() {
|
||||||
this.showMergeModal = !this.showMergeModal;
|
this.showMergeModal = !this.showMergeModal;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
/>
|
/>
|
||||||
<router-link
|
<router-link
|
||||||
:to="searchUrl"
|
:to="searchUrl"
|
||||||
class="search-link flex-1 items-center gap-1 text-left h-6 rtl:mr-3 rtl:text-right rounded-md px-2 py-0 bg-slate-25 dark:bg-slate-800 inline-flex"
|
class="inline-flex items-center flex-1 h-6 gap-1 px-2 py-0 text-left rounded-md search-link rtl:mr-3 rtl:text-right bg-slate-25 dark:bg-slate-800"
|
||||||
>
|
>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<fluent-icon
|
<fluent-icon
|
||||||
@ -19,7 +19,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
class="search--label mb-0 overflow-hidden whitespace-nowrap text-ellipsis text-sm text-slate-800 dark:text-slate-200"
|
class="mb-0 overflow-hidden text-sm search--label whitespace-nowrap text-ellipsis text-slate-800 dark:text-slate-200"
|
||||||
>
|
>
|
||||||
{{ $t('CONVERSATION.SEARCH_MESSAGES') }}
|
{{ $t('CONVERSATION.SEARCH_MESSAGES') }}
|
||||||
</p>
|
</p>
|
||||||
@ -34,7 +34,6 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import timeMixin from '../../../../mixins/time';
|
|
||||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
import SwitchLayout from './SwitchLayout.vue';
|
import SwitchLayout from './SwitchLayout.vue';
|
||||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||||
@ -49,7 +48,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mixins: [timeMixin, messageFormatterMixin],
|
mixins: [messageFormatterMixin],
|
||||||
props: {
|
props: {
|
||||||
isOnExpandedLayout: {
|
isOnExpandedLayout: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="text-slate-700 dark:text-slate-100 last:border-b-0 bg-white dark:bg-slate-900 my-0 -mx-4 grid grid-cols-1 lg:grid-cols-12 gap-4 border-b border-slate-50 dark:border-slate-800 px-6 py-3"
|
class="grid grid-cols-1 gap-4 px-6 py-3 my-0 -mx-4 bg-white border-b text-slate-700 dark:text-slate-100 last:border-b-0 dark:bg-slate-900 lg:grid-cols-12 border-slate-50 dark:border-slate-800"
|
||||||
>
|
>
|
||||||
<span class="items-start flex gap-2 col-span-6 text-left">
|
<span class="flex items-start col-span-6 gap-2 text-left">
|
||||||
<fluent-icon
|
<fluent-icon
|
||||||
v-if="showDragIcon"
|
v-if="showDragIcon"
|
||||||
size="20"
|
size="20"
|
||||||
class="block cursor-move flex-shrink-0 h-4 mt-1 w-4 text-slate-200 dark:text-slate-700 hover:text-slate-400 hover:dark:text-slate-200"
|
class="flex-shrink-0 block w-4 h-4 mt-1 cursor-move text-slate-200 dark:text-slate-700 hover:text-slate-400 hover:dark:text-slate-200"
|
||||||
icon="grab-handle"
|
icon="grab-handle"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col truncate">
|
<div class="flex flex-col truncate">
|
||||||
@ -18,7 +18,7 @@
|
|||||||
{{ title }}
|
{{ title }}
|
||||||
</h6>
|
</h6>
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="flex gap-1 items-center">
|
<div class="flex items-center gap-1">
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
v-if="author"
|
v-if="author"
|
||||||
:src="author.thumbnail"
|
:src="author.thumbnail"
|
||||||
@ -39,7 +39,7 @@
|
|||||||
class="text-woot-300 dark:text-woot-300"
|
class="text-woot-300 dark:text-woot-300"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="font-normal text-slate-700 dark:text-slate-200 text-sm">
|
<span class="text-sm font-normal text-slate-700 dark:text-slate-200">
|
||||||
{{ articleAuthorName }}
|
{{ articleAuthorName }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -60,7 +60,7 @@
|
|||||||
:title="formattedViewCount"
|
:title="formattedViewCount"
|
||||||
>
|
>
|
||||||
{{ readableViewCount }}
|
{{ readableViewCount }}
|
||||||
<span class="lg:hidden ml-1">
|
<span class="ml-1 lg:hidden">
|
||||||
{{ ` ${$t('HELP_CENTER.TABLE.HEADERS.READ_COUNT')}` }}
|
{{ ` ${$t('HELP_CENTER.TABLE.HEADERS.READ_COUNT')}` }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@ -74,7 +74,7 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="flex items-center justify-end col-span-2 first-letter:uppercase text-slate-700 dark:text-slate-100 text-xs"
|
class="flex items-center justify-end col-span-2 text-xs first-letter:uppercase text-slate-700 dark:text-slate-100"
|
||||||
>
|
>
|
||||||
{{ lastUpdatedAt }}
|
{{ lastUpdatedAt }}
|
||||||
</span>
|
</span>
|
||||||
@ -82,7 +82,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||||
import portalMixin from '../mixins/portalMixin';
|
import portalMixin from '../mixins/portalMixin';
|
||||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
@ -91,7 +91,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
},
|
},
|
||||||
mixins: [timeMixin, portalMixin],
|
mixins: [portalMixin],
|
||||||
props: {
|
props: {
|
||||||
showDragIcon: {
|
showDragIcon: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -131,7 +131,7 @@ export default {
|
|||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
lastUpdatedAt() {
|
lastUpdatedAt() {
|
||||||
return this.dynamicTime(this.updatedAt);
|
return dynamicTime(this.updatedAt);
|
||||||
},
|
},
|
||||||
formattedViewCount() {
|
formattedViewCount() {
|
||||||
return Number(this.views || 0).toLocaleString('en');
|
return Number(this.views || 0).toLocaleString('en');
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
@contextmenu="openContextMenu($event)"
|
@contextmenu="openContextMenu($event)"
|
||||||
@click="openConversation(notificationItem)"
|
@click="openConversation(notificationItem)"
|
||||||
>
|
>
|
||||||
<div class="flex relative items-center justify-between w-full">
|
<div class="relative flex items-center justify-between w-full">
|
||||||
<div
|
<div
|
||||||
v-if="isUnread"
|
v-if="isUnread"
|
||||||
class="absolute ltr:-left-3.5 rtl:-right-3.5 flex w-2 h-2 rounded bg-woot-500 dark:bg-woot-500"
|
class="absolute ltr:-left-3.5 rtl:-right-3.5 flex w-2 h-2 rounded bg-woot-500 dark:bg-woot-500"
|
||||||
@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-row justify-between items-center w-full gap-2">
|
<div class="flex flex-row items-center justify-between w-full gap-2">
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
v-if="assigneeMeta"
|
v-if="assigneeMeta"
|
||||||
:src="assigneeMeta.thumbnail"
|
:src="assigneeMeta.thumbnail"
|
||||||
@ -31,19 +31,19 @@
|
|||||||
size="16px"
|
size="16px"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
class="flex-1 text-slate-800 dark:text-slate-50 text-sm overflow-hidden text-ellipsis whitespace-nowrap"
|
class="flex-1 overflow-hidden text-sm text-slate-800 dark:text-slate-50 text-ellipsis whitespace-nowrap"
|
||||||
:class="isUnread ? 'font-medium' : 'font-normal'"
|
:class="isUnread ? 'font-medium' : 'font-normal'"
|
||||||
>
|
>
|
||||||
{{ pushTitle }}
|
{{ pushTitle }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="font-medium text-slate-600 dark:text-slate-300 text-xs whitespace-nowrap"
|
class="text-xs font-medium text-slate-600 dark:text-slate-300 whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{{ lastActivityAt }}
|
{{ lastActivityAt }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="snoozedUntilTime" class="flex items-center">
|
<div v-if="snoozedUntilTime" class="flex items-center">
|
||||||
<span class="text-woot-500 dark:text-woot-500 text-xs font-medium">
|
<span class="text-xs font-medium text-woot-500 dark:text-woot-500">
|
||||||
{{ snoozedDisplayText }}
|
{{ snoozedDisplayText }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -63,7 +63,7 @@ import StatusIcon from './StatusIcon.vue';
|
|||||||
import InboxNameAndId from './InboxNameAndId.vue';
|
import InboxNameAndId from './InboxNameAndId.vue';
|
||||||
import InboxContextMenu from './InboxContextMenu.vue';
|
import InboxContextMenu from './InboxContextMenu.vue';
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import { dynamicTime, shortTimestamp } from 'shared/helpers/timeHelper';
|
||||||
import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
|
import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
|
||||||
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||||
export default {
|
export default {
|
||||||
@ -74,7 +74,6 @@ export default {
|
|||||||
InboxNameAndId,
|
InboxNameAndId,
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
},
|
},
|
||||||
mixins: [timeMixin],
|
|
||||||
props: {
|
props: {
|
||||||
notificationItem: {
|
notificationItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -115,10 +114,8 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
lastActivityAt() {
|
lastActivityAt() {
|
||||||
const dynamicTime = this.dynamicTime(
|
const time = dynamicTime(this.notificationItem?.last_activity_at);
|
||||||
this.notificationItem?.last_activity_at
|
return shortTimestamp(time, true);
|
||||||
);
|
|
||||||
return this.shortTimestamp(dynamicTime, true);
|
|
||||||
},
|
},
|
||||||
menuItems() {
|
menuItems() {
|
||||||
const items = [
|
const items = [
|
||||||
|
|||||||
@ -14,12 +14,12 @@
|
|||||||
v-if="!notificationItem.read_at"
|
v-if="!notificationItem.read_at"
|
||||||
class="w-2 h-2 rounded-full bg-woot-500"
|
class="w-2 h-2 rounded-full bg-woot-500"
|
||||||
/>
|
/>
|
||||||
<div v-else class="w-2 flex" />
|
<div v-else class="flex w-2" />
|
||||||
<div
|
<div
|
||||||
class="flex-col ml-2.5 overflow-hidden w-full flex justify-between"
|
class="flex-col ml-2.5 overflow-hidden w-full flex justify-between"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<div class="items-center flex">
|
<div class="flex items-center">
|
||||||
<span class="font-bold text-slate-800 dark:text-slate-100">
|
<span class="font-bold text-slate-800 dark:text-slate-100">
|
||||||
{{
|
{{
|
||||||
`#${
|
`#${
|
||||||
@ -47,15 +47,15 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex">
|
<div class="flex w-full">
|
||||||
<span
|
<span
|
||||||
class="text-slate-700 dark:text-slate-200 font-normal overflow-hidden whitespace-nowrap text-ellipsis"
|
class="overflow-hidden font-normal text-slate-700 dark:text-slate-200 whitespace-nowrap text-ellipsis"
|
||||||
>
|
>
|
||||||
{{ notificationItem.push_message_title }}
|
{{ notificationItem.push_message_title }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="mt-1 text-slate-500 dark:text-slate-400 text-xxs font-semibold flex"
|
class="flex mt-1 font-semibold text-slate-500 dark:text-slate-400 text-xxs"
|
||||||
>
|
>
|
||||||
{{ dynamicTime(notificationItem.last_activity_at) }}
|
{{ dynamicTime(notificationItem.last_activity_at) }}
|
||||||
</span>
|
</span>
|
||||||
@ -67,13 +67,12 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
},
|
},
|
||||||
mixins: [timeMixin],
|
|
||||||
props: {
|
props: {
|
||||||
notificationItem: {
|
notificationItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -96,6 +95,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
dynamicTime,
|
||||||
onClickOpenNotification() {
|
onClickOpenNotification() {
|
||||||
this.$emit('open-notification', this.notificationItem);
|
this.$emit('open-notification', this.notificationItem);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<section
|
<section
|
||||||
class="h-full flex-shrink flex-grow overflow-hidden py-8 px-4 bg-white dark:bg-slate-900"
|
class="flex-grow flex-shrink h-full px-4 py-8 overflow-hidden bg-white dark:bg-slate-900"
|
||||||
>
|
>
|
||||||
<woot-submit-button
|
<woot-submit-button
|
||||||
v-if="notificationMetadata.unreadCount"
|
v-if="notificationMetadata.unreadCount"
|
||||||
@ -22,7 +22,7 @@
|
|||||||
>
|
>
|
||||||
<td>
|
<td>
|
||||||
<div
|
<div
|
||||||
class="flex-view notification-contant--wrap overflow-hidden whitespace-nowrap text-ellipsis"
|
class="overflow-hidden flex-view notification-contant--wrap whitespace-nowrap text-ellipsis"
|
||||||
>
|
>
|
||||||
<h5 class="notification--title">
|
<h5 class="notification--title">
|
||||||
{{
|
{{
|
||||||
@ -34,7 +34,7 @@
|
|||||||
}}
|
}}
|
||||||
</h5>
|
</h5>
|
||||||
<span
|
<span
|
||||||
class="notification--message-title overflow-hidden whitespace-nowrap text-ellipsis"
|
class="overflow-hidden notification--message-title whitespace-nowrap text-ellipsis"
|
||||||
>
|
>
|
||||||
{{ notificationItem.push_message_title }}
|
{{ notificationItem.push_message_title }}
|
||||||
</span>
|
</span>
|
||||||
@ -88,7 +88,7 @@
|
|||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
import Spinner from 'shared/components/Spinner.vue';
|
import Spinner from 'shared/components/Spinner.vue';
|
||||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||||
import timeMixin from '../../../../mixins/time';
|
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -97,7 +97,6 @@ export default {
|
|||||||
Spinner,
|
Spinner,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
},
|
},
|
||||||
mixins: [timeMixin],
|
|
||||||
props: {
|
props: {
|
||||||
notifications: {
|
notifications: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@ -128,6 +127,9 @@ export default {
|
|||||||
return !this.isLoading && this.notifications.length === 0;
|
return !this.isLoading && this.notifications.length === 0;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
dynamicTime,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex-1 overflow-auto p-4 flex justify-between flex-col">
|
<div class="flex flex-col justify-between flex-1 p-4 overflow-auto">
|
||||||
<!-- List Audit Logs -->
|
<!-- List Audit Logs -->
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
v-if="!uiFlags.fetchingList && !records.length"
|
v-if="!uiFlags.fetchingList && !records.length"
|
||||||
class="flex h-full items-center flex-col justify-center"
|
class="flex flex-col items-center justify-center h-full"
|
||||||
>
|
>
|
||||||
{{ $t('AUDIT_LOGS.LIST.404') }}
|
{{ $t('AUDIT_LOGS.LIST.404') }}
|
||||||
</p>
|
</p>
|
||||||
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<table
|
<table
|
||||||
v-if="!uiFlags.fetchingList && records.length"
|
v-if="!uiFlags.fetchingList && records.length"
|
||||||
class="woot-table w-full"
|
class="w-full woot-table"
|
||||||
>
|
>
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col class="w-3/5" />
|
<col class="w-3/5" />
|
||||||
@ -34,10 +34,10 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="auditLogItem in records" :key="auditLogItem.id">
|
<tr v-for="auditLogItem in records" :key="auditLogItem.id">
|
||||||
<td class="whitespace-nowrap break-all">
|
<td class="break-all whitespace-nowrap">
|
||||||
{{ generateLogText(auditLogItem) }}
|
{{ generateLogText(auditLogItem) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="whitespace-nowrap break-all">
|
<td class="break-all whitespace-nowrap">
|
||||||
{{
|
{{
|
||||||
messageTimestamp(
|
messageTimestamp(
|
||||||
auditLogItem.created_at,
|
auditLogItem.created_at,
|
||||||
@ -65,7 +65,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import TableFooter from 'dashboard/components/widgets/TableFooter.vue';
|
import TableFooter from 'dashboard/components/widgets/TableFooter.vue';
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import { messageTimestamp } from 'shared/helpers/timeHelper';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import {
|
import {
|
||||||
generateTranslationPayload,
|
generateTranslationPayload,
|
||||||
@ -76,7 +76,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
TableFooter,
|
TableFooter,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin, timeMixin],
|
mixins: [alertMixin],
|
||||||
beforeRouteEnter(to, from, next) {
|
beforeRouteEnter(to, from, next) {
|
||||||
// Fetch Audit Logs on page load without manual refresh
|
// Fetch Audit Logs on page load without manual refresh
|
||||||
next(vm => {
|
next(vm => {
|
||||||
@ -104,6 +104,7 @@ export default {
|
|||||||
this.$store.dispatch('agents/get');
|
this.$store.dispatch('agents/get');
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
messageTimestamp,
|
||||||
fetchAuditLogs() {
|
fetchAuditLogs() {
|
||||||
const page = this.$route.query.page ?? 1;
|
const page = this.$route.query.page ?? 1;
|
||||||
this.$store.dispatch('auditlogs/fetch', { page }).catch(error => {
|
this.$store.dispatch('auditlogs/fetch', { page }).catch(error => {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex-1 overflow-auto p-4">
|
<div class="flex-1 p-4 overflow-auto">
|
||||||
<woot-button
|
<woot-button
|
||||||
color-scheme="success"
|
color-scheme="success"
|
||||||
class-names="button--fixed-top"
|
class-names="button--fixed-top"
|
||||||
@ -12,7 +12,7 @@
|
|||||||
<div class="w-full lg:w-3/5">
|
<div class="w-full lg:w-3/5">
|
||||||
<p
|
<p
|
||||||
v-if="!uiFlags.isFetching && !records.length"
|
v-if="!uiFlags.isFetching && !records.length"
|
||||||
class="flex h-full items-center flex-col justify-center"
|
class="flex flex-col items-center justify-center h-full"
|
||||||
>
|
>
|
||||||
{{ $t('AUTOMATION.LIST.404') }}
|
{{ $t('AUTOMATION.LIST.404') }}
|
||||||
</p>
|
</p>
|
||||||
@ -77,7 +77,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hidden lg:block w-1/3">
|
<div class="hidden w-1/3 lg:block">
|
||||||
<span v-dompurify-html="$t('AUTOMATION.SIDEBAR_TXT')" />
|
<span v-dompurify-html="$t('AUTOMATION.SIDEBAR_TXT')" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -128,14 +128,14 @@ import { mapGetters } from 'vuex';
|
|||||||
import AddAutomationRule from './AddAutomationRule.vue';
|
import AddAutomationRule from './AddAutomationRule.vue';
|
||||||
import EditAutomationRule from './EditAutomationRule.vue';
|
import EditAutomationRule from './EditAutomationRule.vue';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import { messageStamp } from 'shared/helpers/timeHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
AddAutomationRule,
|
AddAutomationRule,
|
||||||
EditAutomationRule,
|
EditAutomationRule,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin, timeMixin],
|
mixins: [alertMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: {},
|
loading: {},
|
||||||
@ -278,7 +278,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
readableTime(date) {
|
readableTime(date) {
|
||||||
return this.messageStamp(new Date(date), 'LLL d, h:mm a');
|
return messageStamp(new Date(date), 'LLL d, h:mm a');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -70,14 +70,14 @@
|
|||||||
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName.vue';
|
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName.vue';
|
||||||
import InboxName from 'dashboard/components/widgets/InboxName.vue';
|
import InboxName from 'dashboard/components/widgets/InboxName.vue';
|
||||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import { messageStamp } from 'shared/helpers/timeHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
UserAvatarWithName,
|
UserAvatarWithName,
|
||||||
InboxName,
|
InboxName,
|
||||||
},
|
},
|
||||||
mixins: [messageFormatterMixin, timeMixin],
|
mixins: [messageFormatterMixin],
|
||||||
props: {
|
props: {
|
||||||
campaign: {
|
campaign: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -110,5 +110,8 @@ export default {
|
|||||||
: 'success';
|
: 'success';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
messageStamp,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import { VeTable, VePagination } from 'vue-easytable';
|
|||||||
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName.vue';
|
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName.vue';
|
||||||
import { CSAT_RATINGS } from 'shared/constants/messages';
|
import { CSAT_RATINGS } from 'shared/constants/messages';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import { messageStamp, dynamicTime } from 'shared/helpers/timeHelper';
|
||||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -34,7 +34,7 @@ export default {
|
|||||||
VeTable,
|
VeTable,
|
||||||
VePagination,
|
VePagination,
|
||||||
},
|
},
|
||||||
mixins: [timeMixin, rtlMixin],
|
mixins: [rtlMixin],
|
||||||
props: {
|
props: {
|
||||||
pageIndex: {
|
pageIndex: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@ -137,8 +137,8 @@ export default {
|
|||||||
rating: response.rating,
|
rating: response.rating,
|
||||||
feedbackText: response.feedback_message || '---',
|
feedbackText: response.feedback_message || '---',
|
||||||
conversationId: response.conversation_id,
|
conversationId: response.conversation_id,
|
||||||
createdAgo: this.dynamicTime(response.created_at),
|
createdAgo: dynamicTime(response.created_at),
|
||||||
createdAt: this.messageStamp(response.created_at, 'LLL d yyyy, h:mm a'),
|
createdAt: messageStamp(response.created_at, 'LLL d yyyy, h:mm a'),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
92
app/javascript/shared/helpers/specs/timeHelper.spec.js
Normal file
92
app/javascript/shared/helpers/specs/timeHelper.spec.js
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import {
|
||||||
|
messageStamp,
|
||||||
|
messageTimestamp,
|
||||||
|
dynamicTime,
|
||||||
|
dateFormat,
|
||||||
|
shortTimestamp,
|
||||||
|
} from 'shared/helpers/timeHelper';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env.TZ = 'UTC';
|
||||||
|
vi.useFakeTimers('modern');
|
||||||
|
const mockDate = new Date(Date.UTC(2023, 4, 5));
|
||||||
|
vi.setSystemTime(mockDate);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#messageStamp', () => {
|
||||||
|
it('returns correct value', () => {
|
||||||
|
expect(messageStamp(1612971343)).toEqual('3:35 PM');
|
||||||
|
expect(messageStamp(1612971343, 'LLL d, h:mm a')).toEqual(
|
||||||
|
'Feb 10, 3:35 PM'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#messageTimestamp', () => {
|
||||||
|
it('should return the message date in the specified format if the message was sent in the current year', () => {
|
||||||
|
expect(messageTimestamp(1680777464)).toEqual('Apr 6, 2023');
|
||||||
|
});
|
||||||
|
it('should return the message date and time in a different format if the message was sent in a different year', () => {
|
||||||
|
expect(messageTimestamp(1612971343)).toEqual('Feb 10 2021, 3:35 PM');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#dynamicTime', () => {
|
||||||
|
it('returns correct value', () => {
|
||||||
|
Date.now = vi.fn(() => new Date(Date.UTC(2023, 1, 14)).valueOf());
|
||||||
|
expect(dynamicTime(1612971343)).toEqual('about 2 years ago');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#dateFormat', () => {
|
||||||
|
it('returns correct value', () => {
|
||||||
|
expect(dateFormat(1612971343)).toEqual('Feb 10, 2021');
|
||||||
|
expect(dateFormat(1612971343, 'LLL d, yyyy')).toEqual('Feb 10, 2021');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#shortTimestamp', () => {
|
||||||
|
// Test cases when withAgo is false or not provided
|
||||||
|
it('returns correct value without ago', () => {
|
||||||
|
expect(shortTimestamp('less than a minute ago')).toEqual('now');
|
||||||
|
expect(shortTimestamp('1 minute ago')).toEqual('1m');
|
||||||
|
expect(shortTimestamp('12 minutes ago')).toEqual('12m');
|
||||||
|
expect(shortTimestamp('a minute ago')).toEqual('1m');
|
||||||
|
expect(shortTimestamp('an hour ago')).toEqual('1h');
|
||||||
|
expect(shortTimestamp('1 hour ago')).toEqual('1h');
|
||||||
|
expect(shortTimestamp('2 hours ago')).toEqual('2h');
|
||||||
|
expect(shortTimestamp('1 day ago')).toEqual('1d');
|
||||||
|
expect(shortTimestamp('a day ago')).toEqual('1d');
|
||||||
|
expect(shortTimestamp('3 days ago')).toEqual('3d');
|
||||||
|
expect(shortTimestamp('a month ago')).toEqual('1mo');
|
||||||
|
expect(shortTimestamp('1 month ago')).toEqual('1mo');
|
||||||
|
expect(shortTimestamp('2 months ago')).toEqual('2mo');
|
||||||
|
expect(shortTimestamp('a year ago')).toEqual('1y');
|
||||||
|
expect(shortTimestamp('1 year ago')).toEqual('1y');
|
||||||
|
expect(shortTimestamp('4 years ago')).toEqual('4y');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test cases when withAgo is true
|
||||||
|
it('returns correct value with ago', () => {
|
||||||
|
expect(shortTimestamp('less than a minute ago', true)).toEqual('now');
|
||||||
|
expect(shortTimestamp('1 minute ago', true)).toEqual('1m ago');
|
||||||
|
expect(shortTimestamp('12 minutes ago', true)).toEqual('12m ago');
|
||||||
|
expect(shortTimestamp('a minute ago', true)).toEqual('1m ago');
|
||||||
|
expect(shortTimestamp('an hour ago', true)).toEqual('1h ago');
|
||||||
|
expect(shortTimestamp('1 hour ago', true)).toEqual('1h ago');
|
||||||
|
expect(shortTimestamp('2 hours ago', true)).toEqual('2h ago');
|
||||||
|
expect(shortTimestamp('1 day ago', true)).toEqual('1d ago');
|
||||||
|
expect(shortTimestamp('a day ago', true)).toEqual('1d ago');
|
||||||
|
expect(shortTimestamp('3 days ago', true)).toEqual('3d ago');
|
||||||
|
expect(shortTimestamp('a month ago', true)).toEqual('1mo ago');
|
||||||
|
expect(shortTimestamp('1 month ago', true)).toEqual('1mo ago');
|
||||||
|
expect(shortTimestamp('2 months ago', true)).toEqual('2mo ago');
|
||||||
|
expect(shortTimestamp('a year ago', true)).toEqual('1y ago');
|
||||||
|
expect(shortTimestamp('1 year ago', true)).toEqual('1y ago');
|
||||||
|
expect(shortTimestamp('4 years ago', true)).toEqual('4y ago');
|
||||||
|
});
|
||||||
|
});
|
||||||
93
app/javascript/shared/helpers/timeHelper.js
Normal file
93
app/javascript/shared/helpers/timeHelper.js
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import {
|
||||||
|
format,
|
||||||
|
isSameYear,
|
||||||
|
fromUnixTime,
|
||||||
|
formatDistanceToNow,
|
||||||
|
} from 'date-fns';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a Unix timestamp into a human-readable time format.
|
||||||
|
* @param {number} time - Unix timestamp.
|
||||||
|
* @param {string} [dateFormat='h:mm a'] - Desired format of the time.
|
||||||
|
* @returns {string} Formatted time string.
|
||||||
|
*/
|
||||||
|
export const messageStamp = (time, dateFormat = 'h:mm a') => {
|
||||||
|
const unixTime = fromUnixTime(time);
|
||||||
|
return format(unixTime, dateFormat);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a formatted timestamp, adjusting the format based on the current year.
|
||||||
|
* @param {number} time - Unix timestamp.
|
||||||
|
* @param {string} [dateFormat='MMM d, yyyy'] - Desired date format.
|
||||||
|
* @returns {string} Formatted date string.
|
||||||
|
*/
|
||||||
|
export const messageTimestamp = (time, dateFormat = 'MMM d, yyyy') => {
|
||||||
|
const messageTime = fromUnixTime(time);
|
||||||
|
const now = new Date();
|
||||||
|
const messageDate = format(messageTime, dateFormat);
|
||||||
|
if (!isSameYear(messageTime, now)) {
|
||||||
|
return format(messageTime, 'LLL d y, h:mm a');
|
||||||
|
}
|
||||||
|
return messageDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Unix timestamp to a relative time string (e.g., 3 hours ago).
|
||||||
|
* @param {number} time - Unix timestamp.
|
||||||
|
* @returns {string} Relative time string.
|
||||||
|
*/
|
||||||
|
export const dynamicTime = time => {
|
||||||
|
const unixTime = fromUnixTime(time);
|
||||||
|
return formatDistanceToNow(unixTime, { addSuffix: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a Unix timestamp into a specified date format.
|
||||||
|
* @param {number} time - Unix timestamp.
|
||||||
|
* @param {string} [dateFormat='MMM d, yyyy'] - Desired date format.
|
||||||
|
* @returns {string} Formatted date string.
|
||||||
|
*/
|
||||||
|
export const dateFormat = (time, df = 'MMM d, yyyy') => {
|
||||||
|
const unixTime = fromUnixTime(time);
|
||||||
|
return format(unixTime, df);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a detailed time description into a shorter format, optionally appending 'ago'.
|
||||||
|
* @param {string} time - Detailed time description (e.g., 'a minute ago').
|
||||||
|
* @param {boolean} [withAgo=false] - Whether to append 'ago' to the result.
|
||||||
|
* @returns {string} Shortened time description.
|
||||||
|
*/
|
||||||
|
export const shortTimestamp = (time, withAgo = false) => {
|
||||||
|
// This function takes a time string and converts it to a short time string
|
||||||
|
// with the following format: 1m, 1h, 1d, 1mo, 1y
|
||||||
|
// The function also takes an optional boolean parameter withAgo
|
||||||
|
// which will add the word "ago" to the end of the time string
|
||||||
|
const suffix = withAgo ? ' ago' : '';
|
||||||
|
const timeMappings = {
|
||||||
|
'less than a minute ago': 'now',
|
||||||
|
'a minute ago': `1m${suffix}`,
|
||||||
|
'an hour ago': `1h${suffix}`,
|
||||||
|
'a day ago': `1d${suffix}`,
|
||||||
|
'a month ago': `1mo${suffix}`,
|
||||||
|
'a year ago': `1y${suffix}`,
|
||||||
|
};
|
||||||
|
// Check if the time string is one of the specific cases
|
||||||
|
if (timeMappings[time]) {
|
||||||
|
return timeMappings[time];
|
||||||
|
}
|
||||||
|
const convertToShortTime = time
|
||||||
|
.replace(/about|over|almost|/g, '')
|
||||||
|
.replace(' minute ago', `m${suffix}`)
|
||||||
|
.replace(' minutes ago', `m${suffix}`)
|
||||||
|
.replace(' hour ago', `h${suffix}`)
|
||||||
|
.replace(' hours ago', `h${suffix}`)
|
||||||
|
.replace(' day ago', `d${suffix}`)
|
||||||
|
.replace(' days ago', `d${suffix}`)
|
||||||
|
.replace(' month ago', `mo${suffix}`)
|
||||||
|
.replace(' months ago', `mo${suffix}`)
|
||||||
|
.replace(' year ago', `y${suffix}`)
|
||||||
|
.replace(' years ago', `y${suffix}`);
|
||||||
|
return convertToShortTime;
|
||||||
|
};
|
||||||
@ -30,7 +30,7 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="hasAttachments"
|
v-if="hasAttachments"
|
||||||
class="chat-bubble has-attachment space-y-2 agent"
|
class="space-y-2 chat-bubble has-attachment agent"
|
||||||
:class="(wrapClass, $dm('bg-white', 'dark:bg-slate-700'))"
|
:class="(wrapClass, $dm('bg-white', 'dark:bg-slate-700'))"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -90,7 +90,7 @@
|
|||||||
import UserMessage from 'widget/components/UserMessage.vue';
|
import UserMessage from 'widget/components/UserMessage.vue';
|
||||||
import AgentMessageBubble from 'widget/components/AgentMessageBubble.vue';
|
import AgentMessageBubble from 'widget/components/AgentMessageBubble.vue';
|
||||||
import MessageReplyButton from 'widget/components/MessageReplyButton.vue';
|
import MessageReplyButton from 'widget/components/MessageReplyButton.vue';
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import { messageStamp } from 'shared/helpers/timeHelper';
|
||||||
import ImageBubble from 'widget/components/ImageBubble.vue';
|
import ImageBubble from 'widget/components/ImageBubble.vue';
|
||||||
import VideoBubble from 'widget/components/VideoBubble.vue';
|
import VideoBubble from 'widget/components/VideoBubble.vue';
|
||||||
import FileBubble from 'widget/components/FileBubble.vue';
|
import FileBubble from 'widget/components/FileBubble.vue';
|
||||||
@ -116,7 +116,7 @@ export default {
|
|||||||
MessageReplyButton,
|
MessageReplyButton,
|
||||||
ReplyToChip,
|
ReplyToChip,
|
||||||
},
|
},
|
||||||
mixins: [timeMixin, configMixin, messageMixin, darkModeMixin],
|
mixins: [configMixin, messageMixin, darkModeMixin],
|
||||||
props: {
|
props: {
|
||||||
message: {
|
message: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -146,7 +146,7 @@ export default {
|
|||||||
},
|
},
|
||||||
readableTime() {
|
readableTime() {
|
||||||
const { created_at: createdAt = '' } = this.message;
|
const { created_at: createdAt = '' } = this.message;
|
||||||
return this.messageStamp(createdAt, 'LLL d yyyy, h:mm a');
|
return messageStamp(createdAt, 'LLL d yyyy, h:mm a');
|
||||||
},
|
},
|
||||||
messageType() {
|
messageType() {
|
||||||
const { message_type: type = 1 } = this.message;
|
const { message_type: type = 1 } = this.message;
|
||||||
|
|||||||
@ -83,7 +83,7 @@ import ImageBubble from 'widget/components/ImageBubble.vue';
|
|||||||
import VideoBubble from 'widget/components/VideoBubble.vue';
|
import VideoBubble from 'widget/components/VideoBubble.vue';
|
||||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||||
import FileBubble from 'widget/components/FileBubble.vue';
|
import FileBubble from 'widget/components/FileBubble.vue';
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import { messageStamp } from 'shared/helpers/timeHelper';
|
||||||
import messageMixin from '../mixins/messageMixin';
|
import messageMixin from '../mixins/messageMixin';
|
||||||
import ReplyToChip from 'widget/components/ReplyToChip.vue';
|
import ReplyToChip from 'widget/components/ReplyToChip.vue';
|
||||||
import DragWrapper from 'widget/components/DragWrapper.vue';
|
import DragWrapper from 'widget/components/DragWrapper.vue';
|
||||||
@ -103,7 +103,7 @@ export default {
|
|||||||
ReplyToChip,
|
ReplyToChip,
|
||||||
DragWrapper,
|
DragWrapper,
|
||||||
},
|
},
|
||||||
mixins: [timeMixin, messageMixin],
|
mixins: [messageMixin],
|
||||||
props: {
|
props: {
|
||||||
message: {
|
message: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -135,7 +135,7 @@ export default {
|
|||||||
},
|
},
|
||||||
readableTime() {
|
readableTime() {
|
||||||
const { created_at: createdAt = '' } = this.message;
|
const { created_at: createdAt = '' } = this.message;
|
||||||
return this.messageStamp(createdAt);
|
return messageStamp(createdAt);
|
||||||
},
|
},
|
||||||
isFailed() {
|
isFailed() {
|
||||||
const { status = '' } = this.message;
|
const { status = '' } = this.message;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user