chore: Remove older UI (#11720)
This commit is contained in:
@@ -31,6 +31,7 @@ import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCust
|
||||
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
|
||||
import IntersectionObserver from './IntersectionObserver.vue';
|
||||
import TeleportWithDirection from 'dashboard/components-next/TeleportWithDirection.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
@@ -953,12 +954,12 @@ watch(conversationFilters, (newVal, oldVal) => {
|
||||
</DynamicScrollerItem>
|
||||
</template>
|
||||
<template #after>
|
||||
<div v-if="chatListLoading" class="text-center">
|
||||
<span class="mt-4 mb-4 spinner" />
|
||||
<div v-if="chatListLoading" class="flex justify-center my-4">
|
||||
<Spinner class="text-n-brand" />
|
||||
</div>
|
||||
<p
|
||||
v-else-if="showEndOfListMessage"
|
||||
class="p-4 text-center text-slate-400 dark:text-slate-300"
|
||||
class="p-4 text-center text-n-slate-11"
|
||||
>
|
||||
{{ $t('CHAT_LIST.EOF') }}
|
||||
</p>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||
import { formatNumber } from '@chatwoot/utils';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
|
||||
import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter.vue';
|
||||
import SwitchLayout from 'dashboard/routes/dashboard/conversation/search/SwitchLayout.vue';
|
||||
@@ -30,11 +28,6 @@ const emit = defineEmits([
|
||||
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
const currentAccountId = useMapGetter('getCurrentAccountId');
|
||||
const isFeatureEnabledonAccount = useMapGetter(
|
||||
'accounts/isFeatureEnabledonAccount'
|
||||
);
|
||||
|
||||
const onBasicFilterChange = (value, type) => {
|
||||
emit('basicFilterChange', value, type);
|
||||
};
|
||||
@@ -43,13 +36,6 @@ const hasAppliedFiltersOrActiveFolders = computed(() => {
|
||||
return props.hasAppliedFilters || props.hasActiveFolders;
|
||||
});
|
||||
|
||||
const showV4View = computed(() => {
|
||||
return isFeatureEnabledonAccount.value(
|
||||
currentAccountId.value,
|
||||
FEATURE_FLAGS.CHATWOOT_V4
|
||||
);
|
||||
});
|
||||
|
||||
const allCount = computed(() => props.conversationStats?.allCount || 0);
|
||||
const formattedAllCount = computed(() => formatNumber(allCount.value));
|
||||
|
||||
@@ -174,7 +160,6 @@ const toggleConversationLayout = () => {
|
||||
@change-filter="onBasicFilterChange"
|
||||
/>
|
||||
<SwitchLayout
|
||||
v-if="showV4View"
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
@toggle="toggleConversationLayout"
|
||||
/>
|
||||
|
||||
@@ -14,13 +14,13 @@ defineProps({
|
||||
<template>
|
||||
<div class="flex flex-col items-start w-full gap-6">
|
||||
<div class="flex flex-col w-full gap-4">
|
||||
<h4 v-if="title" class="text-lg font-medium text-ash-900">
|
||||
<h4 v-if="title" class="text-lg font-medium text-n-slate-12">
|
||||
{{ title }}
|
||||
</h4>
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<div class="flex-grow h-px bg-ash-200" />
|
||||
<div class="flex-grow h-px bg-n-weak" />
|
||||
</div>
|
||||
<p v-if="description" class="mb-0 text-sm font-normal text-ash-900">
|
||||
<p v-if="description" class="mb-0 text-sm font-normal text-n-slate-12">
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -28,19 +28,19 @@ export default {
|
||||
<img v-if="headerImage" :src="headerImage" alt="No image" />
|
||||
<h2
|
||||
data-test-id="modal-header-title"
|
||||
class="text-base font-semibold leading-6 text-slate-800 dark:text-slate-50"
|
||||
class="text-base font-semibold leading-6 text-n-slate-12"
|
||||
>
|
||||
{{ headerTitle }}
|
||||
</h2>
|
||||
<p
|
||||
v-if="headerContent"
|
||||
data-test-id="modal-header-content"
|
||||
class="w-full mt-2 text-sm leading-5 break-words text-slate-600 dark:text-slate-300"
|
||||
class="w-full mt-2 text-sm leading-5 break-words text-n-slate-11"
|
||||
>
|
||||
{{ headerContent }}
|
||||
<span
|
||||
v-if="headerContentValue"
|
||||
class="text-sm font-semibold text-slate-600 dark:text-slate-300"
|
||||
class="text-sm font-semibold text-n-slate-11"
|
||||
>
|
||||
{{ headerContentValue }}
|
||||
</span>
|
||||
|
||||
@@ -30,15 +30,10 @@ export default {
|
||||
>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-8 gap-6">
|
||||
<div class="col-span-2">
|
||||
<p
|
||||
v-if="title"
|
||||
class="text-base text-woot-500 dark:text-woot-500 mb-0 font-medium"
|
||||
>
|
||||
<p v-if="title" class="text-base text-n-brand mb-0 font-medium">
|
||||
{{ title }}
|
||||
</p>
|
||||
<p
|
||||
class="text-sm mb-2 text-slate-700 dark:text-slate-300 leading-5 tracking-normal mt-2"
|
||||
>
|
||||
<p class="text-sm mb-2 text-n-slate-11 leading-5 tracking-normal mt-2">
|
||||
<slot v-if="subTitle" name="subTitle">
|
||||
{{ subTitle }}
|
||||
</slot>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<script>
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
size: {
|
||||
type: String,
|
||||
default: 'sm',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
}),
|
||||
hasNextSidebar() {
|
||||
return this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
FEATURE_FLAGS.CHATWOOT_V4
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onMenuItemClick() {
|
||||
emitter.emit(BUS_EVENTS.TOGGLE_SIDEMENU);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||
<template>
|
||||
<NextButton
|
||||
v-if="!hasNextSidebar"
|
||||
ghost
|
||||
slate
|
||||
:size="size"
|
||||
icon="i-lucide-menu"
|
||||
class="-ml-3"
|
||||
@click="onMenuItemClick"
|
||||
/>
|
||||
</template>
|
||||
@@ -178,7 +178,7 @@ useEmitter(CMD_RESOLVE_CONVERSATION, onCmdResolveConversation);
|
||||
<div
|
||||
v-if="showActionsDropdown"
|
||||
v-on-clickaway="closeDropdown"
|
||||
class="dropdown-pane dropdown-pane--open left-auto top-full mt-0.5 start-0 xl:start-auto xl:end-0 max-w-[12.5rem] min-w-[9.75rem]"
|
||||
class="border rounded-lg shadow-lg border-n-strong dark:border-n-strong box-content p-2 w-fit z-10 bg-n-alpha-3 backdrop-blur-[100px] absolute block left-auto top-full mt-0.5 start-0 xl:start-auto xl:end-0 max-w-[12.5rem] min-w-[9.75rem] [&_ul>li]:mb-0"
|
||||
>
|
||||
<WootDropdownMenu class="mb-0">
|
||||
<WootDropdownItem v-if="!isPending">
|
||||
@@ -209,11 +209,3 @@ useEmitter(CMD_RESOLVE_CONVERSATION, onCmdResolveConversation);
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dropdown-pane {
|
||||
.dropdown-menu__item {
|
||||
@apply mb-0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
defineProps({
|
||||
conversationInboxType: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import Label from './ui/Label.vue';
|
||||
import LoadingState from './widgets/LoadingState.vue';
|
||||
import ModalHeader from './ModalHeader.vue';
|
||||
import Modal from './Modal.vue';
|
||||
import SidemenuIcon from './SidemenuIcon.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import Tabs from './ui/Tabs/Tabs.vue';
|
||||
import TabsItem from './ui/Tabs/TabsItem.vue';
|
||||
@@ -38,7 +37,6 @@ const WootUIKit = {
|
||||
LoadingState,
|
||||
Modal,
|
||||
ModalHeader,
|
||||
SidemenuIcon,
|
||||
Spinner,
|
||||
Tabs,
|
||||
TabsItem,
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useImpersonation } from 'dashboard/composables/useImpersonation';
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
|
||||
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider.vue';
|
||||
import AvailabilityStatusBadge from '../widgets/conversation/AvailabilityStatusBadge.vue';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const { AVAILABILITY_STATUS_KEYS } = wootConstants;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootDropdownHeader,
|
||||
WootDropdownDivider,
|
||||
WootDropdownMenu,
|
||||
WootDropdownItem,
|
||||
AvailabilityStatusBadge,
|
||||
NextButton,
|
||||
},
|
||||
setup() {
|
||||
const { isImpersonating } = useImpersonation();
|
||||
return { isImpersonating };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isStatusMenuOpened: false,
|
||||
isUpdating: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getCurrentUserAvailability: 'getCurrentUserAvailability',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
currentUserAutoOffline: 'getCurrentUserAutoOffline',
|
||||
}),
|
||||
statusList() {
|
||||
return [
|
||||
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUS.ONLINE'),
|
||||
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUS.BUSY'),
|
||||
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUS.OFFLINE'),
|
||||
];
|
||||
},
|
||||
availabilityDisplayLabel() {
|
||||
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
|
||||
key => key === this.currentUserAvailability
|
||||
);
|
||||
return this.statusList[availabilityIndex];
|
||||
},
|
||||
currentUserAvailability() {
|
||||
return this.getCurrentUserAvailability;
|
||||
},
|
||||
availabilityStatuses() {
|
||||
return this.statusList.map((statusLabel, index) => ({
|
||||
label: statusLabel,
|
||||
value: AVAILABILITY_STATUS_KEYS[index],
|
||||
disabled:
|
||||
this.currentUserAvailability === AVAILABILITY_STATUS_KEYS[index],
|
||||
}));
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
openStatusMenu() {
|
||||
this.isStatusMenuOpened = true;
|
||||
},
|
||||
closeStatusMenu() {
|
||||
this.isStatusMenuOpened = false;
|
||||
},
|
||||
updateAutoOffline(autoOffline) {
|
||||
this.$store.dispatch('updateAutoOffline', {
|
||||
accountId: this.currentAccountId,
|
||||
autoOffline,
|
||||
});
|
||||
},
|
||||
changeAvailabilityStatus(availability) {
|
||||
if (this.isImpersonating) {
|
||||
useAlert(
|
||||
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.IMPERSONATING_ERROR')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isUpdating) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isUpdating = true;
|
||||
try {
|
||||
this.$store.dispatch('updateAvailability', {
|
||||
availability,
|
||||
account_id: this.currentAccountId,
|
||||
});
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.SET_AVAILABILITY_ERROR')
|
||||
);
|
||||
} finally {
|
||||
this.isUpdating = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WootDropdownMenu>
|
||||
<WootDropdownHeader :title="$t('SIDEBAR.SET_AVAILABILITY_TITLE')" />
|
||||
<WootDropdownItem
|
||||
v-for="status in availabilityStatuses"
|
||||
:key="status.value"
|
||||
class="flex items-baseline"
|
||||
>
|
||||
<NextButton
|
||||
sm
|
||||
:color="status.disabled ? 'blue' : 'slate'"
|
||||
:variant="status.disabled ? 'faded' : 'ghost'"
|
||||
class="status-change--dropdown-button !w-full !justify-start"
|
||||
@click="changeAvailabilityStatus(status.value)"
|
||||
>
|
||||
<AvailabilityStatusBadge :status="status.value" />
|
||||
<span class="min-w-0 truncate font-medium text-xs">
|
||||
{{ status.label }}
|
||||
</span>
|
||||
</NextButton>
|
||||
</WootDropdownItem>
|
||||
<WootDropdownDivider />
|
||||
<WootDropdownItem class="flex items-center justify-between px-3 py-2 m-0">
|
||||
<div class="flex items-center">
|
||||
<fluent-icon
|
||||
v-tooltip.right-start="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_TEXT')"
|
||||
icon="info"
|
||||
size="14"
|
||||
class="mt-px"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="mx-2 my-0 text-xs font-medium text-slate-600 dark:text-slate-100"
|
||||
>
|
||||
{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<woot-switch
|
||||
size="small"
|
||||
class="mx-1 mt-px mb-0"
|
||||
:model-value="currentUserAutoOffline"
|
||||
@input="updateAutoOffline"
|
||||
/>
|
||||
</WootDropdownItem>
|
||||
<WootDropdownDivider />
|
||||
</WootDropdownMenu>
|
||||
</template>
|
||||
@@ -1,246 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getSidebarItems } from './config/default-sidebar';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import PrimarySidebar from './sidebarComponents/Primary.vue';
|
||||
import SecondarySidebar from './sidebarComponents/Secondary.vue';
|
||||
import { routesWithPermissions } from '../../routes';
|
||||
import {
|
||||
getUserPermissions,
|
||||
hasPermissions,
|
||||
} from '../../helper/permissionsHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PrimarySidebar,
|
||||
SecondarySidebar,
|
||||
},
|
||||
props: {
|
||||
showSecondarySidebar: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'toggleAccountModal',
|
||||
'showAddLabelPopup',
|
||||
'openNotificationPanel',
|
||||
'closeKeyShortcutModal',
|
||||
'openKeyShortcutModal',
|
||||
],
|
||||
setup(props, { emit }) {
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { accountId } = useAccount();
|
||||
|
||||
const toggleKeyShortcutModal = () => {
|
||||
emit('openKeyShortcutModal');
|
||||
};
|
||||
const closeKeyShortcutModal = () => {
|
||||
emit('closeKeyShortcutModal');
|
||||
};
|
||||
const isCurrentRouteSameAsNavigation = routeName => {
|
||||
return route.name === routeName;
|
||||
};
|
||||
const navigateToRoute = routeName => {
|
||||
if (!isCurrentRouteSameAsNavigation(routeName)) {
|
||||
router.push({ name: routeName });
|
||||
}
|
||||
};
|
||||
const keyboardEvents = {
|
||||
'$mod+Slash': {
|
||||
action: toggleKeyShortcutModal,
|
||||
},
|
||||
'$mod+Escape': {
|
||||
action: closeKeyShortcutModal,
|
||||
},
|
||||
'Alt+KeyC': {
|
||||
action: () => navigateToRoute('home'),
|
||||
},
|
||||
'Alt+KeyV': {
|
||||
action: () => navigateToRoute('contacts_dashboard'),
|
||||
},
|
||||
'Alt+KeyR': {
|
||||
action: () => navigateToRoute('account_overview_reports'),
|
||||
},
|
||||
'Alt+KeyS': {
|
||||
action: () => navigateToRoute('agent_list'),
|
||||
},
|
||||
};
|
||||
useKeyboardEvents(keyboardEvents);
|
||||
|
||||
return {
|
||||
toggleKeyShortcutModal,
|
||||
accountId,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showOptionsMenu: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
globalConfig: 'globalConfig/get',
|
||||
inboxes: 'inboxes/getInboxes',
|
||||
isACustomBrandedInstance: 'globalConfig/isACustomBrandedInstance',
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
|
||||
labels: 'labels/getLabelsOnSidebar',
|
||||
teams: 'teams/getMyTeams',
|
||||
}),
|
||||
activeCustomView() {
|
||||
if (this.activePrimaryMenu.key === 'contacts') {
|
||||
return 'contact';
|
||||
}
|
||||
if (this.activePrimaryMenu.key === 'conversations') {
|
||||
return 'conversation';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
customViews() {
|
||||
if (!this.activeCustomView) {
|
||||
return [];
|
||||
}
|
||||
return this.$store.getters['customViews/getCustomViewsByFilterType'](
|
||||
this.activeCustomView
|
||||
);
|
||||
},
|
||||
isConversationOrContactActive() {
|
||||
return (
|
||||
this.activePrimaryMenu.key === 'contacts' ||
|
||||
this.activePrimaryMenu.key === 'conversations'
|
||||
);
|
||||
},
|
||||
sideMenuConfig() {
|
||||
return getSidebarItems(this.accountId);
|
||||
},
|
||||
primaryMenuItems() {
|
||||
const userPermissions = getUserPermissions(
|
||||
this.currentUser,
|
||||
this.accountId
|
||||
);
|
||||
const menuItems = this.sideMenuConfig.primaryMenu;
|
||||
return menuItems.filter(menuItem => {
|
||||
const isAvailableForTheUser = hasPermissions(
|
||||
routesWithPermissions[menuItem.toStateName],
|
||||
userPermissions
|
||||
);
|
||||
|
||||
if (!isAvailableForTheUser) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
menuItem.alwaysVisibleOnChatwootInstances &&
|
||||
!this.isACustomBrandedInstance
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (menuItem.featureFlag) {
|
||||
return this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
menuItem.featureFlag
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
activeSecondaryMenu() {
|
||||
const { secondaryMenu } = this.sideMenuConfig;
|
||||
const { name: currentRoute } = this.$route;
|
||||
|
||||
const activeSecondaryMenu =
|
||||
secondaryMenu.find(menuItem =>
|
||||
menuItem.routes.includes(currentRoute)
|
||||
) || {};
|
||||
return activeSecondaryMenu;
|
||||
},
|
||||
activePrimaryMenu() {
|
||||
const activePrimaryMenu =
|
||||
this.primaryMenuItems.find(
|
||||
menuItem => menuItem.key === this.activeSecondaryMenu.parentNav
|
||||
) || {};
|
||||
return activePrimaryMenu;
|
||||
},
|
||||
hasSecondaryMenu() {
|
||||
return (
|
||||
this.activeSecondaryMenu.menuItems &&
|
||||
this.activeSecondaryMenu.menuItems.length
|
||||
);
|
||||
},
|
||||
hasSecondarySidebar() {
|
||||
// if it is explicitly stated to show and it has secondary menu items to show
|
||||
// showSecondarySidebar corresponds to the UI settings, indicating if the user has toggled it
|
||||
return this.showSecondarySidebar && this.hasSecondaryMenu;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
activeCustomView() {
|
||||
this.fetchCustomViews();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('labels/get');
|
||||
this.$store.dispatch('inboxes/get');
|
||||
this.$store.dispatch('notifications/unReadCount');
|
||||
this.$store.dispatch('teams/get');
|
||||
this.$store.dispatch('attributes/get');
|
||||
this.fetchCustomViews();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchCustomViews() {
|
||||
if (this.isConversationOrContactActive) {
|
||||
this.$store.dispatch('customViews/get', this.activeCustomView);
|
||||
}
|
||||
},
|
||||
toggleSupportChatWindow() {
|
||||
window.$chatwoot.toggle();
|
||||
},
|
||||
toggleAccountModal() {
|
||||
this.$emit('toggleAccountModal');
|
||||
},
|
||||
showAddLabelPopup() {
|
||||
this.$emit('showAddLabelPopup');
|
||||
},
|
||||
openNotificationPanel() {
|
||||
this.$emit('openNotificationPanel');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="flex h-full">
|
||||
<PrimarySidebar
|
||||
:logo-source="globalConfig.logoThumbnail"
|
||||
:installation-name="globalConfig.installationName"
|
||||
:is-a-custom-branded-instance="isACustomBrandedInstance"
|
||||
:account-id="accountId"
|
||||
:menu-items="primaryMenuItems"
|
||||
:active-menu-item="activePrimaryMenu.key"
|
||||
@toggle-accounts="toggleAccountModal"
|
||||
@open-key-shortcut-modal="toggleKeyShortcutModal"
|
||||
@open-notification-panel="openNotificationPanel"
|
||||
/>
|
||||
<SecondarySidebar
|
||||
v-if="hasSecondarySidebar"
|
||||
:account-id="accountId"
|
||||
:inboxes="inboxes"
|
||||
:labels="labels"
|
||||
:teams="teams"
|
||||
:custom-views="customViews"
|
||||
:menu-config="activeSecondaryMenu"
|
||||
:current-user="currentUser"
|
||||
:is-on-chatwoot-cloud="isOnChatwootCloud"
|
||||
@add-label="showAddLabelPopup"
|
||||
@toggle-accounts="toggleAccountModal"
|
||||
/>
|
||||
</aside>
|
||||
</template>
|
||||
@@ -1,19 +0,0 @@
|
||||
import conversations from './sidebarItems/conversations';
|
||||
import contacts from './sidebarItems/contacts';
|
||||
import reports from './sidebarItems/reports';
|
||||
import campaigns from './sidebarItems/campaigns';
|
||||
import settings from './sidebarItems/settings';
|
||||
import notifications from './sidebarItems/notifications';
|
||||
import primaryMenu from './sidebarItems/primaryMenu';
|
||||
|
||||
export const getSidebarItems = accountId => ({
|
||||
primaryMenu: primaryMenu(accountId),
|
||||
secondaryMenu: [
|
||||
conversations(accountId),
|
||||
contacts(accountId),
|
||||
reports(accountId),
|
||||
campaigns(accountId),
|
||||
settings(accountId),
|
||||
notifications(accountId),
|
||||
],
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
const campaigns = accountId => ({
|
||||
parentNav: 'campaigns',
|
||||
routes: ['campaigns_sms_index', 'campaigns_livechat_index'],
|
||||
menuItems: [
|
||||
{
|
||||
icon: 'arrow-swap',
|
||||
label: 'LIVE_CHAT',
|
||||
key: 'ongoingCampaigns',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/campaigns/live_chat`),
|
||||
toStateName: 'campaigns_livechat_index',
|
||||
},
|
||||
{
|
||||
key: 'oneOffCampaigns',
|
||||
icon: 'sound-source',
|
||||
label: 'SMS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/campaigns/sms`),
|
||||
toStateName: 'campaigns_sms_index',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default campaigns;
|
||||
@@ -1,32 +0,0 @@
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
const contacts = accountId => ({
|
||||
parentNav: 'contacts',
|
||||
routes: [
|
||||
'contacts_dashboard_index',
|
||||
'contacts_dashboard_segments_index',
|
||||
'contacts_dashboard_labels_index',
|
||||
'contacts_edit',
|
||||
'contacts_edit_segment',
|
||||
'contacts_edit_label',
|
||||
'contacts_dashboard_active',
|
||||
],
|
||||
menuItems: [
|
||||
{
|
||||
icon: 'contact-card-group',
|
||||
label: 'ALL_CONTACTS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/contacts?page=1`),
|
||||
toStateName: 'contacts_dashboard_index',
|
||||
},
|
||||
{
|
||||
icon: 'visitor-contacts',
|
||||
label: 'ACTIVE',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/contacts/active`),
|
||||
toStateName: 'contacts_dashboard_active',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default contacts;
|
||||
@@ -1,50 +0,0 @@
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
const conversations = accountId => ({
|
||||
parentNav: 'conversations',
|
||||
routes: [
|
||||
'home',
|
||||
'inbox_dashboard',
|
||||
'inbox_conversation',
|
||||
'conversation_through_inbox',
|
||||
'notifications_dashboard',
|
||||
'label_conversations',
|
||||
'conversations_through_label',
|
||||
'team_conversations',
|
||||
'conversations_through_team',
|
||||
'conversation_mentions',
|
||||
'conversation_through_mentions',
|
||||
'conversation_participating',
|
||||
'conversation_through_participating',
|
||||
'folder_conversations',
|
||||
'conversations_through_folders',
|
||||
'conversation_unattended',
|
||||
'conversation_through_unattended',
|
||||
],
|
||||
menuItems: [
|
||||
{
|
||||
icon: 'chat',
|
||||
label: 'ALL_CONVERSATIONS',
|
||||
key: 'conversations',
|
||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||
toolTip: 'Conversation from all subscribed inboxes',
|
||||
toStateName: 'home',
|
||||
},
|
||||
{
|
||||
icon: 'mention',
|
||||
label: 'MENTIONED_CONVERSATIONS',
|
||||
key: 'conversation_mentions',
|
||||
toState: frontendURL(`accounts/${accountId}/mentions/conversations`),
|
||||
toStateName: 'conversation_mentions',
|
||||
},
|
||||
{
|
||||
icon: 'mail-unread',
|
||||
label: 'UNATTENDED_CONVERSATIONS',
|
||||
key: 'conversation_unattended',
|
||||
toState: frontendURL(`accounts/${accountId}/unattended/conversations`),
|
||||
toStateName: 'conversation_unattended',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default conversations;
|
||||
@@ -1,7 +0,0 @@
|
||||
const notifications = () => ({
|
||||
parentNav: 'notifications',
|
||||
routes: ['notifications_index'],
|
||||
menuItems: [],
|
||||
});
|
||||
|
||||
export default notifications;
|
||||
@@ -1,70 +0,0 @@
|
||||
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
const primaryMenuItems = accountId => [
|
||||
{
|
||||
icon: 'mail-inbox',
|
||||
key: 'inboxView',
|
||||
label: 'INBOX_VIEW',
|
||||
featureFlag: FEATURE_FLAGS.INBOX_VIEW,
|
||||
toState: frontendURL(`accounts/${accountId}/inbox-view`),
|
||||
toStateName: 'inbox_view',
|
||||
},
|
||||
{
|
||||
icon: 'chat',
|
||||
key: 'conversations',
|
||||
label: 'CONVERSATIONS',
|
||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||
toStateName: 'home',
|
||||
},
|
||||
{
|
||||
icon: 'captain',
|
||||
key: 'captain',
|
||||
label: 'CAPTAIN',
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
||||
toState: frontendURL(`accounts/${accountId}/captain/documents`),
|
||||
toStateName: 'captain',
|
||||
},
|
||||
{
|
||||
icon: 'book-contacts',
|
||||
key: 'contacts',
|
||||
label: 'CONTACTS',
|
||||
featureFlag: FEATURE_FLAGS.CRM,
|
||||
toState: frontendURL(`accounts/${accountId}/contacts`),
|
||||
toStateName: 'contacts_dashboard_index',
|
||||
},
|
||||
{
|
||||
icon: 'arrow-trending-lines',
|
||||
key: 'reports',
|
||||
label: 'REPORTS',
|
||||
featureFlag: FEATURE_FLAGS.REPORTS,
|
||||
toState: frontendURL(`accounts/${accountId}/reports`),
|
||||
toStateName: 'account_overview_reports',
|
||||
},
|
||||
{
|
||||
icon: 'megaphone',
|
||||
key: 'campaigns',
|
||||
label: 'CAMPAIGNS',
|
||||
featureFlag: FEATURE_FLAGS.CAMPAIGNS,
|
||||
toState: frontendURL(`accounts/${accountId}/campaigns`),
|
||||
toStateName: 'campaigns_ongoing_index',
|
||||
},
|
||||
{
|
||||
icon: 'library',
|
||||
key: 'helpcenter',
|
||||
label: 'HELP_CENTER.TITLE',
|
||||
featureFlag: FEATURE_FLAGS.HELP_CENTER,
|
||||
alwaysVisibleOnChatwootInstances: true,
|
||||
toState: frontendURL(`accounts/${accountId}/portals/portal_articles_index`),
|
||||
toStateName: 'portals_index',
|
||||
},
|
||||
{
|
||||
icon: 'settings',
|
||||
key: 'settings',
|
||||
label: 'SETTINGS',
|
||||
toState: frontendURL(`accounts/${accountId}/settings`),
|
||||
toStateName: 'settings_home',
|
||||
},
|
||||
];
|
||||
|
||||
export default primaryMenuItems;
|
||||
@@ -1,7 +0,0 @@
|
||||
const profileSettings = () => ({
|
||||
parentNav: 'profileSettings',
|
||||
routes: ['profile_settings_index'],
|
||||
menuItems: [],
|
||||
});
|
||||
|
||||
export default profileSettings;
|
||||
@@ -1,87 +0,0 @@
|
||||
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
const reports = accountId => ({
|
||||
parentNav: 'reports',
|
||||
routes: [
|
||||
'account_overview_reports',
|
||||
'conversation_reports',
|
||||
'csat_reports',
|
||||
'bot_reports',
|
||||
'agent_reports',
|
||||
'label_reports',
|
||||
'inbox_reports',
|
||||
'inbox_reports_show',
|
||||
'team_reports',
|
||||
'sla_reports',
|
||||
],
|
||||
menuItems: [
|
||||
{
|
||||
icon: 'arrow-trending-lines',
|
||||
label: 'REPORTS_OVERVIEW',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/overview`),
|
||||
toStateName: 'account_overview_reports',
|
||||
},
|
||||
{
|
||||
icon: 'chat',
|
||||
label: 'REPORTS_CONVERSATION',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/conversation`),
|
||||
toStateName: 'conversation_reports',
|
||||
},
|
||||
{
|
||||
icon: 'emoji',
|
||||
label: 'CSAT',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/csat`),
|
||||
toStateName: 'csat_reports',
|
||||
},
|
||||
{
|
||||
icon: 'bot',
|
||||
label: 'REPORTS_BOT',
|
||||
hasSubMenu: false,
|
||||
featureFlag: FEATURE_FLAGS.RESPONSE_BOT,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/bot`),
|
||||
toStateName: 'bot_reports',
|
||||
},
|
||||
{
|
||||
icon: 'people',
|
||||
label: 'REPORTS_AGENT',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/agent`),
|
||||
toStateName: 'agent_reports',
|
||||
},
|
||||
{
|
||||
icon: 'tag',
|
||||
label: 'REPORTS_LABEL',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/label`),
|
||||
toStateName: 'label_reports',
|
||||
},
|
||||
{
|
||||
icon: 'mail-inbox-all',
|
||||
label: 'REPORTS_INBOX',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/inboxes`),
|
||||
toStateName: 'inbox_reports',
|
||||
},
|
||||
{
|
||||
icon: 'people-team',
|
||||
label: 'REPORTS_TEAM',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/teams`),
|
||||
toStateName: 'team_reports',
|
||||
},
|
||||
{
|
||||
icon: 'document-list-clock',
|
||||
label: 'REPORTS_SLA',
|
||||
hasSubMenu: false,
|
||||
featureFlag: FEATURE_FLAGS.SLA,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/sla`),
|
||||
toStateName: 'sla_reports',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default reports;
|
||||
@@ -1,220 +0,0 @@
|
||||
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
const settings = accountId => ({
|
||||
parentNav: 'settings',
|
||||
routes: [
|
||||
'agent_bots',
|
||||
'agent_list',
|
||||
'attributes_list',
|
||||
'automation_list',
|
||||
'auditlogs_list',
|
||||
'billing_settings_index',
|
||||
'canned_list',
|
||||
'general_settings_index',
|
||||
'general_settings',
|
||||
'labels_list',
|
||||
'macros_edit',
|
||||
'macros_new',
|
||||
'macros_wrapper',
|
||||
'settings_applications_integration',
|
||||
'settings_applications_webhook',
|
||||
'settings_applications',
|
||||
'settings_inbox_finish',
|
||||
'settings_inbox_list',
|
||||
'settings_inbox_new',
|
||||
'settings_inbox_show',
|
||||
'settings_inboxes_add_agents',
|
||||
'settings_inboxes_page_channel',
|
||||
'settings_integrations_dashboard_apps',
|
||||
'settings_integrations_integration',
|
||||
'settings_integrations_slack',
|
||||
'settings_integrations_webhook',
|
||||
'settings_integrations',
|
||||
'settings_teams_add_agents',
|
||||
'settings_teams_edit_finish',
|
||||
'settings_teams_edit_members',
|
||||
'settings_teams_edit',
|
||||
'settings_teams_finish',
|
||||
'settings_teams_list',
|
||||
'settings_teams_new',
|
||||
'sla_list',
|
||||
'custom_roles_list',
|
||||
],
|
||||
menuItems: [
|
||||
{
|
||||
icon: 'briefcase',
|
||||
label: 'ACCOUNT_SETTINGS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/general`),
|
||||
toStateName: 'general_settings_index',
|
||||
},
|
||||
{
|
||||
icon: 'people',
|
||||
label: 'AGENTS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/agents/list`),
|
||||
toStateName: 'agent_list',
|
||||
featureFlag: FEATURE_FLAGS.AGENT_MANAGEMENT,
|
||||
},
|
||||
{
|
||||
icon: 'people-team',
|
||||
label: 'TEAMS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/teams/list`),
|
||||
toStateName: 'settings_teams_list',
|
||||
featureFlag: FEATURE_FLAGS.TEAM_MANAGEMENT,
|
||||
},
|
||||
{
|
||||
icon: 'mail-inbox-all',
|
||||
label: 'INBOXES',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`),
|
||||
toStateName: 'settings_inbox_list',
|
||||
featureFlag: FEATURE_FLAGS.INBOX_MANAGEMENT,
|
||||
},
|
||||
{
|
||||
icon: 'tag',
|
||||
label: 'LABELS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/labels/list`),
|
||||
toStateName: 'labels_list',
|
||||
featureFlag: FEATURE_FLAGS.LABELS,
|
||||
},
|
||||
{
|
||||
icon: 'code',
|
||||
label: 'CUSTOM_ATTRIBUTES',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(
|
||||
`accounts/${accountId}/settings/custom-attributes/list`
|
||||
),
|
||||
toStateName: 'attributes_list',
|
||||
featureFlag: FEATURE_FLAGS.CUSTOM_ATTRIBUTES,
|
||||
},
|
||||
{
|
||||
icon: 'automation',
|
||||
label: 'AUTOMATION',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/automation/list`),
|
||||
toStateName: 'automation_list',
|
||||
featureFlag: FEATURE_FLAGS.AUTOMATIONS,
|
||||
},
|
||||
{
|
||||
icon: 'bot',
|
||||
label: 'AGENT_BOTS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/agent-bots`),
|
||||
toStateName: 'agent_bots',
|
||||
featureFlag: FEATURE_FLAGS.AGENT_BOTS,
|
||||
},
|
||||
{
|
||||
icon: 'flash-settings',
|
||||
label: 'MACROS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/macros`),
|
||||
toStateName: 'macros_wrapper',
|
||||
featureFlag: FEATURE_FLAGS.MACROS,
|
||||
},
|
||||
{
|
||||
icon: 'chat-multiple',
|
||||
label: 'CANNED_RESPONSES',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
toState: frontendURL(
|
||||
`accounts/${accountId}/settings/canned-response/list`
|
||||
),
|
||||
toStateName: 'canned_list',
|
||||
featureFlag: FEATURE_FLAGS.CANNED_RESPONSES,
|
||||
},
|
||||
{
|
||||
icon: 'flash-on',
|
||||
label: 'INTEGRATIONS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
|
||||
toStateName: 'settings_applications',
|
||||
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
|
||||
},
|
||||
{
|
||||
icon: 'key',
|
||||
label: 'AUDIT_LOGS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/audit-logs/list`),
|
||||
toStateName: 'auditlogs_list',
|
||||
isEnterpriseOnly: true,
|
||||
featureFlag: FEATURE_FLAGS.AUDIT_LOGS,
|
||||
},
|
||||
{
|
||||
icon: 'scan-person',
|
||||
label: 'CUSTOM_ROLES',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/custom-roles/list`),
|
||||
toStateName: 'custom_roles_list',
|
||||
isEnterpriseOnly: true,
|
||||
beta: true,
|
||||
},
|
||||
{
|
||||
icon: 'document-list-clock',
|
||||
label: 'SLA',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/sla/list`),
|
||||
toStateName: 'sla_list',
|
||||
isEnterpriseOnly: true,
|
||||
featureFlag: FEATURE_FLAGS.SLA,
|
||||
beta: true,
|
||||
},
|
||||
{
|
||||
icon: 'credit-card-person',
|
||||
label: 'BILLING',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/billing`),
|
||||
toStateName: 'billing_settings_index',
|
||||
showOnlyOnCloud: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default settings;
|
||||
@@ -1,110 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
},
|
||||
emits: ['toggleAccounts'],
|
||||
data() {
|
||||
return { showSwitchButton: false };
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
account: 'getCurrentAccount',
|
||||
userAccounts: 'getUserAccounts',
|
||||
}),
|
||||
showShowCurrentAccountContext() {
|
||||
return this.userAccounts.length > 1 && this.account.name;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setShowSwitch() {
|
||||
this.showSwitchButton = true;
|
||||
},
|
||||
resetShowSwitch() {
|
||||
this.showSwitchButton = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||
<template>
|
||||
<div
|
||||
v-if="showShowCurrentAccountContext"
|
||||
class="relative px-2 py-2 mt-2 text-xs border rounded-md cursor-pointer text-slate-700 dark:text-slate-200 border-slate-50 dark:border-slate-800/50 hover:bg-slate-50 dark:hover:bg-slate-800"
|
||||
@mouseover="setShowSwitch"
|
||||
@mouseleave="resetShowSwitch"
|
||||
>
|
||||
{{ $t('SIDEBAR.CURRENTLY_VIEWING_ACCOUNT') }}
|
||||
<p
|
||||
class="mb-0 overflow-hidden font-medium text-ellipsis whitespace-nowrap text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ account.name }}
|
||||
</p>
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="showSwitchButton"
|
||||
class="absolute top-0 right-0 flex items-center justify-end w-full h-full rounded-md ltr:overlay-shadow ltr:dark:overlay-shadow-dark rtl:rtl-overlay-shadow rtl:dark:rtl-overlay-shadow-dark"
|
||||
>
|
||||
<div class="mx-2 my-0">
|
||||
<NextButton
|
||||
ghost
|
||||
xs
|
||||
icon="i-lucide-arrow-right-left"
|
||||
:label="$t('SIDEBAR.SWITCH')"
|
||||
@click="$emit('toggleAccounts')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@tailwind components;
|
||||
@layer components {
|
||||
.overlay-shadow {
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 1) 50%
|
||||
);
|
||||
}
|
||||
|
||||
.overlay-shadow-dark {
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgb(21, 23, 24) 50%
|
||||
);
|
||||
}
|
||||
|
||||
.rtl-overlay-shadow {
|
||||
background-image: linear-gradient(
|
||||
to left,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 1) 50%
|
||||
);
|
||||
}
|
||||
|
||||
.rtl-overlay-shadow-dark {
|
||||
background-image: linear-gradient(
|
||||
to left,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgb(21, 23, 24) 50%
|
||||
);
|
||||
}
|
||||
}
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 300ms ease;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
@apply opacity-0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,88 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
export default {
|
||||
props: {
|
||||
showAccountModal: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ['closeAccountModal', 'showCreateAccountModal'],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
currentUser: 'getCurrentUser',
|
||||
globalConfig: 'globalConfig/get',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
onChangeAccount(accountId) {
|
||||
const accountUrl = `/app/accounts/${accountId}/dashboard`;
|
||||
window.location.href = accountUrl;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<woot-modal
|
||||
:show="showAccountModal"
|
||||
:on-close="() => $emit('closeAccountModal')"
|
||||
>
|
||||
<woot-modal-header
|
||||
:header-title="$t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS')"
|
||||
:header-content="$t('SIDEBAR_ITEMS.SELECTOR_SUBTITLE')"
|
||||
/>
|
||||
<div class="px-8 py-4">
|
||||
<div
|
||||
v-for="account in currentUser.accounts"
|
||||
:id="`account-${account.id}`"
|
||||
:key="account.id"
|
||||
class="pt-0 pb-0"
|
||||
>
|
||||
<button
|
||||
class="flex items-center justify-between w-full px-4 py-3 rounded-lg cursor-pointer expanded clear link hover:underline hover:bg-slate-25 dark:hover:bg-slate-900"
|
||||
@click="onChangeAccount(account.id)"
|
||||
>
|
||||
<span class="w-full">
|
||||
<label :for="account.name" class="text-left rtl:text-right">
|
||||
<div
|
||||
class="text-lg font-medium leading-5 text-slate-700 dark:text-slate-100 hover:underline-offset-4"
|
||||
>
|
||||
{{ account.name }}
|
||||
</div>
|
||||
<div
|
||||
class="text-xs font-medium lowercase text-slate-500 dark:text-slate-500 hover:underline-offset-4"
|
||||
>
|
||||
{{
|
||||
account.custom_role_id
|
||||
? account.custom_role.name
|
||||
: account.role
|
||||
}}
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
<fluent-icon
|
||||
v-show="account.id === accountId"
|
||||
class="text-slate-800 dark:text-slate-100"
|
||||
icon="checkmark-circle"
|
||||
type="solid"
|
||||
size="24"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="globalConfig.createNewAccountFromDashboard"
|
||||
class="flex items-center justify-end gap-2 px-8 pt-4 pb-8"
|
||||
>
|
||||
<button
|
||||
class="w-full button success large expanded nice"
|
||||
@click="$emit('showCreateAccountModal')"
|
||||
>
|
||||
{{ $t('CREATE_ACCOUNT.NEW_ACCOUNT') }}
|
||||
</button>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</template>
|
||||
@@ -1,45 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import Thumbnail from '../../widgets/Thumbnail.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
NextButton,
|
||||
},
|
||||
emits: ['toggleMenu'],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
currentUserAvailability: 'getCurrentUserAvailability',
|
||||
}),
|
||||
statusOfAgent() {
|
||||
return this.currentUserAvailability || 'offline';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
this.$emit('toggleMenu');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NextButton
|
||||
v-tooltip.right="$t(`SIDEBAR.PROFILE_SETTINGS`)"
|
||||
link
|
||||
class="rounded-full"
|
||||
@click="handleClick"
|
||||
>
|
||||
<Thumbnail
|
||||
:src="currentUser.avatar_url"
|
||||
:username="currentUser.name"
|
||||
:status="statusOfAgent"
|
||||
should-show-status-always
|
||||
size="32px"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
</NextButton>
|
||||
</template>
|
||||
@@ -1,33 +0,0 @@
|
||||
<script>
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
source: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
accountId: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
dashboardPath() {
|
||||
return frontendURL(`accounts/${this.accountId}/dashboard`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-8 h-8">
|
||||
<router-link :to="dashboardPath" replace>
|
||||
<img :src="source" :alt="name" />
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,58 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
emits: ['openNotificationPanel'],
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
notificationMetadata: 'notifications/getMeta',
|
||||
}),
|
||||
unreadCount() {
|
||||
if (!this.notificationMetadata.unreadCount) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this.notificationMetadata.unreadCount < 100
|
||||
? `${this.notificationMetadata.unreadCount}`
|
||||
: '99+';
|
||||
},
|
||||
isNotificationPanelActive() {
|
||||
return this.$route.name === 'notifications_index';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openNotificationPanel() {
|
||||
if (this.$route.name !== 'notifications_index') {
|
||||
this.$emit('openNotificationPanel');
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-4">
|
||||
<button
|
||||
class="relative flex items-center justify-center w-10 h-10 p-0 my-2 rounded-lg text-slate-600 dark:text-slate-100 hover:bg-slate-25 dark:hover:bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600"
|
||||
:class="{
|
||||
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50':
|
||||
isNotificationPanelActive,
|
||||
}"
|
||||
@click="openNotificationPanel"
|
||||
>
|
||||
<fluent-icon
|
||||
icon="alert"
|
||||
:class="{
|
||||
'text-woot-500': isNotificationPanelActive,
|
||||
}"
|
||||
/>
|
||||
<span
|
||||
v-if="unreadCount"
|
||||
class="text-black-900 bg-yellow-300 absolute -top-0.5 -right-1 text-xxs min-w-[1rem] rounded-full"
|
||||
>
|
||||
{{ unreadCount }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,204 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import Auth from '../../../api/auth';
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
import AvailabilityStatus from 'dashboard/components/layout/AvailabilityStatus.vue';
|
||||
import { FEATURE_FLAGS } from '../../../featureFlags';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootDropdownMenu,
|
||||
WootDropdownItem,
|
||||
AvailabilityStatus,
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'close',
|
||||
'openKeyShortcutModal',
|
||||
'toggleAccounts',
|
||||
'showSupportChatWindow',
|
||||
],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
globalConfig: 'globalConfig/get',
|
||||
accountId: 'getCurrentAccountId',
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
}),
|
||||
showChangeAccountOption() {
|
||||
if (this.globalConfig.createNewAccountFromDashboard) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { accounts = [] } = this.currentUser;
|
||||
return accounts.length > 1;
|
||||
},
|
||||
showChatSupport() {
|
||||
return (
|
||||
this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
FEATURE_FLAGS.CONTACT_CHATWOOT_SUPPORT_TEAM
|
||||
) && this.globalConfig.chatwootInboxToken
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleProfileSettingClick(e, navigate) {
|
||||
this.$emit('close');
|
||||
navigate(e);
|
||||
},
|
||||
handleKeyboardHelpClick() {
|
||||
this.$emit('openKeyShortcutModal');
|
||||
this.$emit('close');
|
||||
},
|
||||
logout() {
|
||||
Auth.logout();
|
||||
},
|
||||
onClickAway() {
|
||||
if (this.show) this.$emit('close');
|
||||
},
|
||||
openAppearanceOptions() {
|
||||
const ninja = document.querySelector('ninja-keys');
|
||||
ninja.open({ parent: 'appearance_settings' });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="menu-slide">
|
||||
<div
|
||||
v-if="show"
|
||||
v-on-clickaway="onClickAway"
|
||||
class="absolute z-30 w-64 px-2 py-2 bg-white border rounded-md shadow-xl left-3 rtl:left-auto rtl:right-3 bottom-16 dark:bg-slate-800 border-slate-25 dark:border-slate-700"
|
||||
:class="{ 'block visible': show }"
|
||||
>
|
||||
<AvailabilityStatus />
|
||||
<WootDropdownMenu>
|
||||
<WootDropdownItem v-if="showChangeAccountOption">
|
||||
<NextButton
|
||||
ghost
|
||||
sm
|
||||
slate
|
||||
icon="i-lucide-arrow-right-left"
|
||||
class="!w-full !justify-start"
|
||||
@click="$emit('toggleAccounts')"
|
||||
>
|
||||
<span class="min-w-0 truncate font-medium text-xs">
|
||||
{{ $t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS') }}
|
||||
</span>
|
||||
</NextButton>
|
||||
</WootDropdownItem>
|
||||
<WootDropdownItem v-if="showChatSupport">
|
||||
<NextButton
|
||||
ghost
|
||||
sm
|
||||
slate
|
||||
icon="i-lucide-message-circle-question"
|
||||
class="!w-full !justify-start"
|
||||
@click="$emit('showSupportChatWindow')"
|
||||
>
|
||||
<span class="min-w-0 truncate font-medium text-xs">
|
||||
{{ $t('SIDEBAR_ITEMS.CONTACT_SUPPORT') }}
|
||||
</span>
|
||||
</NextButton>
|
||||
</WootDropdownItem>
|
||||
<WootDropdownItem>
|
||||
<NextButton
|
||||
ghost
|
||||
sm
|
||||
slate
|
||||
icon="i-lucide-keyboard"
|
||||
class="!w-full !justify-start"
|
||||
@click="handleKeyboardHelpClick"
|
||||
>
|
||||
<span class="min-w-0 truncate font-medium text-xs">
|
||||
{{ $t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS') }}
|
||||
</span>
|
||||
</NextButton>
|
||||
</WootDropdownItem>
|
||||
<WootDropdownItem>
|
||||
<router-link
|
||||
v-slot="{ href, isActive, navigate }"
|
||||
:to="`/app/accounts/${accountId}/profile/settings`"
|
||||
custom
|
||||
>
|
||||
<a
|
||||
:href="href"
|
||||
:class="{ 'is-active': isActive }"
|
||||
@click="e => handleProfileSettingClick(e, navigate)"
|
||||
>
|
||||
<NextButton
|
||||
ghost
|
||||
sm
|
||||
slate
|
||||
icon="i-lucide-circle-user"
|
||||
class="!w-full !justify-start"
|
||||
>
|
||||
<span class="min-w-0 truncate font-medium text-xs">
|
||||
{{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
|
||||
</span>
|
||||
</NextButton>
|
||||
</a>
|
||||
</router-link>
|
||||
</WootDropdownItem>
|
||||
<WootDropdownItem>
|
||||
<NextButton
|
||||
ghost
|
||||
sm
|
||||
slate
|
||||
icon="i-lucide-sun-moon"
|
||||
class="!w-full !justify-start"
|
||||
@click="openAppearanceOptions"
|
||||
>
|
||||
<span class="min-w-0 truncate font-medium text-xs">
|
||||
{{ $t('SIDEBAR_ITEMS.APPEARANCE') }}
|
||||
</span>
|
||||
</NextButton>
|
||||
</WootDropdownItem>
|
||||
<WootDropdownItem v-if="currentUser.type === 'SuperAdmin'">
|
||||
<a
|
||||
href="/super_admin"
|
||||
target="_blank"
|
||||
rel="noopener nofollow noreferrer"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<NextButton
|
||||
ghost
|
||||
sm
|
||||
slate
|
||||
icon="i-lucide-layout-dashboard"
|
||||
class="!w-full !justify-start"
|
||||
>
|
||||
<span class="min-w-0 truncate font-medium text-xs">
|
||||
{{ $t('SIDEBAR_ITEMS.SUPER_ADMIN_CONSOLE') }}
|
||||
</span>
|
||||
</NextButton>
|
||||
</a>
|
||||
</WootDropdownItem>
|
||||
<WootDropdownItem>
|
||||
<NextButton
|
||||
ghost
|
||||
sm
|
||||
slate
|
||||
icon="i-lucide-circle-power"
|
||||
class="!w-full !justify-start"
|
||||
@click="logout"
|
||||
>
|
||||
<span class="min-w-0 truncate font-medium text-xs">
|
||||
{{ $t('SIDEBAR_ITEMS.LOGOUT') }}
|
||||
</span>
|
||||
</NextButton>
|
||||
</WootDropdownItem>
|
||||
</WootDropdownMenu>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
@@ -1,116 +0,0 @@
|
||||
<script>
|
||||
import Logo from './Logo.vue';
|
||||
import PrimaryNavItem from './PrimaryNavItem.vue';
|
||||
import OptionsMenu from './OptionsMenu.vue';
|
||||
import AgentDetails from './AgentDetails.vue';
|
||||
import NotificationBell from './NotificationBell.vue';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
import { ACCOUNT_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Logo,
|
||||
PrimaryNavItem,
|
||||
OptionsMenu,
|
||||
AgentDetails,
|
||||
NotificationBell,
|
||||
},
|
||||
props: {
|
||||
isACustomBrandedInstance: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
logoSource: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
installationName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
accountId: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
menuItems: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
activeMenuItem: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['toggleAccounts', 'openNotificationPanel', 'openKeyShortcutModal'],
|
||||
data() {
|
||||
return {
|
||||
helpDocsURL: wootConstants.DOCS_URL,
|
||||
showOptionsMenu: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
frontendURL,
|
||||
toggleOptions() {
|
||||
this.showOptionsMenu = !this.showOptionsMenu;
|
||||
},
|
||||
toggleAccountModal() {
|
||||
this.$emit('toggleAccounts');
|
||||
},
|
||||
toggleSupportChatWindow() {
|
||||
window.$chatwoot.toggle();
|
||||
},
|
||||
openNotificationPanel() {
|
||||
useTrack(ACCOUNT_EVENTS.OPENED_NOTIFICATIONS);
|
||||
this.$emit('openNotificationPanel');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col justify-between w-16 h-full bg-white border-r dark:bg-slate-900 border-slate-50 dark:border-slate-800/50 rtl:border-l rtl:border-r-0"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<Logo
|
||||
:source="logoSource"
|
||||
:name="installationName"
|
||||
:account-id="accountId"
|
||||
class="m-4 mb-10"
|
||||
/>
|
||||
<PrimaryNavItem
|
||||
v-for="menuItem in menuItems"
|
||||
:id="menuItem.key"
|
||||
:key="menuItem.toState"
|
||||
:icon="menuItem.icon"
|
||||
:name="menuItem.label"
|
||||
:to="menuItem.toState"
|
||||
:is-child-menu-active="menuItem.key === activeMenuItem"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-end pb-6">
|
||||
<a
|
||||
v-if="!isACustomBrandedInstance"
|
||||
v-tooltip.right="$t(`SIDEBAR.DOCS`)"
|
||||
:href="helpDocsURL"
|
||||
class="relative flex items-center justify-center w-10 h-10 my-2 rounded-lg text-slate-700 dark:text-slate-100 hover:bg-slate-25 dark:hover:bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600"
|
||||
rel="noopener noreferrer nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
<fluent-icon icon="book-open-globe" />
|
||||
<span class="sr-only">{{ $t(`SIDEBAR.DOCS`) }}</span>
|
||||
</a>
|
||||
<NotificationBell @open-notification-panel="openNotificationPanel" />
|
||||
<AgentDetails @toggle-menu="toggleOptions" />
|
||||
<OptionsMenu
|
||||
:show="showOptionsMenu"
|
||||
@toggle-accounts="toggleAccountModal"
|
||||
@show-support-chat-window="toggleSupportChatWindow"
|
||||
@open-key-shortcut-modal="$emit('openKeyShortcutModal')"
|
||||
@close="toggleOptions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,148 +0,0 @@
|
||||
<script>
|
||||
import { OnClickOutside } from '@vueuse/components';
|
||||
import { HELP_CENTER_MENU_ITEMS } from 'dashboard/helper/portalHelper';
|
||||
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DropdownMenu,
|
||||
OnClickOutside,
|
||||
},
|
||||
props: {
|
||||
to: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
count: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isChildMenuActive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
openInNewPage: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
helpCenterMenu: HELP_CENTER_MENU_ITEMS,
|
||||
showHelpCenterMenu: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
helpCenterMenuItems() {
|
||||
return this.helpCenterMenu.map(item => ({
|
||||
...item,
|
||||
isSelected: this.isSelectedMenuItem(item),
|
||||
}));
|
||||
},
|
||||
isHelpCenter() {
|
||||
return this.id === 'helpcenter';
|
||||
},
|
||||
isHelpCenterSelected() {
|
||||
const routes = [
|
||||
'portals_new',
|
||||
'portals_index',
|
||||
'portals_articles_index',
|
||||
'portals_articles_new',
|
||||
'portals_articles_edit',
|
||||
'portals_categories_index',
|
||||
'portals_categories_articles_index',
|
||||
'portals_categories_articles_edit',
|
||||
'portals_locales_index',
|
||||
'portals_settings_index',
|
||||
];
|
||||
return routes.includes(this.$route.name);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isSelectedMenuItem(menuItem) {
|
||||
return menuItem.value.includes(this.$route.name);
|
||||
},
|
||||
toggleHelpCenterMenu() {
|
||||
this.showHelpCenterMenu = !this.showHelpCenterMenu;
|
||||
},
|
||||
handleHelpCenterAction({ action }) {
|
||||
this.$router.push({
|
||||
name: 'portals_index',
|
||||
params: {
|
||||
navigationPath: action,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OnClickOutside v-if="isHelpCenter" @trigger="showHelpCenterMenu = false">
|
||||
<button
|
||||
v-tooltip.top="$t(`SIDEBAR.${name}`)"
|
||||
class="relative flex items-center justify-center w-10 h-10 my-2 rounded-lg text-slate-700 dark:text-slate-100 hover:!bg-slate-25 dark:hover:!bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600"
|
||||
:class="{
|
||||
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50':
|
||||
isHelpCenterSelected,
|
||||
}"
|
||||
@click="toggleHelpCenterMenu"
|
||||
>
|
||||
<fluent-icon
|
||||
:icon="icon"
|
||||
:class="{
|
||||
'text-woot-500': isHelpCenterSelected,
|
||||
}"
|
||||
/>
|
||||
<DropdownMenu
|
||||
v-if="showHelpCenterMenu && isHelpCenter"
|
||||
:menu-items="helpCenterMenuItems"
|
||||
class="ltr:left-10 rtl:right-10 w-36 z-[100] top-0 overflow-y-auto max-h-52"
|
||||
@action="handleHelpCenterAction"
|
||||
/>
|
||||
</button>
|
||||
</OnClickOutside>
|
||||
|
||||
<router-link v-else v-slot="{ href, isActive, navigate }" :to="to" custom>
|
||||
<a
|
||||
v-tooltip.right="$t(`SIDEBAR.${name}`)"
|
||||
:href="href"
|
||||
class="relative flex items-center justify-center w-10 h-10 my-2 rounded-lg text-slate-700 dark:text-slate-100 hover:bg-slate-25 dark:hover:bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600"
|
||||
:class="{
|
||||
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50':
|
||||
isActive || isChildMenuActive,
|
||||
}"
|
||||
:rel="openInNewPage ? 'noopener noreferrer nofollow' : undefined"
|
||||
:target="openInNewPage ? '_blank' : undefined"
|
||||
@click="navigate"
|
||||
>
|
||||
<fluent-icon
|
||||
:icon="icon"
|
||||
:class="{
|
||||
'text-woot-500': isActive || isChildMenuActive,
|
||||
}"
|
||||
/>
|
||||
<span class="sr-only">{{ name }}</span>
|
||||
<span
|
||||
v-if="count"
|
||||
class="absolute bg-yellow-500 text-black-900 -top-1 -right-1"
|
||||
>
|
||||
{{ count }}
|
||||
</span>
|
||||
</a>
|
||||
</router-link>
|
||||
</template>
|
||||
@@ -1,265 +0,0 @@
|
||||
<script>
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
import SecondaryNavItem from './SecondaryNavItem.vue';
|
||||
import AccountContext from './AccountContext.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { FEATURE_FLAGS } from '../../../featureFlags';
|
||||
import {
|
||||
getUserPermissions,
|
||||
hasPermissions,
|
||||
} from '../../../helper/permissionsHelper';
|
||||
import { routesWithPermissions } from '../../../routes';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AccountContext,
|
||||
SecondaryNavItem,
|
||||
},
|
||||
props: {
|
||||
accountId: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
labels: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
inboxes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
teams: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
customViews: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
menuConfig: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
currentUser: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
isOnChatwootCloud: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['addLabel', 'toggleAccounts'],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
}),
|
||||
contactCustomViews() {
|
||||
return this.customViews.filter(view => view.filter_type === 'contact');
|
||||
},
|
||||
accessibleMenuItems() {
|
||||
const menuItemsFilteredByPermissions = this.menuConfig.menuItems.filter(
|
||||
menuItem => {
|
||||
const userPermissions = getUserPermissions(
|
||||
this.currentUser,
|
||||
this.accountId
|
||||
);
|
||||
return hasPermissions(
|
||||
routesWithPermissions[menuItem.toStateName],
|
||||
userPermissions
|
||||
);
|
||||
}
|
||||
);
|
||||
return menuItemsFilteredByPermissions.filter(item => {
|
||||
if (item.showOnlyOnCloud) {
|
||||
return this.isOnChatwootCloud;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
inboxSection() {
|
||||
return {
|
||||
icon: 'folder',
|
||||
label: 'INBOXES',
|
||||
hasSubMenu: true,
|
||||
newLink: this.showNewLink(FEATURE_FLAGS.INBOX_MANAGEMENT),
|
||||
newLinkTag: 'NEW_INBOX',
|
||||
key: 'inbox',
|
||||
toState: frontendURL(`accounts/${this.accountId}/settings/inboxes/new`),
|
||||
toStateName: 'settings_inbox_new',
|
||||
newLinkRouteName: 'settings_inbox_new',
|
||||
children: this.inboxes
|
||||
.map(inbox => ({
|
||||
id: inbox.id,
|
||||
label: inbox.name,
|
||||
truncateLabel: true,
|
||||
toState: frontendURL(
|
||||
`accounts/${this.accountId}/inbox/${inbox.id}`
|
||||
),
|
||||
type: inbox.channel_type,
|
||||
phoneNumber: inbox.phone_number,
|
||||
reauthorizationRequired: inbox.reauthorization_required,
|
||||
}))
|
||||
.sort((a, b) =>
|
||||
a.label.toLowerCase() > b.label.toLowerCase() ? 1 : -1
|
||||
),
|
||||
};
|
||||
},
|
||||
labelSection() {
|
||||
return {
|
||||
icon: 'number-symbol',
|
||||
label: 'LABELS',
|
||||
hasSubMenu: true,
|
||||
newLink: this.showNewLink(FEATURE_FLAGS.TEAM_MANAGEMENT),
|
||||
newLinkTag: 'NEW_LABEL',
|
||||
key: 'label',
|
||||
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
|
||||
toStateName: 'labels_list',
|
||||
showModalForNewItem: true,
|
||||
modalName: 'AddLabel',
|
||||
dataTestid: 'sidebar-new-label-button',
|
||||
children: this.labels.map(label => ({
|
||||
id: label.id,
|
||||
label: label.title,
|
||||
color: label.color,
|
||||
truncateLabel: true,
|
||||
toState: frontendURL(
|
||||
`accounts/${this.accountId}/label/${label.title}`
|
||||
),
|
||||
})),
|
||||
};
|
||||
},
|
||||
contactLabelSection() {
|
||||
return {
|
||||
icon: 'number-symbol',
|
||||
label: 'TAGGED_WITH',
|
||||
hasSubMenu: true,
|
||||
key: 'labels',
|
||||
newLink: this.showNewLink(FEATURE_FLAGS.TEAM_MANAGEMENT),
|
||||
newLinkTag: 'NEW_LABEL',
|
||||
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
|
||||
toStateName: 'labels_list',
|
||||
showModalForNewItem: true,
|
||||
modalName: 'AddLabel',
|
||||
children: this.labels.map(label => ({
|
||||
id: label.id,
|
||||
label: label.title,
|
||||
color: label.color,
|
||||
truncateLabel: true,
|
||||
toState: frontendURL(
|
||||
`accounts/${this.accountId}/contacts/labels/${label.title}`
|
||||
),
|
||||
})),
|
||||
};
|
||||
},
|
||||
teamSection() {
|
||||
return {
|
||||
icon: 'people-team',
|
||||
label: 'TEAMS',
|
||||
hasSubMenu: true,
|
||||
newLink: this.showNewLink(FEATURE_FLAGS.TEAM_MANAGEMENT),
|
||||
newLinkTag: 'NEW_TEAM',
|
||||
key: 'team',
|
||||
toState: frontendURL(`accounts/${this.accountId}/settings/teams/new`),
|
||||
toStateName: 'settings_teams_new',
|
||||
newLinkRouteName: 'settings_teams_new',
|
||||
children: this.teams.map(team => ({
|
||||
id: team.id,
|
||||
label: team.name,
|
||||
truncateLabel: true,
|
||||
toState: frontendURL(`accounts/${this.accountId}/team/${team.id}`),
|
||||
})),
|
||||
};
|
||||
},
|
||||
foldersSection() {
|
||||
return {
|
||||
icon: 'folder',
|
||||
label: 'CUSTOM_VIEWS_FOLDER',
|
||||
hasSubMenu: true,
|
||||
key: 'custom_view',
|
||||
children: this.customViews
|
||||
.filter(view => view.filter_type === 'conversation')
|
||||
.map(view => ({
|
||||
id: view.id,
|
||||
label: view.name,
|
||||
truncateLabel: true,
|
||||
toState: frontendURL(
|
||||
`accounts/${this.accountId}/custom_view/${view.id}`
|
||||
),
|
||||
})),
|
||||
};
|
||||
},
|
||||
contactSegmentsSection() {
|
||||
return {
|
||||
icon: 'folder',
|
||||
label: 'CUSTOM_VIEWS_SEGMENTS',
|
||||
hasSubMenu: true,
|
||||
key: 'segments',
|
||||
children: this.customViews
|
||||
.filter(view => view.filter_type === 'contact')
|
||||
.map(view => ({
|
||||
id: view.id,
|
||||
label: view.name,
|
||||
truncateLabel: true,
|
||||
toState: frontendURL(
|
||||
`accounts/${this.accountId}/contacts/segments/${view.id}`
|
||||
),
|
||||
})),
|
||||
};
|
||||
},
|
||||
additionalSecondaryMenuItems() {
|
||||
let conversationMenuItems = [this.inboxSection, this.labelSection];
|
||||
let contactMenuItems = [this.contactLabelSection];
|
||||
if (this.teams.length) {
|
||||
conversationMenuItems = [this.teamSection, ...conversationMenuItems];
|
||||
}
|
||||
if (this.customViews.length) {
|
||||
conversationMenuItems = [this.foldersSection, ...conversationMenuItems];
|
||||
}
|
||||
if (this.contactCustomViews.length) {
|
||||
contactMenuItems = [this.contactSegmentsSection, ...contactMenuItems];
|
||||
}
|
||||
return {
|
||||
conversations: conversationMenuItems,
|
||||
contacts: contactMenuItems,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showAddLabelPopup() {
|
||||
this.$emit('addLabel');
|
||||
},
|
||||
toggleAccountModal() {
|
||||
this.$emit('toggleAccounts');
|
||||
},
|
||||
showNewLink(featureFlag) {
|
||||
return this.isFeatureEnabledonAccount(this.accountId, featureFlag);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col w-48 h-full px-2 pb-8 overflow-auto text-sm bg-white border-r dark:bg-slate-900 dark:border-slate-800/50 rtl:border-r-0 rtl:border-l border-slate-50"
|
||||
>
|
||||
<AccountContext @toggle-accounts="toggleAccountModal" />
|
||||
<transition-group
|
||||
name="menu-list"
|
||||
tag="ul"
|
||||
class="pt-2 list-none reset-base"
|
||||
>
|
||||
<SecondaryNavItem
|
||||
v-for="menuItem in accessibleMenuItems"
|
||||
:key="menuItem.toState"
|
||||
:menu-item="menuItem"
|
||||
/>
|
||||
<SecondaryNavItem
|
||||
v-for="menuItem in additionalSecondaryMenuItems[menuConfig.parentNav]"
|
||||
:key="menuItem.key"
|
||||
:menu-item="menuItem"
|
||||
@add-label="showAddLabelPopup"
|
||||
/>
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,132 +0,0 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
to: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
labelColor: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
shouldTruncate: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
warningIcon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showChildCount: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
childItemCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showIcon() {
|
||||
return {
|
||||
'overflow-hidden whitespace-nowrap text-ellipsis': this.shouldTruncate,
|
||||
};
|
||||
},
|
||||
isCountZero() {
|
||||
return this.childItemCount === 0;
|
||||
},
|
||||
menuTitle() {
|
||||
return this.shouldTruncate ? this.label : '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-link
|
||||
v-slot="{ href, isActive, navigate }"
|
||||
:to="to"
|
||||
custom
|
||||
active-class="active"
|
||||
>
|
||||
<li
|
||||
class="h-7 my-1 hover:bg-slate-25 hover:text-bg-50 flex items-center px-2 rounded-md dark:hover:bg-slate-800"
|
||||
:class="{
|
||||
'bg-woot-25 dark:bg-slate-800': isActive,
|
||||
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full':
|
||||
shouldTruncate,
|
||||
}"
|
||||
@click="navigate"
|
||||
>
|
||||
<a
|
||||
:href="href"
|
||||
class="inline-flex text-left max-w-full w-full items-center"
|
||||
>
|
||||
<span
|
||||
v-if="icon"
|
||||
class="inline-flex items-center justify-center w-4 rounded-sm bg-slate-100 dark:bg-slate-700 p-0.5 mr-1.5 rtl:mr-0 rtl:ml-1.5"
|
||||
>
|
||||
<fluent-icon
|
||||
class="text-xxs text-slate-700 dark:text-slate-200"
|
||||
:class="{
|
||||
'text-woot-500 dark:text-woot-500': isActive,
|
||||
}"
|
||||
:icon="icon"
|
||||
size="12"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="labelColor"
|
||||
class="inline-flex rounded-sm bg-slate-100 h-3 w-3.5 mr-1.5 rtl:mr-0 rtl:ml-1.5 border border-slate-50 dark:border-slate-900"
|
||||
:style="{ backgroundColor: labelColor }"
|
||||
/>
|
||||
<div
|
||||
class="items-center flex overflow-hidden whitespace-nowrap text-ellipsis w-full justify-between"
|
||||
>
|
||||
<span
|
||||
:title="menuTitle"
|
||||
class="text-sm text-slate-700 dark:text-slate-100"
|
||||
:class="{
|
||||
'text-woot-500 dark:text-woot-500': isActive,
|
||||
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full':
|
||||
shouldTruncate,
|
||||
}"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
<span
|
||||
v-if="showChildCount"
|
||||
class="bg-slate-50 dark:bg-slate-700 rounded-full min-w-[18px] justify-center items-center flex text-xxs mx-1 py-0 px-1"
|
||||
:class="
|
||||
isCountZero
|
||||
? `text-slate-300 dark:text-slate-500`
|
||||
: `text-slate-700 dark:text-slate-50`
|
||||
"
|
||||
>
|
||||
{{ childItemCount }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="warningIcon"
|
||||
class="inline-flex mr-1 bg-red-50 dark:bg-red-900 p-0.5 rounded-sm"
|
||||
>
|
||||
<fluent-icon
|
||||
v-tooltip.top-end="$t('SIDEBAR.REAUTHORIZE')"
|
||||
class="text-xxs text-red-500 dark:text-red-300"
|
||||
:icon="warningIcon"
|
||||
size="12"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</router-link>
|
||||
</template>
|
||||
@@ -1,277 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import { useConfig } from 'dashboard/composables/useConfig';
|
||||
import {
|
||||
getInboxClassByType,
|
||||
getInboxWarningIconClass,
|
||||
} from 'dashboard/helper/inbox';
|
||||
|
||||
import SecondaryChildNavItem from './SecondaryChildNavItem.vue';
|
||||
import {
|
||||
isOnMentionsView,
|
||||
isOnUnattendedView,
|
||||
} from '../../../store/modules/conversations/helpers/actionHelpers';
|
||||
import Policy from '../../policy.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: { SecondaryChildNavItem, Policy, NextButton },
|
||||
props: {
|
||||
menuItem: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
emits: ['addLabel', 'open'],
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
const { isEnterprise } = useConfig();
|
||||
return {
|
||||
isAdmin,
|
||||
isEnterprise,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
activeInbox: 'getSelectedInbox',
|
||||
accountId: 'getCurrentAccountId',
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
globalConfig: 'globalConfig/get',
|
||||
}),
|
||||
isCountZero() {
|
||||
return this.menuItem.count === 0;
|
||||
},
|
||||
isActiveView() {
|
||||
return this.computedClass.includes('active-view');
|
||||
},
|
||||
hasSubMenu() {
|
||||
return !!this.menuItem.children;
|
||||
},
|
||||
isMenuItemVisible() {
|
||||
let isFeatureEnabled = true;
|
||||
if (this.menuItem.featureFlag) {
|
||||
isFeatureEnabled = this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
this.menuItem.featureFlag
|
||||
);
|
||||
}
|
||||
|
||||
if (this.menuItem.isEnterpriseOnly) {
|
||||
if (!this.isEnterprise) return false;
|
||||
return isFeatureEnabled || this.globalConfig.displayManifest;
|
||||
}
|
||||
|
||||
if (this.menuItem.featureFlag) {
|
||||
return this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
this.menuItem.featureFlag
|
||||
);
|
||||
}
|
||||
|
||||
return isFeatureEnabled;
|
||||
},
|
||||
isAllConversations() {
|
||||
return (
|
||||
this.$store.state.route.name === 'inbox_conversation' &&
|
||||
this.menuItem.toStateName === 'home'
|
||||
);
|
||||
},
|
||||
isMentions() {
|
||||
return (
|
||||
isOnMentionsView({ route: this.$route }) &&
|
||||
this.menuItem.toStateName === 'conversation_mentions'
|
||||
);
|
||||
},
|
||||
isUnattended() {
|
||||
return (
|
||||
isOnUnattendedView({ route: this.$route }) &&
|
||||
this.menuItem.toStateName === 'conversation_unattended'
|
||||
);
|
||||
},
|
||||
isTeamsSettings() {
|
||||
return (
|
||||
this.$store.state.route.name === 'settings_teams_edit' &&
|
||||
this.menuItem.toStateName === 'settings_teams_list'
|
||||
);
|
||||
},
|
||||
isInboxSettings() {
|
||||
return (
|
||||
this.$route.name === 'settings_inbox_show' &&
|
||||
this.menuItem.toStateName === 'settings_inbox_list'
|
||||
);
|
||||
},
|
||||
isIntegrationsSettings() {
|
||||
return (
|
||||
this.$store.state.route.name === 'settings_integrations_webhook' &&
|
||||
this.menuItem.toStateName === 'settings_integrations'
|
||||
);
|
||||
},
|
||||
isApplicationsSettings() {
|
||||
return (
|
||||
this.$store.state.route.name === 'settings_applications_integration' &&
|
||||
this.menuItem.toStateName === 'settings_applications'
|
||||
);
|
||||
},
|
||||
isContactsDefaultRoute() {
|
||||
return (
|
||||
this.menuItem.toStateName === 'contacts_dashboard_index' &&
|
||||
(this.$store.state.route.name === 'contacts_dashboard_index' ||
|
||||
this.$store.state.route.name === 'contacts_edit')
|
||||
);
|
||||
},
|
||||
isCurrentRoute() {
|
||||
return this.$store.state.route.name.includes(this.menuItem.toStateName);
|
||||
},
|
||||
|
||||
computedClass() {
|
||||
// If active inbox is present, do not highlight conversations
|
||||
if (this.activeInbox) return ' ';
|
||||
if (
|
||||
this.isAllConversations ||
|
||||
this.isMentions ||
|
||||
this.isUnattended ||
|
||||
this.isContactsDefaultRoute ||
|
||||
this.isCurrentRoute
|
||||
) {
|
||||
return 'bg-woot-25 dark:bg-slate-800 text-woot-500 dark:text-woot-500 hover:text-woot-500 dark:hover:text-woot-500 active-view';
|
||||
}
|
||||
if (this.hasSubMenu) {
|
||||
if (
|
||||
this.isTeamsSettings ||
|
||||
this.isInboxSettings ||
|
||||
this.isIntegrationsSettings ||
|
||||
this.isApplicationsSettings
|
||||
) {
|
||||
return 'bg-woot-25 dark:bg-slate-800 text-woot-500 dark:text-woot-500 hover:text-woot-500 dark:hover:text-woot-500 active-view';
|
||||
}
|
||||
return 'hover:text-slate-700 dark:hover:text-slate-100';
|
||||
}
|
||||
|
||||
return 'hover:text-slate-700 dark:hover:text-slate-100';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
computedInboxClass(child) {
|
||||
const { type, phoneNumber } = child;
|
||||
if (!type) return '';
|
||||
const classByType = getInboxClassByType(type, phoneNumber);
|
||||
return classByType;
|
||||
},
|
||||
computedInboxErrorClass(child) {
|
||||
const { type, reauthorizationRequired } = child;
|
||||
if (!type) return '';
|
||||
const warningClass = getInboxWarningIconClass(
|
||||
type,
|
||||
reauthorizationRequired
|
||||
);
|
||||
return warningClass;
|
||||
},
|
||||
newLinkClick(e, navigate) {
|
||||
if (this.menuItem.newLinkRouteName) {
|
||||
navigate(e);
|
||||
} else if (this.menuItem.showModalForNewItem) {
|
||||
if (this.menuItem.modalName === 'AddLabel') {
|
||||
e.preventDefault();
|
||||
this.$emit('addLabel');
|
||||
}
|
||||
}
|
||||
},
|
||||
showItem(item) {
|
||||
return this.isAdmin && !!item.newLink;
|
||||
},
|
||||
onClickOpen() {
|
||||
this.$emit('open');
|
||||
},
|
||||
showChildCount(count) {
|
||||
return Number.isInteger(count);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li v-show="isMenuItemVisible" class="mt-1">
|
||||
<div v-if="hasSubMenu" class="flex justify-between">
|
||||
<span
|
||||
class="px-2 pt-1 my-2 text-sm font-semibold text-slate-700 dark:text-slate-200"
|
||||
>
|
||||
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
||||
</span>
|
||||
<div v-if="menuItem.showNewButton" class="flex items-center">
|
||||
<NextButton ghost xs slate icon="i-lucide-plus" @click="onClickOpen" />
|
||||
</div>
|
||||
</div>
|
||||
<router-link
|
||||
v-else
|
||||
class="flex items-center p-2 m-0 text-sm leading-4 rounded-lg text-slate-700 dark:text-slate-100 hover:bg-slate-25 dark:hover:bg-slate-800"
|
||||
:class="computedClass"
|
||||
:to="menuItem && menuItem.toState"
|
||||
>
|
||||
<fluent-icon
|
||||
:icon="menuItem.icon"
|
||||
class="min-w-[1rem] mr-1.5 rtl:mr-0 rtl:ml-1.5"
|
||||
size="14"
|
||||
/>
|
||||
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
||||
<span
|
||||
v-if="showChildCount(menuItem.count)"
|
||||
class="px-1 py-0 mx-1 rounded-md text-xxs"
|
||||
:class="{
|
||||
'text-slate-300 dark:text-slate-600': isCountZero && !isActiveView,
|
||||
'text-slate-600 dark:text-slate-50': !isCountZero && !isActiveView,
|
||||
'bg-woot-75 dark:bg-woot-200 text-woot-600 dark:text-woot-600':
|
||||
isActiveView,
|
||||
'bg-slate-50 dark:bg-slate-700': !isActiveView,
|
||||
}"
|
||||
>
|
||||
{{ `${menuItem.count}` }}
|
||||
</span>
|
||||
<span
|
||||
v-if="menuItem.beta"
|
||||
data-view-component="true"
|
||||
label="Beta"
|
||||
class="inline-block px-1 mx-1 leading-4 text-green-500 border border-green-400 rounded-lg text-xxs"
|
||||
>
|
||||
{{ $t('SIDEBAR.BETA') }}
|
||||
</span>
|
||||
</router-link>
|
||||
|
||||
<ul v-if="hasSubMenu" class="list-none reset-base">
|
||||
<SecondaryChildNavItem
|
||||
v-for="child in menuItem.children"
|
||||
:key="child.id"
|
||||
:to="child.toState"
|
||||
:label="child.label"
|
||||
:label-color="child.color"
|
||||
:should-truncate="child.truncateLabel"
|
||||
:icon="computedInboxClass(child)"
|
||||
:warning-icon="computedInboxErrorClass(child)"
|
||||
:show-child-count="showChildCount(child.count)"
|
||||
:child-item-count="child.count"
|
||||
/>
|
||||
<Policy :permissions="['administrator']">
|
||||
<router-link
|
||||
v-if="menuItem.newLink"
|
||||
v-slot="{ href, navigate }"
|
||||
:to="menuItem.toState"
|
||||
custom
|
||||
>
|
||||
<li class="pl-1">
|
||||
<a :href="href">
|
||||
<NextButton
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
icon="i-lucide-plus"
|
||||
:label="$t(`SIDEBAR.${menuItem.newLinkTag}`)"
|
||||
:data-testid="menuItem.dataTestid"
|
||||
@click="e => newLinkClick(e, navigate)"
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
</router-link>
|
||||
</Policy>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
@@ -1,73 +0,0 @@
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { createStore } from 'vuex';
|
||||
import AccountSelector from '../AccountSelector.vue';
|
||||
import WootModal from 'dashboard/components/Modal.vue';
|
||||
import WootModalHeader from 'dashboard/components/ModalHeader.vue';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
|
||||
const store = createStore({
|
||||
modules: {
|
||||
auth: {
|
||||
namespaced: false,
|
||||
getters: {
|
||||
getCurrentAccountId: () => 1,
|
||||
getCurrentUser: () => ({
|
||||
accounts: [
|
||||
{ id: 1, name: 'Chatwoot', role: 'administrator' },
|
||||
{ id: 2, name: 'GitX', role: 'agent' },
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
globalConfig: {
|
||||
namespaced: true,
|
||||
getters: {
|
||||
get: () => ({ createNewAccountFromDashboard: false }),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('AccountSelector', () => {
|
||||
let accountSelector = null;
|
||||
|
||||
beforeEach(() => {
|
||||
accountSelector = mount(AccountSelector, {
|
||||
global: {
|
||||
plugins: [store],
|
||||
components: {
|
||||
'woot-modal': WootModal,
|
||||
'woot-modal-header': WootModalHeader,
|
||||
'fluent-icon': FluentIcon,
|
||||
},
|
||||
stubs: {
|
||||
// override global stub
|
||||
WootModalHeader: false,
|
||||
},
|
||||
},
|
||||
props: { showAccountModal: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('title and sub title exist', () => {
|
||||
const headerComponent = accountSelector.findComponent(WootModalHeader);
|
||||
const title = headerComponent.find('[data-test-id="modal-header-title"]');
|
||||
expect(title.text()).toBe('Switch account');
|
||||
const content = headerComponent.find(
|
||||
'[data-test-id="modal-header-content"]'
|
||||
);
|
||||
expect(content.text()).toBe('Select an account from the following list');
|
||||
});
|
||||
|
||||
it('first account item is checked', () => {
|
||||
const selectedAccountCheckmark = accountSelector.find(
|
||||
'#account-1 > button > svg'
|
||||
);
|
||||
expect(selectedAccountCheckmark.exists()).toBe(true);
|
||||
|
||||
const otherAccountCheckmark = accountSelector.find(
|
||||
'#account-2 > button > svg'
|
||||
);
|
||||
expect(otherAccountCheckmark.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,61 +0,0 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { createStore } from 'vuex';
|
||||
import AgentDetails from '../AgentDetails.vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
describe('AgentDetails', () => {
|
||||
const currentUser = {
|
||||
name: 'Neymar Junior',
|
||||
avatar_url: '',
|
||||
availability_status: 'online',
|
||||
};
|
||||
const currentRole = 'agent';
|
||||
let store = null;
|
||||
let agentDetails = null;
|
||||
|
||||
const mockTooltipDirective = {
|
||||
mounted: (el, binding) => {
|
||||
// You can mock the behavior here if necessary
|
||||
el.setAttribute('data-tooltip', binding.value || '');
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore({
|
||||
modules: {
|
||||
auth: {
|
||||
namespaced: false,
|
||||
getters: {
|
||||
getCurrentUser: () => currentUser,
|
||||
getCurrentRole: () => currentRole,
|
||||
getCurrentUserAvailability: () => currentUser.availability_status,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
agentDetails = shallowMount(AgentDetails, {
|
||||
global: {
|
||||
plugins: [store],
|
||||
components: {
|
||||
Thumbnail,
|
||||
NextButton,
|
||||
},
|
||||
directives: {
|
||||
tooltip: mockTooltipDirective, // Mocking the tooltip directive
|
||||
},
|
||||
stubs: { NextButton: { template: '<button><slot /></button>' } },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the correct agent status', () => {
|
||||
expect(agentDetails.findComponent(Thumbnail).vm.status).toBe('online');
|
||||
});
|
||||
|
||||
it('agent thumbnail exists', () => {
|
||||
const thumbnailComponent = agentDetails.findComponent(Thumbnail);
|
||||
expect(thumbnailComponent.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,88 +0,0 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { createStore } from 'vuex';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
import NotificationBell from '../NotificationBell.vue';
|
||||
|
||||
const $route = {
|
||||
name: 'notifications_index',
|
||||
};
|
||||
|
||||
describe('notificationBell', () => {
|
||||
const accountId = 1;
|
||||
const notificationMetadata = { unreadCount: 19 };
|
||||
let store = null;
|
||||
let actions = null;
|
||||
let modules = null;
|
||||
|
||||
beforeEach(() => {
|
||||
actions = {
|
||||
showNotification: vi.fn(),
|
||||
};
|
||||
modules = {
|
||||
auth: {
|
||||
namespaced: false,
|
||||
getters: {
|
||||
getCurrentAccountId: () => accountId,
|
||||
},
|
||||
},
|
||||
notifications: {
|
||||
namespaced: false,
|
||||
getters: {
|
||||
'notifications/getMeta': () => notificationMetadata,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
store = createStore({
|
||||
actions,
|
||||
modules,
|
||||
});
|
||||
});
|
||||
|
||||
it('it should return unread count 19', () => {
|
||||
const wrapper = shallowMount(NotificationBell, {
|
||||
global: {
|
||||
plugins: [store],
|
||||
mocks: {
|
||||
$route,
|
||||
},
|
||||
components: {
|
||||
'fluent-icon': FluentIcon,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.unreadCount).toBe('19');
|
||||
});
|
||||
|
||||
it('it should return unread count 99+', async () => {
|
||||
notificationMetadata.unreadCount = 100;
|
||||
const wrapper = shallowMount(NotificationBell, {
|
||||
global: {
|
||||
plugins: [store],
|
||||
mocks: {
|
||||
$route,
|
||||
},
|
||||
components: {
|
||||
'fluent-icon': FluentIcon,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.unreadCount).toBe('99+');
|
||||
});
|
||||
|
||||
it('isNotificationPanelActive', async () => {
|
||||
const notificationBell = shallowMount(NotificationBell, {
|
||||
global: {
|
||||
plugins: [store],
|
||||
mocks: {
|
||||
$route,
|
||||
},
|
||||
components: {
|
||||
'fluent-icon': FluentIcon,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(notificationBell.vm.isNotificationPanelActive).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { createStore } from 'vuex';
|
||||
import AvailabilityStatus from '../AvailabilityStatus.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
|
||||
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider.vue';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
|
||||
describe('AvailabilityStatus', () => {
|
||||
const currentAvailability = 'online';
|
||||
const currentAccountId = '1';
|
||||
const currentUserAutoOffline = false;
|
||||
let store = null;
|
||||
let actions = null;
|
||||
|
||||
beforeEach(() => {
|
||||
actions = {
|
||||
updateAvailability: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
store = createStore({
|
||||
modules: {
|
||||
auth: {
|
||||
namespaced: false,
|
||||
getters: {
|
||||
getCurrentUserAvailability: () => currentAvailability,
|
||||
getCurrentAccountId: () => currentAccountId,
|
||||
getCurrentUserAutoOffline: () => currentUserAutoOffline,
|
||||
},
|
||||
},
|
||||
},
|
||||
actions,
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches an action when user changes status', async () => {
|
||||
const wrapper = mount(AvailabilityStatus, {
|
||||
global: {
|
||||
plugins: [store],
|
||||
components: {
|
||||
NextButton,
|
||||
WootDropdownItem,
|
||||
WootDropdownMenu,
|
||||
WootDropdownHeader,
|
||||
WootDropdownDivider,
|
||||
FluentIcon,
|
||||
},
|
||||
stubs: {
|
||||
WootSwitch: { template: '<button />' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Ensure that the dropdown menu is opened
|
||||
await wrapper.vm.openStatusMenu();
|
||||
|
||||
// Simulate the user clicking the 3rd button (offline status)
|
||||
const buttons = wrapper.findAll('.status-change--dropdown-button');
|
||||
expect(buttons.length).toBeGreaterThan(0); // Ensure buttons exist
|
||||
|
||||
await buttons[2].trigger('click');
|
||||
|
||||
expect(actions.updateAvailability).toHaveBeenCalledTimes(1);
|
||||
expect(actions.updateAvailability.mock.calls[0][1]).toEqual({
|
||||
availability: 'offline',
|
||||
account_id: currentAccountId,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { createStore } from 'vuex';
|
||||
import SidemenuIcon from '../SidemenuIcon.vue';
|
||||
|
||||
const store = createStore({
|
||||
modules: {
|
||||
auth: {
|
||||
namespaced: false,
|
||||
getters: {
|
||||
getCurrentAccountId: () => 1,
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
namespaced: true,
|
||||
getters: {
|
||||
isFeatureEnabledonAccount: () => () => false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('SidemenuIcon', () => {
|
||||
test('matches snapshot', () => {
|
||||
const wrapper = shallowMount(SidemenuIcon, {
|
||||
stubs: { NextButton: { template: '<button><slot /></button>' } },
|
||||
global: { plugins: [store] },
|
||||
});
|
||||
expect(wrapper.vm).toBeTruthy();
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`SidemenuIcon > matches snapshot 1`] = `
|
||||
<button
|
||||
class="-ml-3"
|
||||
ghost=""
|
||||
icon="i-lucide-menu"
|
||||
size="sm"
|
||||
slate=""
|
||||
>
|
||||
|
||||
|
||||
</button>
|
||||
`;
|
||||
@@ -19,6 +19,6 @@ const isRTL = useMapGetter('accounts/isRTL');
|
||||
<slot v-if="$slots.default || content">
|
||||
<template v-if="content">{{ content }}</template>
|
||||
</slot>
|
||||
<span v-else class="text-slate-300 dark:text-slate-700"> --- </span>
|
||||
<span v-else class="text-n-slate-10"> --- </span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -118,13 +118,13 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
.banner {
|
||||
&.primary {
|
||||
@apply bg-woot-500 dark:bg-woot-500;
|
||||
@apply bg-n-brand;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
@apply bg-n-slate-3 dark:bg-n-solid-3 text-n-slate-12;
|
||||
a {
|
||||
@apply text-slate-800 dark:text-slate-800;
|
||||
@apply text-n-slate-12;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,18 +137,18 @@ export default {
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@apply bg-yellow-500 dark:bg-yellow-500 text-yellow-500 dark:text-yellow-500;
|
||||
@apply bg-n-amber-5 text-n-amber-12;
|
||||
a {
|
||||
@apply text-yellow-500 dark:text-yellow-500;
|
||||
@apply text-n-amber-12;
|
||||
}
|
||||
}
|
||||
|
||||
&.gray {
|
||||
@apply text-black-500 dark:text-black-500;
|
||||
@apply text-n-gray-10 dark:text-n-gray-10;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply ml-1 underline text-white dark:text-white text-xs;
|
||||
@apply ml-1 underline text-n-amber-12 text-xs;
|
||||
}
|
||||
|
||||
.banner-message {
|
||||
|
||||
@@ -55,7 +55,7 @@ const validateDate = () => {
|
||||
|
||||
<template>
|
||||
<div class="h-[82px] flex flex-col items-start px-5 gap-1.5 pt-4 w-full">
|
||||
<span class="text-sm font-medium text-slate-800 dark:text-slate-50">
|
||||
<span class="text-sm font-medium text-n-slate-12">
|
||||
{{
|
||||
calendarType === START_CALENDAR
|
||||
? $t('DATE_PICKER.DATE_RANGE_INPUT.START')
|
||||
|
||||
@@ -51,21 +51,13 @@ const openDatePicker = () => {
|
||||
class="inline-flex relative items-center rounded-lg gap-2 py-1.5 px-3 h-8 bg-n-alpha-2 hover:bg-n-alpha-1 active:bg-n-alpha-1"
|
||||
@click="openDatePicker"
|
||||
>
|
||||
<fluent-icon
|
||||
class="text-slate-800 dark:text-slate-50"
|
||||
icon="calendar"
|
||||
size="16"
|
||||
/>
|
||||
<span class="text-sm font-medium text-slate-800 dark:text-slate-50">
|
||||
<fluent-icon class="text-n-slate-12" icon="calendar" size="16" />
|
||||
<span class="text-sm font-medium text-n-slate-12">
|
||||
{{ $t(activeDateRange) }}
|
||||
</span>
|
||||
<span class="text-sm font-medium text-slate-600 dark:text-slate-200">
|
||||
<span class="text-sm font-medium text-n-slate-11">
|
||||
{{ formatDateRange }}
|
||||
</span>
|
||||
<fluent-icon
|
||||
class="text-slate-800 dark:text-slate-50"
|
||||
icon="chevron-down"
|
||||
size="14"
|
||||
/>
|
||||
<fluent-icon class="text-n-slate-12" icon="chevron-down" size="14" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -12,12 +12,12 @@ defineProps({
|
||||
<fluent-icon
|
||||
icon="info"
|
||||
size="14"
|
||||
class="mt-0.5 text-slate-600 absolute dark:text-slate-400"
|
||||
class="mt-0.5 text-n-slate-11 absolute"
|
||||
/>
|
||||
<div
|
||||
class="bg-white dark:bg-slate-900 w-fit ltr:left-4 rtl:right-4 top-0 border p-2.5 group-hover:flex items-center hidden absolute border-slate-75 dark:border-slate-800 rounded-lg shadow-md"
|
||||
class="bg-n-background w-fit ltr:left-4 rtl:right-4 top-0 border p-2.5 group-hover:flex items-center hidden absolute border-n-weak rounded-lg shadow-md"
|
||||
>
|
||||
<p class="text-slate-800 dark:text-slate-75 mb-0 text-xs">
|
||||
<p class="text-n-slate-12 mb-0 text-xs">
|
||||
{{ message }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -113,7 +113,7 @@ export default {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.label {
|
||||
@apply items-center font-medium text-xs rounded-[4px] gap-1 p-1 bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-100 border border-solid border-n-strong h-6;
|
||||
@apply items-center font-medium text-xs rounded-[4px] gap-1 p-1 bg-n-slate-3 text-n-slate-12 border border-solid border-n-strong h-6;
|
||||
|
||||
&.small {
|
||||
@apply text-xs py-0.5 px-1 leading-tight h-5;
|
||||
@@ -133,67 +133,71 @@ export default {
|
||||
|
||||
/* Color Schemes */
|
||||
&.primary {
|
||||
@apply bg-woot-100 dark:bg-woot-100 text-woot-900 dark:text-woot-900 border border-solid border-woot-200;
|
||||
@apply bg-n-blue-5 text-n-blue-12 border border-solid border-n-blue-7;
|
||||
|
||||
a {
|
||||
@apply text-woot-900 dark:text-woot-900;
|
||||
@apply text-n-blue-12;
|
||||
}
|
||||
.label-color-dot {
|
||||
@apply bg-woot-600 dark:bg-woot-600;
|
||||
@apply bg-n-blue-9;
|
||||
}
|
||||
}
|
||||
&.secondary {
|
||||
@apply bg-slate-100 dark:bg-slate-700 text-slate-900 dark:text-slate-100 border border-solid border-n-weak;
|
||||
@apply bg-n-slate-5 text-n-slate-12 border border-solid border-n-slate-7;
|
||||
|
||||
a {
|
||||
@apply text-slate-900 dark:text-slate-100;
|
||||
@apply text-n-slate-12;
|
||||
}
|
||||
.label-color-dot {
|
||||
@apply bg-slate-600 dark:bg-slate-600;
|
||||
@apply bg-n-slate-9;
|
||||
}
|
||||
}
|
||||
&.success {
|
||||
@apply bg-green-100 dark:bg-green-700 text-green-900 dark:text-green-100 border border-solid border-green-200 dark:border-green-600;
|
||||
@apply bg-n-teal-5 text-n-teal-12 border border-solid border-n-teal-7;
|
||||
|
||||
a {
|
||||
@apply text-green-900 dark:text-green-100;
|
||||
@apply text-n-teal-12;
|
||||
}
|
||||
.label-color-dot {
|
||||
@apply bg-green-600 dark:bg-green-600;
|
||||
@apply bg-n-teal-9;
|
||||
}
|
||||
}
|
||||
&.alert {
|
||||
@apply bg-red-100 dark:bg-red-700 text-red-900 dark:text-red-100 border border-solid border-red-200 dark:border-red-600;
|
||||
@apply bg-n-ruby-5 text-n-ruby-12 border border-solid border-n-ruby-7;
|
||||
|
||||
a {
|
||||
@apply text-red-900 dark:text-red-100;
|
||||
@apply text-n-ruby-12;
|
||||
}
|
||||
.label-color-dot {
|
||||
@apply bg-red-600 dark:bg-red-600;
|
||||
@apply bg-n-ruby-9;
|
||||
}
|
||||
}
|
||||
&.warning {
|
||||
@apply bg-yellow-100 dark:bg-yellow-700 text-yellow-900 dark:text-yellow-100 border border-solid border-yellow-200 dark:border-yellow-600;
|
||||
@apply bg-n-amber-5 text-n-amber-12 border border-solid border-n-amber-7;
|
||||
|
||||
a {
|
||||
@apply text-yellow-900 dark:text-yellow-100;
|
||||
@apply text-n-amber-12;
|
||||
}
|
||||
.label-color-dot {
|
||||
@apply bg-yellow-900 dark:bg-yellow-900;
|
||||
@apply bg-n-amber-9;
|
||||
}
|
||||
}
|
||||
|
||||
&.smooth {
|
||||
@apply bg-transparent text-slate-700 dark:text-slate-100 border border-solid border-n-strong;
|
||||
@apply bg-transparent text-n-slate-11 dark:text-n-slate-12 border border-solid border-n-strong;
|
||||
}
|
||||
|
||||
&.dashed {
|
||||
@apply bg-transparent text-slate-700 dark:text-slate-100 border border-dashed border-n-strong;
|
||||
@apply bg-transparent text-n-slate-11 dark:text-n-slate-12 border border-dashed border-n-strong;
|
||||
}
|
||||
}
|
||||
|
||||
.label-close--button {
|
||||
@apply text-slate-800 dark:text-slate-100 -mb-0.5 rounded-sm cursor-pointer flex items-center justify-center hover:bg-slate-100 dark:hover:bg-slate-700;
|
||||
@apply text-n-slate-11 -mb-0.5 rounded-sm cursor-pointer flex items-center justify-center hover:bg-n-slate-3;
|
||||
|
||||
svg {
|
||||
@apply text-n-slate-11;
|
||||
}
|
||||
}
|
||||
|
||||
.label-action--button {
|
||||
|
||||
@@ -23,16 +23,17 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col min-w-[15rem] max-h-[21.25rem] max-w-[23.75rem] rounded-md border border-solid border-n-strong"
|
||||
class="flex flex-col min-w-[15rem] max-h-[21.25rem] max-w-[23.75rem] rounded-md border border-solid"
|
||||
:class="{
|
||||
'bg-woot-25 dark:bg-n-solid-2 border border-solid border-n-blue-border':
|
||||
active,
|
||||
'bg-n-blue-1 dark:bg-n-solid-2 border-n-blue-4': active,
|
||||
'border-n-weak': !active,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between items-center rounded-t-md px-2 w-full h-10 bg-slate-50 dark:bg-slate-900 border-b border-solid border-n-strong"
|
||||
class="flex justify-between items-center rounded-t-md px-2 w-full h-10 border-b border-solid"
|
||||
:class="{
|
||||
'bg-woot-50 border-b border-solid border-n-blue-border': active,
|
||||
'bg-n-blue-2 border-n-blue-4': active,
|
||||
'bg-n-slate-2 border-n-weak': !active,
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center p-1 text-sm font-medium">{{ heading }}</div>
|
||||
@@ -41,23 +42,19 @@ export default {
|
||||
icon="checkmark-circle"
|
||||
type="solid"
|
||||
size="24"
|
||||
class="text-woot-500 dark:text-woot-500"
|
||||
class="text-n-brand"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="text-slate-700 dark:text-slate-200 text-xs leading-[1.4] px-3 pt-3 pb-0 text-start"
|
||||
class="text-n-slate-11 text-xs leading-[1.4] px-3 pt-3 pb-0 text-start"
|
||||
>
|
||||
{{ content }}
|
||||
</div>
|
||||
<div v-if="src" class="p-3">
|
||||
<img
|
||||
:src="src"
|
||||
class="border border-solid rounded-md"
|
||||
:class="
|
||||
active
|
||||
? 'border-woot-75 dark:border-woot-700'
|
||||
: 'border-slate-50 dark:border-slate-600'
|
||||
"
|
||||
class="border rounded-md"
|
||||
:class="active ? 'border-n-blue-border' : 'border-n-weak'"
|
||||
/>
|
||||
</div>
|
||||
<slot v-else />
|
||||
|
||||
@@ -29,11 +29,11 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.toggle-button {
|
||||
@apply bg-slate-200 dark:bg-slate-600;
|
||||
@apply bg-n-slate-5;
|
||||
--toggle-button-box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px,
|
||||
rgba(59, 130, 246, 0.5) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 1px 3px 0px,
|
||||
rgba(0, 0, 0, 0.06) 0px 1px 2px 0px;
|
||||
border-radius: var(--border-radius-large);
|
||||
border-radius: 0.5625rem;
|
||||
border: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
@@ -46,7 +46,7 @@ export default {
|
||||
width: 2.125rem;
|
||||
|
||||
&.active {
|
||||
background-color: var(--w-500);
|
||||
@apply bg-n-brand;
|
||||
}
|
||||
|
||||
&.small {
|
||||
@@ -54,30 +54,29 @@ export default {
|
||||
height: 0.875rem;
|
||||
|
||||
span {
|
||||
height: var(--space-one);
|
||||
width: var(--space-one);
|
||||
@apply size-2.5;
|
||||
|
||||
&.active {
|
||||
transform: translate(var(--space-small), var(--space-zero));
|
||||
@apply ltr:translate-x-[0.5rem] ltr:translate-y-0 rtl:translate-x-[-0.5rem] rtl:translate-y-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
@apply bg-white dark:bg-slate-900;
|
||||
--space-one-point-five: 0.9375rem;
|
||||
@apply bg-n-background;
|
||||
|
||||
border-radius: 100%;
|
||||
box-shadow: var(--toggle-button-box-shadow);
|
||||
display: inline-block;
|
||||
height: var(--space-one-point-five);
|
||||
height: 0.9375rem;
|
||||
transform: translate(0, 0);
|
||||
transition-duration: 200ms;
|
||||
transition-property: transform;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
width: var(--space-one-point-five);
|
||||
width: 0.9375rem;
|
||||
|
||||
&.active {
|
||||
transform: translate(var(--space-one-point-five), var(--space-zero));
|
||||
@apply ltr:translate-x-[0.9375rem] ltr:translate-y-0 rtl:translate-x-[-0.9375rem] rtl:translate-y-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,6 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isCompact: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
@@ -70,25 +66,28 @@ watch(
|
||||
<template>
|
||||
<div
|
||||
ref="tabsContainer"
|
||||
:class="{
|
||||
'tabs--container--with-border': border,
|
||||
'tabs--container--compact': isCompact,
|
||||
}"
|
||||
class="tabs--container"
|
||||
class="flex"
|
||||
:class="[border && 'border-b border-b-n-weak']"
|
||||
>
|
||||
<button
|
||||
v-if="hasScroll"
|
||||
class="tabs--scroll-button button clear secondary button--only-icon"
|
||||
class="items-center rounded-none cursor-pointer flex h-auto justify-center min-w-8"
|
||||
@click="onScrollClick('left')"
|
||||
>
|
||||
<fluent-icon icon="chevron-left" :size="16" />
|
||||
</button>
|
||||
<ul ref="tabsList" :class="{ 'tabs--with-scroll': hasScroll }" class="tabs">
|
||||
<ul
|
||||
ref="tabsList"
|
||||
class="border-r-0 border-l-0 border-t-0 flex min-w-[6.25rem] py-0 px-4 list-none mb-0"
|
||||
:class="
|
||||
hasScroll ? 'overflow-hidden py-0 px-1 max-w-[calc(100%-64px)]' : ''
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</ul>
|
||||
<button
|
||||
v-if="hasScroll"
|
||||
class="tabs--scroll-button button clear secondary button--only-icon"
|
||||
class="items-center rounded-none cursor-pointer flex h-auto justify-center min-w-8"
|
||||
@click="onScrollClick('right')"
|
||||
>
|
||||
<fluent-icon icon="chevron-right" :size="16" />
|
||||
|
||||
@@ -22,6 +22,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isCompact: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const activeIndex = inject('activeIndex');
|
||||
@@ -40,14 +44,28 @@ const onTabClick = event => {
|
||||
|
||||
<template>
|
||||
<li
|
||||
class="tabs-title"
|
||||
:class="{
|
||||
'is-active': active,
|
||||
}"
|
||||
class="flex-shrink-0 my-0 mx-2 ltr:first:ml-0 rtl:first:mr-0 ltr:last:mr-0 rtl:last:ml-0 hover:text-n-slate-12"
|
||||
>
|
||||
<a @click="onTabClick">
|
||||
<a
|
||||
class="flex items-center flex-row border-b select-none cursor-pointer text-sm relative top-[1px] transition-[border-color] duration-[150ms] ease-[cubic-bezier(0.37,0,0.63,1)]"
|
||||
:class="[
|
||||
active
|
||||
? 'border-b border-n-brand text-n-blue-text'
|
||||
: 'border-transparent text-n-slate-11',
|
||||
isCompact ? 'py-2 text-sm' : 'text-base py-3',
|
||||
]"
|
||||
@click="onTabClick"
|
||||
>
|
||||
{{ name }}
|
||||
<div v-if="showBadge" class="badge min-w-[20px]">
|
||||
<div
|
||||
v-if="showBadge"
|
||||
class="rounded-md h-5 flex items-center justify-center text-xxs font-semibold my-0 mx-1 px-1 py-0 min-w-[20px]"
|
||||
:class="[
|
||||
active
|
||||
? 'bg-n-brand/10 dark:bg-n-brand/20 text-n-blue-text'
|
||||
: 'bg-n-alpha-black2 dark:bg-n-solid-3 text-n-slate-11',
|
||||
]"
|
||||
>
|
||||
<span>
|
||||
{{ getItemCount }}
|
||||
</span>
|
||||
|
||||
@@ -113,7 +113,7 @@ export default {
|
||||
delay: { show: 1000, hide: 0 },
|
||||
hideOnClick: true,
|
||||
}"
|
||||
class="ml-auto leading-4 text-xxs text-n-slate-10 hover:text-slate-11"
|
||||
class="ml-auto leading-4 text-xxs text-n-slate-10 hover:text-n-slate-11"
|
||||
>
|
||||
<span>{{ `${createdAtTime} • ${lastActivityTime}` }}</span>
|
||||
</div>
|
||||
|
||||
@@ -44,21 +44,18 @@ export default {
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<h3
|
||||
class="text-slate-800 dark:text-slate-100 text-base font-medium pl-6 overflow-hidden whitespace-nowrap mb-1.5 text-ellipsis leading-tight"
|
||||
class="text-n-slate-12 text-base font-medium pl-6 overflow-hidden whitespace-nowrap mt-0.5 text-ellipsis leading-tight"
|
||||
>
|
||||
{{ item.title }}
|
||||
</h3>
|
||||
<span
|
||||
v-if="isOver(item)"
|
||||
class="ml-1 text-green-500 dark:text-green-500"
|
||||
>
|
||||
<span v-if="isOver(item)" class="mx-1 mt-0.5 text-n-teal-9">
|
||||
<fluent-icon icon="checkmark" />
|
||||
</span>
|
||||
</div>
|
||||
<span class="step">
|
||||
{{ items.indexOf(item) + 1 }}
|
||||
</span>
|
||||
<p class="pl-6 m-0 text-sm text-slate-600 dark:text-slate-300">
|
||||
<p class="pl-6 m-0 mt-1.5 text-sm text-n-slate-11">
|
||||
{{ item.body }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -68,7 +65,7 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
.wizard-box {
|
||||
.item {
|
||||
@apply cursor-pointer after:bg-slate-75 before:bg-slate-75 dark:after:bg-slate-600 dark:before:bg-slate-600 py-4 ltr:pr-4 rtl:pl-4 ltr:pl-6 rtl:pr-6 relative before:h-4 before:top-0 last:before:h-0 first:before:h-0 last:after:h-0 before:content-[''] before:absolute before:w-0.5 after:content-[''] after:h-full after:absolute after:top-5 after:w-0.5 rtl:after:left-6 rtl:before:left-6;
|
||||
@apply cursor-pointer after:bg-n-slate-6 before:bg-n-slate-6 py-4 ltr:pr-4 rtl:pl-4 ltr:pl-6 rtl:pr-6 relative before:h-4 before:top-0 last:before:h-0 first:before:h-0 last:after:h-0 before:content-[''] before:absolute before:w-0.5 after:content-[''] after:h-full after:absolute after:top-5 after:w-0.5 rtl:after:left-6 rtl:before:left-6;
|
||||
|
||||
&.active {
|
||||
h3 {
|
||||
@@ -97,7 +94,7 @@ export default {
|
||||
}
|
||||
|
||||
.step {
|
||||
@apply bg-slate-75 dark:bg-slate-600 rounded-2xl font-medium w-4 left-4 leading-4 z-10 absolute text-center text-white dark:text-white text-xxs top-5;
|
||||
@apply bg-n-slate-7 rounded-2xl font-medium w-4 left-4 leading-4 z-10 absolute text-center text-white dark:text-white text-xxs top-5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@ const onClick = () => {
|
||||
/>
|
||||
|
||||
<div
|
||||
class="radar-ping-animation absolute top-0 right-0 -mt-1 -mr-1 rounded-full w-3 h-3 bg-woot-500 dark:bg-woot-500"
|
||||
class="radar-ping-animation absolute top-0 right-0 -mt-1 -mr-1 rounded-full w-3 h-3 bg-n-brand"
|
||||
/>
|
||||
<div
|
||||
class="absolute top-0 right-0 -mt-1 -mr-1 rounded-full w-3 h-3 bg-woot-500 dark:bg-woot-500 opacity-50"
|
||||
class="absolute top-0 right-0 -mt-1 -mr-1 rounded-full w-3 h-3 bg-n-brand opacity-50"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -70,11 +70,11 @@ export default {
|
||||
@submit.prevent="applyText"
|
||||
>
|
||||
<div v-if="draftMessage" class="w-full">
|
||||
<h4 class="mt-1 text-base text-slate-700 dark:text-slate-100">
|
||||
<h4 class="mt-1 text-base text-n-slate-12">
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.DRAFT_TITLE') }}
|
||||
</h4>
|
||||
<p v-dompurify-html="formatMessage(draftMessage, false)" />
|
||||
<h4 class="mt-1 text-base text-slate-700 dark:text-slate-100">
|
||||
<h4 class="mt-1 text-base text-n-slate-12">
|
||||
{{
|
||||
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.GENERATED_TITLE')
|
||||
}}
|
||||
|
||||
@@ -22,23 +22,15 @@
|
||||
gap: 4px;
|
||||
|
||||
.ai-typing--icon {
|
||||
color: var(--v-500);
|
||||
@apply text-n-iris-11;
|
||||
}
|
||||
}
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-right: var(--space-smaller);
|
||||
color: var(--v-400);
|
||||
@apply text-n-iris-11 ltr:mr-1 rtl:ml-1 inline-block;
|
||||
}
|
||||
.loader {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin-right: var(--space-smaller);
|
||||
margin-top: var(--space-slab);
|
||||
background-color: var(--v-300);
|
||||
border-radius: 50%;
|
||||
animation: bubble-scale 1.2s infinite;
|
||||
@apply bg-n-iris-11 inline-block size-1.5 ltr:mr-1 rtl:ml-1 mt-3 rounded-full;
|
||||
}
|
||||
|
||||
.loader:nth-child(2) {
|
||||
|
||||
@@ -50,7 +50,7 @@ const fileName = file => {
|
||||
<div
|
||||
v-for="(attachment, index) in nonRecordedAudioAttachments"
|
||||
:key="attachment.id"
|
||||
class="preview-item flex items-center p-1 bg-slate-50 dark:bg-slate-800 gap-1 rounded-md w-[15rem] mb-1"
|
||||
class="flex items-center p-1 bg-n-slate-3 gap-1 rounded-md w-[15rem] mb-1"
|
||||
>
|
||||
<div class="max-w-[4rem] flex-shrink-0 w-6 flex items-center">
|
||||
<img
|
||||
|
||||
@@ -266,11 +266,11 @@ export default {
|
||||
@apply flex items-center justify-center relative my-2.5 mx-0;
|
||||
|
||||
.operator__line {
|
||||
@apply absolute w-full border-b border-solid border-slate-75 dark:border-slate-600;
|
||||
@apply absolute w-full border-b border-solid border-n-weak;
|
||||
}
|
||||
|
||||
.operator__select {
|
||||
margin-bottom: var(--space-zero) !important;
|
||||
margin-bottom: 0 !important;
|
||||
@apply relative w-auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,9 +56,9 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.multiselect {
|
||||
margin: var(--space-smaller) var(--space-zero);
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
textarea {
|
||||
margin-bottom: var(--space-zero);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -79,13 +79,13 @@ input[type='file'] {
|
||||
@apply hidden;
|
||||
}
|
||||
.input-wrapper {
|
||||
@apply flex h-9 bg-white dark:bg-slate-900 py-1 px-2 items-center text-xs cursor-pointer rounded-sm border border-dashed border-woot-100 dark:border-woot-500;
|
||||
@apply flex h-9 bg-n-background py-1 px-2 items-center text-xs cursor-pointer rounded-sm border border-dashed border-n-strong;
|
||||
}
|
||||
.success-icon {
|
||||
@apply text-green-500 dark:text-green-600 mr-2;
|
||||
@apply text-n-teal-9 mr-2;
|
||||
}
|
||||
.error-icon {
|
||||
@apply text-red-500 dark:text-red-600 mr-2;
|
||||
@apply text-n-ruby-9 mr-2;
|
||||
}
|
||||
|
||||
.processing {
|
||||
|
||||
@@ -50,6 +50,6 @@ export default {
|
||||
}
|
||||
}
|
||||
.avatar-container {
|
||||
@apply flex leading-[100%] font-medium items-center justify-center text-center cursor-default avatar-color dark:dark-avatar-color text-woot-600 dark:text-woot-200;
|
||||
@apply flex leading-[100%] font-medium items-center justify-center text-center cursor-default avatar-color dark:dark-avatar-color text-n-blue-text;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -48,26 +48,17 @@ useKeyboardEvents(keyboardEvents);
|
||||
<template>
|
||||
<woot-tabs
|
||||
:index="activeTabIndex"
|
||||
class="w-full px-3 -mt-1 py-0 tab--chat-type"
|
||||
class="w-full px-3 -mt-1 py-0 [&_ul]:p-0"
|
||||
@change="onTabChange"
|
||||
>
|
||||
<woot-tabs-item
|
||||
v-for="(item, index) in items"
|
||||
:key="item.key"
|
||||
class="text-sm"
|
||||
class="text-sm [&_a]:font-medium"
|
||||
:index="index"
|
||||
:name="item.name"
|
||||
:count="item.count"
|
||||
is-compact
|
||||
/>
|
||||
</woot-tabs>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tab--chat-type {
|
||||
::v-deep {
|
||||
.tabs {
|
||||
@apply p-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -52,19 +52,16 @@ export default {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'dashboard/assets/scss/variables';
|
||||
@import 'dashboard/assets/scss/mixins';
|
||||
|
||||
.colorpicker {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.colorpicker--selected {
|
||||
@apply border border-solid border-slate-50 dark:border-slate-600 rounded cursor-pointer h-8 w-8 mb-4;
|
||||
@apply border border-solid border-n-weak rounded cursor-pointer h-8 w-8 mb-4;
|
||||
}
|
||||
|
||||
.colorpicker--chrome.vc-chrome {
|
||||
@apply shadow-lg -mt-2.5 absolute z-[9999] border border-solid border-slate-75 dark:border-slate-600 rounded;
|
||||
@apply shadow-lg -mt-2.5 absolute z-[9999] border border-solid border-n-weak rounded;
|
||||
|
||||
::v-deep {
|
||||
input {
|
||||
|
||||
@@ -17,7 +17,7 @@ export default {
|
||||
</h3>
|
||||
<p
|
||||
v-if="message"
|
||||
class="block text-center text-n-slate-11 dark:text-slate-400 my-4 mx-auto w-[90%]"
|
||||
class="block text-center text-n-slate-11 my-4 mx-auto w-[90%]"
|
||||
>
|
||||
{{ message }}
|
||||
</p>
|
||||
|
||||
@@ -264,12 +264,10 @@ export default {
|
||||
v-if="showQueryOperator"
|
||||
class="flex items-center justify-center relative my-2.5 mx-0"
|
||||
>
|
||||
<hr
|
||||
class="absolute w-full border-b border-solid border-slate-75 dark:border-slate-800"
|
||||
/>
|
||||
<hr class="absolute w-full border-b border-solid border-n-weak" />
|
||||
<select
|
||||
v-model="query_operator"
|
||||
class="relative w-auto mb-0 bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 border-slate-75 dark:border-slate-600"
|
||||
class="relative w-auto mb-0 bg-n-background text-n-slate-12 border-n-weak"
|
||||
>
|
||||
<option value="and">
|
||||
{{ $t('FILTER.QUERY_DROPDOWN_LABELS.AND') }}
|
||||
@@ -285,7 +283,7 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
.filter__answer--wrap {
|
||||
input {
|
||||
@apply bg-white dark:bg-slate-900 mb-0 text-slate-800 dark:text-slate-100 border-slate-75 dark:border-slate-600;
|
||||
@apply bg-n-background mb-0 text-n-slate-12 border-n-weak;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
message: { type: String, default: '' },
|
||||
},
|
||||
};
|
||||
<script setup>
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
|
||||
defineProps({
|
||||
message: { type: String, default: '' },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-center p-8">
|
||||
<h6
|
||||
class="flex items-center gap-2 text-base text-center w-100 text-slate-800 dark:text-slate-300"
|
||||
class="flex items-center gap-3 text-base text-center w-100 text-n-slate-11"
|
||||
>
|
||||
<span class="text-base font-medium text-slate-800 dark:text-slate-100">
|
||||
<span class="text-base font-medium text-n-slate-12">
|
||||
{{ message }}
|
||||
</span>
|
||||
<span class="spinner" />
|
||||
<Spinner class="text-n-brand" />
|
||||
</h6>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -16,13 +16,10 @@ export default {
|
||||
<template>
|
||||
<div class="border-b border-solid border-n-weak/60">
|
||||
<div class="max-w-6xl w-full mx-auto pt-4 pb-0 px-8">
|
||||
<h2 class="text-2xl text-slate-800 dark:text-slate-100 mb-1 font-medium">
|
||||
<h2 class="text-2xl text-n-slate-12 mb-1 font-medium">
|
||||
{{ headerTitle }}
|
||||
</h2>
|
||||
<p
|
||||
v-if="headerContent"
|
||||
class="w-full text-slate-600 dark:text-slate-300 text-sm mb-2"
|
||||
>
|
||||
<p v-if="headerContent" class="w-full text-n-slate-11 text-sm mb-2">
|
||||
{{ headerContent }}
|
||||
</p>
|
||||
<slot />
|
||||
|
||||
@@ -37,7 +37,7 @@ const toggleShowMore = () => {
|
||||
{{ textToBeDisplayed }}
|
||||
<button
|
||||
v-if="text.length > limit"
|
||||
class="text-woot-500 !p-0 !border-0 align-top"
|
||||
class="text-n-brand !p-0 !border-0 align-top"
|
||||
@click="toggleShowMore"
|
||||
>
|
||||
{{ buttonLabel }}
|
||||
|
||||
@@ -16,7 +16,7 @@ defineProps({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="text-sm text-slate-700 dark:text-slate-200 font-medium">
|
||||
<span class="text-sm text-n-slate-11 font-medium">
|
||||
{{
|
||||
$t('GENERAL.SHOWING_RESULTS', {
|
||||
firstIndex,
|
||||
|
||||
@@ -93,7 +93,7 @@ export default {
|
||||
},
|
||||
thumbnailClass() {
|
||||
const className = this.hasBorder
|
||||
? 'border border-solid border-white dark:border-slate-700/50'
|
||||
? 'border border-solid border-white dark:border-n-weak'
|
||||
: '';
|
||||
const variant =
|
||||
this.variant === 'circle' ? 'thumbnail-rounded' : 'thumbnail-square';
|
||||
@@ -183,7 +183,7 @@ export default {
|
||||
.user-thumbnail {
|
||||
border-radius: 50%;
|
||||
&.thumbnail-square {
|
||||
border-radius: var(--border-radius-large);
|
||||
border-radius: 0.5625rem;
|
||||
}
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
@@ -193,20 +193,16 @@ export default {
|
||||
}
|
||||
|
||||
.source-badge {
|
||||
border-radius: var(--border-radius-small);
|
||||
bottom: var(--space-minus-micro);
|
||||
box-shadow: var(--shadow-small);
|
||||
height: var(--space-slab);
|
||||
padding: var(--space-micro);
|
||||
border-radius: 0.1875rem;
|
||||
bottom: -0.125rem;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: var(--space-slab);
|
||||
@apply bg-white dark:bg-slate-900;
|
||||
@apply bg-n-background p-0.5 size-3;
|
||||
}
|
||||
|
||||
.user-online-status {
|
||||
border-radius: 50%;
|
||||
bottom: var(--space-micro);
|
||||
@apply bottom-0.5 rounded-full;
|
||||
|
||||
&:after {
|
||||
content: ' ';
|
||||
@@ -214,15 +210,15 @@ export default {
|
||||
}
|
||||
|
||||
.user-online-status--online {
|
||||
@apply bg-green-400 dark:bg-green-400;
|
||||
@apply bg-n-teal-10;
|
||||
}
|
||||
|
||||
.user-online-status--busy {
|
||||
@apply bg-yellow-500 dark:bg-yellow-500;
|
||||
@apply bg-n-amber-10;
|
||||
}
|
||||
|
||||
.user-online-status--offline {
|
||||
@apply bg-slate-500 dark:bg-slate-500;
|
||||
@apply bg-n-slate-10;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -60,31 +60,19 @@ export default {
|
||||
|
||||
.overlapping-thumbnail {
|
||||
position: relative;
|
||||
box-shadow: var(--shadow-small);
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: var(--space-minus-smaller);
|
||||
margin-left: -0.25rem;
|
||||
}
|
||||
|
||||
.gap-tight {
|
||||
margin-left: var(--space-minus-small);
|
||||
margin-left: -0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail-more-text {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
margin-left: var(--space-minus-small);
|
||||
padding: 0 var(--space-small);
|
||||
box-shadow: var(--shadow-small);
|
||||
background: var(--color-background);
|
||||
border-radius: var(--space-giga);
|
||||
border: 1px solid var(--white);
|
||||
|
||||
color: var(--s-600);
|
||||
font-size: var(--font-size-mini);
|
||||
font-weight: var(--font-weight-medium);
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
@apply text-n-slate-11 bg-n-slate-4 border border-n-weak text-xs font-medium rounded-full px-2 ltr:-ml-2 rtl:-mr-2 inline-flex items-center relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -709,7 +709,7 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
|
||||
<div ref="editor" />
|
||||
<div
|
||||
v-show="isImageNodeSelected && showImageResizeToolbar"
|
||||
class="absolute shadow-md rounded-[4px] flex gap-1 py-1 px-1 bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-50"
|
||||
class="absolute shadow-md rounded-[6px] flex gap-1 py-1 px-1 bg-n-solid-3 outline outline-1 outline-n-weak text-n-slate-12"
|
||||
:style="{
|
||||
top: toolbarPosition.top,
|
||||
left: toolbarPosition.left,
|
||||
@@ -718,7 +718,7 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
|
||||
<button
|
||||
v-for="size in sizes"
|
||||
:key="size.name"
|
||||
class="text-xs font-medium rounded-[4px] border border-solid border-slate-200 dark:border-slate-600 px-1.5 py-0.5 hover:bg-slate-100 dark:hover:bg-slate-800"
|
||||
class="text-xs font-medium rounded-[4px] outline outline-1 outline-n-strong px-1.5 py-0.5 hover:bg-n-slate-5"
|
||||
@click="setURLWithQueryAndImageSize(size)"
|
||||
>
|
||||
{{ size.name }}
|
||||
@@ -735,16 +735,16 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
|
||||
@apply flex flex-col;
|
||||
|
||||
.ProseMirror-menubar {
|
||||
min-height: var(--space-two) !important;
|
||||
min-height: 1.25rem !important;
|
||||
@apply -ml-2.5 pb-0 bg-transparent text-n-slate-11;
|
||||
|
||||
.ProseMirror-menu-active {
|
||||
@apply bg-slate-75 dark:bg-slate-800;
|
||||
@apply bg-n-slate-5 dark:bg-n-solid-3;
|
||||
}
|
||||
}
|
||||
|
||||
> .ProseMirror {
|
||||
@apply p-0 break-words text-slate-800 dark:text-slate-100;
|
||||
@apply p-0 break-words text-n-slate-12;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
@@ -753,14 +753,14 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
|
||||
h5,
|
||||
h6,
|
||||
p {
|
||||
@apply text-slate-800 dark:text-slate-100;
|
||||
@apply text-n-slate-12;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
@apply border-slate-400 dark:border-slate-500;
|
||||
@apply border-n-slate-7;
|
||||
|
||||
p {
|
||||
@apply text-slate-600 dark:text-slate-400;
|
||||
@apply text-n-slate-11;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -825,6 +825,6 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
|
||||
}
|
||||
|
||||
.editor-warning__message {
|
||||
@apply text-red-400 dark:text-red-400 font-normal text-sm pt-1 pb-0 px-0;
|
||||
@apply text-n-ruby-9 dark:text-n-ruby-9 font-normal text-sm pt-1 pb-0 px-0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -355,10 +355,10 @@ export default {
|
||||
<transition name="modal-fade">
|
||||
<div
|
||||
v-show="uploadRef && uploadRef.dropActive"
|
||||
class="fixed top-0 bottom-0 left-0 right-0 z-20 flex flex-col items-center justify-center w-full h-full gap-2 text-slate-900 dark:text-slate-50 bg-modal-backdrop-light dark:bg-modal-backdrop-dark"
|
||||
class="fixed top-0 bottom-0 left-0 right-0 z-20 flex flex-col items-center justify-center w-full h-full gap-2 text-n-slate-12 bg-modal-backdrop-light dark:bg-modal-backdrop-dark"
|
||||
>
|
||||
<fluent-icon icon="cloud-backup" size="40" />
|
||||
<h4 class="text-2xl break-words text-slate-900 dark:text-slate-50">
|
||||
<h4 class="text-2xl break-words text-n-slate-12">
|
||||
{{ $t('CONVERSATION.REPLYBOX.DRAG_DROP') }}
|
||||
</h4>
|
||||
</div>
|
||||
@@ -402,7 +402,7 @@ export default {
|
||||
}
|
||||
|
||||
&:hover button {
|
||||
@apply dark:bg-slate-800 bg-slate-100;
|
||||
@apply enabled:bg-n-slate-9/20;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -73,7 +73,7 @@ export default {
|
||||
};
|
||||
},
|
||||
charLengthClass() {
|
||||
return this.charactersRemaining < 0 ? 'text-red-600' : 'text-slate-600';
|
||||
return this.charactersRemaining < 0 ? 'text-n-ruby-9' : 'text-n-slate-11';
|
||||
},
|
||||
characterLengthWarning() {
|
||||
return this.charactersRemaining < 0
|
||||
|
||||
@@ -52,13 +52,13 @@ onMounted(() => {
|
||||
>
|
||||
<template #default="{ item, selected }">
|
||||
<span
|
||||
class="max-w-full inline-flex items-center gap-0.5 min-w-0 mb-0 text-sm font-medium text-slate-900 dark:text-slate-100 group-hover:text-woot-500 dark:group-hover:text-woot-500 truncate"
|
||||
class="max-w-full inline-flex items-center gap-0.5 min-w-0 mb-0 text-sm font-medium text-n-slate-12 group-hover:text-n-brand truncate"
|
||||
>
|
||||
{{ item.emoji }}
|
||||
<p
|
||||
class="relative mb-0 truncate bottom-px"
|
||||
:class="{
|
||||
'text-woot-500 dark:text-woot-500': selected,
|
||||
'text-n-brand': selected,
|
||||
'font-normal': !selected,
|
||||
}"
|
||||
>
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
status: { type: String, default: '' },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="`status-badge status-badge__${status} rounded-full w-2.5 h-2.5 mr-0.5 rtl:mr-0 rtl:ml-0.5 inline-flex`"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.status-badge {
|
||||
&__online {
|
||||
@apply bg-green-400;
|
||||
}
|
||||
&__offline {
|
||||
@apply bg-slate-500;
|
||||
}
|
||||
&__busy {
|
||||
@apply bg-yellow-500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1 +0,0 @@
|
||||
<!-- // not using this component -->
|
||||
@@ -91,7 +91,7 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="conversation-details-wrap bg-n-background relative"
|
||||
class="conversation-details-wrap flex flex-col min-w-0 w-full bg-n-background relative"
|
||||
:class="{
|
||||
'border-l rtl:border-l-0 rtl:border-r border-n-weak': !isOnExpandedLayout,
|
||||
}"
|
||||
@@ -104,7 +104,7 @@ export default {
|
||||
<woot-tabs
|
||||
v-if="dashboardApps.length && currentChat.id"
|
||||
:index="activeIndex"
|
||||
class="-mt-px dashboard-app--tabs border-t border-t-n-background"
|
||||
class="-mt-px border-t border-t-n-background"
|
||||
@change="onDashboardAppTabChange"
|
||||
>
|
||||
<woot-tabs-item
|
||||
@@ -113,6 +113,8 @@ export default {
|
||||
:index="tab.index"
|
||||
:name="tab.name"
|
||||
:show-badge="false"
|
||||
is-compact
|
||||
class="[&_a]:pt-1"
|
||||
/>
|
||||
</woot-tabs>
|
||||
<div v-show="!activeIndex" class="flex h-full min-h-0 m-0">
|
||||
@@ -138,19 +140,3 @@ export default {
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.conversation-details-wrap {
|
||||
@apply flex flex-col min-w-0 w-full;
|
||||
}
|
||||
|
||||
.dashboard-app--tabs {
|
||||
::v-deep {
|
||||
.tabs-title {
|
||||
a {
|
||||
@apply pb-2 pt-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -328,9 +328,7 @@ export default {
|
||||
{{ $t(`CHAT_LIST.NO_MESSAGES`) }}
|
||||
</span>
|
||||
</p>
|
||||
<div
|
||||
class="absolute flex flex-col conversation--meta ltr:right-4 rtl:left-4 top-4"
|
||||
>
|
||||
<div class="absolute flex flex-col mt-4 ltr:right-4 rtl:left-4 top-4">
|
||||
<span class="ml-auto font-normal leading-4 text-xxs">
|
||||
<TimeAgo
|
||||
:last-activity-timestamp="chat.timestamp"
|
||||
@@ -338,7 +336,7 @@ export default {
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="unread shadow-lg rounded-full hidden text-xxs font-semibold h-4 leading-4 ml-auto mt-1 min-w-[1rem] px-1 py-0 text-center text-white bg-n-teal-9"
|
||||
class="unread shadow-lg rounded-full hidden text-xxs font-semibold h-4 leading-4 ltr:ml-auto rtl:mr-auto mt-1 min-w-[1rem] px-1 py-0 text-center text-white bg-n-teal-9"
|
||||
>
|
||||
{{ unreadCount > 9 ? '9+' : unreadCount }}
|
||||
</span>
|
||||
|
||||
@@ -23,9 +23,7 @@ export default {
|
||||
src="dashboard/assets/images/no-chat.svg"
|
||||
alt="No Chat"
|
||||
/>
|
||||
<span
|
||||
class="text-sm text-slate-800 dark:text-slate-200 font-medium text-center"
|
||||
>
|
||||
<span class="text-sm text-n-slate-12 font-medium text-center">
|
||||
{{ message }}
|
||||
<br />
|
||||
</span>
|
||||
|
||||
@@ -1,800 +0,0 @@
|
||||
<script>
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import BubbleActions from './bubble/Actions.vue';
|
||||
import BubbleContact from './bubble/Contact.vue';
|
||||
import BubbleFile from './bubble/File.vue';
|
||||
import BubbleImageAudioVideo from './bubble/ImageAudioVideo.vue';
|
||||
import BubbleIntegration from './bubble/Integration.vue';
|
||||
import BubbleLocation from './bubble/Location.vue';
|
||||
import BubbleMailHead from './bubble/MailHead.vue';
|
||||
import BubbleReplyTo from './bubble/ReplyTo.vue';
|
||||
import BubbleText from './bubble/Text.vue';
|
||||
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
|
||||
import InstagramStory from './bubble/InstagramStory.vue';
|
||||
import InstagramStoryReply from './bubble/InstagramStoryReply.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import { CONTENT_TYPES } from 'shared/constants/contentType';
|
||||
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
||||
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
import { getDayDifferenceFromNow } from 'shared/helpers/DateHelper';
|
||||
import * as Sentry from '@sentry/vue';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BubbleActions,
|
||||
BubbleContact,
|
||||
BubbleFile,
|
||||
BubbleImageAudioVideo,
|
||||
BubbleIntegration,
|
||||
BubbleLocation,
|
||||
BubbleMailHead,
|
||||
BubbleReplyTo,
|
||||
BubbleText,
|
||||
ContextMenu,
|
||||
InstagramStory,
|
||||
InstagramStoryReply,
|
||||
Spinner,
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isATweet: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isAFacebookInbox: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isInstagram: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isAWhatsAppChannel: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isAnEmailInbox: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
inboxSupportsReplyTo: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
inReplyTo: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showContextMenu: false,
|
||||
hasMediaLoadError: false,
|
||||
contextMenuPosition: {},
|
||||
showBackgroundHighlight: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
attachments() {
|
||||
// Here it is used to get sender and created_at for each attachment
|
||||
return this.data?.attachments.map(attachment => ({
|
||||
...attachment,
|
||||
sender: this.data.sender || {},
|
||||
created_at: this.data.created_at || '',
|
||||
}));
|
||||
},
|
||||
hasOneDayPassed() {
|
||||
// Disable retry button if the message is failed and the message is older than 24 hours
|
||||
return getDayDifferenceFromNow(new Date(), this.data?.created_at) >= 1;
|
||||
},
|
||||
shouldRenderMessage() {
|
||||
return (
|
||||
this.hasAttachments ||
|
||||
this.data.content ||
|
||||
this.isEmailContentType ||
|
||||
this.isUnsupported ||
|
||||
this.isAnIntegrationMessage
|
||||
);
|
||||
},
|
||||
emailMessageContent() {
|
||||
const {
|
||||
html_content: { full: fullHTMLContent } = {},
|
||||
text_content: { full: fullTextContent } = {},
|
||||
} = this.contentAttributes.email || {};
|
||||
|
||||
if (fullHTMLContent) {
|
||||
return fullHTMLContent;
|
||||
}
|
||||
|
||||
if (fullTextContent) {
|
||||
return fullTextContent.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
displayQuotedButton() {
|
||||
if (this.emailMessageContent.includes('<blockquote')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.isIncoming) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
message() {
|
||||
// If the message is an email, emailMessageContent would be present
|
||||
// In that case, we would use letter package to render the email
|
||||
if (this.emailMessageContent && this.isIncoming) {
|
||||
return this.emailMessageContent;
|
||||
}
|
||||
|
||||
const botMessageContent = generateBotMessageContent(
|
||||
this.contentType,
|
||||
this.contentAttributes,
|
||||
{
|
||||
noResponseText: this.$t('CONVERSATION.NO_RESPONSE'),
|
||||
csat: {
|
||||
ratingTitle: this.$t('CONVERSATION.RATING_TITLE'),
|
||||
feedbackTitle: this.$t('CONVERSATION.FEEDBACK_TITLE'),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (this.contentType === 'input_csat') {
|
||||
return this.$t('CONVERSATION.CSAT_REPLY_MESSAGE') + botMessageContent;
|
||||
}
|
||||
|
||||
return (
|
||||
this.formatMessage(
|
||||
this.data.content,
|
||||
this.isATweet,
|
||||
this.data.private
|
||||
) + botMessageContent
|
||||
);
|
||||
},
|
||||
inReplyToMessageId() {
|
||||
// Why not use the inReplyTo object directly?
|
||||
// Glad you asked! The inReplyTo object may or may not be available
|
||||
// depending on the current scroll position of the message list
|
||||
// since old messages are only loaded when the user scrolls up
|
||||
return this.data.content_attributes?.in_reply_to;
|
||||
},
|
||||
isAnInstagramStory() {
|
||||
return this.contentAttributes.image_type === 'story_mention';
|
||||
},
|
||||
contextMenuEnabledOptions() {
|
||||
return {
|
||||
copy: this.hasText,
|
||||
delete:
|
||||
(this.hasText || this.hasAttachments) &&
|
||||
!this.isMessageDeleted &&
|
||||
!this.isFailed,
|
||||
cannedResponse:
|
||||
this.isOutgoing && this.hasText && !this.isMessageDeleted,
|
||||
copyLink: !this.isFailed || !this.isProcessing,
|
||||
translate:
|
||||
(!this.isFailed || !this.isProcessing) &&
|
||||
!this.isMessageDeleted &&
|
||||
this.hasText,
|
||||
replyTo: !this.data.private && this.inboxSupportsReplyTo.outgoing,
|
||||
};
|
||||
},
|
||||
contentAttributes() {
|
||||
return this.data.content_attributes || {};
|
||||
},
|
||||
externalError() {
|
||||
return this.contentAttributes.external_error || '';
|
||||
},
|
||||
sender() {
|
||||
return this.data.sender || {};
|
||||
},
|
||||
status() {
|
||||
return this.data.status;
|
||||
},
|
||||
storySender() {
|
||||
return this.contentAttributes.story_sender || null;
|
||||
},
|
||||
storyId() {
|
||||
return this.contentAttributes.story_id || null;
|
||||
},
|
||||
storyUrl() {
|
||||
return this.contentAttributes.story_url || null;
|
||||
},
|
||||
contentType() {
|
||||
const {
|
||||
data: { content_type: contentType },
|
||||
} = this;
|
||||
return contentType;
|
||||
},
|
||||
twitterProfileLink() {
|
||||
const additionalAttributes = this.sender.additional_attributes || {};
|
||||
const { screen_name: screenName } = additionalAttributes;
|
||||
return `https://twitter.com/${screenName}`;
|
||||
},
|
||||
alignBubble() {
|
||||
const { message_type: messageType } = this.data;
|
||||
const isCentered = messageType === MESSAGE_TYPE.ACTIVITY;
|
||||
const isLeftAligned = messageType === MESSAGE_TYPE.INCOMING;
|
||||
const isRightAligned =
|
||||
messageType === MESSAGE_TYPE.OUTGOING ||
|
||||
messageType === MESSAGE_TYPE.TEMPLATE;
|
||||
return {
|
||||
center: isCentered,
|
||||
left: isLeftAligned,
|
||||
right: isRightAligned,
|
||||
'has-context-menu': this.showContextMenu,
|
||||
// this handles the offset required to align the context menu button
|
||||
// extra alignment is required since a tweet message has a the user name and avatar below it
|
||||
'has-tweet-menu': this.isATweet,
|
||||
'has-bg': this.showBackgroundHighlight,
|
||||
};
|
||||
},
|
||||
createdAt() {
|
||||
return this.contentAttributes.external_created_at || this.data.created_at;
|
||||
},
|
||||
isBubble() {
|
||||
return [0, 1, 3].includes(this.data.message_type);
|
||||
},
|
||||
isIncoming() {
|
||||
return this.data.message_type === MESSAGE_TYPE.INCOMING;
|
||||
},
|
||||
isOutgoing() {
|
||||
return this.data.message_type === MESSAGE_TYPE.OUTGOING;
|
||||
},
|
||||
isTemplate() {
|
||||
return this.data.message_type === MESSAGE_TYPE.TEMPLATE;
|
||||
},
|
||||
isAnIntegrationMessage() {
|
||||
return this.contentType === 'integrations';
|
||||
},
|
||||
emailHeadAttributes() {
|
||||
return {
|
||||
email: this.contentAttributes.email,
|
||||
cc: this.contentAttributes.cc_emails,
|
||||
bcc: this.contentAttributes.bcc_emails,
|
||||
};
|
||||
},
|
||||
hasAttachments() {
|
||||
return !!(this.data.attachments && this.data.attachments.length > 0);
|
||||
},
|
||||
isMessageDeleted() {
|
||||
return this.contentAttributes.deleted;
|
||||
},
|
||||
hasText() {
|
||||
return !!this.data.content;
|
||||
},
|
||||
tooltipForSender() {
|
||||
const name = this.senderNameForAvatar;
|
||||
const { message_type: messageType } = this.data;
|
||||
const showTooltip =
|
||||
messageType === MESSAGE_TYPE.OUTGOING ||
|
||||
messageType === MESSAGE_TYPE.TEMPLATE;
|
||||
return showTooltip
|
||||
? {
|
||||
content: `${this.$t('CONVERSATION.SENT_BY')} ${name}`,
|
||||
}
|
||||
: false;
|
||||
},
|
||||
errorMessageTooltip() {
|
||||
if (this.isFailed) {
|
||||
return this.externalError || this.$t(`CONVERSATION.SEND_FAILED`);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
wrapClass() {
|
||||
return {
|
||||
wrap: this.isBubble,
|
||||
'activity-wrap': !this.isBubble,
|
||||
'is-pending': this.isPending,
|
||||
'is-failed': this.isFailed,
|
||||
'is-email': this.isEmailContentType,
|
||||
};
|
||||
},
|
||||
bubbleClass() {
|
||||
return {
|
||||
bubble: this.isBubble,
|
||||
'is-private': this.data.private,
|
||||
'is-unsupported': this.isUnsupported,
|
||||
'is-image': this.hasMediaAttachment('image'),
|
||||
'is-video': this.hasMediaAttachment('video'),
|
||||
'is-text': this.hasText,
|
||||
'is-from-bot': this.isSentByBot,
|
||||
'is-failed': this.isFailed,
|
||||
'is-email': this.isEmailContentType,
|
||||
};
|
||||
},
|
||||
isUnsupported() {
|
||||
return this.contentAttributes.is_unsupported ?? false;
|
||||
},
|
||||
isPending() {
|
||||
return this.data.status === MESSAGE_STATUS.PROGRESS;
|
||||
},
|
||||
isFailed() {
|
||||
return this.data.status === MESSAGE_STATUS.FAILED;
|
||||
},
|
||||
isSentByBot() {
|
||||
if (this.isPending || this.isFailed) return false;
|
||||
return !this.sender.type || this.sender.type === 'agent_bot';
|
||||
},
|
||||
shouldShowContextMenu() {
|
||||
return !this.isUnsupported;
|
||||
},
|
||||
showAvatar() {
|
||||
if (this.isOutgoing || this.isTemplate) {
|
||||
return true;
|
||||
}
|
||||
return this.isATweet && this.isIncoming && this.sender;
|
||||
},
|
||||
senderNameForAvatar() {
|
||||
if (this.isOutgoing || this.isTemplate) {
|
||||
const { name = this.$t('CONVERSATION.BOT') } = this.sender || {};
|
||||
return name;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
isEmailContentType() {
|
||||
return this.contentType === CONTENT_TYPES.INCOMING_EMAIL;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
data() {
|
||||
this.hasMediaLoadError = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.hasMediaLoadError = false;
|
||||
emitter.on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
|
||||
this.setupHighlightTimer();
|
||||
},
|
||||
unmounted() {
|
||||
emitter.off(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
|
||||
clearTimeout(this.higlightTimeout);
|
||||
},
|
||||
methods: {
|
||||
isAttachmentImageVideoAudio(fileType) {
|
||||
return ['image', 'audio', 'video', 'story_mention', 'ig_reel'].includes(
|
||||
fileType
|
||||
);
|
||||
},
|
||||
hasMediaAttachment(type) {
|
||||
if (this.hasAttachments && this.data.attachments.length > 0) {
|
||||
return this.compareMessageFileType(this.data, type);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
compareMessageFileType(messageData, type) {
|
||||
try {
|
||||
const { attachments = [{}] } = messageData;
|
||||
const { file_type: fileType } = attachments[0];
|
||||
return fileType === type && !this.hasMediaLoadError;
|
||||
} catch (err) {
|
||||
Sentry.setContext('attachment-parsing-error', {
|
||||
messageData,
|
||||
type,
|
||||
hasMediaLoadError: this.hasMediaLoadError,
|
||||
});
|
||||
|
||||
Sentry.captureException(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
handleContextMenuClick() {
|
||||
this.showContextMenu = !this.showContextMenu;
|
||||
},
|
||||
async retrySendMessage() {
|
||||
await this.$store.dispatch('sendMessageWithData', this.data);
|
||||
},
|
||||
onMediaLoadError() {
|
||||
this.hasMediaLoadError = true;
|
||||
},
|
||||
openContextMenu(e) {
|
||||
const shouldSkipContextMenu =
|
||||
e.target?.classList.contains('skip-context-menu') ||
|
||||
e.target?.tagName.toLowerCase() === 'a';
|
||||
if (shouldSkipContextMenu || getSelection().toString()) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
if (e.type === 'contextmenu') {
|
||||
useTrack(ACCOUNT_EVENTS.OPEN_MESSAGE_CONTEXT_MENU);
|
||||
}
|
||||
this.contextMenuPosition = {
|
||||
x: e.pageX || e.clientX,
|
||||
y: e.pageY || e.clientY,
|
||||
};
|
||||
this.showContextMenu = true;
|
||||
},
|
||||
closeContextMenu() {
|
||||
this.showContextMenu = false;
|
||||
this.contextMenuPosition = { x: null, y: null };
|
||||
},
|
||||
handleReplyTo() {
|
||||
const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO;
|
||||
const { conversation_id: conversationId, id: replyTo } = this.data;
|
||||
|
||||
LocalStorage.updateJsonStore(replyStorageKey, conversationId, replyTo);
|
||||
emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.data);
|
||||
},
|
||||
setupHighlightTimer() {
|
||||
if (Number(this.$route.query.messageId) !== Number(this.data.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showBackgroundHighlight = true;
|
||||
const HIGHLIGHT_TIMER = 1000;
|
||||
this.higlightTimeout = setTimeout(() => {
|
||||
this.showBackgroundHighlight = false;
|
||||
}, HIGHLIGHT_TIMER);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||
<template>
|
||||
<li
|
||||
v-if="shouldRenderMessage"
|
||||
:id="`message${data.id}`"
|
||||
class="group/context-menu"
|
||||
:class="[alignBubble]"
|
||||
>
|
||||
<div :class="wrapClass">
|
||||
<div
|
||||
v-if="isFailed && !hasOneDayPassed && !isAnEmailInbox"
|
||||
class="message-failed--alert"
|
||||
>
|
||||
<NextButton
|
||||
v-tooltip.top-end="$t('CONVERSATION.TRY_AGAIN')"
|
||||
ghost
|
||||
xs
|
||||
ruby
|
||||
icon="i-lucide-refresh-ccw"
|
||||
@click="retrySendMessage"
|
||||
/>
|
||||
</div>
|
||||
<div :class="bubbleClass" @contextmenu="openContextMenu($event)">
|
||||
<BubbleMailHead
|
||||
:email-attributes="contentAttributes.email"
|
||||
:cc="emailHeadAttributes.cc"
|
||||
:bcc="emailHeadAttributes.bcc"
|
||||
:is-incoming="isIncoming"
|
||||
/>
|
||||
<InstagramStoryReply v-if="storyUrl" :story-url="storyUrl" />
|
||||
<BubbleReplyTo
|
||||
v-if="inReplyToMessageId && inboxSupportsReplyTo.incoming"
|
||||
:message="inReplyTo"
|
||||
:message-type="data.message_type"
|
||||
:parent-has-attachments="hasAttachments"
|
||||
/>
|
||||
<div v-if="isUnsupported">
|
||||
<template v-if="isAFacebookInbox && isInstagram">
|
||||
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE_INSTAGRAM') }}
|
||||
</template>
|
||||
<template v-else-if="isAFacebookInbox">
|
||||
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE_FACEBOOK') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE') }}
|
||||
</template>
|
||||
</div>
|
||||
<BubbleText
|
||||
v-else-if="data.content"
|
||||
:message="message"
|
||||
:is-email="isEmailContentType"
|
||||
:display-quoted-button="displayQuotedButton"
|
||||
/>
|
||||
<BubbleIntegration
|
||||
:message-id="data.id"
|
||||
:content-attributes="contentAttributes"
|
||||
:inbox-id="data.inbox_id"
|
||||
/>
|
||||
<span
|
||||
v-if="isPending && hasAttachments"
|
||||
class="chat-bubble has-attachment agent"
|
||||
>
|
||||
{{ $t('CONVERSATION.UPLOADING_ATTACHMENTS') }}
|
||||
</span>
|
||||
<div v-if="!isPending && hasAttachments">
|
||||
<div v-for="attachment in attachments" :key="attachment.id">
|
||||
<InstagramStory
|
||||
v-if="isAnInstagramStory"
|
||||
:story-url="attachment.data_url"
|
||||
@error="onMediaLoadError"
|
||||
/>
|
||||
<BubbleImageAudioVideo
|
||||
v-else-if="isAttachmentImageVideoAudio(attachment.file_type)"
|
||||
:attachment="attachment"
|
||||
@error="onMediaLoadError"
|
||||
/>
|
||||
<BubbleLocation
|
||||
v-else-if="attachment.file_type === 'location'"
|
||||
:latitude="attachment.coordinates_lat"
|
||||
:longitude="attachment.coordinates_long"
|
||||
:name="attachment.fallback_title"
|
||||
/>
|
||||
<BubbleContact
|
||||
v-else-if="attachment.file_type === 'contact'"
|
||||
:name="data.content"
|
||||
:phone-number="attachment.fallback_title"
|
||||
/>
|
||||
<BubbleFile v-else :url="attachment.data_url" />
|
||||
</div>
|
||||
</div>
|
||||
<BubbleActions
|
||||
:id="data.id"
|
||||
:sender="data.sender"
|
||||
:story-sender="storySender"
|
||||
:external-error="errorMessageTooltip"
|
||||
:story-id="`${storyId}`"
|
||||
:is-a-tweet="isATweet"
|
||||
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||
:is-email="isEmailContentType"
|
||||
:is-private="data.private"
|
||||
:message-type="data.message_type"
|
||||
:message-status="status"
|
||||
:source-id="data.source_id"
|
||||
:inbox-id="data.inbox_id"
|
||||
:created-at="createdAt"
|
||||
/>
|
||||
</div>
|
||||
<Spinner v-if="isPending" size="tiny" />
|
||||
<div
|
||||
v-if="showAvatar"
|
||||
v-tooltip.left="tooltipForSender"
|
||||
class="sender--info"
|
||||
>
|
||||
<woot-thumbnail
|
||||
:src="sender.thumbnail"
|
||||
:username="senderNameForAvatar"
|
||||
size="16px"
|
||||
/>
|
||||
<a
|
||||
v-if="isATweet && isIncoming"
|
||||
class="sender--available-name"
|
||||
:href="twitterProfileLink"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
{{ sender.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="shouldShowContextMenu" class="context-menu-wrap">
|
||||
<ContextMenu
|
||||
v-if="isBubble && !isMessageDeleted"
|
||||
:context-menu-position="contextMenuPosition"
|
||||
:is-open="showContextMenu"
|
||||
:enabled-options="contextMenuEnabledOptions"
|
||||
:message="data"
|
||||
@open="openContextMenu"
|
||||
@close="closeContextMenu"
|
||||
@reply-to="handleReplyTo"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.wrap {
|
||||
> .bubble {
|
||||
@apply min-w-[128px];
|
||||
|
||||
&.is-unsupported {
|
||||
@apply text-xs max-w-[300px] border-dashed border border-slate-200 text-slate-600 dark:text-slate-200 bg-slate-50 dark:bg-slate-700 dark:border-slate-500;
|
||||
|
||||
.message-text--metadata .time {
|
||||
@apply text-slate-400 dark:text-slate-300;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-image,
|
||||
&.is-video {
|
||||
@apply p-0 overflow-hidden;
|
||||
|
||||
.image,
|
||||
.video {
|
||||
@apply max-w-[20rem] p-0.5;
|
||||
|
||||
> img,
|
||||
> video {
|
||||
/** ensure that the bubble radius and image radius match*/
|
||||
@apply rounded-[0.4rem];
|
||||
}
|
||||
|
||||
> video {
|
||||
@apply h-full w-full object-cover;
|
||||
}
|
||||
}
|
||||
|
||||
.video {
|
||||
@apply h-[11.25rem];
|
||||
}
|
||||
}
|
||||
|
||||
&.is-image.is-text > .message-text__wrap,
|
||||
&.is-video.is-text > .message-text__wrap {
|
||||
@apply max-w-[20rem] py-2 px-4;
|
||||
}
|
||||
|
||||
&.is-private .file.message-text__wrap {
|
||||
.file--icon {
|
||||
@apply text-woot-400 dark:text-woot-400;
|
||||
}
|
||||
|
||||
.attachment-name {
|
||||
@apply text-slate-700 dark:text-slate-200;
|
||||
}
|
||||
|
||||
.download.button {
|
||||
@apply text-woot-400 dark:text-woot-400;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-private.is-text > .message-text__wrap .link {
|
||||
@apply text-woot-600 dark:text-woot-200;
|
||||
}
|
||||
|
||||
&.is-private.is-text > .message-text__wrap .prosemirror-mention-node {
|
||||
@apply font-bold bg-none rounded-sm p-0 bg-yellow-100 dark:bg-yellow-700 text-slate-700 dark:text-slate-25 underline;
|
||||
}
|
||||
|
||||
&.is-from-bot {
|
||||
@apply bg-violet-400 dark:bg-violet-400;
|
||||
|
||||
.message-text--metadata .time {
|
||||
@apply text-violet-50 dark:text-violet-50;
|
||||
}
|
||||
|
||||
&.is-private .message-text--metadata .time {
|
||||
@apply text-slate-400 dark:text-slate-400;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-failed {
|
||||
@apply bg-n-ruby-4 dark:bg-n-ruby-4 text-n-slate-12;
|
||||
|
||||
.message-text--metadata .time {
|
||||
@apply text-n-ruby-12 dark:text-n-ruby-12;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-pending {
|
||||
@apply relative opacity-80;
|
||||
|
||||
.spinner {
|
||||
@apply absolute bottom-1 right-1;
|
||||
}
|
||||
|
||||
> .is-image.is-text.bubble > .message-text__wrap {
|
||||
@apply p-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wrap.is-email {
|
||||
--bubble-max-width: 84% !important;
|
||||
}
|
||||
|
||||
.sender--info {
|
||||
@apply items-center text-black-700 dark:text-black-100 inline-flex py-1 px-0;
|
||||
|
||||
.sender--available-name {
|
||||
@apply text-xs ml-1;
|
||||
}
|
||||
}
|
||||
|
||||
.message-failed--alert {
|
||||
@apply text-red-900 dark:text-red-900 flex-grow text-right mt-1 mr-1 mb-0 ml-0;
|
||||
}
|
||||
|
||||
li.left,
|
||||
li.right {
|
||||
@apply flex items-end;
|
||||
}
|
||||
|
||||
li.left.has-tweet-menu .context-menu {
|
||||
// this handles the offset required to align the context menu button
|
||||
// extra alignment is required since a tweet message has a the user name and avatar below it
|
||||
@apply mb-6;
|
||||
}
|
||||
|
||||
li.has-bg {
|
||||
@apply bg-woot-75 dark:bg-woot-600;
|
||||
}
|
||||
|
||||
li.right .context-menu-wrap {
|
||||
@apply ml-auto;
|
||||
}
|
||||
|
||||
li.right {
|
||||
@apply flex-row-reverse justify-end;
|
||||
|
||||
.wrap.is-pending {
|
||||
@apply ml-auto;
|
||||
}
|
||||
|
||||
.wrap.is-failed {
|
||||
@apply flex items-end ltr:ml-auto rtl:mr-auto;
|
||||
}
|
||||
}
|
||||
|
||||
.has-context-menu {
|
||||
@apply bg-slate-50 dark:bg-slate-700;
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
/* Markdown styling */
|
||||
|
||||
.bubble .text-content {
|
||||
p code {
|
||||
@apply bg-slate-75 dark:bg-slate-700 inline-block leading-none rounded-sm p-1;
|
||||
}
|
||||
|
||||
ol li {
|
||||
@apply list-item list-decimal;
|
||||
}
|
||||
|
||||
pre {
|
||||
@apply bg-slate-75 dark:bg-slate-700 block border-slate-75 dark:border-slate-700 text-slate-800 dark:text-slate-100 rounded-md p-2 mt-1 mb-2 leading-relaxed whitespace-pre-wrap;
|
||||
|
||||
code {
|
||||
@apply bg-transparent text-slate-800 dark:text-slate-100 p-0;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
@apply border-l-4 mx-0 my-1 pt-2 pr-2 pb-0 pl-4 border-slate-75 border-solid dark:border-slate-600 text-slate-800 dark:text-slate-100;
|
||||
|
||||
p {
|
||||
@apply text-slate-800 dark:text-slate-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right .bubble .text-content {
|
||||
p code {
|
||||
@apply bg-woot-600 dark:bg-woot-600 text-white dark:text-white;
|
||||
}
|
||||
|
||||
pre {
|
||||
@apply bg-woot-800 dark:bg-woot-800 border-woot-700 dark:border-woot-700 text-white dark:text-white;
|
||||
|
||||
code {
|
||||
@apply bg-transparent text-white dark:text-white;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
@apply border-l-4 border-solid border-woot-400 dark:border-woot-400 text-white dark:text-white;
|
||||
|
||||
p {
|
||||
@apply text-woot-75 dark:text-woot-75;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -66,26 +66,26 @@ export default {
|
||||
<fluent-icon
|
||||
v-if="isMessagePrivate"
|
||||
size="16"
|
||||
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
|
||||
class="-mt-0.5 align-middle text-n-slate-11 inline-block"
|
||||
icon="lock-closed"
|
||||
/>
|
||||
<fluent-icon
|
||||
v-else-if="messageByAgent"
|
||||
size="16"
|
||||
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
|
||||
class="-mt-0.5 align-middle text-n-slate-11 inline-block"
|
||||
icon="arrow-reply"
|
||||
/>
|
||||
<fluent-icon
|
||||
v-else-if="isMessageAnActivity"
|
||||
size="16"
|
||||
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
|
||||
class="-mt-0.5 align-middle text-n-slate-11 inline-block"
|
||||
icon="info"
|
||||
/>
|
||||
</template>
|
||||
<span v-if="message.content && isMessageSticker">
|
||||
<fluent-icon
|
||||
size="16"
|
||||
class="-mt-0.5 align-middle inline-block text-slate-600 dark:text-slate-300"
|
||||
class="-mt-0.5 align-middle inline-block text-n-slate-11"
|
||||
icon="image"
|
||||
/>
|
||||
{{ $t('CHAT_LIST.ATTACHMENTS.image.CONTENT') }}
|
||||
@@ -97,7 +97,7 @@ export default {
|
||||
<fluent-icon
|
||||
v-if="attachmentIcon && showMessageType"
|
||||
size="16"
|
||||
class="-mt-0.5 align-middle inline-block text-slate-600 dark:text-slate-300"
|
||||
class="-mt-0.5 align-middle inline-block text-n-slate-11"
|
||||
:icon="attachmentIcon"
|
||||
/>
|
||||
{{ $t(`${attachmentMessageContent}`) }}
|
||||
|
||||
@@ -11,7 +11,7 @@ const openProfileSettings = () => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="my-0 mx-4 px-1 flex max-h-[8vh] items-baseline justify-between hover:bg-slate-25 dark:hover:bg-slate-800 border border-dashed border-slate-100 dark:border-slate-700 rounded-sm overflow-auto"
|
||||
class="my-0 mx-4 px-1 flex max-h-[8vh] items-baseline justify-between hover:bg-n-slate-1 border border-dashed border-n-weak rounded-sm overflow-auto"
|
||||
>
|
||||
<p class="w-fit !m-0">
|
||||
{{ $t('CONVERSATION.FOOTER.MESSAGE_SIGNATURE_NOT_CONFIGURED') }}
|
||||
|
||||
@@ -4,14 +4,13 @@ import { ref, provide } from 'vue';
|
||||
import { useConfig } from 'dashboard/composables/useConfig';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import { useAI } from 'dashboard/composables/useAI';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
// components
|
||||
import ReplyBox from './ReplyBox.vue';
|
||||
import Message from './Message.vue';
|
||||
import NextMessageList from 'next/message/MessageList.vue';
|
||||
import MessageList from 'next/message/MessageList.vue';
|
||||
import ConversationLabelSuggestion from './conversation/LabelSuggestion.vue';
|
||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
|
||||
// stores and apis
|
||||
import { mapGetters } from 'vuex';
|
||||
@@ -35,16 +34,15 @@ import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { REPLY_POLICY } from 'shared/constants/links';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { FEATURE_FLAGS } from '../../../featureFlags';
|
||||
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Message,
|
||||
NextMessageList,
|
||||
MessageList,
|
||||
ReplyBox,
|
||||
Banner,
|
||||
ConversationLabelSuggestion,
|
||||
Spinner,
|
||||
},
|
||||
mixins: [inboxMixin],
|
||||
setup() {
|
||||
@@ -52,17 +50,11 @@ export default {
|
||||
const conversationPanelRef = ref(null);
|
||||
const { isEnterprise } = useConfig();
|
||||
|
||||
const closePopOutReplyBox = () => {
|
||||
isPopOutReplyBox.value = false;
|
||||
};
|
||||
|
||||
const showPopOutReplyBox = () => {
|
||||
isPopOutReplyBox.value = !isPopOutReplyBox.value;
|
||||
};
|
||||
|
||||
const keyboardEvents = {
|
||||
Escape: {
|
||||
action: closePopOutReplyBox,
|
||||
action: () => {
|
||||
isPopOutReplyBox.value = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -75,28 +67,15 @@ export default {
|
||||
fetchLabelSuggestions,
|
||||
} = useAI();
|
||||
|
||||
const currentAccountId = useMapGetter('getCurrentAccountId');
|
||||
const isFeatureEnabledonAccount = useMapGetter(
|
||||
'accounts/isFeatureEnabledonAccount'
|
||||
);
|
||||
|
||||
const showNextBubbles = isFeatureEnabledonAccount.value(
|
||||
currentAccountId.value,
|
||||
FEATURE_FLAGS.CHATWOOT_V4
|
||||
);
|
||||
|
||||
provide('contextMenuElementTarget', conversationPanelRef);
|
||||
|
||||
return {
|
||||
isEnterprise,
|
||||
isPopOutReplyBox,
|
||||
closePopOutReplyBox,
|
||||
showPopOutReplyBox,
|
||||
isAIIntegrationEnabled,
|
||||
isLabelSuggestionFeatureEnabled,
|
||||
fetchIntegrationsIfRequired,
|
||||
fetchLabelSuggestions,
|
||||
showNextBubbles,
|
||||
conversationPanelRef,
|
||||
};
|
||||
},
|
||||
@@ -180,20 +159,6 @@ export default {
|
||||
(!this.listLoadingStatus && this.isLoadingPrevious)
|
||||
);
|
||||
},
|
||||
conversationType() {
|
||||
const { additional_attributes: additionalAttributes } = this.currentChat;
|
||||
const type = additionalAttributes ? additionalAttributes.type : '';
|
||||
return type || '';
|
||||
},
|
||||
|
||||
isATweet() {
|
||||
return this.conversationType === 'tweet';
|
||||
},
|
||||
getLastSeenAt() {
|
||||
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
|
||||
return contactLastSeenAt;
|
||||
},
|
||||
|
||||
// Check there is a instagram inbox exists with the same instagram_id
|
||||
hasDuplicateInstagramInbox() {
|
||||
const instagramId = this.inbox.instagram_id;
|
||||
@@ -269,9 +234,6 @@ export default {
|
||||
: 'CONVERSATION.UNREAD_MESSAGE';
|
||||
return `${count} ${this.$t(label)}`;
|
||||
},
|
||||
isInstagramDM() {
|
||||
return this.conversationType === 'instagram_direct_message';
|
||||
},
|
||||
inboxSupportsReplyTo() {
|
||||
const incoming = this.inboxHasFeature(INBOX_FEATURES.REPLY_TO);
|
||||
const outgoing =
|
||||
@@ -475,18 +437,6 @@ export default {
|
||||
makeMessagesRead() {
|
||||
this.$store.dispatch('markMessagesRead', { id: this.currentChat.id });
|
||||
},
|
||||
getInReplyToMessage(parentMessage) {
|
||||
if (!parentMessage) return {};
|
||||
const inReplyToMessageId = parentMessage.content_attributes?.in_reply_to;
|
||||
if (!inReplyToMessageId) return {};
|
||||
|
||||
return this.currentChat?.messages.find(message => {
|
||||
if (message.id === inReplyToMessageId) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -507,10 +457,9 @@ export default {
|
||||
class="mx-2 mt-2 overflow-hidden rounded-lg"
|
||||
:banner-message="$t('CONVERSATION.OLD_INSTAGRAM_INBOX_REPLY_BANNER')"
|
||||
/>
|
||||
<NextMessageList
|
||||
v-if="showNextBubbles"
|
||||
<MessageList
|
||||
ref="conversationPanelRef"
|
||||
class="conversation-panel"
|
||||
class="conversation-panel flex-shrink flex-grow basis-px flex flex-col overflow-y-auto relative h-full m-0 pb-4"
|
||||
:current-user-id="currentUserId"
|
||||
:first-unread-id="unReadMessages[0]?.id"
|
||||
:is-an-email-channel="isAnEmailChannel"
|
||||
@@ -520,14 +469,18 @@ export default {
|
||||
<template #beforeAll>
|
||||
<transition name="slide-up">
|
||||
<!-- eslint-disable-next-line vue/require-toggle-inside-transition -->
|
||||
<li class="min-h-[4rem]">
|
||||
<span v-if="shouldShowSpinner" class="spinner message" />
|
||||
<li
|
||||
class="min-h-[4rem] flex flex-shrink-0 flex-grow-0 items-center flex-auto justify-center max-w-full mt-0 mr-0 mb-1 ml-0 relative first:mt-auto last:mb-0"
|
||||
>
|
||||
<Spinner v-if="shouldShowSpinner" class="text-n-brand" />
|
||||
</li>
|
||||
</transition>
|
||||
</template>
|
||||
<template #unreadBadge>
|
||||
<li v-show="unreadMessageCount != 0" class="unread--toast">
|
||||
<span>
|
||||
<li v-show="unreadMessageCount != 0">
|
||||
<span
|
||||
class="shadow-lg rounded-full bg-n-brand text-white text-xs font-medium my-2.5 mx-auto px-2.5 py-1.5"
|
||||
>
|
||||
{{ unreadMessageLabel }}
|
||||
</span>
|
||||
</li>
|
||||
@@ -540,65 +493,12 @@ export default {
|
||||
:conversation-id="currentChat.id"
|
||||
/>
|
||||
</template>
|
||||
</NextMessageList>
|
||||
<ul v-else ref="conversationPanelRef" class="conversation-panel">
|
||||
<transition name="slide-up">
|
||||
<!-- eslint-disable-next-line vue/require-toggle-inside-transition -->
|
||||
<li class="min-h-[4rem]">
|
||||
<span v-if="shouldShowSpinner" class="spinner message" />
|
||||
</li>
|
||||
</transition>
|
||||
<Message
|
||||
v-for="message in readMessages"
|
||||
:key="message.id"
|
||||
class="message--read ph-no-capture"
|
||||
data-clarity-mask="True"
|
||||
:data="message"
|
||||
:is-a-tweet="isATweet"
|
||||
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||
:is-web-widget-inbox="isAWebWidgetInbox"
|
||||
:is-a-facebook-inbox="isAFacebookInbox"
|
||||
:is-an-email-inbox="isAnEmailChannel"
|
||||
:is-instagram="isInstagramDM"
|
||||
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
||||
:in-reply-to="getInReplyToMessage(message)"
|
||||
/>
|
||||
<li v-show="unreadMessageCount != 0" class="unread--toast">
|
||||
<span>
|
||||
{{ unreadMessageCount > 9 ? '9+' : unreadMessageCount }}
|
||||
{{
|
||||
unreadMessageCount > 1
|
||||
? $t('CONVERSATION.UNREAD_MESSAGES')
|
||||
: $t('CONVERSATION.UNREAD_MESSAGE')
|
||||
}}
|
||||
</span>
|
||||
</li>
|
||||
<Message
|
||||
v-for="message in unReadMessages"
|
||||
:key="message.id"
|
||||
class="message--unread ph-no-capture"
|
||||
data-clarity-mask="True"
|
||||
:data="message"
|
||||
:is-a-tweet="isATweet"
|
||||
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||
:is-web-widget-inbox="isAWebWidgetInbox"
|
||||
:is-a-facebook-inbox="isAFacebookInbox"
|
||||
:is-instagram-dm="isInstagramDM"
|
||||
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
||||
:in-reply-to="getInReplyToMessage(message)"
|
||||
/>
|
||||
<ConversationLabelSuggestion
|
||||
v-if="shouldShowLabelSuggestions"
|
||||
:suggested-labels="labelSuggestions"
|
||||
:chat-labels="currentChat.labels"
|
||||
:conversation-id="currentChat.id"
|
||||
/>
|
||||
</ul>
|
||||
</MessageList>
|
||||
<div
|
||||
class="conversation-footer"
|
||||
class="flex relative flex-col"
|
||||
:class="{
|
||||
'modal-mask': isPopOutReplyBox,
|
||||
'bg-n-background': showNextBubbles && !isPopOutReplyBox,
|
||||
'bg-n-background': !isPopOutReplyBox,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
@@ -606,7 +506,7 @@ export default {
|
||||
class="absolute flex items-center w-full h-0 -top-7"
|
||||
>
|
||||
<div
|
||||
class="flex py-2 pr-4 pl-5 shadow-md rounded-full bg-white dark:bg-slate-700 text-n-slate-11 text-xs font-semibold my-2.5 mx-auto"
|
||||
class="flex py-2 pr-4 pl-5 shadow-md rounded-full bg-white dark:bg-n-solid-3 text-n-slate-11 text-xs font-semibold my-2.5 mx-auto"
|
||||
>
|
||||
{{ typingUserNames }}
|
||||
<img
|
||||
@@ -617,8 +517,8 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
<ReplyBox
|
||||
v-model:popout-reply-box="isPopOutReplyBox"
|
||||
@toggle-popout="showPopOutReplyBox"
|
||||
:pop-out-reply-box="isPopOutReplyBox"
|
||||
@update:pop-out-reply-box="isPopOutReplyBox = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -626,7 +526,7 @@ export default {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-mask {
|
||||
@apply absolute;
|
||||
@apply fixed;
|
||||
|
||||
&::v-deep {
|
||||
.ProseMirror-woot-style {
|
||||
@@ -650,7 +550,7 @@ export default {
|
||||
}
|
||||
|
||||
.emoji-dialog {
|
||||
@apply absolute left-auto bottom-1;
|
||||
@apply absolute ltr:left-auto rtl:right-auto bottom-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,24 +29,24 @@ defineProps({
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="h-full w-full bg-white dark:bg-slate-900 border border-slate-100 dark:border-white/10 rounded-lg p-4 flex flex-col"
|
||||
class="h-full w-full bg-n-background border border-n-weak rounded-lg p-4 flex flex-col"
|
||||
>
|
||||
<div class="flex-1 flex items-center justify-center">
|
||||
<img :src="imageSrc" :alt="imageAlt" class="h-36 w-auto mx-auto" />
|
||||
</div>
|
||||
<div class="mt-auto">
|
||||
<p
|
||||
class="text-base text-slate-800 dark:text-slate-100 font-interDisplay font-semibold tracking-[0.3px]"
|
||||
class="text-base text-n-slate-12 font-interDisplay font-semibold tracking-[0.3px]"
|
||||
>
|
||||
{{ title }}
|
||||
</p>
|
||||
<p class="text-slate-600 dark:text-slate-400 text-sm">
|
||||
<p class="text-n-slate-11 text-sm">
|
||||
{{ description }}
|
||||
</p>
|
||||
<router-link
|
||||
v-if="to"
|
||||
:to="{ name: to }"
|
||||
class="no-underline text-woot-500 text-sm font-medium"
|
||||
class="no-underline text-n-brand text-sm font-medium"
|
||||
>
|
||||
<span>{{ linkText }}</span>
|
||||
<span class="ml-2">{{ `→` }}</span>
|
||||
|
||||
@@ -32,11 +32,11 @@ const greetingMessage = computed(() => {
|
||||
>
|
||||
<div class="col-span-full self-start">
|
||||
<p
|
||||
class="text-xl font-semibold text-slate-900 dark:text-white font-interDisplay tracking-[0.3px]"
|
||||
class="text-xl font-semibold text-n-slate-12 font-interDisplay tracking-[0.3px]"
|
||||
>
|
||||
{{ greetingMessage }}
|
||||
</p>
|
||||
<p class="text-slate-600 dark:text-slate-400 max-w-2xl text-base">
|
||||
<p class="text-n-slate-11 max-w-2xl text-base">
|
||||
{{
|
||||
$t('ONBOARDING.DESCRIPTION', {
|
||||
installationName: globalConfig.installationName,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script>
|
||||
// [TODO] The popout events are needlessly complex and should be simplified
|
||||
import { defineAsyncComponent, defineModel, useTemplateRef } from 'vue';
|
||||
import { defineAsyncComponent, useTemplateRef } from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
@@ -66,7 +65,13 @@ export default {
|
||||
WootMessageEditor,
|
||||
},
|
||||
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
|
||||
emits: ['update:popoutReplyBox', 'togglePopout'],
|
||||
props: {
|
||||
popOutReplyBox: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:popOutReplyBox'],
|
||||
setup() {
|
||||
const {
|
||||
uiSettings,
|
||||
@@ -75,16 +80,10 @@ export default {
|
||||
fetchSignatureFlagFromUISettings,
|
||||
} = useUISettings();
|
||||
|
||||
const popoutReplyBox = defineModel('popoutReplyBox', {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
});
|
||||
|
||||
const replyEditor = useTemplateRef('replyEditor');
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
popoutReplyBox,
|
||||
updateUISettings,
|
||||
isEditorHotKeyEnabled,
|
||||
fetchSignatureFlagFromUISettings,
|
||||
@@ -123,7 +122,6 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isRTL: 'accounts/isRTL',
|
||||
currentChat: 'getSelectedChat',
|
||||
messageSignature: 'getMessageSignature',
|
||||
currentUser: 'getCurrentUser',
|
||||
@@ -316,15 +314,6 @@ export default {
|
||||
this.uiSettings;
|
||||
return conversationDisplayType !== CONDENSED;
|
||||
},
|
||||
emojiDialogClassOnExpandedLayoutAndRTLView() {
|
||||
if (this.isOnExpandedLayout || this.popoutReplyBox) {
|
||||
return 'emoji-dialog--expanded';
|
||||
}
|
||||
if (this.isRTL) {
|
||||
return 'emoji-dialog--rtl';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
isMessageEmpty() {
|
||||
if (!this.message) {
|
||||
return true;
|
||||
@@ -719,7 +708,7 @@ export default {
|
||||
|
||||
this.clearMessage();
|
||||
this.hideEmojiPicker();
|
||||
this.$emit('update:popoutReplyBox', false);
|
||||
this.$emit('update:popOutReplyBox', false);
|
||||
}
|
||||
},
|
||||
sendMessageAsMultipleMessages(message) {
|
||||
@@ -1098,6 +1087,9 @@ export default {
|
||||
file => !file?.isRecordedAudio
|
||||
);
|
||||
},
|
||||
togglePopout() {
|
||||
this.$emit('update:popOutReplyBox', !this.popOutReplyBox);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1118,9 +1110,9 @@ export default {
|
||||
:mode="replyType"
|
||||
:is-message-length-reaching-threshold="isMessageLengthReachingThreshold"
|
||||
:characters-remaining="charactersRemaining"
|
||||
:popout-reply-box="popoutReplyBox"
|
||||
:popout-reply-box="popOutReplyBox"
|
||||
@set-reply-mode="setReplyMode"
|
||||
@toggle-popout="$emit('togglePopout')"
|
||||
@toggle-popout="togglePopout"
|
||||
/>
|
||||
<ArticleSearchPopover
|
||||
v-if="showArticleSearchPopover && connectedPortalSlug"
|
||||
@@ -1144,7 +1136,9 @@ export default {
|
||||
<EmojiInput
|
||||
v-if="showEmojiPicker"
|
||||
v-on-clickaway="hideEmojiPicker"
|
||||
:class="emojiDialogClassOnExpandedLayoutAndRTLView"
|
||||
:class="{
|
||||
'emoji-dialog--expanded': isOnExpandedLayout || popOutReplyBox,
|
||||
}"
|
||||
:on-click="addIntoEditor"
|
||||
/>
|
||||
<ReplyEmailHead
|
||||
@@ -1297,21 +1291,11 @@ export default {
|
||||
}
|
||||
|
||||
.emoji-dialog {
|
||||
@apply top-[unset] -bottom-10 -left-80 right-[unset];
|
||||
@apply top-[unset] -bottom-10 ltr:-left-80 ltr:right-[unset] rtl:left-[unset] rtl:-right-80;
|
||||
|
||||
&::before {
|
||||
transform: rotate(270deg);
|
||||
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
|
||||
@apply -right-4 bottom-2 rtl:right-0 rtl:-left-4;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-dialog--rtl {
|
||||
@apply left-[unset] -right-80;
|
||||
|
||||
&::before {
|
||||
transform: rotate(90deg);
|
||||
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
|
||||
@apply ltr:-right-4 bottom-2 rtl:-left-4 ltr:rotate-[270deg] rtl:rotate-[90deg];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1320,12 +1304,12 @@ export default {
|
||||
|
||||
&::before {
|
||||
transform: rotate(0deg);
|
||||
@apply left-1 -bottom-2;
|
||||
@apply ltr:left-1 rtl:right-1 -bottom-2;
|
||||
}
|
||||
}
|
||||
|
||||
.normal-editor__canned-box {
|
||||
width: calc(100% - 2 * var(--space-normal));
|
||||
left: var(--space-normal);
|
||||
width: calc(100% - 2 * 1rem);
|
||||
left: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -27,7 +27,7 @@ const getStatusClass = status => {
|
||||
const classes = {
|
||||
paid: 'bg-n-teal-5 text-n-teal-12',
|
||||
};
|
||||
return classes[status] || 'bg-slate-50 text-slate-700';
|
||||
return classes[status] || 'bg-n-solid-3 text-n-slate-12';
|
||||
};
|
||||
|
||||
const getStatusI18nKey = (type, status = '') => {
|
||||
@@ -52,11 +52,11 @@ const financialStatus = computed(() => {
|
||||
|
||||
const getFulfillmentClass = status => {
|
||||
const classes = {
|
||||
fulfilled: 'text-green-600',
|
||||
partial: 'text-yellow-600',
|
||||
unfulfilled: 'text-red-600',
|
||||
fulfilled: 'text-n-teal-9',
|
||||
partial: 'text-n-amber-9',
|
||||
unfulfilled: 'text-n-ruby-9',
|
||||
};
|
||||
return classes[status] || 'text-slate-600';
|
||||
return classes[status] || 'text-n-slate-11';
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ const onAgentSelect = index => {
|
||||
@click="onAgentSelect(index)"
|
||||
@mouseover="onHover(index)"
|
||||
>
|
||||
<div class="mr-2">
|
||||
<div class="ltr:mr-2 rtl:ml-2">
|
||||
<Avatar :src="agent.thumbnail" :name="agent.name" rounded-full />
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -69,6 +69,6 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.variable--list-label {
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,357 +0,0 @@
|
||||
<script>
|
||||
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import { messageTimestamp } from 'shared/helpers/timeHelper';
|
||||
|
||||
export default {
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
sender: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
createdAt: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
storySender: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
externalError: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
storyId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isEmail: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isPrivate: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isATweet: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
messageType: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
messageStatus: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
sourceId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
inboxId: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
inbox() {
|
||||
return this.$store.getters['inboxes/getInbox'](this.inboxId);
|
||||
},
|
||||
isIncoming() {
|
||||
return MESSAGE_TYPE.INCOMING === this.messageType;
|
||||
},
|
||||
isOutgoing() {
|
||||
return MESSAGE_TYPE.OUTGOING === this.messageType;
|
||||
},
|
||||
isTemplate() {
|
||||
return MESSAGE_TYPE.TEMPLATE === this.messageType;
|
||||
},
|
||||
isDelivered() {
|
||||
return MESSAGE_STATUS.DELIVERED === this.messageStatus;
|
||||
},
|
||||
isRead() {
|
||||
return MESSAGE_STATUS.READ === this.messageStatus;
|
||||
},
|
||||
isSent() {
|
||||
return MESSAGE_STATUS.SENT === this.messageStatus;
|
||||
},
|
||||
readableTime() {
|
||||
return messageTimestamp(this.createdAt, 'LLL d, h:mm a');
|
||||
},
|
||||
screenName() {
|
||||
const { additional_attributes: additionalAttributes = {} } =
|
||||
this.sender || {};
|
||||
return additionalAttributes?.screen_name || '';
|
||||
},
|
||||
linkToTweet() {
|
||||
if (!this.sourceId || !this.inbox.name) {
|
||||
return '';
|
||||
}
|
||||
const { screenName, sourceId } = this;
|
||||
return `https://twitter.com/${
|
||||
screenName || this.inbox.name
|
||||
}/status/${sourceId}`;
|
||||
},
|
||||
linkToStory() {
|
||||
if (!this.storyId || !this.storySender) {
|
||||
return '';
|
||||
}
|
||||
const { storySender, storyId } = this;
|
||||
return `https://www.instagram.com/stories/direct/${storySender}_${storyId}`;
|
||||
},
|
||||
showStatusIndicators() {
|
||||
if ((this.isOutgoing || this.isTemplate) && !this.isPrivate) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
showSentIndicator() {
|
||||
if (!this.showStatusIndicators) {
|
||||
return false;
|
||||
}
|
||||
// Messages will be marked as sent for the Email channel if they have a source ID.
|
||||
if (this.isAnEmailChannel) {
|
||||
return !!this.sourceId;
|
||||
}
|
||||
|
||||
if (
|
||||
this.isAWhatsAppChannel ||
|
||||
this.isATwilioChannel ||
|
||||
this.isAFacebookInbox ||
|
||||
this.isASmsInbox ||
|
||||
this.isATelegramChannel
|
||||
) {
|
||||
return this.sourceId && this.isSent;
|
||||
}
|
||||
// All messages will be mark as sent for the Line channel, as there is no source ID.
|
||||
if (this.isALineChannel) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
showDeliveredIndicator() {
|
||||
if (!this.showStatusIndicators) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
this.isAWhatsAppChannel ||
|
||||
this.isATwilioChannel ||
|
||||
this.isASmsInbox ||
|
||||
this.isAFacebookInbox
|
||||
) {
|
||||
return this.sourceId && this.isDelivered;
|
||||
}
|
||||
// All messages marked as delivered for the web widget inbox and API inbox once they are sent.
|
||||
if (this.isAWebWidgetInbox || this.isAPIInbox) {
|
||||
return this.isSent;
|
||||
}
|
||||
if (this.isALineChannel) {
|
||||
return this.isDelivered;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
showReadIndicator() {
|
||||
if (!this.showStatusIndicators) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
this.isAWhatsAppChannel ||
|
||||
this.isATwilioChannel ||
|
||||
this.isAFacebookInbox
|
||||
) {
|
||||
return this.sourceId && this.isRead;
|
||||
}
|
||||
|
||||
if (this.isAWebWidgetInbox || this.isAPIInbox) {
|
||||
return this.isRead;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="message-text--metadata">
|
||||
<span
|
||||
class="time"
|
||||
:class="{
|
||||
'has-status-icon':
|
||||
showSentIndicator || showDeliveredIndicator || showReadIndicator,
|
||||
}"
|
||||
>
|
||||
{{ readableTime }}
|
||||
</span>
|
||||
<span v-if="externalError" class="read-indicator-wrap">
|
||||
<fluent-icon
|
||||
v-tooltip.top-start="externalError"
|
||||
icon="error-circle"
|
||||
class="action--icon"
|
||||
size="14"
|
||||
/>
|
||||
</span>
|
||||
<span v-if="showReadIndicator" class="read-indicator-wrap">
|
||||
<fluent-icon
|
||||
v-tooltip.top-start="$t('CHAT_LIST.MESSAGE_READ')"
|
||||
icon="checkmark-double"
|
||||
class="action--icon read-tick read-indicator"
|
||||
size="14"
|
||||
/>
|
||||
</span>
|
||||
<span v-else-if="showDeliveredIndicator" class="read-indicator-wrap">
|
||||
<fluent-icon
|
||||
v-tooltip.top-start="$t('CHAT_LIST.DELIVERED')"
|
||||
icon="checkmark-double"
|
||||
class="action--icon read-tick"
|
||||
size="14"
|
||||
/>
|
||||
</span>
|
||||
<span v-else-if="showSentIndicator" class="read-indicator-wrap">
|
||||
<fluent-icon
|
||||
v-tooltip.top-start="$t('CHAT_LIST.SENT')"
|
||||
icon="checkmark"
|
||||
class="action--icon read-tick"
|
||||
size="14"
|
||||
/>
|
||||
</span>
|
||||
<fluent-icon
|
||||
v-if="isEmail"
|
||||
v-tooltip.top-start="$t('CHAT_LIST.RECEIVED_VIA_EMAIL')"
|
||||
icon="mail"
|
||||
class="action--icon"
|
||||
size="16"
|
||||
/>
|
||||
<fluent-icon
|
||||
v-if="isPrivate"
|
||||
v-tooltip.top-start="$t('CONVERSATION.VISIBLE_TO_AGENTS')"
|
||||
icon="lock-closed"
|
||||
class="action--icon lock--icon--private"
|
||||
size="16"
|
||||
@mouseenter="isHovered = true"
|
||||
@mouseleave="isHovered = false"
|
||||
/>
|
||||
<a
|
||||
v-if="isATweet && (isOutgoing || isIncoming) && linkToTweet"
|
||||
:href="linkToTweet"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
<fluent-icon
|
||||
v-tooltip.top-start="$t('CHAT_LIST.VIEW_TWEET_IN_TWITTER')"
|
||||
icon="open"
|
||||
class="cursor-pointer action--icon"
|
||||
size="16"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.right {
|
||||
.message-text--metadata {
|
||||
@apply items-center;
|
||||
.time {
|
||||
@apply text-woot-100 dark:text-woot-100;
|
||||
}
|
||||
|
||||
.action--icon {
|
||||
@apply text-white dark:text-white;
|
||||
|
||||
&.read-tick {
|
||||
@apply text-violet-100 dark:text-violet-100;
|
||||
}
|
||||
|
||||
&.read-indicator {
|
||||
@apply text-green-200 dark:text-green-200;
|
||||
}
|
||||
}
|
||||
|
||||
.lock--icon--private {
|
||||
@apply text-slate-400 dark:text-slate-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.left {
|
||||
.message-text--metadata {
|
||||
.time {
|
||||
@apply text-slate-400 dark:text-slate-200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-text--metadata {
|
||||
@apply items-start flex;
|
||||
|
||||
.time {
|
||||
@apply mr-2 block text-xxs leading-[1.8];
|
||||
}
|
||||
|
||||
.action--icon {
|
||||
@apply mr-2 ml-2 text-slate-900 dark:text-slate-100;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-slate-900 dark:text-slate-100;
|
||||
}
|
||||
}
|
||||
|
||||
.activity-wrap {
|
||||
.message-text--metadata {
|
||||
.time {
|
||||
@apply ml-2 rtl:mr-2 rtl:ml-0 flex text-center text-xxs text-slate-300 dark:text-slate-200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-image,
|
||||
.is-video {
|
||||
.message-text--metadata {
|
||||
.time {
|
||||
@apply bottom-1 text-white dark:text-slate-50 absolute right-2 whitespace-nowrap;
|
||||
|
||||
&.has-status-icon {
|
||||
@apply right-8 leading-loose;
|
||||
}
|
||||
}
|
||||
.read-tick {
|
||||
@apply absolute bottom-2 right-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-private {
|
||||
.message-text--metadata {
|
||||
@apply items-center;
|
||||
|
||||
.time {
|
||||
@apply text-slate-400 dark:text-slate-400;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@apply text-slate-400 dark:text-slate-400;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-image,
|
||||
&.is-video {
|
||||
.time {
|
||||
position: inherit;
|
||||
@apply pl-2.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delivered-icon {
|
||||
@apply ml-4;
|
||||
}
|
||||
|
||||
.read-indicator-wrap {
|
||||
@apply leading-none flex items-center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,123 +0,0 @@
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import {
|
||||
DuplicateContactException,
|
||||
ExceptionWithMessage,
|
||||
} from 'shared/helpers/CustomErrors';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
phoneNumber: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
formattedPhoneNumber() {
|
||||
return this.phoneNumber.replace(/\s|-|[A-Za-z]/g, '');
|
||||
},
|
||||
rawPhoneNumber() {
|
||||
return this.phoneNumber.replace(/\D/g, '');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async addContact() {
|
||||
try {
|
||||
let contact = await this.filterContactByNumber(this.rawPhoneNumber);
|
||||
if (!contact) {
|
||||
contact = await this.$store.dispatch(
|
||||
'contacts/create',
|
||||
this.getContactObject()
|
||||
);
|
||||
useAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
|
||||
}
|
||||
this.openContactNewTab(contact.id);
|
||||
} catch (error) {
|
||||
if (error instanceof DuplicateContactException) {
|
||||
if (error.data.includes('phone_number')) {
|
||||
useAlert(this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE'));
|
||||
}
|
||||
} else if (error instanceof ExceptionWithMessage) {
|
||||
useAlert(error.data);
|
||||
} else {
|
||||
useAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
|
||||
}
|
||||
}
|
||||
},
|
||||
getContactObject() {
|
||||
const contactItem = {
|
||||
name: this.name,
|
||||
phone_number: `+${this.rawPhoneNumber}`,
|
||||
};
|
||||
return contactItem;
|
||||
},
|
||||
async filterContactByNumber(phoneNumber) {
|
||||
const query = {
|
||||
attribute_key: 'phone_number',
|
||||
filter_operator: 'equal_to',
|
||||
values: [phoneNumber],
|
||||
attribute_model: 'standard',
|
||||
custom_attribute_type: '',
|
||||
};
|
||||
|
||||
const queryPayload = { payload: [query] };
|
||||
const contacts = await this.$store.dispatch('contacts/filter', {
|
||||
queryPayload,
|
||||
resetState: false,
|
||||
});
|
||||
return contacts.shift();
|
||||
},
|
||||
openContactNewTab(contactId) {
|
||||
const accountId = window.location.pathname.split('/')[3];
|
||||
const url = `/app/accounts/${accountId}/contacts/${contactId}`;
|
||||
window.open(url, '_blank');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="contact--group">
|
||||
<fluent-icon icon="call" class="file--icon" size="18" />
|
||||
<div class="meta">
|
||||
<p
|
||||
class="overflow-hidden whitespace-nowrap text-ellipsis margin-bottom-0"
|
||||
>
|
||||
{{ phoneNumber }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="formattedPhoneNumber" class="link-wrap">
|
||||
<NextButton
|
||||
ghost
|
||||
xs
|
||||
:label="$t('CONVERSATION.SAVE_CONTACT')"
|
||||
@click.prevent="addContact"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.contact--group {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-top: var(--space-smaller);
|
||||
|
||||
.meta {
|
||||
flex: 1;
|
||||
margin-left: var(--space-small);
|
||||
}
|
||||
|
||||
.link-wrap {
|
||||
margin-left: var(--space-small);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,86 +0,0 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
fileName() {
|
||||
if (this.url) {
|
||||
const filename = this.url.substring(this.url.lastIndexOf('/') + 1);
|
||||
return filename || this.$t('CONVERSATION.UNKNOWN_FILE_TYPE');
|
||||
}
|
||||
return this.$t('CONVERSATION.UNKNOWN_FILE_TYPE');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openLink() {
|
||||
const win = window.open(this.url, '_blank', 'noopener');
|
||||
if (win) win.focus();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="file message-text__wrap">
|
||||
<div class="icon-wrap">
|
||||
<fluent-icon icon="document" class="file--icon" size="32" />
|
||||
</div>
|
||||
<div class="meta">
|
||||
<h5 class="attachment-name text-slate-700 dark:text-slate-400">
|
||||
{{ decodeURI(fileName) }}
|
||||
</h5>
|
||||
<a
|
||||
class="download clear link button small"
|
||||
rel="noreferrer noopener nofollow"
|
||||
target="_blank"
|
||||
:href="url"
|
||||
>
|
||||
{{ $t('CONVERSATION.DOWNLOAD') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'dashboard/assets/scss/variables';
|
||||
|
||||
.file {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: $space-smaller 0;
|
||||
cursor: pointer;
|
||||
|
||||
.icon-wrap {
|
||||
font-size: $font-size-giga;
|
||||
color: $color-white;
|
||||
line-height: 1;
|
||||
margin-left: $space-smaller;
|
||||
margin-right: $space-slab;
|
||||
}
|
||||
|
||||
.attachment-name {
|
||||
margin: 0;
|
||||
color: $color-white;
|
||||
font-weight: $font-weight-bold;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: $color-primary-light;
|
||||
}
|
||||
|
||||
.meta {
|
||||
padding-right: $space-two;
|
||||
}
|
||||
|
||||
.time {
|
||||
min-width: $space-larger;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,34 +0,0 @@
|
||||
<script>
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['error'],
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onClose() {
|
||||
this.show = false;
|
||||
},
|
||||
onClick() {
|
||||
this.show = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="image message-text__wrap">
|
||||
<img :src="url" @click="onClick" @error="$emit('error')" />
|
||||
<woot-modal v-model:show="show" full-width :on-close="onClose">
|
||||
<img :src="url" class="modal-image skip-context-menu" />
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,127 +0,0 @@
|
||||
<script>
|
||||
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',
|
||||
VIDEO: 'video',
|
||||
AUDIO: 'audio',
|
||||
IG_REEL: 'ig_reel',
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GalleryView,
|
||||
},
|
||||
props: {
|
||||
attachment: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['error'],
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
isImageError: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChatAttachments: 'getSelectedChatAttachments',
|
||||
}),
|
||||
isImage() {
|
||||
return this.attachment.file_type === ALLOWED_FILE_TYPES.IMAGE;
|
||||
},
|
||||
isVideo() {
|
||||
return (
|
||||
this.attachment.file_type === ALLOWED_FILE_TYPES.VIDEO ||
|
||||
this.attachment.file_type === ALLOWED_FILE_TYPES.IG_REEL
|
||||
);
|
||||
},
|
||||
isAudio() {
|
||||
return this.attachment.file_type === ALLOWED_FILE_TYPES.AUDIO;
|
||||
},
|
||||
timeStampURL() {
|
||||
return timeStampAppendedURL(this.dataUrl);
|
||||
},
|
||||
attachmentTypeClasses() {
|
||||
return {
|
||||
image: this.isImage,
|
||||
video: this.isVideo,
|
||||
};
|
||||
},
|
||||
filteredCurrentChatAttachments() {
|
||||
const attachments = this.currentChatAttachments.filter(attachment =>
|
||||
['image', 'video', 'audio'].includes(attachment.file_type)
|
||||
);
|
||||
return attachments;
|
||||
},
|
||||
dataUrl() {
|
||||
return this.attachment.data_url;
|
||||
},
|
||||
imageWidth() {
|
||||
return this.attachment.width ? `${this.attachment.width}px` : 'auto';
|
||||
},
|
||||
imageHeight() {
|
||||
return this.attachment.height ? `${this.attachment.height}px` : 'auto';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
attachment() {
|
||||
this.isImageError = false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClose() {
|
||||
this.show = false;
|
||||
},
|
||||
onClick(e) {
|
||||
if (hasPressedCommand(e)) {
|
||||
window.open(this.attachment.data_url, '_blank');
|
||||
return;
|
||||
}
|
||||
this.show = true;
|
||||
},
|
||||
onImgError() {
|
||||
this.isImageError = true;
|
||||
this.$emit('error');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="message-text__wrap" :class="attachmentTypeClasses">
|
||||
<img
|
||||
v-if="isImage && !isImageError"
|
||||
class="bg-woot-200 dark:bg-woot-900"
|
||||
:src="dataUrl"
|
||||
:width="imageWidth"
|
||||
:height="imageHeight"
|
||||
@click="onClick"
|
||||
@error="onImgError"
|
||||
/>
|
||||
<video
|
||||
v-if="isVideo"
|
||||
:src="dataUrl"
|
||||
muted
|
||||
playsInline
|
||||
@error="onImgError"
|
||||
@click="onClick"
|
||||
/>
|
||||
<audio v-else-if="isAudio" controls class="skip-context-menu mb-0.5">
|
||||
<source :src="timeStampURL" />
|
||||
</audio>
|
||||
<GalleryView
|
||||
v-if="show"
|
||||
v-model:show="show"
|
||||
:attachment="attachment"
|
||||
:all-attachments="filteredCurrentChatAttachments"
|
||||
@error="onImgError"
|
||||
@close="onClose"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,53 +0,0 @@
|
||||
<script>
|
||||
import BubbleImage from './Image.vue';
|
||||
import BubbleVideo from './Video.vue';
|
||||
import InstagramStoryErrorPlaceHolder from './InstagramStoryErrorPlaceHolder.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BubbleImage,
|
||||
BubbleVideo,
|
||||
InstagramStoryErrorPlaceHolder,
|
||||
},
|
||||
props: {
|
||||
storyUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['error'],
|
||||
data() {
|
||||
return {
|
||||
hasImgStoryError: false,
|
||||
hasVideoStoryError: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onImageLoadError() {
|
||||
this.hasImgStoryError = true;
|
||||
this.emitError();
|
||||
},
|
||||
onVideoLoadError() {
|
||||
this.hasVideoStoryError = true;
|
||||
this.emitError();
|
||||
},
|
||||
emitError() {
|
||||
this.$emit('error');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BubbleImage
|
||||
v-if="!hasImgStoryError"
|
||||
:url="storyUrl"
|
||||
@error="onImageLoadError"
|
||||
/>
|
||||
<BubbleVideo
|
||||
v-else-if="!hasVideoStoryError"
|
||||
:url="storyUrl"
|
||||
@error="onVideoLoadError"
|
||||
/>
|
||||
<InstagramStoryErrorPlaceHolder v-else />
|
||||
</template>
|
||||
@@ -1,14 +0,0 @@
|
||||
<script>
|
||||
export default {};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-center px-8 h-28 w-full bg-slate-100 text-slate-700 dark:bg-slate-500 dark:text-slate-75"
|
||||
>
|
||||
<fluent-icon icon="document-error" size="32" />
|
||||
<p class="mb-0 text-slate-700 dark:text-slate-75">
|
||||
{{ $t('COMPONENTS.FILE_BUBBLE.INSTAGRAM_STORY_UNAVAILABLE') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,36 +0,0 @@
|
||||
<script>
|
||||
import InstagramStory from './InstagramStory.vue';
|
||||
|
||||
export default {
|
||||
components: { InstagramStory },
|
||||
props: {
|
||||
storyUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasImgStoryError: false,
|
||||
hasVideoStoryError: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onImageLoadError() {
|
||||
this.hasImgStoryError = true;
|
||||
},
|
||||
onVideoLoadError() {
|
||||
this.hasVideoStoryError = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<blockquote
|
||||
class="my-0 px-2 pb-0 pt-0 border-l-4 border-solid border-slate-75 dark:border-slate-600 text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
<span>{{ $t('CONVERSATION.REPLIED_TO_STORY') }}</span>
|
||||
<InstagramStory :story-url="storyUrl" class="mt-3 rounded-md" />
|
||||
</blockquote>
|
||||
</template>
|
||||
@@ -1,41 +0,0 @@
|
||||
<script>
|
||||
import DyteVideoCall from './integrations/Dyte.vue';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
|
||||
export default {
|
||||
components: { DyteVideoCall },
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
messageId: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
contentAttributes: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
inboxId: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showDyteIntegration() {
|
||||
const isEnabledOnTheInbox = this.isAPIInbox || this.isAWebWidgetInbox;
|
||||
return isEnabledOnTheInbox && this.contentAttributes.type === 'dyte';
|
||||
},
|
||||
inbox() {
|
||||
return this.$store.getters['inboxes/getInbox'](this.inboxId);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||
<template>
|
||||
<DyteVideoCall
|
||||
v-if="showDyteIntegration"
|
||||
:message-id="messageId"
|
||||
:meeting-data="contentAttributes.data"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,52 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
latitude: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
longitude: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const mapUrl = computed(
|
||||
() => `https://maps.google.com/?q=${props.latitude},${props.longitude}`
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-row items-center justify-start gap-1 w-full py-1 px-0 cursor-pointer overflow-hidden"
|
||||
>
|
||||
<fluent-icon
|
||||
icon="location"
|
||||
class="text-slate-600 dark:text-slate-200 leading-none my-0 flex items-center flex-shrink-0"
|
||||
size="32"
|
||||
/>
|
||||
<div class="flex flex-col items-start flex-1 min-w-0">
|
||||
<h5
|
||||
class="text-sm text-slate-800 dark:text-slate-100 truncate m-0 w-full"
|
||||
:title="name"
|
||||
>
|
||||
{{ name }}
|
||||
</h5>
|
||||
<div class="flex items-center">
|
||||
<a
|
||||
class="text-woot-600 dark:text-woot-600 text-xs underline"
|
||||
rel="noreferrer noopener nofollow"
|
||||
target="_blank"
|
||||
:href="mapUrl"
|
||||
>
|
||||
{{ $t('COMPONENTS.LOCATION_BUBBLE.SEE_ON_MAP') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,100 +0,0 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
emailAttributes: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
isIncoming: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
cc: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
bcc: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
fromMail() {
|
||||
const from = this.emailAttributes.from || [];
|
||||
return from.join(', ');
|
||||
},
|
||||
toMails() {
|
||||
const to = this.emailAttributes.to || [];
|
||||
return to.join(', ');
|
||||
},
|
||||
ccMails() {
|
||||
const cc = this.emailAttributes.cc || this.cc || [];
|
||||
return cc.join(', ');
|
||||
},
|
||||
bccMails() {
|
||||
const bcc = this.emailAttributes.bcc || this.bcc || [];
|
||||
return bcc.join(', ');
|
||||
},
|
||||
subject() {
|
||||
return this.emailAttributes.subject || '';
|
||||
},
|
||||
showHead() {
|
||||
return this.toMails || this.ccMails || this.bccMails || this.fromMail;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||
<template>
|
||||
<div
|
||||
v-if="showHead"
|
||||
class="message__mail-head"
|
||||
:class="{ 'is-incoming': isIncoming }"
|
||||
>
|
||||
<div v-if="fromMail" class="meta-wrap">
|
||||
<span class="message__content--type">{{ $t('EMAIL_HEADER.FROM') }}:</span>
|
||||
<span>{{ fromMail }}</span>
|
||||
</div>
|
||||
<div v-if="toMails" class="meta-wrap">
|
||||
<span class="message__content--type">{{ $t('EMAIL_HEADER.TO') }}:</span>
|
||||
<span>{{ toMails }}</span>
|
||||
</div>
|
||||
<div v-if="ccMails" class="meta-wrap">
|
||||
<span class="message__content--type">{{ $t('EMAIL_HEADER.CC') }}:</span>
|
||||
<span>{{ ccMails }}</span>
|
||||
</div>
|
||||
<div v-if="bccMails" class="meta-wrap">
|
||||
<span class="message__content--type">{{ $t('EMAIL_HEADER.BCC') }}:</span>
|
||||
<span>{{ bccMails }}</span>
|
||||
</div>
|
||||
<div v-if="subject" class="meta-wrap">
|
||||
<span class="message__content--type">
|
||||
{{ $t('EMAIL_HEADER.SUBJECT') }}:
|
||||
</span>
|
||||
<span>{{ subject }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.message__mail-head {
|
||||
padding-bottom: var(--space-small);
|
||||
margin-bottom: var(--space-small);
|
||||
border-bottom: 1px solid var(--w-300);
|
||||
|
||||
&.is-incoming {
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
}
|
||||
}
|
||||
|
||||
.meta-wrap {
|
||||
.message__content--type {
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
span {
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,57 +0,0 @@
|
||||
<script>
|
||||
import MessagePreview from 'dashboard/components/widgets/conversation/MessagePreview.vue';
|
||||
import { MESSAGE_TYPE } from 'shared/constants/messages';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
export default {
|
||||
name: 'ReplyTo',
|
||||
components: {
|
||||
MessagePreview,
|
||||
},
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
messageType: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
parentHasAttachments: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { MESSAGE_TYPE };
|
||||
},
|
||||
methods: {
|
||||
scrollToMessage() {
|
||||
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE, {
|
||||
messageId: this.message.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="px-2 py-1.5 rounded-sm min-w-[10rem] mb-2"
|
||||
:class="{
|
||||
'bg-slate-50 dark:bg-slate-600 dark:text-slate-50':
|
||||
messageType === MESSAGE_TYPE.INCOMING,
|
||||
'bg-woot-600 text-woot-50': messageType === MESSAGE_TYPE.OUTGOING,
|
||||
'-mx-2': !parentHasAttachments,
|
||||
}"
|
||||
@click="scrollToMessage"
|
||||
>
|
||||
<MessagePreview
|
||||
class="cursor-pointer"
|
||||
:message="message"
|
||||
:show-message-type="false"
|
||||
:default-empty-message="$t('CONVERSATION.REPLY_MESSAGE_NOT_FOUND')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,162 +0,0 @@
|
||||
<script>
|
||||
import { Letter } from 'vue-letter';
|
||||
import GalleryView from '../components/GalleryView.vue';
|
||||
|
||||
export default {
|
||||
components: { Letter, GalleryView },
|
||||
props: {
|
||||
message: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isEmail: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
displayQuotedButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showQuotedContent: false,
|
||||
showGalleryViewer: false,
|
||||
attachment: {},
|
||||
availableAttachments: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isQuotedContentPresent() {
|
||||
if (!this.isEmail) {
|
||||
return this.message.includes('<blockquote');
|
||||
}
|
||||
return this.showQuotedContent;
|
||||
},
|
||||
showQuoteToggle() {
|
||||
if (!this.isEmail) {
|
||||
return false;
|
||||
}
|
||||
return this.displayQuotedButton;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleQuotedContent() {
|
||||
this.showQuotedContent = !this.showQuotedContent;
|
||||
},
|
||||
handleClickOnContent(event) {
|
||||
// if event target is IMG and not close in A tag
|
||||
// then open image preview
|
||||
const isImageElement = event.target.tagName === 'IMG';
|
||||
const isWrappedInLink = event.target.closest('A');
|
||||
|
||||
if (isImageElement && !isWrappedInLink) {
|
||||
this.openImagePreview(event.target.src);
|
||||
}
|
||||
},
|
||||
openImagePreview(src) {
|
||||
this.showGalleryViewer = true;
|
||||
this.attachment = {
|
||||
file_type: 'image',
|
||||
data_url: src,
|
||||
message_id: Math.floor(Math.random() * 100),
|
||||
};
|
||||
this.availableAttachments = [{ ...this.attachment }];
|
||||
},
|
||||
onClose() {
|
||||
this.showGalleryViewer = false;
|
||||
this.resetAttachmentData();
|
||||
},
|
||||
resetAttachmentData() {
|
||||
this.attachment = {};
|
||||
this.availableAttachments = [];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="message-text__wrap"
|
||||
:class="{
|
||||
'show--quoted': isQuotedContentPresent,
|
||||
'hide--quoted': !isQuotedContentPresent,
|
||||
}"
|
||||
>
|
||||
<div v-if="!isEmail" v-dompurify-html="message" class="text-content" />
|
||||
<div v-else @click="handleClickOnContent">
|
||||
<Letter
|
||||
class="text-content bg-white dark:bg-white text-slate-900 dark:text-slate-900 p-2 rounded-[4px]"
|
||||
:html="message"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
v-if="showQuoteToggle"
|
||||
class="py-1 text-xs cursor-pointer text-slate-300 dark:text-slate-300"
|
||||
@click="toggleQuotedContent"
|
||||
>
|
||||
<span v-if="showQuotedContent" class="flex items-center gap-0.5">
|
||||
<fluent-icon icon="chevron-up" size="16" />
|
||||
{{ $t('CHAT_LIST.HIDE_QUOTED_TEXT') }}
|
||||
</span>
|
||||
<span v-else class="flex items-center gap-0.5">
|
||||
<fluent-icon icon="chevron-down" size="16" />
|
||||
{{ $t('CHAT_LIST.SHOW_QUOTED_TEXT') }}
|
||||
</span>
|
||||
</button>
|
||||
<GalleryView
|
||||
v-if="showGalleryViewer"
|
||||
v-model:show="showGalleryViewer"
|
||||
:attachment="attachment"
|
||||
:all-attachments="availableAttachments"
|
||||
@error="onClose"
|
||||
@close="onClose"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.text-content {
|
||||
overflow: auto;
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: var(--space-two);
|
||||
}
|
||||
|
||||
table {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
|
||||
td {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: var(--font-size-normal);
|
||||
}
|
||||
}
|
||||
|
||||
.show--quoted {
|
||||
blockquote {
|
||||
@apply block;
|
||||
}
|
||||
}
|
||||
|
||||
.hide--quoted {
|
||||
blockquote {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user