mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-03 04:27:53 +00:00
feat: Revamp notification and audio preferences (#9312)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
@@ -57,13 +57,19 @@
|
||||
},
|
||||
"ACCESS_TOKEN": {
|
||||
"TITLE": "Access Token",
|
||||
"NOTE": "This token can be used if you are building an API based integration"
|
||||
"NOTE": "This token can be used if you are building an API based integration",
|
||||
"COPY": "Copy"
|
||||
},
|
||||
"AUDIO_NOTIFICATIONS_SECTION": {
|
||||
"TITLE": "Audio Notifications",
|
||||
"NOTE": "Enable audio notifications in dashboard for new messages and conversations.",
|
||||
"ALERT_TYPES": {
|
||||
"NONE": "None",
|
||||
"MINE": "Assigned",
|
||||
"ALL": "All"
|
||||
},
|
||||
"ALERT_TYPE": {
|
||||
"TITLE": "Alert events:",
|
||||
"TITLE": "Alert events for conversations:",
|
||||
"NONE": "None",
|
||||
"ASSIGNED": "Assigned Conversations",
|
||||
"ALL_CONVERSATIONS": "All Conversations"
|
||||
@@ -89,6 +95,22 @@
|
||||
"SLA_MISSED_NEXT_RESPONSE": "Send email notifications when a conversation misses next response SLA",
|
||||
"SLA_MISSED_RESOLUTION": "Send email notifications when a conversation misses resolution SLA"
|
||||
},
|
||||
"NOTIFICATIONS": {
|
||||
"TITLE": "Notification preferences",
|
||||
"TYPE_TITLE": "Notification type",
|
||||
"EMAIL": "Email",
|
||||
"PUSH": "Push notification",
|
||||
"TYPES": {
|
||||
"CONVERSATION_CREATED": "A new conversation is created",
|
||||
"CONVERSATION_ASSIGNED": "A conversation is assigned to you",
|
||||
"CONVERSATION_MENTION": "You are mentioned in a conversation",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "A new message is created in an assigned conversation",
|
||||
"PARTICIPATING_CONVERSATION_NEW_MESSAGE": "A new message is created in a participating conversation",
|
||||
"SLA_MISSED_FIRST_RESPONSE": "A conversation misses first response SLA",
|
||||
"SLA_MISSED_NEXT_RESPONSE": "A conversation misses next response SLA",
|
||||
"SLA_MISSED_RESOLUTION": "A conversation misses resolution SLA"
|
||||
}
|
||||
},
|
||||
"API": {
|
||||
"UPDATE_SUCCESS": "Your notification preferences are updated successfully",
|
||||
"UPDATE_ERROR": "There is an error while updating the preferences, please try again"
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div>
|
||||
<label
|
||||
class="flex justify-between pb-1 text-sm font-medium leading-6 text-ash-900"
|
||||
>
|
||||
{{ label }}
|
||||
</label>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
class="flex flex-row items-start gap-2"
|
||||
>
|
||||
<CheckBox
|
||||
:is-checked="item.model"
|
||||
:value="item.value"
|
||||
@update="onChange"
|
||||
/>
|
||||
<label class="text-sm font-normal text-ash-900">
|
||||
{{ item.label }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import CheckBox from 'v3/components/Form/CheckBox.vue';
|
||||
defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['change']);
|
||||
const onChange = (id, value) => {
|
||||
emit('change', id, value);
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div>
|
||||
<label
|
||||
class="flex justify-between pb-1 text-sm font-medium leading-6 text-ash-900"
|
||||
>
|
||||
{{ label }}
|
||||
</label>
|
||||
<div
|
||||
class="flex flex-row justify-between h-10 max-w-xl p-2 border border-solid rounded-xl border-ash-200"
|
||||
>
|
||||
<div
|
||||
v-for="option in alertEvents"
|
||||
:key="option.value"
|
||||
class="flex flex-row items-center justify-center gap-2 px-4 border-r border-ash-200 grow last:border-r-0"
|
||||
>
|
||||
<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"
|
||||
type="radio"
|
||||
:value="option.value"
|
||||
/>
|
||||
<label
|
||||
:for="`radio-${option.value}`"
|
||||
class="text-sm font-medium"
|
||||
:class="
|
||||
selectedValue === option.value ? 'text-ash-900' : 'text-ash-800'
|
||||
"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
`PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.ALERT_TYPES.${option.label.toUpperCase()}`
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { ALERT_EVENTS } from './constants';
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: 'all',
|
||||
},
|
||||
});
|
||||
|
||||
const alertEvents = ALERT_EVENTS;
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const selectedValue = computed({
|
||||
get: () => props.value,
|
||||
set: value => {
|
||||
emit('update', value);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<form-select
|
||||
v-model="selectedValue"
|
||||
name="alertTone"
|
||||
spacing="compact"
|
||||
:value="selectedValue"
|
||||
:options="alertTones"
|
||||
:label="label"
|
||||
class="max-w-xl"
|
||||
>
|
||||
<option
|
||||
v-for="tone in alertTones"
|
||||
:key="tone.label"
|
||||
:value="tone.value"
|
||||
:selected="tone.value === selectedValue"
|
||||
>
|
||||
{{ tone.label }}
|
||||
</option>
|
||||
</form-select>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import FormSelect from 'v3/components/Form/Select.vue';
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: value => ['ding', 'bell'].includes(value),
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const alertTones = computed(() => [
|
||||
{
|
||||
value: 'ding',
|
||||
label: 'Ding',
|
||||
},
|
||||
{
|
||||
value: 'bell',
|
||||
label: 'Bell',
|
||||
},
|
||||
]);
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
const selectedValue = computed({
|
||||
get: () => props.value,
|
||||
set: value => {
|
||||
emit('change', value);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div id="profile-settings-notifications" class="flex flex-col gap-6">
|
||||
<audio-alert-tone
|
||||
:value="alertTone"
|
||||
:label="
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.DEFAULT_TONE.TITLE'
|
||||
)
|
||||
"
|
||||
@change="handleAudioToneChange"
|
||||
/>
|
||||
|
||||
<audio-alert-event
|
||||
:label="
|
||||
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.ALERT_TYPE.TITLE')
|
||||
"
|
||||
:value="audioAlert"
|
||||
@update="handAudioAlertChange"
|
||||
/>
|
||||
|
||||
<audio-alert-condition
|
||||
:items="audioAlertConditions"
|
||||
:label="
|
||||
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.CONDITIONS.TITLE')
|
||||
"
|
||||
@change="handleAudioAlertConditions"
|
||||
/>
|
||||
</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 AudioAlertTone from './AudioAlertTone.vue';
|
||||
import AudioAlertEvent from './AudioAlertEvent.vue';
|
||||
import AudioAlertCondition from './AudioAlertCondition.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AudioAlertEvent,
|
||||
AudioAlertTone,
|
||||
AudioAlertCondition,
|
||||
},
|
||||
mixins: [alertMixin, configMixin, uiSettingsMixin],
|
||||
data() {
|
||||
return {
|
||||
audioAlert: '',
|
||||
playAudioWhenTabIsInactive: false,
|
||||
alertIfUnreadConversationExist: false,
|
||||
alertTone: 'ding',
|
||||
audioAlertConditions: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
uiSettings: 'getUISettings',
|
||||
}),
|
||||
},
|
||||
watch: {
|
||||
uiSettings(value) {
|
||||
this.notificationUISettings(value);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.notificationUISettings(this.uiSettings);
|
||||
this.$store.dispatch('userNotificationSettings/get');
|
||||
},
|
||||
methods: {
|
||||
notificationUISettings(uiSettings) {
|
||||
const {
|
||||
enable_audio_alerts: audioAlert = '',
|
||||
always_play_audio_alert: alwaysPlayAudioAlert,
|
||||
alert_if_unread_assigned_conversation_exist:
|
||||
alertIfUnreadConversationExist,
|
||||
notification_tone: alertTone,
|
||||
} = uiSettings;
|
||||
this.audioAlert = audioAlert;
|
||||
this.playAudioWhenTabIsInactive = !alwaysPlayAudioAlert;
|
||||
this.alertIfUnreadConversationExist = alertIfUnreadConversationExist;
|
||||
this.audioAlertConditions = [
|
||||
{
|
||||
id: 'audio1',
|
||||
label: this.$t(
|
||||
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.CONDITIONS.CONDITION_ONE'
|
||||
),
|
||||
model: this.playAudioWhenTabIsInactive,
|
||||
value: 'tab_is_inactive',
|
||||
},
|
||||
{
|
||||
id: 'audio2',
|
||||
label: this.$t(
|
||||
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.CONDITIONS.CONDITION_TWO'
|
||||
),
|
||||
model: this.alertIfUnreadConversationExist,
|
||||
value: 'conversations_are_read',
|
||||
},
|
||||
];
|
||||
this.alertTone = alertTone || 'ding';
|
||||
},
|
||||
handAudioAlertChange(value) {
|
||||
this.audioAlert = value;
|
||||
this.updateUISettings({
|
||||
enable_audio_alerts: this.audioAlert,
|
||||
});
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
|
||||
},
|
||||
handleAudioAlertConditions(id, value) {
|
||||
if (id === 'tab_is_inactive') {
|
||||
this.updateUISettings({
|
||||
always_play_audio_alert: !value,
|
||||
});
|
||||
} else if (id === 'conversations_are_read') {
|
||||
this.updateUISettings({
|
||||
alert_if_unread_assigned_conversation_exist: value,
|
||||
});
|
||||
}
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
|
||||
},
|
||||
handleAudioToneChange(value) {
|
||||
this.updateUISettings({ notification_tone: value });
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<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 sm:max-w-[720px]">
|
||||
<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') }}
|
||||
@@ -62,6 +62,19 @@
|
||||
>
|
||||
<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>
|
||||
@@ -80,6 +93,8 @@ 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 {
|
||||
@@ -90,6 +105,8 @@ export default {
|
||||
UserBasicDetails,
|
||||
HotKeyCard,
|
||||
ChangePassword,
|
||||
NotificationPreferences,
|
||||
AudioNotifications,
|
||||
},
|
||||
mixins: [alertMixin, globalConfigMixin, uiSettingsMixin],
|
||||
data() {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-start gap-2 px-0 text-sm tracking-[0.5] text-left rtl:text-right"
|
||||
:class="`col-span-${span}`"
|
||||
>
|
||||
<input
|
||||
v-model="localFlags"
|
||||
class="mt-1 flex-shrink-0 border-ash-200 border checked:border-none checked:bg-primary-600 dark:checked:bg-primary-600 shadow appearance-none rounded-[4px] w-4 h-4 focus:ring-1 after:content-[''] after:text-white checked:after:content-['✓'] after:flex after:items-center after:justify-center after:text-center after:text-xs after:font-bold after:relative"
|
||||
type="checkbox"
|
||||
:value="localValue"
|
||||
@input="handleInput"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
span: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
selectedFlags: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['input']);
|
||||
|
||||
const localValue = ref(props.value);
|
||||
const localFlags = ref(props.selectedFlags);
|
||||
|
||||
watch(
|
||||
() => props.selectedFlags,
|
||||
newValue => {
|
||||
localFlags.value = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
const handleInput = e => {
|
||||
emit('input', props.type, e);
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<div id="profile-settings-notifications" class="flex flex-col gap-6">
|
||||
<!-- Layout for desktop devices -->
|
||||
<div class="hidden sm:block">
|
||||
<div
|
||||
class="grid content-center h-12 grid-cols-12 gap-4 py-0 rounded-t-xl"
|
||||
>
|
||||
<table-header-cell
|
||||
:span="7"
|
||||
label="`${$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPE_TITLE')}`"
|
||||
>
|
||||
<span class="text-sm font-normal normal-case text-ash-800">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPE_TITLE') }}
|
||||
</span>
|
||||
</table-header-cell>
|
||||
<table-header-cell
|
||||
:span="2"
|
||||
label="`${$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.EMAIL')}`"
|
||||
>
|
||||
<span class="text-sm font-medium normal-case text-ash-900">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.EMAIL') }}
|
||||
</span>
|
||||
</table-header-cell>
|
||||
<table-header-cell
|
||||
:span="3"
|
||||
label="`${$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.PUSH')}`"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-1">
|
||||
<span
|
||||
class="text-sm font-medium normal-case text-ash-900 whitespace-nowrap"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.PUSH') }}
|
||||
</span>
|
||||
<form-switch
|
||||
:value="hasEnabledPushPermissions"
|
||||
@input="onRequestPermissions"
|
||||
/>
|
||||
</div>
|
||||
</table-header-cell>
|
||||
</div>
|
||||
<div
|
||||
v-for="(notification, index) in filteredNotificationTypes"
|
||||
:key="index"
|
||||
>
|
||||
<div
|
||||
class="grid items-center content-center h-12 grid-cols-12 gap-4 py-0 rounded-t-xl"
|
||||
>
|
||||
<div
|
||||
class="flex flex-row items-start gap-2 col-span-7 px-0 py-2 text-sm tracking-[0.5] rtl:text-right"
|
||||
>
|
||||
<span class="text-sm text-ash-900">
|
||||
{{ $t(notification.label) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="(type, typeIndex) in ['email', 'push']"
|
||||
:key="typeIndex"
|
||||
class="flex items-start gap-2 px-0 text-sm tracking-[0.5] text-left rtl:text-right"
|
||||
:class="`col-span-${type === 'push' ? 3 : 2}`"
|
||||
>
|
||||
<CheckBox
|
||||
:value="`${type}_${notification.value}`"
|
||||
:is-checked="
|
||||
checkFlagStatus(type, notification.value, selectedPushFlags)
|
||||
"
|
||||
@update="id => handleInput(type, id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Layout for mobile devices -->
|
||||
<div class="flex flex-col gap-6 sm:hidden">
|
||||
<span class="text-sm font-medium normal-case text-ash-900">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.TITLE') }}
|
||||
</span>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div
|
||||
v-for="(notification, index) in filteredNotificationTypes"
|
||||
:key="index"
|
||||
class="flex flex-row items-start gap-2"
|
||||
>
|
||||
<CheckBox
|
||||
:id="`email_${notification.value}`"
|
||||
:value="`email_${notification.value}`"
|
||||
:is-checked="checkFlagStatus('email', notification.value)"
|
||||
@update="handleEmailInput"
|
||||
/>
|
||||
<span class="text-sm text-ash-900">{{ $t(notification.label) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-start gap-2">
|
||||
<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">
|
||||
<div
|
||||
v-for="(notification, index) in filteredNotificationTypes"
|
||||
:key="index"
|
||||
class="flex flex-row items-start gap-2"
|
||||
>
|
||||
<CheckBox
|
||||
:id="`push_${notification.value}`"
|
||||
:value="`push_${notification.value}`"
|
||||
:is-checked="checkFlagStatus('push', notification.value)"
|
||||
@update="handlePushInput"
|
||||
/>
|
||||
<span class="text-sm text-ash-900">{{ $t(notification.label) }}</span>
|
||||
</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 TableHeaderCell from 'dashboard/components/widgets/TableHeaderCell.vue';
|
||||
import CheckBox from 'v3/components/Form/CheckBox.vue';
|
||||
import {
|
||||
hasPushPermissions,
|
||||
requestPushPermissions,
|
||||
verifyServiceWorkerExistence,
|
||||
} from 'dashboard/helper/pushHelper.js';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import FormSwitch from 'v3/components/Form/Switch.vue';
|
||||
import { NOTIFICATION_TYPES } from './constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TableHeaderCell,
|
||||
FormSwitch,
|
||||
|
||||
CheckBox,
|
||||
},
|
||||
mixins: [alertMixin, configMixin, uiSettingsMixin],
|
||||
data() {
|
||||
return {
|
||||
selectedEmailFlags: [],
|
||||
selectedPushFlags: [],
|
||||
enableAudioAlerts: false,
|
||||
hasEnabledPushPermissions: false,
|
||||
notificationTypes: NOTIFICATION_TYPES,
|
||||
};
|
||||
},
|
||||
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);
|
||||
},
|
||||
filteredNotificationTypes() {
|
||||
return this.notificationTypes.filter(notification =>
|
||||
this.isSLAEnabled
|
||||
? true
|
||||
: ![
|
||||
'sla_missed_first_response',
|
||||
'sla_missed_next_response',
|
||||
'sla_missed_resolution',
|
||||
].includes(notification.value)
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
emailFlags(value) {
|
||||
this.selectedEmailFlags = value;
|
||||
},
|
||||
pushFlags(value) {
|
||||
this.selectedPushFlags = value;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (hasPushPermissions()) {
|
||||
this.getPushSubscription();
|
||||
}
|
||||
this.$store.dispatch('userNotificationSettings/get');
|
||||
},
|
||||
methods: {
|
||||
checkFlagStatus(type, flagType) {
|
||||
const selectedFlags =
|
||||
type === 'email' ? this.selectedEmailFlags : this.selectedPushFlags;
|
||||
return selectedFlags.includes(`${type}_${flagType}`);
|
||||
},
|
||||
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'));
|
||||
}
|
||||
},
|
||||
|
||||
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(value) {
|
||||
this.updateUISettings({ notification_tone: value });
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
|
||||
},
|
||||
handleInput(type, id) {
|
||||
if (type === 'email') {
|
||||
this.handleEmailInput(id);
|
||||
} else {
|
||||
this.handlePushInput(id);
|
||||
}
|
||||
},
|
||||
handleEmailInput(id) {
|
||||
this.selectedEmailFlags = this.toggleInput(this.selectedEmailFlags, id);
|
||||
this.updateNotificationSettings();
|
||||
},
|
||||
handlePushInput(id) {
|
||||
this.selectedPushFlags = this.toggleInput(this.selectedPushFlags, id);
|
||||
this.updateNotificationSettings();
|
||||
},
|
||||
toggleInput(selected, current) {
|
||||
if (selected.includes(current)) {
|
||||
const newSelectedFlags = selected.filter(flag => flag !== current);
|
||||
return newSelectedFlags;
|
||||
}
|
||||
return [...selected, current];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,52 @@
|
||||
export const NOTIFICATION_TYPES = [
|
||||
{
|
||||
label: 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.CONVERSATION_CREATED',
|
||||
value: 'conversation_creation',
|
||||
},
|
||||
{
|
||||
label: 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.CONVERSATION_ASSIGNED',
|
||||
value: 'conversation_assignment',
|
||||
},
|
||||
{
|
||||
label: 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.CONVERSATION_MENTION',
|
||||
value: 'conversation_mention',
|
||||
},
|
||||
{
|
||||
label:
|
||||
'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.ASSIGNED_CONVERSATION_NEW_MESSAGE',
|
||||
value: 'assigned_conversation_new_message',
|
||||
},
|
||||
{
|
||||
label:
|
||||
'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.PARTICIPATING_CONVERSATION_NEW_MESSAGE',
|
||||
value: 'participating_conversation_new_message',
|
||||
},
|
||||
{
|
||||
label:
|
||||
'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.SLA_MISSED_FIRST_RESPONSE',
|
||||
value: 'sla_missed_first_response',
|
||||
},
|
||||
{
|
||||
label: 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.SLA_MISSED_NEXT_RESPONSE',
|
||||
value: 'sla_missed_next_response',
|
||||
},
|
||||
{
|
||||
label: 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.SLA_MISSED_RESOLUTION',
|
||||
value: 'sla_missed_resolution',
|
||||
},
|
||||
];
|
||||
|
||||
export const ALERT_EVENTS = [
|
||||
{
|
||||
value: 'none',
|
||||
label: 'none',
|
||||
},
|
||||
{
|
||||
value: 'mine',
|
||||
label: 'mine',
|
||||
},
|
||||
{
|
||||
value: 'all',
|
||||
label: 'all',
|
||||
},
|
||||
];
|
||||
@@ -58,7 +58,8 @@ const attrs = useAttrs();
|
||||
const baseClasses = {
|
||||
outline: 'outline outline-1 -outline-offset-1 focus:ring focus:ring-offset-1',
|
||||
ghost: 'hover:text-600 active:text-600 focus:ring focus:ring-offset-1',
|
||||
solid: 'hover:bg-700 active:bg-700 focus:ring focus:ring-offset-1',
|
||||
solid:
|
||||
'hover:bg-700 active:bg-700 focus:ring focus:ring-offset-1 focus:ring-2',
|
||||
};
|
||||
|
||||
const colorClass = computed(() => {
|
||||
|
||||
31
app/javascript/v3/components/Form/CheckBox.vue
Normal file
31
app/javascript/v3/components/Form/CheckBox.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<input
|
||||
:id="value"
|
||||
v-model="checked"
|
||||
type="checkbox"
|
||||
:value="value"
|
||||
class="flex-shrink-0 mt-0.5 border-ash-200 border checked:border-none checked:bg-primary-600 dark:checked:bg-primary-600 shadow appearance-none rounded-[4px] w-4 h-4 focus:ring-1 after:content-[''] after:text-white checked:after:content-['✓'] after:flex after:items-center after:justify-center after:text-center after:text-xs after:font-bold after:relative"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits, computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
isChecked: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['input']);
|
||||
|
||||
const checked = computed({
|
||||
get: () => props.isChecked,
|
||||
set: value => emit('update', props.value, value),
|
||||
});
|
||||
</script>
|
||||
@@ -11,11 +11,11 @@
|
||||
:selected="value"
|
||||
:name="name"
|
||||
:class="{
|
||||
'text-slate-400': !value,
|
||||
'text-slate-900 dark:text-slate-100': value,
|
||||
'text-ash-400': !value,
|
||||
'text-ash-900': value,
|
||||
'pl-9': icon,
|
||||
}"
|
||||
class="block w-full px-3 py-2 pr-6 mb-0 border-0 rounded-md shadow-sm outline-none appearance-none select-caret ring-1 ring-inset placeholder:text-slate-400 focus:ring-2 focus:ring-inset focus:ring-woot-500 sm:text-sm sm:leading-6 dark:bg-slate-700 dark:ring-slate-600 dark:focus:ring-woot-500 ring-slate-200"
|
||||
class="block w-full px-3 py-2 pr-6 mb-0 border-0 shadow-sm outline-none appearance-none rounded-xl select-caret ring-ash-200 ring-1 ring-inset placeholder:text-ash-900 focus:ring-2 focus:ring-inset focus:ring-primary-500 sm:text-sm sm:leading-6"
|
||||
@input="onInput"
|
||||
>
|
||||
<option value="" disabled selected class="hidden">
|
||||
|
||||
35
app/javascript/v3/components/Form/Switch.vue
Normal file
35
app/javascript/v3/components/Form/Switch.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="relative flex-shrink-0 h-4 p-0 border-none shadow-inner w-7 rounded-3xl"
|
||||
:class="
|
||||
value ? 'bg-primary-600 shadow-primary-800' : 'shadow-ash-400 bg-ash-200'
|
||||
"
|
||||
role="switch"
|
||||
:aria-checked="value.toString()"
|
||||
@click="onClick"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="rounded-full bg-white top-0.5 absolute dark:bg-white w-3 h-3 translate-y-0 duration-200 transition-transform ease-in-out"
|
||||
:class="
|
||||
value
|
||||
? 'ltr:translate-x-0 rtl:translate-x-[12px]'
|
||||
: 'ltr:-translate-x-[12px] rtl:translate-x-0'
|
||||
"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: { type: Boolean, default: false },
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit('input', !this.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user