Merge branch 'develop' into feat/new-auth-screens
This commit is contained in:
commit
bba50e7e64
15 changed files with 657 additions and 54 deletions
|
@ -19,7 +19,7 @@ class V2::ReportBuilder
|
|||
# For backward compatible with old report
|
||||
def build
|
||||
timeseries.each_with_object([]) do |p, arr|
|
||||
arr << { value: p[1], timestamp: p[0].to_time.to_i }
|
||||
arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
:active-menu-item="activePrimaryMenu.key"
|
||||
@toggle-accounts="toggleAccountModal"
|
||||
@key-shortcut-modal="toggleKeyShortcutModal"
|
||||
@open-notification-panel="openNotificationPanel"
|
||||
/>
|
||||
<secondary-sidebar
|
||||
:account-id="accountId"
|
||||
|
@ -176,6 +177,9 @@ export default {
|
|||
showAddLabelPopup() {
|
||||
this.$emit('show-add-label-popup');
|
||||
},
|
||||
openNotificationPanel() {
|
||||
this.$emit('open-notification-panel');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
<template>
|
||||
<div class="notifications-link">
|
||||
<primary-nav-item
|
||||
name="NOTIFICATIONS"
|
||||
icon="alert"
|
||||
:to="`/app/accounts/${accountId}/notifications`"
|
||||
:count="unreadCount"
|
||||
/>
|
||||
<woot-button
|
||||
class-names="notifications-link--button"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
:class="{ 'is-active': isNotificationPanelActive }"
|
||||
@click="openNotificationPanel"
|
||||
>
|
||||
<fluent-icon icon="alert" />
|
||||
<span v-if="unreadCount" class="badge warning">{{ unreadCount }}</span>
|
||||
</woot-button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import PrimaryNavItem from './PrimaryNavItem';
|
||||
|
||||
export default {
|
||||
components: { PrimaryNavItem },
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
|
@ -28,8 +30,17 @@ export default {
|
|||
? `${this.notificationMetadata.unreadCount}`
|
||||
: '99+';
|
||||
},
|
||||
isNotificationPanelActive() {
|
||||
return this.$route.name === 'notifications_index';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openNotificationPanel() {
|
||||
if (this.$route.name !== 'notifications_index') {
|
||||
this.$emit('open-notification-panel');
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -37,4 +48,32 @@ export default {
|
|||
.notifications-link {
|
||||
margin-bottom: var(--space-small);
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
right: var(--space-minus-smaller);
|
||||
top: var(--space-minus-smaller);
|
||||
}
|
||||
.notifications-link--button {
|
||||
display: flex;
|
||||
position: relative;
|
||||
border-radius: var(--border-radius-large);
|
||||
border: 1px solid transparent;
|
||||
color: var(--s-600);
|
||||
margin: var(--space-small) 0;
|
||||
|
||||
&:hover {
|
||||
background: var(--w-50);
|
||||
color: var(--s-600);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--w-500);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: var(--w-50);
|
||||
color: var(--w-500);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
/>
|
||||
</nav>
|
||||
<div class="menu vertical user-menu">
|
||||
<notification-bell />
|
||||
<notification-bell @open-notification-panel="openNotificationPanel" />
|
||||
<agent-details @toggle-menu="toggleOptions" />
|
||||
<options-menu
|
||||
:show="showOptionsMenu"
|
||||
|
@ -83,6 +83,9 @@ export default {
|
|||
toggleSupportChatWindow() {
|
||||
window.$chatwoot.toggle();
|
||||
},
|
||||
openNotificationPanel() {
|
||||
this.$emit('open-notification-panel');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -14,6 +14,10 @@ const i18nConfig = new VueI18n({
|
|||
messages: i18n,
|
||||
});
|
||||
|
||||
const $route = {
|
||||
name: 'notifications_index',
|
||||
};
|
||||
|
||||
describe('notificationBell', () => {
|
||||
const accountId = 1;
|
||||
const notificationMetadata = { unreadCount: 19 };
|
||||
|
@ -45,24 +49,40 @@ describe('notificationBell', () => {
|
|||
});
|
||||
|
||||
it('it should return unread count 19 ', () => {
|
||||
const notificationBell = shallowMount(NotificationBell, {
|
||||
store,
|
||||
const wrapper = shallowMount(NotificationBell, {
|
||||
localVue,
|
||||
i18n: i18nConfig,
|
||||
store,
|
||||
mocks: {
|
||||
$route,
|
||||
},
|
||||
});
|
||||
|
||||
const statusViewTitle = notificationBell.find('primary-nav-item-stub');
|
||||
expect(statusViewTitle.vm.count).toBe('19');
|
||||
expect(wrapper.vm.unreadCount).toBe('19');
|
||||
});
|
||||
|
||||
it('it should return unread count 99+ ', async () => {
|
||||
notificationMetadata.unreadCount = 101;
|
||||
notificationMetadata.unreadCount = 100;
|
||||
const wrapper = shallowMount(NotificationBell, {
|
||||
localVue,
|
||||
i18n: i18nConfig,
|
||||
store,
|
||||
mocks: {
|
||||
$route,
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.unreadCount).toBe('99+');
|
||||
});
|
||||
|
||||
it('isNotificationPanelActive', async () => {
|
||||
const notificationBell = shallowMount(NotificationBell, {
|
||||
store,
|
||||
localVue,
|
||||
i18n: i18nConfig,
|
||||
mocks: {
|
||||
$route,
|
||||
},
|
||||
});
|
||||
const statusViewTitle = notificationBell.find('primary-nav-item-stub');
|
||||
expect(statusViewTitle.vm.count).toBe('99+');
|
||||
|
||||
expect(notificationBell.vm.isNotificationPanelActive).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -60,6 +60,13 @@
|
|||
"NOTIFICATIONS_PAGE": {
|
||||
"HEADER": "Notifications",
|
||||
"MARK_ALL_DONE": "Mark All Done",
|
||||
"DELETE_TITLE": "deleted",
|
||||
"UNREAD_NOTIFICATION": {
|
||||
"TITLE": "Unread Notifications",
|
||||
"ALL_NOTIFICATIONS": "View all notifications",
|
||||
"LOADING_UNREAD_MESSAGE": "Loading unread notifications...",
|
||||
"EMPTY_MESSAGE": "You have no unread notifications"
|
||||
},
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Loading notifications...",
|
||||
"404": "No Notifications",
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<sidebar
|
||||
:route="currentRoute"
|
||||
:class="sidebarClassName"
|
||||
@open-notification-panel="openNotificationPanel"
|
||||
@toggle-account-modal="toggleAccountModal"
|
||||
@open-key-shortcut-modal="toggleKeyShortcutModal"
|
||||
@close-key-shortcut-modal="closeKeyShortcutModal"
|
||||
|
@ -25,6 +26,10 @@
|
|||
@close="closeKeyShortcutModal"
|
||||
@clickaway="closeKeyShortcutModal"
|
||||
/>
|
||||
<notification-panel
|
||||
v-if="isNotificationPanel"
|
||||
@close="closeNotificationPanel"
|
||||
/>
|
||||
<woot-modal :show.sync="showAddLabelModal" :on-close="hideAddLabelPopup">
|
||||
<add-label-modal @close="hideAddLabelPopup" />
|
||||
</woot-modal>
|
||||
|
@ -40,6 +45,7 @@ import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShor
|
|||
import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAccountModal';
|
||||
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector';
|
||||
import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.vue';
|
||||
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -49,6 +55,7 @@ export default {
|
|||
AddAccountModal,
|
||||
AccountSelector,
|
||||
AddLabelModal,
|
||||
NotificationPanel,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -58,6 +65,7 @@ export default {
|
|||
showCreateAccountModal: false,
|
||||
showAddLabelModal: false,
|
||||
showShortcutModal: false,
|
||||
isNotificationPanel: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -126,6 +134,12 @@ export default {
|
|||
hideAddLabelPopup() {
|
||||
this.showAddLabelModal = false;
|
||||
},
|
||||
openNotificationPanel() {
|
||||
this.isNotificationPanel = true;
|
||||
},
|
||||
closeNotificationPanel() {
|
||||
this.isNotificationPanel = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,274 @@
|
|||
<template>
|
||||
<div class="modal-mask">
|
||||
<div
|
||||
v-on-clickaway="closeNotificationPanel"
|
||||
class="notification-wrap flex-space-between"
|
||||
>
|
||||
<div class="header-wrap w-full flex-space-between">
|
||||
<div class="header-title--wrap flex-view">
|
||||
<span class="header-title">
|
||||
{{ $t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.TITLE') }}
|
||||
</span>
|
||||
<span v-if="totalUnreadNotifications" class="total-count block-title">
|
||||
{{ totalUnreadNotifications }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-view">
|
||||
<woot-button
|
||||
v-if="!noUnreadNotificationAvailable"
|
||||
color-scheme="primary"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
class-names="action-button"
|
||||
:is-loading="uiFlags.isUpdating"
|
||||
@click="onMarkAllDoneClick"
|
||||
>
|
||||
{{ $t('NOTIFICATIONS_PAGE.MARK_ALL_DONE') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
color-scheme="secondary"
|
||||
variant="link"
|
||||
size="tiny"
|
||||
icon="dismiss"
|
||||
@click="closeNotificationPanel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<notification-panel-list
|
||||
:notifications="getUnreadNotifications"
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:on-click-notification="openConversation"
|
||||
:in-last-page="inLastPage"
|
||||
/>
|
||||
<div v-if="records.length !== 0" class="footer-wrap flex-space-between">
|
||||
<div class="flex-view">
|
||||
<woot-button
|
||||
size="medium"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
class-names="page-change--button"
|
||||
:is-disabled="inFirstPage"
|
||||
@click="onClickFirstPage"
|
||||
>
|
||||
<fluent-icon icon="chevron-left" size="16" />
|
||||
<fluent-icon
|
||||
icon="chevron-left"
|
||||
size="16"
|
||||
class="margin-left-minus-slab"
|
||||
/>
|
||||
</woot-button>
|
||||
<woot-button
|
||||
color-scheme="secondary"
|
||||
variant="clear"
|
||||
size="medium"
|
||||
icon="chevron-left"
|
||||
:disabled="inFirstPage"
|
||||
@click="onClickPreviousPage"
|
||||
>
|
||||
</woot-button>
|
||||
</div>
|
||||
<span class="page-count"> {{ currentPage }} - {{ lastPage }} </span>
|
||||
<div class="flex-view">
|
||||
<woot-button
|
||||
color-scheme="secondary"
|
||||
variant="clear"
|
||||
size="medium"
|
||||
icon="chevron-right"
|
||||
:disabled="inLastPage"
|
||||
@click="onClickNextPage"
|
||||
>
|
||||
</woot-button>
|
||||
<woot-button
|
||||
size="medium"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
class-names="page-change--button"
|
||||
:disabled="inLastPage"
|
||||
@click="onClickLastPage"
|
||||
>
|
||||
<fluent-icon icon="chevron-right" size="16" />
|
||||
<fluent-icon
|
||||
icon="chevron-right"
|
||||
size="16"
|
||||
class="margin-left-minus-slab"
|
||||
/>
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
|
||||
import NotificationPanelList from './NotificationPanelList';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NotificationPanelList,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
data() {
|
||||
return {
|
||||
pageSize: 15,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
meta: 'notifications/getMeta',
|
||||
records: 'notifications/getNotifications',
|
||||
uiFlags: 'notifications/getUIFlags',
|
||||
}),
|
||||
totalUnreadNotifications() {
|
||||
return this.meta.unreadCount;
|
||||
},
|
||||
noUnreadNotificationAvailable() {
|
||||
return this.meta.unreadCount === 0;
|
||||
},
|
||||
getUnreadNotifications() {
|
||||
return this.records.filter(notification => notification.read_at === null);
|
||||
},
|
||||
currentPage() {
|
||||
return Number(this.meta.currentPage);
|
||||
},
|
||||
lastPage() {
|
||||
if (this.totalUnreadNotifications > 15) {
|
||||
return Math.ceil(this.totalUnreadNotifications / this.pageSize);
|
||||
}
|
||||
return 1;
|
||||
},
|
||||
inFirstPage() {
|
||||
const page = Number(this.meta.currentPage);
|
||||
return page === 1;
|
||||
},
|
||||
inLastPage() {
|
||||
return this.currentPage === this.lastPage;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('notifications/get', { page: 1 });
|
||||
},
|
||||
methods: {
|
||||
onPageChange(page) {
|
||||
this.$store.dispatch('notifications/get', { page });
|
||||
},
|
||||
openConversation(notification) {
|
||||
const {
|
||||
primary_actor_id: primaryActorId,
|
||||
primary_actor_type: primaryActorType,
|
||||
primary_actor: { id: conversationId },
|
||||
} = notification;
|
||||
|
||||
this.$store.dispatch('notifications/read', {
|
||||
primaryActorId,
|
||||
primaryActorType,
|
||||
unreadCount: this.meta.unreadCount,
|
||||
});
|
||||
this.$router.push({
|
||||
name: 'inbox_conversation',
|
||||
params: { conversation_id: conversationId },
|
||||
});
|
||||
this.$emit('close');
|
||||
},
|
||||
onClickNextPage() {
|
||||
if (!this.inLastPage) {
|
||||
const page = this.currentPage + 1;
|
||||
this.onPageChange(page);
|
||||
}
|
||||
},
|
||||
onClickPreviousPage() {
|
||||
if (!this.inFirstPage) {
|
||||
const page = this.currentPage - 1;
|
||||
this.onPageChange(page);
|
||||
}
|
||||
},
|
||||
onClickFirstPage() {
|
||||
if (!this.inFirstPage) {
|
||||
const page = 1;
|
||||
this.onPageChange(page);
|
||||
}
|
||||
},
|
||||
onClickLastPage() {
|
||||
if (!this.inLastPage) {
|
||||
const page = this.lastPage;
|
||||
this.onPageChange(page);
|
||||
}
|
||||
},
|
||||
onMarkAllDoneClick() {
|
||||
this.$store.dispatch('notifications/readAll');
|
||||
},
|
||||
closeNotificationPanel() {
|
||||
this.$emit('close');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.flex-view {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-space-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.notification-wrap {
|
||||
flex-direction: column;
|
||||
height: 90vh;
|
||||
width: 52rem;
|
||||
background-color: var(--white);
|
||||
border-radius: var(--border-radius-medium);
|
||||
position: absolute;
|
||||
left: var(--space-jumbo);
|
||||
margin: var(--space-small);
|
||||
}
|
||||
.header-wrap {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--s-50);
|
||||
padding: var(--space-two) var(--space-medium) var(--space-slab)
|
||||
var(--space-medium);
|
||||
|
||||
.header-title--wrap {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: var(--font-size-two);
|
||||
font-weight: var(--font-weight-black);
|
||||
}
|
||||
|
||||
.total-count {
|
||||
padding: var(--space-smaller) var(--space-small);
|
||||
background: var(--b-50);
|
||||
border-radius: var(--border-radius-rounded);
|
||||
font-size: var(--font-size-micro);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.action-button {
|
||||
padding: var(--space-micro) var(--space-small);
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
}
|
||||
|
||||
.page-count {
|
||||
font-size: var(--font-size-micro);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--s-500);
|
||||
}
|
||||
|
||||
.footer-wrap {
|
||||
align-items: center;
|
||||
padding: var(--space-smaller) var(--space-two);
|
||||
}
|
||||
|
||||
.page-change--button:hover {
|
||||
background: var(--s-50);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,219 @@
|
|||
<template>
|
||||
<div class="notification-list-item--wrap h-full flex-view ">
|
||||
<woot-button
|
||||
v-for="notificationItem in notifications"
|
||||
v-show="!isLoading"
|
||||
:key="notificationItem.id"
|
||||
size="expanded"
|
||||
color-scheme="secondary"
|
||||
variant="link"
|
||||
@click="() => onClickNotification(notificationItem)"
|
||||
>
|
||||
<div class="notification-list--wrap flex-view w-full">
|
||||
<div
|
||||
v-if="!notificationItem.read_at"
|
||||
class="notification-unread--indicator"
|
||||
></div>
|
||||
<div v-else class="empty flex-view"></div>
|
||||
<div class="notification-content--wrap w-full flex-space-between">
|
||||
<div class="flex-space-between">
|
||||
<div class="title-wrap flex-view ">
|
||||
<span class="notification-title">
|
||||
{{
|
||||
`#${
|
||||
notificationItem.primary_actor
|
||||
? notificationItem.primary_actor.id
|
||||
: $t(`NOTIFICATIONS_PAGE.DELETE_TITLE`)
|
||||
}`
|
||||
}}
|
||||
</span>
|
||||
<span class="notification-type">
|
||||
{{
|
||||
$t(
|
||||
`NOTIFICATIONS_PAGE.TYPE_LABEL.${notificationItem.notification_type}`
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<thumbnail
|
||||
v-if="notificationItem.primary_actor.meta.assignee"
|
||||
:src="notificationItem.primary_actor.meta.assignee.thumbnail"
|
||||
size="16px"
|
||||
:username="notificationItem.primary_actor.meta.assignee.name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex-view ">
|
||||
<span class="notification-message text-truncate">
|
||||
{{ notificationItem.push_message_title }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="timestamp flex-view">
|
||||
{{ dynamicTime(notificationItem.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</woot-button>
|
||||
<empty-state
|
||||
v-if="showEmptyResult"
|
||||
:title="$t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.EMPTY_MESSAGE')"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="!isLoading && inLastPage"
|
||||
size="medium"
|
||||
variant="clear"
|
||||
color-scheme="primary"
|
||||
class-names="action-button"
|
||||
@click="openNotificationPage"
|
||||
>
|
||||
{{ $t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.ALL_NOTIFICATIONS') }}
|
||||
</woot-button>
|
||||
<div v-if="isLoading" class="notifications-loader flex-view">
|
||||
<spinner />
|
||||
<span>{{
|
||||
$t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.LOADING_UNREAD_MESSAGE')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
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';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
Spinner,
|
||||
EmptyState,
|
||||
},
|
||||
mixins: [timeMixin],
|
||||
props: {
|
||||
notifications: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
onClickNotification: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
inLastPage: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
notificationMetadata: 'notifications/getMeta',
|
||||
}),
|
||||
showEmptyResult() {
|
||||
return !this.isLoading && this.notifications.length === 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openNotificationPage() {
|
||||
if (this.$route.name !== 'notifications_index') {
|
||||
this.$router.push({
|
||||
name: 'notifications_index',
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.flex-view {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-space-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.notification-list-item--wrap {
|
||||
flex-direction: column;
|
||||
padding: var(--space-small) var(--space-slab);
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.empty {
|
||||
width: var(--space-small);
|
||||
}
|
||||
|
||||
.notification-list--wrap {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: var(--space-slab);
|
||||
line-height: 1.4;
|
||||
border-bottom: 1px solid var(--b-50);
|
||||
}
|
||||
|
||||
.notification-list--wrap:hover {
|
||||
background: var(--b-100);
|
||||
border-radius: var(--border-radius-normal);
|
||||
}
|
||||
|
||||
.notification-content--wrap {
|
||||
flex-direction: column;
|
||||
margin-left: var(--space-slab);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.title-wrap {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
font-weight: var(--font-weight-black);
|
||||
}
|
||||
|
||||
.notification-type {
|
||||
font-size: var(--font-size-micro);
|
||||
padding: var(--space-micro) var(--space-smaller);
|
||||
margin-left: var(--space-small);
|
||||
background: var(--s-50);
|
||||
border-radius: var(--border-radius-normal);
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
color: var(--color-body);
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
margin-top: var(--space-smaller);
|
||||
color: var(--b-500);
|
||||
font-size: var(--font-size-micro);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.notification-unread--indicator {
|
||||
width: var(--space-small);
|
||||
height: var(--space-small);
|
||||
border-radius: var(--border-radius-rounded);
|
||||
background: var(--color-woot);
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin-top: var(--space-slab);
|
||||
}
|
||||
|
||||
.notifications-loader {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: var(--space-larger) var(--space-small);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
</style>
|
|
@ -17,17 +17,17 @@
|
|||
@click="() => onClickNotification(notificationItem)"
|
||||
>
|
||||
<td>
|
||||
<div class="">
|
||||
<div class="flex-view notification-contant--wrap">
|
||||
<h5 class="notification--title">
|
||||
{{
|
||||
`#${
|
||||
notificationItem.primary_actor
|
||||
? notificationItem.primary_actor.id
|
||||
: 'deleted'
|
||||
: $t(`NOTIFICATIONS_PAGE.DELETE_TITLE`)
|
||||
}`
|
||||
}}
|
||||
</h5>
|
||||
<span class="notification--message-title">
|
||||
<span class="notification--message-title text-truncate">
|
||||
{{ notificationItem.push_message_title }}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -197,6 +197,11 @@ export default {
|
|||
text-align: right;
|
||||
}
|
||||
|
||||
.notification-contant--wrap {
|
||||
flex-direction: column;
|
||||
max-width: 50rem;
|
||||
}
|
||||
|
||||
.notification--message-title {
|
||||
color: var(--s-700);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="flex overflow-hidden">
|
||||
<div class="flex">
|
||||
<span
|
||||
v-for="(user, index) in users"
|
||||
:key="user.id"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="px-5">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="text-black-700">
|
||||
<div class="text-black-700 max-w-xs">
|
||||
<div class="text-base leading-5 font-medium mb-1">
|
||||
{{
|
||||
isOnline
|
||||
|
|
|
@ -40,6 +40,10 @@ module ConversationReplyMailerHelper
|
|||
end
|
||||
|
||||
def email_smtp_enabled
|
||||
@inbox.inbox_type == 'Email' && @channel.smtp_enabled
|
||||
end
|
||||
|
||||
def email_imap_enabled
|
||||
@inbox.inbox_type == 'Email' && @channel.imap_enabled
|
||||
end
|
||||
|
||||
|
@ -48,6 +52,6 @@ module ConversationReplyMailerHelper
|
|||
end
|
||||
|
||||
def email_reply_to
|
||||
email_smtp_enabled ? @channel.smtp_email : reply_email
|
||||
email_imap_enabled ? @channel.imap_email : reply_email
|
||||
end
|
||||
end
|
||||
|
|
|
@ -111,7 +111,7 @@ class Account < ApplicationRecord
|
|||
end
|
||||
|
||||
def support_email
|
||||
super || GlobalConfig.get('MAILER_SUPPORT_EMAIL')['MAILER_SUPPORT_EMAIL'] || ENV.fetch('MAILER_SENDER_EMAIL', 'Chatwoot <accounts@chatwoot.com>')
|
||||
super || ENV['MAILER_SENDER_EMAIL'] || GlobalConfig.get('MAILER_SUPPORT_EMAIL')['MAILER_SUPPORT_EMAIL']
|
||||
end
|
||||
|
||||
def usage_limits
|
||||
|
|
|
@ -7,6 +7,8 @@ RSpec.describe 'Reports API', type: :request do
|
|||
let!(:user) { create(:user, account: account) }
|
||||
let!(:inbox) { create(:inbox, account: account) }
|
||||
let(:inbox_member) { create(:inbox_member, user: user, inbox: inbox) }
|
||||
let(:date_timestamp) { Time.current.beginning_of_day.to_i }
|
||||
let(:params) { { timezone_offset: Time.zone.utc_offset } }
|
||||
|
||||
before do
|
||||
create_list(:conversation, 10, account: account, inbox: inbox,
|
||||
|
@ -23,12 +25,14 @@ RSpec.describe 'Reports API', type: :request do
|
|||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
params = {
|
||||
metric: 'conversations_count',
|
||||
type: :account,
|
||||
since: Time.zone.today.to_time.to_i.to_s,
|
||||
until: Time.zone.today.to_time.to_i.to_s
|
||||
}
|
||||
let(:params) do
|
||||
super().merge(
|
||||
metric: 'conversations_count',
|
||||
type: :account,
|
||||
since: date_timestamp.to_s,
|
||||
until: date_timestamp.to_s
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns unauthorized for agents' do
|
||||
get "/api/v2/accounts/#{account.id}/reports",
|
||||
|
@ -48,7 +52,7 @@ RSpec.describe 'Reports API', type: :request do
|
|||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
|
||||
current_day_metric = json_response.select { |x| x['timestamp'] == Time.zone.today.to_time.to_i }
|
||||
current_day_metric = json_response.select { |x| x['timestamp'] == date_timestamp }
|
||||
expect(current_day_metric.length).to eq(1)
|
||||
expect(current_day_metric[0]['value']).to eq(10)
|
||||
end
|
||||
|
@ -65,11 +69,13 @@ RSpec.describe 'Reports API', type: :request do
|
|||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
params = {
|
||||
type: :account,
|
||||
since: Time.zone.today.to_time.to_i.to_s,
|
||||
until: Time.zone.today.to_time.to_i.to_s
|
||||
}
|
||||
let(:params) do
|
||||
super().merge(
|
||||
type: :account,
|
||||
since: date_timestamp.to_s,
|
||||
until: date_timestamp.to_s
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns unauthorized for agents' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/summary",
|
||||
|
@ -104,10 +110,12 @@ RSpec.describe 'Reports API', type: :request do
|
|||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
params = {
|
||||
since: 30.days.ago.to_i.to_s,
|
||||
until: Time.zone.today.to_time.to_i.to_s
|
||||
}
|
||||
let(:params) do
|
||||
super().merge(
|
||||
since: 30.days.ago.to_i.to_s,
|
||||
until: date_timestamp.to_s
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns unauthorized for agents' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/agents.csv",
|
||||
|
@ -137,10 +145,12 @@ RSpec.describe 'Reports API', type: :request do
|
|||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
params = {
|
||||
since: 30.days.ago.to_i.to_s,
|
||||
until: Time.zone.today.to_time.to_i.to_s
|
||||
}
|
||||
let(:params) do
|
||||
super().merge(
|
||||
since: 30.days.ago.to_i.to_s,
|
||||
until: date_timestamp.to_s
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns unauthorized for inboxes' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/inboxes",
|
||||
|
@ -170,10 +180,12 @@ RSpec.describe 'Reports API', type: :request do
|
|||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
params = {
|
||||
since: 30.days.ago.to_i.to_s,
|
||||
until: Time.zone.today.to_time.to_i.to_s
|
||||
}
|
||||
let(:params) do
|
||||
super().merge(
|
||||
since: 30.days.ago.to_i.to_s,
|
||||
until: date_timestamp.to_s
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns unauthorized for labels' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/labels.csv",
|
||||
|
@ -203,10 +215,12 @@ RSpec.describe 'Reports API', type: :request do
|
|||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
params = {
|
||||
since: 30.days.ago.to_i.to_s,
|
||||
until: Time.zone.today.to_time.to_i.to_s
|
||||
}
|
||||
let(:params) do
|
||||
super().merge(
|
||||
since: 30.days.ago.to_i.to_s,
|
||||
until: date_timestamp.to_s
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns unauthorized for teams' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/teams.csv",
|
||||
|
|
Loading…
Reference in a new issue