Merge branch 'develop' into feat/email-forwarding
This commit is contained in:
@@ -117,7 +117,7 @@ const props = defineProps({
|
||||
},
|
||||
conversationId: { type: Number, required: true },
|
||||
createdAt: { type: Number, required: true }, // eslint-disable-line vue/no-unused-properties
|
||||
currentUserId: { type: Number, required: true },
|
||||
currentUserId: { type: Number, required: true }, // eslint-disable-line vue/no-unused-properties
|
||||
groupWithNext: { type: Boolean, default: false },
|
||||
inboxId: { type: Number, default: null }, // eslint-disable-line vue/no-unused-properties
|
||||
inboxSupportsReplyTo: { type: Object, default: () => ({}) },
|
||||
@@ -173,7 +173,10 @@ const variant = computed(() => {
|
||||
return variants[props.messageType] || MESSAGE_VARIANTS.USER;
|
||||
});
|
||||
|
||||
const isMyMessage = computed(() => {
|
||||
const isBotOrAgentMessage = computed(() => {
|
||||
if (props.messageType === MESSAGE_TYPES.ACTIVITY) {
|
||||
return false;
|
||||
}
|
||||
// if an outgoing message is still processing, then it's definitely a
|
||||
// message sent by the current user
|
||||
if (
|
||||
@@ -186,13 +189,10 @@ const isMyMessage = computed(() => {
|
||||
const senderType = props.senderType ?? props.sender?.type;
|
||||
|
||||
if (!senderType || !senderId) {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
senderType.toLowerCase() === SENDER_TYPES.USER.toLowerCase() &&
|
||||
props.currentUserId === senderId
|
||||
);
|
||||
return senderType.toLowerCase() === SENDER_TYPES.USER.toLowerCase();
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -200,7 +200,7 @@ const isMyMessage = computed(() => {
|
||||
* @returns {import('vue').ComputedRef<'left'|'right'|'center'>} The computed orientation
|
||||
*/
|
||||
const orientation = computed(() => {
|
||||
if (isMyMessage.value) {
|
||||
if (isBotOrAgentMessage.value) {
|
||||
return ORIENTATION.RIGHT;
|
||||
}
|
||||
|
||||
@@ -221,8 +221,8 @@ const flexOrientationClass = computed(() => {
|
||||
|
||||
const gridClass = computed(() => {
|
||||
const map = {
|
||||
[ORIENTATION.LEFT]: 'grid grid-cols-[24px_1fr]',
|
||||
[ORIENTATION.RIGHT]: 'grid grid-cols-1fr',
|
||||
[ORIENTATION.LEFT]: 'grid grid-cols-1fr',
|
||||
[ORIENTATION.RIGHT]: 'grid grid-cols-[1fr_24px]',
|
||||
};
|
||||
|
||||
return map[orientation.value];
|
||||
@@ -231,13 +231,13 @@ const gridClass = computed(() => {
|
||||
const gridTemplate = computed(() => {
|
||||
const map = {
|
||||
[ORIENTATION.LEFT]: `
|
||||
"avatar bubble"
|
||||
"spacer meta"
|
||||
`,
|
||||
[ORIENTATION.RIGHT]: `
|
||||
"bubble"
|
||||
"meta"
|
||||
`,
|
||||
[ORIENTATION.RIGHT]: `
|
||||
"bubble avatar"
|
||||
"meta spacer"
|
||||
`,
|
||||
};
|
||||
|
||||
return map[orientation.value];
|
||||
@@ -251,7 +251,7 @@ const shouldGroupWithNext = computed(() => {
|
||||
|
||||
const shouldShowAvatar = computed(() => {
|
||||
if (props.messageType === MESSAGE_TYPES.ACTIVITY) return false;
|
||||
if (orientation.value === ORIENTATION.RIGHT) return false;
|
||||
if (orientation.value === ORIENTATION.LEFT) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
@@ -444,7 +444,7 @@ provideMessageContext({
|
||||
isPrivate: computed(() => props.private),
|
||||
variant,
|
||||
orientation,
|
||||
isMyMessage,
|
||||
isBotOrAgentMessage,
|
||||
shouldGroupWithNext,
|
||||
});
|
||||
</script>
|
||||
@@ -476,14 +476,14 @@ provideMessageContext({
|
||||
'w-full': variant === MESSAGE_VARIANTS.EMAIL,
|
||||
},
|
||||
]"
|
||||
class="gap-x-3"
|
||||
class="gap-x-2"
|
||||
:style="{
|
||||
gridTemplateAreas: gridTemplate,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="!shouldGroupWithNext && shouldShowAvatar"
|
||||
v-tooltip.right-end="avatarTooltip"
|
||||
v-tooltip.left-end="avatarTooltip"
|
||||
class="[grid-area:avatar] flex items-end"
|
||||
>
|
||||
<Avatar v-bind="avatarInfo" :size="24" />
|
||||
@@ -491,7 +491,8 @@ provideMessageContext({
|
||||
<div
|
||||
class="[grid-area:bubble] flex"
|
||||
:class="{
|
||||
'ltr:pl-9 rtl:pl-0 justify-end': orientation === ORIENTATION.RIGHT,
|
||||
'ltr:pl-8 rtl:pr-8 justify-end': orientation === ORIENTATION.RIGHT,
|
||||
'ltr:pr-8 rtl:pl-8': orientation === ORIENTATION.LEFT,
|
||||
'min-w-0': variant === MESSAGE_VARIANTS.EMAIL,
|
||||
}"
|
||||
@contextmenu="openContextMenu($event)"
|
||||
|
||||
@@ -14,7 +14,7 @@ const readableTime = computed(() =>
|
||||
<template>
|
||||
<BaseBubble
|
||||
v-tooltip.top="readableTime"
|
||||
class="px-2 py-0.5 !rounded-full flex min-w-0 items-center gap-2"
|
||||
class="px-3 py-1 !rounded-xl flex min-w-0 items-center gap-2"
|
||||
data-bubble-name="activity"
|
||||
>
|
||||
<span v-dompurify-html="content" :title="content" />
|
||||
|
||||
@@ -98,7 +98,7 @@ const MessageControl = Symbol('MessageControl');
|
||||
* @property {import('vue').Ref<Sender|null>} [sender=null] - The sender information
|
||||
* @property {import('vue').ComputedRef<MessageOrientation>} orientation - The visual variant of the message
|
||||
* @property {import('vue').ComputedRef<MessageVariant>} variant - The visual variant of the message
|
||||
* @property {import('vue').ComputedRef<boolean>} isMyMessage - Does the message belong to the current user
|
||||
* @property {import('vue').ComputedRef<boolean>} isBotOrAgentMessage - Does the message belong to the current user
|
||||
* @property {import('vue').ComputedRef<boolean>} isPrivate - Proxy computed value for private
|
||||
* @property {import('vue').ComputedRef<boolean>} shouldGroupWithNext - Should group with the next message or not, it is different from groupWithNext, this has a bypass for a failed message
|
||||
*/
|
||||
|
||||
@@ -15,6 +15,13 @@ const props = defineProps({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
subMenuPosition: {
|
||||
type: String,
|
||||
default: 'right',
|
||||
validator: value => {
|
||||
return ['right', 'left', 'bottom'].includes(value);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
@@ -44,14 +51,21 @@ const handleSelect = value => {
|
||||
trailing-icon
|
||||
color="slate"
|
||||
variant="faded"
|
||||
class="!w-fit"
|
||||
class="!w-fit max-w-40"
|
||||
:class="{ 'dark:!bg-n-alpha-2 !bg-n-slate-9/20': isOpen }"
|
||||
:label="labelValue"
|
||||
@click="toggleMenu"
|
||||
/>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="absolute ltr:left-full rtl:right-full select-none max-w-48 ltr:ml-1 rtl:mr-1 flex flex-col gap-1 bg-n-alpha-3 backdrop-blur-[100px] p-1 top-0 shadow-lg rounded-lg border border-n-weak"
|
||||
class="absolute select-none max-w-64 flex flex-col gap-1 bg-n-alpha-3 backdrop-blur-[100px] p-1 top-0 shadow-lg z-40 rounded-lg border border-n-weak dark:border-n-strong/50"
|
||||
:class="{
|
||||
'ltr:left-full rtl:right-full ltr:ml-1 rtl:mr-1':
|
||||
subMenuPosition === 'right',
|
||||
'ltr:right-full rtl:left-full ltr:mr-1 rtl:ml-1':
|
||||
subMenuPosition === 'left',
|
||||
'top-full mt-1 ltr:right-0 rtl:left-0': subMenuPosition === 'bottom',
|
||||
}"
|
||||
>
|
||||
<Button
|
||||
v-for="option in options"
|
||||
|
||||
+139
-103
@@ -1,93 +1,129 @@
|
||||
<script>
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { mapGetters } from 'vuex';
|
||||
import FilterItem from './FilterItem.vue';
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import SelectMenu from 'dashboard/components-next/selectmenu/SelectMenu.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const CHAT_STATUS_FILTER_ITEMS = Object.freeze([
|
||||
'open',
|
||||
'resolved',
|
||||
'pending',
|
||||
'snoozed',
|
||||
'all',
|
||||
]);
|
||||
defineProps({
|
||||
isOnExpandedLayout: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const SORT_ORDER_ITEMS = Object.freeze([
|
||||
'last_activity_at_asc',
|
||||
'last_activity_at_desc',
|
||||
'created_at_desc',
|
||||
'created_at_asc',
|
||||
'priority_desc',
|
||||
'priority_asc',
|
||||
'waiting_since_asc',
|
||||
'waiting_since_desc',
|
||||
]);
|
||||
const emit = defineEmits(['changeFilter']);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FilterItem,
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
isOnExpandedLayout: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['changeFilter'],
|
||||
setup() {
|
||||
const { updateUISettings } = useUISettings();
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
return {
|
||||
updateUISettings,
|
||||
};
|
||||
const { updateUISettings } = useUISettings();
|
||||
|
||||
const chatStatusFilter = useMapGetter('getChatStatusFilter');
|
||||
const chatSortFilter = useMapGetter('getChatSortFilter');
|
||||
|
||||
const [showActionsDropdown, toggleDropdown] = useToggle();
|
||||
|
||||
const currentStatusFilter = computed(() => {
|
||||
return chatStatusFilter.value || wootConstants.STATUS_TYPE.OPEN;
|
||||
});
|
||||
|
||||
const currentSortBy = computed(() => {
|
||||
return (
|
||||
chatSortFilter.value || wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC
|
||||
);
|
||||
});
|
||||
|
||||
const chatStatusOptions = [
|
||||
{
|
||||
label: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.open.TEXT'),
|
||||
value: 'open',
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showActionsDropdown: false,
|
||||
chatStatusItems: CHAT_STATUS_FILTER_ITEMS,
|
||||
chatSortItems: SORT_ORDER_ITEMS,
|
||||
};
|
||||
{
|
||||
label: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.resolved.TEXT'),
|
||||
value: 'resolved',
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
chatStatusFilter: 'getChatStatusFilter',
|
||||
chatSortFilter: 'getChatSortFilter',
|
||||
}),
|
||||
chatStatus() {
|
||||
return this.chatStatusFilter || wootConstants.STATUS_TYPE.OPEN;
|
||||
},
|
||||
sortFilter() {
|
||||
return (
|
||||
this.chatSortFilter || wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC
|
||||
);
|
||||
},
|
||||
{
|
||||
label: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.pending.TEXT'),
|
||||
value: 'pending',
|
||||
},
|
||||
methods: {
|
||||
onTabChange(value) {
|
||||
this.$emit('changeFilter', value);
|
||||
this.closeDropdown();
|
||||
},
|
||||
toggleDropdown() {
|
||||
this.showActionsDropdown = !this.showActionsDropdown;
|
||||
},
|
||||
closeDropdown() {
|
||||
this.showActionsDropdown = false;
|
||||
},
|
||||
onChangeFilter(value, type) {
|
||||
this.$emit('changeFilter', value, type);
|
||||
this.saveSelectedFilter(type, value);
|
||||
},
|
||||
saveSelectedFilter(type, value) {
|
||||
this.updateUISettings({
|
||||
conversations_filter_by: {
|
||||
status: type === 'status' ? value : this.chatStatus,
|
||||
order_by: type === 'sort' ? value : this.sortFilter,
|
||||
},
|
||||
});
|
||||
},
|
||||
{
|
||||
label: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.snoozed.TEXT'),
|
||||
value: 'snoozed',
|
||||
},
|
||||
{
|
||||
label: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.all.TEXT'),
|
||||
value: 'all',
|
||||
},
|
||||
];
|
||||
|
||||
const chatSortOptions = [
|
||||
{
|
||||
label: t('CHAT_LIST.SORT_ORDER_ITEMS.last_activity_at_asc.TEXT'),
|
||||
value: 'last_activity_at_asc',
|
||||
},
|
||||
{
|
||||
label: t('CHAT_LIST.SORT_ORDER_ITEMS.last_activity_at_desc.TEXT'),
|
||||
value: 'last_activity_at_desc',
|
||||
},
|
||||
{
|
||||
label: t('CHAT_LIST.SORT_ORDER_ITEMS.created_at_desc.TEXT'),
|
||||
value: 'created_at_desc',
|
||||
},
|
||||
{
|
||||
label: t('CHAT_LIST.SORT_ORDER_ITEMS.created_at_asc.TEXT'),
|
||||
value: 'created_at_asc',
|
||||
},
|
||||
{
|
||||
label: t('CHAT_LIST.SORT_ORDER_ITEMS.priority_desc.TEXT'),
|
||||
value: 'priority_desc',
|
||||
},
|
||||
{
|
||||
label: t('CHAT_LIST.SORT_ORDER_ITEMS.priority_asc.TEXT'),
|
||||
value: 'priority_asc',
|
||||
},
|
||||
{
|
||||
label: t('CHAT_LIST.SORT_ORDER_ITEMS.waiting_since_asc.TEXT'),
|
||||
value: 'waiting_since_asc',
|
||||
},
|
||||
{
|
||||
label: t('CHAT_LIST.SORT_ORDER_ITEMS.waiting_since_desc.TEXT'),
|
||||
value: 'waiting_since_desc',
|
||||
},
|
||||
];
|
||||
|
||||
const activeChatStatusLabel = computed(
|
||||
() =>
|
||||
chatStatusOptions.find(m => m.value === chatStatusFilter.value)?.label || ''
|
||||
);
|
||||
|
||||
const activeChatSortLabel = computed(
|
||||
() => chatSortOptions.find(m => m.value === chatSortFilter.value)?.label || ''
|
||||
);
|
||||
|
||||
const saveSelectedFilter = (type, value) => {
|
||||
updateUISettings({
|
||||
conversations_filter_by: {
|
||||
status: type === 'status' ? value : currentStatusFilter.value,
|
||||
order_by: type === 'sort' ? value : currentSortBy.value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleStatusChange = value => {
|
||||
emit('changeFilter', value, 'status');
|
||||
store.dispatch('setChatStatusFilter', value);
|
||||
saveSelectedFilter('status', value);
|
||||
};
|
||||
|
||||
const handleSortChange = value => {
|
||||
emit('changeFilter', value, 'sort');
|
||||
store.dispatch('setChatSortFilter', value);
|
||||
saveSelectedFilter('sort', value);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -99,39 +135,39 @@ export default {
|
||||
slate
|
||||
faded
|
||||
xs
|
||||
@click="toggleDropdown"
|
||||
@click="toggleDropdown()"
|
||||
/>
|
||||
<div
|
||||
v-if="showActionsDropdown"
|
||||
v-on-clickaway="closeDropdown"
|
||||
class="mt-1 dropdown-pane dropdown-pane--open !w-52 !p-4 top-6 border !border-n-weak dark:!border-n-weak !bg-n-alpha-3 dark:!bg-n-alpha-3 backdrop-blur-[100px]"
|
||||
v-on-click-outside="() => toggleDropdown()"
|
||||
class="mt-1 bg-n-alpha-3 backdrop-blur-[100px] border border-n-weak w-72 rounded-xl p-4 absolute z-40 top-full"
|
||||
:class="{
|
||||
'ltr:left-0 rtl:right-0': !isOnExpandedLayout,
|
||||
'ltr:right-0 rtl:left-0': isOnExpandedLayout,
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center justify-between last:mt-4">
|
||||
<span class="text-xs font-medium text-n-slate-12">{{
|
||||
$t('CHAT_LIST.CHAT_SORT.STATUS')
|
||||
}}</span>
|
||||
<FilterItem
|
||||
type="status"
|
||||
:selected-value="chatStatus"
|
||||
:items="chatStatusItems"
|
||||
path-prefix="CHAT_LIST.CHAT_STATUS_FILTER_ITEMS"
|
||||
@on-change-filter="onChangeFilter"
|
||||
<div class="flex items-center justify-between last:mt-4 gap-2">
|
||||
<span class="text-sm truncate text-n-slate-12">
|
||||
{{ $t('CHAT_LIST.CHAT_SORT.STATUS') }}
|
||||
</span>
|
||||
<SelectMenu
|
||||
:model-value="chatStatusFilter"
|
||||
:options="chatStatusOptions"
|
||||
:label="activeChatStatusLabel"
|
||||
:sub-menu-position="isOnExpandedLayout ? 'left' : 'right'"
|
||||
@update:model-value="handleStatusChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between last:mt-4">
|
||||
<span class="text-xs font-medium text-n-slate-12">{{
|
||||
$t('CHAT_LIST.CHAT_SORT.ORDER_BY')
|
||||
}}</span>
|
||||
<FilterItem
|
||||
type="sort"
|
||||
:selected-value="sortFilter"
|
||||
:items="chatSortItems"
|
||||
path-prefix="CHAT_LIST.SORT_ORDER_ITEMS"
|
||||
@on-change-filter="onChangeFilter"
|
||||
<div class="flex items-center justify-between last:mt-4 gap-2">
|
||||
<span class="text-sm truncate text-n-slate-12">
|
||||
{{ $t('CHAT_LIST.CHAT_SORT.ORDER_BY') }}
|
||||
</span>
|
||||
<SelectMenu
|
||||
:model-value="chatSortFilter"
|
||||
:options="chatSortOptions"
|
||||
:label="activeChatSortLabel"
|
||||
:sub-menu-position="isOnExpandedLayout ? 'left' : 'right'"
|
||||
@update:model-value="handleSortChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user