Allow knowledge-base managers to manage Captain FAQs
Some checks failed
Some checks failed
This commit is contained in:
parent
cbbfccaf42
commit
efc61936e9
@ -10,6 +10,7 @@ import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||
import Policy from 'dashboard/components/policy.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import { PORTAL_PERMISSIONS } from 'dashboard/constants/permissions';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
@ -71,6 +72,7 @@ const emit = defineEmits(['action', 'navigate', 'select', 'hover']);
|
||||
const { t } = useI18n();
|
||||
|
||||
const [showActionsDropdown, toggleDropdown] = useToggle();
|
||||
const responseManagePermissions = ['administrator', PORTAL_PERMISSIONS];
|
||||
|
||||
const modelValue = computed({
|
||||
get: () => props.isSelected,
|
||||
@ -142,7 +144,7 @@ const handleDocumentableClick = () => {
|
||||
<div v-if="!compact && showMenu" class="flex items-center gap-2">
|
||||
<Policy
|
||||
v-on-clickaway="() => toggleDropdown(false)"
|
||||
:permissions="['administrator']"
|
||||
:permissions="responseManagePermissions"
|
||||
class="relative flex items-center group"
|
||||
>
|
||||
<Button
|
||||
@ -168,7 +170,7 @@ const handleDocumentableClick = () => {
|
||||
v-if="!compact"
|
||||
class="flex items-start justify-between flex-col-reverse md:flex-row gap-3"
|
||||
>
|
||||
<Policy v-if="showActions" :permissions="['administrator']">
|
||||
<Policy v-if="showActions" :permissions="responseManagePermissions">
|
||||
<div class="flex items-center gap-2 sm:gap-5 w-full">
|
||||
<Button
|
||||
v-if="status === 'pending'"
|
||||
|
||||
@ -6,6 +6,7 @@ import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ResponseCard from 'dashboard/components-next/captain/assistant/ResponseCard.vue';
|
||||
import FeatureSpotlight from 'dashboard/components-next/feature-spotlight/FeatureSpotlight.vue';
|
||||
import { responsesList } from 'dashboard/components-next/captain/pageComponents/emptyStates/captainEmptyStateContent.js';
|
||||
import { PORTAL_PERMISSIONS } from 'dashboard/constants/permissions';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
@ -28,6 +29,7 @@ const isPending = computed(() => props.variant === 'pending');
|
||||
|
||||
const { isOnChatwootCloud } = useAccount();
|
||||
const { replaceInstallationName } = useBranding();
|
||||
const responseManagePermissions = ['administrator', PORTAL_PERMISSIONS];
|
||||
|
||||
const onClick = () => {
|
||||
emit('click');
|
||||
@ -56,7 +58,7 @@ const onClearFilters = () => {
|
||||
: $t('CAPTAIN.RESPONSES.EMPTY_STATE.TITLE')
|
||||
"
|
||||
:subtitle="isApproved ? $t('CAPTAIN.RESPONSES.EMPTY_STATE.SUBTITLE') : ''"
|
||||
:action-perms="['administrator']"
|
||||
:action-perms="responseManagePermissions"
|
||||
:show-backdrop="isApproved"
|
||||
>
|
||||
<template v-if="isApproved" #empty-state-item>
|
||||
|
||||
@ -6,6 +6,7 @@ import { useMapGetter, useStore } from 'dashboard/composables/store.js';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
import Policy from 'dashboard/components/policy.vue';
|
||||
|
||||
const emit = defineEmits(['close', 'createAssistant']);
|
||||
|
||||
@ -105,14 +106,16 @@ const openCreateAssistantDialog = () => {
|
||||
{{ t('CAPTAIN.ASSISTANT_SWITCHER.SWITCH_ASSISTANT') }}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
:label="t('CAPTAIN.ASSISTANT_SWITCHER.NEW_ASSISTANT')"
|
||||
color="slate"
|
||||
icon="i-lucide-plus"
|
||||
size="sm"
|
||||
class="!bg-n-alpha-2 hover:!bg-n-alpha-3"
|
||||
@click="openCreateAssistantDialog"
|
||||
/>
|
||||
<Policy :permissions="['administrator']">
|
||||
<Button
|
||||
:label="t('CAPTAIN.ASSISTANT_SWITCHER.NEW_ASSISTANT')"
|
||||
color="slate"
|
||||
icon="i-lucide-plus"
|
||||
size="sm"
|
||||
class="!bg-n-alpha-2 hover:!bg-n-alpha-3"
|
||||
@click="openCreateAssistantDialog"
|
||||
/>
|
||||
</Policy>
|
||||
</div>
|
||||
<div v-if="assistants.length > 0" class="flex flex-col gap-2 px-4">
|
||||
<Button
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
|
||||
import { PORTAL_PERMISSIONS } from 'dashboard/constants/permissions';
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
|
||||
import CaptainPageRouteView from './pages/CaptainPageRouteView.vue';
|
||||
@ -31,6 +32,11 @@ const meta = {
|
||||
installationTypes: [INSTALLATION_TYPES.CLOUD, INSTALLATION_TYPES.ENTERPRISE],
|
||||
};
|
||||
|
||||
const knowledgeBaseMeta = {
|
||||
...meta,
|
||||
permissions: ['administrator', 'agent', PORTAL_PERMISSIONS],
|
||||
};
|
||||
|
||||
const metaV2 = {
|
||||
permissions: ['administrator', 'agent'],
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN_V2,
|
||||
@ -42,13 +48,13 @@ const assistantRoutes = [
|
||||
path: frontendURL('accounts/:accountId/captain/:assistantId/faqs'),
|
||||
component: ResponsesIndex,
|
||||
name: 'captain_assistants_responses_index',
|
||||
meta,
|
||||
meta: knowledgeBaseMeta,
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/captain/:assistantId/documents'),
|
||||
component: DocumentsIndex,
|
||||
name: 'captain_assistants_documents_index',
|
||||
meta,
|
||||
meta: knowledgeBaseMeta,
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/captain/:assistantId/tools'),
|
||||
@ -78,7 +84,7 @@ const assistantRoutes = [
|
||||
path: frontendURL('accounts/:accountId/captain/:assistantId/faqs/pending'),
|
||||
component: ResponsesPendingIndex,
|
||||
name: 'captain_assistants_responses_pending',
|
||||
meta,
|
||||
meta: knowledgeBaseMeta,
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/captain/:assistantId/settings'),
|
||||
@ -119,7 +125,7 @@ const assistantRoutes = [
|
||||
path: frontendURL('accounts/:accountId/captain/:navigationPath'),
|
||||
component: AssistantsIndexPage,
|
||||
name: 'captain_assistants_index',
|
||||
meta,
|
||||
meta: knowledgeBaseMeta,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -6,6 +6,8 @@ import { useRouter, useRoute } from 'vue-router';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { usePolicy } from 'dashboard/composables/usePolicy';
|
||||
import { PORTAL_PERMISSIONS } from 'dashboard/constants/permissions';
|
||||
|
||||
import Banner from 'dashboard/components-next/banner/Banner.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
@ -24,6 +26,7 @@ const router = useRouter();
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
const { isOnChatwootCloud } = useAccount();
|
||||
const { checkPermissions } = usePolicy();
|
||||
const uiFlags = useMapGetter('captainResponses/getUIFlags');
|
||||
const responseMeta = useMapGetter('captainResponses/getMeta');
|
||||
const responses = useMapGetter('captainResponses/getRecords');
|
||||
@ -38,6 +41,10 @@ const searchQuery = ref('');
|
||||
const { t } = useI18n();
|
||||
|
||||
const createDialog = ref(null);
|
||||
const responseManagePermissions = ['administrator', PORTAL_PERMISSIONS];
|
||||
const canManageResponses = computed(() =>
|
||||
checkPermissions(responseManagePermissions)
|
||||
);
|
||||
|
||||
const selectedAssistantId = computed(() => Number(route.params.assistantId));
|
||||
|
||||
@ -206,7 +213,7 @@ onMounted(() => {
|
||||
<PageLayout
|
||||
:total-count="responseMeta.totalCount"
|
||||
:current-page="responseMeta.page"
|
||||
:button-policy="['administrator']"
|
||||
:button-policy="responseManagePermissions"
|
||||
:header-title="$t('CAPTAIN.RESPONSES.HEADER')"
|
||||
:button-label="$t('CAPTAIN.RESPONSES.ADD_NEW')"
|
||||
:is-fetching="isFetching"
|
||||
@ -247,6 +254,7 @@ onMounted(() => {
|
||||
|
||||
<template #subHeader>
|
||||
<BulkSelectBar
|
||||
v-if="canManageResponses"
|
||||
v-model="bulkSelectedIds"
|
||||
:all-items="responses"
|
||||
:select-all-label="buildSelectedCountLabel"
|
||||
@ -293,8 +301,11 @@ onMounted(() => {
|
||||
: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="!bulkSelectedIds.has(response.id)"
|
||||
:selectable="
|
||||
canManageResponses &&
|
||||
(hoveredCard === response.id || bulkSelectedIds.size > 0)
|
||||
"
|
||||
:show-menu="canManageResponses && !bulkSelectedIds.has(response.id)"
|
||||
:show-actions="false"
|
||||
@action="handleAction"
|
||||
@navigate="handleNavigationAction"
|
||||
|
||||
@ -7,6 +7,8 @@ import { useRouter, useRoute } from 'vue-router';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { usePolicy } from 'dashboard/composables/usePolicy';
|
||||
import { PORTAL_PERMISSIONS } from 'dashboard/constants/permissions';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
@ -25,6 +27,7 @@ const router = useRouter();
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
const { isOnChatwootCloud } = useAccount();
|
||||
const { checkPermissions } = usePolicy();
|
||||
const uiFlags = useMapGetter('captainResponses/getUIFlags');
|
||||
const responseMeta = useMapGetter('captainResponses/getMeta');
|
||||
const responses = useMapGetter('captainResponses/getRecords');
|
||||
@ -40,6 +43,10 @@ const searchQuery = ref('');
|
||||
const { t } = useI18n();
|
||||
|
||||
const createDialog = ref(null);
|
||||
const responseManagePermissions = ['administrator', PORTAL_PERMISSIONS];
|
||||
const canManageResponses = computed(() =>
|
||||
checkPermissions(responseManagePermissions)
|
||||
);
|
||||
|
||||
const backUrl = computed(() => ({
|
||||
name: 'captain_assistants_responses_index',
|
||||
@ -286,6 +293,7 @@ onMounted(() => {
|
||||
|
||||
<template #subHeader>
|
||||
<BulkSelectBar
|
||||
v-if="canManageResponses"
|
||||
v-model="bulkSelectedIds"
|
||||
:all-items="filteredResponses"
|
||||
:select-all-label="buildSelectedCountLabel"
|
||||
@ -338,9 +346,14 @@ onMounted(() => {
|
||||
:created-at="response.created_at"
|
||||
:updated-at="response.updated_at"
|
||||
:is-selected="bulkSelectedIds.has(response.id)"
|
||||
:selectable="hoveredCard === response.id || bulkSelectedIds.size > 0"
|
||||
:selectable="
|
||||
canManageResponses &&
|
||||
(hoveredCard === response.id || bulkSelectedIds.size > 0)
|
||||
"
|
||||
:show-menu="false"
|
||||
:show-actions="!bulkSelectedIds.has(response.id)"
|
||||
:show-actions="
|
||||
canManageResponses && !bulkSelectedIds.has(response.id)
|
||||
"
|
||||
@action="handleAction"
|
||||
@navigate="handleNavigationAction"
|
||||
@select="handleCardSelect"
|
||||
|
||||
@ -24,10 +24,6 @@ class Captain::AssistantResponsePolicy < ApplicationPolicy
|
||||
def manage?
|
||||
return true if @account_user.administrator?
|
||||
|
||||
if @account_user.custom_role.present?
|
||||
return @account_user.custom_role.permissions.include?('knowledge_base_manage')
|
||||
end
|
||||
|
||||
@account_user.agent?
|
||||
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || false
|
||||
end
|
||||
end
|
||||
|
||||
@ -180,16 +180,15 @@ RSpec.describe 'Api::V1::Accounts::Captain::AssistantResponses', type: :request
|
||||
expect(json_response[:answer]).to eq('Test answer')
|
||||
end
|
||||
|
||||
it 'creates a new response if the user is an agent' do
|
||||
it 'does not create a new response if the user is an agent without knowledge base permission' do
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/captain/assistant_responses",
|
||||
params: valid_params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.to change(Captain::AssistantResponse, :count).by(1)
|
||||
end.not_to change(Captain::AssistantResponse, :count)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(json_response[:question]).to eq('Test question?')
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
|
||||
it 'creates a new response if the user has a custom role with knowledge base permission' do
|
||||
@ -254,7 +253,6 @@ RSpec.describe 'Api::V1::Accounts::Captain::AssistantResponses', type: :request
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@ -281,12 +279,25 @@ RSpec.describe 'Api::V1::Accounts::Captain::AssistantResponses', type: :request
|
||||
expect(json_response[:answer]).to eq('Updated answer')
|
||||
end
|
||||
|
||||
it 'updates the response if the user is an agent' do
|
||||
it 'does not update the response if the user is an agent without knowledge base permission' do
|
||||
patch "/api/v1/accounts/#{account.id}/captain/assistant_responses/#{response_record.id}",
|
||||
params: update_params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(response_record.reload.question).not_to eq('Updated question?')
|
||||
end
|
||||
|
||||
it 'updates the response if the user has a custom role with knowledge base permission' do
|
||||
custom_role = create(:custom_role, account: account, permissions: ['knowledge_base_manage'])
|
||||
AccountUser.find_by!(account: account, user: agent_with_custom_role).update!(custom_role: custom_role)
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/captain/assistant_responses/#{response_record.id}",
|
||||
params: update_params,
|
||||
headers: agent_with_custom_role.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json_response[:question]).to eq('Updated question?')
|
||||
end
|
||||
@ -315,11 +326,24 @@ RSpec.describe 'Api::V1::Accounts::Captain::AssistantResponses', type: :request
|
||||
describe 'DELETE /api/v1/accounts/:account_id/captain/assistant_responses/:id' do
|
||||
let!(:response_record) { create(:captain_assistant_response, assistant: assistant) }
|
||||
|
||||
it 'deletes the response' do
|
||||
it 'does not delete the response if the user is an agent without knowledge base permission' do
|
||||
expect do
|
||||
delete "/api/v1/accounts/#{account.id}/captain/assistant_responses/#{response_record.id}",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.not_to change(Captain::AssistantResponse, :count)
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
|
||||
it 'deletes the response if the user has a custom role with knowledge base permission' do
|
||||
custom_role = create(:custom_role, account: account, permissions: ['knowledge_base_manage'])
|
||||
AccountUser.find_by!(account: account, user: agent_with_custom_role).update!(custom_role: custom_role)
|
||||
|
||||
expect do
|
||||
delete "/api/v1/accounts/#{account.id}/captain/assistant_responses/#{response_record.id}",
|
||||
headers: agent_with_custom_role.create_new_auth_token,
|
||||
as: :json
|
||||
end.to change(Captain::AssistantResponse, :count).by(-1)
|
||||
|
||||
expect(response).to have_http_status(:no_content)
|
||||
|
||||
@ -30,19 +30,16 @@ RSpec.describe 'Api::V1::Accounts::Captain::BulkActions', type: :request do
|
||||
}
|
||||
end
|
||||
|
||||
it 'approves the responses and returns the updated records' do
|
||||
it 'does not approve the responses if the user is an agent without knowledge base permission' do
|
||||
post "/api/v1/accounts/#{account.id}/captain/bulk_actions",
|
||||
params: valid_params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json_response).to be_an(Array)
|
||||
expect(json_response.length).to eq(2)
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
|
||||
# Verify responses were approved
|
||||
pending_responses.each do |response|
|
||||
expect(response.reload.status).to eq('approved')
|
||||
expect(response.reload.status).to eq('pending')
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -56,20 +53,18 @@ RSpec.describe 'Api::V1::Accounts::Captain::BulkActions', type: :request do
|
||||
}
|
||||
end
|
||||
|
||||
it 'deletes the responses and returns an empty array' do
|
||||
it 'does not delete the responses if the user is an agent without knowledge base permission' do
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/captain/bulk_actions",
|
||||
params: delete_params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.to change(Captain::AssistantResponse, :count).by(-2)
|
||||
end.not_to change(Captain::AssistantResponse, :count)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json_response).to eq([])
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
|
||||
# Verify responses were deleted
|
||||
pending_responses.each do |response|
|
||||
expect { response.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect(response.reload.status).to eq('pending')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Loading…
Reference in New Issue
Block a user