mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 03:27:52 +00:00
chore: Fix issues with Contact pages (#10544)
This commit is contained in:
@@ -50,7 +50,7 @@ const handleBreadcrumbClick = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section
|
<section
|
||||||
class="flex w-full h-full gap-4 overflow-hidden justify-evenly bg-n-background"
|
class="flex w-full h-full overflow-hidden justify-evenly bg-n-background"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col w-full h-full transition-all duration-300 ltr:2xl:ml-56 rtl:2xl:mr-56"
|
class="flex flex-col w-full h-full transition-all duration-300 ltr:2xl:ml-56 rtl:2xl:mr-56"
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ const emit = defineEmits([
|
|||||||
"
|
"
|
||||||
color="slate"
|
color="slate"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="relative"
|
class="relative w-8"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@click="emit('filter')"
|
@click="emit('filter')"
|
||||||
>
|
>
|
||||||
@@ -109,7 +109,6 @@ const emit = defineEmits([
|
|||||||
icon="i-lucide-save"
|
icon="i-lucide-save"
|
||||||
color="slate"
|
color="slate"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="relative"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@click="emit('createSegment')"
|
@click="emit('createSegment')"
|
||||||
/>
|
/>
|
||||||
@@ -118,7 +117,6 @@ const emit = defineEmits([
|
|||||||
icon="i-lucide-trash"
|
icon="i-lucide-trash"
|
||||||
color="slate"
|
color="slate"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="relative"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@click="emit('deleteSegment')"
|
@click="emit('deleteSegment')"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,28 +1,67 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
import { useMapGetter } from 'dashboard/composables/store';
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
|
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
|
||||||
|
|
||||||
import ActiveFilterPreview from 'dashboard/components-next/filter/ActiveFilterPreview.vue';
|
import ActiveFilterPreview from 'dashboard/components-next/filter/ActiveFilterPreview.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
activeSegment: { type: Object, default: null },
|
||||||
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['clearFilters', 'openFilter']);
|
const emit = defineEmits(['clearFilters', 'openFilter']);
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
const appliedFilters = useMapGetter('contacts/getAppliedContactFiltersV4');
|
const appliedFilters = useMapGetter('contacts/getAppliedContactFiltersV4');
|
||||||
|
const activeSegmentId = computed(() => route.params.segmentId);
|
||||||
|
|
||||||
|
const activeSegmentQuery = computed(() => {
|
||||||
|
const query = props.activeSegment?.query?.payload;
|
||||||
|
if (!Array.isArray(query)) return [];
|
||||||
|
|
||||||
|
const newFilters = query.map(filter => {
|
||||||
|
const transformed = useCamelCase(filter);
|
||||||
|
return {
|
||||||
|
attributeKey: transformed.attributeKey,
|
||||||
|
attributeModel: transformed.attributeModel,
|
||||||
|
customAttributeType: transformed.customAttributeType,
|
||||||
|
filterOperator: transformed.filterOperator,
|
||||||
|
queryOperator: transformed.queryOperator ?? 'and',
|
||||||
|
values: transformed.values,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return newFilters;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasActiveSegments = computed(
|
||||||
|
() => props.activeSegment && activeSegmentId.value !== 0
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeFilterQueryData = computed(() => {
|
||||||
|
return hasActiveSegments.value
|
||||||
|
? activeSegmentQuery.value
|
||||||
|
: appliedFilters.value;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ActiveFilterPreview
|
<ActiveFilterPreview
|
||||||
:applied-filters="appliedFilters"
|
:applied-filters="activeFilterQueryData"
|
||||||
:max-visible-filters="2"
|
:max-visible-filters="2"
|
||||||
:more-filters-label="
|
:more-filters-label="
|
||||||
t('CONTACTS_LAYOUT.FILTER.ACTIVE_FILTERS.MORE_FILTERS', {
|
t('CONTACTS_LAYOUT.FILTER.ACTIVE_FILTERS.MORE_FILTERS', {
|
||||||
count: appliedFilters.length - 2,
|
count: activeFilterQueryData.length - 2,
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
:clear-button-label="
|
:clear-button-label="
|
||||||
t('CONTACTS_LAYOUT.FILTER.ACTIVE_FILTERS.CLEAR_FILTERS')
|
t('CONTACTS_LAYOUT.FILTER.ACTIVE_FILTERS.CLEAR_FILTERS')
|
||||||
"
|
"
|
||||||
|
:show-clear-button="!hasActiveSegments"
|
||||||
class="max-w-[960px] px-6"
|
class="max-w-[960px] px-6"
|
||||||
@open-filter="emit('openFilter')"
|
@open-filter="emit('openFilter')"
|
||||||
@clear-filters="emit('clearFilters')"
|
@clear-filters="emit('clearFilters')"
|
||||||
|
|||||||
@@ -7,50 +7,18 @@ import ContactsActiveFiltersPreview from 'dashboard/components-next/Contacts/Con
|
|||||||
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
|
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
searchValue: {
|
searchValue: { type: String, default: '' },
|
||||||
type: String,
|
headerTitle: { type: String, default: '' },
|
||||||
default: '',
|
showPaginationFooter: { type: Boolean, default: true },
|
||||||
},
|
currentPage: { type: Number, default: 1 },
|
||||||
headerTitle: {
|
totalItems: { type: Number, default: 100 },
|
||||||
type: String,
|
itemsPerPage: { type: Number, default: 15 },
|
||||||
default: '',
|
activeSort: { type: String, default: '' },
|
||||||
},
|
activeOrdering: { type: String, default: '' },
|
||||||
showPaginationFooter: {
|
activeSegment: { type: Object, default: null },
|
||||||
type: Boolean,
|
segmentsId: { type: [String, Number], default: 0 },
|
||||||
default: true,
|
hasAppliedFilters: { type: Boolean, default: false },
|
||||||
},
|
isFetchingList: { type: Boolean, default: false },
|
||||||
currentPage: {
|
|
||||||
type: Number,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
totalItems: {
|
|
||||||
type: Number,
|
|
||||||
default: 100,
|
|
||||||
},
|
|
||||||
itemsPerPage: {
|
|
||||||
type: Number,
|
|
||||||
default: 15,
|
|
||||||
},
|
|
||||||
activeSort: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
activeOrdering: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
activeSegment: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
segmentsId: {
|
|
||||||
type: [String, Number],
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
hasAppliedFilters: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@@ -106,7 +74,12 @@ const openFilter = () => {
|
|||||||
<main class="flex-1 overflow-y-auto">
|
<main class="flex-1 overflow-y-auto">
|
||||||
<div class="w-full mx-auto max-w-[960px]">
|
<div class="w-full mx-auto max-w-[960px]">
|
||||||
<ContactsActiveFiltersPreview
|
<ContactsActiveFiltersPreview
|
||||||
v-if="hasAppliedFilters && isNotSegmentView"
|
v-if="
|
||||||
|
(hasAppliedFilters || !isNotSegmentView) &&
|
||||||
|
!isFetchingList &&
|
||||||
|
!isLabelView
|
||||||
|
"
|
||||||
|
:active-segment="activeSegment"
|
||||||
@clear-filters="emit('clearFilters')"
|
@clear-filters="emit('clearFilters')"
|
||||||
@open-filter="openFilter"
|
@open-filter="openFilter"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -34,23 +34,20 @@ const contactConversations = computed(() =>
|
|||||||
>
|
>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="contactConversations.length > 0" class="flex flex-col py-6">
|
|
||||||
<div
|
<div
|
||||||
v-for="conversation in contactConversations"
|
v-else-if="contactConversations.length > 0"
|
||||||
:key="conversation.id"
|
class="px-6 py-4 divide-y divide-n-strong [&>*:hover]:!border-y-transparent [&>*:hover+*]:!border-t-transparent"
|
||||||
class="border-b border-n-strong"
|
|
||||||
>
|
>
|
||||||
<ConversationCard
|
<ConversationCard
|
||||||
v-if="conversation"
|
v-for="conversation in contactConversations"
|
||||||
:key="conversation.id"
|
:key="conversation.id"
|
||||||
:conversation="conversation"
|
:conversation="conversation"
|
||||||
:contact="contactsById(conversation.meta.sender.id)"
|
:contact="contactsById(conversation.meta.sender.id)"
|
||||||
:state-inbox="stateInbox(conversation.inboxId)"
|
:state-inbox="stateInbox(conversation.inboxId)"
|
||||||
:account-labels="accountLabelsValue"
|
:account-labels="accountLabelsValue"
|
||||||
class="px-6 !rounded-none dark:hover:bg-n-alpha-3 hover:bg-n-alpha-1"
|
class="rounded-none hover:rounded-xl hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<p v-else class="px-6 py-10 text-sm leading-6 text-center text-n-slate-11">
|
<p v-else class="px-6 py-10 text-sm leading-6 text-center text-n-slate-11">
|
||||||
{{ t('CONTACTS_LAYOUT.SIDEBAR.HISTORY.EMPTY_STATE') }}
|
{{ t('CONTACTS_LAYOUT.SIDEBAR.HISTORY.EMPTY_STATE') }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const handleDelete = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-2 px-6 py-2 border-b border-n-strong group/note"
|
class="flex flex-col gap-2 py-2 mx-6 border-b border-n-strong group/note"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-1.5 py-2.5 min-w-0">
|
<div class="flex items-center gap-1.5 py-2.5 min-w-0">
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ const onCardClick = e => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex w-full gap-3 px-3 py-4 transition-colors duration-300 ease-in-out cursor-pointer rounded-xl"
|
class="flex w-full gap-3 px-3 py-4 transition-all duration-300 ease-in-out cursor-pointer"
|
||||||
@click="onCardClick"
|
@click="onCardClick"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
|
|||||||
@@ -4,22 +4,11 @@ import { replaceUnderscoreWithSpace } from './helper/filterHelper.js';
|
|||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
appliedFilters: {
|
appliedFilters: { type: Array, default: () => [] },
|
||||||
type: Array,
|
maxVisibleFilters: { type: Number, default: 2 },
|
||||||
default: () => [],
|
clearButtonLabel: { type: String, default: '' },
|
||||||
},
|
moreFiltersLabel: { type: String, default: '' },
|
||||||
maxVisibleFilters: {
|
showClearButton: { type: Boolean, default: true },
|
||||||
type: Number,
|
|
||||||
default: 2,
|
|
||||||
},
|
|
||||||
clearButtonLabel: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
moreFiltersLabel: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['clearFilters', 'openFilter']);
|
const emit = defineEmits(['clearFilters', 'openFilter']);
|
||||||
@@ -46,6 +35,9 @@ const formatOperatorLabel = operator => {
|
|||||||
|
|
||||||
const formatFilterValue = value => {
|
const formatFilterValue = value => {
|
||||||
if (!value) return '';
|
if (!value) return '';
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.join(', ');
|
||||||
|
}
|
||||||
if (typeof value === 'object' && value.name) {
|
if (typeof value === 'object' && value.name) {
|
||||||
return value.name;
|
return value.name;
|
||||||
}
|
}
|
||||||
@@ -55,10 +47,7 @@ const formatFilterValue = value => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap items-center w-full gap-2 mx-auto">
|
<div class="flex flex-wrap items-center w-full gap-2 mx-auto">
|
||||||
<template
|
<template v-for="(filter, index) in appliedFilters" :key="index">
|
||||||
v-for="(filter, index) in appliedFilters"
|
|
||||||
:key="filter.attributeKey"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
v-if="index < maxVisibleFilters"
|
v-if="index < maxVisibleFilters"
|
||||||
class="inline-flex items-center gap-2 h-7"
|
class="inline-flex items-center gap-2 h-7"
|
||||||
@@ -107,8 +96,9 @@ const formatFilterValue = value => {
|
|||||||
>
|
>
|
||||||
{{ moreFiltersLabel }}
|
{{ moreFiltersLabel }}
|
||||||
</div>
|
</div>
|
||||||
<div class="w-px h-3 rounded-lg bg-n-strong" />
|
<div v-if="showClearButton" class="w-px h-3 rounded-lg bg-n-strong" />
|
||||||
<Button
|
<Button
|
||||||
|
v-if="showClearButton"
|
||||||
:label="clearButtonLabel"
|
:label="clearButtonLabel"
|
||||||
size="xs"
|
size="xs"
|
||||||
class="!px-1"
|
class="!px-1"
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ onMounted(async () => {
|
|||||||
:active-ordering="sortState.activeOrdering"
|
:active-ordering="sortState.activeOrdering"
|
||||||
:active-segment="activeSegment"
|
:active-segment="activeSegment"
|
||||||
:segments-id="activeSegmentId"
|
:segments-id="activeSegmentId"
|
||||||
|
:is-fetching-list="isFetchingList"
|
||||||
:has-applied-filters="hasAppliedFilters"
|
:has-applied-filters="hasAppliedFilters"
|
||||||
@update:current-page="fetchContactsBasedOnContext"
|
@update:current-page="fetchContactsBasedOnContext"
|
||||||
@search="searchContacts"
|
@search="searchContacts"
|
||||||
|
|||||||
Reference in New Issue
Block a user