chore: Update captain pending FAQ interface (#12752)

# Pull Request Template

## Description

**This PR includes,**
- Added new pending FAQs view with approve/edit/delete actions for each
response.
- Implemented banner notification showing pending FAQ count on main
approved responses page.
- Created dedicated route for pending FAQs review at
/captain/responses/pending.
- Added automatic pending count updates when switching assistants or
routes.
- Modified ResponseCard component to show action buttons instead of
dropdown in pending view.

Fixes
https://linear.app/chatwoot/issue/CW-5833/pending-faqs-in-a-different-ux

## Type of change

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

## How Has This Been Tested?

### Loom video
https://www.loom.com/share/5fe8f79b04cd4681b9360c48710b9373


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] 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

---------

Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
Sivin Varghese
2025-10-29 09:17:42 +05:30
committed by GitHub
parent 38af08534c
commit 3e27e28848
7 changed files with 591 additions and 162 deletions

View File

@@ -10,6 +10,7 @@ import AssistantGuidelinesIndex from './assistants/guidelines/Index.vue';
import AssistantScenariosIndex from './assistants/scenarios/Index.vue';
import DocumentsIndex from './documents/Index.vue';
import ResponsesIndex from './responses/Index.vue';
import ResponsesPendingIndex from './responses/Pending.vue';
import CustomToolsIndex from './tools/Index.vue';
export const routes = [
@@ -125,6 +126,19 @@ export const routes = [
],
},
},
{
path: frontendURL('accounts/:accountId/captain/responses/pending'),
component: ResponsesPendingIndex,
name: 'captain_responses_pending',
meta: {
permissions: ['administrator', 'agent'],
featureFlag: FEATURE_FLAGS.CAPTAIN,
installationTypes: [
INSTALLATION_TYPES.CLOUD,
INSTALLATION_TYPES.ENTERPRISE,
],
},
},
{
path: frontendURL('accounts/:accountId/captain/tools'),
component: CustomToolsIndex,

View File

@@ -1,17 +1,15 @@
<script setup>
import { computed, onMounted, ref, nextTick } from 'vue';
import { useMapGetter, useStore } from 'dashboard/composables/store';
import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n';
import { OnClickOutside } from '@vueuse/components';
import { useRouter } from 'vue-router';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { debounce } from '@chatwoot/utils';
import { useAccount } from 'dashboard/composables/useAccount';
import Banner from 'dashboard/components-next/banner/Banner.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
import Input from 'dashboard/components-next/input/Input.vue';
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
import BulkDeleteDialog from 'dashboard/components-next/captain/pageComponents/BulkDeleteDialog.vue';
@@ -37,7 +35,6 @@ const selectedResponse = ref(null);
const deleteDialog = ref(null);
const bulkDeleteDialog = ref(null);
const selectedStatus = ref('all');
const selectedAssistant = ref('all');
const dialogType = ref('');
const searchQuery = ref('');
@@ -45,54 +42,17 @@ const { t } = useI18n();
const createDialog = ref(null);
const isStatusFilterOpen = ref(false);
const shouldShowDropdown = computed(() => {
if (assistants.value.length === 0) return false;
return !isFetching.value;
});
const statusOptions = computed(() =>
['all', 'pending', 'approved'].map(key => ({
label: t(`CAPTAIN.RESPONSES.STATUS.${key.toUpperCase()}`),
value: key,
action: 'filter',
}))
);
const filteredResponses = computed(() => {
return selectedStatus.value === 'pending'
? responses.value.filter(r => r.status === 'pending')
: responses.value;
});
const selectedStatusLabel = computed(() => {
const status = statusOptions.value.find(
option => option.value === selectedStatus.value
);
return t('CAPTAIN.RESPONSES.FILTER.STATUS', {
selected: status ? status.label : '',
});
});
const pendingCount = useMapGetter('captainResponses/getPendingCount');
const handleDelete = () => {
deleteDialog.value.dialogRef.open();
};
const handleAccept = async () => {
try {
await store.dispatch('captainResponses/update', {
id: selectedResponse.value.id,
status: 'approved',
});
useAlert(t(`CAPTAIN.RESPONSES.EDIT.APPROVE_SUCCESS_MESSAGE`));
} catch (error) {
const errorMessage =
error?.message || t(`CAPTAIN.RESPONSES.EDIT.ERROR_MESSAGE`);
useAlert(errorMessage);
} finally {
selectedResponse.value = null;
}
};
const handleCreate = () => {
dialogType.value = 'create';
@@ -105,9 +65,7 @@ const handleEdit = () => {
};
const handleAction = ({ action, id }) => {
selectedResponse.value = filteredResponses.value.find(
response => id === response.id
);
selectedResponse.value = responses.value.find(response => id === response.id);
nextTick(() => {
if (action === 'delete') {
handleDelete();
@@ -115,9 +73,6 @@ const handleAction = ({ action, id }) => {
if (action === 'edit') {
handleEdit();
}
if (action === 'approve') {
handleAccept();
}
});
};
@@ -136,10 +91,8 @@ const handleCreateClose = () => {
};
const fetchResponses = (page = 1) => {
const filterParams = { page };
if (selectedStatus.value !== 'all') {
filterParams.status = selectedStatus.value;
}
const filterParams = { page, status: 'approved' };
if (selectedAssistant.value !== 'all') {
filterParams.assistantId = selectedAssistant.value;
}
@@ -155,7 +108,7 @@ const hoveredCard = ref(null);
const bulkSelectionState = computed(() => {
const selectedCount = bulkSelectedIds.value.size;
const totalCount = filteredResponses.value?.length || 0;
const totalCount = responses.value?.length || 0;
return {
hasSelected: selectedCount > 0,
@@ -168,13 +121,13 @@ const bulkCheckbox = computed({
get: () => bulkSelectionState.value.allSelected,
set: value => {
bulkSelectedIds.value = value
? new Set(filteredResponses.value.map(r => r.id))
? new Set(responses.value.map(r => r.id))
: new Set();
},
});
const buildSelectedCountLabel = computed(() => {
const count = filteredResponses.value?.length || 0;
const count = responses.value?.length || 0;
return bulkSelectionState.value.allSelected
? t('CAPTAIN.RESPONSES.UNSELECT_ALL', { count })
: t('CAPTAIN.RESPONSES.SELECT_ALL', { count });
@@ -191,7 +144,7 @@ const handleCardSelect = id => {
};
const fetchResponseAfterBulkAction = () => {
const hasNoResponsesLeft = filteredResponses.value?.length === 0;
const hasNoResponsesLeft = responses.value?.length === 0;
const currentPage = responseMeta.value?.page;
if (hasNoResponsesLeft) {
@@ -208,22 +161,6 @@ const fetchResponseAfterBulkAction = () => {
bulkSelectedIds.value = new Set();
};
const handleBulkApprove = async () => {
try {
await store.dispatch(
'captainBulkActions/handleBulkApprove',
Array.from(bulkSelectedIds.value)
);
fetchResponseAfterBulkAction();
useAlert(t('CAPTAIN.RESPONSES.BULK_APPROVE.SUCCESS_MESSAGE'));
} catch (error) {
useAlert(
error?.message || t('CAPTAIN.RESPONSES.BULK_APPROVE.ERROR_MESSAGE')
);
}
};
const onPageChange = page => {
// Store current selection state before fetching new page
const wasAllPageSelected = bulkSelectionState.value.allSelected;
@@ -238,7 +175,7 @@ const onPageChange = page => {
};
const onDeleteSuccess = () => {
if (filteredResponses.value?.length === 0 && responseMeta.value?.page > 1) {
if (responses.value?.length === 0 && responseMeta.value?.page > 1) {
onPageChange(responseMeta.value.page - 1);
}
};
@@ -247,12 +184,6 @@ const onBulkDeleteSuccess = () => {
fetchResponseAfterBulkAction();
};
const handleStatusFilterChange = ({ value }) => {
selectedStatus.value = value;
isStatusFilterOpen.value = false;
fetchResponses();
};
const handleAssistantFilterChange = assistant => {
selectedAssistant.value = assistant;
fetchResponses();
@@ -262,9 +193,14 @@ const debouncedSearch = debounce(async () => {
fetchResponses();
}, 500);
const navigateToPendingFAQs = () => {
router.push({ name: 'captain_responses_pending' });
};
onMounted(() => {
store.dispatch('captainAssistants/get');
fetchResponses();
store.dispatch('captainResponses/fetchPendingCount');
});
</script>
@@ -276,8 +212,8 @@ onMounted(() => {
:header-title="$t('CAPTAIN.RESPONSES.HEADER')"
:button-label="$t('CAPTAIN.RESPONSES.ADD_NEW')"
:is-fetching="isFetching"
:is-empty="!filteredResponses.length"
:show-pagination-footer="!isFetching && !!filteredResponses.length"
:is-empty="!responses.length"
:show-pagination-footer="!isFetching && !!responses.length"
:feature-flag="FEATURE_FLAGS.CAPTAIN"
@update:current-page="onPageChange"
@click="handleCreate"
@@ -315,25 +251,7 @@ onMounted(() => {
v-if="!bulkSelectionState.hasSelected"
class="flex gap-3 justify-between w-full items-center"
>
<div class="flex gap-3">
<OnClickOutside @trigger="isStatusFilterOpen = false">
<Button
:label="selectedStatusLabel"
icon="i-lucide-chevron-down"
size="sm"
color="slate"
trailing-icon
class="max-w-48"
@click="isStatusFilterOpen = !isStatusFilterOpen"
/>
<DropdownMenu
v-if="isStatusFilterOpen"
:menu-items="statusOptions"
class="mt-2"
@action="handleStatusFilterChange"
/>
</OnClickOutside>
<div class="flex items-center gap-3">
<AssistantSelector
:assistant-id="selectedAssistant"
@update="handleAssistantFilterChange"
@@ -344,6 +262,7 @@ onMounted(() => {
:placeholder="$t('CAPTAIN.RESPONSES.SEARCH_PLACEHOLDER')"
class="w-64"
size="sm"
type="search"
autofocus
@input="debouncedSearch"
/>
@@ -380,15 +299,6 @@ onMounted(() => {
</div>
<div class="h-4 w-px bg-n-strong" />
<div class="flex gap-3 items-center">
<Button
:label="$t('CAPTAIN.RESPONSES.BULK_APPROVE_BUTTON')"
sm
ghost
icon="i-lucide-check"
class="!px-1.5"
@click="handleBulkApprove"
/>
<div class="h-4 w-px bg-n-strong" />
<Button
:label="$t('CAPTAIN.RESPONSES.BULK_DELETE_BUTTON')"
sm
@@ -406,10 +316,19 @@ onMounted(() => {
<template #body>
<LimitBanner class="mb-5" />
<Banner
v-if="pendingCount > 0"
color="blue"
class="mb-4"
:action-label="$t('CAPTAIN.RESPONSES.PENDING_BANNER.ACTION')"
@action="navigateToPendingFAQs"
>
{{ $t('CAPTAIN.RESPONSES.PENDING_BANNER.TITLE') }}
</Banner>
<div class="flex flex-col gap-4">
<ResponseCard
v-for="response in filteredResponses"
v-for="response in responses"
:id="response.id"
:key="response.id"
:question="response.question"
@@ -422,6 +341,7 @@ onMounted(() => {
:is-selected="bulkSelectedIds.has(response.id)"
:selectable="hoveredCard === response.id || bulkSelectedIds.size > 0"
:show-menu="!bulkSelectedIds.has(response.id)"
:show-actions="false"
@action="handleAction"
@navigate="handleNavigationAction"
@select="handleCardSelect"

View File

@@ -0,0 +1,412 @@
<script setup>
import { computed, onMounted, ref, nextTick } from 'vue';
import { useMapGetter, useStore } from 'dashboard/composables/store';
import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n';
import { useRouter, useRoute } from 'vue-router';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { debounce } from '@chatwoot/utils';
import { useAccount } from 'dashboard/composables/useAccount';
import { frontendURL } from 'dashboard/helper/URLHelper';
import Button from 'dashboard/components-next/button/Button.vue';
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
import Input from 'dashboard/components-next/input/Input.vue';
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
import BulkDeleteDialog from 'dashboard/components-next/captain/pageComponents/BulkDeleteDialog.vue';
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
import CaptainPaywall from 'dashboard/components-next/captain/pageComponents/Paywall.vue';
import AssistantSelector from 'dashboard/components-next/captain/pageComponents/AssistantSelector.vue';
import ResponseCard from 'dashboard/components-next/captain/assistant/ResponseCard.vue';
import CreateResponseDialog from 'dashboard/components-next/captain/pageComponents/response/CreateResponseDialog.vue';
import ResponsePageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/ResponsePageEmptyState.vue';
import FeatureSpotlightPopover from 'dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.vue';
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');
const assistants = useMapGetter('captainAssistants/getRecords');
const responseMeta = useMapGetter('captainResponses/getMeta');
const responses = useMapGetter('captainResponses/getRecords');
const isFetching = computed(() => uiFlags.value.fetchingList);
const selectedResponse = ref(null);
const deleteDialog = ref(null);
const bulkDeleteDialog = ref(null);
const selectedAssistant = ref('all');
const dialogType = ref('');
const searchQuery = ref('');
const { t } = useI18n();
const createDialog = ref(null);
const shouldShowDropdown = computed(() => {
if (assistants.value.length === 0) return false;
return !isFetching.value;
});
const backUrl = computed(() =>
frontendURL(`accounts/${route.params.accountId}/captain/responses`)
);
// Filter out approved responses in pending view
const filteredResponses = computed(() =>
responses.value.filter(response => response.status !== 'approved')
);
const handleDelete = () => {
deleteDialog.value.dialogRef.open();
};
const handleAccept = async () => {
try {
await store.dispatch('captainResponses/update', {
id: selectedResponse.value.id,
status: 'approved',
});
useAlert(t(`CAPTAIN.RESPONSES.EDIT.APPROVE_SUCCESS_MESSAGE`));
} catch (error) {
const errorMessage =
error?.message || t(`CAPTAIN.RESPONSES.EDIT.ERROR_MESSAGE`);
useAlert(errorMessage);
} finally {
selectedResponse.value = null;
}
};
const handleCreate = () => {
dialogType.value = 'create';
nextTick(() => createDialog.value.dialogRef.open());
};
const handleEdit = () => {
dialogType.value = 'edit';
nextTick(() => createDialog.value.dialogRef.open());
};
const handleAction = ({ action, id }) => {
selectedResponse.value = filteredResponses.value.find(
response => id === response.id
);
nextTick(() => {
if (action === 'delete') {
handleDelete();
}
if (action === 'edit') {
handleEdit();
}
if (action === 'approve') {
handleAccept();
}
});
};
const handleNavigationAction = ({ id, type }) => {
if (type === 'Conversation') {
router.push({
name: 'inbox_conversation',
params: { conversation_id: id },
});
}
};
const handleCreateClose = () => {
dialogType.value = '';
selectedResponse.value = null;
};
const fetchResponses = (page = 1) => {
const filterParams = { page, status: 'pending' };
if (selectedAssistant.value !== 'all') {
filterParams.assistantId = selectedAssistant.value;
}
if (searchQuery.value) {
filterParams.search = searchQuery.value;
}
store.dispatch('captainResponses/get', filterParams);
};
// Bulk action
const bulkSelectedIds = ref(new Set());
const hoveredCard = ref(null);
const bulkSelectionState = computed(() => {
const selectedCount = bulkSelectedIds.value.size;
const totalCount = filteredResponses.value?.length || 0;
return {
hasSelected: selectedCount > 0,
isIndeterminate: selectedCount > 0 && selectedCount < totalCount,
allSelected: totalCount > 0 && selectedCount === totalCount,
};
});
const bulkCheckbox = computed({
get: () => bulkSelectionState.value.allSelected,
set: value => {
bulkSelectedIds.value = value
? new Set(filteredResponses.value.map(r => r.id))
: new Set();
},
});
const buildSelectedCountLabel = computed(() => {
const count = filteredResponses.value?.length || 0;
return bulkSelectionState.value.allSelected
? t('CAPTAIN.RESPONSES.UNSELECT_ALL', { count })
: t('CAPTAIN.RESPONSES.SELECT_ALL', { count });
});
const handleCardHover = (isHovered, id) => {
hoveredCard.value = isHovered ? id : null;
};
const handleCardSelect = id => {
const selected = new Set(bulkSelectedIds.value);
selected[selected.has(id) ? 'delete' : 'add'](id);
bulkSelectedIds.value = selected;
};
const fetchResponseAfterBulkAction = () => {
const hasNoResponsesLeft = filteredResponses.value?.length === 0;
const currentPage = responseMeta.value?.page;
if (hasNoResponsesLeft) {
const pageToFetch = currentPage > 1 ? currentPage - 1 : currentPage;
fetchResponses(pageToFetch);
} else {
fetchResponses(currentPage);
}
bulkSelectedIds.value = new Set();
};
const handleBulkApprove = async () => {
try {
await store.dispatch(
'captainBulkActions/handleBulkApprove',
Array.from(bulkSelectedIds.value)
);
fetchResponseAfterBulkAction();
useAlert(t('CAPTAIN.RESPONSES.BULK_APPROVE.SUCCESS_MESSAGE'));
} catch (error) {
useAlert(
error?.message || t('CAPTAIN.RESPONSES.BULK_APPROVE.ERROR_MESSAGE')
);
}
};
const onPageChange = page => {
const wasAllPageSelected = bulkSelectionState.value.allSelected;
const hadPartialSelection = bulkSelectedIds.value.size > 0;
fetchResponses(page);
if (wasAllPageSelected || hadPartialSelection) {
bulkSelectedIds.value = new Set();
}
};
const onDeleteSuccess = () => {
if (filteredResponses.value?.length === 0 && responseMeta.value?.page > 1) {
onPageChange(responseMeta.value.page - 1);
}
};
const onBulkDeleteSuccess = () => {
fetchResponseAfterBulkAction();
};
const handleAssistantFilterChange = assistant => {
selectedAssistant.value = assistant;
fetchResponses();
};
const debouncedSearch = debounce(async () => {
fetchResponses();
}, 500);
onMounted(() => {
store.dispatch('captainAssistants/get');
fetchResponses();
});
</script>
<template>
<PageLayout
:total-count="responseMeta.totalCount"
:current-page="responseMeta.page"
:button-policy="['administrator']"
:header-title="$t('CAPTAIN.RESPONSES.PENDING_FAQS')"
:button-label="$t('CAPTAIN.RESPONSES.ADD_NEW')"
:is-fetching="isFetching"
:is-empty="!filteredResponses.length"
:show-pagination-footer="!isFetching && !!filteredResponses.length"
:feature-flag="FEATURE_FLAGS.CAPTAIN"
:back-url="backUrl"
@update:current-page="onPageChange"
@click="handleCreate"
>
<template #knowMore>
<FeatureSpotlightPopover
:button-label="$t('CAPTAIN.HEADER_KNOW_MORE')"
:title="$t('CAPTAIN.RESPONSES.EMPTY_STATE.FEATURE_SPOTLIGHT.TITLE')"
:note="$t('CAPTAIN.RESPONSES.EMPTY_STATE.FEATURE_SPOTLIGHT.NOTE')"
:hide-actions="!isOnChatwootCloud"
fallback-thumbnail="/assets/images/dashboard/captain/faqs-popover-light.svg"
fallback-thumbnail-dark="/assets/images/dashboard/captain/faqs-popover-dark.svg"
learn-more-url="https://chwt.app/captain-faq"
/>
</template>
<template #emptyState>
<ResponsePageEmptyState @click="handleCreate" />
</template>
<template #paywall>
<CaptainPaywall />
</template>
<template #controls>
<div
v-if="shouldShowDropdown"
class="mb-4 -mt-3 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,
}"
>
<div
v-if="!bulkSelectionState.hasSelected"
class="flex gap-3 justify-between w-full items-center"
>
<div class="flex items-center gap-3">
<AssistantSelector
:assistant-id="selectedAssistant"
@update="handleAssistantFilterChange"
/>
</div>
<Input
v-model="searchQuery"
:placeholder="$t('CAPTAIN.RESPONSES.SEARCH_PLACEHOLDER')"
class="w-64"
size="sm"
type="search"
autofocus
@input="debouncedSearch"
/>
</div>
<transition
name="slide-fade"
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="opacity-0 transform ltr:-translate-x-4 rtl:translate-x-4"
enter-to-class="opacity-100 transform translate-x-0"
leave-active-class="hidden opacity-0"
>
<div
v-if="bulkSelectionState.hasSelected"
class="flex items-center gap-3"
>
<div class="flex items-center gap-3">
<div class="flex items-center gap-1.5">
<Checkbox
v-model="bulkCheckbox"
:indeterminate="bulkSelectionState.isIndeterminate"
/>
<span class="text-sm text-n-slate-12 font-medium tabular-nums">
{{ buildSelectedCountLabel }}
</span>
</div>
<span class="text-sm text-n-slate-10 tabular-nums">
{{
$t('CAPTAIN.RESPONSES.SELECTED', {
count: bulkSelectedIds.size,
})
}}
</span>
</div>
<div class="h-4 w-px bg-n-strong" />
<div class="flex gap-3 items-center">
<Button
:label="$t('CAPTAIN.RESPONSES.BULK_APPROVE_BUTTON')"
sm
ghost
icon="i-lucide-check"
class="!px-1.5"
@click="handleBulkApprove"
/>
<div class="h-4 w-px bg-n-strong" />
<Button
:label="$t('CAPTAIN.RESPONSES.BULK_DELETE_BUTTON')"
sm
ruby
ghost
class="!px-1.5"
icon="i-lucide-trash"
@click="bulkDeleteDialog.dialogRef.open()"
/>
</div>
</div>
</transition>
</div>
</template>
<template #body>
<LimitBanner class="mb-5" />
<div class="flex flex-col gap-4">
<ResponseCard
v-for="response in filteredResponses"
:id="response.id"
:key="response.id"
:question="response.question"
:answer="response.answer"
:assistant="response.assistant"
:documentable="response.documentable"
:status="response.status"
:created-at="response.created_at"
:updated-at="response.updated_at"
:is-selected="bulkSelectedIds.has(response.id)"
:selectable="hoveredCard === response.id || bulkSelectedIds.size > 0"
:show-menu="false"
:show-actions="!bulkSelectedIds.has(response.id)"
@action="handleAction"
@navigate="handleNavigationAction"
@select="handleCardSelect"
@hover="isHovered => handleCardHover(isHovered, response.id)"
/>
</div>
</template>
<DeleteDialog
v-if="selectedResponse"
ref="deleteDialog"
:entity="selectedResponse"
type="Responses"
@delete-success="onDeleteSuccess"
/>
<BulkDeleteDialog
v-if="bulkSelectedIds"
ref="bulkDeleteDialog"
:bulk-ids="bulkSelectedIds"
type="Responses"
@delete-success="onBulkDeleteSuccess"
/>
<CreateResponseDialog
v-if="dialogType"
ref="createDialog"
:type="dialogType"
:selected-response="selectedResponse"
@close="handleCreateClose"
/>
</PageLayout>
</template>