chore: Improvements in pending FAQs (#12755)

# Pull Request Template

## Description

**This PR includes:**

1. Added URL-based filter persistence for the responses pages, including
page and search parameters.
2. Introduced a new empty state variant for pending FAQs — without a
backdrop and with a “Clear Filters” option.
3. Made the actions, filter, and search row remain fixed at the top
while scrolling.

Fixes
https://linear.app/chatwoot/issue/CW-5852/improvements-in-pending-faqs

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

### Loom video
https://www.loom.com/share/1d9eee68c0684f0ab05e08b4ca1e0ce9


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
This commit is contained in:
Sivin Varghese
2025-10-30 03:04:28 +05:30
committed by GitHub
parent 31497d9c63
commit c31d693add
6 changed files with 159 additions and 39 deletions

View File

@@ -2,7 +2,7 @@
import { computed, onMounted, ref, nextTick } from 'vue';
import { useMapGetter, useStore } from 'dashboard/composables/store';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useRouter, useRoute } from 'vue-router';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { debounce } from '@chatwoot/utils';
import { useAccount } from 'dashboard/composables/useAccount';
@@ -23,6 +23,7 @@ import FeatureSpotlightPopover from 'dashboard/components-next/feature-spotlight
import LimitBanner from 'dashboard/components-next/captain/pageComponents/response/LimitBanner.vue';
const router = useRouter();
const route = useRoute();
const store = useStore();
const { isOnChatwootCloud } = useAccount();
const uiFlags = useMapGetter('captainResponses/getUIFlags');
@@ -90,6 +91,18 @@ const handleCreateClose = () => {
selectedResponse.value = null;
};
const updateURLWithFilters = (page, search) => {
const query = {
page: page || 1,
};
if (search) {
query.search = search;
}
router.replace({ query });
};
const fetchResponses = (page = 1) => {
const filterParams = { page, status: 'approved' };
@@ -99,6 +112,10 @@ const fetchResponses = (page = 1) => {
if (searchQuery.value) {
filterParams.search = searchQuery.value;
}
// Update URL with current filters
updateURLWithFilters(page, searchQuery.value);
store.dispatch('captainResponses/get', filterParams);
};
@@ -186,20 +203,28 @@ const onBulkDeleteSuccess = () => {
const handleAssistantFilterChange = assistant => {
selectedAssistant.value = assistant;
fetchResponses();
fetchResponses(1);
};
const debouncedSearch = debounce(async () => {
fetchResponses();
fetchResponses(1);
}, 500);
const initializeFromURL = () => {
if (route.query.search) {
searchQuery.value = route.query.search;
}
const pageFromURL = parseInt(route.query.page, 10) || 1;
fetchResponses(pageFromURL);
};
const navigateToPendingFAQs = () => {
router.push({ name: 'captain_responses_pending' });
};
onMounted(() => {
store.dispatch('captainAssistants/get');
fetchResponses();
initializeFromURL();
store.dispatch('captainResponses/fetchPendingCount');
});
</script>
@@ -230,18 +255,10 @@ onMounted(() => {
/>
</template>
<template #emptyState>
<ResponsePageEmptyState @click="handleCreate" />
</template>
<template #paywall>
<CaptainPaywall />
</template>
<template #controls>
<template #subHeader>
<div
v-if="shouldShowDropdown"
class="mb-4 -mt-3 flex justify-between items-center py-1"
class="mb-2 flex justify-between items-center py-1"
:class="{
'ltr:pl-3 rtl:pr-3 ltr:pr-1 rtl:pl-1 rounded-lg outline outline-1 outline-n-weak bg-n-solid-3 w-fit':
bulkSelectionState.hasSelected,
@@ -314,12 +331,20 @@ onMounted(() => {
</div>
</template>
<template #emptyState>
<ResponsePageEmptyState @click="handleCreate" />
</template>
<template #paywall>
<CaptainPaywall />
</template>
<template #body>
<LimitBanner class="mb-5" />
<Banner
v-if="pendingCount > 0"
color="blue"
class="mb-4"
class="mb-4 -mt-3"
:action-label="$t('CAPTAIN.RESPONSES.PENDING_BANNER.ACTION')"
@action="navigateToPendingFAQs"
>

View File

@@ -119,6 +119,18 @@ const handleCreateClose = () => {
selectedResponse.value = null;
};
const updateURLWithFilters = (page, search) => {
const query = {
page: page || 1,
};
if (search) {
query.search = search;
}
router.replace({ query });
};
const fetchResponses = (page = 1) => {
const filterParams = { page, status: 'pending' };
@@ -128,6 +140,10 @@ const fetchResponses = (page = 1) => {
if (searchQuery.value) {
filterParams.search = searchQuery.value;
}
// Update URL with current filters
updateURLWithFilters(page, searchQuery.value);
store.dispatch('captainResponses/get', filterParams);
};
@@ -225,16 +241,34 @@ const onBulkDeleteSuccess = () => {
const handleAssistantFilterChange = assistant => {
selectedAssistant.value = assistant;
fetchResponses();
fetchResponses(1);
};
const debouncedSearch = debounce(async () => {
fetchResponses();
fetchResponses(1);
}, 500);
const hasActiveFilters = computed(() => {
return Boolean(searchQuery.value || selectedAssistant.value !== 'all');
});
const clearFilters = () => {
searchQuery.value = '';
selectedAssistant.value = 'all';
fetchResponses(1);
};
const initializeFromURL = () => {
if (route.query.search) {
searchQuery.value = route.query.search;
}
const pageFromURL = parseInt(route.query.page, 10) || 1;
fetchResponses(pageFromURL);
};
onMounted(() => {
store.dispatch('captainAssistants/get');
fetchResponses();
initializeFromURL();
});
</script>
@@ -265,18 +299,10 @@ onMounted(() => {
/>
</template>
<template #emptyState>
<ResponsePageEmptyState @click="handleCreate" />
</template>
<template #paywall>
<CaptainPaywall />
</template>
<template #controls>
<template #subHeader>
<div
v-if="shouldShowDropdown"
class="mb-4 -mt-3 flex justify-between items-center py-1"
class="mb-2 flex justify-between items-center py-1"
:class="{
'ltr:pl-3 rtl:pr-3 ltr:pr-1 rtl:pl-1 rounded-lg outline outline-1 outline-n-weak bg-n-solid-3 w-fit':
bulkSelectionState.hasSelected,
@@ -358,6 +384,19 @@ onMounted(() => {
</div>
</template>
<template #emptyState>
<ResponsePageEmptyState
variant="pending"
:has-active-filters="hasActiveFilters"
@click="handleCreate"
@clear-filters="clearFilters"
/>
</template>
<template #paywall>
<CaptainPaywall />
</template>
<template #body>
<LimitBanner class="mb-5" />