feat: Update Captain pages UI (#13184)
This commit is contained in:
@@ -37,7 +37,7 @@ const updateCurrentPage = page => {
|
||||
<slot name="default" />
|
||||
</div>
|
||||
</main>
|
||||
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-0 px-4 pb-4">
|
||||
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-0">
|
||||
<PaginationFooter
|
||||
current-page-info="COMPANIES_LAYOUT.PAGINATION_FOOTER.SHOWING"
|
||||
:current-page="currentPage"
|
||||
|
||||
@@ -100,7 +100,7 @@ const togglePortalSwitcher = () => {
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</main>
|
||||
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-10 px-4 pb-4">
|
||||
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-10">
|
||||
<PaginationFooter
|
||||
:current-page="currentPage"
|
||||
:total-items="totalItems"
|
||||
|
||||
@@ -20,11 +20,11 @@ const emit = defineEmits(['action']);
|
||||
|
||||
const bannerClass = computed(() => {
|
||||
const classMap = {
|
||||
slate: 'bg-n-slate-3 border-n-slate-4 text-n-slate-11',
|
||||
amber: 'bg-n-amber-3 border-n-amber-4 text-n-amber-11',
|
||||
teal: 'bg-n-teal-3 border-n-teal-4 text-n-teal-11',
|
||||
ruby: 'bg-n-ruby-3 border-n-ruby-4 text-n-ruby-11',
|
||||
blue: 'bg-n-blue-3 border-n-blue-4 text-n-blue-11',
|
||||
slate: 'bg-n-slate-1 outline-n-slate-5 text-n-slate-12',
|
||||
amber: 'bg-n-amber-1 outline-n-amber-5 text-n-amber-12',
|
||||
teal: 'bg-n-teal-1 outline-n-teal-5 text-n-teal-12',
|
||||
ruby: 'bg-n-ruby-1 outline-n-ruby-5 text-n-ruby-12',
|
||||
blue: 'bg-n-blue-1 outline-n-blue-5 text-n-blue-12',
|
||||
};
|
||||
|
||||
return classMap[props.color];
|
||||
@@ -32,11 +32,11 @@ const bannerClass = computed(() => {
|
||||
|
||||
const buttonClass = computed(() => {
|
||||
const classMap = {
|
||||
slate: 'bg-n-slate-4 hover:bg-n-slate-5 text-n-slate-11',
|
||||
amber: 'bg-n-amber-4 hover:bg-n-amber-5 text-n-amber-11',
|
||||
teal: 'bg-n-teal-4 hover:bg-n-teal-5 text-n-teal-11',
|
||||
ruby: 'bg-n-ruby-4 hover:bg-n-ruby-5 text-n-ruby-11',
|
||||
blue: 'bg-n-blue-4 hover:bg-n-blue-5 text-n-blue-11',
|
||||
slate: 'hover:bg-n-slate-4 text-n-slate-11',
|
||||
amber: 'hover:bg-n-amber-4 text-n-amber-11',
|
||||
teal: 'hover:bg-n-teal-4 text-n-teal-11',
|
||||
ruby: 'hover:bg-n-ruby-4 text-n-ruby-11',
|
||||
blue: 'hover:bg-n-blue-4 text-n-blue-11',
|
||||
};
|
||||
|
||||
return classMap[props.color];
|
||||
@@ -49,7 +49,7 @@ const triggerAction = () => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="text-sm rounded-xl flex items-center justify-between gap-2 border"
|
||||
class="text-sm rounded-xl flex items-center justify-between min-h-12 gap-2 outline outline-1 -outline-offset-1"
|
||||
:class="[
|
||||
bannerClass,
|
||||
{
|
||||
@@ -64,11 +64,11 @@ const triggerAction = () => {
|
||||
<div>
|
||||
<button
|
||||
v-if="actionLabel"
|
||||
class="px-3 py-1 w-auto grid place-content-center rounded-lg"
|
||||
class="px-3 py-1 w-auto flex items-center justify-center rounded-lg flex-shrink-0"
|
||||
:class="buttonClass"
|
||||
@click="triggerAction"
|
||||
>
|
||||
{{ actionLabel }}
|
||||
<span class="line-clamp-2 text-button">{{ actionLabel }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -130,7 +130,7 @@ const handleCreateAssistant = () => {
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
v-if="!isFetchingAssistants"
|
||||
class="text-xl font-medium truncate text-n-slate-12"
|
||||
class="text-heading-1 truncate text-n-slate-12"
|
||||
>
|
||||
{{ activeAssistantName }}
|
||||
</span>
|
||||
@@ -162,11 +162,16 @@ const handleCreateAssistant = () => {
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
v-if="showAssistantSwitcher && !showPaywall && headerTitle"
|
||||
class="w-0.5 h-4 rounded-2xl bg-n-weak"
|
||||
class="w-px h-3 rounded-xl bg-n-weak"
|
||||
/>
|
||||
<span
|
||||
v-if="headerTitle"
|
||||
class="text-xl font-medium text-n-slate-12"
|
||||
class="text-n-slate-12"
|
||||
:class="
|
||||
showAssistantSwitcher && !showPaywall
|
||||
? 'text-body-main'
|
||||
: 'text-heading-1'
|
||||
"
|
||||
>
|
||||
{{ headerTitle }}
|
||||
</span>
|
||||
@@ -174,7 +179,7 @@ const handleCreateAssistant = () => {
|
||||
v-if="!isEmpty && showKnowMore"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<div class="w-0.5 h-4 rounded-2xl bg-n-weak" />
|
||||
<div class="w-px h-3 rounded-xl bg-n-weak" />
|
||||
<slot name="knowMore" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,7 +227,7 @@ const handleCreateAssistant = () => {
|
||||
<slot />
|
||||
</div>
|
||||
</main>
|
||||
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-10 px-4 pb-4">
|
||||
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-10">
|
||||
<PaginationFooter
|
||||
:current-page="currentPage"
|
||||
:total-items="totalCount"
|
||||
|
||||
@@ -20,6 +20,11 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: 'Delete',
|
||||
},
|
||||
animationDirection: {
|
||||
type: String,
|
||||
default: 'horizontal',
|
||||
validator: value => ['horizontal', 'vertical'].includes(value),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['bulkDelete']);
|
||||
@@ -49,19 +54,32 @@ const bulkCheckboxState = computed({
|
||||
modelValue.value = newSelectedIds;
|
||||
},
|
||||
});
|
||||
|
||||
const animationClasses = computed(() => {
|
||||
if (props.animationDirection === 'vertical') {
|
||||
return {
|
||||
enterFrom: 'opacity-0 transform -translate-y-4',
|
||||
enterTo: 'opacity-100 transform translate-y-0',
|
||||
};
|
||||
}
|
||||
return {
|
||||
enterFrom: 'opacity-0 transform ltr:-translate-x-4 rtl:translate-x-4',
|
||||
enterTo: 'opacity-100 transform translate-x-0',
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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"
|
||||
:enter-from-class="animationClasses.enterFrom"
|
||||
:enter-to-class="animationClasses.enterTo"
|
||||
leave-active-class="hidden opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="hasSelected"
|
||||
class="flex items-center gap-3 py-1 ltr:pl-3 rtl:pr-3 ltr:pr-4 rtl:pl-4 rounded-lg bg-n-solid-2 outline outline-1 outline-n-container shadow"
|
||||
class="flex items-center gap-3 py-2 ltr:pl-3 rtl:pr-3 ltr:pr-2 rtl:pl-2 rounded-xl bg-n-solid-2 outline outline-1 outline-n-container -outline-offset-1 shadow-sm"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-1.5 min-w-0">
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
import { usePolicy } from 'dashboard/composables/usePolicy';
|
||||
import {
|
||||
isPdfDocument,
|
||||
formatDocumentLink,
|
||||
} from 'shared/helpers/documentHelper';
|
||||
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Policy from 'dashboard/components/policy.vue';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
@@ -37,88 +35,59 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const emit = defineEmits(['action']);
|
||||
const { checkPermissions } = usePolicy();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const [showActionsDropdown, toggleDropdown] = useToggle();
|
||||
|
||||
const menuItems = computed(() => {
|
||||
const allOptions = [
|
||||
{
|
||||
label: t('CAPTAIN.DOCUMENTS.OPTIONS.VIEW_RELATED_RESPONSES'),
|
||||
value: 'viewRelatedQuestions',
|
||||
action: 'viewRelatedQuestions',
|
||||
icon: 'i-ph-tree-view-duotone',
|
||||
},
|
||||
];
|
||||
|
||||
if (checkPermissions(['administrator'])) {
|
||||
allOptions.push({
|
||||
label: t('CAPTAIN.DOCUMENTS.OPTIONS.DELETE_DOCUMENT'),
|
||||
value: 'delete',
|
||||
action: 'delete',
|
||||
icon: 'i-lucide-trash',
|
||||
});
|
||||
}
|
||||
|
||||
return allOptions;
|
||||
});
|
||||
|
||||
const createdAt = computed(() => dynamicTime(props.createdAt));
|
||||
|
||||
const displayLink = computed(() => formatDocumentLink(props.externalLink));
|
||||
const linkIcon = computed(() =>
|
||||
isPdfDocument(props.externalLink) ? 'i-ph-file-pdf' : 'i-ph-link-simple'
|
||||
isPdfDocument(props.externalLink) ? 'i-lucide-file-text' : 'i-lucide-link'
|
||||
);
|
||||
|
||||
const handleAction = ({ action, value }) => {
|
||||
toggleDropdown(false);
|
||||
const handleAction = (action, value) => {
|
||||
emit('action', { action, value, id: props.id });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardLayout>
|
||||
<CardLayout class="[&>div]:px-5 [&>div]:gap-2">
|
||||
<div class="flex gap-1 justify-between w-full">
|
||||
<span class="text-base text-n-slate-12 line-clamp-1">
|
||||
<span class="text-heading-3 text-n-slate-12 line-clamp-1">
|
||||
{{ name }}
|
||||
</span>
|
||||
<div class="flex gap-2 items-center">
|
||||
<div
|
||||
v-on-clickaway="() => toggleDropdown(false)"
|
||||
class="flex relative items-center group"
|
||||
>
|
||||
<Button
|
||||
icon="i-woot-menu-list"
|
||||
slate
|
||||
sm
|
||||
ghost
|
||||
@click="handleAction('viewRelatedQuestions', 'viewRelatedQuestions')"
|
||||
/>
|
||||
<div class="w-px h-3 bg-n-weak rounded-lg" />
|
||||
<Policy :permissions="['administrator']">
|
||||
<Button
|
||||
icon="i-lucide-ellipsis-vertical"
|
||||
color="slate"
|
||||
size="xs"
|
||||
class="rounded-md group-hover:bg-n-alpha-2"
|
||||
@click="toggleDropdown()"
|
||||
icon="i-woot-bin"
|
||||
slate
|
||||
sm
|
||||
ghost
|
||||
@click="handleAction('delete', 'delete')"
|
||||
/>
|
||||
<DropdownMenu
|
||||
v-if="showActionsDropdown"
|
||||
:menu-items="menuItems"
|
||||
class="top-full mt-1 ltr:right-0 rtl:left-0 xl:ltr:right-0 xl:rtl:left-0"
|
||||
@action="handleAction($event)"
|
||||
/>
|
||||
</div>
|
||||
</Policy>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4 justify-between items-center w-full">
|
||||
<span
|
||||
class="flex gap-1 items-center text-sm truncate shrink-0 text-n-slate-11"
|
||||
>
|
||||
<i class="i-woot-captain" />
|
||||
<Icon icon="i-woot-captain" />
|
||||
{{ assistant?.name || '' }}
|
||||
</span>
|
||||
<span
|
||||
class="flex flex-1 gap-1 justify-start items-center text-sm truncate text-n-slate-11"
|
||||
>
|
||||
<i :class="linkIcon" class="shrink-0" />
|
||||
<span class="truncate">{{ displayLink }}</span>
|
||||
<Icon :icon="linkIcon" class="shrink-0" />
|
||||
<span class="truncate text-body-main">{{ displayLink }}</span>
|
||||
</span>
|
||||
<div class="text-sm shrink-0 text-n-slate-11 line-clamp-1">
|
||||
<div class="text-label-small shrink-0 text-n-slate-11 line-clamp-1">
|
||||
{{ createdAt }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Policy from 'dashboard/components/policy.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import { INBOX_TYPES, getInboxIconByType } from 'dashboard/helper/inbox';
|
||||
import ChannelIcon from 'dashboard/components-next/icon/ChannelIcon.vue';
|
||||
import { INBOX_TYPES, getChannelTypeDisplayName } from 'dashboard/helper/inbox';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
@@ -23,10 +20,6 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['action']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const [showActionsDropdown, toggleDropdown] = useToggle();
|
||||
|
||||
const inboxName = computed(() => {
|
||||
const inbox = props.inbox;
|
||||
if (!inbox?.name) {
|
||||
@@ -49,56 +42,47 @@ const inboxName = computed(() => {
|
||||
return inbox.name;
|
||||
});
|
||||
|
||||
const menuItems = computed(() => [
|
||||
{
|
||||
label: t('CAPTAIN.INBOXES.OPTIONS.DISCONNECT'),
|
||||
value: 'delete',
|
||||
action: 'delete',
|
||||
icon: 'i-lucide-trash',
|
||||
},
|
||||
]);
|
||||
|
||||
const icon = computed(() => {
|
||||
const { medium, channel_type: type } = props.inbox;
|
||||
return getInboxIconByType(type, medium, 'line');
|
||||
const channelType = computed(() => {
|
||||
const { channel_type: type, medium } = props.inbox;
|
||||
return getChannelTypeDisplayName(type, medium);
|
||||
});
|
||||
|
||||
const handleAction = ({ action, value }) => {
|
||||
toggleDropdown(false);
|
||||
const handleAction = (action, value) => {
|
||||
emit('action', { action, value, id: props.id });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardLayout>
|
||||
<CardLayout
|
||||
class="ltr:[&>div]:pl-4 ltr:[&>div]:pr-3 rtl:[&>div]:pl-3 rtl:[&>div]:pr-4 [&>div]:gap-2 [&>div]:py-4"
|
||||
>
|
||||
<div class="flex justify-between w-full gap-1">
|
||||
<span
|
||||
class="text-base text-n-slate-12 line-clamp-1 flex items-center gap-2"
|
||||
>
|
||||
<Icon :icon="icon" class="size-5" />
|
||||
{{ inboxName }}
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<Policy
|
||||
v-on-clickaway="() => toggleDropdown(false)"
|
||||
:permissions="['administrator']"
|
||||
class="relative flex items-center group"
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div
|
||||
class="size-8 rounded-[0.625rem] flex items-center justify-center outline outline-1 outline-n-weak -outline-offset-1 flex-shrink-0"
|
||||
>
|
||||
<Button
|
||||
icon="i-lucide-ellipsis-vertical"
|
||||
color="slate"
|
||||
size="xs"
|
||||
class="rounded-md group-hover:bg-n-alpha-2"
|
||||
@click="toggleDropdown()"
|
||||
<ChannelIcon
|
||||
:inbox="inbox"
|
||||
class="size-4 flex-shrink-0 text-n-slate-11"
|
||||
/>
|
||||
<DropdownMenu
|
||||
v-if="showActionsDropdown"
|
||||
:menu-items="menuItems"
|
||||
class="mt-1 ltr:right-0 rtl:left-0 top-full"
|
||||
@action="handleAction($event)"
|
||||
/>
|
||||
</Policy>
|
||||
</div>
|
||||
<span class="text-heading-3 text-n-slate-12 line-clamp-1 min-w-0">
|
||||
{{ inboxName }}
|
||||
</span>
|
||||
<div class="w-px h-3 bg-n-weak rounded-lg flex-shrink-0" />
|
||||
<span class="text-body-main text-n-slate-11 flex-shrink-0">{{
|
||||
channelType
|
||||
}}</span>
|
||||
</div>
|
||||
<Policy :permissions="['administrator']">
|
||||
<Button
|
||||
icon="i-lucide-unlink"
|
||||
slate
|
||||
sm
|
||||
ghost
|
||||
@click="handleAction('delete', 'delete')"
|
||||
/>
|
||||
</Policy>
|
||||
</div>
|
||||
</CardLayout>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
|
||||
import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
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';
|
||||
@@ -36,10 +32,6 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
assistant: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
updatedAt: {
|
||||
type: Number,
|
||||
required: true,
|
||||
@@ -52,10 +44,6 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
selectable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showMenu: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
@@ -66,53 +54,18 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['action', 'navigate', 'select', 'hover']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const [showActionsDropdown, toggleDropdown] = useToggle();
|
||||
const emit = defineEmits(['action', 'navigate', 'select']);
|
||||
|
||||
const modelValue = computed({
|
||||
get: () => props.isSelected,
|
||||
set: () => emit('select', props.id),
|
||||
});
|
||||
|
||||
const statusAction = computed(() => {
|
||||
if (props.status === 'pending') {
|
||||
return [
|
||||
{
|
||||
label: t('CAPTAIN.RESPONSES.OPTIONS.APPROVE'),
|
||||
value: 'approve',
|
||||
action: 'approve',
|
||||
icon: 'i-lucide-circle-check-big',
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const menuItems = computed(() => [
|
||||
...statusAction.value,
|
||||
{
|
||||
label: t('CAPTAIN.RESPONSES.OPTIONS.EDIT_RESPONSE'),
|
||||
value: 'edit',
|
||||
action: 'edit',
|
||||
icon: 'i-lucide-pencil-line',
|
||||
},
|
||||
{
|
||||
label: t('CAPTAIN.RESPONSES.OPTIONS.DELETE_RESPONSE'),
|
||||
value: 'delete',
|
||||
action: 'delete',
|
||||
icon: 'i-lucide-trash',
|
||||
},
|
||||
]);
|
||||
|
||||
const timestamp = computed(() =>
|
||||
dynamicTime(props.updatedAt || props.createdAt)
|
||||
);
|
||||
|
||||
const handleAssistantAction = ({ action, value }) => {
|
||||
toggleDropdown(false);
|
||||
const handleAssistantAction = (action, value) => {
|
||||
emit('action', { action, value, id: props.id });
|
||||
};
|
||||
|
||||
@@ -125,43 +78,103 @@ const handleDocumentableClick = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardLayout
|
||||
selectable
|
||||
class="relative"
|
||||
:class="{ 'rounded-md': compact }"
|
||||
@mouseenter="emit('hover', true)"
|
||||
@mouseleave="emit('hover', false)"
|
||||
>
|
||||
<div v-show="selectable" class="absolute top-7 ltr:left-3 rtl:right-3">
|
||||
<Checkbox v-model="modelValue" />
|
||||
</div>
|
||||
<div class="flex relative justify-between w-full gap-1">
|
||||
<span class="text-base text-n-slate-12 line-clamp-1">
|
||||
{{ question }}
|
||||
</span>
|
||||
<div v-if="!compact && showMenu" class="flex items-center gap-2">
|
||||
<div class="py-4 flex flex-col gap-2">
|
||||
<div
|
||||
v-if="!compact"
|
||||
class="flex items-start gap-3 h-6"
|
||||
:class="{ 'justify-between w-full': !showActions }"
|
||||
>
|
||||
<div class="inline-flex items-center gap-2 min-w-0">
|
||||
<!-- <span
|
||||
v-if="status === 'approved'"
|
||||
class="text-sm shrink-0 truncate text-n-slate-11 inline-flex items-center gap-1"
|
||||
>
|
||||
<Icon icon="i-woot-captain" class="size-3.5" />
|
||||
{{ assistant?.name || '' }}
|
||||
</span> -->
|
||||
<div
|
||||
v-if="documentable"
|
||||
class="text-sm text-n-slate-11 grid grid-cols-[auto_1fr] items-center gap-1.5 min-w-0"
|
||||
>
|
||||
<Icon
|
||||
v-if="documentable.type === 'Captain::Document'"
|
||||
icon="i-lucide-files"
|
||||
class="size-3.5 flex-shrink-0"
|
||||
/>
|
||||
<Icon
|
||||
v-else-if="documentable.type === 'User'"
|
||||
icon="i-lucide-circle-user-round"
|
||||
class="size-3.5 flex-shrink-0"
|
||||
/>
|
||||
<Icon
|
||||
v-else-if="documentable.type === 'Conversation'"
|
||||
icon="i-lucide-message-circle"
|
||||
class="size-3.5 flex-shrink-0"
|
||||
/>
|
||||
<span
|
||||
v-if="documentable.type === 'Captain::Document'"
|
||||
class="truncate text-body-main text-n-slate-11"
|
||||
:title="documentable.name"
|
||||
>
|
||||
{{ documentable.name }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="documentable.type === 'User'"
|
||||
class="truncate text-body-main text-n-slate-11"
|
||||
:title="documentable.available_name"
|
||||
>
|
||||
{{ documentable.available_name }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="documentable.type === 'Conversation'"
|
||||
class="hover:underline truncate cursor-pointer text-body-main text-n-slate-11"
|
||||
role="button"
|
||||
@click="handleDocumentableClick"
|
||||
>
|
||||
{{ documentable.display_id }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-px h-3 rounded-lg bg-n-strong" />
|
||||
<span class="shrink-0 text-body-main text-n-slate-11">
|
||||
{{ timestamp }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="!compact && showMenu">
|
||||
<Policy
|
||||
v-on-clickaway="() => toggleDropdown(false)"
|
||||
:permissions="['administrator']"
|
||||
class="relative flex items-center group"
|
||||
class="gap-2 flex items-center flex-shrink-0"
|
||||
>
|
||||
<Button
|
||||
icon="i-lucide-ellipsis-vertical"
|
||||
color="slate"
|
||||
size="xs"
|
||||
class="rounded-md group-hover:bg-n-alpha-2"
|
||||
@click="toggleDropdown()"
|
||||
icon="i-lucide-pencil-line"
|
||||
slate
|
||||
sm
|
||||
outline
|
||||
@click="handleAssistantAction('edit', 'edit')"
|
||||
/>
|
||||
<DropdownMenu
|
||||
v-if="showActionsDropdown"
|
||||
:menu-items="menuItems"
|
||||
class="mt-1 ltr:right-0 rtl:right-0 top-full"
|
||||
@action="handleAssistantAction($event)"
|
||||
<Button
|
||||
icon="i-lucide-trash"
|
||||
slate
|
||||
sm
|
||||
outline
|
||||
@click="handleAssistantAction('delete', 'delete')"
|
||||
/>
|
||||
</Policy>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-n-slate-11 text-sm line-clamp-5">
|
||||
<div class="flex justify-between items-center w-full gap-1">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div v-if="!compact" class="size-5 flex items-center">
|
||||
<Checkbox v-model="modelValue" />
|
||||
</div>
|
||||
<span class="text-n-slate-12 text-heading-3 line-clamp-1">
|
||||
{{ question }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="text-n-slate-11 text-body-main line-clamp-5"
|
||||
:class="{ 'ltr:pl-[1.625rem] rtl:pr-[1.625rem]': !compact }"
|
||||
>
|
||||
{{ answer }}
|
||||
</span>
|
||||
<div
|
||||
@@ -177,9 +190,7 @@ const handleDocumentableClick = () => {
|
||||
sm
|
||||
link
|
||||
class="hover:!no-underline"
|
||||
@click="
|
||||
handleAssistantAction({ action: 'approve', value: 'approve' })
|
||||
"
|
||||
@click="handleAssistantAction('approve', 'approve')"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('CAPTAIN.RESPONSES.OPTIONS.EDIT_RESPONSE')"
|
||||
@@ -188,12 +199,7 @@ const handleDocumentableClick = () => {
|
||||
slate
|
||||
link
|
||||
class="hover:!no-underline"
|
||||
@click="
|
||||
handleAssistantAction({
|
||||
action: 'edit',
|
||||
value: 'edit',
|
||||
})
|
||||
"
|
||||
@click="handleAssistantAction('edit', 'edit')"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('CAPTAIN.RESPONSES.OPTIONS.DELETE_RESPONSE')"
|
||||
@@ -202,78 +208,10 @@ const handleDocumentableClick = () => {
|
||||
ruby
|
||||
link
|
||||
class="hover:!no-underline"
|
||||
@click="
|
||||
handleAssistantAction({ action: 'delete', value: 'delete' })
|
||||
"
|
||||
@click="handleAssistantAction('delete', 'delete')"
|
||||
/>
|
||||
</div>
|
||||
</Policy>
|
||||
<div
|
||||
class="flex items-center gap-3"
|
||||
:class="{ 'justify-between w-full': !showActions }"
|
||||
>
|
||||
<div class="inline-flex items-center gap-3 min-w-0">
|
||||
<span
|
||||
v-if="status === 'approved'"
|
||||
class="text-sm shrink-0 truncate text-n-slate-11 inline-flex items-center gap-1"
|
||||
>
|
||||
<Icon icon="i-woot-captain" class="size-3.5" />
|
||||
{{ assistant?.name || '' }}
|
||||
</span>
|
||||
<div
|
||||
v-if="documentable"
|
||||
class="text-sm text-n-slate-11 grid grid-cols-[auto_1fr] items-center gap-1 min-w-0"
|
||||
>
|
||||
<Icon
|
||||
v-if="documentable.type === 'Captain::Document'"
|
||||
icon="i-ph-files-light"
|
||||
class="size-3.5"
|
||||
/>
|
||||
<Icon
|
||||
v-else-if="documentable.type === 'User'"
|
||||
icon="i-ph-user-circle-plus"
|
||||
class="size-3.5"
|
||||
/>
|
||||
<Icon
|
||||
v-else-if="documentable.type === 'Conversation'"
|
||||
icon="i-ph-chat-circle-dots"
|
||||
class="size-3.5"
|
||||
/>
|
||||
<span
|
||||
v-if="documentable.type === 'Captain::Document'"
|
||||
class="truncate"
|
||||
:title="documentable.name"
|
||||
>
|
||||
{{ documentable.name }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="documentable.type === 'User'"
|
||||
class="truncate"
|
||||
:title="documentable.available_name"
|
||||
>
|
||||
{{ documentable.available_name }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="documentable.type === 'Conversation'"
|
||||
class="hover:underline truncate cursor-pointer"
|
||||
role="button"
|
||||
@click="handleDocumentableClick"
|
||||
>
|
||||
{{
|
||||
t(`CAPTAIN.RESPONSES.DOCUMENTABLE.CONVERSATION`, {
|
||||
id: documentable.display_id,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="shrink-0 text-sm text-n-slate-11 line-clamp-1 inline-flex items-center gap-1"
|
||||
>
|
||||
<Icon icon="i-ph-calendar-dot" class="size-3.5" />
|
||||
{{ timestamp }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ defineExpose({ dialogRef });
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-3 min-h-48">
|
||||
<div v-else class="flex flex-col divide-y divide-n-weak min-h-48">
|
||||
<ResponseCard
|
||||
v-for="response in responses"
|
||||
:id="response.id"
|
||||
|
||||
+2
-1
@@ -60,7 +60,7 @@ const onClearFilters = () => {
|
||||
:show-backdrop="isApproved"
|
||||
>
|
||||
<template v-if="isApproved" #empty-state-item>
|
||||
<div class="grid grid-cols-1 gap-4 p-px overflow-hidden">
|
||||
<div class="grid grid-cols-1 divide-y divide-n-weak p-px overflow-hidden">
|
||||
<ResponseCard
|
||||
v-for="(response, index) in responsesList.slice(0, 5)"
|
||||
:id="response.id"
|
||||
@@ -68,6 +68,7 @@ const onClearFilters = () => {
|
||||
:question="replaceInstallationName(response.question)"
|
||||
:answer="replaceInstallationName(response.answer)"
|
||||
:status="response.status"
|
||||
:documentable="response.documentable"
|
||||
:assistant="response.assistant"
|
||||
:created-at="response.created_at"
|
||||
:updated-at="response.created_at"
|
||||
|
||||
+30
@@ -141,6 +141,11 @@ export const responsesList = [
|
||||
id: 87,
|
||||
question: 'Why is my Messenger in Chatwoot deactivated?',
|
||||
status: 'pending',
|
||||
documentable: {
|
||||
id: 1,
|
||||
type: 'Captain::Document',
|
||||
name: 'Document 1',
|
||||
},
|
||||
assistant: {
|
||||
account_id: 1,
|
||||
config: { product_name: 'Chatwoot' },
|
||||
@@ -157,6 +162,11 @@ export const responsesList = [
|
||||
created_at: 1736283340,
|
||||
id: 88,
|
||||
question: 'How do I integrate WhatsApp with Chatwoot?',
|
||||
documentable: {
|
||||
id: 1,
|
||||
type: 'User',
|
||||
available_name: 'John Doe',
|
||||
},
|
||||
assistant: {
|
||||
account_id: 2,
|
||||
config: { product_name: 'Chatwoot' },
|
||||
@@ -173,6 +183,11 @@ export const responsesList = [
|
||||
created_at: 1736283350,
|
||||
id: 89,
|
||||
question: 'How can I reset my password in Chatwoot?',
|
||||
documentable: {
|
||||
id: 1,
|
||||
type: 'Conversation',
|
||||
display_id: '12345',
|
||||
},
|
||||
assistant: {
|
||||
account_id: 3,
|
||||
config: { product_name: 'Chatwoot' },
|
||||
@@ -189,6 +204,11 @@ export const responsesList = [
|
||||
created_at: 1736283360,
|
||||
id: 90,
|
||||
question: 'How do I enable dark mode in Chatwoot?',
|
||||
documentable: {
|
||||
id: 1,
|
||||
type: 'Captain::Document',
|
||||
name: 'Document 1',
|
||||
},
|
||||
assistant: {
|
||||
account_id: 4,
|
||||
config: { product_name: 'Chatwoot' },
|
||||
@@ -205,6 +225,11 @@ export const responsesList = [
|
||||
created_at: 1736283370,
|
||||
id: 91,
|
||||
question: 'How do I add a new team member in Chatwoot?',
|
||||
documentable: {
|
||||
id: 1,
|
||||
type: 'User',
|
||||
available_name: 'John Doe',
|
||||
},
|
||||
assistant: {
|
||||
account_id: 5,
|
||||
config: { product_name: 'Chatwoot' },
|
||||
@@ -221,6 +246,11 @@ export const responsesList = [
|
||||
created_at: 1736283380,
|
||||
id: 92,
|
||||
question: 'What are campaigns in Chatwoot?',
|
||||
documentable: {
|
||||
id: 1,
|
||||
type: 'User',
|
||||
available_name: 'John Doe',
|
||||
},
|
||||
assistant: {
|
||||
account_id: 6,
|
||||
config: { product_name: 'Chatwoot' },
|
||||
|
||||
@@ -171,3 +171,30 @@ export const getInboxWarningIconClass = (type, reauthorizationRequired) => {
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export const getChannelTypeDisplayName = (type, medium) => {
|
||||
// Handle Twilio special case
|
||||
if (type === INBOX_TYPES.TWILIO) {
|
||||
return medium === TWILIO_CHANNEL_MEDIUM.WHATSAPP
|
||||
? 'Twilio WhatsApp'
|
||||
: 'Twilio SMS';
|
||||
}
|
||||
|
||||
// Map channel types to display names
|
||||
const channelTypeMap = {
|
||||
[INBOX_TYPES.WEB]: 'Website',
|
||||
[INBOX_TYPES.FB]: 'Messenger',
|
||||
[INBOX_TYPES.TWITTER]: 'Twitter',
|
||||
[INBOX_TYPES.WHATSAPP]: 'WhatsApp',
|
||||
[INBOX_TYPES.API]: 'API',
|
||||
[INBOX_TYPES.EMAIL]: 'Email',
|
||||
[INBOX_TYPES.TELEGRAM]: 'Telegram',
|
||||
[INBOX_TYPES.LINE]: 'Line',
|
||||
[INBOX_TYPES.SMS]: 'SMS',
|
||||
[INBOX_TYPES.INSTAGRAM]: 'Instagram',
|
||||
[INBOX_TYPES.TIKTOK]: 'TikTok',
|
||||
[INBOX_TYPES.VOICE]: 'Voice',
|
||||
};
|
||||
|
||||
return channelTypeMap[type] || 'Chat';
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
getInboxClassByType,
|
||||
getInboxIconByType,
|
||||
getInboxWarningIconClass,
|
||||
getChannelTypeDisplayName,
|
||||
} from '../inbox';
|
||||
|
||||
describe('#Inbox Helpers', () => {
|
||||
@@ -166,4 +167,86 @@ describe('#Inbox Helpers', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getChannelTypeDisplayName', () => {
|
||||
it('returns "Website" for web widget', () => {
|
||||
expect(getChannelTypeDisplayName(INBOX_TYPES.WEB)).toBe('Website');
|
||||
});
|
||||
|
||||
it('returns "Messenger" for Facebook', () => {
|
||||
expect(getChannelTypeDisplayName(INBOX_TYPES.FB)).toBe('Messenger');
|
||||
});
|
||||
|
||||
it('returns "Twitter" for Twitter', () => {
|
||||
expect(getChannelTypeDisplayName(INBOX_TYPES.TWITTER)).toBe('Twitter');
|
||||
});
|
||||
|
||||
it('returns "WhatsApp" for WhatsApp', () => {
|
||||
expect(getChannelTypeDisplayName(INBOX_TYPES.WHATSAPP)).toBe('WhatsApp');
|
||||
});
|
||||
|
||||
it('returns "API" for API', () => {
|
||||
expect(getChannelTypeDisplayName(INBOX_TYPES.API)).toBe('API');
|
||||
});
|
||||
|
||||
it('returns "Email" for Email', () => {
|
||||
expect(getChannelTypeDisplayName(INBOX_TYPES.EMAIL)).toBe('Email');
|
||||
});
|
||||
|
||||
it('returns "Telegram" for Telegram', () => {
|
||||
expect(getChannelTypeDisplayName(INBOX_TYPES.TELEGRAM)).toBe('Telegram');
|
||||
});
|
||||
|
||||
it('returns "Line" for Line', () => {
|
||||
expect(getChannelTypeDisplayName(INBOX_TYPES.LINE)).toBe('Line');
|
||||
});
|
||||
|
||||
it('returns "SMS" for SMS', () => {
|
||||
expect(getChannelTypeDisplayName(INBOX_TYPES.SMS)).toBe('SMS');
|
||||
});
|
||||
|
||||
it('returns "Instagram" for Instagram', () => {
|
||||
expect(getChannelTypeDisplayName(INBOX_TYPES.INSTAGRAM)).toBe(
|
||||
'Instagram'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns "TikTok" for TikTok', () => {
|
||||
expect(getChannelTypeDisplayName(INBOX_TYPES.TIKTOK)).toBe('TikTok');
|
||||
});
|
||||
|
||||
it('returns "Voice" for Voice', () => {
|
||||
expect(getChannelTypeDisplayName(INBOX_TYPES.VOICE)).toBe('Voice');
|
||||
});
|
||||
|
||||
it('returns "Chat" for unknown type', () => {
|
||||
expect(getChannelTypeDisplayName('UNKNOWN_TYPE')).toBe('Chat');
|
||||
});
|
||||
|
||||
describe('Twilio cases', () => {
|
||||
it('returns "Twilio WhatsApp" for Twilio with whatsapp medium', () => {
|
||||
expect(getChannelTypeDisplayName(INBOX_TYPES.TWILIO, 'whatsapp')).toBe(
|
||||
'Twilio WhatsApp'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns "Twilio SMS" for Twilio with sms medium', () => {
|
||||
expect(getChannelTypeDisplayName(INBOX_TYPES.TWILIO, 'sms')).toBe(
|
||||
'Twilio SMS'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns "Twilio SMS" for Twilio with undefined medium', () => {
|
||||
expect(getChannelTypeDisplayName(INBOX_TYPES.TWILIO, undefined)).toBe(
|
||||
'Twilio SMS'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns "Twilio SMS" for Twilio with empty medium', () => {
|
||||
expect(getChannelTypeDisplayName(INBOX_TYPES.TWILIO, '')).toBe(
|
||||
'Twilio SMS'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -867,6 +867,7 @@
|
||||
"SEARCH_PLACEHOLDER": "Search FAQs...",
|
||||
"BULK_APPROVE_BUTTON": "Approve",
|
||||
"BULK_DELETE_BUTTON": "Delete",
|
||||
"CLEAR_SELECTION": "Clear selection",
|
||||
"BULK_APPROVE": {
|
||||
"SUCCESS_MESSAGE": "FAQs approved successfully",
|
||||
"ERROR_MESSAGE": "There was an error approving the FAQs, please try again."
|
||||
|
||||
@@ -212,7 +212,7 @@ const addAllExample = () => {
|
||||
</template>
|
||||
</SuggestedRules>
|
||||
</div>
|
||||
<div class="flex mt-7 flex-col gap-4">
|
||||
<div class="flex mt-7 flex-col gap-4 pb-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<BulkSelectBar
|
||||
v-model="bulkSelectedIds"
|
||||
|
||||
@@ -221,7 +221,7 @@ const addAllExample = async () => {
|
||||
</template>
|
||||
</SuggestedRules>
|
||||
</div>
|
||||
<div class="flex mt-7 flex-col gap-4">
|
||||
<div class="flex mt-7 flex-col gap-4 pb-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<BulkSelectBar
|
||||
v-model="bulkSelectedIds"
|
||||
|
||||
@@ -73,7 +73,7 @@ onMounted(() =>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-4 pb-4">
|
||||
<InboxCard
|
||||
v-for="captainInbox in captainInboxes"
|
||||
:id="captainInbox.id"
|
||||
|
||||
@@ -277,7 +277,7 @@ onMounted(() => {
|
||||
{{ t('CAPTAIN.ASSISTANTS.SCENARIOS.SEARCH_EMPTY_MESSAGE') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<div v-else class="flex flex-col gap-2 pb-4">
|
||||
<ScenariosCard
|
||||
v-for="scenario in filteredScenarios"
|
||||
:id="scenario.id"
|
||||
|
||||
@@ -129,7 +129,7 @@ onMounted(() => {
|
||||
<template #body>
|
||||
<LimitBanner class="mb-5" />
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-4 pb-4">
|
||||
<DocumentCard
|
||||
v-for="doc in documents"
|
||||
:id="doc.id"
|
||||
|
||||
@@ -7,6 +7,7 @@ import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Banner from 'dashboard/components-next/banner/Banner.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import BulkSelectBar from 'dashboard/components-next/captain/assistant/BulkSelectBar.vue';
|
||||
@@ -113,7 +114,6 @@ const fetchResponses = (page = 1) => {
|
||||
|
||||
// Bulk action
|
||||
const bulkSelectedIds = ref(new Set());
|
||||
const hoveredCard = ref(null);
|
||||
|
||||
const buildSelectedCountLabel = computed(() => {
|
||||
const count = responses.value?.length || 0;
|
||||
@@ -129,10 +129,6 @@ const selectedCountLabel = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -157,6 +153,10 @@ const fetchResponseAfterBulkAction = () => {
|
||||
bulkSelectedIds.value = new Set();
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
bulkSelectedIds.value = new Set();
|
||||
};
|
||||
|
||||
const onPageChange = page => {
|
||||
const hadSelection = bulkSelectedIds.value.size > 0;
|
||||
|
||||
@@ -252,12 +252,23 @@ onMounted(() => {
|
||||
:select-all-label="buildSelectedCountLabel"
|
||||
:selected-count-label="selectedCountLabel"
|
||||
:delete-label="$t('CAPTAIN.RESPONSES.BULK_DELETE_BUTTON')"
|
||||
class="w-fit"
|
||||
:class="{
|
||||
'mb-2': bulkSelectedIds.size > 0,
|
||||
'mt-2': bulkSelectedIds.size > 0,
|
||||
}"
|
||||
class="justify-between"
|
||||
animation-direction="vertical"
|
||||
@bulk-delete="bulkDeleteDialog.dialogRef.open()"
|
||||
/>
|
||||
>
|
||||
<template #secondary-actions>
|
||||
<Button
|
||||
sm
|
||||
ghost
|
||||
:label="t('CAPTAIN.RESPONSES.CLEAR_SELECTION')"
|
||||
class="!px-1"
|
||||
@click="clearSelection"
|
||||
/>
|
||||
</template>
|
||||
</BulkSelectBar>
|
||||
</template>
|
||||
|
||||
<template #emptyState>
|
||||
@@ -271,16 +282,16 @@ onMounted(() => {
|
||||
<template #body>
|
||||
<LimitBanner class="mb-5" />
|
||||
<Banner
|
||||
v-if="pendingCount > 0"
|
||||
v-if="pendingCount > 0 && bulkSelectedIds.size === 0"
|
||||
color="blue"
|
||||
class="mb-4 -mt-3"
|
||||
class="mb-4 -mt-2"
|
||||
:action-label="$t('CAPTAIN.RESPONSES.PENDING_BANNER.ACTION')"
|
||||
@action="navigateToPendingFAQs"
|
||||
>
|
||||
{{ $t('CAPTAIN.RESPONSES.PENDING_BANNER.TITLE') }}
|
||||
</Banner>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col divide-y divide-n-weak">
|
||||
<ResponseCard
|
||||
v-for="response in responses"
|
||||
:id="response.id"
|
||||
@@ -293,13 +304,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)"
|
||||
:show-actions="false"
|
||||
@action="handleAction"
|
||||
@navigate="handleNavigationAction"
|
||||
@select="handleCardSelect"
|
||||
@hover="isHovered => handleCardHover(isHovered, response.id)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -140,7 +140,6 @@ const fetchResponses = (page = 1) => {
|
||||
|
||||
// Bulk action
|
||||
const bulkSelectedIds = ref(new Set());
|
||||
const hoveredCard = ref(null);
|
||||
|
||||
const buildSelectedCountLabel = computed(() => {
|
||||
const count = filteredResponses.value?.length || 0;
|
||||
@@ -156,10 +155,6 @@ const selectedCountLabel = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -196,6 +191,10 @@ const handleBulkApprove = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
bulkSelectedIds.value = new Set();
|
||||
};
|
||||
|
||||
const onPageChange = page => {
|
||||
const hadSelection = bulkSelectedIds.value.size > 0;
|
||||
|
||||
@@ -291,10 +290,11 @@ onMounted(() => {
|
||||
:select-all-label="buildSelectedCountLabel"
|
||||
:selected-count-label="selectedCountLabel"
|
||||
:delete-label="$t('CAPTAIN.RESPONSES.BULK_DELETE_BUTTON')"
|
||||
class="w-fit"
|
||||
:class="{
|
||||
'mb-2': bulkSelectedIds.size > 0,
|
||||
}"
|
||||
class="justify-between"
|
||||
animation-direction="vertical"
|
||||
@bulk-delete="bulkDeleteDialog.dialogRef.open()"
|
||||
>
|
||||
<template #secondary-actions>
|
||||
@@ -306,6 +306,14 @@ onMounted(() => {
|
||||
class="!px-1.5"
|
||||
@click="handleBulkApprove"
|
||||
/>
|
||||
<div class="h-4 w-px bg-n-strong" />
|
||||
<Button
|
||||
sm
|
||||
ghost
|
||||
:label="$t('CAPTAIN.RESPONSES.CLEAR_SELECTION')"
|
||||
class="!px-1"
|
||||
@click="clearSelection"
|
||||
/>
|
||||
</template>
|
||||
</BulkSelectBar>
|
||||
</template>
|
||||
@@ -325,7 +333,7 @@ onMounted(() => {
|
||||
<template #body>
|
||||
<LimitBanner class="mb-5" />
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col divide-y divide-n-weak">
|
||||
<ResponseCard
|
||||
v-for="response in filteredResponses"
|
||||
:id="response.id"
|
||||
@@ -338,13 +346,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="false"
|
||||
:show-actions="!bulkSelectedIds.has(response.id)"
|
||||
@action="handleAction"
|
||||
@navigate="handleNavigationAction"
|
||||
@select="handleCardSelect"
|
||||
@hover="isHovered => handleCardHover(isHovered, response.id)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -100,7 +100,7 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-4 pb-4">
|
||||
<CustomToolCard
|
||||
v-for="tool in customTools"
|
||||
:id="tool.id"
|
||||
|
||||
+3
-2
@@ -88,7 +88,8 @@ const handleAssignLabels = labels => {
|
||||
:all-items="allItems"
|
||||
:select-all-label="selectAllLabel"
|
||||
:selected-count-label="selectedCountLabel"
|
||||
class="py-2 ltr:!pr-2 rtl:!pl-2 justify-between"
|
||||
animation-direction="vertical"
|
||||
class="justify-between"
|
||||
>
|
||||
<template #secondary-actions>
|
||||
<Button
|
||||
@@ -105,7 +106,7 @@ const handleAssignLabels = labels => {
|
||||
type="contact"
|
||||
:is-loading="isLoading"
|
||||
:disabled="!selectedCount"
|
||||
class="[&>button]:!text-n-blue-11 [&>button]:!px-2"
|
||||
class="[&>button]:!text-n-blue-11 [&>button>span]:!text-n-blue-11 [&>button]:!px-2"
|
||||
@assign="handleAssignLabels"
|
||||
/>
|
||||
<div class="w-px h-3 bg-n-weak rounded-lg" />
|
||||
|
||||
@@ -438,7 +438,7 @@ onMounted(async () => {
|
||||
|
||||
<template v-else>
|
||||
<ContactsBulkActionBar
|
||||
v-if="hasSelection"
|
||||
v-show="hasSelection"
|
||||
:visible-contact-ids="visibleContactIds"
|
||||
:selected-contact-ids="selectedContactIds"
|
||||
:is-loading="isBulkActionLoading"
|
||||
|
||||
Reference in New Issue
Block a user