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
|
# For backward compatible with old report
|
||||||
def build
|
def build
|
||||||
timeseries.each_with_object([]) do |p, arr|
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
:active-menu-item="activePrimaryMenu.key"
|
:active-menu-item="activePrimaryMenu.key"
|
||||||
@toggle-accounts="toggleAccountModal"
|
@toggle-accounts="toggleAccountModal"
|
||||||
@key-shortcut-modal="toggleKeyShortcutModal"
|
@key-shortcut-modal="toggleKeyShortcutModal"
|
||||||
|
@open-notification-panel="openNotificationPanel"
|
||||||
/>
|
/>
|
||||||
<secondary-sidebar
|
<secondary-sidebar
|
||||||
:account-id="accountId"
|
:account-id="accountId"
|
||||||
|
@ -176,6 +177,9 @@ export default {
|
||||||
showAddLabelPopup() {
|
showAddLabelPopup() {
|
||||||
this.$emit('show-add-label-popup');
|
this.$emit('show-add-label-popup');
|
||||||
},
|
},
|
||||||
|
openNotificationPanel() {
|
||||||
|
this.$emit('open-notification-panel');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="notifications-link">
|
<div class="notifications-link">
|
||||||
<primary-nav-item
|
<woot-button
|
||||||
name="NOTIFICATIONS"
|
class-names="notifications-link--button"
|
||||||
icon="alert"
|
variant="clear"
|
||||||
:to="`/app/accounts/${accountId}/notifications`"
|
color-scheme="secondary"
|
||||||
:count="unreadCount"
|
:class="{ 'is-active': isNotificationPanelActive }"
|
||||||
/>
|
@click="openNotificationPanel"
|
||||||
|
>
|
||||||
|
<fluent-icon icon="alert" />
|
||||||
|
<span v-if="unreadCount" class="badge warning">{{ unreadCount }}</span>
|
||||||
|
</woot-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import PrimaryNavItem from './PrimaryNavItem';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { PrimaryNavItem },
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
accountId: 'getCurrentAccountId',
|
accountId: 'getCurrentAccountId',
|
||||||
|
@ -28,8 +30,17 @@ export default {
|
||||||
? `${this.notificationMetadata.unreadCount}`
|
? `${this.notificationMetadata.unreadCount}`
|
||||||
: '99+';
|
: '99+';
|
||||||
},
|
},
|
||||||
|
isNotificationPanelActive() {
|
||||||
|
return this.$route.name === 'notifications_index';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openNotificationPanel() {
|
||||||
|
if (this.$route.name !== 'notifications_index') {
|
||||||
|
this.$emit('open-notification-panel');
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -37,4 +48,32 @@ export default {
|
||||||
.notifications-link {
|
.notifications-link {
|
||||||
margin-bottom: var(--space-small);
|
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>
|
</style>
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="menu vertical user-menu">
|
<div class="menu vertical user-menu">
|
||||||
<notification-bell />
|
<notification-bell @open-notification-panel="openNotificationPanel" />
|
||||||
<agent-details @toggle-menu="toggleOptions" />
|
<agent-details @toggle-menu="toggleOptions" />
|
||||||
<options-menu
|
<options-menu
|
||||||
:show="showOptionsMenu"
|
:show="showOptionsMenu"
|
||||||
|
@ -83,6 +83,9 @@ export default {
|
||||||
toggleSupportChatWindow() {
|
toggleSupportChatWindow() {
|
||||||
window.$chatwoot.toggle();
|
window.$chatwoot.toggle();
|
||||||
},
|
},
|
||||||
|
openNotificationPanel() {
|
||||||
|
this.$emit('open-notification-panel');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -14,6 +14,10 @@ const i18nConfig = new VueI18n({
|
||||||
messages: i18n,
|
messages: i18n,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const $route = {
|
||||||
|
name: 'notifications_index',
|
||||||
|
};
|
||||||
|
|
||||||
describe('notificationBell', () => {
|
describe('notificationBell', () => {
|
||||||
const accountId = 1;
|
const accountId = 1;
|
||||||
const notificationMetadata = { unreadCount: 19 };
|
const notificationMetadata = { unreadCount: 19 };
|
||||||
|
@ -45,24 +49,40 @@ describe('notificationBell', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should return unread count 19 ', () => {
|
it('it should return unread count 19 ', () => {
|
||||||
const notificationBell = shallowMount(NotificationBell, {
|
const wrapper = shallowMount(NotificationBell, {
|
||||||
store,
|
|
||||||
localVue,
|
localVue,
|
||||||
i18n: i18nConfig,
|
i18n: i18nConfig,
|
||||||
|
store,
|
||||||
|
mocks: {
|
||||||
|
$route,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
expect(wrapper.vm.unreadCount).toBe('19');
|
||||||
const statusViewTitle = notificationBell.find('primary-nav-item-stub');
|
|
||||||
expect(statusViewTitle.vm.count).toBe('19');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should return unread count 99+ ', async () => {
|
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, {
|
const notificationBell = shallowMount(NotificationBell, {
|
||||||
store,
|
store,
|
||||||
localVue,
|
localVue,
|
||||||
i18n: i18nConfig,
|
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": {
|
"NOTIFICATIONS_PAGE": {
|
||||||
"HEADER": "Notifications",
|
"HEADER": "Notifications",
|
||||||
"MARK_ALL_DONE": "Mark All Done",
|
"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": {
|
"LIST": {
|
||||||
"LOADING_MESSAGE": "Loading notifications...",
|
"LOADING_MESSAGE": "Loading notifications...",
|
||||||
"404": "No Notifications",
|
"404": "No Notifications",
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<sidebar
|
<sidebar
|
||||||
:route="currentRoute"
|
:route="currentRoute"
|
||||||
:class="sidebarClassName"
|
:class="sidebarClassName"
|
||||||
|
@open-notification-panel="openNotificationPanel"
|
||||||
@toggle-account-modal="toggleAccountModal"
|
@toggle-account-modal="toggleAccountModal"
|
||||||
@open-key-shortcut-modal="toggleKeyShortcutModal"
|
@open-key-shortcut-modal="toggleKeyShortcutModal"
|
||||||
@close-key-shortcut-modal="closeKeyShortcutModal"
|
@close-key-shortcut-modal="closeKeyShortcutModal"
|
||||||
|
@ -25,6 +26,10 @@
|
||||||
@close="closeKeyShortcutModal"
|
@close="closeKeyShortcutModal"
|
||||||
@clickaway="closeKeyShortcutModal"
|
@clickaway="closeKeyShortcutModal"
|
||||||
/>
|
/>
|
||||||
|
<notification-panel
|
||||||
|
v-if="isNotificationPanel"
|
||||||
|
@close="closeNotificationPanel"
|
||||||
|
/>
|
||||||
<woot-modal :show.sync="showAddLabelModal" :on-close="hideAddLabelPopup">
|
<woot-modal :show.sync="showAddLabelModal" :on-close="hideAddLabelPopup">
|
||||||
<add-label-modal @close="hideAddLabelPopup" />
|
<add-label-modal @close="hideAddLabelPopup" />
|
||||||
</woot-modal>
|
</woot-modal>
|
||||||
|
@ -40,6 +45,7 @@ import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShor
|
||||||
import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAccountModal';
|
import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAccountModal';
|
||||||
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector';
|
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector';
|
||||||
import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.vue';
|
import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.vue';
|
||||||
|
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -49,6 +55,7 @@ export default {
|
||||||
AddAccountModal,
|
AddAccountModal,
|
||||||
AccountSelector,
|
AccountSelector,
|
||||||
AddLabelModal,
|
AddLabelModal,
|
||||||
|
NotificationPanel,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -58,6 +65,7 @@ export default {
|
||||||
showCreateAccountModal: false,
|
showCreateAccountModal: false,
|
||||||
showAddLabelModal: false,
|
showAddLabelModal: false,
|
||||||
showShortcutModal: false,
|
showShortcutModal: false,
|
||||||
|
isNotificationPanel: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -126,6 +134,12 @@ export default {
|
||||||
hideAddLabelPopup() {
|
hideAddLabelPopup() {
|
||||||
this.showAddLabelModal = false;
|
this.showAddLabelModal = false;
|
||||||
},
|
},
|
||||||
|
openNotificationPanel() {
|
||||||
|
this.isNotificationPanel = true;
|
||||||
|
},
|
||||||
|
closeNotificationPanel() {
|
||||||
|
this.isNotificationPanel = false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</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)"
|
@click="() => onClickNotification(notificationItem)"
|
||||||
>
|
>
|
||||||
<td>
|
<td>
|
||||||
<div class="">
|
<div class="flex-view notification-contant--wrap">
|
||||||
<h5 class="notification--title">
|
<h5 class="notification--title">
|
||||||
{{
|
{{
|
||||||
`#${
|
`#${
|
||||||
notificationItem.primary_actor
|
notificationItem.primary_actor
|
||||||
? notificationItem.primary_actor.id
|
? notificationItem.primary_actor.id
|
||||||
: 'deleted'
|
: $t(`NOTIFICATIONS_PAGE.DELETE_TITLE`)
|
||||||
}`
|
}`
|
||||||
}}
|
}}
|
||||||
</h5>
|
</h5>
|
||||||
<span class="notification--message-title">
|
<span class="notification--message-title text-truncate">
|
||||||
{{ notificationItem.push_message_title }}
|
{{ notificationItem.push_message_title }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -197,6 +197,11 @@ export default {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-contant--wrap {
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 50rem;
|
||||||
|
}
|
||||||
|
|
||||||
.notification--message-title {
|
.notification--message-title {
|
||||||
color: var(--s-700);
|
color: var(--s-700);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex overflow-hidden">
|
<div class="flex">
|
||||||
<span
|
<span
|
||||||
v-for="(user, index) in users"
|
v-for="(user, index) in users"
|
||||||
:key="user.id"
|
:key="user.id"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="px-5">
|
<div class="px-5">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<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">
|
<div class="text-base leading-5 font-medium mb-1">
|
||||||
{{
|
{{
|
||||||
isOnline
|
isOnline
|
||||||
|
|
|
@ -40,6 +40,10 @@ module ConversationReplyMailerHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def email_smtp_enabled
|
def email_smtp_enabled
|
||||||
|
@inbox.inbox_type == 'Email' && @channel.smtp_enabled
|
||||||
|
end
|
||||||
|
|
||||||
|
def email_imap_enabled
|
||||||
@inbox.inbox_type == 'Email' && @channel.imap_enabled
|
@inbox.inbox_type == 'Email' && @channel.imap_enabled
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -48,6 +52,6 @@ module ConversationReplyMailerHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def email_reply_to
|
def email_reply_to
|
||||||
email_smtp_enabled ? @channel.smtp_email : reply_email
|
email_imap_enabled ? @channel.imap_email : reply_email
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -111,7 +111,7 @@ class Account < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def support_email
|
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
|
end
|
||||||
|
|
||||||
def usage_limits
|
def usage_limits
|
||||||
|
|
|
@ -7,6 +7,8 @@ RSpec.describe 'Reports API', type: :request do
|
||||||
let!(:user) { create(:user, account: account) }
|
let!(:user) { create(:user, account: account) }
|
||||||
let!(:inbox) { create(:inbox, account: account) }
|
let!(:inbox) { create(:inbox, account: account) }
|
||||||
let(:inbox_member) { create(:inbox_member, user: user, inbox: inbox) }
|
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
|
before do
|
||||||
create_list(:conversation, 10, account: account, inbox: inbox,
|
create_list(:conversation, 10, account: account, inbox: inbox,
|
||||||
|
@ -23,12 +25,14 @@ RSpec.describe 'Reports API', type: :request do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when it is an authenticated user' do
|
context 'when it is an authenticated user' do
|
||||||
params = {
|
let(:params) do
|
||||||
metric: 'conversations_count',
|
super().merge(
|
||||||
type: :account,
|
metric: 'conversations_count',
|
||||||
since: Time.zone.today.to_time.to_i.to_s,
|
type: :account,
|
||||||
until: Time.zone.today.to_time.to_i.to_s
|
since: date_timestamp.to_s,
|
||||||
}
|
until: date_timestamp.to_s
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
it 'returns unauthorized for agents' do
|
it 'returns unauthorized for agents' do
|
||||||
get "/api/v2/accounts/#{account.id}/reports",
|
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)
|
expect(response).to have_http_status(:success)
|
||||||
json_response = JSON.parse(response.body)
|
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.length).to eq(1)
|
||||||
expect(current_day_metric[0]['value']).to eq(10)
|
expect(current_day_metric[0]['value']).to eq(10)
|
||||||
end
|
end
|
||||||
|
@ -65,11 +69,13 @@ RSpec.describe 'Reports API', type: :request do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when it is an authenticated user' do
|
context 'when it is an authenticated user' do
|
||||||
params = {
|
let(:params) do
|
||||||
type: :account,
|
super().merge(
|
||||||
since: Time.zone.today.to_time.to_i.to_s,
|
type: :account,
|
||||||
until: Time.zone.today.to_time.to_i.to_s
|
since: date_timestamp.to_s,
|
||||||
}
|
until: date_timestamp.to_s
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
it 'returns unauthorized for agents' do
|
it 'returns unauthorized for agents' do
|
||||||
get "/api/v2/accounts/#{account.id}/reports/summary",
|
get "/api/v2/accounts/#{account.id}/reports/summary",
|
||||||
|
@ -104,10 +110,12 @@ RSpec.describe 'Reports API', type: :request do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when it is an authenticated user' do
|
context 'when it is an authenticated user' do
|
||||||
params = {
|
let(:params) do
|
||||||
since: 30.days.ago.to_i.to_s,
|
super().merge(
|
||||||
until: Time.zone.today.to_time.to_i.to_s
|
since: 30.days.ago.to_i.to_s,
|
||||||
}
|
until: date_timestamp.to_s
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
it 'returns unauthorized for agents' do
|
it 'returns unauthorized for agents' do
|
||||||
get "/api/v2/accounts/#{account.id}/reports/agents.csv",
|
get "/api/v2/accounts/#{account.id}/reports/agents.csv",
|
||||||
|
@ -137,10 +145,12 @@ RSpec.describe 'Reports API', type: :request do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when it is an authenticated user' do
|
context 'when it is an authenticated user' do
|
||||||
params = {
|
let(:params) do
|
||||||
since: 30.days.ago.to_i.to_s,
|
super().merge(
|
||||||
until: Time.zone.today.to_time.to_i.to_s
|
since: 30.days.ago.to_i.to_s,
|
||||||
}
|
until: date_timestamp.to_s
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
it 'returns unauthorized for inboxes' do
|
it 'returns unauthorized for inboxes' do
|
||||||
get "/api/v2/accounts/#{account.id}/reports/inboxes",
|
get "/api/v2/accounts/#{account.id}/reports/inboxes",
|
||||||
|
@ -170,10 +180,12 @@ RSpec.describe 'Reports API', type: :request do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when it is an authenticated user' do
|
context 'when it is an authenticated user' do
|
||||||
params = {
|
let(:params) do
|
||||||
since: 30.days.ago.to_i.to_s,
|
super().merge(
|
||||||
until: Time.zone.today.to_time.to_i.to_s
|
since: 30.days.ago.to_i.to_s,
|
||||||
}
|
until: date_timestamp.to_s
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
it 'returns unauthorized for labels' do
|
it 'returns unauthorized for labels' do
|
||||||
get "/api/v2/accounts/#{account.id}/reports/labels.csv",
|
get "/api/v2/accounts/#{account.id}/reports/labels.csv",
|
||||||
|
@ -203,10 +215,12 @@ RSpec.describe 'Reports API', type: :request do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when it is an authenticated user' do
|
context 'when it is an authenticated user' do
|
||||||
params = {
|
let(:params) do
|
||||||
since: 30.days.ago.to_i.to_s,
|
super().merge(
|
||||||
until: Time.zone.today.to_time.to_i.to_s
|
since: 30.days.ago.to_i.to_s,
|
||||||
}
|
until: date_timestamp.to_s
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
it 'returns unauthorized for teams' do
|
it 'returns unauthorized for teams' do
|
||||||
get "/api/v2/accounts/#{account.id}/reports/teams.csv",
|
get "/api/v2/accounts/#{account.id}/reports/teams.csv",
|
||||||
|
|
Loading…
Reference in a new issue