Merge branch 'develop' of github.com:chatwoot/chatwoot into chore/vuelidate-v2-migration
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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
|
||||
:fixed-header="true"
|
||||
@@ -20,7 +20,7 @@
|
||||
v-else-if="!isLoading && !contacts.length"
|
||||
: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 />
|
||||
<span>{{ $t('CONTACTS_PAGE.LIST.LOADING_MESSAGE') }}</span>
|
||||
</div>
|
||||
@@ -34,7 +34,7 @@ import { getCountryFlag } from 'dashboard/helper/flag';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.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 FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
|
||||
@@ -44,7 +44,7 @@ export default {
|
||||
Spinner,
|
||||
VeTable,
|
||||
},
|
||||
mixins: [timeMixin, rtlMixin],
|
||||
mixins: [rtlMixin],
|
||||
props: {
|
||||
contacts: {
|
||||
type: Array,
|
||||
@@ -105,9 +105,9 @@ export default {
|
||||
countryCode: additional.country_code,
|
||||
conversationsCount: item.conversations_count || '---',
|
||||
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}
|
||||
/>
|
||||
<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
|
||||
to={`/app/accounts/${this.$route.params.accountId}/contacts/${row.id}`}
|
||||
class="user-name"
|
||||
|
||||
@@ -28,10 +28,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
export default {
|
||||
mixins: [timeMixin],
|
||||
|
||||
props: {
|
||||
eventType: {
|
||||
type: String,
|
||||
@@ -53,7 +51,7 @@ export default {
|
||||
|
||||
computed: {
|
||||
readableTime() {
|
||||
return this.dynamicTime(this.timeStamp);
|
||||
return dynamicTime(this.timeStamp);
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="relative items-center p-4 bg-white dark:bg-slate-900 w-full">
|
||||
<div class="text-left rtl:text-right flex flex-col gap-2 w-full">
|
||||
<div class="flex justify-between flex-row">
|
||||
<div class="relative items-center w-full p-4 bg-white dark:bg-slate-900">
|
||||
<div class="flex flex-col w-full gap-2 text-left rtl:text-right">
|
||||
<div class="flex flex-row justify-between">
|
||||
<thumbnail
|
||||
v-if="showAvatar"
|
||||
:src="contact.thumbnail"
|
||||
@@ -18,9 +18,9 @@
|
||||
</div>
|
||||
|
||||
<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
|
||||
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 }}
|
||||
</h3>
|
||||
@@ -55,7 +55,7 @@
|
||||
<p v-if="additionalAttributes.description" class="break-words mb-0.5">
|
||||
{{ additionalAttributes.description }}
|
||||
</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
|
||||
:href="contact.email ? `mailto:${contact.email}` : ''"
|
||||
:value="contact.email"
|
||||
@@ -166,7 +166,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
import ContactInfoRow from './ContactInfoRow.vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import SocialIcons from './SocialIcons.vue';
|
||||
@@ -194,7 +194,7 @@ export default {
|
||||
NewConversation,
|
||||
ContactMergeModal,
|
||||
},
|
||||
mixins: [alertMixin, adminMixin, timeMixin],
|
||||
mixins: [alertMixin, adminMixin],
|
||||
props: {
|
||||
contact: {
|
||||
type: Object,
|
||||
@@ -260,6 +260,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dynamicTime,
|
||||
toggleMergeModal() {
|
||||
this.showMergeModal = !this.showMergeModal;
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
/>
|
||||
<router-link
|
||||
: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">
|
||||
<fluent-icon
|
||||
@@ -19,7 +19,7 @@
|
||||
/>
|
||||
</div>
|
||||
<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') }}
|
||||
</p>
|
||||
@@ -34,7 +34,6 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import timeMixin from '../../../../mixins/time';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import SwitchLayout from './SwitchLayout.vue';
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
@@ -49,7 +48,7 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
mixins: [timeMixin, messageFormatterMixin],
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
isOnExpandedLayout: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<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
|
||||
v-if="showDragIcon"
|
||||
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"
|
||||
/>
|
||||
<div class="flex flex-col truncate">
|
||||
@@ -18,7 +18,7 @@
|
||||
{{ title }}
|
||||
</h6>
|
||||
</router-link>
|
||||
<div class="flex gap-1 items-center">
|
||||
<div class="flex items-center gap-1">
|
||||
<Thumbnail
|
||||
v-if="author"
|
||||
:src="author.thumbnail"
|
||||
@@ -39,7 +39,7 @@
|
||||
class="text-woot-300 dark:text-woot-300"
|
||||
/>
|
||||
</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 }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -60,7 +60,7 @@
|
||||
:title="formattedViewCount"
|
||||
>
|
||||
{{ readableViewCount }}
|
||||
<span class="lg:hidden ml-1">
|
||||
<span class="ml-1 lg:hidden">
|
||||
{{ ` ${$t('HELP_CENTER.TABLE.HEADERS.READ_COUNT')}` }}
|
||||
</span>
|
||||
</span>
|
||||
@@ -74,7 +74,7 @@
|
||||
/>
|
||||
</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 }}
|
||||
</span>
|
||||
@@ -82,7 +82,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
import portalMixin from '../mixins/portalMixin';
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
@@ -91,7 +91,7 @@ export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [timeMixin, portalMixin],
|
||||
mixins: [portalMixin],
|
||||
props: {
|
||||
showDragIcon: {
|
||||
type: Boolean,
|
||||
@@ -131,7 +131,7 @@ export default {
|
||||
|
||||
computed: {
|
||||
lastUpdatedAt() {
|
||||
return this.dynamicTime(this.updatedAt);
|
||||
return dynamicTime(this.updatedAt);
|
||||
},
|
||||
formattedViewCount() {
|
||||
return Number(this.views || 0).toLocaleString('en');
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
@contextmenu="openContextMenu($event)"
|
||||
@click="openConversation(notificationItem)"
|
||||
>
|
||||
<div class="flex relative items-center justify-between w-full">
|
||||
<div class="relative flex items-center justify-between w-full">
|
||||
<div
|
||||
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"
|
||||
@@ -23,7 +23,7 @@
|
||||
</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
|
||||
v-if="assigneeMeta"
|
||||
:src="assigneeMeta.thumbnail"
|
||||
@@ -31,19 +31,19 @@
|
||||
size="16px"
|
||||
/>
|
||||
<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'"
|
||||
>
|
||||
{{ pushTitle }}
|
||||
</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 }}
|
||||
</span>
|
||||
</div>
|
||||
<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 }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -63,7 +63,7 @@ import StatusIcon from './StatusIcon.vue';
|
||||
import InboxNameAndId from './InboxNameAndId.vue';
|
||||
import InboxContextMenu from './InboxContextMenu.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 { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
export default {
|
||||
@@ -74,7 +74,6 @@ export default {
|
||||
InboxNameAndId,
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [timeMixin],
|
||||
props: {
|
||||
notificationItem: {
|
||||
type: Object,
|
||||
@@ -115,10 +114,8 @@ export default {
|
||||
);
|
||||
},
|
||||
lastActivityAt() {
|
||||
const dynamicTime = this.dynamicTime(
|
||||
this.notificationItem?.last_activity_at
|
||||
);
|
||||
return this.shortTimestamp(dynamicTime, true);
|
||||
const time = dynamicTime(this.notificationItem?.last_activity_at);
|
||||
return shortTimestamp(time, true);
|
||||
},
|
||||
menuItems() {
|
||||
const items = [
|
||||
|
||||
+7
-7
@@ -14,12 +14,12 @@
|
||||
v-if="!notificationItem.read_at"
|
||||
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
|
||||
class="flex-col ml-2.5 overflow-hidden w-full 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">
|
||||
{{
|
||||
`#${
|
||||
@@ -47,15 +47,15 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex">
|
||||
<div class="flex w-full">
|
||||
<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 }}
|
||||
</span>
|
||||
</div>
|
||||
<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) }}
|
||||
</span>
|
||||
@@ -67,13 +67,12 @@
|
||||
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [timeMixin],
|
||||
props: {
|
||||
notificationItem: {
|
||||
type: Object,
|
||||
@@ -96,6 +95,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dynamicTime,
|
||||
onClickOpenNotification() {
|
||||
this.$emit('open-notification', this.notificationItem);
|
||||
},
|
||||
|
||||
+7
-5
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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
|
||||
v-if="notificationMetadata.unreadCount"
|
||||
@@ -22,7 +22,7 @@
|
||||
>
|
||||
<td>
|
||||
<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">
|
||||
{{
|
||||
@@ -34,7 +34,7 @@
|
||||
}}
|
||||
</h5>
|
||||
<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 }}
|
||||
</span>
|
||||
@@ -88,7 +88,7 @@
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import timeMixin from '../../../../mixins/time';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
@@ -97,7 +97,6 @@ export default {
|
||||
Spinner,
|
||||
EmptyState,
|
||||
},
|
||||
mixins: [timeMixin],
|
||||
props: {
|
||||
notifications: {
|
||||
type: Array,
|
||||
@@ -128,6 +127,9 @@ export default {
|
||||
return !this.isLoading && this.notifications.length === 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dynamicTime,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<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 -->
|
||||
<div>
|
||||
<div>
|
||||
<p
|
||||
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') }}
|
||||
</p>
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<table
|
||||
v-if="!uiFlags.fetchingList && records.length"
|
||||
class="woot-table w-full"
|
||||
class="w-full woot-table"
|
||||
>
|
||||
<colgroup>
|
||||
<col class="w-3/5" />
|
||||
@@ -34,10 +34,10 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="auditLogItem in records" :key="auditLogItem.id">
|
||||
<td class="whitespace-nowrap break-all">
|
||||
<td class="break-all whitespace-nowrap">
|
||||
{{ generateLogText(auditLogItem) }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap break-all">
|
||||
<td class="break-all whitespace-nowrap">
|
||||
{{
|
||||
messageTimestamp(
|
||||
auditLogItem.created_at,
|
||||
@@ -65,7 +65,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
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 {
|
||||
generateTranslationPayload,
|
||||
@@ -76,7 +76,7 @@ export default {
|
||||
components: {
|
||||
TableFooter,
|
||||
},
|
||||
mixins: [alertMixin, timeMixin],
|
||||
mixins: [alertMixin],
|
||||
beforeRouteEnter(to, from, next) {
|
||||
// Fetch Audit Logs on page load without manual refresh
|
||||
next(vm => {
|
||||
@@ -104,6 +104,7 @@ export default {
|
||||
this.$store.dispatch('agents/get');
|
||||
},
|
||||
methods: {
|
||||
messageTimestamp,
|
||||
fetchAuditLogs() {
|
||||
const page = this.$route.query.page ?? 1;
|
||||
this.$store.dispatch('auditlogs/fetch', { page }).catch(error => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex-1 overflow-auto p-4">
|
||||
<div class="flex-1 p-4 overflow-auto">
|
||||
<woot-button
|
||||
color-scheme="success"
|
||||
class-names="button--fixed-top"
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="w-full lg:w-3/5">
|
||||
<p
|
||||
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') }}
|
||||
</p>
|
||||
@@ -77,7 +77,7 @@
|
||||
</table>
|
||||
</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')" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -128,14 +128,14 @@ import { mapGetters } from 'vuex';
|
||||
import AddAutomationRule from './AddAutomationRule.vue';
|
||||
import EditAutomationRule from './EditAutomationRule.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import { messageStamp } from 'shared/helpers/timeHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddAutomationRule,
|
||||
EditAutomationRule,
|
||||
},
|
||||
mixins: [alertMixin, timeMixin],
|
||||
mixins: [alertMixin],
|
||||
data() {
|
||||
return {
|
||||
loading: {},
|
||||
@@ -278,7 +278,7 @@ export default {
|
||||
}
|
||||
},
|
||||
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 InboxName from 'dashboard/components/widgets/InboxName.vue';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import { messageStamp } from 'shared/helpers/timeHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
UserAvatarWithName,
|
||||
InboxName,
|
||||
},
|
||||
mixins: [messageFormatterMixin, timeMixin],
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
campaign: {
|
||||
type: Object,
|
||||
@@ -110,5 +110,8 @@ export default {
|
||||
: 'success';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
messageStamp,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
<div
|
||||
class="border border-slate-25 dark:border-slate-800/60 bg-white dark:bg-slate-900 h-full p-6 w-full max-w-full md:w-3/4 md:max-w-[75%] flex-shrink-0 flex-grow-0"
|
||||
>
|
||||
<div v-if="!hasLoginStarted" class="login-init h-full">
|
||||
<div v-if="!hasLoginStarted" class="pt-[30%] h-full">
|
||||
<a href="#" @click="startLogin()">
|
||||
<img
|
||||
class="w-auto h-10"
|
||||
src="~dashboard/assets/images/channels/facebook_login.png"
|
||||
alt="Facebook-logo"
|
||||
/>
|
||||
</a>
|
||||
<p>
|
||||
<p class="py-6">
|
||||
{{
|
||||
useInstallationName(
|
||||
$t('INBOX_MGMT.ADD.FB.HELP'),
|
||||
@@ -29,7 +30,7 @@
|
||||
<loading-state v-else-if="showLoader" :message="emptyStateMessage" />
|
||||
<form
|
||||
v-else
|
||||
class="mx-0 flex flex-wrap"
|
||||
class="flex flex-wrap mx-0"
|
||||
@submit.prevent="createChannel()"
|
||||
>
|
||||
<div class="w-full">
|
||||
@@ -100,6 +101,9 @@ import router from '../../../../index';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import accountMixin from '../../../../../mixins/account';
|
||||
|
||||
import { loadScript } from 'dashboard/helper/DOMHelpers';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoadingState,
|
||||
@@ -151,19 +155,29 @@ export default {
|
||||
}),
|
||||
},
|
||||
|
||||
created() {
|
||||
this.initFB();
|
||||
this.loadFBsdk();
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.initFB();
|
||||
window.fbAsyncInit = this.runFBInit;
|
||||
},
|
||||
|
||||
methods: {
|
||||
startLogin() {
|
||||
async startLogin() {
|
||||
this.hasLoginStarted = true;
|
||||
this.tryFBlogin();
|
||||
try {
|
||||
// this will load the SDK in a promise, and resolve it when the sdk is loaded
|
||||
// in case the SDK is already present, it will resolve immediately
|
||||
await this.loadFBsdk();
|
||||
this.runFBInit(); // run init anyway, `tryFBlogin` won't wait for `fbAsyncInit` otherwise.
|
||||
this.tryFBlogin(); // make an attempt to login
|
||||
} catch (error) {
|
||||
if (error.name === 'ScriptLoaderError') {
|
||||
// if the error was related to script loading, we show a toast
|
||||
this.showAlert(this.$t('INBOX_MGMT.DETAILS.ERROR_FB_LOADING'));
|
||||
} else {
|
||||
// if the error was anything else, we capture it and show a toast
|
||||
Sentry.captureException(error);
|
||||
this.showAlert(this.$t('INBOX_MGMT.DETAILS.ERROR_FB_AUTH'));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setPageName({ name }) {
|
||||
@@ -177,34 +191,21 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
initFB() {
|
||||
if (window.fbSDKLoaded === undefined) {
|
||||
window.fbAsyncInit = () => {
|
||||
FB.init({
|
||||
appId: window.chatwootConfig.fbAppId,
|
||||
xfbml: true,
|
||||
version: window.chatwootConfig.fbApiVersion,
|
||||
status: true,
|
||||
});
|
||||
window.fbSDKLoaded = true;
|
||||
FB.AppEvents.logPageView();
|
||||
};
|
||||
}
|
||||
runFBInit() {
|
||||
FB.init({
|
||||
appId: window.chatwootConfig.fbAppId,
|
||||
xfbml: true,
|
||||
version: window.chatwootConfig.fbApiVersion,
|
||||
status: true,
|
||||
});
|
||||
window.fbSDKLoaded = true;
|
||||
FB.AppEvents.logPageView();
|
||||
},
|
||||
|
||||
loadFBsdk() {
|
||||
((d, s, id) => {
|
||||
let js;
|
||||
// eslint-disable-next-line
|
||||
const fjs = (js = d.getElementsByTagName(s)[0]);
|
||||
if (d.getElementById(id)) {
|
||||
return;
|
||||
}
|
||||
js = d.createElement(s);
|
||||
js.id = id;
|
||||
js.src = '//connect.facebook.net/en_US/sdk.js';
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
})(document, 'script', 'facebook-jssdk');
|
||||
async loadFBsdk() {
|
||||
return loadScript('https://connect.facebook.net/en_US/sdk.js', {
|
||||
id: 'facebook-jssdk',
|
||||
});
|
||||
},
|
||||
|
||||
tryFBlogin() {
|
||||
@@ -289,16 +290,3 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.login-init {
|
||||
@apply pt-[30%] text-center;
|
||||
|
||||
p {
|
||||
@apply p-6;
|
||||
}
|
||||
|
||||
> a > img {
|
||||
@apply w-60;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<template>
|
||||
<inbox-reconnection-required class="mx-8 mt-5" @reauthorize="tryFBlogin" />
|
||||
<inbox-reconnection-required class="mx-8 mt-5" @reauthorize="startLogin" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global FB */
|
||||
import InboxReconnectionRequired from '../components/InboxReconnectionRequired';
|
||||
|
||||
import { loadScript } from 'dashboard/helper/DOMHelpers';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -24,38 +27,44 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initFB();
|
||||
this.loadFBsdk();
|
||||
window.fbAsyncInit = this.runFBInit;
|
||||
},
|
||||
methods: {
|
||||
initFB() {
|
||||
if (window.fbSDKLoaded === undefined) {
|
||||
window.fbAsyncInit = () => {
|
||||
FB.init({
|
||||
appId: window.chatwootConfig.fbAppId,
|
||||
xfbml: true,
|
||||
version: window.chatwootConfig.fbApiVersion,
|
||||
status: true,
|
||||
});
|
||||
window.fbSDKLoaded = true;
|
||||
FB.AppEvents.logPageView();
|
||||
};
|
||||
}
|
||||
runFBInit() {
|
||||
FB.init({
|
||||
appId: window.chatwootConfig.fbAppId,
|
||||
xfbml: true,
|
||||
version: window.chatwootConfig.fbApiVersion,
|
||||
status: true,
|
||||
});
|
||||
window.fbSDKLoaded = true;
|
||||
FB.AppEvents.logPageView();
|
||||
},
|
||||
|
||||
loadFBsdk() {
|
||||
((d, s, id) => {
|
||||
let js;
|
||||
// eslint-disable-next-line
|
||||
const fjs = (js = d.getElementsByTagName(s)[0]);
|
||||
if (d.getElementById(id)) {
|
||||
return;
|
||||
async loadFBsdk() {
|
||||
return loadScript('https://connect.facebook.net/en_US/sdk.js', {
|
||||
id: 'facebook-jssdk',
|
||||
});
|
||||
},
|
||||
|
||||
async startLogin() {
|
||||
this.hasLoginStarted = true;
|
||||
try {
|
||||
// this will load the SDK in a promise, and resolve it when the sdk is loaded
|
||||
// in case the SDK is already present, it will resolve immediately
|
||||
await this.loadFBsdk();
|
||||
this.runFBInit(); // run init anyway, `tryFBlogin` won't wait for `fbAsyncInit` otherwise.
|
||||
this.tryFBlogin(); // make an attempt to login
|
||||
} catch (error) {
|
||||
if (error.name === 'ScriptLoaderError') {
|
||||
// if the error was related to script loading, we show a toast
|
||||
this.showAlert(this.$t('INBOX_MGMT.DETAILS.ERROR_FB_LOADING'));
|
||||
} else {
|
||||
// if the error was anything else, we capture it and show a toast
|
||||
Sentry.captureException(error);
|
||||
this.showAlert(this.$t('INBOX_MGMT.DETAILS.ERROR_FB_AUTH'));
|
||||
}
|
||||
js = d.createElement(s);
|
||||
js.id = id;
|
||||
js.src = 'https://connect.facebook.net/en_US/sdk.js';
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
})(document, 'script', 'facebook-jssdk');
|
||||
}
|
||||
},
|
||||
|
||||
tryFBlogin() {
|
||||
|
||||
@@ -26,7 +26,7 @@ import { VeTable, VePagination } from 'vue-easytable';
|
||||
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName.vue';
|
||||
import { CSAT_RATINGS } from 'shared/constants/messages';
|
||||
import { mapGetters } from 'vuex';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import { messageStamp, dynamicTime } from 'shared/helpers/timeHelper';
|
||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||
|
||||
export default {
|
||||
@@ -34,7 +34,7 @@ export default {
|
||||
VeTable,
|
||||
VePagination,
|
||||
},
|
||||
mixins: [timeMixin, rtlMixin],
|
||||
mixins: [rtlMixin],
|
||||
props: {
|
||||
pageIndex: {
|
||||
type: Number,
|
||||
@@ -137,8 +137,8 @@ export default {
|
||||
rating: response.rating,
|
||||
feedbackText: response.feedback_message || '---',
|
||||
conversationId: response.conversation_id,
|
||||
createdAgo: this.dynamicTime(response.created_at),
|
||||
createdAt: this.messageStamp(response.created_at, 'LLL d yyyy, h:mm a'),
|
||||
createdAgo: dynamicTime(response.created_at),
|
||||
createdAt: messageStamp(response.created_at, 'LLL d yyyy, h:mm a'),
|
||||
}));
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="h-auto overflow-auto flex flex-col">
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<woot-modal-header
|
||||
:header-title="$t('SLA.ADD.TITLE')"
|
||||
:header-content="$t('SLA.ADD.DESC')"
|
||||
@@ -14,17 +14,14 @@
|
||||
|
||||
<script>
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import validationMixin from './validationMixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
import validations from './validations';
|
||||
import SlaForm from './SlaForm.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SlaForm,
|
||||
},
|
||||
mixins: [alertMixin, validationMixin],
|
||||
validations,
|
||||
mixins: [alertMixin],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'sla/getUIFlags',
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
}"
|
||||
:label="$t('SLA.FORM.NAME.LABEL')"
|
||||
:placeholder="$t('SLA.FORM.NAME.PLACEHOLDER')"
|
||||
:error="getSlaNameErrorMessage"
|
||||
:error="slaNameErrorMessage"
|
||||
@input="v$.name.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
@@ -70,7 +70,6 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { convertSecondsToTimeUnit } from '@chatwoot/utils';
|
||||
import validationMixin from './validationMixin';
|
||||
import validations from './validations';
|
||||
import SlaTimeInput from './SlaTimeInput.vue';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
@@ -79,7 +78,6 @@ export default {
|
||||
components: {
|
||||
SlaTimeInput,
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
props: {
|
||||
selectedResponse: {
|
||||
type: Object,
|
||||
@@ -134,6 +132,17 @@ export default {
|
||||
this.uiFlags.isUpdating
|
||||
);
|
||||
},
|
||||
slaNameErrorMessage() {
|
||||
let errorMessage = '';
|
||||
if (this.$v.name.$error) {
|
||||
if (!this.$v.name.required) {
|
||||
errorMessage = this.$t('SLA.FORM.NAME.REQUIRED_ERROR');
|
||||
} else if (!this.$v.name.minLength) {
|
||||
errorMessage = this.$t('SLA.FORM.NAME.MINIMUM_LENGTH_ERROR');
|
||||
}
|
||||
}
|
||||
return errorMessage;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.selectedResponse) this.setFormValues();
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
}"
|
||||
:label="label"
|
||||
:placeholder="placeholder"
|
||||
:error="getThresholdTimeErrorMessage"
|
||||
:error="thresholdTimeErrorMessage"
|
||||
@input="onThresholdTimeChange"
|
||||
/>
|
||||
<!-- the mt-7 handles the label offset -->
|
||||
@@ -34,12 +34,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import validationMixin from './validationMixin';
|
||||
import validations from './validations';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
|
||||
export default {
|
||||
mixins: [validationMixin],
|
||||
props: {
|
||||
threshold: {
|
||||
type: Number,
|
||||
@@ -73,6 +71,19 @@ export default {
|
||||
};
|
||||
},
|
||||
validations,
|
||||
computed: {
|
||||
thresholdTimeErrorMessage() {
|
||||
let errorMessage = '';
|
||||
if (this.$v.thresholdTime.$error) {
|
||||
if (!this.$v.thresholdTime.numeric || !this.$v.thresholdTime.minValue) {
|
||||
errorMessage = this.$t(
|
||||
'SLA.FORM.THRESHOLD_TIME.INVALID_FORMAT_ERROR'
|
||||
);
|
||||
}
|
||||
}
|
||||
return errorMessage;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
threshold: {
|
||||
immediate: true,
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import Vuelidate from 'vuelidate';
|
||||
|
||||
import validationMixin from '../validationMixin';
|
||||
import validations from '../validations';
|
||||
import i18n from 'dashboard/i18n';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueI18n);
|
||||
localVue.use(Vuelidate);
|
||||
|
||||
const i18nConfig = new VueI18n({
|
||||
locale: 'en',
|
||||
messages: i18n,
|
||||
});
|
||||
|
||||
const TestComponent = {
|
||||
render() {},
|
||||
mixins: [validationMixin],
|
||||
validations,
|
||||
};
|
||||
|
||||
describe('validationMixin', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(TestComponent, {
|
||||
localVue,
|
||||
i18n: i18nConfig,
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
thresholdTime: '',
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not return required error message if name is empty but not touched', () => {
|
||||
wrapper.setData({ name: '' });
|
||||
expect(wrapper.vm.getSlaNameErrorMessage).toBe('');
|
||||
});
|
||||
|
||||
it('should return empty error message if name is valid', () => {
|
||||
wrapper.setData({ name: 'ValidName' });
|
||||
wrapper.vm.$v.name.$touch();
|
||||
expect(wrapper.vm.getSlaNameErrorMessage).toBe('');
|
||||
});
|
||||
|
||||
it('should return required error message if name is empty', () => {
|
||||
wrapper.setData({ name: '' });
|
||||
wrapper.vm.$v.name.$touch();
|
||||
expect(wrapper.vm.getSlaNameErrorMessage).toBe(
|
||||
wrapper.vm.$t('SLA.FORM.NAME.REQUIRED_ERROR')
|
||||
);
|
||||
});
|
||||
|
||||
it('should return minimum length error message if name is too short', () => {
|
||||
wrapper.setData({ name: 'a' });
|
||||
wrapper.vm.$v.name.$touch();
|
||||
expect(wrapper.vm.getSlaNameErrorMessage).toBe(
|
||||
wrapper.vm.$t('SLA.FORM.NAME.MINIMUM_LENGTH_ERROR')
|
||||
);
|
||||
});
|
||||
|
||||
it('should accept valid threshold values', () => {
|
||||
wrapper.setData({ thresholdTime: 10 });
|
||||
wrapper.vm.$v.thresholdTime.$touch();
|
||||
expect(wrapper.vm.getThresholdTimeErrorMessage).toBe(wrapper.vm.$t(''));
|
||||
|
||||
wrapper.setData({ thresholdTime: 10.5 });
|
||||
wrapper.vm.$v.thresholdTime.$touch();
|
||||
expect(wrapper.vm.getThresholdTimeErrorMessage).toBe(wrapper.vm.$t(''));
|
||||
});
|
||||
|
||||
it('should not return invalid format error message if thresholdTime is empty but not touched', () => {
|
||||
wrapper.setData({ thresholdTime: '' });
|
||||
expect(wrapper.vm.getThresholdTimeErrorMessage).toBe('');
|
||||
});
|
||||
|
||||
it('should return invalid format error message if thresholdTime has an invalid format', () => {
|
||||
wrapper.setData({ thresholdTime: 'fsdfsdfsdfsd' });
|
||||
wrapper.vm.$v.thresholdTime.$touch();
|
||||
|
||||
expect(wrapper.vm.getThresholdTimeErrorMessage).toBe(
|
||||
wrapper.vm.$t('SLA.FORM.THRESHOLD_TIME.INVALID_FORMAT_ERROR')
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject invalid threshold values', () => {
|
||||
wrapper.setData({ thresholdTime: 0 });
|
||||
wrapper.vm.$v.thresholdTime.$touch();
|
||||
expect(wrapper.vm.getThresholdTimeErrorMessage).toBe(
|
||||
wrapper.vm.$t('SLA.FORM.THRESHOLD_TIME.INVALID_FORMAT_ERROR')
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject invalid threshold values', () => {
|
||||
wrapper.setData({ thresholdTime: -1 });
|
||||
wrapper.vm.$v.thresholdTime.$touch();
|
||||
expect(wrapper.vm.getThresholdTimeErrorMessage).toBe(
|
||||
wrapper.vm.$t('SLA.FORM.THRESHOLD_TIME.INVALID_FORMAT_ERROR')
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
export default {
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
computed: {
|
||||
getSlaNameErrorMessage() {
|
||||
let errorMessage = '';
|
||||
if (this.v$.name.$error) {
|
||||
if (!this.v$.name.required) {
|
||||
errorMessage = this.$t('SLA.FORM.NAME.REQUIRED_ERROR');
|
||||
} else if (!this.v$.name.minLength) {
|
||||
errorMessage = this.$t('SLA.FORM.NAME.MINIMUM_LENGTH_ERROR');
|
||||
}
|
||||
}
|
||||
return errorMessage;
|
||||
},
|
||||
getThresholdTimeErrorMessage() {
|
||||
let errorMessage = '';
|
||||
if (this.v$.thresholdTime.$error) {
|
||||
if (!this.v$.thresholdTime.numeric || !this.v$.thresholdTime.minValue) {
|
||||
errorMessage = this.$t(
|
||||
'SLA.FORM.THRESHOLD_TIME.INVALID_FORMAT_ERROR'
|
||||
);
|
||||
}
|
||||
}
|
||||
return errorMessage;
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user