Merge branch 'develop' into fix/CW-3385
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import configMixin from 'shared/mixins/configMixin';
|
||||
import { useConfig } from 'dashboard/composables/useConfig';
|
||||
import {
|
||||
getInboxClassByType,
|
||||
getInboxWarningIconClass,
|
||||
@@ -16,7 +16,6 @@ import Policy from '../../policy.vue';
|
||||
|
||||
export default {
|
||||
components: { SecondaryChildNavItem, Policy },
|
||||
mixins: [configMixin],
|
||||
props: {
|
||||
menuItem: {
|
||||
type: Object,
|
||||
@@ -25,8 +24,10 @@ export default {
|
||||
},
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
const { isEnterprise } = useConfig();
|
||||
return {
|
||||
isAdmin,
|
||||
isEnterprise,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -116,29 +116,27 @@ export default {
|
||||
setup() {
|
||||
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
|
||||
useUISettings();
|
||||
|
||||
const uploadRef = ref(null);
|
||||
// TODO: This is really hacky, we need to replace the file picker component with
|
||||
// a custom one, where the logic and the component markup is isolated.
|
||||
// Once we have the custom component, we can remove the hacky logic below.
|
||||
const uploadTriggerButton = computed(() => {
|
||||
if (uploadRef.value) {
|
||||
return uploadRef.value.$children[1].$el;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
const uploadRefElem = computed(() => uploadRef.value?.$el);
|
||||
|
||||
const keyboardEvents = {
|
||||
'Alt+KeyA': {
|
||||
action: () => {
|
||||
uploadTriggerButton.value.click();
|
||||
const uploadTriggerButton = document.querySelector(
|
||||
'#conversationAttachment'
|
||||
);
|
||||
uploadTriggerButton.click();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
useKeyboardEvents(keyboardEvents, uploadTriggerButton);
|
||||
useKeyboardEvents(keyboardEvents, uploadRefElem);
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import { ref } from 'vue';
|
||||
// composable
|
||||
import { useConfig } from 'dashboard/composables/useConfig';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
|
||||
// components
|
||||
@@ -14,7 +15,6 @@ import { mapGetters } from 'vuex';
|
||||
|
||||
// mixins
|
||||
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
||||
import configMixin from 'shared/mixins/configMixin';
|
||||
import aiMixin from 'dashboard/mixins/aiMixin';
|
||||
|
||||
// utils
|
||||
@@ -40,7 +40,7 @@ export default {
|
||||
Banner,
|
||||
ConversationLabelSuggestion,
|
||||
},
|
||||
mixins: [inboxMixin, configMixin, aiMixin],
|
||||
mixins: [inboxMixin, aiMixin],
|
||||
props: {
|
||||
isContactPanelOpen: {
|
||||
type: Boolean,
|
||||
@@ -54,6 +54,7 @@ export default {
|
||||
setup() {
|
||||
const conversationFooterRef = ref(null);
|
||||
const isPopOutReplyBox = ref(false);
|
||||
const { isEnterprise } = useConfig();
|
||||
|
||||
const closePopOutReplyBox = () => {
|
||||
isPopOutReplyBox.value = false;
|
||||
@@ -72,6 +73,7 @@ export default {
|
||||
useKeyboardEvents(keyboardEvents, conversationFooterRef);
|
||||
|
||||
return {
|
||||
isEnterprise,
|
||||
conversationFooterRef,
|
||||
isPopOutReplyBox,
|
||||
closePopOutReplyBox,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { hasPressedCommand } from 'shared/helpers/KeyboardHelpers';
|
||||
import GalleryView from '../components/GalleryView.vue';
|
||||
import { timeStampAppendedURL } from 'dashboard/helper/URLHelper';
|
||||
|
||||
const ALLOWED_FILE_TYPES = {
|
||||
IMAGE: 'image',
|
||||
@@ -42,6 +43,9 @@ export default {
|
||||
isAudio() {
|
||||
return this.attachment.file_type === ALLOWED_FILE_TYPES.AUDIO;
|
||||
},
|
||||
timeStampURL() {
|
||||
return timeStampAppendedURL(this.dataUrl);
|
||||
},
|
||||
attachmentTypeClasses() {
|
||||
return {
|
||||
image: this.isImage,
|
||||
@@ -108,7 +112,7 @@ export default {
|
||||
@click="onClick"
|
||||
/>
|
||||
<audio v-else-if="isAudio" controls class="skip-context-menu mb-0.5">
|
||||
<source :src="`${dataUrl}?t=${Date.now()}`" />
|
||||
<source :src="timeStampURL" />
|
||||
</audio>
|
||||
<GalleryView
|
||||
v-if="show"
|
||||
|
||||
+3
-3
@@ -13,8 +13,8 @@ export default {
|
||||
const accountLabels = useMapGetter('labels/getLabels');
|
||||
|
||||
const activeLabels = computed(() => {
|
||||
return props.conversationLabels.map(label =>
|
||||
accountLabels.value.find(l => l.title === label)
|
||||
return accountLabels.value.filter(({ title }) =>
|
||||
props.conversationLabels.includes(title)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -83,7 +83,7 @@ export default {
|
||||
<slot name="before" />
|
||||
<woot-label
|
||||
v-for="(label, index) in activeLabels"
|
||||
:key="label.id"
|
||||
:key="label ? label.id : index"
|
||||
:title="label.title"
|
||||
:description="label.description"
|
||||
:color="label.color"
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useConfig } from '../useConfig';
|
||||
|
||||
describe('useConfig', () => {
|
||||
const originalChatwootConfig = window.chatwootConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
window.chatwootConfig = {
|
||||
hostURL: 'https://example.com',
|
||||
vapidPublicKey: 'vapid-key',
|
||||
enabledLanguages: ['en', 'fr'],
|
||||
isEnterprise: 'true',
|
||||
enterprisePlanName: 'enterprise',
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.chatwootConfig = originalChatwootConfig;
|
||||
});
|
||||
|
||||
it('returns the correct configuration values', () => {
|
||||
const config = useConfig();
|
||||
|
||||
expect(config.hostURL).toBe('https://example.com');
|
||||
expect(config.vapidPublicKey).toBe('vapid-key');
|
||||
expect(config.enabledLanguages).toEqual(['en', 'fr']);
|
||||
expect(config.isEnterprise).toBe(true);
|
||||
expect(config.enterprisePlanName).toBe('enterprise');
|
||||
});
|
||||
|
||||
it('handles missing configuration values', () => {
|
||||
window.chatwootConfig = {};
|
||||
const config = useConfig();
|
||||
|
||||
expect(config.hostURL).toBeUndefined();
|
||||
expect(config.vapidPublicKey).toBeUndefined();
|
||||
expect(config.enabledLanguages).toBeUndefined();
|
||||
expect(config.isEnterprise).toBe(false);
|
||||
expect(config.enterprisePlanName).toBeUndefined();
|
||||
});
|
||||
|
||||
it('handles undefined window.chatwootConfig', () => {
|
||||
window.chatwootConfig = undefined;
|
||||
const config = useConfig();
|
||||
|
||||
expect(config.hostURL).toBeUndefined();
|
||||
expect(config.vapidPublicKey).toBeUndefined();
|
||||
expect(config.enabledLanguages).toBeUndefined();
|
||||
expect(config.isEnterprise).toBe(false);
|
||||
expect(config.enterprisePlanName).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* A function that provides access to various configuration values.
|
||||
* @returns {Object} An object containing configuration values.
|
||||
*/
|
||||
export function useConfig() {
|
||||
const config = window.chatwootConfig || {};
|
||||
|
||||
/**
|
||||
* The host URL of the Chatwoot instance.
|
||||
* @type {string|undefined}
|
||||
*/
|
||||
const hostURL = config.hostURL;
|
||||
|
||||
/**
|
||||
* The VAPID public key for web push notifications.
|
||||
* @type {string|undefined}
|
||||
*/
|
||||
const vapidPublicKey = config.vapidPublicKey;
|
||||
|
||||
/**
|
||||
* An array of enabled languages in the Chatwoot instance.
|
||||
* @type {string[]|undefined}
|
||||
*/
|
||||
const enabledLanguages = config.enabledLanguages;
|
||||
|
||||
/**
|
||||
* Indicates whether the current instance is an enterprise version.
|
||||
* @type {boolean}
|
||||
*/
|
||||
const isEnterprise = config.isEnterprise === 'true';
|
||||
|
||||
/**
|
||||
* The name of the enterprise plan, if applicable.
|
||||
* Returns "community" or "enterprise"
|
||||
* @type {string|undefined}
|
||||
*/
|
||||
const enterprisePlanName = config.enterprisePlanName;
|
||||
|
||||
return {
|
||||
hostURL,
|
||||
vapidPublicKey,
|
||||
enabledLanguages,
|
||||
isEnterprise,
|
||||
enterprisePlanName,
|
||||
};
|
||||
}
|
||||
@@ -108,3 +108,12 @@ export const hasValidAvatarUrl = avatarUrl => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const timeStampAppendedURL = dataUrl => {
|
||||
const url = new URL(dataUrl);
|
||||
if (!url.searchParams.has('t')) {
|
||||
url.searchParams.append('t', Date.now());
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
@@ -47,12 +47,13 @@ export const filterDuplicateSourceMessages = (messages = []) => {
|
||||
* @returns {Object} The last message of the conversation.
|
||||
*/
|
||||
export const getLastMessage = m => {
|
||||
const lastMessageIncludingActivity = m.messages.at(-1);
|
||||
const lastMessageIncludingActivity = m.messages[m.messages.length - 1];
|
||||
|
||||
const nonActivityMessages = m.messages.filter(
|
||||
message => message.message_type !== 2
|
||||
);
|
||||
const lastNonActivityMessageInStore = nonActivityMessages.at(-1);
|
||||
const lastNonActivityMessageInStore =
|
||||
nonActivityMessages[nonActivityMessages.length - 1];
|
||||
|
||||
const lastNonActivityMessageFromAPI = m.last_non_activity_message;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const FEATURE_HELP_URLS = {
|
||||
agent_bots: 'https://chwt.app/hc/agent-bots',
|
||||
agents: 'https://chwt.app/hc/agents',
|
||||
audit_logs: 'https://chwt.app/hc/audit-logs',
|
||||
campaigns: 'https://chwt.app/hc/campaigns',
|
||||
canned_responses: 'https://chwt.app/hc/canned',
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
conversationListPageURL,
|
||||
getArticleSearchURL,
|
||||
hasValidAvatarUrl,
|
||||
timeStampAppendedURL,
|
||||
} from '../URLHelper';
|
||||
|
||||
describe('#URL Helpers', () => {
|
||||
@@ -190,4 +191,51 @@ describe('#URL Helpers', () => {
|
||||
expect(hasValidAvatarUrl()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeStampAppendedURL', () => {
|
||||
const FIXED_TIMESTAMP = 1234567890000;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(Date, 'now').mockImplementation(() => FIXED_TIMESTAMP);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should append timestamp to a URL without query parameters', () => {
|
||||
const input = 'https://example.com/audio.mp3';
|
||||
const expected = `https://example.com/audio.mp3?t=${FIXED_TIMESTAMP}`;
|
||||
expect(timeStampAppendedURL(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should append timestamp to a URL with existing query parameters', () => {
|
||||
const input = 'https://example.com/audio.mp3?volume=50';
|
||||
const expected = `https://example.com/audio.mp3?volume=50&t=${FIXED_TIMESTAMP}`;
|
||||
expect(timeStampAppendedURL(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should not append timestamp if it already exists', () => {
|
||||
const input = 'https://example.com/audio.mp3?t=9876543210';
|
||||
expect(timeStampAppendedURL(input)).toBe(input);
|
||||
});
|
||||
|
||||
it('should handle URLs with hash fragments', () => {
|
||||
const input = 'https://example.com/audio.mp3#section1';
|
||||
const expected = `https://example.com/audio.mp3?t=${FIXED_TIMESTAMP}#section1`;
|
||||
expect(timeStampAppendedURL(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle complex URLs', () => {
|
||||
const input =
|
||||
'https://example.com/path/to/audio.mp3?key1=value1&key2=value2#fragment';
|
||||
const expected = `https://example.com/path/to/audio.mp3?key1=value1&key2=value2&t=${FIXED_TIMESTAMP}#fragment`;
|
||||
expect(timeStampAppendedURL(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should throw an error for invalid URLs', () => {
|
||||
const input = 'not a valid url';
|
||||
expect(() => timeStampAppendedURL(input)).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"HEADER": "Agents",
|
||||
"HEADER_BTN_TXT": "Add Agent",
|
||||
"LOADING": "Fetching Agent List",
|
||||
"SIDEBAR_TXT": "<p><b>Agents</b></p> <p> An <b>Agent</b> is a member of your Customer Support team. </p><p> Agents will be able to view and reply to messages from your users. The list shows all agents currently in your account. </p><p> Click on <b>Add Agent</b> to add a new agent. Agent you add will receive an email with a confirmation link to activate their account, after which they can access Chatwoot and respond to messages. </p><p> Access to Chatwoot's features are based on following roles. </p><p> <b>Agent</b> - Agents with this role can only access inboxes, reports and conversations. They can assign conversations to other agents or themselves and resolve conversations.</p><p> <b>Administrator</b> - Administrator will have access to all Chatwoot features enabled for your account, including settings, along with all of a normal agents' privileges.</p>",
|
||||
"DESCRIPTION": "An agent is a member of your customer support team who can view and respond to user messages. The list below shows all the agents in your account.",
|
||||
"LEARN_MORE": "Learn about user roles",
|
||||
"AGENT_TYPES": {
|
||||
"ADMINISTRATOR": "Administrator",
|
||||
"AGENT": "Agent"
|
||||
@@ -33,7 +34,6 @@
|
||||
"PLACEHOLDER": "Please select a role",
|
||||
"ERROR": "Role is required"
|
||||
},
|
||||
|
||||
"EMAIL": {
|
||||
"LABEL": "Email Address",
|
||||
"PLACEHOLDER": "Please enter an email address of the agent"
|
||||
|
||||
@@ -4,18 +4,18 @@ import { required, minValue, maxValue } from '@vuelidate/validators';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import configMixin from 'shared/mixins/configMixin';
|
||||
import { useConfig } from 'dashboard/composables/useConfig';
|
||||
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||
import semver from 'semver';
|
||||
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
|
||||
export default {
|
||||
mixins: [configMixin],
|
||||
setup() {
|
||||
const { updateUISettings } = useUISettings();
|
||||
const { enabledLanguages } = useConfig();
|
||||
const v$ = useVuelidate();
|
||||
|
||||
return { updateUISettings, v$ };
|
||||
return { updateUISettings, v$, enabledLanguages };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -1,230 +1,208 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
<script setup>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import Thumbnail from '../../../../components/widgets/Thumbnail.vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
|
||||
import AddAgent from './AddAgent.vue';
|
||||
import EditAgent from './EditAgent.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddAgent,
|
||||
EditAgent,
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [globalConfigMixin],
|
||||
data() {
|
||||
return {
|
||||
loading: {},
|
||||
showAddPopup: false,
|
||||
showDeletePopup: false,
|
||||
showEditPopup: false,
|
||||
agentAPI: {
|
||||
message: '',
|
||||
},
|
||||
currentAgent: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
agentList: 'agents/getAgents',
|
||||
uiFlags: 'agents/getUIFlags',
|
||||
currentUserId: 'getCurrentUserID',
|
||||
globalConfig: 'globalConfig/get',
|
||||
}),
|
||||
deleteConfirmText() {
|
||||
return `${this.$t('AGENT_MGMT.DELETE.CONFIRM.YES')} ${
|
||||
this.currentAgent.name
|
||||
}`;
|
||||
},
|
||||
deleteRejectText() {
|
||||
return `${this.$t('AGENT_MGMT.DELETE.CONFIRM.NO')} ${
|
||||
this.currentAgent.name
|
||||
}`;
|
||||
},
|
||||
deleteMessage() {
|
||||
return ` ${this.currentAgent.name}?`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('agents/get');
|
||||
},
|
||||
methods: {
|
||||
showEditAction(agent) {
|
||||
return this.currentUserId !== agent.id;
|
||||
},
|
||||
showDeleteAction(agent) {
|
||||
if (this.currentUserId === agent.id) {
|
||||
return false;
|
||||
}
|
||||
const getters = useStoreGetters();
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
if (!agent.confirmed) {
|
||||
return true;
|
||||
}
|
||||
const loading = ref({});
|
||||
const showAddPopup = ref(false);
|
||||
const showDeletePopup = ref(false);
|
||||
const showEditPopup = ref(false);
|
||||
const agentAPI = ref({ message: '' });
|
||||
const currentAgent = ref({});
|
||||
|
||||
if (agent.role === 'administrator') {
|
||||
return this.verifiedAdministrators().length !== 1;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
verifiedAdministrators() {
|
||||
return this.agentList.filter(
|
||||
agent => agent.role === 'administrator' && agent.confirmed
|
||||
);
|
||||
},
|
||||
// Edit Function
|
||||
openAddPopup() {
|
||||
this.showAddPopup = true;
|
||||
},
|
||||
hideAddPopup() {
|
||||
this.showAddPopup = false;
|
||||
},
|
||||
const deleteConfirmText = computed(
|
||||
() => `${t('AGENT_MGMT.DELETE.CONFIRM.YES')} ${currentAgent.value.name}`
|
||||
);
|
||||
const deleteRejectText = computed(() => {
|
||||
return `${t('AGENT_MGMT.DELETE.CONFIRM.NO')} ${currentAgent.value.name}`;
|
||||
});
|
||||
const deleteMessage = computed(() => {
|
||||
return ` ${currentAgent.value.name}?`;
|
||||
});
|
||||
|
||||
// Edit Function
|
||||
openEditPopup(agent) {
|
||||
this.showEditPopup = true;
|
||||
this.currentAgent = agent;
|
||||
},
|
||||
hideEditPopup() {
|
||||
this.showEditPopup = false;
|
||||
},
|
||||
const agentList = computed(() => getters['agents/getAgents'].value);
|
||||
const uiFlags = computed(() => getters['agents/getUIFlags'].value);
|
||||
const currentUserId = computed(() => getters.getCurrentUserID.value);
|
||||
|
||||
// Delete Function
|
||||
openDeletePopup(agent) {
|
||||
this.showDeletePopup = true;
|
||||
this.currentAgent = agent;
|
||||
},
|
||||
closeDeletePopup() {
|
||||
this.showDeletePopup = false;
|
||||
},
|
||||
confirmDeletion() {
|
||||
this.loading[this.currentAgent.id] = true;
|
||||
this.closeDeletePopup();
|
||||
this.deleteAgent(this.currentAgent.id);
|
||||
},
|
||||
async deleteAgent(id) {
|
||||
try {
|
||||
await this.$store.dispatch('agents/delete', id);
|
||||
this.showAlertMessage(this.$t('AGENT_MGMT.DELETE.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
this.showAlertMessage(this.$t('AGENT_MGMT.DELETE.API.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
// Show SnackBar
|
||||
showAlertMessage(message) {
|
||||
// Reset loading, current selected agent
|
||||
this.loading[this.currentAgent.id] = false;
|
||||
this.currentAgent = {};
|
||||
// Show message
|
||||
this.agentAPI.message = message;
|
||||
useAlert(message);
|
||||
},
|
||||
},
|
||||
onMounted(() => {
|
||||
store.dispatch('agents/get');
|
||||
});
|
||||
|
||||
const verifiedAdministrators = computed(() => {
|
||||
return agentList.value.filter(
|
||||
agent => agent.role === 'administrator' && agent.confirmed
|
||||
);
|
||||
});
|
||||
|
||||
const showEditAction = agent => {
|
||||
return currentUserId.value !== agent.id;
|
||||
};
|
||||
|
||||
const showDeleteAction = agent => {
|
||||
if (currentUserId.value === agent.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!agent.confirmed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (agent.role === 'administrator') {
|
||||
return verifiedAdministrators.value.length !== 1;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const showAlertMessage = message => {
|
||||
loading.value[currentAgent.value.id] = false;
|
||||
currentAgent.value = {};
|
||||
agentAPI.value.message = message;
|
||||
useAlert(message);
|
||||
};
|
||||
|
||||
const openAddPopup = () => {
|
||||
showAddPopup.value = true;
|
||||
};
|
||||
const hideAddPopup = () => {
|
||||
showAddPopup.value = false;
|
||||
};
|
||||
|
||||
const openEditPopup = agent => {
|
||||
showEditPopup.value = true;
|
||||
currentAgent.value = agent;
|
||||
};
|
||||
const hideEditPopup = () => {
|
||||
showEditPopup.value = false;
|
||||
};
|
||||
|
||||
const openDeletePopup = agent => {
|
||||
showDeletePopup.value = true;
|
||||
currentAgent.value = agent;
|
||||
};
|
||||
const closeDeletePopup = () => {
|
||||
showDeletePopup.value = false;
|
||||
};
|
||||
|
||||
const deleteAgent = async id => {
|
||||
try {
|
||||
await store.dispatch('agents/delete', id);
|
||||
showAlertMessage(t('AGENT_MGMT.DELETE.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
showAlertMessage(t('AGENT_MGMT.DELETE.API.ERROR_MESSAGE'));
|
||||
}
|
||||
};
|
||||
const confirmDeletion = () => {
|
||||
loading.value[currentAgent.value.id] = true;
|
||||
closeDeletePopup();
|
||||
deleteAgent(currentAgent.value.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 p-4 overflow-auto">
|
||||
<woot-button
|
||||
color-scheme="success"
|
||||
class-names="button--fixed-top"
|
||||
icon="add-circle"
|
||||
@click="openAddPopup()"
|
||||
>
|
||||
{{ $t('AGENT_MGMT.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
|
||||
<!-- List Agents -->
|
||||
<div class="flex flex-row gap-4">
|
||||
<div class="w-full lg:w-3/5">
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.isFetching"
|
||||
:message="$t('AGENT_MGMT.LOADING')"
|
||||
/>
|
||||
<div v-else>
|
||||
<p v-if="!agentList.length">
|
||||
{{ $t('AGENT_MGMT.LIST.404') }}
|
||||
</p>
|
||||
<table v-else class="woot-table">
|
||||
<tbody>
|
||||
<tr v-for="(agent, index) in agentList" :key="agent.email">
|
||||
<!-- Gravtar Image -->
|
||||
<td>
|
||||
<Thumbnail
|
||||
:src="agent.thumbnail"
|
||||
:username="agent.name"
|
||||
size="40px"
|
||||
:status="agent.availability_status"
|
||||
/>
|
||||
</td>
|
||||
<!-- Agent Name + Email -->
|
||||
<td>
|
||||
<span class="agent-name">
|
||||
<SettingsLayout
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:loading-message="$t('AGENT_MGMT.LOADING')"
|
||||
:no-records-found="!agentList.length"
|
||||
:no-records-message="$t('AGENT_MGMT.LIST.404')"
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
:title="$t('AGENT_MGMT.HEADER')"
|
||||
:description="$t('AGENT_MGMT.DESCRIPTION')"
|
||||
:link-text="$t('AGENT_MGMT.LEARN_MORE')"
|
||||
feature-name="agents"
|
||||
>
|
||||
<template #actions>
|
||||
<woot-button
|
||||
class="button nice rounded-md"
|
||||
icon="add-circle"
|
||||
@click="openAddPopup"
|
||||
>
|
||||
{{ $t('AGENT_MGMT.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<table class="divide-y divide-slate-75 dark:divide-slate-700">
|
||||
<tbody
|
||||
class="divide-y divide-slate-50 dark:divide-slate-800 text-slate-700 dark:text-slate-300"
|
||||
>
|
||||
<tr v-for="(agent, index) in agentList" :key="agent.email">
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
<div class="flex items-center flex-row gap-4">
|
||||
<Thumbnail
|
||||
:src="agent.thumbnail"
|
||||
:username="agent.name"
|
||||
size="40px"
|
||||
:status="agent.availability_status"
|
||||
/>
|
||||
<div>
|
||||
<span class="block font-medium capitalize">
|
||||
{{ agent.name }}
|
||||
</span>
|
||||
<span>{{ agent.email }}</span>
|
||||
</td>
|
||||
<!-- Agent Role + Verification Status -->
|
||||
<td>
|
||||
<span class="agent-name">
|
||||
{{
|
||||
$t(`AGENT_MGMT.AGENT_TYPES.${agent.role.toUpperCase()}`)
|
||||
}}
|
||||
</span>
|
||||
<span v-if="agent.confirmed">
|
||||
{{ $t('AGENT_MGMT.LIST.VERIFIED') }}
|
||||
</span>
|
||||
<span v-if="!agent.confirmed">
|
||||
{{ $t('AGENT_MGMT.LIST.VERIFICATION_PENDING') }}
|
||||
</span>
|
||||
</td>
|
||||
<!-- Actions -->
|
||||
<td>
|
||||
<div class="button-wrapper">
|
||||
<woot-button
|
||||
v-if="showEditAction(agent)"
|
||||
v-tooltip.top="$t('AGENT_MGMT.EDIT.BUTTON_TEXT')"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
icon="edit"
|
||||
class-names="grey-btn"
|
||||
@click="openEditPopup(agent)"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="showDeleteAction(agent)"
|
||||
v-tooltip.top="$t('AGENT_MGMT.DELETE.BUTTON_TEXT')"
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
size="tiny"
|
||||
icon="dismiss-circle"
|
||||
class-names="grey-btn"
|
||||
:is-loading="loading[agent.id]"
|
||||
@click="openDeletePopup(agent, index)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden w-1/3 lg:block">
|
||||
<span
|
||||
v-dompurify-html="
|
||||
useInstallationName(
|
||||
$t('AGENT_MGMT.SIDEBAR_TXT'),
|
||||
globalConfig.installationName
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add Agent -->
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
<span class="block font-medium capitalize">
|
||||
{{ $t(`AGENT_MGMT.AGENT_TYPES.${agent.role.toUpperCase()}`) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
<span v-if="agent.confirmed">
|
||||
{{ $t('AGENT_MGMT.LIST.VERIFIED') }}
|
||||
</span>
|
||||
<span v-if="!agent.confirmed">
|
||||
{{ $t('AGENT_MGMT.LIST.VERIFICATION_PENDING') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-4">
|
||||
<div class="flex justify-end gap-1">
|
||||
<woot-button
|
||||
v-if="showEditAction(agent)"
|
||||
v-tooltip.top="$t('AGENT_MGMT.EDIT.BUTTON_TEXT')"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
icon="edit"
|
||||
class-names="grey-btn"
|
||||
@click="openEditPopup(agent)"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="showDeleteAction(agent)"
|
||||
v-tooltip.top="$t('AGENT_MGMT.DELETE.BUTTON_TEXT')"
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
size="tiny"
|
||||
icon="dismiss-circle"
|
||||
class-names="grey-btn"
|
||||
:is-loading="loading[agent.id]"
|
||||
@click="openDeletePopup(agent, index)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
|
||||
<AddAgent :on-close="hideAddPopup" />
|
||||
</woot-modal>
|
||||
<!-- Edit Agent -->
|
||||
|
||||
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
|
||||
<EditAgent
|
||||
v-if="showEditPopup"
|
||||
@@ -236,7 +214,7 @@ export default {
|
||||
:on-close="hideEditPopup"
|
||||
/>
|
||||
</woot-modal>
|
||||
<!-- Delete Agent -->
|
||||
|
||||
<woot-delete-modal
|
||||
:show.sync="showDeletePopup"
|
||||
:on-close="closeDeletePopup"
|
||||
@@ -247,5 +225,5 @@ export default {
|
||||
:confirm-text="deleteConfirmText"
|
||||
:reject-text="deleteRejectText"
|
||||
/>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
const SettingsContent = () => import('../Wrapper.vue');
|
||||
const SettingsWrapper = () => import('../SettingsWrapper.vue');
|
||||
const AgentHome = () => import('./Index.vue');
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/agents'),
|
||||
component: SettingsContent,
|
||||
component: SettingsWrapper,
|
||||
props: {
|
||||
headerTitle: 'AGENT_MGMT.HEADER',
|
||||
icon: 'people',
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<script>
|
||||
import configMixin from 'shared/mixins/configMixin';
|
||||
import EmptyState from '../../../../components/widgets/EmptyState.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmptyState,
|
||||
},
|
||||
mixins: [configMixin],
|
||||
computed: {
|
||||
currentInbox() {
|
||||
return this.$store.getters['inboxes/getInbox'](
|
||||
@@ -94,11 +92,11 @@ export default {
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isWhatsAppCloudInbox" class="w-[50%] max-w-[50%] ml-[25%]">
|
||||
<p class="text-slate-700 dark:text-slate-200 font-medium mt-8">
|
||||
<p class="mt-8 font-medium text-slate-700 dark:text-slate-200">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.API_CALLBACK.WEBHOOK_URL') }}
|
||||
</p>
|
||||
<woot-code lang="html" :script="currentInbox.callback_webhook_url" />
|
||||
<p class="text-slate-700 dark:text-slate-200 font-medium mt-8">
|
||||
<p class="mt-8 font-medium text-slate-700 dark:text-slate-200">
|
||||
{{
|
||||
$t(
|
||||
'INBOX_MGMT.ADD.WHATSAPP.API_CALLBACK.WEBHOOK_VERIFICATION_TOKEN'
|
||||
@@ -132,7 +130,7 @@ export default {
|
||||
</div>
|
||||
<div class="flex justify-center gap-2 mt-4">
|
||||
<router-link
|
||||
class="button hollow primary rounded"
|
||||
class="rounded button hollow primary"
|
||||
:to="{
|
||||
name: 'settings_inbox_show',
|
||||
params: { inboxId: $route.params.inbox_id },
|
||||
@@ -141,7 +139,7 @@ export default {
|
||||
{{ $t('INBOX_MGMT.FINISH.MORE_SETTINGS') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
class="button success rounded"
|
||||
class="rounded button success"
|
||||
:to="{
|
||||
name: 'inbox_dashboard',
|
||||
params: { inboxId: $route.params.inbox_id },
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { shouldBeUrl } from 'shared/helpers/Validators';
|
||||
import configMixin from 'shared/mixins/configMixin';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import SettingIntroBanner from 'dashboard/components/widgets/SettingIntroBanner.vue';
|
||||
@@ -34,7 +33,7 @@ export default {
|
||||
SenderNameExamplePreview,
|
||||
MicrosoftReauthorize,
|
||||
},
|
||||
mixins: [configMixin, inboxMixin],
|
||||
mixins: [inboxMixin],
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
|
||||
+4
-3
@@ -3,14 +3,13 @@ import { mapGetters } from 'vuex';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { minValue } from '@vuelidate/validators';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import configMixin from 'shared/mixins/configMixin';
|
||||
import { useConfig } from 'dashboard/composables/useConfig';
|
||||
import SettingsSection from '../../../../../components/SettingsSection.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SettingsSection,
|
||||
},
|
||||
mixins: [configMixin],
|
||||
props: {
|
||||
inbox: {
|
||||
type: Object,
|
||||
@@ -18,7 +17,9 @@ export default {
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
const { isEnterprise } = useConfig();
|
||||
|
||||
return { v$: useVuelidate(), isEnterprise };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import configMixin from 'shared/mixins/configMixin';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import AudioAlertTone from './AudioAlertTone.vue';
|
||||
import AudioAlertEvent from './AudioAlertEvent.vue';
|
||||
@@ -13,7 +12,6 @@ export default {
|
||||
AudioAlertTone,
|
||||
AudioAlertCondition,
|
||||
},
|
||||
mixins: [configMixin],
|
||||
setup() {
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import configMixin from 'shared/mixins/configMixin';
|
||||
import TableHeaderCell from 'dashboard/components/widgets/TableHeaderCell.vue';
|
||||
import CheckBox from 'v3/components/Form/CheckBox.vue';
|
||||
import {
|
||||
@@ -19,7 +18,6 @@ export default {
|
||||
FormSwitch,
|
||||
CheckBox,
|
||||
},
|
||||
mixins: [configMixin],
|
||||
data() {
|
||||
return {
|
||||
selectedEmailFlags: [],
|
||||
|
||||
@@ -10,7 +10,6 @@ import SLAPaywallEnterprise from './components/SLAPaywallEnterprise.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { convertSecondsToTimeUnit } from '@chatwoot/utils';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import configMixin from 'shared/mixins/configMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -22,7 +21,6 @@ export default {
|
||||
SLAListItemLoading,
|
||||
SLAPaywallEnterprise,
|
||||
},
|
||||
mixins: [configMixin],
|
||||
data() {
|
||||
return {
|
||||
loading: {},
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
export default {
|
||||
computed: {
|
||||
hostURL() {
|
||||
return window.chatwootConfig.hostURL;
|
||||
},
|
||||
vapidPublicKey() {
|
||||
return window.chatwootConfig.vapidPublicKey;
|
||||
},
|
||||
enabledLanguages() {
|
||||
return window.chatwootConfig.enabledLanguages;
|
||||
},
|
||||
isEnterprise() {
|
||||
return window.chatwootConfig.isEnterprise === 'true';
|
||||
},
|
||||
enterprisePlanName() {
|
||||
// returns "community" or "enterprise"
|
||||
return window.chatwootConfig?.enterprisePlanName;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -5,7 +5,6 @@ import Spinner from 'shared/components/Spinner.vue';
|
||||
import Rating from 'survey/components/Rating.vue';
|
||||
import Feedback from 'survey/components/Feedback.vue';
|
||||
import Banner from 'survey/components/Banner.vue';
|
||||
import configMixin from 'shared/mixins/configMixin';
|
||||
import { getSurveyDetails, updateSurvey } from 'survey/api/survey';
|
||||
|
||||
export default {
|
||||
@@ -17,8 +16,6 @@ export default {
|
||||
Banner,
|
||||
Feedback,
|
||||
},
|
||||
mixins: [configMixin],
|
||||
|
||||
data() {
|
||||
return {
|
||||
surveyDetails: null,
|
||||
|
||||
Reference in New Issue
Block a user