feat: Rewrite uiSettings mixin to a composable (#9819)

This commit is contained in:
Sivin Varghese
2024-07-23 21:27:22 +05:30
committed by GitHub
parent 79aa5a5d7f
commit fb99ba7b40
31 changed files with 579 additions and 385 deletions

View File

@@ -116,6 +116,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import VirtualList from 'vue-virtual-scroll-list'; import VirtualList from 'vue-virtual-scroll-list';
@@ -132,7 +133,6 @@ import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomView
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue'; import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue'; import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
import filterMixin from 'shared/mixins/filterMixin'; import filterMixin from 'shared/mixins/filterMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages'; import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
import countries from 'shared/constants/countries'; import countries from 'shared/constants/countries';
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper'; import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
@@ -157,12 +157,7 @@ export default {
IntersectionObserver, IntersectionObserver,
VirtualList, VirtualList,
}, },
mixins: [ mixins: [conversationMixin, keyboardEventListenerMixins, filterMixin],
conversationMixin,
keyboardEventListenerMixins,
filterMixin,
uiSettingsMixin,
],
provide() { provide() {
return { return {
// Actions to be performed on virtual list item and context menu. // Actions to be performed on virtual list item and context menu.
@@ -207,6 +202,13 @@ export default {
type: Boolean, type: Boolean,
}, },
}, },
setup() {
const { uiSettings } = useUISettings();
return {
uiSettings,
};
},
data() { data() {
return { return {
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME, activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,

View File

@@ -36,12 +36,12 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAdmin } from 'dashboard/composables/useAdmin'; import { useAdmin } from 'dashboard/composables/useAdmin';
import { useUISettings } from 'dashboard/composables/useUISettings';
import AICTAModal from './AICTAModal.vue'; import AICTAModal from './AICTAModal.vue';
import AIAssistanceModal from './AIAssistanceModal.vue'; import AIAssistanceModal from './AIAssistanceModal.vue';
import aiMixin from 'dashboard/mixins/aiMixin'; import aiMixin from 'dashboard/mixins/aiMixin';
import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents'; import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins'; import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue'; import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue';
export default { export default {
@@ -50,10 +50,14 @@ export default {
AICTAModal, AICTAModal,
AIAssistanceCTAButton, AIAssistanceCTAButton,
}, },
mixins: [aiMixin, keyboardEventListenerMixins, uiSettingsMixin], mixins: [aiMixin, keyboardEventListenerMixins],
setup() { setup() {
const { uiSettings, updateUISettings } = useUISettings();
const { isAdmin } = useAdmin(); const { isAdmin } = useAdmin();
return { return {
uiSettings,
updateUISettings,
isAdmin, isAdmin,
}; };
}, },

View File

@@ -1,14 +1,14 @@
<template> <template>
<div class="px-0 min-w-0 flex-1"> <div class="flex-1 min-w-0 px-0">
<woot-modal-header <woot-modal-header
:header-title="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.TITLE')" :header-title="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.TITLE')"
:header-content="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DESC')" :header-content="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DESC')"
/> />
<form <form
class="flex flex-wrap flex-col modal-content" class="flex flex-col flex-wrap modal-content"
@submit.prevent="finishOpenAI" @submit.prevent="finishOpenAI"
> >
<div class="mt-2 w-full"> <div class="w-full mt-2">
<woot-input <woot-input
v-model="value" v-model="value"
type="text" type="text"
@@ -19,7 +19,7 @@
@blur="$v.value.$touch" @blur="$v.value.$touch"
/> />
</div> </div>
<div class="flex flex-row justify-between gap-2 py-2 px-0 w-full"> <div class="flex flex-row justify-between w-full gap-2 px-0 py-2">
<woot-button variant="link" @click.prevent="openOpenAIDoc"> <woot-button variant="link" @click.prevent="openOpenAIDoc">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP') }} {{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP') }}
</woot-button> </woot-button>
@@ -39,13 +39,20 @@
<script> <script>
import { required } from 'vuelidate/lib/validators'; import { required } from 'vuelidate/lib/validators';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import aiMixin from 'dashboard/mixins/aiMixin';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import { useUISettings } from 'dashboard/composables/useUISettings';
import aiMixin from 'dashboard/mixins/aiMixin';
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
export default { export default {
mixins: [aiMixin, uiSettingsMixin], mixins: [aiMixin],
setup() {
const { updateUISettings } = useUISettings();
return {
updateUISettings,
};
},
data() { data() {
return { return {
value: '', value: '',

View File

@@ -80,8 +80,7 @@ import {
hasPressedCommandAndEnter, hasPressedCommandAndEnter,
} from 'shared/helpers/KeyboardHelpers'; } from 'shared/helpers/KeyboardHelpers';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins'; import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import { useUISettings } from 'dashboard/composables/useUISettings';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import { import {
replaceVariablesInMessage, replaceVariablesInMessage,
createTypingIndicator, createTypingIndicator,
@@ -119,7 +118,7 @@ const createState = (
export default { export default {
name: 'WootMessageEditor', name: 'WootMessageEditor',
components: { TagAgents, CannedResponse, VariableList }, components: { TagAgents, CannedResponse, VariableList },
mixins: [keyboardEventListenerMixins, uiSettingsMixin], mixins: [keyboardEventListenerMixins],
props: { props: {
value: { type: String, default: '' }, value: { type: String, default: '' },
editorId: { type: String, default: '' }, editorId: { type: String, default: '' },
@@ -139,6 +138,19 @@ export default {
channelType: { type: String, default: '' }, channelType: { type: String, default: '' },
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
}, },
setup() {
const {
uiSettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
} = useUISettings();
return {
uiSettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
};
},
data() { data() {
return { return {
typingIndicator: createTypingIndicator( typingIndicator: createTypingIndicator(
@@ -278,7 +290,7 @@ export default {
// this is considered the source of truth, we watch this property // this is considered the source of truth, we watch this property
// on change, we toggle the signature in the editor // on change, we toggle the signature in the editor
if (this.allowSignature && !this.isPrivate && this.channelType) { if (this.allowSignature && !this.isPrivate && this.channelType) {
return this.fetchSignatureFlagFromUiSettings(this.channelType); return this.fetchSignatureFlagFromUISettings(this.channelType);
} }
return false; return false;
@@ -521,10 +533,10 @@ export default {
} }
}, },
isEnterToSendEnabled() { isEnterToSendEnabled() {
return isEditorHotKeyEnabled(this.uiSettings, 'enter'); return this.isEditorHotKeyEnabled('enter');
}, },
isCmdPlusEnterToSendEnabled() { isCmdPlusEnterToSendEnabled() {
return isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter'); return this.isEditorHotKeyEnabled('cmd_enter');
}, },
getKeyboardEvents() { getKeyboardEvents() {
return { return {

View File

@@ -25,8 +25,8 @@ import {
} from '@chatwoot/prosemirror-schema'; } from '@chatwoot/prosemirror-schema';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper'; import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins'; import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
const createState = ( const createState = (
@@ -51,13 +51,21 @@ const createState = (
}; };
export default { export default {
mixins: [keyboardEventListenerMixins, uiSettingsMixin], mixins: [keyboardEventListenerMixins],
props: { props: {
value: { type: String, default: '' }, value: { type: String, default: '' },
editorId: { type: String, default: '' }, editorId: { type: String, default: '' },
placeholder: { type: String, default: '' }, placeholder: { type: String, default: '' },
enabledMenuOptions: { type: Array, default: () => [] }, enabledMenuOptions: { type: Array, default: () => [] },
}, },
setup() {
const { uiSettings, updateUISettings } = useUISettings();
return {
uiSettings,
updateUISettings,
};
},
data() { data() {
return { return {
editorView: null, editorView: null,

View File

@@ -134,10 +134,10 @@
</template> </template>
<script> <script>
import { useUISettings } from 'dashboard/composables/useUISettings';
import FileUpload from 'vue-upload-component'; import FileUpload from 'vue-upload-component';
import * as ActiveStorage from 'activestorage'; import * as ActiveStorage from 'activestorage';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins'; import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import inboxMixin from 'shared/mixins/inboxMixin'; import inboxMixin from 'shared/mixins/inboxMixin';
import { FEATURE_FLAGS } from 'dashboard/featureFlags'; import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { import {
@@ -153,7 +153,7 @@ import { mapGetters } from 'vuex';
export default { export default {
name: 'ReplyBottomPanel', name: 'ReplyBottomPanel',
components: { FileUpload, VideoCallButton, AIAssistanceButton }, components: { FileUpload, VideoCallButton, AIAssistanceButton },
mixins: [keyboardEventListenerMixins, uiSettingsMixin, inboxMixin], mixins: [keyboardEventListenerMixins, inboxMixin],
props: { props: {
mode: { mode: {
type: String, type: String,
@@ -248,6 +248,15 @@ export default {
required: true, required: true,
}, },
}, },
setup() {
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
useUISettings();
return {
setSignatureFlagForInbox,
fetchSignatureFlagFromUISettings,
};
},
computed: { computed: {
...mapGetters({ ...mapGetters({
accountId: 'getCurrentAccountId', accountId: 'getCurrentAccountId',
@@ -320,7 +329,7 @@ export default {
}, },
sendWithSignature() { sendWithSignature() {
// channelType is sourced from inboxMixin // channelType is sourced from inboxMixin
return this.fetchSignatureFlagFromUiSettings(this.channelType); return this.fetchSignatureFlagFromUISettings(this.channelType);
}, },
signatureToggleTooltip() { signatureToggleTooltip() {
return this.sendWithSignature return this.sendWithSignature

View File

@@ -12,10 +12,10 @@
<div <div
v-if="showActionsDropdown" v-if="showActionsDropdown"
v-on-clickaway="closeDropdown" v-on-clickaway="closeDropdown"
class="dropdown-pane dropdown-pane--open mt-1 right-0 basic-filter" class="right-0 mt-1 dropdown-pane dropdown-pane--open basic-filter"
> >
<div class="items-center flex justify-between last:mt-4"> <div class="flex items-center justify-between last:mt-4">
<span class="text-slate-800 dark:text-slate-100 text-xs font-medium">{{ <span class="text-xs font-medium text-slate-800 dark:text-slate-100">{{
$t('CHAT_LIST.CHAT_SORT.STATUS') $t('CHAT_LIST.CHAT_SORT.STATUS')
}}</span> }}</span>
<filter-item <filter-item
@@ -26,8 +26,8 @@
@onChangeFilter="onChangeFilter" @onChangeFilter="onChangeFilter"
/> />
</div> </div>
<div class="items-center flex justify-between last:mt-4"> <div class="flex items-center justify-between last:mt-4">
<span class="text-slate-800 dark:text-slate-100 text-xs font-medium">{{ <span class="text-xs font-medium text-slate-800 dark:text-slate-100">{{
$t('CHAT_LIST.CHAT_SORT.ORDER_BY') $t('CHAT_LIST.CHAT_SORT.ORDER_BY')
}}</span> }}</span>
<filter-item <filter-item
@@ -46,13 +46,19 @@
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import FilterItem from './FilterItem.vue'; import FilterItem from './FilterItem.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import { useUISettings } from 'dashboard/composables/useUISettings';
export default { export default {
components: { components: {
FilterItem, FilterItem,
}, },
mixins: [uiSettingsMixin], setup() {
const { updateUISettings } = useUISettings();
return {
updateUISettings,
};
},
data() { data() {
return { return {
showActionsDropdown: false, showActionsDropdown: false,

View File

@@ -154,6 +154,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins'; import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import CannedResponse from './CannedResponse.vue'; import CannedResponse from './CannedResponse.vue';
@@ -180,10 +181,8 @@ import {
import WhatsappTemplates from './WhatsappTemplates/Modal.vue'; import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper'; import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin'; import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { trimContent, debounce } from '@chatwoot/utils'; import { trimContent, debounce } from '@chatwoot/utils';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events'; import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import rtlMixin from 'shared/mixins/rtlMixin'; import rtlMixin from 'shared/mixins/rtlMixin';
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin'; import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
@@ -218,7 +217,6 @@ export default {
}, },
mixins: [ mixins: [
inboxMixin, inboxMixin,
uiSettingsMixin,
messageFormatterMixin, messageFormatterMixin,
rtlMixin, rtlMixin,
fileUploadMixin, fileUploadMixin,
@@ -230,6 +228,21 @@ export default {
default: false, default: false,
}, },
}, },
setup() {
const {
uiSettings,
updateUISettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
} = useUISettings();
return {
uiSettings,
updateUISettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
};
},
data() { data() {
return { return {
message: '', message: '',
@@ -405,7 +418,7 @@ export default {
if (this.isPrivate) { if (this.isPrivate) {
sendMessageText = this.$t('CONVERSATION.REPLYBOX.CREATE'); sendMessageText = this.$t('CONVERSATION.REPLYBOX.CREATE');
} }
const keyLabel = isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter') const keyLabel = this.isEditorHotKeyEnabled('cmd_enter')
? '(⌘ + ↵)' ? '(⌘ + ↵)'
: '(↵)'; : '(↵)';
return `${sendMessageText} ${keyLabel}`; return `${sendMessageText} ${keyLabel}`;
@@ -478,7 +491,7 @@ export default {
return !!this.signatureToApply; return !!this.signatureToApply;
}, },
sendWithSignature() { sendWithSignature() {
return this.fetchSignatureFlagFromUiSettings(this.channelType); return this.fetchSignatureFlagFromUISettings(this.channelType);
}, },
editorMessageKey() { editorMessageKey() {
const { editor_message_key: isEnabled } = this.uiSettings; const { editor_message_key: isEnabled } = this.uiSettings;
@@ -747,7 +760,7 @@ export default {
!this.showCannedMenu && !this.showCannedMenu &&
!this.showVariablesMenu && !this.showVariablesMenu &&
this.isFocused && this.isFocused &&
isEditorHotKeyEnabled(this.uiSettings, selectedKey) this.isEditorHotKeyEnabled(selectedKey)
); );
}, },
onPaste(e) { onPaste(e) {

View File

@@ -0,0 +1,138 @@
import { ref } from 'vue';
import {
useUISettings,
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
} from 'dashboard/composables/useUISettings';
// Mocking the store composables
const mockDispatch = vi.fn();
const getUISettingsMock = ref({
is_ct_labels_open: true,
conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
editor_message_key: 'enter',
});
vi.mock('dashboard/composables/store', () => ({
useStoreGetters: () => ({
getUISettings: getUISettingsMock,
}),
useStore: () => ({
dispatch: mockDispatch,
}),
}));
describe('useUISettings', () => {
beforeEach(() => {
mockDispatch.mockClear();
});
it('returns uiSettings', () => {
const { uiSettings } = useUISettings();
expect(uiSettings.value).toEqual({
is_ct_labels_open: true,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
editor_message_key: 'enter',
});
});
it('updates UI settings correctly', () => {
const { updateUISettings } = useUISettings();
updateUISettings({ enter_to_send_enabled: true });
expect(mockDispatch).toHaveBeenCalledWith('updateUISettings', {
uiSettings: {
enter_to_send_enabled: true,
is_ct_labels_open: true,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
editor_message_key: 'enter',
},
});
});
it('toggles sidebar UI state correctly', () => {
const { toggleSidebarUIState } = useUISettings();
toggleSidebarUIState('is_ct_labels_open');
expect(mockDispatch).toHaveBeenCalledWith('updateUISettings', {
uiSettings: {
is_ct_labels_open: false,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
editor_message_key: 'enter',
},
});
});
it('returns correct conversation sidebar items order', () => {
const { conversationSidebarItemsOrder } = useUISettings();
expect(conversationSidebarItemsOrder.value).toEqual(
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER
);
});
it('returns correct contact sidebar items order', () => {
const { contactSidebarItemsOrder } = useUISettings();
expect(contactSidebarItemsOrder.value).toEqual(
DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER
);
});
it('returns correct value for isContactSidebarItemOpen', () => {
const { isContactSidebarItemOpen } = useUISettings();
expect(isContactSidebarItemOpen('is_ct_labels_open')).toBe(true);
expect(isContactSidebarItemOpen('non_existent_key')).toBe(false);
});
it('sets signature flag for inbox correctly', () => {
const { setSignatureFlagForInbox } = useUISettings();
setSignatureFlagForInbox('email', true);
expect(mockDispatch).toHaveBeenCalledWith('updateUISettings', {
uiSettings: {
is_ct_labels_open: true,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
email_signature_enabled: true,
editor_message_key: 'enter',
},
});
});
it('fetches signature flag from UI settings correctly', () => {
const { fetchSignatureFlagFromUISettings } = useUISettings();
expect(fetchSignatureFlagFromUISettings('email')).toBe(undefined);
});
it('returns correct value for isEditorHotKeyEnabled when editor_message_key is configured', () => {
getUISettingsMock.value.enter_to_send_enabled = false;
const { isEditorHotKeyEnabled } = useUISettings();
expect(isEditorHotKeyEnabled('enter')).toBe(true);
expect(isEditorHotKeyEnabled('cmd_enter')).toBe(false);
});
it('returns correct value for isEditorHotKeyEnabled when editor_message_key is not configured', () => {
getUISettingsMock.value.editor_message_key = undefined;
const { isEditorHotKeyEnabled } = useUISettings();
expect(isEditorHotKeyEnabled('enter')).toBe(false);
expect(isEditorHotKeyEnabled('cmd_enter')).toBe(true);
});
it('handles non-existent keys', () => {
const {
isContactSidebarItemOpen,
fetchSignatureFlagFromUISettings,
isEditorHotKeyEnabled,
} = useUISettings();
expect(isContactSidebarItemOpen('non_existent_key')).toBe(false);
expect(fetchSignatureFlagFromUISettings('non_existent_key')).toBe(
undefined
);
expect(isEditorHotKeyEnabled('non_existent_key')).toBe(false);
});
});

View File

@@ -0,0 +1,149 @@
import { computed } from 'vue';
import { useStore, useStoreGetters } from 'dashboard/composables/store';
export const DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER = Object.freeze([
{ name: 'conversation_actions' },
{ name: 'macros' },
{ name: 'conversation_info' },
{ name: 'contact_attributes' },
{ name: 'previous_conversation' },
{ name: 'conversation_participants' },
]);
export const DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER = Object.freeze([
{ name: 'contact_attributes' },
{ name: 'contact_labels' },
{ name: 'previous_conversation' },
]);
/**
* Slugifies the channel name.
* Replaces spaces, hyphens, and double colons with underscores.
* @param {string} name - The channel name to slugify.
* @returns {string} The slugified channel name.
*/
const slugifyChannel = name =>
name?.toLowerCase().replace(' ', '_').replace('-', '_').replace('::', '_');
/**
* Computes the order of items in the conversation sidebar, using defaults if not present.
* @param {Object} uiSettings - Reactive UI settings object.
* @returns {Array} Ordered list of sidebar items.
*/
const useConversationSidebarItemsOrder = uiSettings => {
return computed(() => {
const { conversation_sidebar_items_order: itemsOrder } = uiSettings.value;
// If the sidebar order is not set, use the default order.
if (!itemsOrder) {
return DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER;
}
// Create a copy of itemsOrder to avoid mutating the original store object.
const itemsOrderCopy = [...itemsOrder];
// If the sidebar order doesn't have the new elements, then add them to the list.
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER.forEach(item => {
if (!itemsOrderCopy.find(i => i.name === item.name)) {
itemsOrderCopy.push(item);
}
});
return itemsOrderCopy;
});
};
/**
* Computes the order of items in the contact sidebar,using defaults if not present.
* @param {Object} uiSettings - Reactive UI settings object.
* @returns {Array} Ordered list of sidebar items.
*/
const useContactSidebarItemsOrder = uiSettings => {
return computed(() => {
const { contact_sidebar_items_order: itemsOrder } = uiSettings.value;
return itemsOrder || DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER;
});
};
/**
* Toggles the open state of a sidebar item.
* @param {string} key - The key of the sidebar item to toggle.
* @param {Object} uiSettings - Reactive UI settings object.
* @param {Function} updateUISettings - Function to update UI settings.
*/
const toggleSidebarUIState = (key, uiSettings, updateUISettings) => {
updateUISettings({ [key]: !uiSettings.value[key] });
};
/**
* Sets the signature flag for a specific channel type in the inbox settings.
* @param {string} channelType - The type of the channel.
* @param {boolean} value - The value to set for the signature enabled flag.
* @param {Function} updateUISettings - Function to update UI settings.
*/
const setSignatureFlagForInbox = (channelType, value, updateUISettings) => {
if (!channelType) return;
const slugifiedChannel = slugifyChannel(channelType);
updateUISettings({ [`${slugifiedChannel}_signature_enabled`]: value });
};
/**
* Fetches the signature flag for a specific channel type from UI settings.
* @param {string} channelType - The type of the channel.
* @param {Object} uiSettings - Reactive UI settings object.
* @returns {boolean} The value of the signature enabled flag.
*/
const fetchSignatureFlagFromUISettings = (channelType, uiSettings) => {
if (!channelType) return false;
const slugifiedChannel = slugifyChannel(channelType);
return uiSettings.value[`${slugifiedChannel}_signature_enabled`];
};
/**
* Checks if a specific editor hotkey is enabled.
* @param {string} key - The key to check.
* @param {Object} uiSettings - Reactive UI settings object.
* @returns {boolean} True if the hotkey is enabled, otherwise false.
*/
const isEditorHotKeyEnabled = (key, uiSettings) => {
const {
editor_message_key: editorMessageKey,
enter_to_send_enabled: enterToSendEnabled,
} = uiSettings.value || {};
if (!editorMessageKey) {
return key === (enterToSendEnabled ? 'enter' : 'cmd_enter');
}
return editorMessageKey === key;
};
/**
* Main composable function for managing UI settings.
* @returns {Object} An object containing reactive properties and methods for UI settings management.
*/
export function useUISettings() {
const getters = useStoreGetters();
const store = useStore();
const uiSettings = computed(() => getters.getUISettings.value);
const updateUISettings = (settings = {}) => {
store.dispatch('updateUISettings', {
uiSettings: {
...uiSettings.value,
...settings,
},
});
};
return {
uiSettings,
updateUISettings,
conversationSidebarItemsOrder: useConversationSidebarItemsOrder(uiSettings),
contactSidebarItemsOrder: useContactSidebarItemsOrder(uiSettings),
isContactSidebarItemOpen: key => !!uiSettings.value[key],
toggleSidebarUIState: key =>
toggleSidebarUIState(key, uiSettings, updateUISettings),
setSignatureFlagForInbox: (channelType, value) =>
setSignatureFlagForInbox(channelType, value, updateUISettings),
fetchSignatureFlagFromUISettings: channelType =>
fetchSignatureFlagFromUISettings(channelType, uiSettings),
isEditorHotKeyEnabled: key => isEditorHotKeyEnabled(key, uiSettings),
};
}

View File

@@ -1,169 +0,0 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import uiSettingsMixin, {
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
isEditorHotKeyEnabled,
} from '../uiSettings';
import Vuex from 'vuex';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('uiSettingsMixin', () => {
let getters;
let actions;
let store;
beforeEach(() => {
actions = { updateUISettings: vi.fn(), toggleSidebarUIState: vi.fn() };
getters = {
getUISettings: () => ({
enter_to_send_enabled: false,
is_ct_labels_open: true,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
}),
};
store = new Vuex.Store({ actions, getters });
});
it('returns uiSettings', () => {
const Component = {
render() {},
title: 'TestComponent',
mixins: [uiSettingsMixin],
};
const wrapper = shallowMount(Component, { store, localVue });
expect(wrapper.vm.uiSettings).toEqual({
enter_to_send_enabled: false,
is_ct_labels_open: true,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
});
});
describe('#updateUISettings', () => {
it('dispatches store actions correctly', () => {
const Component = {
render() {},
title: 'TestComponent',
mixins: [uiSettingsMixin],
};
const wrapper = shallowMount(Component, { store, localVue });
wrapper.vm.updateUISettings({ enter_to_send_enabled: true });
expect(actions.updateUISettings).toHaveBeenCalledWith(
expect.anything(),
{
uiSettings: {
enter_to_send_enabled: true,
is_ct_labels_open: true,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
},
},
undefined
);
});
});
describe('#toggleSidebarUIState', () => {
it('dispatches store actions correctly', () => {
const Component = {
render() {},
title: 'TestComponent',
mixins: [uiSettingsMixin],
};
const wrapper = shallowMount(Component, { store, localVue });
wrapper.vm.toggleSidebarUIState('is_ct_labels_open');
expect(actions.updateUISettings).toHaveBeenCalledWith(
expect.anything(),
{
uiSettings: {
enter_to_send_enabled: false,
is_ct_labels_open: false,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
},
},
undefined
);
});
});
describe('#isContactSidebarItemOpen', () => {
it('returns correct values', () => {
const Component = {
render() {},
title: 'TestComponent',
mixins: [uiSettingsMixin],
};
const wrapper = shallowMount(Component, { store, localVue });
expect(wrapper.vm.isContactSidebarItemOpen('is_ct_labels_open')).toEqual(
true
);
expect(
wrapper.vm.isContactSidebarItemOpen('is_ct_prev_conv_open')
).toEqual(false);
});
});
describe('#conversationSidebarItemsOrder', () => {
it('returns correct values', () => {
const Component = {
render() {},
title: 'TestComponent',
mixins: [uiSettingsMixin],
};
const wrapper = shallowMount(Component, { store, localVue });
expect(wrapper.vm.conversationSidebarItemsOrder).toEqual([
{ name: 'conversation_actions' },
{ name: 'macros' },
{ name: 'conversation_info' },
{ name: 'contact_attributes' },
{ name: 'previous_conversation' },
{ name: 'conversation_participants' },
]);
});
});
describe('#contactSidebarItemsOrder', () => {
it('returns correct values', () => {
const Component = {
render() {},
title: 'TestComponent',
mixins: [uiSettingsMixin],
};
const wrapper = shallowMount(Component, { store, localVue });
expect(wrapper.vm.contactSidebarItemsOrder).toEqual([
{ name: 'contact_attributes' },
{ name: 'contact_labels' },
{ name: 'previous_conversation' },
]);
});
});
});
describe('isEditorHotKeyEnabled', () => {
it('returns true if hot key is not configured and enter to send flag is true', () => {
expect(
isEditorHotKeyEnabled({ enter_to_send_enabled: true }, 'enter')
).toEqual(true);
expect(
isEditorHotKeyEnabled({ enter_to_send_enabled: true }, 'cmd_enter')
).toEqual(false);
expect(isEditorHotKeyEnabled({}, 'cmd_enter')).toEqual(true);
expect(isEditorHotKeyEnabled({}, 'enter')).toEqual(false);
});
it('returns correct value if hot key is configured', () => {
expect(
isEditorHotKeyEnabled({ editor_message_key: 'enter' }, 'enter')
).toEqual(true);
expect(
isEditorHotKeyEnabled({ editor_message_key: 'cmd_enter' }, 'enter')
).toEqual(false);
});
});

View File

@@ -1,87 +0,0 @@
import { mapGetters } from 'vuex';
export const DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER = [
{ name: 'conversation_actions' },
{ name: 'macros' },
{ name: 'conversation_info' },
{ name: 'contact_attributes' },
{ name: 'previous_conversation' },
{ name: 'conversation_participants' },
];
export const DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER = [
{ name: 'contact_attributes' },
{ name: 'contact_labels' },
{ name: 'previous_conversation' },
];
const slugifyChannel = name =>
name?.toLowerCase().replace(' ', '_').replace('-', '_').replace('::', '_');
export const isEditorHotKeyEnabled = (uiSettings, key) => {
const {
editor_message_key: editorMessageKey,
enter_to_send_enabled: enterToSendEnabled,
} = uiSettings || {};
if (!editorMessageKey) {
if (enterToSendEnabled) {
return key === 'enter';
}
return key === 'cmd_enter';
}
return editorMessageKey === key;
};
export default {
computed: {
...mapGetters({ uiSettings: 'getUISettings' }),
conversationSidebarItemsOrder() {
const { conversation_sidebar_items_order: itemsOrder } = this.uiSettings;
// If the sidebar order is not set, use the default order.
if (!itemsOrder) {
return DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER;
}
// If the sidebar order doesn't have the new elements, then add them to the list.
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER.forEach(item => {
if (!itemsOrder.find(i => i.name === item.name)) {
itemsOrder.push(item);
}
});
return itemsOrder;
},
contactSidebarItemsOrder() {
const { contact_sidebar_items_order: itemsOrder } = this.uiSettings;
return itemsOrder || DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER;
},
},
methods: {
updateUISettings(uiSettings = {}) {
this.$store.dispatch('updateUISettings', {
uiSettings: {
...this.uiSettings,
...uiSettings,
},
});
},
isContactSidebarItemOpen(key) {
const { [key]: isOpen } = this.uiSettings;
return !!isOpen;
},
toggleSidebarUIState(key) {
this.updateUISettings({ [key]: !this.isContactSidebarItemOpen(key) });
},
setSignatureFlagForInbox(channelType, value) {
if (!channelType) return;
channelType = slugifyChannel(channelType);
this.updateUISettings({
[`${channelType}_signature_enabled`]: value,
});
},
fetchSignatureFlagFromUiSettings(channelType) {
if (!channelType) return false;
channelType = slugifyChannel(channelType);
return this.uiSettings[`${channelType}_signature_enabled`];
},
},
};

View File

@@ -1,6 +1,6 @@
<template> <template>
<div <div
class="app-wrapper h-full flex-grow-0 min-h-0 w-full max-w-full ml-auto mr-auto flex flex-wrap dark:text-slate-300" class="flex flex-wrap flex-grow-0 w-full h-full max-w-full min-h-0 ml-auto mr-auto app-wrapper dark:text-slate-300"
> >
<sidebar <sidebar
:route="currentRoute" :route="currentRoute"
@@ -11,7 +11,7 @@
@close-key-shortcut-modal="closeKeyShortcutModal" @close-key-shortcut-modal="closeKeyShortcutModal"
@show-add-label-popup="showAddLabelPopup" @show-add-label-popup="showAddLabelPopup"
/> />
<section class="flex h-full min-h-0 overflow-hidden flex-1 px-0"> <section class="flex flex-1 h-full min-h-0 px-0 overflow-hidden">
<router-view /> <router-view />
<command-bar /> <command-bar />
<account-selector <account-selector
@@ -47,7 +47,7 @@ import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAc
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue'; import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue';
import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.vue'; import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.vue';
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue'; import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import { useUISettings } from 'dashboard/composables/useUISettings';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
const CommandBar = () => import('./commands/commandbar.vue'); const CommandBar = () => import('./commands/commandbar.vue');
@@ -61,7 +61,14 @@ export default {
AddLabelModal, AddLabelModal,
NotificationPanel, NotificationPanel,
}, },
mixins: [uiSettingsMixin], setup() {
const { uiSettings, updateUISettings } = useUISettings();
return {
uiSettings,
updateUISettings,
};
},
data() { data() {
return { return {
showAccountModal: false, showAccountModal: false,

View File

@@ -86,7 +86,7 @@ import ContactInfo from 'dashboard/routes/dashboard/conversation/contact/Contact
import ContactLabel from 'dashboard/routes/dashboard/contacts/components/ContactLabels.vue'; import ContactLabel from 'dashboard/routes/dashboard/contacts/components/ContactLabels.vue';
import CustomAttributes from 'dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue'; import CustomAttributes from 'dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue';
import draggable from 'vuedraggable'; import draggable from 'vuedraggable';
import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import { useUISettings } from 'dashboard/composables/useUISettings';
export default { export default {
components: { components: {
@@ -97,7 +97,6 @@ export default {
CustomAttributes, CustomAttributes,
draggable, draggable,
}, },
mixins: [uiSettingsMixin],
props: { props: {
contact: { contact: {
type: Object, type: Object,
@@ -116,6 +115,21 @@ export default {
default: true, default: true,
}, },
}, },
setup() {
const {
updateUISettings,
isContactSidebarItemOpen,
contactSidebarItemsOrder,
toggleSidebarUIState,
} = useUISettings();
return {
updateUISettings,
isContactSidebarItemOpen,
contactSidebarItemsOrder,
toggleSidebarUIState,
};
},
data() { data() {
return { return {
dragEnabled: true, dragEnabled: true,

View File

@@ -133,6 +133,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useUISettings } from 'dashboard/composables/useUISettings';
import AccordionItem from 'dashboard/components/Accordion/AccordionItem.vue'; import AccordionItem from 'dashboard/components/Accordion/AccordionItem.vue';
import ContactConversations from './ContactConversations.vue'; import ContactConversations from './ContactConversations.vue';
import ConversationAction from './ConversationAction.vue'; import ConversationAction from './ConversationAction.vue';
@@ -142,7 +143,6 @@ import ContactInfo from './contact/ContactInfo.vue';
import ConversationInfo from './ConversationInfo.vue'; import ConversationInfo from './ConversationInfo.vue';
import CustomAttributes from './customAttributes/CustomAttributes.vue'; import CustomAttributes from './customAttributes/CustomAttributes.vue';
import draggable from 'vuedraggable'; import draggable from 'vuedraggable';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import MacrosList from './Macros/List.vue'; import MacrosList from './Macros/List.vue';
export default { export default {
components: { components: {
@@ -156,7 +156,6 @@ export default {
draggable, draggable,
MacrosList, MacrosList,
}, },
mixins: [uiSettingsMixin],
props: { props: {
conversationId: { conversationId: {
type: [Number, String], type: [Number, String],
@@ -171,6 +170,21 @@ export default {
default: () => {}, default: () => {},
}, },
}, },
setup() {
const {
updateUISettings,
isContactSidebarItemOpen,
conversationSidebarItemsOrder,
toggleSidebarUIState,
} = useUISettings();
return {
updateUISettings,
isContactSidebarItemOpen,
conversationSidebarItemsOrder,
toggleSidebarUIState,
};
},
data() { data() {
return { return {
dragEnabled: true, dragEnabled: true,

View File

@@ -37,12 +37,12 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { getUnixTime } from 'date-fns'; import { getUnixTime } from 'date-fns';
import ChatList from '../../../components/ChatList.vue'; import ChatList from '../../../components/ChatList.vue';
import ConversationBox from '../../../components/widgets/conversation/ConversationBox.vue'; import ConversationBox from '../../../components/widgets/conversation/ConversationBox.vue';
import PopOverSearch from './search/PopOverSearch.vue'; import PopOverSearch from './search/PopOverSearch.vue';
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue'; import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import { CMD_SNOOZE_CONVERSATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents'; import { CMD_SNOOZE_CONVERSATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
@@ -55,7 +55,6 @@ export default {
PopOverSearch, PopOverSearch,
CustomSnoozeModal, CustomSnoozeModal,
}, },
mixins: [uiSettingsMixin],
props: { props: {
inboxId: { inboxId: {
type: [String, Number], type: [String, Number],
@@ -82,6 +81,14 @@ export default {
default: 0, default: 0,
}, },
}, },
setup() {
const { uiSettings, updateUISettings } = useUISettings();
return {
uiSettings,
updateUISettings,
};
},
data() { data() {
return { return {
showSearchModal: false, showSearchModal: false,

View File

@@ -240,6 +240,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue'; import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
import ReplyEmailHead from 'dashboard/components/widgets/conversation/ReplyEmailHead.vue'; import ReplyEmailHead from 'dashboard/components/widgets/conversation/ReplyEmailHead.vue';
@@ -260,7 +261,6 @@ import {
appendSignature, appendSignature,
removeSignature, removeSignature,
} from 'dashboard/helper/editorHelper'; } from 'dashboard/helper/editorHelper';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
export default { export default {
components: { components: {
@@ -274,7 +274,7 @@ export default {
AttachmentPreview, AttachmentPreview,
MessageSignatureMissingAlert, MessageSignatureMissingAlert,
}, },
mixins: [uiSettingsMixin, inboxMixin, fileUploadMixin], mixins: [inboxMixin, fileUploadMixin],
props: { props: {
contact: { contact: {
type: Object, type: Object,
@@ -289,6 +289,15 @@ export default {
default: '', default: '',
}, },
}, },
setup() {
const { fetchSignatureFlagFromUISettings, setSignatureFlagForInbox } =
useUISettings();
return {
fetchSignatureFlagFromUISettings,
setSignatureFlagForInbox,
};
},
data() { data() {
return { return {
name: '', name: '',
@@ -323,7 +332,7 @@ export default {
messageSignature: 'getMessageSignature', messageSignature: 'getMessageSignature',
}), }),
sendWithSignature() { sendWithSignature() {
return this.fetchSignatureFlagFromUiSettings(this.channelType); return this.fetchSignatureFlagFromUISettings(this.channelType);
}, },
signatureToApply() { signatureToApply() {
return this.messageSignature; return this.messageSignature;

View File

@@ -41,17 +41,17 @@
</template> </template>
<script> <script>
import CustomAttribute from 'dashboard/components/CustomAttribute.vue';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import attributeMixin from 'dashboard/mixins/attributeMixin'; import { useUISettings } from 'dashboard/composables/useUISettings';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { copyTextToClipboard } from 'shared/helpers/clipboard'; import { copyTextToClipboard } from 'shared/helpers/clipboard';
import CustomAttribute from 'dashboard/components/CustomAttribute.vue';
import attributeMixin from 'dashboard/mixins/attributeMixin';
export default { export default {
components: { components: {
CustomAttribute, CustomAttribute,
}, },
mixins: [attributeMixin, uiSettingsMixin], mixins: [attributeMixin],
props: { props: {
attributeType: { attributeType: {
type: String, type: String,
@@ -71,6 +71,14 @@ export default {
default: '', default: '',
}, },
}, },
setup() {
const { uiSettings, updateUISettings } = useUISettings();
return {
uiSettings,
updateUISettings,
};
},
data() { data() {
return { return {
showAllAttributes: false, showAllAttributes: false,

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="app-wrapper flex h-full flex-grow-0 min-h-0 w-full"> <div class="flex flex-grow-0 w-full h-full min-h-0 app-wrapper">
<sidebar <sidebar
:route="currentRoute" :route="currentRoute"
@toggle-account-modal="toggleAccountModal" @toggle-account-modal="toggleAccountModal"
@@ -20,7 +20,7 @@
/> />
<section <section
v-if="isHelpCenterEnabled" v-if="isHelpCenterEnabled"
class="flex h-full min-h-0 overflow-hidden flex-1 px-0 bg-white dark:bg-slate-900" class="flex flex-1 h-full min-h-0 px-0 overflow-hidden bg-white dark:bg-slate-900"
> >
<router-view @reload-locale="fetchPortalAndItsCategories" /> <router-view @reload-locale="fetchPortalAndItsCategories" />
<command-bar /> <command-bar />
@@ -68,7 +68,7 @@ import HelpCenterSidebar from '../components/Sidebar/Sidebar.vue';
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal.vue'; import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal.vue';
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue'; import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue';
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue'; import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import { useUISettings } from 'dashboard/composables/useUISettings';
import portalMixin from '../mixins/portalMixin'; import portalMixin from '../mixins/portalMixin';
import AddCategory from '../pages/categories/AddCategory.vue'; import AddCategory from '../pages/categories/AddCategory.vue';
import { FEATURE_FLAGS } from 'dashboard/featureFlags'; import { FEATURE_FLAGS } from 'dashboard/featureFlags';
@@ -87,7 +87,15 @@ export default {
UpgradePage, UpgradePage,
WootKeyShortcutModal, WootKeyShortcutModal,
}, },
mixins: [portalMixin, uiSettingsMixin], mixins: [portalMixin],
setup() {
const { uiSettings, updateUISettings } = useUISettings();
return {
uiSettings,
updateUISettings,
};
},
data() { data() {
return { return {
isOnDesktop: true, isOnDesktop: true,

View File

@@ -191,9 +191,9 @@
<script> <script>
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import LocaleItemTable from './PortalListItemTable.vue'; import LocaleItemTable from './PortalListItemTable.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { PORTALS_EVENTS } from '../../../../helper/AnalyticsHelper/events'; import { PORTALS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
export default { export default {
@@ -201,7 +201,6 @@ export default {
thumbnail, thumbnail,
LocaleItemTable, LocaleItemTable,
}, },
mixins: [uiSettingsMixin],
props: { props: {
portal: { portal: {
type: Object, type: Object,
@@ -213,6 +212,13 @@ export default {
values: ['archived', 'draft', 'published'], values: ['archived', 'draft', 'published'],
}, },
}, },
setup() {
const { updateUISettings } = useUISettings();
return {
updateUISettings,
};
},
data() { data() {
return { return {
showDeleteConfirmationPopup: false, showDeleteConfirmationPopup: false,

View File

@@ -1,17 +1,23 @@
<template> <template>
<div <div
class="text-slate-600 dark:text-slate-200 flex items-center justify-center w-full" class="flex items-center justify-center w-full text-slate-600 dark:text-slate-200"
> >
Loading... Loading...
</div> </div>
</template> </template>
<script> <script>
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useUISettings } from 'dashboard/composables/useUISettings';
export default { export default {
mixins: [uiSettingsMixin], setup() {
const { uiSettings } = useUISettings();
return {
uiSettings,
};
},
computed: { computed: {
...mapGetters({ portals: 'portals/allPortals' }), ...mapGetters({ portals: 'portals/allPortals' }),
}, },

View File

@@ -47,13 +47,13 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
import InboxCard from './components/InboxCard.vue'; import InboxCard from './components/InboxCard.vue';
import InboxListHeader from './components/InboxListHeader.vue'; import InboxListHeader from './components/InboxListHeader.vue';
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import IntersectionObserver from 'dashboard/components/IntersectionObserver.vue'; import IntersectionObserver from 'dashboard/components/IntersectionObserver.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
export default { export default {
components: { components: {
@@ -61,7 +61,13 @@ export default {
InboxListHeader, InboxListHeader,
IntersectionObserver, IntersectionObserver,
}, },
mixins: [uiSettingsMixin], setup() {
const { uiSettings } = useUISettings();
return {
uiSettings,
};
},
data() { data() {
return { return {
infiniteLoaderOptions: { infiniteLoaderOptions: {

View File

@@ -5,7 +5,7 @@
:empty-state-message="$t('INBOX.LIST.NO_MESSAGES_AVAILABLE')" :empty-state-message="$t('INBOX.LIST.NO_MESSAGES_AVAILABLE')"
/> />
</div> </div>
<div v-else class="flex flex-col h-full w-full"> <div v-else class="flex flex-col w-full h-full">
<inbox-item-header <inbox-item-header
class="flex-1" class="flex-1"
:total-length="totalNotificationCount" :total-length="totalNotificationCount"
@@ -18,7 +18,7 @@
v-if="isConversationLoading" v-if="isConversationLoading"
class="flex items-center h-[calc(100%-56px)] justify-center bg-slate-25 dark:bg-slate-800" class="flex items-center h-[calc(100%-56px)] justify-center bg-slate-25 dark:bg-slate-800"
> >
<span class="spinner my-4" /> <span class="my-4 spinner" />
</div> </div>
<conversation-box <conversation-box
v-else v-else
@@ -38,7 +38,7 @@ import { mapGetters } from 'vuex';
import InboxItemHeader from './components/InboxItemHeader.vue'; import InboxItemHeader from './components/InboxItemHeader.vue';
import ConversationBox from 'dashboard/components/widgets/conversation/ConversationBox.vue'; import ConversationBox from 'dashboard/components/widgets/conversation/ConversationBox.vue';
import InboxEmptyState from './InboxEmptyState.vue'; import InboxEmptyState from './InboxEmptyState.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import { useUISettings } from 'dashboard/composables/useUISettings';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
@@ -48,7 +48,14 @@ export default {
InboxEmptyState, InboxEmptyState,
ConversationBox, ConversationBox,
}, },
mixins: [uiSettingsMixin], setup() {
const { uiSettings, updateUISettings } = useUISettings();
return {
uiSettings,
updateUISettings,
};
},
data() { data() {
return { return {
isConversationLoading: false, isConversationLoading: false,

View File

@@ -2,7 +2,7 @@
<div <div
class="flex flex-col bg-white z-50 dark:bg-slate-900 w-[170px] border shadow-md border-slate-100 dark:border-slate-700/50 rounded-xl divide-y divide-slate-100 dark:divide-slate-700/50" class="flex flex-col bg-white z-50 dark:bg-slate-900 w-[170px] border shadow-md border-slate-100 dark:border-slate-700/50 rounded-xl divide-y divide-slate-100 dark:divide-slate-700/50"
> >
<div class="flex items-center justify-between h-11 p-3 rounded-t-lg"> <div class="flex items-center justify-between p-3 rounded-t-lg h-11">
<div class="flex gap-1.5"> <div class="flex gap-1.5">
<fluent-icon <fluent-icon
icon="arrow-sort" icon="arrow-sort"
@@ -10,7 +10,7 @@
size="16" size="16"
class="text-slate-700 dark:text-slate-100" class="text-slate-700 dark:text-slate-100"
/> />
<span class="font-medium text-xs text-slate-800 dark:text-slate-100"> <span class="text-xs font-medium text-slate-800 dark:text-slate-100">
{{ $t('INBOX.DISPLAY_MENU.SORT') }} {{ $t('INBOX.DISPLAY_MENU.SORT') }}
</span> </span>
</div> </div>
@@ -64,7 +64,7 @@
</div> </div>
<div> <div>
<span <span
class="font-medium text-xs py-4 px-3 text-slate-400 dark:text-slate-400" class="px-3 py-4 text-xs font-medium text-slate-400 dark:text-slate-400"
> >
{{ $t('INBOX.DISPLAY_MENU.DISPLAY') }} {{ $t('INBOX.DISPLAY_MENU.DISPLAY') }}
</span> </span>
@@ -98,10 +98,17 @@
<script> <script>
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import { useUISettings } from 'dashboard/composables/useUISettings';
export default { export default {
mixins: [uiSettingsMixin], setup() {
const { uiSettings, updateUISettings } = useUISettings();
return {
uiSettings,
updateUISettings,
};
},
data() { data() {
return { return {
showSortMenu: false, showSortMenu: false,

View File

@@ -87,7 +87,7 @@
</div> </div>
<div <div
class="p-4 border-slate-25 dark:border-slate-700 text-black-900 dark:text-slate-300 flex flex-row" class="flex flex-row p-4 border-slate-25 dark:border-slate-700 text-black-900 dark:text-slate-300"
> >
<div <div
class="flex-grow-0 flex-shrink-0 flex-[25%] min-w-0 py-4 pr-6 pl-0" class="flex-grow-0 flex-shrink-0 flex-[25%] min-w-0 py-4 pr-6 pl-0"
@@ -103,7 +103,7 @@
<woot-code :script="getAccountId" /> <woot-code :script="getAccountId" />
</div> </div>
</div> </div>
<div class="text-sm text-center p-4"> <div class="p-4 text-sm text-center">
<div>{{ `v${globalConfig.appVersion}` }}</div> <div>{{ `v${globalConfig.appVersion}` }}</div>
<div v-if="hasAnUpdateAvailable && globalConfig.displayManifest"> <div v-if="hasAnUpdateAvailable && globalConfig.displayManifest">
{{ {{
@@ -132,15 +132,22 @@
import { required, minValue, maxValue } from 'vuelidate/lib/validators'; import { required, minValue, maxValue } from 'vuelidate/lib/validators';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import configMixin from 'shared/mixins/configMixin'; import configMixin from 'shared/mixins/configMixin';
import accountMixin from '../../../../mixins/account'; import accountMixin from '../../../../mixins/account';
import { FEATURE_FLAGS } from '../../../../featureFlags'; import { FEATURE_FLAGS } from '../../../../featureFlags';
import semver from 'semver'; import semver from 'semver';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages'; import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
export default { export default {
mixins: [accountMixin, configMixin, uiSettingsMixin], mixins: [accountMixin, configMixin],
setup() {
const { updateUISettings } = useUISettings();
return {
updateUISettings,
};
},
data() { data() {
return { return {
id: '', id: '',

View File

@@ -32,7 +32,7 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import configMixin from 'shared/mixins/configMixin'; import configMixin from 'shared/mixins/configMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import { useUISettings } from 'dashboard/composables/useUISettings';
import AudioAlertTone from './AudioAlertTone.vue'; import AudioAlertTone from './AudioAlertTone.vue';
import AudioAlertEvent from './AudioAlertEvent.vue'; import AudioAlertEvent from './AudioAlertEvent.vue';
import AudioAlertCondition from './AudioAlertCondition.vue'; import AudioAlertCondition from './AudioAlertCondition.vue';
@@ -43,7 +43,15 @@ export default {
AudioAlertTone, AudioAlertTone,
AudioAlertCondition, AudioAlertCondition,
}, },
mixins: [configMixin, uiSettingsMixin], mixins: [configMixin],
setup() {
const { uiSettings, updateUISettings } = useUISettings();
return {
uiSettings,
updateUISettings,
};
},
data() { data() {
return { return {
audioAlert: '', audioAlert: '',
@@ -56,7 +64,6 @@ export default {
computed: { computed: {
...mapGetters({ ...mapGetters({
accountId: 'getCurrentAccountId', accountId: 'getCurrentAccountId',
uiSettings: 'getUISettings',
}), }),
}, },
watch: { watch: {

View File

@@ -47,7 +47,7 @@
:description="hotKey.description" :description="hotKey.description"
:light-image="hotKey.lightImage" :light-image="hotKey.lightImage"
:dark-image="hotKey.darkImage" :dark-image="hotKey.darkImage"
:active="isEditorHotKeyEnabled(uiSettings, hotKey.key)" :active="isEditorHotKeyEnabled(hotKey.key)"
@click="toggleHotKey(hotKey.key)" @click="toggleHotKey(hotKey.key)"
/> />
</button> </button>
@@ -81,15 +81,12 @@
</div> </div>
</template> </template>
<script> <script>
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import uiSettingsMixin, {
isEditorHotKeyEnabled,
} from 'dashboard/mixins/uiSettings';
import { useAlert } from 'dashboard/composables';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { clearCookiesOnLogout } from 'dashboard/store/utils/api.js'; import { clearCookiesOnLogout } from 'dashboard/store/utils/api.js';
import { copyTextToClipboard } from 'shared/helpers/clipboard'; import { copyTextToClipboard } from 'shared/helpers/clipboard';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import UserProfilePicture from './UserProfilePicture.vue'; import UserProfilePicture from './UserProfilePicture.vue';
import UserBasicDetails from './UserBasicDetails.vue'; import UserBasicDetails from './UserBasicDetails.vue';
import MessageSignature from './MessageSignature.vue'; import MessageSignature from './MessageSignature.vue';
@@ -112,7 +109,17 @@ export default {
AudioNotifications, AudioNotifications,
AccessToken, AccessToken,
}, },
mixins: [globalConfigMixin, uiSettingsMixin], mixins: [globalConfigMixin],
setup() {
const { uiSettings, updateUISettings, isEditorHotKeyEnabled } =
useUISettings();
return {
uiSettings,
updateUISettings,
isEditorHotKeyEnabled,
};
},
data() { data() {
return { return {
avatarFile: '', avatarFile: '',
@@ -168,7 +175,6 @@ export default {
this.displayName = this.currentUser.display_name; this.displayName = this.currentUser.display_name;
this.messageSignature = this.currentUser.message_signature; this.messageSignature = this.currentUser.message_signature;
}, },
isEditorHotKeyEnabled,
async dispatchUpdate(payload, successMessage, errorMessage) { async dispatchUpdate(payload, successMessage, errorMessage) {
let alertMessage = ''; let alertMessage = '';
try { try {

View File

@@ -134,7 +134,6 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import configMixin from 'shared/mixins/configMixin'; import configMixin from 'shared/mixins/configMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import TableHeaderCell from 'dashboard/components/widgets/TableHeaderCell.vue'; import TableHeaderCell from 'dashboard/components/widgets/TableHeaderCell.vue';
import CheckBox from 'v3/components/Form/CheckBox.vue'; import CheckBox from 'v3/components/Form/CheckBox.vue';
import { import {
@@ -152,7 +151,7 @@ export default {
FormSwitch, FormSwitch,
CheckBox, CheckBox,
}, },
mixins: [configMixin, uiSettingsMixin], mixins: [configMixin],
data() { data() {
return { return {
selectedEmailFlags: [], selectedEmailFlags: [],
@@ -167,7 +166,6 @@ export default {
accountId: 'getCurrentAccountId', accountId: 'getCurrentAccountId',
emailFlags: 'userNotificationSettings/getSelectedEmailFlags', emailFlags: 'userNotificationSettings/getSelectedEmailFlags',
pushFlags: 'userNotificationSettings/getSelectedPushFlags', pushFlags: 'userNotificationSettings/getSelectedPushFlags',
uiSettings: 'getUISettings',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount', isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
}), }),
hasPushAPISupport() { hasPushAPISupport() {
@@ -242,31 +240,6 @@ export default {
useAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_ERROR')); useAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_ERROR'));
} }
}, },
handleAudioInput(e) {
this.enableAudioAlerts = e.target.value;
this.updateUISettings({
enable_audio_alerts: this.enableAudioAlerts,
});
useAlert(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,
});
}
useAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
},
handleAudioToneChange(value) {
this.updateUISettings({ notification_tone: value });
useAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
},
handleInput(type, id) { handleInput(type, id) {
if (type === 'email') { if (type === 'email') {
this.handleEmailInput(id); this.handleEmailInput(id);

View File

@@ -42,7 +42,7 @@ export default {
type: Number, type: Number,
default: 2, default: 2,
}, },
// add this as a prop, so that we won't have to include uiSettingsMixin // add this as a prop, so that we won't have to add useUISettings
sendWithSignature: { sendWithSignature: {
type: Boolean, type: Boolean,
default: false, default: false,

View File

@@ -1,8 +1,15 @@
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages'; import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
import { useUISettings } from 'dashboard/composables/useUISettings';
export default { export default {
mixins: [uiSettingsMixin], setup() {
const { uiSettings, updateUISettings } = useUISettings();
return {
uiSettings,
updateUISettings,
};
},
computed: { computed: {
isRTLView() { isRTLView() {
const { rtl_view: isRTLView } = this.uiSettings; const { rtl_view: isRTLView } = this.uiSettings;

View File

@@ -1,16 +1,19 @@
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import rtlMixin from 'shared/mixins/rtlMixin'; import rtlMixin from 'shared/mixins/rtlMixin';
import { useUISettings } from 'dashboard/composables/useUISettings';
vi.mock('dashboard/composables/useUISettings');
describe('rtlMixin', () => { describe('rtlMixin', () => {
const createComponent = rtl_view => { const createComponent = rtl_view => {
useUISettings.mockReturnValue({
uiSettings: { rtl_view },
updateUISettings: vi.fn(),
});
return shallowMount({ return shallowMount({
render() {}, render() {},
mixins: [rtlMixin], mixins: [rtlMixin],
computed: {
uiSettings() {
return { rtl_view };
},
},
}); });
}; };