Merge branch 'develop' into fix/CW-3385
This commit is contained in:
@@ -10,7 +10,6 @@ import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
|
||||
import PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue';
|
||||
import vueActionCable from './helper/actionCable';
|
||||
import WootSnackbarBox from './components/SnackbarContainer.vue';
|
||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||
import { setColorTheme } from './helper/themeHelper';
|
||||
import { isOnOnboardingView } from 'v3/helpers/RouteHelper';
|
||||
import {
|
||||
@@ -32,9 +31,6 @@ export default {
|
||||
UpgradeBanner,
|
||||
PendingEmailVerificationBanner,
|
||||
},
|
||||
|
||||
mixins: [rtlMixin],
|
||||
|
||||
data() {
|
||||
return {
|
||||
showAddAccountModal: false,
|
||||
@@ -46,6 +42,7 @@ export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getAccount: 'accounts/getAccount',
|
||||
isRTL: 'accounts/isRTL',
|
||||
currentUser: 'getCurrentUser',
|
||||
authUIFlags: 'getAuthUIFlags',
|
||||
accountUIFlags: 'accounts/getUIFlags',
|
||||
@@ -102,7 +99,6 @@ export default {
|
||||
this.getAccount(this.currentAccountId);
|
||||
const { pubsub_token: pubsubToken } = this.currentUser || {};
|
||||
this.setLocale(locale);
|
||||
this.updateRTLDirectionView(locale);
|
||||
this.latestChatwootVersion = latestChatwootVersion;
|
||||
vueActionCable.init(pubsubToken);
|
||||
this.reconnectService = new ReconnectService(this.$store, router);
|
||||
@@ -124,8 +120,8 @@ export default {
|
||||
v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem"
|
||||
id="app"
|
||||
class="flex-grow-0 w-full h-full min-h-0 app-wrapper"
|
||||
:class="{ 'app-rtl--wrapper': isRTLView }"
|
||||
:dir="isRTLView ? 'rtl' : 'ltr'"
|
||||
:class="{ 'app-rtl--wrapper': isRTL }"
|
||||
:dir="isRTL ? 'rtl' : 'ltr'"
|
||||
>
|
||||
<UpdateBanner :latest-chatwoot-version="latestChatwootVersion" />
|
||||
<template v-if="currentAccountId">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import accountMixin from 'dashboard/mixins/account';
|
||||
|
||||
const EMPTY_SUBSCRIPTION_INFO = {
|
||||
status: null,
|
||||
@@ -11,10 +11,13 @@ const EMPTY_SUBSCRIPTION_INFO = {
|
||||
|
||||
export default {
|
||||
components: { Banner },
|
||||
mixins: [accountMixin],
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
|
||||
const { accountId } = useAccount();
|
||||
|
||||
return {
|
||||
accountId,
|
||||
isAdmin,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<script>
|
||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import accountMixin from 'dashboard/mixins/account';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
export default {
|
||||
components: { Banner },
|
||||
mixins: [accountMixin],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
<script>
|
||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import accountMixin from 'dashboard/mixins/account';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
|
||||
export default {
|
||||
components: { Banner },
|
||||
mixins: [accountMixin],
|
||||
setup() {
|
||||
const { accountId } = useAccount();
|
||||
return {
|
||||
accountId,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return { conversationMeta: {} };
|
||||
},
|
||||
|
||||
@@ -316,7 +316,11 @@ export default {
|
||||
{{ unreadCount > 9 ? '9+' : unreadCount }}
|
||||
</span>
|
||||
</div>
|
||||
<CardLabels :conversation-id="chat.id" class="mt-0.5 mx-2 mb-0">
|
||||
<CardLabels
|
||||
:conversation-id="chat.id"
|
||||
:conversation-labels="chat.labels"
|
||||
class="mt-0.5 mx-2 mb-0"
|
||||
>
|
||||
<template v-if="hasSlaPolicyId" #before>
|
||||
<SLACardLabel :chat="chat" class="ltr:mr-1 rtl:ml-1" />
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import accountMixin from 'dashboard/mixins/account';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import OnboardingView from '../OnboardingView.vue';
|
||||
import EmptyStateMessage from './EmptyStateMessage.vue';
|
||||
|
||||
@@ -10,7 +10,6 @@ export default {
|
||||
OnboardingView,
|
||||
EmptyStateMessage,
|
||||
},
|
||||
mixins: [accountMixin],
|
||||
props: {
|
||||
isOnExpandedLayout: {
|
||||
type: Boolean,
|
||||
@@ -19,8 +18,12 @@ export default {
|
||||
},
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
|
||||
const { accountScopedUrl } = useAccount();
|
||||
|
||||
return {
|
||||
isAdmin,
|
||||
accountScopedUrl,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -44,7 +47,7 @@ export default {
|
||||
return this.$t('CONVERSATION.404');
|
||||
},
|
||||
newInboxURL() {
|
||||
return this.addAccountScoping('settings/inboxes/new');
|
||||
return this.accountScopedUrl('settings/inboxes/new');
|
||||
},
|
||||
emptyClassName() {
|
||||
if (
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import accountMixin from '../../../mixins/account';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
|
||||
export default {
|
||||
mixins: [accountMixin],
|
||||
setup() {
|
||||
const { accountScopedUrl } = useAccount();
|
||||
return {
|
||||
accountScopedUrl,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ globalConfig: 'globalConfig/get' }),
|
||||
newInboxURL() {
|
||||
return this.addAccountScoping('settings/inboxes/new');
|
||||
return this.accountScopedUrl('settings/inboxes/new');
|
||||
},
|
||||
newAgentURL() {
|
||||
return this.addAccountScoping('settings/agents/list');
|
||||
return this.accountScopedUrl('settings/agents/list');
|
||||
},
|
||||
newLabelsURL() {
|
||||
return this.addAccountScoping('settings/labels/list');
|
||||
return this.accountScopedUrl('settings/labels/list');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -31,7 +31,6 @@ import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
||||
import { trimContent, debounce } from '@chatwoot/utils';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
|
||||
import {
|
||||
appendSignature,
|
||||
@@ -65,7 +64,6 @@ export default {
|
||||
mixins: [
|
||||
inboxMixin,
|
||||
messageFormatterMixin,
|
||||
rtlMixin,
|
||||
fileUploadMixin,
|
||||
keyboardEventListenerMixins,
|
||||
],
|
||||
@@ -121,6 +119,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isRTL: 'accounts/isRTL',
|
||||
currentChat: 'getSelectedChat',
|
||||
messageSignature: 'getMessageSignature',
|
||||
currentUser: 'getCurrentUser',
|
||||
@@ -307,7 +306,7 @@ export default {
|
||||
if (this.isOnExpandedLayout || this.popoutReplyBox) {
|
||||
return 'emoji-dialog--expanded';
|
||||
}
|
||||
if (this.isRTLView) {
|
||||
if (this.isRTL) {
|
||||
return 'emoji-dialog--rtl';
|
||||
}
|
||||
return '';
|
||||
|
||||
+18
-15
@@ -1,23 +1,26 @@
|
||||
<script>
|
||||
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
|
||||
import { computed } from 'vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
export default {
|
||||
mixins: [conversationLabelMixin],
|
||||
props: {
|
||||
// conversationId prop is used in /conversation/labelMixin,
|
||||
// remove this props when refactoring to composable if not needed
|
||||
// eslint-disable-next-line vue/no-unused-properties
|
||||
conversationId: {
|
||||
type: Number,
|
||||
conversationLabels: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
// conversationLabels prop is used in /conversation/labelMixin,
|
||||
// remove this props when refactoring to composable if not needed
|
||||
// eslint-disable-next-line vue/no-unused-properties
|
||||
conversationLabels: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const accountLabels = useMapGetter('labels/getLabels');
|
||||
|
||||
const activeLabels = computed(() => {
|
||||
return props.conversationLabels.map(label =>
|
||||
accountLabels.value.find(l => l.title === label)
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
activeLabels,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ref } from 'vue';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { useAccount } from '../useAccount';
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
|
||||
describe('useAccount', () => {
|
||||
beforeEach(() => {
|
||||
useStoreGetters.mockReturnValue({
|
||||
getCurrentAccountId: ref(123),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns accountId as a computed property', () => {
|
||||
const { accountId } = useAccount();
|
||||
expect(accountId.value).toBe(123);
|
||||
});
|
||||
|
||||
it('generates account-scoped URLs correctly', () => {
|
||||
const { accountScopedUrl } = useAccount();
|
||||
const result = accountScopedUrl('settings/inbox/new');
|
||||
expect(result).toBe('/app/accounts/123/settings/inbox/new');
|
||||
});
|
||||
|
||||
it('handles URLs with leading slash', () => {
|
||||
const { accountScopedUrl } = useAccount();
|
||||
const result = accountScopedUrl('users');
|
||||
expect(result).toBe('/app/accounts/123/users');
|
||||
});
|
||||
|
||||
it('handles empty URL', () => {
|
||||
const { accountScopedUrl } = useAccount();
|
||||
const result = accountScopedUrl('');
|
||||
expect(result).toBe('/app/accounts/123/');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useConversationLabels } from '../useConversationLabels';
|
||||
import { useStore, useStoreGetters } from 'dashboard/composables/store';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
|
||||
describe('useConversationLabels', () => {
|
||||
let store;
|
||||
let getters;
|
||||
|
||||
beforeEach(() => {
|
||||
store = {
|
||||
getters: {
|
||||
'conversationLabels/getConversationLabels': vi.fn(),
|
||||
},
|
||||
dispatch: vi.fn(),
|
||||
};
|
||||
getters = {
|
||||
getSelectedChat: { value: { id: 1 } },
|
||||
'labels/getLabels': {
|
||||
value: [
|
||||
{ id: 1, title: 'Label 1' },
|
||||
{ id: 2, title: 'Label 2' },
|
||||
{ id: 3, title: 'Label 3' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
useStore.mockReturnValue(store);
|
||||
useStoreGetters.mockReturnValue(getters);
|
||||
});
|
||||
|
||||
it('should return the correct computed properties', () => {
|
||||
store.getters['conversationLabels/getConversationLabels'].mockReturnValue([
|
||||
'Label 1',
|
||||
'Label 2',
|
||||
]);
|
||||
const { accountLabels, savedLabels, activeLabels, inactiveLabels } =
|
||||
useConversationLabels();
|
||||
|
||||
expect(accountLabels.value).toEqual(getters['labels/getLabels'].value);
|
||||
expect(savedLabels.value).toEqual(['Label 1', 'Label 2']);
|
||||
expect(activeLabels.value).toEqual([
|
||||
{ id: 1, title: 'Label 1' },
|
||||
{ id: 2, title: 'Label 2' },
|
||||
]);
|
||||
expect(inactiveLabels.value).toEqual([{ id: 3, title: 'Label 3' }]);
|
||||
});
|
||||
|
||||
it('should update labels correctly', async () => {
|
||||
const { onUpdateLabels } = useConversationLabels();
|
||||
await onUpdateLabels(['Label 1', 'Label 3']);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith('conversationLabels/update', {
|
||||
conversationId: 1,
|
||||
labels: ['Label 1', 'Label 3'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should add a label to the conversation', () => {
|
||||
store.getters['conversationLabels/getConversationLabels'].mockReturnValue([
|
||||
'Label 1',
|
||||
]);
|
||||
const { addLabelToConversation } = useConversationLabels();
|
||||
|
||||
addLabelToConversation({ title: 'Label 2' });
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith('conversationLabels/update', {
|
||||
conversationId: 1,
|
||||
labels: ['Label 1', 'Label 2'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove a label from the conversation', () => {
|
||||
store.getters['conversationLabels/getConversationLabels'].mockReturnValue([
|
||||
'Label 1',
|
||||
'Label 2',
|
||||
]);
|
||||
const { removeLabelFromConversation } = useConversationLabels();
|
||||
|
||||
removeLabelFromConversation('Label 2');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith('conversationLabels/update', {
|
||||
conversationId: 1,
|
||||
labels: ['Label 1'],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ export const useStoreGetters = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const mapGetter = key => {
|
||||
export const useMapGetter = key => {
|
||||
const store = useStore();
|
||||
return computed(() => store.getters[key]);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { computed } from 'vue';
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
|
||||
/**
|
||||
* Composable for account-related operations.
|
||||
* @returns {Object} An object containing account-related properties and methods.
|
||||
*/
|
||||
export function useAccount() {
|
||||
const getters = useStoreGetters();
|
||||
|
||||
/**
|
||||
* Computed property for the current account ID.
|
||||
* @type {import('vue').ComputedRef<number>}
|
||||
*/
|
||||
const accountId = computed(() => getters.getCurrentAccountId.value);
|
||||
|
||||
/**
|
||||
* Generates an account-scoped URL.
|
||||
* @param {string} url - The URL to be scoped to the account.
|
||||
* @returns {string} The account-scoped URL.
|
||||
*/
|
||||
const accountScopedUrl = url => {
|
||||
return `/app/accounts/${accountId.value}/${url}`;
|
||||
};
|
||||
|
||||
return {
|
||||
accountId,
|
||||
accountScopedUrl,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { computed } from 'vue';
|
||||
import { useStore, useStoreGetters } from 'dashboard/composables/store';
|
||||
|
||||
/**
|
||||
* Composable for managing conversation labels
|
||||
* @returns {Object} An object containing methods and computed properties for conversation labels
|
||||
*/
|
||||
export function useConversationLabels() {
|
||||
const store = useStore();
|
||||
const getters = useStoreGetters();
|
||||
|
||||
/**
|
||||
* The currently selected chat
|
||||
* @type {import('vue').ComputedRef<Object>}
|
||||
*/
|
||||
const currentChat = computed(() => getters.getSelectedChat.value);
|
||||
|
||||
/**
|
||||
* The ID of the current conversation
|
||||
* @type {import('vue').ComputedRef<number|null>}
|
||||
*/
|
||||
const conversationId = computed(() => currentChat.value?.id);
|
||||
|
||||
/**
|
||||
* All labels available for the account
|
||||
* @type {import('vue').ComputedRef<Array>}
|
||||
*/
|
||||
const accountLabels = computed(() => getters['labels/getLabels'].value);
|
||||
|
||||
/**
|
||||
* Labels currently saved to the conversation
|
||||
* @type {import('vue').ComputedRef<Array>}
|
||||
*/
|
||||
const savedLabels = computed(() => {
|
||||
return store.getters['conversationLabels/getConversationLabels'](
|
||||
conversationId.value
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Labels currently active on the conversation
|
||||
* @type {import('vue').ComputedRef<Array>}
|
||||
*/
|
||||
const activeLabels = computed(() =>
|
||||
accountLabels.value.filter(({ title }) => savedLabels.value.includes(title))
|
||||
);
|
||||
|
||||
/**
|
||||
* Labels available but not active on the conversation
|
||||
* @type {import('vue').ComputedRef<Array>}
|
||||
*/
|
||||
const inactiveLabels = computed(() =>
|
||||
accountLabels.value.filter(
|
||||
({ title }) => !savedLabels.value.includes(title)
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates the labels for the current conversation
|
||||
* @param {string[]} selectedLabels - Array of label titles to be set for the conversation
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const onUpdateLabels = async selectedLabels => {
|
||||
await store.dispatch('conversationLabels/update', {
|
||||
conversationId: conversationId.value,
|
||||
labels: selectedLabels,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a label to the current conversation
|
||||
* @param {Object} value - The label object to be added
|
||||
* @param {string} value.title - The title of the label to be added
|
||||
*/
|
||||
const addLabelToConversation = value => {
|
||||
const result = activeLabels.value.map(item => item.title);
|
||||
result.push(value.title);
|
||||
onUpdateLabels(result);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a label from the current conversation
|
||||
* @param {string} value - The title of the label to be removed
|
||||
*/
|
||||
const removeLabelFromConversation = value => {
|
||||
const result = activeLabels.value
|
||||
.map(label => label.title)
|
||||
.filter(label => label !== value);
|
||||
onUpdateLabels(result);
|
||||
};
|
||||
|
||||
return {
|
||||
accountLabels,
|
||||
savedLabels,
|
||||
activeLabels,
|
||||
inactiveLabels,
|
||||
addLabelToConversation,
|
||||
removeLabelFromConversation,
|
||||
onUpdateLabels,
|
||||
};
|
||||
}
|
||||
@@ -3,13 +3,18 @@
|
||||
"HEADER": "Labels",
|
||||
"HEADER_BTN_TXT": "Add label",
|
||||
"LOADING": "Fetching labels",
|
||||
"DESCRIPTION": "Labels help you categorize and prioritize conversations and leads. You can assign a label to a conversation or contact using the side panel.",
|
||||
"LEARN_MORE": "Learn more about labels",
|
||||
"SEARCH_404": "There are no items matching this query",
|
||||
"SIDEBAR_TXT": "<p><b>Labels</b> <p>Labels help you to categorize conversations and prioritize them. You can assign label to a conversation from the sidepanel. <br /><br />Labels are tied to the account and can be used to create custom workflows in your organization. You can assign custom color to a label, it makes it easier to identify the label. You will be able to display the label on the sidebar to filter the conversations easily.</p>",
|
||||
"LIST": {
|
||||
"404": "There are no labels available in this account.",
|
||||
"TITLE": "Manage labels",
|
||||
"DESC": "Labels let you group the conversations together.",
|
||||
"TABLE_HEADER": ["Name", "Description", "Color"]
|
||||
"TABLE_HEADER": [
|
||||
"Name",
|
||||
"Description",
|
||||
"Color"
|
||||
]
|
||||
},
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"LEARN_MORE": "Learn more about teams",
|
||||
"LIST": {
|
||||
"404": "There are no teams created on this account.",
|
||||
"EDIT_TEAM": "Edit team"
|
||||
"EDIT_TEAM": "Edit team",
|
||||
"NONE": "None"
|
||||
},
|
||||
"CREATE_FLOW": {
|
||||
"CREATE": {
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
addAccountScoping(url) {
|
||||
return `/app/accounts/${this.accountId}/${url}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { isValidURL } from '../helper/URLHelper';
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
accountId: 'getCurrentAccountId',
|
||||
}),
|
||||
attributes() {
|
||||
return this.$store.getters['attributes/getAttributesByModel'](
|
||||
this.attributeType
|
||||
);
|
||||
},
|
||||
customAttributes() {
|
||||
if (this.attributeType === 'conversation_attribute')
|
||||
return this.currentChat.custom_attributes || {};
|
||||
return this.contact.custom_attributes || {};
|
||||
},
|
||||
contactIdentifier() {
|
||||
return (
|
||||
this.currentChat.meta?.sender?.id ||
|
||||
this.$route.params.contactId ||
|
||||
this.contactId
|
||||
);
|
||||
},
|
||||
contact() {
|
||||
return this.$store.getters['contacts/getContact'](this.contactIdentifier);
|
||||
},
|
||||
conversationId() {
|
||||
return this.currentChat.id;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isAttributeNumber(attributeValue) {
|
||||
return (
|
||||
Number.isInteger(Number(attributeValue)) && Number(attributeValue) > 0
|
||||
);
|
||||
},
|
||||
attributeDisplayType(attributeValue) {
|
||||
if (this.isAttributeNumber(attributeValue)) {
|
||||
return 'number';
|
||||
}
|
||||
if (isValidURL(attributeValue)) {
|
||||
return 'link';
|
||||
}
|
||||
return 'text';
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters({ accountLabels: 'labels/getLabels' }),
|
||||
savedLabels() {
|
||||
// If conversationLabels is passed as prop, use it
|
||||
if (this.conversationLabels)
|
||||
return this.conversationLabels.split(',').map(item => item.trim());
|
||||
// Otherwise, get labels from store
|
||||
return this.$store.getters['conversationLabels/getConversationLabels'](
|
||||
this.conversationId
|
||||
);
|
||||
},
|
||||
// TODO - Get rid of this from the mixin
|
||||
activeLabels() {
|
||||
return this.accountLabels.filter(({ title }) =>
|
||||
this.savedLabels.includes(title)
|
||||
);
|
||||
},
|
||||
inactiveLabels() {
|
||||
return this.accountLabels.filter(
|
||||
({ title }) => !this.savedLabels.includes(title)
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addLabelToConversation(value) {
|
||||
const result = this.activeLabels.map(item => item.title);
|
||||
result.push(value.title);
|
||||
this.onUpdateLabels(result);
|
||||
},
|
||||
removeLabelFromConversation(value) {
|
||||
const result = this.activeLabels
|
||||
.map(label => label.title)
|
||||
.filter(label => label !== value);
|
||||
this.onUpdateLabels(result);
|
||||
},
|
||||
async onUpdateLabels(selectedLabels) {
|
||||
this.$store.dispatch('conversationLabels/update', {
|
||||
conversationId: this.conversationId,
|
||||
labels: selectedLabels,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters({ teams: 'teams/getTeams' }),
|
||||
hasAnAssignedTeam() {
|
||||
return !!this.currentChat?.meta?.team;
|
||||
},
|
||||
teamsList() {
|
||||
if (this.hasAnAssignedTeam) {
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
name: 'None',
|
||||
},
|
||||
...this.teams,
|
||||
];
|
||||
}
|
||||
return this.teams;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import accountMixin from '../account';
|
||||
import Vuex from 'vuex';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('accountMixin', () => {
|
||||
let getters;
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
getters = {
|
||||
getCurrentAccountId: () => 1,
|
||||
};
|
||||
|
||||
store = new Vuex.Store({ getters });
|
||||
});
|
||||
|
||||
it('set accountId properly', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [accountMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.accountId).toBe(1);
|
||||
});
|
||||
|
||||
it('returns current url', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [accountMixin],
|
||||
};
|
||||
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.addAccountScoping('settings/inboxes/new')).toBe(
|
||||
'/app/accounts/1/settings/inboxes/new'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,145 +0,0 @@
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import attributeMixin from '../attributeMixin';
|
||||
import Vuex from 'vuex';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('attributeMixin', () => {
|
||||
let getters;
|
||||
let actions;
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
actions = { updateUISettings: vi.fn(), toggleSidebarUIState: vi.fn() };
|
||||
getters = {
|
||||
getSelectedChat: () => ({
|
||||
id: 7165,
|
||||
custom_attributes: {
|
||||
product_id: 2021,
|
||||
},
|
||||
meta: {
|
||||
sender: {
|
||||
id: 1212,
|
||||
},
|
||||
},
|
||||
}),
|
||||
getCurrentAccountId: () => 1,
|
||||
attributeType: () => 'conversation_attribute',
|
||||
};
|
||||
store = new Vuex.Store({ actions, getters });
|
||||
});
|
||||
|
||||
it('returns currently selected conversation id', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [attributeMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.conversationId).toEqual(7165);
|
||||
});
|
||||
|
||||
it('return display type if attribute passed', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [attributeMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.attributeDisplayType('date')).toBe('text');
|
||||
expect(
|
||||
wrapper.vm.attributeDisplayType('https://www.chatwoot.com/pricing')
|
||||
).toBe('link');
|
||||
expect(wrapper.vm.attributeDisplayType(9988)).toBe('number');
|
||||
});
|
||||
|
||||
it('return true if number is passed', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [attributeMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.isAttributeNumber(9988)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns currently selected contact', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [attributeMixin],
|
||||
computed: {
|
||||
contact() {
|
||||
return {
|
||||
id: 7165,
|
||||
custom_attributes: {
|
||||
product_id: 2021,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.contact).toEqual({
|
||||
id: 7165,
|
||||
custom_attributes: {
|
||||
product_id: 2021,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns currently selected contact id', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [attributeMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.contactIdentifier).toEqual(1212);
|
||||
});
|
||||
|
||||
it('returns currently selected conversation custom attributes', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [attributeMixin],
|
||||
computed: {
|
||||
contact() {
|
||||
return {
|
||||
id: 7165,
|
||||
custom_attributes: {
|
||||
product_id: 2021,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.customAttributes).toEqual({
|
||||
product_id: 2021,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns currently selected contact custom attributes', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [attributeMixin],
|
||||
computed: {
|
||||
contact() {
|
||||
return {
|
||||
id: 7165,
|
||||
custom_attributes: {
|
||||
cloudCustomer: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.customAttributes).toEqual({
|
||||
cloudCustomer: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import '@chatwoot/ninja-keys';
|
||||
import { useConversationLabels } from 'dashboard/composables/useConversationLabels';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import conversationHotKeysMixin from './conversationHotKeys';
|
||||
import bulkActionsHotKeysMixin from './bulkActionsHotKeys';
|
||||
@@ -7,8 +8,6 @@ import inboxHotKeysMixin from './inboxHotKeys';
|
||||
import goToCommandHotKeys from './goToCommandHotKeys';
|
||||
import appearanceHotKeys from './appearanceHotKeys';
|
||||
import agentMixin from 'dashboard/mixins/agentMixin';
|
||||
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
|
||||
import conversationTeamMixin from 'dashboard/mixins/conversation/teamMixin';
|
||||
import { GENERAL_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
|
||||
export default {
|
||||
@@ -17,11 +16,25 @@ export default {
|
||||
conversationHotKeysMixin,
|
||||
bulkActionsHotKeysMixin,
|
||||
inboxHotKeysMixin,
|
||||
conversationLabelMixin,
|
||||
conversationTeamMixin,
|
||||
appearanceHotKeys,
|
||||
goToCommandHotKeys,
|
||||
],
|
||||
setup() {
|
||||
// used in conversationHotKeysMixin
|
||||
const {
|
||||
activeLabels,
|
||||
inactiveLabels,
|
||||
addLabelToConversation,
|
||||
removeLabelFromConversation,
|
||||
} = useConversationLabels();
|
||||
|
||||
return {
|
||||
activeLabels,
|
||||
inactiveLabels,
|
||||
addLabelToConversation,
|
||||
removeLabelFromConversation,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// Added selectedSnoozeType to track the selected snooze type
|
||||
|
||||
@@ -65,6 +65,7 @@ export default {
|
||||
currentChat: 'getSelectedChat',
|
||||
replyMode: 'draftMessages/getReplyEditorMode',
|
||||
contextMenuChatId: 'getContextMenuChatId',
|
||||
teams: 'teams/getTeams',
|
||||
}),
|
||||
draftMessage() {
|
||||
return this.$store.getters['draftMessages/get'](this.draftKey);
|
||||
@@ -78,7 +79,18 @@ export default {
|
||||
conversationId() {
|
||||
return this.currentChat?.id;
|
||||
},
|
||||
|
||||
hasAnAssignedTeam() {
|
||||
return !!this.currentChat?.meta?.team;
|
||||
},
|
||||
teamsList() {
|
||||
if (this.hasAnAssignedTeam) {
|
||||
return [
|
||||
{ id: 0, name: this.$t('TEAMS_SETTINGS.LIST.NONE') },
|
||||
...this.teams,
|
||||
];
|
||||
}
|
||||
return this.teams;
|
||||
},
|
||||
statusActions() {
|
||||
const isOpen =
|
||||
this.currentChat?.status === wootConstants.STATUS_TYPE.OPEN;
|
||||
|
||||
@@ -115,13 +115,11 @@ export default {
|
||||
<CustomAttributes
|
||||
:contact-id="contact.id"
|
||||
attribute-type="contact_attribute"
|
||||
attribute-class="conversation--attribute"
|
||||
attribute-from="contact_panel"
|
||||
:custom-attributes="contact.custom_attributes"
|
||||
:empty-state-message="
|
||||
$t('CONTACT_PANEL.SIDEBAR_SECTIONS.NO_RECORDS_FOUND')
|
||||
"
|
||||
class="even"
|
||||
/>
|
||||
</AccordionItem>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { VeTable } from 'vue-easytable';
|
||||
import { getCountryFlag } from 'dashboard/helper/flag';
|
||||
|
||||
@@ -6,7 +7,6 @@ import Spinner from 'shared/components/Spinner.vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
|
||||
export default {
|
||||
@@ -15,7 +15,6 @@ export default {
|
||||
Spinner,
|
||||
VeTable,
|
||||
},
|
||||
mixins: [rtlMixin],
|
||||
props: {
|
||||
contacts: {
|
||||
type: Array,
|
||||
@@ -52,6 +51,9 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isRTL: 'accounts/isRTL',
|
||||
}),
|
||||
tableData() {
|
||||
if (this.isLoading) {
|
||||
return [];
|
||||
@@ -85,7 +87,7 @@ export default {
|
||||
key: 'name',
|
||||
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.NAME'),
|
||||
fixed: 'left',
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
align: this.isRTL ? 'right' : 'left',
|
||||
sortBy: this.sortConfig.name || '',
|
||||
width: 300,
|
||||
renderBodyCell: ({ row }) => (
|
||||
@@ -121,7 +123,7 @@ export default {
|
||||
field: 'email',
|
||||
key: 'email',
|
||||
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.EMAIL_ADDRESS'),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
align: this.isRTL ? 'right' : 'left',
|
||||
sortBy: this.sortConfig.email || '',
|
||||
width: 240,
|
||||
renderBodyCell: ({ row }) => {
|
||||
@@ -145,27 +147,27 @@ export default {
|
||||
key: 'phone_number',
|
||||
sortBy: this.sortConfig.phone_number || '',
|
||||
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.PHONE_NUMBER'),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
align: this.isRTL ? 'right' : 'left',
|
||||
},
|
||||
{
|
||||
field: 'company',
|
||||
key: 'company',
|
||||
sortBy: this.sortConfig.company_name || '',
|
||||
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.COMPANY'),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
align: this.isRTL ? 'right' : 'left',
|
||||
},
|
||||
{
|
||||
field: 'city',
|
||||
key: 'city',
|
||||
sortBy: this.sortConfig.city || '',
|
||||
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.CITY'),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
align: this.isRTL ? 'right' : 'left',
|
||||
},
|
||||
{
|
||||
field: 'country',
|
||||
key: 'country',
|
||||
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.COUNTRY'),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
align: this.isRTL ? 'right' : 'left',
|
||||
sortBy: this.sortConfig.country || '',
|
||||
renderBodyCell: ({ row }) => {
|
||||
if (row.country) {
|
||||
@@ -182,7 +184,7 @@ export default {
|
||||
field: 'profiles',
|
||||
key: 'profiles',
|
||||
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.SOCIAL_PROFILES'),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
align: this.isRTL ? 'right' : 'left',
|
||||
renderBodyCell: ({ row }) => {
|
||||
const { profiles } = row;
|
||||
|
||||
@@ -213,14 +215,14 @@ export default {
|
||||
key: 'last_activity_at',
|
||||
sortBy: this.sortConfig.last_activity_at || '',
|
||||
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.LAST_ACTIVITY'),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
align: this.isRTL ? 'right' : 'left',
|
||||
},
|
||||
{
|
||||
field: 'created_at',
|
||||
key: 'created_at',
|
||||
sortBy: this.sortConfig.created_at || '',
|
||||
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.CREATED_AT'),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
align: this.isRTL ? 'right' : 'left',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
@@ -218,8 +218,6 @@ export default {
|
||||
>
|
||||
<CustomAttributes
|
||||
attribute-type="contact_attribute"
|
||||
attribute-class="conversation--attribute"
|
||||
class="even"
|
||||
attribute-from="conversation_contact_panel"
|
||||
:contact-id="contact.id"
|
||||
:empty-state-message="
|
||||
|
||||
@@ -6,7 +6,6 @@ import ContactDetailsItem from './ContactDetailsItem.vue';
|
||||
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||
import ConversationLabels from './labels/LabelBox.vue';
|
||||
import agentMixin from 'dashboard/mixins/agentMixin';
|
||||
import teamMixin from 'dashboard/mixins/conversation/teamMixin';
|
||||
import { CONVERSATION_PRIORITY } from '../../../../shared/constants/messages';
|
||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
|
||||
@@ -16,7 +15,7 @@ export default {
|
||||
MultiselectDropdown,
|
||||
ConversationLabels,
|
||||
},
|
||||
mixins: [agentMixin, teamMixin],
|
||||
mixins: [agentMixin],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
@@ -65,7 +64,20 @@ export default {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
currentUser: 'getCurrentUser',
|
||||
teams: 'teams/getTeams',
|
||||
}),
|
||||
hasAnAssignedTeam() {
|
||||
return !!this.currentChat?.meta?.team;
|
||||
},
|
||||
teamsList() {
|
||||
if (this.hasAnAssignedTeam) {
|
||||
return [
|
||||
{ id: 0, name: this.$t('TEAMS_SETTINGS.LIST.NONE') },
|
||||
...this.teams,
|
||||
];
|
||||
}
|
||||
return this.teams;
|
||||
},
|
||||
assignedAgent: {
|
||||
get() {
|
||||
return this.currentChat.meta.assignee;
|
||||
|
||||
@@ -70,41 +70,39 @@ const staticElements = computed(() =>
|
||||
},
|
||||
].filter(attribute => !!attribute.content.value)
|
||||
);
|
||||
|
||||
const evenClass = [
|
||||
'[&>*:nth-child(odd)]:!bg-white [&>*:nth-child(even)]:!bg-slate-25',
|
||||
'dark:[&>*:nth-child(odd)]:!bg-slate-900 dark:[&>*:nth-child(even)]:!bg-slate-800/50',
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="conversation--details">
|
||||
<ContactDetailsItem
|
||||
v-for="element in staticElements"
|
||||
:key="element.title"
|
||||
:title="$t(element.title)"
|
||||
:value="element.content.value"
|
||||
class="conversation--attribute"
|
||||
>
|
||||
<a
|
||||
v-if="element.type === 'link'"
|
||||
:href="referer"
|
||||
rel="noopener noreferrer nofollow"
|
||||
target="_blank"
|
||||
class="text-woot-400 dark:text-woot-600"
|
||||
<div :class="evenClass">
|
||||
<ContactDetailsItem
|
||||
v-for="element in staticElements"
|
||||
:key="element.title"
|
||||
:title="$t(element.title)"
|
||||
:value="element.content.value"
|
||||
class="border-b border-solid border-slate-50 dark:border-slate-700/50"
|
||||
>
|
||||
{{ referer }}
|
||||
</a>
|
||||
</ContactDetailsItem>
|
||||
<a
|
||||
v-if="element.type === 'link'"
|
||||
:href="referer"
|
||||
rel="noopener noreferrer nofollow"
|
||||
target="_blank"
|
||||
class="text-woot-400 dark:text-woot-600"
|
||||
>
|
||||
{{ referer }}
|
||||
</a>
|
||||
</ContactDetailsItem>
|
||||
</div>
|
||||
<CustomAttributes
|
||||
:class="staticElements.length % 2 === 0 ? 'even' : 'odd'"
|
||||
:start-at="staticElements.length % 2 === 0 ? 'even' : 'odd'"
|
||||
attribute-class="conversation--attribute"
|
||||
attribute-from="conversation_panel"
|
||||
attribute-type="conversation_attribute"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.conversation--attribute {
|
||||
@apply border-slate-50 dark:border-slate-700/50 border-b border-solid;
|
||||
&:nth-child(2n) {
|
||||
@apply bg-slate-25 dark:bg-slate-800/50;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import MacroItem from './MacroItem.vue';
|
||||
import accountMixin from 'dashboard/mixins/account.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MacroItem,
|
||||
},
|
||||
mixins: [accountMixin],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { accountScopedUrl } = useAccount();
|
||||
|
||||
return {
|
||||
accountScopedUrl,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
macros: ['macros/getMacros'],
|
||||
@@ -32,10 +38,10 @@ export default {
|
||||
v-if="!uiFlags.isFetching && !macros.length"
|
||||
class="macros_list--empty-state"
|
||||
>
|
||||
<p class="flex h-full items-center flex-col justify-center">
|
||||
<p class="flex flex-col items-center justify-center h-full">
|
||||
{{ $t('MACROS.LIST.404') }}
|
||||
</p>
|
||||
<router-link :to="addAccountScoping('settings/macros')">
|
||||
<router-link :to="accountScopedUrl('settings/macros')">
|
||||
<woot-button
|
||||
variant="smooth"
|
||||
icon="add"
|
||||
|
||||
+168
-158
@@ -1,153 +1,187 @@
|
||||
<script>
|
||||
<script setup>
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { useRoute } from 'dashboard/composables/route';
|
||||
import { useStore, useStoreGetters } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
import CustomAttribute from 'dashboard/components/CustomAttribute.vue';
|
||||
import attributeMixin from 'dashboard/mixins/attributeMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CustomAttribute,
|
||||
const props = defineProps({
|
||||
attributeType: {
|
||||
type: String,
|
||||
default: 'conversation_attribute',
|
||||
},
|
||||
mixins: [attributeMixin],
|
||||
props: {
|
||||
attributeType: {
|
||||
type: String,
|
||||
default: 'conversation_attribute',
|
||||
},
|
||||
attributeClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
contactId: { type: Number, default: null },
|
||||
attributeFrom: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
emptyStateMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
contactId: { type: Number, default: null },
|
||||
attributeFrom: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
setup() {
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
emptyStateMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
startAt: {
|
||||
type: String,
|
||||
default: 'even',
|
||||
validator: value => value === 'even' || value === 'odd',
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
const getters = useStoreGetters();
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
const [showAllAttributes, toggleShowAllAttributes] = useToggle(false);
|
||||
|
||||
const currentChat = computed(() => getters.getSelectedChat.value);
|
||||
const attributes = computed(() =>
|
||||
getters['attributes/getAttributesByModel'].value(props.attributeType)
|
||||
);
|
||||
|
||||
const contactIdentifier = computed(
|
||||
() =>
|
||||
currentChat.value.meta?.sender?.id ||
|
||||
route.params.contactId ||
|
||||
props.contactId
|
||||
);
|
||||
|
||||
const contact = computed(() =>
|
||||
getters['contacts/getContact'].value(contactIdentifier.value)
|
||||
);
|
||||
|
||||
const customAttributes = computed(() => {
|
||||
if (props.attributeType === 'conversation_attribute')
|
||||
return currentChat.value.custom_attributes || {};
|
||||
return contact.value.custom_attributes || {};
|
||||
});
|
||||
|
||||
const conversationId = computed(() => currentChat.value.id);
|
||||
|
||||
const toggleButtonText = computed(() =>
|
||||
!showAllAttributes.value
|
||||
? t('CUSTOM_ATTRIBUTES.SHOW_MORE')
|
||||
: t('CUSTOM_ATTRIBUTES.SHOW_LESS')
|
||||
);
|
||||
|
||||
const filteredAttributes = computed(() =>
|
||||
attributes.value.map(attribute => {
|
||||
// Check if the attribute key exists in customAttributes
|
||||
const hasValue = Object.hasOwnProperty.call(
|
||||
customAttributes.value,
|
||||
attribute.attribute_key
|
||||
);
|
||||
const isCheckbox = attribute.attribute_display_type === 'checkbox';
|
||||
const defaultValue = isCheckbox ? false : '';
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
...attribute,
|
||||
// Set value from customAttributes if it exists, otherwise use default value
|
||||
value: hasValue
|
||||
? customAttributes.value[attribute.attribute_key]
|
||||
: defaultValue,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showAllAttributes: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
toggleButtonText() {
|
||||
return !this.showAllAttributes
|
||||
? this.$t('CUSTOM_ATTRIBUTES.SHOW_MORE')
|
||||
: this.$t('CUSTOM_ATTRIBUTES.SHOW_LESS');
|
||||
},
|
||||
filteredAttributes() {
|
||||
return this.attributes.map(attribute => {
|
||||
// Check if the attribute key exists in customAttributes
|
||||
const hasValue = Object.hasOwnProperty.call(
|
||||
this.customAttributes,
|
||||
attribute.attribute_key
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
const isCheckbox = attribute.attribute_display_type === 'checkbox';
|
||||
const defaultValue = isCheckbox ? false : '';
|
||||
const displayedAttributes = computed(() => {
|
||||
// Show only the first 5 attributes or all depending on showAllAttributes
|
||||
if (showAllAttributes.value || filteredAttributes.value.length <= 5) {
|
||||
return filteredAttributes.value;
|
||||
}
|
||||
return filteredAttributes.value.slice(0, 5);
|
||||
});
|
||||
|
||||
return {
|
||||
...attribute,
|
||||
// Set value from customAttributes if it exists, otherwise use default value
|
||||
value: hasValue
|
||||
? this.customAttributes[attribute.attribute_key]
|
||||
: defaultValue,
|
||||
};
|
||||
});
|
||||
},
|
||||
displayedAttributes() {
|
||||
// Show only the first 5 attributes or all depending on showAllAttributes
|
||||
if (this.showAllAttributes || this.filteredAttributes.length <= 5) {
|
||||
return this.filteredAttributes;
|
||||
}
|
||||
return this.filteredAttributes.slice(0, 5);
|
||||
},
|
||||
showMoreUISettingsKey() {
|
||||
return `show_all_attributes_${this.attributeFrom}`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initializeSettings();
|
||||
},
|
||||
methods: {
|
||||
initializeSettings() {
|
||||
this.showAllAttributes =
|
||||
this.uiSettings[this.showMoreUISettingsKey] || false;
|
||||
},
|
||||
onClickToggle() {
|
||||
this.showAllAttributes = !this.showAllAttributes;
|
||||
this.updateUISettings({
|
||||
[this.showMoreUISettingsKey]: this.showAllAttributes,
|
||||
});
|
||||
},
|
||||
async onUpdate(key, value) {
|
||||
const updatedAttributes = { ...this.customAttributes, [key]: value };
|
||||
try {
|
||||
if (this.attributeType === 'conversation_attribute') {
|
||||
await this.$store.dispatch('updateCustomAttributes', {
|
||||
conversationId: this.conversationId,
|
||||
customAttributes: updatedAttributes,
|
||||
});
|
||||
} else {
|
||||
this.$store.dispatch('contacts/update', {
|
||||
id: this.contactId,
|
||||
custom_attributes: updatedAttributes,
|
||||
});
|
||||
}
|
||||
useAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.UPDATE.SUCCESS'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.message ||
|
||||
this.$t('CUSTOM_ATTRIBUTES.FORM.UPDATE.ERROR');
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
},
|
||||
async onDelete(key) {
|
||||
try {
|
||||
const { [key]: remove, ...updatedAttributes } = this.customAttributes;
|
||||
if (this.attributeType === 'conversation_attribute') {
|
||||
await this.$store.dispatch('updateCustomAttributes', {
|
||||
conversationId: this.conversationId,
|
||||
customAttributes: updatedAttributes,
|
||||
});
|
||||
} else {
|
||||
this.$store.dispatch('contacts/deleteCustomAttributes', {
|
||||
id: this.contactId,
|
||||
customAttributes: [key],
|
||||
});
|
||||
}
|
||||
const showMoreUISettingsKey = computed(
|
||||
() => `show_all_attributes_${props.attributeFrom}`
|
||||
);
|
||||
|
||||
useAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.DELETE.SUCCESS'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.message ||
|
||||
this.$t('CUSTOM_ATTRIBUTES.FORM.DELETE.ERROR');
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
},
|
||||
async onCopy(attributeValue) {
|
||||
await copyTextToClipboard(attributeValue);
|
||||
useAlert(this.$t('CUSTOM_ATTRIBUTES.COPY_SUCCESSFUL'));
|
||||
},
|
||||
},
|
||||
const initializeSettings = () => {
|
||||
showAllAttributes.value =
|
||||
uiSettings.value[showMoreUISettingsKey.value] || false;
|
||||
};
|
||||
|
||||
const onClickToggle = () => {
|
||||
toggleShowAllAttributes();
|
||||
updateUISettings({
|
||||
[showMoreUISettingsKey.value]: showAllAttributes.value,
|
||||
});
|
||||
};
|
||||
|
||||
const onUpdate = async (key, value) => {
|
||||
const updatedAttributes = { ...customAttributes.value, [key]: value };
|
||||
try {
|
||||
if (props.attributeType === 'conversation_attribute') {
|
||||
await store.dispatch('updateCustomAttributes', {
|
||||
conversationId: conversationId.value,
|
||||
customAttributes: updatedAttributes,
|
||||
});
|
||||
} else {
|
||||
store.dispatch('contacts/update', {
|
||||
id: props.contactId,
|
||||
custom_attributes: updatedAttributes,
|
||||
});
|
||||
}
|
||||
useAlert(t('CUSTOM_ATTRIBUTES.FORM.UPDATE.SUCCESS'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.message || t('CUSTOM_ATTRIBUTES.FORM.UPDATE.ERROR');
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async key => {
|
||||
try {
|
||||
const { [key]: remove, ...updatedAttributes } = customAttributes.value;
|
||||
if (props.attributeType === 'conversation_attribute') {
|
||||
await store.dispatch('updateCustomAttributes', {
|
||||
conversationId: conversationId.value,
|
||||
customAttributes: updatedAttributes,
|
||||
});
|
||||
} else {
|
||||
store.dispatch('contacts/deleteCustomAttributes', {
|
||||
id: props.contactId,
|
||||
customAttributes: [key],
|
||||
});
|
||||
}
|
||||
useAlert(t('CUSTOM_ATTRIBUTES.FORM.DELETE.SUCCESS'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.message || t('CUSTOM_ATTRIBUTES.FORM.DELETE.ERROR');
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const onCopy = async attributeValue => {
|
||||
await copyTextToClipboard(attributeValue);
|
||||
useAlert(t('CUSTOM_ATTRIBUTES.COPY_SUCCESSFUL'));
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initializeSettings();
|
||||
});
|
||||
|
||||
const evenClass = [
|
||||
'[&>*:nth-child(odd)]:!bg-white [&>*:nth-child(even)]:!bg-slate-25',
|
||||
'dark:[&>*:nth-child(odd)]:!bg-slate-900 dark:[&>*:nth-child(even)]:!bg-slate-800/50',
|
||||
];
|
||||
const oddClass = [
|
||||
'[&>*:nth-child(odd)]:!bg-slate-25 [&>*:nth-child(even)]:!bg-white',
|
||||
'dark:[&>*:nth-child(odd)]:!bg-slate-800/50 dark:[&>*:nth-child(even)]:!bg-slate-900',
|
||||
];
|
||||
|
||||
const wrapperClass = computed(() => {
|
||||
return props.startAt === 'even' ? evenClass : oddClass;
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- TODO: After migration to Vue 3, remove the top level div -->
|
||||
<template>
|
||||
<div class="custom-attributes--panel">
|
||||
<div :class="wrapperClass">
|
||||
<CustomAttribute
|
||||
v-for="attribute in displayedAttributes"
|
||||
:key="attribute.id"
|
||||
@@ -160,8 +194,8 @@ export default {
|
||||
show-actions
|
||||
:attribute-regex="attribute.regex_pattern"
|
||||
:regex-cue="attribute.regex_cue"
|
||||
:class="attributeClass"
|
||||
:contact-id="contactId"
|
||||
class="border-b border-solid border-slate-50 dark:border-slate-700/50"
|
||||
@update="onUpdate"
|
||||
@delete="onDelete"
|
||||
@copy="onCopy"
|
||||
@@ -187,27 +221,3 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.custom-attributes--panel {
|
||||
.conversation--attribute {
|
||||
@apply border-slate-50 dark:border-slate-700/50 border-b border-solid;
|
||||
}
|
||||
|
||||
&.odd {
|
||||
.conversation--attribute {
|
||||
&:nth-child(2n + 1) {
|
||||
@apply bg-slate-25 dark:bg-slate-800/50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.even {
|
||||
.conversation--attribute {
|
||||
&:nth-child(2n) {
|
||||
@apply bg-slate-25 dark:bg-slate-800/50;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
import { ref } from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import { useConversationLabels } from 'dashboard/composables/useConversationLabels';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
|
||||
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
|
||||
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -14,20 +14,17 @@ export default {
|
||||
LabelDropdown,
|
||||
AddLabel,
|
||||
},
|
||||
|
||||
mixins: [conversationLabelMixin],
|
||||
props: {
|
||||
// conversationId prop is used in /conversation/labelMixin,
|
||||
// remove this props when refactoring to composable if not needed
|
||||
// eslint-disable-next-line vue/no-unused-properties
|
||||
conversationId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
|
||||
const {
|
||||
savedLabels,
|
||||
activeLabels,
|
||||
accountLabels,
|
||||
addLabelToConversation,
|
||||
removeLabelFromConversation,
|
||||
} = useConversationLabels();
|
||||
|
||||
const conversationLabelBoxRef = ref(null);
|
||||
const showSearchDropdownLabel = ref(false);
|
||||
|
||||
@@ -58,6 +55,11 @@ export default {
|
||||
useKeyboardEvents(keyboardEvents, conversationLabelBoxRef);
|
||||
return {
|
||||
isAdmin,
|
||||
savedLabels,
|
||||
activeLabels,
|
||||
accountLabels,
|
||||
addLabelToConversation,
|
||||
removeLabelFromConversation,
|
||||
conversationLabelBoxRef,
|
||||
showSearchDropdownLabel,
|
||||
closeDropdownLabel,
|
||||
|
||||
+2
-7
@@ -1,6 +1,5 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||
import NotificationPanelList from './NotificationPanelList.vue';
|
||||
|
||||
import { ACCOUNT_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
@@ -9,7 +8,6 @@ export default {
|
||||
components: {
|
||||
NotificationPanelList,
|
||||
},
|
||||
mixins: [rtlMixin],
|
||||
data() {
|
||||
return {
|
||||
pageSize: 15,
|
||||
@@ -21,9 +19,6 @@ export default {
|
||||
records: 'notifications/getNotifications',
|
||||
uiFlags: 'notifications/getUIFlags',
|
||||
}),
|
||||
notificationPanelFooterIconClass() {
|
||||
return this.isRTLView ? '-mr-3' : '-ml-3';
|
||||
},
|
||||
totalUnreadNotifications() {
|
||||
return this.meta.unreadCount;
|
||||
},
|
||||
@@ -201,7 +196,7 @@ export default {
|
||||
<fluent-icon
|
||||
icon="chevron-left"
|
||||
size="16"
|
||||
:class="notificationPanelFooterIconClass"
|
||||
class="rtl:-mr-3 ltr:-ml-3"
|
||||
/>
|
||||
</woot-button>
|
||||
<woot-button
|
||||
@@ -236,7 +231,7 @@ export default {
|
||||
<fluent-icon
|
||||
icon="chevron-right"
|
||||
size="16"
|
||||
:class="notificationPanelFooterIconClass"
|
||||
class="rtl:-mr-3 ltr:-ml-3"
|
||||
/>
|
||||
</woot-button>
|
||||
</div>
|
||||
|
||||
@@ -4,21 +4,35 @@ defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
noRecordsFound: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loadingMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
noRecordsMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col w-full h-full gap-10 font-inter">
|
||||
<slot name="header" />
|
||||
<div>
|
||||
<slot v-if="isLoading" name="loading">
|
||||
<woot-loading-state :message="loadingMessage" />
|
||||
</slot>
|
||||
<slot v-else name="body" />
|
||||
</div>
|
||||
<slot v-if="isLoading" name="loading">
|
||||
<woot-loading-state :message="loadingMessage" />
|
||||
</slot>
|
||||
<p
|
||||
v-else-if="noRecordsFound"
|
||||
class="flex-1 text-slate-700 dark:text-slate-100 flex items-center justify-center text-base"
|
||||
>
|
||||
{{ noRecordsMessage }}
|
||||
</p>
|
||||
<slot v-else name="body" />
|
||||
<!-- Do not delete the slot below. It is required to render anything that is not defined in the above slots. -->
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -5,13 +5,12 @@ import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import configMixin from 'shared/mixins/configMixin';
|
||||
import accountMixin from '../../../../mixins/account';
|
||||
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||
import semver from 'semver';
|
||||
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
|
||||
export default {
|
||||
mixins: [accountMixin, configMixin],
|
||||
mixins: [configMixin],
|
||||
setup() {
|
||||
const { updateUISettings } = useUISettings();
|
||||
const v$ = useVuelidate();
|
||||
|
||||
@@ -2,12 +2,19 @@
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
|
||||
import { mapGetters } from 'vuex';
|
||||
import accountMixin from '../../../../mixins/account';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import BillingItem from './components/BillingItem.vue';
|
||||
// sdds
|
||||
|
||||
export default {
|
||||
components: { BillingItem },
|
||||
mixins: [accountMixin, messageFormatterMixin],
|
||||
mixins: [messageFormatterMixin],
|
||||
setup() {
|
||||
const { accountId } = useAccount();
|
||||
|
||||
return {
|
||||
accountId,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getAccount: 'accounts/getAccount',
|
||||
|
||||
@@ -2,19 +2,21 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import Settings from './Settings.vue';
|
||||
import accountMixin from '../../../../mixins/account';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Settings,
|
||||
},
|
||||
mixins: [accountMixin, globalConfigMixin],
|
||||
mixins: [globalConfigMixin],
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
const { accountScopedUrl } = useAccount();
|
||||
return {
|
||||
isAdmin,
|
||||
accountScopedUrl,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
@@ -102,7 +104,7 @@ export default {
|
||||
{{ $t('INBOX_MGMT.LIST.404') }}
|
||||
<router-link
|
||||
v-if="isAdmin"
|
||||
:to="addAccountScoping('settings/inboxes/new')"
|
||||
:to="accountScopedUrl('settings/inboxes/new')"
|
||||
>
|
||||
{{ $t('SETTINGS.INBOXES.NEW_INBOX') }}
|
||||
</router-link>
|
||||
@@ -164,7 +166,7 @@ export default {
|
||||
<td>
|
||||
<div class="button-wrapper">
|
||||
<router-link
|
||||
:to="addAccountScoping(`settings/inboxes/${item.id}`)"
|
||||
:to="accountScopedUrl(`settings/inboxes/${item.id}`)"
|
||||
>
|
||||
<woot-button
|
||||
v-if="isAdmin"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
/* global FB */
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { required } from '@vuelidate/validators';
|
||||
import LoadingState from 'dashboard/components/widgets/LoadingState.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
@@ -10,7 +11,6 @@ import ChannelApi from '../../../../../api/channels';
|
||||
import PageHeader from '../../SettingsSubPageHeader.vue';
|
||||
import router from '../../../../index';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import accountMixin from '../../../../../mixins/account';
|
||||
|
||||
import { loadScript } from 'dashboard/helper/DOMHelpers';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
@@ -20,9 +20,13 @@ export default {
|
||||
LoadingState,
|
||||
PageHeader,
|
||||
},
|
||||
mixins: [globalConfigMixin, accountMixin],
|
||||
mixins: [globalConfigMixin],
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
const { accountId } = useAccount();
|
||||
return {
|
||||
accountId,
|
||||
v$: useVuelidate(),
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -1,138 +1,137 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
<script setup>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { computed, onBeforeMount, ref } from 'vue';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
|
||||
import AddLabel from './AddLabel.vue';
|
||||
import EditLabel from './EditLabel.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddLabel,
|
||||
EditLabel,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: {},
|
||||
showAddPopup: false,
|
||||
showEditPopup: false,
|
||||
showDeleteConfirmationPopup: false,
|
||||
selectedResponse: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
records: 'labels/getLabels',
|
||||
uiFlags: 'labels/getUIFlags',
|
||||
}),
|
||||
// Delete Modal
|
||||
deleteConfirmText() {
|
||||
return this.$t('LABEL_MGMT.DELETE.CONFIRM.YES');
|
||||
},
|
||||
deleteRejectText() {
|
||||
return this.$t('LABEL_MGMT.DELETE.CONFIRM.NO');
|
||||
},
|
||||
deleteMessage() {
|
||||
return ` ${this.selectedResponse.title}?`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('labels/get');
|
||||
},
|
||||
methods: {
|
||||
openAddPopup() {
|
||||
this.showAddPopup = true;
|
||||
},
|
||||
hideAddPopup() {
|
||||
this.showAddPopup = false;
|
||||
},
|
||||
const getters = useStoreGetters();
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
openEditPopup(response) {
|
||||
this.showEditPopup = true;
|
||||
this.selectedResponse = response;
|
||||
},
|
||||
hideEditPopup() {
|
||||
this.showEditPopup = false;
|
||||
},
|
||||
const loading = ref({});
|
||||
const showAddPopup = ref(false);
|
||||
const showEditPopup = ref(false);
|
||||
const showDeleteConfirmationPopup = ref(false);
|
||||
const selectedLabel = ref({});
|
||||
|
||||
openDeletePopup(response) {
|
||||
this.showDeleteConfirmationPopup = true;
|
||||
this.selectedResponse = response;
|
||||
},
|
||||
closeDeletePopup() {
|
||||
this.showDeleteConfirmationPopup = false;
|
||||
},
|
||||
const records = computed(() => getters['labels/getLabels'].value);
|
||||
const uiFlags = computed(() => getters['labels/getUIFlags'].value);
|
||||
|
||||
confirmDeletion() {
|
||||
this.loading[this.selectedResponse.id] = true;
|
||||
this.closeDeletePopup();
|
||||
this.deleteLabel(this.selectedResponse.id);
|
||||
},
|
||||
deleteLabel(id) {
|
||||
this.$store
|
||||
.dispatch('labels/delete', id)
|
||||
.then(() => {
|
||||
useAlert(this.$t('LABEL_MGMT.DELETE.API.SUCCESS_MESSAGE'));
|
||||
})
|
||||
.catch(() => {
|
||||
useAlert(this.$t('LABEL_MGMT.DELETE.API.ERROR_MESSAGE'));
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading[this.selectedResponse.id] = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
const deleteMessage = computed(() => ` ${selectedLabel.value.title}?`);
|
||||
|
||||
const openAddPopup = () => {
|
||||
showAddPopup.value = true;
|
||||
};
|
||||
const hideAddPopup = () => {
|
||||
showAddPopup.value = false;
|
||||
};
|
||||
|
||||
const openEditPopup = response => {
|
||||
showEditPopup.value = true;
|
||||
selectedLabel.value = response;
|
||||
};
|
||||
const hideEditPopup = () => {
|
||||
showEditPopup.value = false;
|
||||
};
|
||||
|
||||
const openDeletePopup = response => {
|
||||
showDeleteConfirmationPopup.value = true;
|
||||
selectedLabel.value = response;
|
||||
};
|
||||
const closeDeletePopup = () => {
|
||||
showDeleteConfirmationPopup.value = false;
|
||||
};
|
||||
|
||||
const deleteLabel = async id => {
|
||||
try {
|
||||
await store.dispatch('labels/delete', id);
|
||||
useAlert(t('LABEL_MGMT.DELETE.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.message || t('LABEL_MGMT.DELETE.API.ERROR_MESSAGE');
|
||||
useAlert(errorMessage);
|
||||
} finally {
|
||||
loading.value[selectedLabel.value.id] = false;
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDeletion = () => {
|
||||
loading.value[selectedLabel.value.id] = true;
|
||||
closeDeletePopup();
|
||||
deleteLabel(selectedLabel.value.id);
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
store.dispatch('labels/get');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 overflow-auto">
|
||||
<woot-button
|
||||
color-scheme="success"
|
||||
class-names="button--fixed-top"
|
||||
icon="add-circle"
|
||||
@click="openAddPopup"
|
||||
>
|
||||
{{ $t('LABEL_MGMT.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
<div class="flex flex-row gap-4 p-8">
|
||||
<div class="w-full xl:w-3/5">
|
||||
<p
|
||||
v-if="!uiFlags.isFetching && !records.length"
|
||||
class="flex flex-col items-center justify-center h-full"
|
||||
<SettingsLayout
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:loading-message="$t('LABEL_MGMT.LOADING')"
|
||||
:no-records-found="!records.length"
|
||||
:no-records-message="$t('LABEL_MGMT.LIST.404')"
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
:title="$t('LABEL_MGMT.HEADER')"
|
||||
:description="$t('LABEL_MGMT.DESCRIPTION')"
|
||||
:link-text="$t('LABEL_MGMT.LEARN_MORE')"
|
||||
feature-name="labels"
|
||||
>
|
||||
<template #actions>
|
||||
<woot-button
|
||||
class="button nice rounded-md"
|
||||
icon="add-circle"
|
||||
@click="openAddPopup"
|
||||
>
|
||||
{{ $t('LABEL_MGMT.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<table
|
||||
class="min-w-full overflow-x-auto divide-y divide-slate-75 dark:divide-slate-700"
|
||||
>
|
||||
<thead>
|
||||
<th
|
||||
v-for="thHeader in $t('LABEL_MGMT.LIST.TABLE_HEADER')"
|
||||
:key="thHeader"
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 text-left font-semibold text-slate-700 dark:text-slate-300"
|
||||
>
|
||||
{{ thHeader }}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody
|
||||
class="divide-y divide-slate-25 dark:divide-slate-800 flex-1 text-slate-700 dark:text-slate-100"
|
||||
>
|
||||
{{ $t('LABEL_MGMT.LIST.404') }}
|
||||
</p>
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.isFetching"
|
||||
:message="$t('LABEL_MGMT.LOADING')"
|
||||
/>
|
||||
<table v-if="!uiFlags.isFetching && records.length" class="woot-table">
|
||||
<thead>
|
||||
<th
|
||||
v-for="thHeader in $t('LABEL_MGMT.LIST.TABLE_HEADER')"
|
||||
:key="thHeader"
|
||||
>
|
||||
{{ thHeader }}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(label, index) in records" :key="label.title">
|
||||
<td class="label-title">
|
||||
<span class="overflow-hidden whitespace-nowrap text-ellipsis">{{
|
||||
label.title
|
||||
}}</span>
|
||||
</td>
|
||||
<td>{{ label.description }}</td>
|
||||
<td>
|
||||
<div class="label-color--container">
|
||||
<span
|
||||
class="label-color--display"
|
||||
:style="{ backgroundColor: label.color }"
|
||||
/>
|
||||
{{ label.color }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="button-wrapper">
|
||||
<tr v-for="(label, index) in records" :key="label.title">
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
<span
|
||||
class="font-medium break-words text-slate-700 dark:text-slate-100 mb-1"
|
||||
>
|
||||
{{ label.title }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">{{ label.description }}</td>
|
||||
<td class="leading-6 py-4 ltr:pr-4 rtl:pl-4">
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
class="rounded h-4 w-4 mr-1 rtl:mr-0 rtl:ml-1 border border-solid border-slate-50 dark:border-slate-700"
|
||||
:style="{ backgroundColor: label.color }"
|
||||
/>
|
||||
{{ label.color }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 min-w-xs">
|
||||
<div class="flex gap-1">
|
||||
<woot-button
|
||||
v-tooltip.top="$t('LABEL_MGMT.FORM.EDIT')"
|
||||
variant="smooth"
|
||||
@@ -153,22 +152,19 @@ export default {
|
||||
:is-loading="loading[label.id]"
|
||||
@click="openDeletePopup(label, index)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<div class="hidden w-1/3 xl:block">
|
||||
<span v-dompurify-html="$t('LABEL_MGMT.SIDEBAR_TXT')" />
|
||||
</div>
|
||||
</div>
|
||||
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
|
||||
<AddLabel @close="hideAddPopup" />
|
||||
</woot-modal>
|
||||
|
||||
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
|
||||
<EditLabel :selected-response="selectedResponse" @close="hideEditPopup" />
|
||||
<EditLabel :selected-response="selectedLabel" @close="hideEditPopup" />
|
||||
</woot-modal>
|
||||
|
||||
<woot-delete-modal
|
||||
@@ -178,26 +174,8 @@ export default {
|
||||
:title="$t('LABEL_MGMT.DELETE.CONFIRM.TITLE')"
|
||||
:message="$t('LABEL_MGMT.DELETE.CONFIRM.MESSAGE')"
|
||||
:message-value="deleteMessage"
|
||||
:confirm-text="deleteConfirmText"
|
||||
:reject-text="deleteRejectText"
|
||||
:confirm-text="$t('LABEL_MGMT.DELETE.CONFIRM.YES')"
|
||||
:reject-text="$t('LABEL_MGMT.DELETE.CONFIRM.NO')"
|
||||
/>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
|
||||
.label-color--container {
|
||||
@apply flex items-center;
|
||||
}
|
||||
|
||||
.label-color--display {
|
||||
@apply rounded h-4 w-4 mr-1 rtl:mr-0 rtl:ml-1 border border-solid border-slate-50 dark:border-slate-700;
|
||||
}
|
||||
|
||||
.label-title {
|
||||
span {
|
||||
@apply w-60 inline-block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
const SettingsContent = () => import('../Wrapper.vue');
|
||||
const SettingsWrapper = () => import('../SettingsWrapper.vue');
|
||||
const Index = () => import('./Index.vue');
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/labels'),
|
||||
component: SettingsContent,
|
||||
props: {
|
||||
headerTitle: 'LABEL_MGMT.HEADER',
|
||||
icon: 'tag',
|
||||
showNewButton: false,
|
||||
},
|
||||
component: SettingsWrapper,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import accountMixin from 'dashboard/mixins/account.js';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import MacrosTableRow from './MacrosTableRow.vue';
|
||||
export default {
|
||||
components: {
|
||||
MacrosTableRow,
|
||||
},
|
||||
mixins: [accountMixin],
|
||||
setup() {
|
||||
const { accountScopedUrl } = useAccount();
|
||||
return {
|
||||
accountScopedUrl,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDeleteConfirmationPopup: false,
|
||||
@@ -56,7 +61,7 @@ export default {
|
||||
<template>
|
||||
<div class="flex-1 overflow-auto">
|
||||
<router-link
|
||||
:to="addAccountScoping('settings/macros/new')"
|
||||
:to="accountScopedUrl('settings/macros/new')"
|
||||
class="button success button--fixed-top button success button--fixed-top px-3.5 py-1 rounded-[5px] flex gap-2"
|
||||
>
|
||||
<fluent-icon icon="add-circle" />
|
||||
@@ -67,7 +72,7 @@ export default {
|
||||
<div class="flex flex-row gap-4 p-8">
|
||||
<div class="w-full lg:w-3/5">
|
||||
<div v-if="!uiFlags.isFetching && !records.length" class="p-3">
|
||||
<p class="flex h-full items-center flex-col justify-center">
|
||||
<p class="flex flex-col items-center justify-center h-full">
|
||||
{{ $t('MACROS.LIST.404') }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -94,7 +99,7 @@ export default {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="hidden lg:block w-1/3">
|
||||
<div class="hidden w-1/3 lg:block">
|
||||
<span v-dompurify-html="$t('MACROS.SIDEBAR_TXT')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
<script>
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import accountMixin from 'dashboard/mixins/account.js';
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [accountMixin],
|
||||
props: {
|
||||
macro: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { accountScopedUrl } = useAccount();
|
||||
|
||||
return {
|
||||
accountScopedUrl,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
createdByName() {
|
||||
const createdBy = this.macro.created_by;
|
||||
@@ -47,7 +53,7 @@ export default {
|
||||
</td>
|
||||
<td>{{ visibilityLabel }}</td>
|
||||
<td class="button-wrapper">
|
||||
<router-link :to="addAccountScoping(`settings/macros/${macro.id}/edit`)">
|
||||
<router-link :to="accountScopedUrl(`settings/macros/${macro.id}/edit`)">
|
||||
<woot-button
|
||||
v-tooltip.top="$t('MACROS.EDIT.TOOLTIP')"
|
||||
variant="smooth"
|
||||
|
||||
@@ -4,14 +4,12 @@ import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName.
|
||||
import { CSAT_RATINGS } from 'shared/constants/messages';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { messageStamp, dynamicTime } from 'shared/helpers/timeHelper';
|
||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
VeTable,
|
||||
VePagination,
|
||||
},
|
||||
mixins: [rtlMixin],
|
||||
props: {
|
||||
pageIndex: {
|
||||
type: Number,
|
||||
@@ -20,6 +18,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isRTL: 'accounts/isRTL',
|
||||
csatResponses: 'csat/getCSATResponses',
|
||||
metrics: 'csat/getMetrics',
|
||||
}),
|
||||
@@ -29,7 +28,7 @@ export default {
|
||||
field: 'contact',
|
||||
key: 'contact',
|
||||
title: this.$t('CSAT_REPORTS.TABLE.HEADER.CONTACT_NAME'),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
align: this.isRTL ? 'right' : 'left',
|
||||
width: 200,
|
||||
renderBodyCell: ({ row }) => {
|
||||
if (row.contact) {
|
||||
@@ -48,7 +47,7 @@ export default {
|
||||
field: 'assignedAgent',
|
||||
key: 'assignedAgent',
|
||||
title: this.$t('CSAT_REPORTS.TABLE.HEADER.AGENT_NAME'),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
align: this.isRTL ? 'right' : 'left',
|
||||
width: 200,
|
||||
renderBodyCell: ({ row }) => {
|
||||
if (row.assignedAgent) {
|
||||
@@ -78,14 +77,14 @@ export default {
|
||||
field: 'feedbackText',
|
||||
key: 'feedbackText',
|
||||
title: this.$t('CSAT_REPORTS.TABLE.HEADER.FEEDBACK_TEXT'),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
align: this.isRTL ? 'right' : 'left',
|
||||
width: 400,
|
||||
},
|
||||
{
|
||||
field: 'conversationId',
|
||||
key: 'conversationId',
|
||||
title: '',
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
align: this.isRTL ? 'right' : 'left',
|
||||
width: 100,
|
||||
renderBodyCell: ({ row }) => {
|
||||
const routerParams = {
|
||||
|
||||
+9
-2
@@ -1,8 +1,9 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName.vue';
|
||||
import CardLabels from 'dashboard/components/widgets/conversation/conversationCardComponents/CardLabels.vue';
|
||||
import SLAViewDetails from './SLAViewDetails.vue';
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
slaName: {
|
||||
type: String,
|
||||
required: true,
|
||||
@@ -20,6 +21,12 @@ defineProps({
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const conversationLabels = computed(() => {
|
||||
return props.conversation.labels
|
||||
? props.conversation.labels.split(',').map(item => item.trim())
|
||||
: [];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -41,7 +48,7 @@ defineProps({
|
||||
<CardLabels
|
||||
class="w-[80%]"
|
||||
:conversation-id="conversationId"
|
||||
:conversation-labels="conversation.labels"
|
||||
:conversation-labels="conversationLabels"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
||||
+7
-5
@@ -1,8 +1,8 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { VeTable, VePagination } from 'vue-easytable';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
|
||||
export default {
|
||||
@@ -13,7 +13,6 @@ export default {
|
||||
VeTable,
|
||||
VePagination,
|
||||
},
|
||||
mixins: [rtlMixin],
|
||||
props: {
|
||||
agents: {
|
||||
type: Array,
|
||||
@@ -33,6 +32,9 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isRTL: 'accounts/isRTL',
|
||||
}),
|
||||
tableData() {
|
||||
return this.agentMetrics
|
||||
.filter(agentMetric => this.getAgentInformation(agentMetric.id))
|
||||
@@ -57,7 +59,7 @@ export default {
|
||||
'OVERVIEW_REPORTS.AGENT_CONVERSATIONS.TABLE_HEADER.AGENT'
|
||||
),
|
||||
fixed: 'left',
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
align: this.isRTL ? 'right' : 'left',
|
||||
width: 25,
|
||||
renderBodyCell: ({ row }) => (
|
||||
<div class="row-user-block">
|
||||
@@ -82,7 +84,7 @@ export default {
|
||||
title: this.$t(
|
||||
'OVERVIEW_REPORTS.AGENT_CONVERSATIONS.TABLE_HEADER.OPEN'
|
||||
),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
align: this.isRTL ? 'right' : 'left',
|
||||
width: 10,
|
||||
},
|
||||
{
|
||||
@@ -91,7 +93,7 @@ export default {
|
||||
title: this.$t(
|
||||
'OVERVIEW_REPORTS.AGENT_CONVERSATIONS.TABLE_HEADER.UNATTENDED'
|
||||
),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
align: this.isRTL ? 'right' : 'left',
|
||||
width: 10,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -4,6 +4,7 @@ import AccountAPI from '../../api/account';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import EnterpriseAccountAPI from '../../api/enterprise/account';
|
||||
import { throwErrorMessage } from '../utils/api';
|
||||
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
|
||||
const findRecordById = ($state, id) =>
|
||||
$state.records.find(record => record.id === Number(id)) || {};
|
||||
@@ -27,6 +28,13 @@ export const getters = {
|
||||
getUIFlags($state) {
|
||||
return $state.uiFlags;
|
||||
},
|
||||
isRTL: ($state, _, rootState) => {
|
||||
const accountId = rootState.route?.params?.accountId;
|
||||
if (!accountId) return false;
|
||||
|
||||
const { locale } = findRecordById($state, Number(accountId));
|
||||
return locale ? getLanguageDirection(locale) : false;
|
||||
},
|
||||
isTrialAccount: $state => id => {
|
||||
const account = findRecordById($state, id);
|
||||
const createdAt = new Date(account.created_at);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getters } from '../../accounts';
|
||||
import * as languageHelpers from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
|
||||
const accountData = {
|
||||
id: 1,
|
||||
@@ -46,4 +47,37 @@ describe('#getters', () => {
|
||||
)(1, 'auto_resolve_conversations')
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
describe('isRTL', () => {
|
||||
it('returns false when accountId is not present', () => {
|
||||
const rootState = { route: { params: {} } };
|
||||
expect(getters.isRTL({}, null, rootState)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for RTL language', () => {
|
||||
const state = {
|
||||
records: [{ id: 1, locale: 'ar' }],
|
||||
};
|
||||
const rootState = { route: { params: { accountId: '1' } } };
|
||||
vi.spyOn(languageHelpers, 'getLanguageDirection').mockReturnValue(true);
|
||||
expect(getters.isRTL(state, null, rootState)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for LTR language', () => {
|
||||
const state = {
|
||||
records: [{ id: 1, locale: 'en' }],
|
||||
};
|
||||
const rootState = { route: { params: { accountId: '1' } } };
|
||||
vi.spyOn(languageHelpers, 'getLanguageDirection').mockReturnValue(false);
|
||||
expect(getters.isRTL(state, null, rootState)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when account is not found', () => {
|
||||
const state = {
|
||||
records: [],
|
||||
};
|
||||
const rootState = { route: { params: { accountId: '1' } } };
|
||||
expect(getters.isRTL(state, null, rootState)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isRTLView() {
|
||||
const { rtl_view: isRTLView } = this.uiSettings;
|
||||
return isRTLView;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateRTLDirectionView(locale) {
|
||||
const isRTLSupported = getLanguageDirection(locale);
|
||||
this.updateUISettings({
|
||||
rtl_view: isRTLSupported,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
|
||||
vi.mock('dashboard/composables/useUISettings');
|
||||
|
||||
describe('rtlMixin', () => {
|
||||
const createComponent = rtl_view => {
|
||||
useUISettings.mockReturnValue({
|
||||
uiSettings: { rtl_view },
|
||||
updateUISettings: vi.fn(),
|
||||
});
|
||||
|
||||
return shallowMount({
|
||||
render() {},
|
||||
mixins: [rtlMixin],
|
||||
});
|
||||
};
|
||||
|
||||
it('returns is direction right-to-left view', () => {
|
||||
const wrapper = createComponent(true);
|
||||
expect(wrapper.vm.isRTLView).toBe(true);
|
||||
});
|
||||
|
||||
it('returns is direction left-to-right view', () => {
|
||||
const wrapper = createComponent(false);
|
||||
expect(wrapper.vm.isRTLView).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,7 @@ import FileBubble from 'widget/components/FileBubble.vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||
import configMixin from '../mixins/configMixin';
|
||||
import messageMixin from '../mixins/messageMixin';
|
||||
import { useMessage } from '../composables/useMessage';
|
||||
import { isASubmittedFormMessage } from 'shared/helpers/MessageTypeHelper';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||
import ReplyToChip from 'widget/components/ReplyToChip.vue';
|
||||
@@ -28,7 +28,7 @@ export default {
|
||||
MessageReplyButton,
|
||||
ReplyToChip,
|
||||
},
|
||||
mixins: [configMixin, messageMixin, darkModeMixin],
|
||||
mixins: [configMixin, darkModeMixin],
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
@@ -39,6 +39,15 @@ export default {
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { messageContentAttributes, hasAttachments } = useMessage(
|
||||
props.message
|
||||
);
|
||||
return {
|
||||
messageContentAttributes,
|
||||
hasAttachments,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasImageError: false,
|
||||
|
||||
@@ -6,7 +6,7 @@ import VideoBubble from 'widget/components/VideoBubble.vue';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import FileBubble from 'widget/components/FileBubble.vue';
|
||||
import { messageStamp } from 'shared/helpers/timeHelper';
|
||||
import messageMixin from '../mixins/messageMixin';
|
||||
import { useMessage } from '../composables/useMessage';
|
||||
import ReplyToChip from 'widget/components/ReplyToChip.vue';
|
||||
import DragWrapper from 'widget/components/DragWrapper.vue';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
@@ -25,7 +25,6 @@ export default {
|
||||
ReplyToChip,
|
||||
DragWrapper,
|
||||
},
|
||||
mixins: [messageMixin],
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
@@ -36,6 +35,12 @@ export default {
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { hasAttachments } = useMessage(props.message);
|
||||
return {
|
||||
hasAttachments,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasImageError: false,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { reactive, nextTick } from 'vue';
|
||||
import { useMessage } from '../useMessage';
|
||||
|
||||
describe('useMessage', () => {
|
||||
it('should handle deleted messages', () => {
|
||||
const message = reactive({
|
||||
content_attributes: { deleted: true },
|
||||
attachments: [],
|
||||
});
|
||||
|
||||
const { messageContentAttributes, hasAttachments } = useMessage(message);
|
||||
|
||||
expect(messageContentAttributes.value).toEqual({ deleted: true });
|
||||
expect(hasAttachments.value).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle non-deleted messages with attachments', () => {
|
||||
const message = reactive({
|
||||
content_attributes: {},
|
||||
attachments: ['attachment1', 'attachment2'],
|
||||
});
|
||||
|
||||
const { messageContentAttributes, hasAttachments } = useMessage(message);
|
||||
|
||||
expect(messageContentAttributes.value).toEqual({});
|
||||
expect(hasAttachments.value).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle messages without content_attributes', () => {
|
||||
const message = reactive({
|
||||
attachments: [],
|
||||
});
|
||||
|
||||
const { messageContentAttributes, hasAttachments } = useMessage(message);
|
||||
|
||||
expect(messageContentAttributes.value).toEqual({});
|
||||
expect(hasAttachments.value).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle messages with empty attachments array', () => {
|
||||
const message = reactive({
|
||||
content_attributes: { someAttribute: 'value' },
|
||||
attachments: [],
|
||||
});
|
||||
|
||||
const { messageContentAttributes, hasAttachments } = useMessage(message);
|
||||
|
||||
expect(messageContentAttributes.value).toEqual({ someAttribute: 'value' });
|
||||
expect(hasAttachments.value).toBe(false);
|
||||
});
|
||||
|
||||
it('should update reactive properties when message changes', async () => {
|
||||
const message = reactive({
|
||||
content_attributes: {},
|
||||
attachments: [],
|
||||
});
|
||||
|
||||
const { messageContentAttributes, hasAttachments } = useMessage(message);
|
||||
|
||||
expect(messageContentAttributes.value).toEqual({});
|
||||
expect(hasAttachments.value).toBe(false);
|
||||
|
||||
message.content_attributes = { updated: true };
|
||||
message.attachments.push('newAttachment');
|
||||
await nextTick();
|
||||
|
||||
expect(messageContentAttributes.value).toEqual({ updated: true });
|
||||
expect(hasAttachments.value).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { computed } from 'vue';
|
||||
|
||||
/**
|
||||
* Composable for handling message-related computations.
|
||||
* @param {Object} message - The message object to be processed.
|
||||
* @returns {Object} An object containing computed properties for message content and attachments.
|
||||
*/
|
||||
export function useMessage(message) {
|
||||
const messageContentAttributes = computed(() => {
|
||||
const { content_attributes: attribute = {} } = message;
|
||||
return attribute;
|
||||
});
|
||||
|
||||
const hasAttachments = computed(() => {
|
||||
return !!(message.attachments && message.attachments.length > 0);
|
||||
});
|
||||
|
||||
return {
|
||||
messageContentAttributes,
|
||||
hasAttachments,
|
||||
};
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export default {
|
||||
computed: {
|
||||
messageContentAttributes() {
|
||||
const { content_attributes: attribute = {} } = this.message;
|
||||
return attribute;
|
||||
},
|
||||
hasAttachments() {
|
||||
return !!(
|
||||
this.message.attachments && this.message.attachments.length > 0
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import messageMixin from '../messageMixin';
|
||||
import messages from './messageFixture';
|
||||
|
||||
describe('messageMixin', () => {
|
||||
let Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [messageMixin],
|
||||
};
|
||||
|
||||
it('deleted messages', async () => {
|
||||
const wrapper = shallowMount(Component, {
|
||||
data() {
|
||||
return {
|
||||
message: messages.deletedMessage,
|
||||
};
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.messageContentAttributes).toEqual({
|
||||
deleted: true,
|
||||
});
|
||||
expect(wrapper.vm.hasAttachments).toBe(false);
|
||||
});
|
||||
it('non-deleted messages', async () => {
|
||||
const wrapper = shallowMount(Component, {
|
||||
data() {
|
||||
return {
|
||||
message: messages.nonDeletedMessage,
|
||||
};
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.deletedMessage).toBe(undefined);
|
||||
expect(wrapper.vm.messageContentAttributes).toEqual({});
|
||||
expect(wrapper.vm.hasAttachments).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user