Merge branch 'develop' into feat/scroll-to-notification
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
import {
|
||||
CMD_BULK_ACTION_SNOOZE_CONVERSATION,
|
||||
CMD_BULK_ACTION_REOPEN_CONVERSATION,
|
||||
CMD_BULK_ACTION_RESOLVE_CONVERSATION,
|
||||
} from './commandBarBusEvents';
|
||||
import {
|
||||
ICON_SNOOZE_CONVERSATION,
|
||||
ICON_REOPEN_CONVERSATION,
|
||||
ICON_RESOLVE_CONVERSATION,
|
||||
} from './CommandBarIcons';
|
||||
|
||||
import { createSnoozeHandlers } from './commandBarActions';
|
||||
|
||||
const SNOOZE_OPTIONS = wootConstants.SNOOZE_OPTIONS;
|
||||
|
||||
export const SNOOZE_CONVERSATION_BULK_ACTIONS = [
|
||||
{
|
||||
id: 'bulk_action_snooze_conversation',
|
||||
title: 'COMMAND_BAR.COMMANDS.SNOOZE_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS',
|
||||
icon: ICON_SNOOZE_CONVERSATION,
|
||||
children: Object.values(SNOOZE_OPTIONS),
|
||||
},
|
||||
...createSnoozeHandlers(
|
||||
CMD_BULK_ACTION_SNOOZE_CONVERSATION,
|
||||
'bulk_action_snooze_conversation',
|
||||
'COMMAND_BAR.SECTIONS.BULK_ACTIONS'
|
||||
),
|
||||
];
|
||||
|
||||
export const RESOLVED_CONVERSATION_BULK_ACTIONS = [
|
||||
{
|
||||
id: 'bulk_action_reopen_conversation',
|
||||
title: 'COMMAND_BAR.COMMANDS.REOPEN_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS',
|
||||
icon: ICON_REOPEN_CONVERSATION,
|
||||
handler: () => bus.$emit(CMD_BULK_ACTION_REOPEN_CONVERSATION),
|
||||
},
|
||||
];
|
||||
|
||||
export const OPEN_CONVERSATION_BULK_ACTIONS = [
|
||||
{
|
||||
id: 'bulk_action_resolve_conversation',
|
||||
title: 'COMMAND_BAR.COMMANDS.RESOLVE_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS',
|
||||
icon: ICON_RESOLVE_CONVERSATION,
|
||||
handler: () => bus.$emit(CMD_BULK_ACTION_RESOLVE_CONVERSATION),
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedConversations: 'bulkActions/getSelectedConversationIds',
|
||||
}),
|
||||
bulkActionsHotKeys() {
|
||||
let actions = [];
|
||||
if (this.selectedConversations.length > 0) {
|
||||
actions = [
|
||||
...SNOOZE_CONVERSATION_BULK_ACTIONS,
|
||||
...RESOLVED_CONVERSATION_BULK_ACTIONS,
|
||||
...OPEN_CONVERSATION_BULK_ACTIONS,
|
||||
];
|
||||
}
|
||||
return this.prepareActions(actions);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectedConversations() {
|
||||
this.setCommandbarData();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
prepareActions(actions) {
|
||||
return actions.map(action => ({
|
||||
...action,
|
||||
title: this.$t(action.title),
|
||||
section: this.$t(action.section),
|
||||
}));
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -30,6 +30,17 @@ export const OPEN_CONVERSATION_ACTIONS = [
|
||||
},
|
||||
];
|
||||
|
||||
export const createSnoozeHandlers = (busEventName, parentId, section) => {
|
||||
return Object.values(SNOOZE_OPTIONS).map(option => ({
|
||||
id: option,
|
||||
title: `COMMAND_BAR.COMMANDS.${option.toUpperCase()}`,
|
||||
parent: parentId,
|
||||
section: section,
|
||||
icon: ICON_SNOOZE_CONVERSATION,
|
||||
handler: () => bus.$emit(busEventName, option),
|
||||
}));
|
||||
};
|
||||
|
||||
export const SNOOZE_CONVERSATION_ACTIONS = [
|
||||
{
|
||||
id: 'snooze_conversation',
|
||||
@@ -37,61 +48,11 @@ export const SNOOZE_CONVERSATION_ACTIONS = [
|
||||
icon: ICON_SNOOZE_CONVERSATION,
|
||||
children: Object.values(SNOOZE_OPTIONS),
|
||||
},
|
||||
|
||||
{
|
||||
id: SNOOZE_OPTIONS.UNTIL_NEXT_REPLY,
|
||||
title: 'COMMAND_BAR.COMMANDS.UNTIL_NEXT_REPLY',
|
||||
parent: 'snooze_conversation',
|
||||
section: 'COMMAND_BAR.SECTIONS.SNOOZE_CONVERSATION',
|
||||
icon: ICON_SNOOZE_CONVERSATION,
|
||||
handler: () =>
|
||||
bus.$emit(CMD_SNOOZE_CONVERSATION, SNOOZE_OPTIONS.UNTIL_NEXT_REPLY),
|
||||
},
|
||||
{
|
||||
id: SNOOZE_OPTIONS.AN_HOUR_FROM_NOW,
|
||||
title: 'COMMAND_BAR.COMMANDS.AN_HOUR_FROM_NOW',
|
||||
parent: 'snooze_conversation',
|
||||
section: 'COMMAND_BAR.SECTIONS.SNOOZE_CONVERSATION',
|
||||
icon: ICON_SNOOZE_CONVERSATION,
|
||||
handler: () =>
|
||||
bus.$emit(CMD_SNOOZE_CONVERSATION, SNOOZE_OPTIONS.AN_HOUR_FROM_NOW),
|
||||
},
|
||||
{
|
||||
id: SNOOZE_OPTIONS.UNTIL_TOMORROW,
|
||||
title: 'COMMAND_BAR.COMMANDS.UNTIL_TOMORROW',
|
||||
section: 'COMMAND_BAR.SECTIONS.SNOOZE_CONVERSATION',
|
||||
parent: 'snooze_conversation',
|
||||
icon: ICON_SNOOZE_CONVERSATION,
|
||||
handler: () =>
|
||||
bus.$emit(CMD_SNOOZE_CONVERSATION, SNOOZE_OPTIONS.UNTIL_TOMORROW),
|
||||
},
|
||||
{
|
||||
id: SNOOZE_OPTIONS.UNTIL_NEXT_WEEK,
|
||||
title: 'COMMAND_BAR.COMMANDS.UNTIL_NEXT_WEEK',
|
||||
section: 'COMMAND_BAR.SECTIONS.SNOOZE_CONVERSATION',
|
||||
parent: 'snooze_conversation',
|
||||
icon: ICON_SNOOZE_CONVERSATION,
|
||||
handler: () =>
|
||||
bus.$emit(CMD_SNOOZE_CONVERSATION, SNOOZE_OPTIONS.UNTIL_NEXT_WEEK),
|
||||
},
|
||||
{
|
||||
id: SNOOZE_OPTIONS.UNTIL_NEXT_MONTH,
|
||||
title: 'COMMAND_BAR.COMMANDS.UNTIL_NEXT_MONTH',
|
||||
section: 'COMMAND_BAR.SECTIONS.SNOOZE_CONVERSATION',
|
||||
parent: 'snooze_conversation',
|
||||
icon: ICON_SNOOZE_CONVERSATION,
|
||||
handler: () =>
|
||||
bus.$emit(CMD_SNOOZE_CONVERSATION, SNOOZE_OPTIONS.UNTIL_NEXT_MONTH),
|
||||
},
|
||||
{
|
||||
id: SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME,
|
||||
title: 'COMMAND_BAR.COMMANDS.CUSTOM',
|
||||
section: 'COMMAND_BAR.SECTIONS.SNOOZE_CONVERSATION',
|
||||
parent: 'snooze_conversation',
|
||||
icon: ICON_SNOOZE_CONVERSATION,
|
||||
handler: () =>
|
||||
bus.$emit(CMD_SNOOZE_CONVERSATION, SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME),
|
||||
},
|
||||
...createSnoozeHandlers(
|
||||
CMD_SNOOZE_CONVERSATION,
|
||||
'snooze_conversation',
|
||||
'COMMAND_BAR.SECTIONS.SNOOZE_CONVERSATION'
|
||||
),
|
||||
];
|
||||
|
||||
export const RESOLVED_CONVERSATION_ACTIONS = [
|
||||
|
||||
@@ -16,5 +16,13 @@ export const CMD_RESOLVE_CONVERSATION = 'CMD_RESOLVE_CONVERSATION';
|
||||
export const CMD_SNOOZE_CONVERSATION = 'CMD_SNOOZE_CONVERSATION';
|
||||
export const CMD_AI_ASSIST = 'CMD_AI_ASSIST';
|
||||
|
||||
// Bulk Actions
|
||||
export const CMD_BULK_ACTION_SNOOZE_CONVERSATION =
|
||||
'CMD_BULK_ACTION_SNOOZE_CONVERSATION';
|
||||
export const CMD_BULK_ACTION_REOPEN_CONVERSATION =
|
||||
'CMD_BULK_ACTION_REOPEN_CONVERSATION';
|
||||
export const CMD_BULK_ACTION_RESOLVE_CONVERSATION =
|
||||
'CMD_BULK_ACTION_RESOLVE_CONVERSATION';
|
||||
|
||||
// Inbox Commands (Notifications)
|
||||
export const CMD_SNOOZE_NOTIFICATION = 'CMD_SNOOZE_NOTIFICATION';
|
||||
|
||||
@@ -6,12 +6,15 @@
|
||||
hideBreadcrumbs
|
||||
:placeholder="placeholder"
|
||||
@selected="onSelected"
|
||||
@closed="onClosed"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import 'ninja-keys';
|
||||
import '@chatwoot/ninja-keys';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import conversationHotKeysMixin from './conversationHotKeys';
|
||||
import bulkActionsHotKeysMixin from './bulkActionsHotKeys';
|
||||
import inboxHotKeysMixin from './inboxHotKeys';
|
||||
import goToCommandHotKeys from './goToCommandHotKeys';
|
||||
import appearanceHotKeys from './appearanceHotKeys';
|
||||
@@ -26,12 +29,21 @@ export default {
|
||||
adminMixin,
|
||||
agentMixin,
|
||||
conversationHotKeysMixin,
|
||||
bulkActionsHotKeysMixin,
|
||||
inboxHotKeysMixin,
|
||||
conversationLabelMixin,
|
||||
conversationTeamMixin,
|
||||
appearanceHotKeys,
|
||||
goToCommandHotKeys,
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
// Added selectedSnoozeType to track the selected snooze type
|
||||
// So if the selected snooze type is "custom snooze" then we set selectedSnoozeType with the CMD action id
|
||||
// So that we can track the selected snooze type and when we close the command bar
|
||||
selectedSnoozeType: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
placeholder() {
|
||||
return this.$t('COMMAND_BAR.SEARCH_PLACEHOLDER');
|
||||
@@ -46,6 +58,7 @@ export default {
|
||||
return [
|
||||
...this.inboxHotKeys,
|
||||
...this.conversationHotKeys,
|
||||
...this.bulkActionsHotKeys,
|
||||
...this.goToCommandHotKeys,
|
||||
...this.goToAppearanceHotKeys,
|
||||
];
|
||||
@@ -64,14 +77,35 @@ export default {
|
||||
this.$refs.ninjakeys.data = this.hotKeys;
|
||||
},
|
||||
onSelected(item) {
|
||||
const { detail: { action: { title = null, section = null } = {} } = {} } =
|
||||
item;
|
||||
const {
|
||||
detail: {
|
||||
action: { title = null, section = null, id = null } = {},
|
||||
} = {},
|
||||
} = item;
|
||||
// Added this condition to prevent setting the selectedSnoozeType to null
|
||||
// When we select the "custom snooze" (CMD bar will close and the custom snooze modal will open)
|
||||
if (id === wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME) {
|
||||
this.selectedSnoozeType =
|
||||
wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME;
|
||||
} else {
|
||||
this.selectedSnoozeType = null;
|
||||
}
|
||||
this.$track(GENERAL_EVENTS.COMMAND_BAR, {
|
||||
section,
|
||||
action: title,
|
||||
});
|
||||
this.setCommandbarData();
|
||||
},
|
||||
onClosed() {
|
||||
// If the selectedSnoozeType is not "SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME (custom snooze)" then we set the context menu chat id to null
|
||||
// Else we do nothing and its handled in the ChatList.vue hideCustomSnoozeModal() method
|
||||
if (
|
||||
this.selectedSnoozeType !==
|
||||
wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME
|
||||
) {
|
||||
this.$store.dispatch('setContextMenuChatId', null);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -55,11 +55,15 @@ export default {
|
||||
replyMode() {
|
||||
this.setCommandbarData();
|
||||
},
|
||||
contextMenuChatId() {
|
||||
this.setCommandbarData();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
replyMode: 'draftMessages/getReplyEditorMode',
|
||||
contextMenuChatId: 'getContextMenuChatId',
|
||||
}),
|
||||
draftMessage() {
|
||||
return this.$store.getters['draftMessages/get'](this.draftKey);
|
||||
@@ -93,6 +97,7 @@ export default {
|
||||
}
|
||||
return this.prepareActions(actions);
|
||||
},
|
||||
|
||||
priorityOptions() {
|
||||
return [
|
||||
{
|
||||
@@ -327,25 +332,42 @@ export default {
|
||||
];
|
||||
},
|
||||
|
||||
conversationHotKeys() {
|
||||
if (
|
||||
isConversationOrInboxRoute() {
|
||||
return (
|
||||
isAConversationRoute(this.$route.name) ||
|
||||
isAInboxViewRoute(this.$route.name)
|
||||
) {
|
||||
const defaultConversationHotKeys = [
|
||||
...this.statusActions,
|
||||
...this.conversationAdditionalActions,
|
||||
...this.assignAgentActions,
|
||||
...this.assignTeamActions,
|
||||
...this.labelActions,
|
||||
...this.assignPriorityActions,
|
||||
];
|
||||
if (this.isAIIntegrationEnabled) {
|
||||
return [...defaultConversationHotKeys, ...this.AIAssistActions];
|
||||
}
|
||||
return defaultConversationHotKeys;
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
shouldShowSnoozeOption() {
|
||||
return (
|
||||
isAConversationRoute(this.$route.name, true, false) &&
|
||||
this.contextMenuChatId
|
||||
);
|
||||
},
|
||||
|
||||
getDefaultConversationHotKeys() {
|
||||
const defaultConversationHotKeys = [
|
||||
...this.statusActions,
|
||||
...this.conversationAdditionalActions,
|
||||
...this.assignAgentActions,
|
||||
...this.assignTeamActions,
|
||||
...this.labelActions,
|
||||
...this.assignPriorityActions,
|
||||
];
|
||||
if (this.isAIIntegrationEnabled) {
|
||||
return [...defaultConversationHotKeys, ...this.AIAssistActions];
|
||||
}
|
||||
return defaultConversationHotKeys;
|
||||
},
|
||||
|
||||
conversationHotKeys() {
|
||||
if (this.shouldShowSnoozeOption) {
|
||||
return this.prepareActions(SNOOZE_CONVERSATION_ACTIONS);
|
||||
}
|
||||
if (this.isConversationOrInboxRoute) {
|
||||
return this.getDefaultConversationHotKeys;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="w-full flex flex-row">
|
||||
<div class="flex flex-row w-full">
|
||||
<div class="flex flex-col h-full" :class="wrapClass">
|
||||
<contacts-header
|
||||
:search-query="searchQuery"
|
||||
@@ -391,8 +391,19 @@ export default {
|
||||
this.fetchContacts(this.pageParameter);
|
||||
},
|
||||
onExportSubmit() {
|
||||
let query = { payload: [] };
|
||||
|
||||
if (this.hasActiveSegments) {
|
||||
query = this.activeSegment.query;
|
||||
} else if (this.hasAppliedFilters) {
|
||||
query = filterQueryGenerator(this.getAppliedContactFilters);
|
||||
}
|
||||
|
||||
try {
|
||||
this.$store.dispatch('contacts/export');
|
||||
this.$store.dispatch('contacts/export', {
|
||||
...query,
|
||||
label: this.label,
|
||||
});
|
||||
this.showAlert(this.$t('EXPORT_CONTACTS.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
this.showAlert(
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<header
|
||||
class="bg-white dark:bg-slate-900 border-b border-slate-50 dark:border-slate-800"
|
||||
class="bg-white border-b dark:bg-slate-900 border-slate-50 dark:border-slate-800"
|
||||
>
|
||||
<div class="flex justify-between w-full py-2 px-4">
|
||||
<div class="flex justify-between w-full px-4 py-2">
|
||||
<div class="flex items-center justify-center max-w-full min-w-[6.25rem]">
|
||||
<woot-sidemenu-icon />
|
||||
<h1
|
||||
class="m-0 text-xl text-slate-900 dark:text-slate-100 overflow-hidden whitespace-nowrap text-ellipsis my-0 mx-2"
|
||||
class="m-0 mx-2 my-0 overflow-hidden text-xl text-slate-900 dark:text-slate-100 whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ headerTitle }}
|
||||
</h1>
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="flex items-center absolute h-full left-2.5">
|
||||
<fluent-icon
|
||||
icon="search"
|
||||
class="h-5 leading-9 text-sm text-slate-700 dark:text-slate-200"
|
||||
class="h-5 text-sm leading-9 text-slate-700 dark:text-slate-200"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
@@ -59,7 +59,7 @@
|
||||
<div v-if="!hasActiveSegments" class="relative">
|
||||
<div
|
||||
v-if="hasAppliedFilters"
|
||||
class="absolute h-2 w-2 top-1 right-3 bg-slate-500 dark:bg-slate-500 rounded-full"
|
||||
class="absolute w-2 h-2 rounded-full top-1 right-3 bg-slate-500 dark:bg-slate-500"
|
||||
/>
|
||||
<woot-button
|
||||
class="clear"
|
||||
@@ -116,7 +116,7 @@
|
||||
<woot-confirm-modal
|
||||
ref="confirmExportContactsDialog"
|
||||
:title="$t('EXPORT_CONTACTS.CONFIRM.TITLE')"
|
||||
:description="$t('EXPORT_CONTACTS.CONFIRM.MESSAGE')"
|
||||
:description="exportDescription"
|
||||
:confirm-label="$t('EXPORT_CONTACTS.CONFIRM.YES')"
|
||||
:cancel-label="$t('EXPORT_CONTACTS.CONFIRM.NO')"
|
||||
/>
|
||||
@@ -162,6 +162,11 @@ export default {
|
||||
hasActiveSegments() {
|
||||
return this.segmentsId !== 0;
|
||||
},
|
||||
exportDescription() {
|
||||
return this.hasAppliedFilters
|
||||
? this.$t('EXPORT_CONTACTS.CONFIRM.FILTERED_MESSAGE')
|
||||
: this.$t('EXPORT_CONTACTS.CONFIRM.MESSAGE');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onToggleSegmentsModal() {
|
||||
|
||||
@@ -32,9 +32,25 @@
|
||||
<th
|
||||
v-for="thHeader in $t('CANNED_MGMT.LIST.TABLE_HEADER')"
|
||||
:key="thHeader"
|
||||
class="last:text-right"
|
||||
class="last:text-right first:m-0 first:p-0"
|
||||
>
|
||||
{{ thHeader }}
|
||||
<p v-if="thHeader !== $t('CANNED_MGMT.LIST.TABLE_HEADER[0]')">
|
||||
{{ thHeader }}
|
||||
</p>
|
||||
|
||||
<button
|
||||
v-if="thHeader === $t('CANNED_MGMT.LIST.TABLE_HEADER[0]')"
|
||||
class="cursor-pointer flex items-center p-0"
|
||||
@click="toggleSort"
|
||||
>
|
||||
<p class="uppercase">
|
||||
{{ thHeader }}
|
||||
</p>
|
||||
<fluent-icon
|
||||
class="mb-2 ml-2"
|
||||
:icon="sortOrder === 'asc' ? 'chevron-up' : 'chevron-down'"
|
||||
/>
|
||||
</button>
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -132,6 +148,7 @@ export default {
|
||||
cannedResponseAPI: {
|
||||
message: '',
|
||||
},
|
||||
sortOrder: 'asc',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -156,9 +173,20 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
// Fetch API Call
|
||||
this.$store.dispatch('getCannedResponse');
|
||||
this.$store.dispatch('getCannedResponse').then(() => {
|
||||
this.toggleSort();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
toggleSort() {
|
||||
this.records.sort((a, b) => {
|
||||
if (this.sortOrder === 'asc') {
|
||||
return a.short_code.localeCompare(b.short_code);
|
||||
}
|
||||
return b.short_code.localeCompare(a.short_code);
|
||||
});
|
||||
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
|
||||
},
|
||||
showAlert(message) {
|
||||
// Reset loading, current selected agent
|
||||
this.loading[this.selectedResponse.id] = false;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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"
|
||||
>
|
||||
<page-header
|
||||
class="max-w-4xl"
|
||||
:header-title="$t('INBOX_MGMT.ADD.AUTH.TITLE')"
|
||||
:header-content="
|
||||
useInstallationName(
|
||||
@@ -11,7 +12,9 @@
|
||||
)
|
||||
"
|
||||
/>
|
||||
<div class="mt-6 mx-0 flex flex-wrap">
|
||||
<div
|
||||
class="mt-6 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 mx-0 max-w-3xl"
|
||||
>
|
||||
<channel-item
|
||||
v-for="channel in channelList"
|
||||
:key="channel.key"
|
||||
|
||||
@@ -21,6 +21,11 @@
|
||||
</woot-tabs>
|
||||
</setting-intro-banner>
|
||||
|
||||
<inbox-reconnection-required
|
||||
v-if="isReconnectionRequired"
|
||||
class="mx-8 mt-5"
|
||||
/>
|
||||
|
||||
<div v-if="selectedTabKey === 'inbox_settings'" class="mx-8">
|
||||
<settings-section
|
||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_UPDATE_TITLE')"
|
||||
@@ -287,7 +292,7 @@
|
||||
<label v-if="isAWebWidgetInbox">
|
||||
{{ $t('INBOX_MGMT.FEATURES.LABEL') }}
|
||||
</label>
|
||||
<div v-if="isAWebWidgetInbox" class="pt-2 pb-4 flex gap-2">
|
||||
<div v-if="isAWebWidgetInbox" class="flex gap-2 pt-2 pb-4">
|
||||
<input
|
||||
v-model="selectedFeatureFlags"
|
||||
type="checkbox"
|
||||
@@ -298,7 +303,7 @@
|
||||
{{ $t('INBOX_MGMT.FEATURES.DISPLAY_FILE_PICKER') }}
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="isAWebWidgetInbox" class="pb-4 flex gap-2">
|
||||
<div v-if="isAWebWidgetInbox" class="flex gap-2 pb-4">
|
||||
<input
|
||||
v-model="selectedFeatureFlags"
|
||||
type="checkbox"
|
||||
@@ -309,7 +314,7 @@
|
||||
{{ $t('INBOX_MGMT.FEATURES.DISPLAY_EMOJI_PICKER') }}
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="isAWebWidgetInbox" class="pb-4 flex gap-2">
|
||||
<div v-if="isAWebWidgetInbox" class="flex gap-2 pb-4">
|
||||
<input
|
||||
v-model="selectedFeatureFlags"
|
||||
type="checkbox"
|
||||
@@ -320,7 +325,7 @@
|
||||
{{ $t('INBOX_MGMT.FEATURES.ALLOW_END_CONVERSATION') }}
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="isAWebWidgetInbox" class="pb-4 flex gap-2">
|
||||
<div v-if="isAWebWidgetInbox" class="flex gap-2 pb-4">
|
||||
<input
|
||||
v-model="selectedFeatureFlags"
|
||||
type="checkbox"
|
||||
@@ -435,6 +440,7 @@ import WeeklyAvailability from './components/WeeklyAvailability.vue';
|
||||
import GreetingsEditor from 'shared/components/GreetingsEditor.vue';
|
||||
import ConfigurationPage from './settingsPage/ConfigurationPage.vue';
|
||||
import CollaboratorsPage from './settingsPage/CollaboratorsPage.vue';
|
||||
import InboxReconnectionRequired from './components/InboxReconnectionRequired';
|
||||
import WidgetBuilder from './WidgetBuilder.vue';
|
||||
import BotConfiguration from './components/BotConfiguration.vue';
|
||||
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||
@@ -453,6 +459,7 @@ export default {
|
||||
WeeklyAvailability,
|
||||
WidgetBuilder,
|
||||
SenderNameExamplePreview,
|
||||
InboxReconnectionRequired,
|
||||
},
|
||||
mixins: [alertMixin, configMixin, inboxMixin],
|
||||
data() {
|
||||
@@ -614,6 +621,9 @@ export default {
|
||||
return true;
|
||||
return false;
|
||||
},
|
||||
isReconnectionRequired() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route(to) {
|
||||
|
||||
+3
-1
@@ -111,7 +111,9 @@ export default {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE'));
|
||||
this.showAlert(
|
||||
error.message || this.$t('INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -155,7 +155,9 @@ export default {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE'));
|
||||
this.showAlert(
|
||||
error.message || this.$t('INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,16 +4,18 @@
|
||||
class="border border-slate-25 dark:border-slate-800/60 bg-white dark:bg-slate-900 h-full p-6 w-full md:w-full max-w-full md:max-w-[75%] flex-shrink-0 flex-grow-0"
|
||||
>
|
||||
<page-header
|
||||
class="max-w-4xl"
|
||||
:header-title="$t('INBOX_MGMT.ADD.EMAIL_PROVIDER.TITLE')"
|
||||
:header-content="$t('INBOX_MGMT.ADD.EMAIL_PROVIDER.DESCRIPTION')"
|
||||
/>
|
||||
<div class="flex flex-row flex-wrap mx-0 mt-6">
|
||||
<div class="grid grid-cols-4 max-w-3xl mx-0 mt-6">
|
||||
<channel-selector
|
||||
v-for="emailProvider in emailProviderList"
|
||||
:key="emailProvider.key"
|
||||
:class="{ inactive: !emailProvider.isEnabled }"
|
||||
:title="emailProvider.title"
|
||||
:src="emailProvider.src"
|
||||
@click="() => onClick(emailProvider.key)"
|
||||
@click="() => onClick(emailProvider)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,7 +40,10 @@ export default {
|
||||
return { provider: '' };
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ globalConfig: 'globalConfig/get' }),
|
||||
...mapGetters({
|
||||
globalConfig: 'globalConfig/get',
|
||||
isAChatwootInstance: 'globalConfig/isAChatwootInstance',
|
||||
}),
|
||||
emailProviderList() {
|
||||
return [
|
||||
{
|
||||
@@ -53,12 +58,19 @@ export default {
|
||||
key: 'other_provider',
|
||||
src: '/assets/images/dashboard/channels/email.png',
|
||||
},
|
||||
].filter(provider => provider.isEnabled);
|
||||
].filter(provider => {
|
||||
if (this.isAChatwootInstance) {
|
||||
return true;
|
||||
}
|
||||
return provider.isEnabled;
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClick(provider) {
|
||||
this.provider = provider;
|
||||
onClick(emailProvider) {
|
||||
if (emailProvider.isEnabled) {
|
||||
this.provider = emailProvider.key;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -86,7 +86,8 @@ export default {
|
||||
});
|
||||
} catch (error) {
|
||||
this.showAlert(
|
||||
this.$t('INBOX_MGMT.ADD.TELEGRAM_CHANNEL.API.ERROR_MESSAGE')
|
||||
error.message ||
|
||||
this.$t('INBOX_MGMT.ADD.TELEGRAM_CHANNEL.API.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
<option value="twilio">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.TWILIO') }}
|
||||
</option>
|
||||
<option value="360dialog">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.360_DIALOG') }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
actionUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center gap-2 px-4 py-3 text-sm text-white bg-red-500 rounded-md dark:bg-red-800/30 dark:text-red-50 min-h-10"
|
||||
>
|
||||
<fluent-icon icon="error-circle" class="text-white dark:text-red-50" />
|
||||
<slot>
|
||||
<span v-html="$t('INBOX_MGMT.RECONNECTION_REQUIRED', { actionUrl })" />
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<div class="flex-shrink flex-grow overflow-auto p-4">
|
||||
<div class="flex-grow flex-shrink p-4 overflow-auto">
|
||||
<div class="flex flex-col">
|
||||
<div v-if="uiFlags.isFetching" class="my-0 mx-auto">
|
||||
<div v-if="uiFlags.isFetching" class="mx-auto my-0">
|
||||
<woot-loading-state :message="$t('INTEGRATION_APPS.FETCHING')" />
|
||||
</div>
|
||||
|
||||
<div v-else class="w-full">
|
||||
<div>
|
||||
<div
|
||||
v-for="item in integrationsList"
|
||||
v-for="item in enabledIntegrations"
|
||||
:key="item.id"
|
||||
class="bg-white dark:bg-slate-800 border border-solid border-slate-75 dark:border-slate-700/50 rounded-sm mb-4 p-4"
|
||||
class="p-4 mb-4 bg-white border border-solid rounded-sm dark:bg-slate-800 border-slate-75 dark:border-slate-700/50"
|
||||
>
|
||||
<integration-item
|
||||
:integration-id="item.id"
|
||||
@@ -25,22 +25,38 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import IntegrationItem from './IntegrationItem.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
IntegrationItem,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'labels/getUIFlags',
|
||||
integrationsList: 'integrations/getAppIntegrations',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('integrations/get');
|
||||
},
|
||||
};
|
||||
<script setup>
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
import { computed, onMounted } from 'vue';
|
||||
import IntegrationItem from './IntegrationItem.vue';
|
||||
const store = useStore();
|
||||
const getters = useStoreGetters();
|
||||
|
||||
const uiFlags = getters['integrations/getUIFlags'];
|
||||
|
||||
const accountId = getters.getCurrentAccountId;
|
||||
|
||||
const integrationList = computed(() => {
|
||||
return getters['integrations/getAppIntegrations'].value;
|
||||
});
|
||||
|
||||
const isLinearIntegrationEnabled = computed(() => {
|
||||
return getters['accounts/isFeatureEnabledonAccount'].value(
|
||||
accountId.value,
|
||||
'linear_integration'
|
||||
);
|
||||
});
|
||||
const enabledIntegrations = computed(() => {
|
||||
if (!isLinearIntegrationEnabled.value) {
|
||||
return integrationList.value.filter(
|
||||
integration => integration.id !== 'linear'
|
||||
);
|
||||
}
|
||||
return integrationList.value;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('integrations/get');
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
<tr>
|
||||
<td>{{ macro.name }}</td>
|
||||
<td>
|
||||
<div class="avatar-container">
|
||||
<thumbnail :username="macro.created_by.name" size="24px" />
|
||||
<span>{{ macro.created_by.name }}</span>
|
||||
<div v-if="macro.created_by" class="avatar-container">
|
||||
<thumbnail :username="createdByName" size="24px" />
|
||||
<span>{{ createdByName }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="avatar-container">
|
||||
<thumbnail :username="macro.updated_by.name" size="24px" />
|
||||
<span>{{ macro.updated_by.name }}</span>
|
||||
<div v-if="macro.updated_by" class="avatar-container">
|
||||
<thumbnail :username="updatedByName" size="24px" />
|
||||
<span>{{ updatedByName }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ visibilityLabel }}</td>
|
||||
@@ -53,6 +53,14 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
createdByName() {
|
||||
const createdBy = this.macro.created_by;
|
||||
return createdBy.available_name ?? createdBy.email ?? '';
|
||||
},
|
||||
updatedByName() {
|
||||
const updatedBy = this.macro.updated_by;
|
||||
return updatedBy.available_name ?? updatedBy.email ?? '';
|
||||
},
|
||||
visibilityLabel() {
|
||||
return this.macro.visibility === 'global'
|
||||
? this.$t('MACROS.EDITOR.VISIBILITY.GLOBAL.LABEL')
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
<template>
|
||||
<form @submit.prevent="changePassword()">
|
||||
<div class="flex flex-col w-full gap-4">
|
||||
<woot-input
|
||||
v-model="currentPassword"
|
||||
type="password"
|
||||
:styles="inputStyles"
|
||||
:class="{ error: $v.currentPassword.$error }"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.LABEL')"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.PLACEHOLDER')"
|
||||
:error="`${
|
||||
$v.currentPassword.$error
|
||||
? $t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.ERROR')
|
||||
: ''
|
||||
}`"
|
||||
@input="$v.currentPassword.$touch"
|
||||
/>
|
||||
|
||||
<woot-input
|
||||
v-model="password"
|
||||
type="password"
|
||||
:styles="inputStyles"
|
||||
:class="{ error: $v.password.$error }"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.PASSWORD.LABEL')"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.PASSWORD.PLACEHOLDER')"
|
||||
:error="`${
|
||||
$v.password.$error ? $t('PROFILE_SETTINGS.FORM.PASSWORD.ERROR') : ''
|
||||
}`"
|
||||
@input="$v.password.$touch"
|
||||
/>
|
||||
|
||||
<woot-input
|
||||
v-model="passwordConfirmation"
|
||||
type="password"
|
||||
:styles="inputStyles"
|
||||
:class="{ error: $v.passwordConfirmation.$error }"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.LABEL')"
|
||||
:placeholder="
|
||||
$t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.PLACEHOLDER')
|
||||
"
|
||||
:error="`${
|
||||
$v.passwordConfirmation.$error
|
||||
? $t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.ERROR')
|
||||
: ''
|
||||
}`"
|
||||
@input="$v.passwordConfirmation.$touch"
|
||||
/>
|
||||
|
||||
<form-button
|
||||
type="submit"
|
||||
color-scheme="primary"
|
||||
variant="solid"
|
||||
size="large"
|
||||
:disabled="isButtonDisabled"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.BTN_TEXT') }}
|
||||
</form-button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { parseAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
import FormButton from 'v3/components/Form/Button.vue';
|
||||
export default {
|
||||
components: {
|
||||
FormButton,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
data() {
|
||||
return {
|
||||
currentPassword: '',
|
||||
password: '',
|
||||
passwordConfirmation: '',
|
||||
isPasswordChanging: false,
|
||||
errorMessage: '',
|
||||
inputStyles: {
|
||||
borderRadius: '12px',
|
||||
padding: '6px 12px',
|
||||
fontSize: '14px',
|
||||
marginBottom: '2px',
|
||||
},
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
currentPassword: {
|
||||
required,
|
||||
},
|
||||
password: {
|
||||
minLength: minLength(6),
|
||||
},
|
||||
passwordConfirmation: {
|
||||
minLength: minLength(6),
|
||||
isEqPassword(value) {
|
||||
if (value !== this.password) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isButtonDisabled() {
|
||||
return (
|
||||
!this.currentPassword ||
|
||||
!this.passwordConfirmation ||
|
||||
!this.$v.passwordConfirmation.isEqPassword
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async changePassword() {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.ERROR'));
|
||||
return;
|
||||
}
|
||||
let alertMessage = this.$t('PROFILE_SETTINGS.PASSWORD_UPDATE_SUCCESS');
|
||||
try {
|
||||
await this.$store.dispatch('updateProfile', {
|
||||
password: this.password,
|
||||
password_confirmation: this.passwordConfirmation,
|
||||
current_password: this.currentPassword,
|
||||
});
|
||||
} catch (error) {
|
||||
alertMessage =
|
||||
parseAPIErrorResponse(error) ||
|
||||
this.$t('RESET_PASSWORD.API.ERROR_MESSAGE');
|
||||
} finally {
|
||||
this.showAlert(alertMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,245 +0,0 @@
|
||||
<template>
|
||||
<div class="flex items-center w-full overflow-y-auto">
|
||||
<div class="flex flex-col h-full p-5 pt-16 mx-auto my-0 font-inter">
|
||||
<div class="flex flex-col gap-16 pb-8 sm:max-w-[720px]">
|
||||
<div class="flex flex-col gap-6">
|
||||
<h2 class="mt-4 text-2xl font-medium text-ash-900">
|
||||
{{ $t('PROFILE_SETTINGS.TITLE') }}
|
||||
</h2>
|
||||
<user-profile-picture
|
||||
:src="avatarUrl"
|
||||
:name="name"
|
||||
size="72px"
|
||||
@change="updateProfilePicture"
|
||||
@delete="deleteProfilePicture"
|
||||
/>
|
||||
<user-basic-details
|
||||
:name="name"
|
||||
:display-name="displayName"
|
||||
:email="email"
|
||||
:email-enabled="!globalConfig.disableUserProfileUpdate"
|
||||
@update-user="updateProfile"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<form-section
|
||||
:title="$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.TITLE')"
|
||||
:description="
|
||||
$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.NOTE')
|
||||
"
|
||||
>
|
||||
<message-signature
|
||||
:message-signature="messageSignature"
|
||||
@update-signature="updateSignature"
|
||||
/>
|
||||
</form-section>
|
||||
<form-section
|
||||
:header="$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.TITLE')"
|
||||
:description="$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.NOTE')"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col justify-between w-full gap-5 sm:gap-4 sm:flex-row"
|
||||
>
|
||||
<button
|
||||
v-for="hotKey in hotKeys"
|
||||
:key="hotKey.key"
|
||||
class="px-0 reset-base"
|
||||
>
|
||||
<hot-key-card
|
||||
:key="hotKey.title"
|
||||
:title="hotKey.title"
|
||||
:description="hotKey.description"
|
||||
:light-image="hotKey.lightImage"
|
||||
:dark-image="hotKey.darkImage"
|
||||
:active="isEditorHotKeyEnabled(uiSettings, hotKey.key)"
|
||||
@click="toggleHotKey(hotKey.key)"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</form-section>
|
||||
<form-section
|
||||
:header="$t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.TITLE')"
|
||||
>
|
||||
<change-password v-if="!globalConfig.disableUserProfileUpdate" />
|
||||
</form-section>
|
||||
<form-section
|
||||
:header="
|
||||
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.TITLE')
|
||||
"
|
||||
:description="
|
||||
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.NOTE')
|
||||
"
|
||||
>
|
||||
<audio-notifications />
|
||||
</form-section>
|
||||
<form-section :header="$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TITLE')">
|
||||
<notification-preferences />
|
||||
</form-section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import uiSettingsMixin, {
|
||||
isEditorHotKeyEnabled,
|
||||
} from 'dashboard/mixins/uiSettings';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { clearCookiesOnLogout } from 'dashboard/store/utils/api.js';
|
||||
|
||||
import UserProfilePicture from './UserProfilePicture.vue';
|
||||
import UserBasicDetails from './UserBasicDetails.vue';
|
||||
import MessageSignature from './MessageSignature.vue';
|
||||
import HotKeyCard from './HotKeyCard.vue';
|
||||
import ChangePassword from './ChangePassword.vue';
|
||||
import NotificationPreferences from './NotificationPreferences.vue';
|
||||
import AudioNotifications from './AudioNotifications.vue';
|
||||
import FormSection from 'dashboard/components/FormSection.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MessageSignature,
|
||||
FormSection,
|
||||
UserProfilePicture,
|
||||
UserBasicDetails,
|
||||
HotKeyCard,
|
||||
ChangePassword,
|
||||
NotificationPreferences,
|
||||
AudioNotifications,
|
||||
},
|
||||
mixins: [alertMixin, globalConfigMixin, uiSettingsMixin],
|
||||
data() {
|
||||
return {
|
||||
avatarFile: '',
|
||||
avatarUrl: '',
|
||||
name: '',
|
||||
displayName: '',
|
||||
email: '',
|
||||
messageSignature: '',
|
||||
hotKeys: [
|
||||
{
|
||||
key: 'enter',
|
||||
title: this.$t(
|
||||
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.ENTER_KEY.HEADING'
|
||||
),
|
||||
description: this.$t(
|
||||
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.ENTER_KEY.CONTENT'
|
||||
),
|
||||
lightImage: '/assets/images/dashboard/profile/hot-key-enter.svg',
|
||||
darkImage: '/assets/images/dashboard/profile/hot-key-enter-dark.svg',
|
||||
},
|
||||
{
|
||||
key: 'cmd_enter',
|
||||
title: this.$t(
|
||||
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.CMD_ENTER_KEY.HEADING'
|
||||
),
|
||||
description: this.$t(
|
||||
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.CMD_ENTER_KEY.CONTENT'
|
||||
),
|
||||
lightImage: '/assets/images/dashboard/profile/hot-key-ctrl-enter.svg',
|
||||
darkImage:
|
||||
'/assets/images/dashboard/profile/hot-key-ctrl-enter-dark.svg',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
currentUserId: 'getCurrentUserID',
|
||||
globalConfig: 'globalConfig/get',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
if (this.currentUserId) {
|
||||
this.initializeUser();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initializeUser() {
|
||||
this.name = this.currentUser.name;
|
||||
this.email = this.currentUser.email;
|
||||
this.avatarUrl = this.currentUser.avatar_url;
|
||||
this.displayName = this.currentUser.display_name;
|
||||
this.messageSignature = this.currentUser.message_signature;
|
||||
},
|
||||
isEditorHotKeyEnabled,
|
||||
async dispatchUpdate(payload, successMessage, errorMessage) {
|
||||
let alertMessage = '';
|
||||
try {
|
||||
await this.$store.dispatch('updateProfile', payload);
|
||||
alertMessage = successMessage;
|
||||
|
||||
return true; // return the value so that the status can be known
|
||||
} catch (error) {
|
||||
alertMessage = error?.response?.data?.error
|
||||
? error.response.data.error
|
||||
: errorMessage;
|
||||
|
||||
return false; // return the value so that the status can be known
|
||||
} finally {
|
||||
this.showAlert(alertMessage);
|
||||
}
|
||||
},
|
||||
async updateProfile(userAttributes) {
|
||||
const { name, email, displayName } = userAttributes;
|
||||
const hasEmailChanged = this.currentUser.email !== email;
|
||||
this.name = name || this.name;
|
||||
this.email = email || this.email;
|
||||
this.displayName = displayName || this.displayName;
|
||||
|
||||
const updatePayload = {
|
||||
name: this.name,
|
||||
email: this.email,
|
||||
displayName: this.displayName,
|
||||
avatar: this.avatarFile,
|
||||
};
|
||||
|
||||
const success = await this.dispatchUpdate(
|
||||
updatePayload,
|
||||
hasEmailChanged
|
||||
? this.$t('PROFILE_SETTINGS.AFTER_EMAIL_CHANGED')
|
||||
: this.$t('PROFILE_SETTINGS.UPDATE_SUCCESS'),
|
||||
this.$t('RESET_PASSWORD.API.ERROR_MESSAGE')
|
||||
);
|
||||
|
||||
if (hasEmailChanged && success) clearCookiesOnLogout();
|
||||
},
|
||||
async updateSignature(signature) {
|
||||
const payload = { message_signature: signature };
|
||||
let successMessage = this.$t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_SUCCESS'
|
||||
);
|
||||
let errorMessage = this.$t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_ERROR'
|
||||
);
|
||||
|
||||
await this.dispatchUpdate(payload, successMessage, errorMessage);
|
||||
},
|
||||
updateProfilePicture({ file, url }) {
|
||||
this.avatarFile = file;
|
||||
this.avatarUrl = url;
|
||||
},
|
||||
async deleteProfilePicture() {
|
||||
try {
|
||||
await this.$store.dispatch('deleteAvatar');
|
||||
this.avatarUrl = '';
|
||||
this.avatarFile = '';
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.AVATAR_DELETE_SUCCESS'));
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.AVATAR_DELETE_FAILED'));
|
||||
}
|
||||
},
|
||||
toggleHotKey(key) {
|
||||
this.hotKeys = this.hotKeys.map(hotKey =>
|
||||
hotKey.key === key ? { ...hotKey, active: !hotKey.active } : hotKey
|
||||
);
|
||||
this.updateUISettings({ editor_message_key: key });
|
||||
this.showAlert(
|
||||
this.$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.UPDATE_SUCCESS')
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<form class="flex flex-col gap-6" @submit.prevent="updateSignature()">
|
||||
<woot-message-editor
|
||||
id="message-signature-input"
|
||||
v-model="signature"
|
||||
class="message-editor h-[10rem] !px-3"
|
||||
:is-format-mode="true"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE.PLACEHOLDER')"
|
||||
:enabled-menu-options="customEditorMenuList"
|
||||
:enable-suggestions="false"
|
||||
:show-image-resize-toolbar="true"
|
||||
/>
|
||||
<form-button
|
||||
type="submit"
|
||||
color-scheme="primary"
|
||||
variant="solid"
|
||||
size="large"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.BTN_TEXT') }}
|
||||
</form-button>
|
||||
</form>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
import { MESSAGE_SIGNATURE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
|
||||
import FormButton from 'v3/components/Form/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
messageSignature: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const customEditorMenuList = MESSAGE_SIGNATURE_EDITOR_MENU_OPTIONS;
|
||||
const signature = ref(props.messageSignature);
|
||||
const emit = defineEmits(['update-signature']);
|
||||
|
||||
watch(
|
||||
() => props.messageSignature,
|
||||
newValue => {
|
||||
signature.value = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
const updateSignature = () => {
|
||||
emit('update-signature', signature.value);
|
||||
};
|
||||
</script>
|
||||
@@ -1,20 +0,0 @@
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
|
||||
const Index = () => import('./Index.vue');
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/personal'),
|
||||
name: 'personal_settings',
|
||||
roles: ['administrator', 'agent'],
|
||||
component: Index,
|
||||
props: {
|
||||
headerTitle: 'PROFILE_SETTINGS.TITLE',
|
||||
icon: 'edit',
|
||||
showNewButton: false,
|
||||
showSidemenuIcon: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="flex flex-row justify-between gap-4">
|
||||
<woot-input
|
||||
name="access_token"
|
||||
class="flex-1 [&>input]:!py-1.5 ltr:[&>input]:!pr-9 ltr:[&>input]:!pl-3 rtl:[&>input]:!pl-9 rtl:[&>input]:!pr-3 focus:[&>input]:!border-slate-200 focus:[&>input]:dark:!border-slate-600 [&>input]:cursor-not-allowed relative"
|
||||
:styles="{
|
||||
borderRadius: '12px',
|
||||
fontSize: '14px',
|
||||
marginBottom: '2px',
|
||||
}"
|
||||
:type="inputType"
|
||||
:value="value"
|
||||
readonly
|
||||
>
|
||||
<template #masked>
|
||||
<button
|
||||
class="absolute top-1.5 ltr:right-0.5 rtl:left-0.5"
|
||||
@click="toggleMasked"
|
||||
>
|
||||
<fluent-icon :icon="maskIcon" :size="16" />
|
||||
</button>
|
||||
</template>
|
||||
</woot-input>
|
||||
<form-button
|
||||
type="submit"
|
||||
size="large"
|
||||
icon="text-copy"
|
||||
variant="outline"
|
||||
color-scheme="secondary"
|
||||
@click="onClick"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.COPY') }}
|
||||
</form-button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import FormButton from 'v3/components/Form/Button.vue';
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['on-copy']);
|
||||
const inputType = ref('password');
|
||||
const toggleMasked = () => {
|
||||
inputType.value = inputType.value === 'password' ? 'text' : 'password';
|
||||
};
|
||||
|
||||
const maskIcon = computed(() => {
|
||||
return inputType.value === 'password' ? 'eye-hide' : 'eye-show';
|
||||
});
|
||||
|
||||
const onClick = () => {
|
||||
emit('on-copy', props.value);
|
||||
};
|
||||
</script>
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
<input
|
||||
:id="`radio-${option.value}`"
|
||||
v-model="selectedValue"
|
||||
class="shadow cursor-pointer grid place-items-center border-2 border-ash-200 appearance-none rounded-full w-4 h-4 checked:bg-primary-600 before:content-[''] before:bg-primary-600 before:border-4 before:rounded-full before:border-ash-25 checked:before:w-[14px] checked:before:h-[14px] checked:border checked:border-primary-600"
|
||||
class="shadow-sm cursor-pointer grid place-items-center border-2 border-ash-200 appearance-none rounded-full w-4 h-4 checked:bg-primary-600 before:content-[''] before:bg-primary-600 before:border-4 before:rounded-full before:border-ash-25 checked:before:w-[14px] checked:before:h-[14px] checked:border checked:border-primary-600"
|
||||
type="radio"
|
||||
:value="option.value"
|
||||
/>
|
||||
@@ -1,82 +1,73 @@
|
||||
<template>
|
||||
<form @submit.prevent="changePassword()">
|
||||
<div
|
||||
class="profile--settings--row text-black-900 dark:text-slate-300 flex items-center"
|
||||
>
|
||||
<div class="w-1/4">
|
||||
<h4 class="text-lg text-black-900 dark:text-slate-200">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.TITLE') }}
|
||||
</h4>
|
||||
<p>{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.NOTE') }}</p>
|
||||
</div>
|
||||
<div class="w-[45%] p-4">
|
||||
<woot-input
|
||||
v-model="currentPassword"
|
||||
type="password"
|
||||
:class="{ error: $v.currentPassword.$error }"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.LABEL')"
|
||||
:placeholder="
|
||||
$t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.PLACEHOLDER')
|
||||
"
|
||||
:error="
|
||||
$v.currentPassword.$error
|
||||
? $t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.ERROR')
|
||||
: ''
|
||||
"
|
||||
@blur="$v.currentPassword.$touch"
|
||||
/>
|
||||
<div class="flex flex-col w-full gap-4">
|
||||
<woot-input
|
||||
v-model="currentPassword"
|
||||
type="password"
|
||||
:styles="inputStyles"
|
||||
:class="{ error: $v.currentPassword.$error }"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.LABEL')"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.PLACEHOLDER')"
|
||||
:error="`${
|
||||
$v.currentPassword.$error
|
||||
? $t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.ERROR')
|
||||
: ''
|
||||
}`"
|
||||
@input="$v.currentPassword.$touch"
|
||||
/>
|
||||
|
||||
<woot-input
|
||||
v-model="password"
|
||||
type="password"
|
||||
:class="{ error: $v.password.$error }"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.PASSWORD.LABEL')"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.PASSWORD.PLACEHOLDER')"
|
||||
:error="
|
||||
$v.password.$error ? $t('PROFILE_SETTINGS.FORM.PASSWORD.ERROR') : ''
|
||||
"
|
||||
@blur="$v.password.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-model="password"
|
||||
type="password"
|
||||
:styles="inputStyles"
|
||||
:class="{ error: $v.password.$error }"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.PASSWORD.LABEL')"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.PASSWORD.PLACEHOLDER')"
|
||||
:error="`${
|
||||
$v.password.$error ? $t('PROFILE_SETTINGS.FORM.PASSWORD.ERROR') : ''
|
||||
}`"
|
||||
@input="$v.password.$touch"
|
||||
/>
|
||||
|
||||
<woot-input
|
||||
v-model="passwordConfirmation"
|
||||
type="password"
|
||||
:class="{ error: $v.passwordConfirmation.$error }"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.LABEL')"
|
||||
:placeholder="
|
||||
$t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.PLACEHOLDER')
|
||||
"
|
||||
:error="
|
||||
$v.passwordConfirmation.$error
|
||||
? $t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.ERROR')
|
||||
: ''
|
||||
"
|
||||
@blur="$v.passwordConfirmation.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-model="passwordConfirmation"
|
||||
type="password"
|
||||
:styles="inputStyles"
|
||||
:class="{ error: $v.passwordConfirmation.$error }"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.LABEL')"
|
||||
:placeholder="
|
||||
$t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.PLACEHOLDER')
|
||||
"
|
||||
:error="`${
|
||||
$v.passwordConfirmation.$error
|
||||
? $t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.ERROR')
|
||||
: ''
|
||||
}`"
|
||||
@input="$v.passwordConfirmation.$touch"
|
||||
/>
|
||||
|
||||
<woot-button
|
||||
:is-loading="isPasswordChanging"
|
||||
type="submit"
|
||||
:disabled="
|
||||
!currentPassword ||
|
||||
!passwordConfirmation ||
|
||||
!$v.passwordConfirmation.isEqPassword
|
||||
"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.BTN_TEXT') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<form-button
|
||||
type="submit"
|
||||
color-scheme="primary"
|
||||
variant="solid"
|
||||
size="large"
|
||||
:disabled="isButtonDisabled"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.BTN_TEXT') }}
|
||||
</form-button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { parseAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
|
||||
import FormButton from 'v3/components/Form/Button.vue';
|
||||
export default {
|
||||
components: {
|
||||
FormButton,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
data() {
|
||||
return {
|
||||
@@ -85,6 +76,12 @@ export default {
|
||||
passwordConfirmation: '',
|
||||
isPasswordChanging: false,
|
||||
errorMessage: '',
|
||||
inputStyles: {
|
||||
borderRadius: '12px',
|
||||
padding: '6px 12px',
|
||||
fontSize: '14px',
|
||||
marginBottom: '2px',
|
||||
},
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
@@ -105,10 +102,13 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
currentUserId: 'getCurrentUserID',
|
||||
}),
|
||||
isButtonDisabled() {
|
||||
return (
|
||||
!this.currentPassword ||
|
||||
!this.passwordConfirmation ||
|
||||
!this.$v.passwordConfirmation.isEqPassword
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async changePassword() {
|
||||
@@ -117,38 +117,21 @@ export default {
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.ERROR'));
|
||||
return;
|
||||
}
|
||||
|
||||
let alertMessage = this.$t('PROFILE_SETTINGS.PASSWORD_UPDATE_SUCCESS');
|
||||
try {
|
||||
await this.$store.dispatch('updateProfile', {
|
||||
password: this.password,
|
||||
password_confirmation: this.passwordConfirmation,
|
||||
current_password: this.currentPassword,
|
||||
});
|
||||
this.errorMessage = this.$t('PROFILE_SETTINGS.PASSWORD_UPDATE_SUCCESS');
|
||||
} catch (error) {
|
||||
this.errorMessage =
|
||||
alertMessage =
|
||||
parseAPIErrorResponse(error) ||
|
||||
this.$t('RESET_PASSWORD.API.ERROR_MESSAGE');
|
||||
} finally {
|
||||
this.isPasswordChanging = false;
|
||||
this.showAlert(this.errorMessage);
|
||||
this.showAlert(alertMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~dashboard/assets/scss/mixins.scss';
|
||||
|
||||
.profile--settings--row {
|
||||
@include border-normal-bottom;
|
||||
padding: var(--space-normal);
|
||||
.small-3 {
|
||||
padding: var(--space-normal) var(--space-medium) var(--space-normal) 0;
|
||||
}
|
||||
.small-9 {
|
||||
padding: var(--space-normal);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,152 +1,116 @@
|
||||
<template>
|
||||
<div class="overflow-auto p-6">
|
||||
<form @submit.prevent="updateUser('profile')">
|
||||
<div
|
||||
class="flex flex-row border-b border-slate-50 dark:border-slate-700 items-center flex p-4"
|
||||
>
|
||||
<div class="w-1/4 py-4 pr-6 ml-0">
|
||||
<h4 class="text-lg text-black-900 dark:text-slate-200">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PROFILE_SECTION.TITLE') }}
|
||||
</h4>
|
||||
<p>{{ $t('PROFILE_SETTINGS.FORM.PROFILE_SECTION.NOTE') }}</p>
|
||||
</div>
|
||||
<div class="p-4 w-[45%]">
|
||||
<woot-avatar-uploader
|
||||
:label="$t('PROFILE_SETTINGS.FORM.PROFILE_IMAGE.LABEL')"
|
||||
:src="avatarUrl"
|
||||
@change="handleImageUpload"
|
||||
/>
|
||||
<div v-if="showDeleteButton" class="avatar-delete-btn">
|
||||
<woot-button
|
||||
type="button"
|
||||
color-scheme="alert"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
@click="deleteAvatar"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.DELETE_AVATAR') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<label :class="{ error: $v.name.$error }">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model="name"
|
||||
type="text"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.NAME.PLACEHOLDER')"
|
||||
@input="$v.name.$touch"
|
||||
/>
|
||||
<span v-if="$v.name.$error" class="message">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.NAME.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label :class="{ error: $v.displayName.$error }">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.LABEL') }}
|
||||
<input
|
||||
v-model="displayName"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.PLACEHOLDER')
|
||||
"
|
||||
@input="$v.displayName.$touch"
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
v-if="!globalConfig.disableUserProfileUpdate"
|
||||
:class="{ error: $v.email.$error }"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.FORM.EMAIL.LABEL') }}
|
||||
<input
|
||||
v-model.trim="email"
|
||||
type="email"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.EMAIL.PLACEHOLDER')"
|
||||
@input="$v.email.$touch"
|
||||
/>
|
||||
<span v-if="$v.email.$error" class="message">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.EMAIL.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<woot-button type="submit" :is-loading="isProfileUpdating">
|
||||
{{ $t('PROFILE_SETTINGS.BTN_TEXT') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<message-signature />
|
||||
<div
|
||||
class="border-b border-slate-50 dark:border-slate-700 items-center flex p-4 text-black-900 dark:text-slate-300 row"
|
||||
<div class="grid py-16 px-5 font-inter mx-auto gap-16 sm:max-w-[720px]">
|
||||
<div class="flex flex-col gap-6">
|
||||
<h2 class="text-2xl font-medium text-ash-900">
|
||||
{{ $t('PROFILE_SETTINGS.TITLE') }}
|
||||
</h2>
|
||||
<user-profile-picture
|
||||
:src="avatarUrl"
|
||||
:name="name"
|
||||
size="72px"
|
||||
@change="updateProfilePicture"
|
||||
@delete="deleteProfilePicture"
|
||||
/>
|
||||
<user-basic-details
|
||||
:name="name"
|
||||
:display-name="displayName"
|
||||
:email="email"
|
||||
:email-enabled="!globalConfig.disableUserProfileUpdate"
|
||||
@update-user="updateProfile"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<form-section
|
||||
:title="$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.TITLE')"
|
||||
:description="$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.NOTE')"
|
||||
>
|
||||
<div class="w-1/4 py-4 pr-6 ml-0">
|
||||
<h4 class="text-lg text-black-900 dark:text-slate-200">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.TITLE') }}
|
||||
</h4>
|
||||
<p>
|
||||
{{ $t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.NOTE') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4 w-[45%] flex gap-4 flex-row">
|
||||
<message-signature
|
||||
:message-signature="messageSignature"
|
||||
@update-signature="updateSignature"
|
||||
/>
|
||||
</form-section>
|
||||
<form-section
|
||||
:title="$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.TITLE')"
|
||||
:description="$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.NOTE')"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col justify-between w-full gap-5 sm:gap-4 sm:flex-row"
|
||||
>
|
||||
<button
|
||||
v-for="keyOption in keyOptions"
|
||||
:key="keyOption.key"
|
||||
class="cursor-pointer p-0"
|
||||
@click="toggleEditorMessageKey(keyOption.key)"
|
||||
v-for="hotKey in hotKeys"
|
||||
:key="hotKey.key"
|
||||
class="px-0 reset-base"
|
||||
>
|
||||
<preview-card
|
||||
:heading="keyOption.heading"
|
||||
:content="keyOption.content"
|
||||
:src="keyOption.src"
|
||||
:active="isEditorHotKeyEnabled(uiSettings, keyOption.key)"
|
||||
<hot-key-card
|
||||
:key="hotKey.title"
|
||||
:title="hotKey.title"
|
||||
:description="hotKey.description"
|
||||
:light-image="hotKey.lightImage"
|
||||
:dark-image="hotKey.darkImage"
|
||||
:active="isEditorHotKeyEnabled(uiSettings, hotKey.key)"
|
||||
@click="toggleHotKey(hotKey.key)"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<change-password v-if="!globalConfig.disableUserProfileUpdate" />
|
||||
<notification-settings />
|
||||
<div
|
||||
class="border-b border-slate-50 dark:border-slate-700 items-center flex p-4 text-black-900 dark:text-slate-300 row"
|
||||
</form-section>
|
||||
<form-section :title="$t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.TITLE')">
|
||||
<change-password v-if="!globalConfig.disableUserProfileUpdate" />
|
||||
</form-section>
|
||||
<form-section
|
||||
:title="$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.TITLE')"
|
||||
:description="
|
||||
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.NOTE')
|
||||
"
|
||||
>
|
||||
<div class="w-1/4 py-4 pr-6 ml-0">
|
||||
<h4 class="text-lg text-black-900 dark:text-slate-200">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.TITLE') }}
|
||||
</h4>
|
||||
<p>
|
||||
{{
|
||||
useInstallationName(
|
||||
$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.NOTE'),
|
||||
globalConfig.installationName
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4 w-[45%]">
|
||||
<masked-text :value="currentUser.access_token" />
|
||||
</div>
|
||||
</div>
|
||||
<audio-notifications />
|
||||
</form-section>
|
||||
<form-section :title="$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TITLE')">
|
||||
<notification-preferences />
|
||||
</form-section>
|
||||
<form-section
|
||||
:title="$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.TITLE')"
|
||||
:description="
|
||||
useInstallationName(
|
||||
$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.NOTE'),
|
||||
globalConfig.installationName
|
||||
)
|
||||
"
|
||||
>
|
||||
<access-token :value="currentUser.access_token" @on-copy="onCopyToken" />
|
||||
</form-section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { clearCookiesOnLogout } from '../../../../store/utils/api';
|
||||
import { hasValidAvatarUrl } from 'dashboard/helper/URLHelper';
|
||||
import NotificationSettings from './NotificationSettings.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import ChangePassword from './ChangePassword.vue';
|
||||
import MessageSignature from './MessageSignature.vue';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import uiSettingsMixin, {
|
||||
isEditorHotKeyEnabled,
|
||||
} from 'dashboard/mixins/uiSettings';
|
||||
import MaskedText from 'dashboard/components/MaskedText.vue';
|
||||
import PreviewCard from 'dashboard/components/ui/PreviewCard.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { clearCookiesOnLogout } from 'dashboard/store/utils/api.js';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
|
||||
import UserProfilePicture from './UserProfilePicture.vue';
|
||||
import UserBasicDetails from './UserBasicDetails.vue';
|
||||
import MessageSignature from './MessageSignature.vue';
|
||||
import HotKeyCard from './HotKeyCard.vue';
|
||||
import ChangePassword from './ChangePassword.vue';
|
||||
import NotificationPreferences from './NotificationPreferences.vue';
|
||||
import AudioNotifications from './AudioNotifications.vue';
|
||||
import FormSection from 'dashboard/components/FormSection.vue';
|
||||
import AccessToken from './AccessToken.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NotificationSettings,
|
||||
ChangePassword,
|
||||
MessageSignature,
|
||||
PreviewCard,
|
||||
MaskedText,
|
||||
FormSection,
|
||||
UserProfilePicture,
|
||||
UserBasicDetails,
|
||||
HotKeyCard,
|
||||
ChangePassword,
|
||||
NotificationPreferences,
|
||||
AudioNotifications,
|
||||
AccessToken,
|
||||
},
|
||||
mixins: [alertMixin, globalConfigMixin, uiSettingsMixin],
|
||||
data() {
|
||||
@@ -156,59 +120,40 @@ export default {
|
||||
name: '',
|
||||
displayName: '',
|
||||
email: '',
|
||||
isProfileUpdating: false,
|
||||
errorMessage: '',
|
||||
keyOptions: [
|
||||
messageSignature: '',
|
||||
hotKeys: [
|
||||
{
|
||||
key: 'enter',
|
||||
src: '/assets/images/dashboard/editor/enter-editor.png',
|
||||
heading: this.$t(
|
||||
title: this.$t(
|
||||
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.ENTER_KEY.HEADING'
|
||||
),
|
||||
content: this.$t(
|
||||
description: this.$t(
|
||||
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.ENTER_KEY.CONTENT'
|
||||
),
|
||||
lightImage: '/assets/images/dashboard/profile/hot-key-enter.svg',
|
||||
darkImage: '/assets/images/dashboard/profile/hot-key-enter-dark.svg',
|
||||
},
|
||||
{
|
||||
key: 'cmd_enter',
|
||||
src: '/assets/images/dashboard/editor/cmd-editor.png',
|
||||
heading: this.$t(
|
||||
title: this.$t(
|
||||
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.CMD_ENTER_KEY.HEADING'
|
||||
),
|
||||
content: this.$t(
|
||||
description: this.$t(
|
||||
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.CMD_ENTER_KEY.CONTENT'
|
||||
),
|
||||
lightImage: '/assets/images/dashboard/profile/hot-key-ctrl-enter.svg',
|
||||
darkImage:
|
||||
'/assets/images/dashboard/profile/hot-key-ctrl-enter-dark.svg',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
name: {
|
||||
required,
|
||||
minLength: minLength(1),
|
||||
},
|
||||
displayName: {},
|
||||
email: {
|
||||
required,
|
||||
email,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
currentUserId: 'getCurrentUserID',
|
||||
globalConfig: 'globalConfig/get',
|
||||
}),
|
||||
showDeleteButton() {
|
||||
return hasValidAvatarUrl(this.avatarUrl);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentUserId(newCurrentUserId, prevCurrentUserId) {
|
||||
if (prevCurrentUserId !== newCurrentUserId) {
|
||||
this.initializeUser();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.currentUserId) {
|
||||
@@ -221,45 +166,66 @@ export default {
|
||||
this.email = this.currentUser.email;
|
||||
this.avatarUrl = this.currentUser.avatar_url;
|
||||
this.displayName = this.currentUser.display_name;
|
||||
this.messageSignature = this.currentUser.message_signature;
|
||||
},
|
||||
isEditorHotKeyEnabled,
|
||||
async updateUser() {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.ERROR'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.isProfileUpdating = true;
|
||||
const hasEmailChanged = this.currentUser.email !== this.email;
|
||||
async dispatchUpdate(payload, successMessage, errorMessage) {
|
||||
let alertMessage = '';
|
||||
try {
|
||||
await this.$store.dispatch('updateProfile', {
|
||||
name: this.name,
|
||||
email: this.email,
|
||||
avatar: this.avatarFile,
|
||||
displayName: this.displayName,
|
||||
});
|
||||
this.isProfileUpdating = false;
|
||||
if (hasEmailChanged) {
|
||||
clearCookiesOnLogout();
|
||||
this.errorMessage = this.$t('PROFILE_SETTINGS.AFTER_EMAIL_CHANGED');
|
||||
}
|
||||
this.errorMessage = this.$t('PROFILE_SETTINGS.UPDATE_SUCCESS');
|
||||
await this.$store.dispatch('updateProfile', payload);
|
||||
alertMessage = successMessage;
|
||||
|
||||
return true; // return the value so that the status can be known
|
||||
} catch (error) {
|
||||
this.errorMessage = this.$t('RESET_PASSWORD.API.ERROR_MESSAGE');
|
||||
if (error?.response?.data?.error) {
|
||||
this.errorMessage = error.response.data.error;
|
||||
}
|
||||
alertMessage = error?.response?.data?.error
|
||||
? error.response.data.error
|
||||
: errorMessage;
|
||||
|
||||
return false; // return the value so that the status can be known
|
||||
} finally {
|
||||
this.isProfileUpdating = false;
|
||||
this.showAlert(this.errorMessage);
|
||||
this.showAlert(alertMessage);
|
||||
}
|
||||
},
|
||||
handleImageUpload({ file, url }) {
|
||||
async updateProfile(userAttributes) {
|
||||
const { name, email, displayName } = userAttributes;
|
||||
const hasEmailChanged = this.currentUser.email !== email;
|
||||
this.name = name || this.name;
|
||||
this.email = email || this.email;
|
||||
this.displayName = displayName || this.displayName;
|
||||
|
||||
const updatePayload = {
|
||||
name: this.name,
|
||||
email: this.email,
|
||||
displayName: this.displayName,
|
||||
avatar: this.avatarFile,
|
||||
};
|
||||
|
||||
const success = await this.dispatchUpdate(
|
||||
updatePayload,
|
||||
hasEmailChanged
|
||||
? this.$t('PROFILE_SETTINGS.AFTER_EMAIL_CHANGED')
|
||||
: this.$t('PROFILE_SETTINGS.UPDATE_SUCCESS'),
|
||||
this.$t('RESET_PASSWORD.API.ERROR_MESSAGE')
|
||||
);
|
||||
|
||||
if (hasEmailChanged && success) clearCookiesOnLogout();
|
||||
},
|
||||
async updateSignature(signature) {
|
||||
const payload = { message_signature: signature };
|
||||
let successMessage = this.$t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_SUCCESS'
|
||||
);
|
||||
let errorMessage = this.$t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_ERROR'
|
||||
);
|
||||
|
||||
await this.dispatchUpdate(payload, successMessage, errorMessage);
|
||||
},
|
||||
updateProfilePicture({ file, url }) {
|
||||
this.avatarFile = file;
|
||||
this.avatarUrl = url;
|
||||
},
|
||||
async deleteAvatar() {
|
||||
async deleteProfilePicture() {
|
||||
try {
|
||||
await this.$store.dispatch('deleteAvatar');
|
||||
this.avatarUrl = '';
|
||||
@@ -269,12 +235,19 @@ export default {
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.AVATAR_DELETE_FAILED'));
|
||||
}
|
||||
},
|
||||
toggleEditorMessageKey(key) {
|
||||
toggleHotKey(key) {
|
||||
this.hotKeys = this.hotKeys.map(hotKey =>
|
||||
hotKey.key === key ? { ...hotKey, active: !hotKey.active } : hotKey
|
||||
);
|
||||
this.updateUISettings({ editor_message_key: key });
|
||||
this.showAlert(
|
||||
this.$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.UPDATE_SUCCESS')
|
||||
);
|
||||
},
|
||||
async onCopyToken(value) {
|
||||
await copyTextToClipboard(value);
|
||||
this.showAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,110 +1,50 @@
|
||||
<template>
|
||||
<div
|
||||
class="profile--settings--row text-black-900 dark:text-slate-300 flex items-center"
|
||||
>
|
||||
<div class="w-1/4 py-4 pr-6 ml-0">
|
||||
<h4 class="text-lg text-black-900 dark:text-slate-200">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.TITLE') }}
|
||||
</h4>
|
||||
<p>{{ $t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.NOTE') }}</p>
|
||||
</div>
|
||||
<div class="p-4 w-[45%]">
|
||||
<div>
|
||||
<label for="message-signature-input">{{
|
||||
$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE.LABEL')
|
||||
}}</label>
|
||||
<woot-message-editor
|
||||
id="message-signature-input"
|
||||
v-model="messageSignature"
|
||||
class="message-editor h-[10rem]"
|
||||
:is-format-mode="true"
|
||||
:placeholder="
|
||||
$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE.PLACEHOLDER')
|
||||
"
|
||||
:enabled-menu-options="customEditorMenuList"
|
||||
:enable-suggestions="false"
|
||||
:show-image-resize-toolbar="true"
|
||||
/>
|
||||
</div>
|
||||
<woot-button
|
||||
:is-loading="isUpdating"
|
||||
type="button"
|
||||
@click.prevent="updateSignature"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.BTN_TEXT') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
<form class="flex flex-col gap-6" @submit.prevent="updateSignature()">
|
||||
<woot-message-editor
|
||||
id="message-signature-input"
|
||||
v-model="signature"
|
||||
class="message-editor h-[10rem] !px-3"
|
||||
:is-format-mode="true"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE.PLACEHOLDER')"
|
||||
:enabled-menu-options="customEditorMenuList"
|
||||
:enable-suggestions="false"
|
||||
:show-image-resize-toolbar="true"
|
||||
/>
|
||||
<form-button
|
||||
type="submit"
|
||||
color-scheme="primary"
|
||||
variant="solid"
|
||||
size="large"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.BTN_TEXT') }}
|
||||
</form-button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { MESSAGE_SIGNATURE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
|
||||
import FormButton from 'v3/components/Form/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootMessageEditor,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
data() {
|
||||
return {
|
||||
messageSignature: '',
|
||||
enableMessageSignature: false,
|
||||
isUpdating: false,
|
||||
errorMessage: '',
|
||||
customEditorMenuList: MESSAGE_SIGNATURE_EDITOR_MENU_OPTIONS,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
currentUserId: 'getCurrentUserID',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.initValues();
|
||||
},
|
||||
methods: {
|
||||
initValues() {
|
||||
const { message_signature: messageSignature } = this.currentUser;
|
||||
this.messageSignature = messageSignature || '';
|
||||
},
|
||||
async updateSignature() {
|
||||
try {
|
||||
await this.$store.dispatch('updateProfile', {
|
||||
message_signature: this.messageSignature || '',
|
||||
});
|
||||
this.errorMessage = this.$t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_SUCCESS'
|
||||
);
|
||||
} catch (error) {
|
||||
this.errorMessage = this.$t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_ERROR'
|
||||
);
|
||||
if (error?.response?.data?.message) {
|
||||
this.errorMessage = error.response.data.message;
|
||||
}
|
||||
} finally {
|
||||
this.isUpdating = false;
|
||||
this.initValues();
|
||||
this.showAlert(this.errorMessage);
|
||||
}
|
||||
},
|
||||
const props = defineProps({
|
||||
messageSignature: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const customEditorMenuList = MESSAGE_SIGNATURE_EDITOR_MENU_OPTIONS;
|
||||
const signature = ref(props.messageSignature);
|
||||
const emit = defineEmits(['update-signature']);
|
||||
|
||||
watch(
|
||||
() => props.messageSignature ?? '',
|
||||
newValue => {
|
||||
signature.value = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
const updateSignature = () => {
|
||||
emit('update-signature', signature.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.message-editor {
|
||||
@apply px-3 mb-4;
|
||||
|
||||
::v-deep {
|
||||
.ProseMirror-menubar {
|
||||
@apply left-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+19
-9
@@ -31,10 +31,6 @@
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.PUSH') }}
|
||||
</span>
|
||||
<form-switch
|
||||
:value="hasEnabledPushPermissions"
|
||||
@input="onRequestPermissions"
|
||||
/>
|
||||
</div>
|
||||
</table-header-cell>
|
||||
</div>
|
||||
@@ -94,10 +90,6 @@
|
||||
<span class="text-sm font-medium normal-case text-ash-900">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.TITLE') }}
|
||||
</span>
|
||||
<form-switch
|
||||
:value="hasEnabledPushPermissions"
|
||||
@input="onRequestPermissions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
@@ -116,6 +108,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between w-full gap-2 p-4 border border-solid border-ash-200 rounded-xl"
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<fluent-icon
|
||||
icon="alert"
|
||||
class="flex-shrink-0 text-ash-900"
|
||||
size="18"
|
||||
/>
|
||||
<span class="text-sm text-ash-900">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.BROWSER_PERMISSION') }}
|
||||
</span>
|
||||
</div>
|
||||
<form-switch
|
||||
:value="hasEnabledPushPermissions"
|
||||
@input="onRequestPermissions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -139,7 +150,6 @@ export default {
|
||||
components: {
|
||||
TableHeaderCell,
|
||||
FormSwitch,
|
||||
|
||||
CheckBox,
|
||||
},
|
||||
mixins: [alertMixin, configMixin, uiSettingsMixin],
|
||||
@@ -1,649 +0,0 @@
|
||||
<template>
|
||||
<div id="profile-settings-notifications">
|
||||
<div
|
||||
class="profile--settings--row text-black-900 dark:text-slate-300 flex items-center"
|
||||
>
|
||||
<div class="w-1/4">
|
||||
<h4 class="text-lg text-black-900 dark:text-slate-200">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.TITLE') }}
|
||||
</h4>
|
||||
<p>
|
||||
{{ $t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.NOTE') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-[45%] p-4">
|
||||
<div class="mb-4">
|
||||
<span class="text-sm notification-label">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.ALERT_TYPE.TITLE'
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
id="audio_enable_alert_none"
|
||||
v-model="enableAudioAlerts"
|
||||
class="notification--checkbox"
|
||||
type="radio"
|
||||
value="none"
|
||||
@input="handleAudioInput"
|
||||
/>
|
||||
<label for="audio_enable_alert_none">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.ALERT_TYPE.NONE'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
id="audio_enable_alert_mine"
|
||||
v-model="enableAudioAlerts"
|
||||
class="notification--checkbox"
|
||||
type="radio"
|
||||
value="mine"
|
||||
@input="handleAudioInput"
|
||||
/>
|
||||
<label for="audio_enable_alert_mine">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.ALERT_TYPE.ASSIGNED'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
id="audio_enable_alert_all"
|
||||
v-model="enableAudioAlerts"
|
||||
class="notification--checkbox"
|
||||
type="radio"
|
||||
value="all"
|
||||
@input="handleAudioInput"
|
||||
/>
|
||||
<label for="audio_enable_alert_all">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.ALERT_TYPE.ALL_CONVERSATIONS'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<span class="text-sm notification-label">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.DEFAULT_TONE.TITLE'
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<div>
|
||||
<select
|
||||
v-model="notificationTone"
|
||||
class="tone-selector mb-0"
|
||||
@change="handleAudioToneChange"
|
||||
>
|
||||
<option
|
||||
v-for="tone in notificationAlertTones"
|
||||
:key="tone.value"
|
||||
:value="tone.value"
|
||||
>
|
||||
{{ tone.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<span class="text-sm notification-label">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.CONDITIONS.TITLE'
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
id="audio_alert_when_tab_is_inactive"
|
||||
v-model="playAudioWhenTabIsInactive"
|
||||
class="notification--checkbox"
|
||||
type="checkbox"
|
||||
value="tab_is_inactive"
|
||||
@input="handleAudioAlertConditions"
|
||||
/>
|
||||
<label for="audio_alert_when_tab_is_inactive">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.CONDITIONS.CONDITION_ONE'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
id="audio_alert_until_all_conversations_are_read"
|
||||
v-model="alertIfUnreadConversationExist"
|
||||
class="notification--checkbox"
|
||||
type="checkbox"
|
||||
value="conversations_are_read"
|
||||
@input="handleAudioAlertConditions"
|
||||
/>
|
||||
<label for="audio_alert_until_all_conversations_are_read">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.CONDITIONS.CONDITION_TWO'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="profile--settings--row text-black-900 dark:text-slate-300 flex items-center"
|
||||
>
|
||||
<div class="w-1/4">
|
||||
<h4 class="text-lg text-black-900 dark:text-slate-200">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.TITLE') }}
|
||||
</h4>
|
||||
<p>
|
||||
{{ $t('PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.NOTE') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-[45%] p-4">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
v-model="selectedEmailFlags"
|
||||
class="notification--checkbox"
|
||||
type="checkbox"
|
||||
value="email_conversation_creation"
|
||||
@input="handleEmailInput"
|
||||
/>
|
||||
<label for="conversation_creation">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.CONVERSATION_CREATION'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
v-model="selectedEmailFlags"
|
||||
class="notification--checkbox"
|
||||
type="checkbox"
|
||||
value="email_conversation_assignment"
|
||||
@input="handleEmailInput"
|
||||
/>
|
||||
<label for="conversation_assignment">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.CONVERSATION_ASSIGNMENT'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
v-model="selectedEmailFlags"
|
||||
class="notification--checkbox"
|
||||
type="checkbox"
|
||||
value="email_conversation_mention"
|
||||
@input="handleEmailInput"
|
||||
/>
|
||||
<label for="conversation_mention">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.CONVERSATION_MENTION'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
v-model="selectedEmailFlags"
|
||||
class="notification--checkbox"
|
||||
type="checkbox"
|
||||
value="email_assigned_conversation_new_message"
|
||||
@input="handleEmailInput"
|
||||
/>
|
||||
<label for="assigned_conversation_new_message">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.ASSIGNED_CONVERSATION_NEW_MESSAGE'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
v-model="selectedEmailFlags"
|
||||
class="notification--checkbox"
|
||||
type="checkbox"
|
||||
value="email_participating_conversation_new_message"
|
||||
@input="handleEmailInput"
|
||||
/>
|
||||
<label for="assigned_conversation_new_message">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.PARTICIPATING_CONVERSATION_NEW_MESSAGE'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="isSLAEnabled" class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
v-model="selectedEmailFlags"
|
||||
class="notification--checkbox"
|
||||
type="checkbox"
|
||||
value="email_sla_missed_first_response"
|
||||
@input="handleEmailInput"
|
||||
/>
|
||||
<label for="sla_missed_first_response">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.SLA_MISSED_FIRST_RESPONSE'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="isSLAEnabled" class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
v-model="selectedEmailFlags"
|
||||
class="notification--checkbox"
|
||||
type="checkbox"
|
||||
value="email_sla_missed_next_response"
|
||||
@input="handleEmailInput"
|
||||
/>
|
||||
<label for="sla_missed_next_response">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.SLA_MISSED_NEXT_RESPONSE'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="isSLAEnabled" class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
v-model="selectedEmailFlags"
|
||||
class="notification--checkbox"
|
||||
type="checkbox"
|
||||
value="email_sla_missed_resolution"
|
||||
@input="handleEmailInput"
|
||||
/>
|
||||
<label for="sla_missed_resolution">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.SLA_MISSED_RESOLUTION'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="vapidPublicKey && hasPushAPISupport"
|
||||
class="profile--settings--row text-black-900 dark:text-slate-300 flex items-center push-row"
|
||||
>
|
||||
<div class="w-1/4">
|
||||
<h4 class="text-lg text-black-900 dark:text-slate-200">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.TITLE') }}
|
||||
</h4>
|
||||
<p>{{ $t('PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.NOTE') }}</p>
|
||||
</div>
|
||||
<div class="w-[45%] p-4">
|
||||
<p v-if="hasEnabledPushPermissions">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.HAS_ENABLED_PUSH'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<div v-else class="push-notification--button">
|
||||
<woot-submit-button
|
||||
:button-text="
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.REQUEST_PUSH'
|
||||
)
|
||||
"
|
||||
class="button nice small"
|
||||
type="button"
|
||||
@click="onRequestPermissions"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
v-model="selectedPushFlags"
|
||||
class="notification--checkbox"
|
||||
type="checkbox"
|
||||
value="push_conversation_creation"
|
||||
@input="handlePushInput"
|
||||
/>
|
||||
<label for="conversation_creation">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.CONVERSATION_CREATION'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
v-model="selectedPushFlags"
|
||||
class="notification--checkbox"
|
||||
type="checkbox"
|
||||
value="push_conversation_assignment"
|
||||
@input="handlePushInput"
|
||||
/>
|
||||
<label for="conversation_assignment">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.CONVERSATION_ASSIGNMENT'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
v-model="selectedPushFlags"
|
||||
class="notification--checkbox"
|
||||
type="checkbox"
|
||||
value="push_conversation_mention"
|
||||
@input="handlePushInput"
|
||||
/>
|
||||
<label for="conversation_mention">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.CONVERSATION_MENTION'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
v-model="selectedPushFlags"
|
||||
class="notification--checkbox"
|
||||
type="checkbox"
|
||||
value="push_assigned_conversation_new_message"
|
||||
@input="handlePushInput"
|
||||
/>
|
||||
<label for="assigned_conversation_new_message">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.ASSIGNED_CONVERSATION_NEW_MESSAGE'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
v-model="selectedPushFlags"
|
||||
class="notification--checkbox"
|
||||
type="checkbox"
|
||||
value="push_participating_conversation_new_message"
|
||||
@input="handlePushInput"
|
||||
/>
|
||||
<label for="assigned_conversation_new_message">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.PARTICIPATING_CONVERSATION_NEW_MESSAGE'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="isSLAEnabled" class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
v-model="selectedPushFlags"
|
||||
class="notification--checkbox"
|
||||
type="checkbox"
|
||||
value="push_sla_missed_first_response"
|
||||
@input="handlePushInput"
|
||||
/>
|
||||
<label for="sla_missed_first_response">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.SLA_MISSED_FIRST_RESPONSE'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="isSLAEnabled" class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
v-model="selectedPushFlags"
|
||||
class="notification--checkbox"
|
||||
type="checkbox"
|
||||
value="push_sla_missed_next_response"
|
||||
@input="handlePushInput"
|
||||
/>
|
||||
<label for="sla_missed_next_response">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.SLA_MISSED_NEXT_RESPONSE'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="isSLAEnabled" class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
v-model="selectedPushFlags"
|
||||
class="notification--checkbox"
|
||||
type="checkbox"
|
||||
value="push_sla_missed_resolution"
|
||||
@input="handlePushInput"
|
||||
/>
|
||||
<label for="sla_missed_resolution">
|
||||
{{
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.SLA_MISSED_RESOLUTION'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import configMixin from 'shared/mixins/configMixin';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import {
|
||||
hasPushPermissions,
|
||||
requestPushPermissions,
|
||||
verifyServiceWorkerExistence,
|
||||
} from '../../../../helper/pushHelper';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin, configMixin, uiSettingsMixin],
|
||||
data() {
|
||||
return {
|
||||
selectedEmailFlags: [],
|
||||
selectedPushFlags: [],
|
||||
enableAudioAlerts: false,
|
||||
hasEnabledPushPermissions: false,
|
||||
playAudioWhenTabIsInactive: false,
|
||||
alertIfUnreadConversationExist: false,
|
||||
notificationTone: 'ding',
|
||||
notificationAlertTones: [
|
||||
{
|
||||
value: 'ding',
|
||||
label: 'Ding',
|
||||
},
|
||||
{
|
||||
value: 'bell',
|
||||
label: 'Bell',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
emailFlags: 'userNotificationSettings/getSelectedEmailFlags',
|
||||
pushFlags: 'userNotificationSettings/getSelectedPushFlags',
|
||||
uiSettings: 'getUISettings',
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
}),
|
||||
hasPushAPISupport() {
|
||||
return !!('Notification' in window);
|
||||
},
|
||||
isSLAEnabled() {
|
||||
return this.isFeatureEnabledonAccount(this.accountId, FEATURE_FLAGS.SLA);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
emailFlags(value) {
|
||||
this.selectedEmailFlags = value;
|
||||
},
|
||||
pushFlags(value) {
|
||||
this.selectedPushFlags = value;
|
||||
},
|
||||
uiSettings(value) {
|
||||
this.notificationUISettings(value);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (hasPushPermissions()) {
|
||||
this.getPushSubscription();
|
||||
}
|
||||
this.notificationUISettings(this.uiSettings);
|
||||
this.$store.dispatch('userNotificationSettings/get');
|
||||
},
|
||||
methods: {
|
||||
notificationUISettings(uiSettings) {
|
||||
const {
|
||||
enable_audio_alerts: enableAudio = false,
|
||||
always_play_audio_alert: alwaysPlayAudioAlert,
|
||||
alert_if_unread_assigned_conversation_exist:
|
||||
alertIfUnreadConversationExist,
|
||||
notification_tone: notificationTone,
|
||||
} = uiSettings;
|
||||
this.enableAudioAlerts = enableAudio;
|
||||
this.playAudioWhenTabIsInactive = !alwaysPlayAudioAlert;
|
||||
this.alertIfUnreadConversationExist = alertIfUnreadConversationExist;
|
||||
this.notificationTone = notificationTone || 'ding';
|
||||
},
|
||||
onRegistrationSuccess() {
|
||||
this.hasEnabledPushPermissions = true;
|
||||
},
|
||||
onRequestPermissions() {
|
||||
requestPushPermissions({
|
||||
onSuccess: this.onRegistrationSuccess,
|
||||
});
|
||||
},
|
||||
getPushSubscription() {
|
||||
verifyServiceWorkerExistence(registration =>
|
||||
registration.pushManager
|
||||
.getSubscription()
|
||||
.then(subscription => {
|
||||
if (!subscription) {
|
||||
this.hasEnabledPushPermissions = false;
|
||||
} else {
|
||||
this.hasEnabledPushPermissions = true;
|
||||
}
|
||||
})
|
||||
// eslint-disable-next-line no-console
|
||||
.catch(error => console.log(error))
|
||||
);
|
||||
},
|
||||
async updateNotificationSettings() {
|
||||
try {
|
||||
this.$store.dispatch('userNotificationSettings/update', {
|
||||
selectedEmailFlags: this.selectedEmailFlags,
|
||||
selectedPushFlags: this.selectedPushFlags,
|
||||
});
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_ERROR'));
|
||||
}
|
||||
},
|
||||
handleEmailInput(e) {
|
||||
this.selectedEmailFlags = this.toggleInput(
|
||||
this.selectedEmailFlags,
|
||||
e.target.value
|
||||
);
|
||||
|
||||
this.updateNotificationSettings();
|
||||
},
|
||||
handlePushInput(e) {
|
||||
this.selectedPushFlags = this.toggleInput(
|
||||
this.selectedPushFlags,
|
||||
e.target.value
|
||||
);
|
||||
|
||||
this.updateNotificationSettings();
|
||||
},
|
||||
handleAudioInput(e) {
|
||||
this.enableAudioAlerts = e.target.value;
|
||||
this.updateUISettings({
|
||||
enable_audio_alerts: this.enableAudioAlerts,
|
||||
});
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
|
||||
},
|
||||
handleAudioAlertConditions(e) {
|
||||
let condition = e.target.value;
|
||||
if (condition === 'tab_is_inactive') {
|
||||
this.updateUISettings({
|
||||
always_play_audio_alert: !e.target.checked,
|
||||
});
|
||||
} else if (condition === 'conversations_are_read') {
|
||||
this.updateUISettings({
|
||||
alert_if_unread_assigned_conversation_exist: e.target.checked,
|
||||
});
|
||||
}
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
|
||||
},
|
||||
handleAudioToneChange(e) {
|
||||
this.updateUISettings({ notification_tone: e.target.value });
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
|
||||
},
|
||||
toggleInput(selected, current) {
|
||||
if (selected.includes(current)) {
|
||||
const newSelectedFlags = selected.filter(flag => flag !== current);
|
||||
return newSelectedFlags;
|
||||
}
|
||||
return [...selected, current];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~dashboard/assets/scss/variables.scss';
|
||||
|
||||
.notification--checkbox {
|
||||
font-size: $font-size-large;
|
||||
}
|
||||
|
||||
.push-notification--button {
|
||||
margin-bottom: var(--space-one);
|
||||
}
|
||||
|
||||
.notification-label {
|
||||
display: flex;
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: var(--space-small);
|
||||
}
|
||||
|
||||
.tone-selector {
|
||||
height: var(--space-large);
|
||||
padding-bottom: var(--space-micro);
|
||||
padding-top: var(--space-micro);
|
||||
width: var(--space-mega);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col justify-between flex-1 h-full m-0 overflow-auto bg-white dark:bg-slate-900"
|
||||
>
|
||||
<keep-alive v-if="keepAlive">
|
||||
<router-view />
|
||||
</keep-alive>
|
||||
<router-view v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
keepAlive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
const SettingsContent = () => import('../Wrapper.vue');
|
||||
const SettingsContent = () => import('./Wrapper.vue');
|
||||
const Index = () => import('./Index.vue');
|
||||
|
||||
export default {
|
||||
@@ -10,12 +10,6 @@ export default {
|
||||
name: 'profile_settings',
|
||||
roles: ['administrator', 'agent'],
|
||||
component: SettingsContent,
|
||||
props: {
|
||||
headerTitle: 'PROFILE_SETTINGS.TITLE',
|
||||
icon: 'edit',
|
||||
showNewButton: false,
|
||||
showSidemenuIcon: false,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'settings',
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import FilterButton from './FilterButton.vue';
|
||||
import FilterListDropdown from './FilterListDropdown.vue';
|
||||
import FilterButton from 'dashboard/components/ui/Dropdown/DropdownButton.vue';
|
||||
import FilterListDropdown from 'dashboard/components/ui/Dropdown/DropdownList.vue';
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
<script setup>
|
||||
import FilterButton from './FilterButton.vue';
|
||||
import FilterListDropdown from './FilterListDropdown.vue';
|
||||
import FilterListItemButton from './FilterListItemButton.vue';
|
||||
import FilterDropdownEmptyState from './FilterDropdownEmptyState.vue';
|
||||
import FilterButton from 'dashboard/components/ui/Dropdown/DropdownButton.vue';
|
||||
import FilterListDropdown from 'dashboard/components/ui/Dropdown/DropdownList.vue';
|
||||
import FilterListItemButton from 'dashboard/components/ui/Dropdown/DropdownListItemButton.vue';
|
||||
import FilterDropdownEmptyState from 'dashboard/components/ui/Dropdown/DropdownEmptyState.vue';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
buttonText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
rightIcon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
leftIcon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<button
|
||||
class="inline-flex relative items-center p-1.5 w-fit h-8 gap-1.5 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-800 active:bg-slate-75 dark:active:bg-slate-800"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<slot name="leftIcon">
|
||||
<fluent-icon
|
||||
v-if="leftIcon"
|
||||
:icon="leftIcon"
|
||||
size="18"
|
||||
class="flex-shrink-0 text-slate-900 dark:text-slate-50"
|
||||
/>
|
||||
</slot>
|
||||
<span
|
||||
v-if="buttonText"
|
||||
class="text-sm font-medium truncate text-slate-900 dark:text-slate-50"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</span>
|
||||
<slot name="rightIcon">
|
||||
<fluent-icon
|
||||
v-if="rightIcon"
|
||||
:icon="rightIcon"
|
||||
size="18"
|
||||
class="flex-shrink-0 text-slate-900 dark:text-slate-50"
|
||||
/>
|
||||
</slot>
|
||||
|
||||
<slot name="dropdown" />
|
||||
</button>
|
||||
</template>
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
message: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-center h-10 text-sm text-slate-500 dark:text-slate-300"
|
||||
>
|
||||
{{ message }}
|
||||
</div>
|
||||
</template>
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
inputValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
inputPlaceholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showClearFilter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between h-10 min-h-[40px] sticky top-0 bg-white z-10 dark:bg-slate-800 gap-2 px-3 border-b rounded-t-xl border-slate-50 dark:border-slate-700"
|
||||
>
|
||||
<div class="flex items-center w-full gap-2">
|
||||
<fluent-icon
|
||||
icon="search"
|
||||
size="18"
|
||||
class="text-slate-400 dark:text-slate-400"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
class="w-full mb-0 text-sm bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-75 reset-base"
|
||||
:placeholder="inputPlaceholder"
|
||||
:value="inputValue"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
<!-- Clear filter button -->
|
||||
<woot-button
|
||||
v-if="!inputValue && showClearFilter"
|
||||
size="small"
|
||||
variant="clear"
|
||||
color-scheme="primary"
|
||||
class="!px-1 !py-1.5"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
{{ $t('REPORT.FILTER_ACTIONS.CLEAR_FILTER') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</template>
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
import FilterListItemButton from './FilterListItemButton.vue';
|
||||
import FilterDropdownSearch from './FilterDropdownSearch.vue';
|
||||
import FilterDropdownEmptyState from './FilterDropdownEmptyState.vue';
|
||||
|
||||
const props = defineProps({
|
||||
listItems: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
enableSearch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
inputPlaceholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
activeFilterId: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
showClearFilter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const searchTerm = ref('');
|
||||
|
||||
const onSearch = value => {
|
||||
searchTerm.value = value;
|
||||
};
|
||||
|
||||
const filteredListItems = computed(() => {
|
||||
if (!searchTerm.value) return props.listItems;
|
||||
return picoSearch(props.listItems, searchTerm.value, ['name']);
|
||||
});
|
||||
|
||||
const isDropdownListEmpty = computed(() => {
|
||||
return !filteredListItems.value.length;
|
||||
});
|
||||
|
||||
const isFilterActive = id => {
|
||||
if (!props.activeFilterId) return false;
|
||||
return id === props.activeFilterId;
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="absolute z-20 w-40 bg-white border shadow dark:bg-slate-800 rounded-xl border-slate-50 dark:border-slate-700/50 max-h-[400px]"
|
||||
@click.stop
|
||||
>
|
||||
<slot name="search">
|
||||
<filter-dropdown-search
|
||||
v-if="enableSearch && listItems.length"
|
||||
:input-value="searchTerm"
|
||||
:input-placeholder="inputPlaceholder"
|
||||
:show-clear-filter="showClearFilter"
|
||||
@input="onSearch"
|
||||
@click="$emit('removeFilter')"
|
||||
/>
|
||||
</slot>
|
||||
<slot name="listItem">
|
||||
<filter-dropdown-empty-state
|
||||
v-if="isDropdownListEmpty"
|
||||
:message="$t('REPORT.FILTER_ACTIONS.EMPTY_LIST')"
|
||||
/>
|
||||
<filter-list-item-button
|
||||
v-for="item in filteredListItems"
|
||||
:key="item.id"
|
||||
:is-active="isFilterActive(item.id)"
|
||||
:button-text="item.name"
|
||||
@click="$emit('click', item)"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
buttonText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<button
|
||||
class="relative inline-flex items-center justify-start w-full p-3 border-0 rounded-none first:rounded-t-xl last:rounded-b-xl h-11 hover:bg-slate-50 dark:hover:bg-slate-700 active:bg-slate-75 dark:active:bg-slate-800"
|
||||
@click.stop="$emit('click')"
|
||||
@mouseenter="$emit('mouseenter')"
|
||||
@mouseleave="$emit('mouseleave')"
|
||||
@focus="$emit('focus')"
|
||||
>
|
||||
<div class="inline-flex items-center gap-3 overflow-hidden">
|
||||
<span
|
||||
class="text-sm font-medium truncate text-slate-900 dark:text-slate-50"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</span>
|
||||
<fluent-icon
|
||||
v-if="isActive"
|
||||
icon="checkmark"
|
||||
size="18"
|
||||
class="flex-shrink-0 text-slate-900 dark:text-slate-50"
|
||||
/>
|
||||
</div>
|
||||
<slot name="dropdown" />
|
||||
</button>
|
||||
</template>
|
||||
+1
-1
@@ -63,7 +63,7 @@ import {
|
||||
getActiveFilter,
|
||||
getFilterType,
|
||||
} from './helpers/SLAFilterHelpers';
|
||||
import FilterButton from '../Filters/v3/FilterButton.vue';
|
||||
import FilterButton from 'dashboard/components/ui/Dropdown/DropdownButton.vue';
|
||||
import ActiveFilterChip from '../Filters/v3/ActiveFilterChip.vue';
|
||||
import AddFilterChip from '../Filters/v3/AddFilterChip.vue';
|
||||
|
||||
|
||||
@@ -13,12 +13,11 @@ import integrationapps from './integrationapps/integrations.routes';
|
||||
import integrations from './integrations/integrations.routes';
|
||||
import labels from './labels/labels.routes';
|
||||
import macros from './macros/macros.routes';
|
||||
import profile from './profile/profile.routes';
|
||||
import reports from './reports/reports.routes';
|
||||
import store from '../../../store';
|
||||
import sla from './sla/sla.routes';
|
||||
import teams from './teams/teams.routes';
|
||||
import personal from './personal/personal.routes';
|
||||
import profile from './profile/profile.routes';
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
@@ -47,10 +46,9 @@ export default {
|
||||
...integrations.routes,
|
||||
...labels.routes,
|
||||
...macros.routes,
|
||||
...profile.routes,
|
||||
...reports.routes,
|
||||
...sla.routes,
|
||||
...teams.routes,
|
||||
...personal.routes,
|
||||
...profile.routes,
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user