Merge branch 'develop' into feat/github-integration
This commit is contained in:
@@ -8,6 +8,7 @@ import UpgradePage from 'dashboard/routes/dashboard/upgrade/UpgradePage.vue';
|
||||
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useWindowSize } from '@vueuse/core';
|
||||
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
@@ -18,6 +19,8 @@ const CommandBar = defineAsyncComponent(
|
||||
import CopilotLauncher from 'dashboard/components-next/copilot/CopilotLauncher.vue';
|
||||
import CopilotContainer from 'dashboard/components/copilot/CopilotContainer.vue';
|
||||
|
||||
import MobileSidebarLauncher from 'dashboard/components-next/sidebar/MobileSidebarLauncher.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextSidebar,
|
||||
@@ -27,17 +30,20 @@ export default {
|
||||
UpgradePage,
|
||||
CopilotLauncher,
|
||||
CopilotContainer,
|
||||
MobileSidebarLauncher,
|
||||
},
|
||||
setup() {
|
||||
const upgradePageRef = ref(null);
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
const { accountId } = useAccount();
|
||||
const { width: windowWidth } = useWindowSize();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
accountId,
|
||||
upgradePageRef,
|
||||
windowWidth,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
@@ -45,10 +51,13 @@ export default {
|
||||
showAccountModal: false,
|
||||
showCreateAccountModal: false,
|
||||
showShortcutModal: false,
|
||||
displayLayoutType: '',
|
||||
isMobileSidebarOpen: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isSmallScreen() {
|
||||
return this.windowWidth < wootConstants.SMALL_SCREEN_BREAKPOINT;
|
||||
},
|
||||
showUpgradePage() {
|
||||
return this.upgradePageRef?.shouldShowUpgradePage;
|
||||
},
|
||||
@@ -66,54 +75,30 @@ export default {
|
||||
} = this.uiSettings;
|
||||
return conversationDisplayType;
|
||||
},
|
||||
previouslyUsedSidebarView() {
|
||||
const { previously_used_sidebar_view: showSecondarySidebar } =
|
||||
this.uiSettings;
|
||||
return showSecondarySidebar;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
displayLayoutType() {
|
||||
const { LAYOUT_TYPES } = wootConstants;
|
||||
this.updateUISettings({
|
||||
conversation_display_type:
|
||||
this.displayLayoutType === LAYOUT_TYPES.EXPANDED
|
||||
? LAYOUT_TYPES.EXPANDED
|
||||
: this.previouslyUsedDisplayType,
|
||||
show_secondary_sidebar:
|
||||
this.displayLayoutType === LAYOUT_TYPES.EXPANDED
|
||||
? false
|
||||
: this.previouslyUsedSidebarView,
|
||||
});
|
||||
isSmallScreen: {
|
||||
handler() {
|
||||
const { LAYOUT_TYPES } = wootConstants;
|
||||
if (window.innerWidth <= wootConstants.SMALL_SCREEN_BREAKPOINT) {
|
||||
this.updateUISettings({
|
||||
conversation_display_type: LAYOUT_TYPES.EXPANDED,
|
||||
});
|
||||
} else {
|
||||
this.updateUISettings({
|
||||
conversation_display_type: this.previouslyUsedDisplayType,
|
||||
});
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.handleResize();
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
unmounted() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleResize() {
|
||||
const { SMALL_SCREEN_BREAKPOINT, LAYOUT_TYPES } = wootConstants;
|
||||
let throttled = false;
|
||||
const delay = 150;
|
||||
|
||||
if (throttled) {
|
||||
return;
|
||||
}
|
||||
throttled = true;
|
||||
|
||||
setTimeout(() => {
|
||||
throttled = false;
|
||||
if (window.innerWidth <= SMALL_SCREEN_BREAKPOINT) {
|
||||
this.displayLayoutType = LAYOUT_TYPES.EXPANDED;
|
||||
} else {
|
||||
this.displayLayoutType = LAYOUT_TYPES.CONDENSED;
|
||||
}
|
||||
}, delay);
|
||||
toggleMobileSidebar() {
|
||||
this.isMobileSidebarOpen = !this.isMobileSidebarOpen;
|
||||
},
|
||||
closeMobileSidebar() {
|
||||
this.isMobileSidebarOpen = false;
|
||||
},
|
||||
openCreateAccountModal() {
|
||||
this.showAccountModal = false;
|
||||
@@ -136,23 +121,35 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap app-wrapper text-n-slate-12">
|
||||
<div class="flex flex-grow overflow-hidden text-n-slate-12">
|
||||
<NextSidebar
|
||||
:is-mobile-sidebar-open="isMobileSidebarOpen"
|
||||
@toggle-account-modal="toggleAccountModal"
|
||||
@open-key-shortcut-modal="toggleKeyShortcutModal"
|
||||
@close-key-shortcut-modal="closeKeyShortcutModal"
|
||||
@show-create-account-modal="openCreateAccountModal"
|
||||
@close-mobile-sidebar="closeMobileSidebar"
|
||||
/>
|
||||
<main class="flex flex-1 h-full min-h-0 px-0 overflow-hidden">
|
||||
|
||||
<main class="flex flex-1 h-full w-full min-h-0 px-0 overflow-hidden">
|
||||
<UpgradePage
|
||||
v-show="showUpgradePage"
|
||||
ref="upgradePageRef"
|
||||
:bypass-upgrade-page="bypassUpgradePage"
|
||||
/>
|
||||
>
|
||||
<MobileSidebarLauncher
|
||||
:is-mobile-sidebar-open="isMobileSidebarOpen"
|
||||
@toggle="toggleMobileSidebar"
|
||||
/>
|
||||
</UpgradePage>
|
||||
<template v-if="!showUpgradePage">
|
||||
<router-view />
|
||||
<CommandBar />
|
||||
<CopilotLauncher />
|
||||
<MobileSidebarLauncher
|
||||
:is-mobile-sidebar-open="isMobileSidebarOpen"
|
||||
@toggle="toggleMobileSidebar"
|
||||
/>
|
||||
<CopilotContainer />
|
||||
</template>
|
||||
<AddAccountModal
|
||||
|
||||
@@ -266,6 +266,11 @@ const addAllExample = () => {
|
||||
{{ t('CAPTAIN.ASSISTANTS.GUARDRAILS.EMPTY_MESSAGE') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else-if="filteredGuardrails.length === 0" class="mt-1 mb-2">
|
||||
<span class="text-n-slate-11 text-sm">
|
||||
{{ t('CAPTAIN.ASSISTANTS.GUARDRAILS.SEARCH_EMPTY_MESSAGE') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<RuleCard
|
||||
v-for="guardrail in filteredGuardrails"
|
||||
|
||||
@@ -284,6 +284,13 @@ const addAllExample = async () => {
|
||||
{{ t('CAPTAIN.ASSISTANTS.RESPONSE_GUIDELINES.EMPTY_MESSAGE') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else-if="filteredGuidelines.length === 0" class="mt-1 mb-2">
|
||||
<span class="text-n-slate-11 text-sm">
|
||||
{{
|
||||
t('CAPTAIN.ASSISTANTS.RESPONSE_GUIDELINES.SEARCH_EMPTY_MESSAGE')
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<RuleCard
|
||||
v-for="guideline in filteredGuidelines"
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
<script setup>
|
||||
import { computed, h, ref, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
|
||||
import SettingsPageLayout from 'dashboard/components-next/captain/SettingsPageLayout.vue';
|
||||
import SettingsHeader from 'dashboard/components-next/captain/pageComponents/settings/SettingsHeader.vue';
|
||||
import SuggestedScenarios from 'dashboard/components-next/captain/assistant/SuggestedRules.vue';
|
||||
import ScenariosCard from 'dashboard/components-next/captain/assistant/ScenariosCard.vue';
|
||||
import BulkSelectBar from 'dashboard/components-next/captain/assistant/BulkSelectBar.vue';
|
||||
import AddNewScenariosDialog from 'dashboard/components-next/captain/assistant/AddNewScenariosDialog.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
const assistantId = route.params.assistantId;
|
||||
|
||||
const uiFlags = useMapGetter('captainScenarios/getUIFlags');
|
||||
const isFetching = computed(() => uiFlags.value.fetchingList);
|
||||
const assistant = computed(() =>
|
||||
store.getters['captainAssistants/getRecord'](Number(assistantId))
|
||||
);
|
||||
const scenarios = useMapGetter('captainScenarios/getRecords');
|
||||
|
||||
const searchQuery = ref('');
|
||||
|
||||
const breadcrumbItems = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: t('CAPTAIN.ASSISTANTS.SETTINGS.BREADCRUMB.ASSISTANT'),
|
||||
routeName: 'captain_assistants_index',
|
||||
},
|
||||
{ label: assistant.value?.name, routeName: 'captain_assistants_edit' },
|
||||
{ label: t('CAPTAIN.ASSISTANTS.SCENARIOS.BREADCRUMB.TITLE') },
|
||||
];
|
||||
});
|
||||
|
||||
const TOOL_LINK_REGEX = /\[([^\]]+)]\(tool:\/\/.+?\)/g;
|
||||
|
||||
const renderInstruction = instruction => () =>
|
||||
h('span', {
|
||||
class: 'text-sm text-n-slate-12 py-4',
|
||||
innerHTML: instruction.replace(
|
||||
TOOL_LINK_REGEX,
|
||||
(_, title) =>
|
||||
`<span class="text-n-iris-11 font-medium">@${title.replace(/^@/, '')}</span>`
|
||||
),
|
||||
});
|
||||
|
||||
// Suggested example scenarios for quick add
|
||||
const scenariosExample = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Refund Order',
|
||||
description: 'User encountered a technical issue or error message.',
|
||||
instruction:
|
||||
'Ask for steps to reproduce + browser/app version. Use [Known Issues](tool://known_issues) to check if it’s a known bug. File with [Create Bug Report](tool://bug_report_create) if new.',
|
||||
tools: ['create_bug_report', 'known_issues'],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Product Recommendation',
|
||||
description: 'User is unsure which product or service to choose.',
|
||||
instruction:
|
||||
'Ask 2–3 clarifying questions. Use [Product Match](tool://product_match[user_needs]) and suggest 2–3 options with pros/cons. Link to compare page if available.',
|
||||
tools: ['product_match[user_needs]'],
|
||||
},
|
||||
];
|
||||
|
||||
const filteredScenarios = computed(() => {
|
||||
const query = searchQuery.value.trim();
|
||||
const source = scenarios.value;
|
||||
if (!query) return source;
|
||||
return picoSearch(source, query, ['title', 'description', 'instruction']);
|
||||
});
|
||||
|
||||
const shouldShowSuggestedRules = computed(() => {
|
||||
return uiSettings.value?.show_scenarios_suggestions !== false;
|
||||
});
|
||||
|
||||
const closeSuggestedRules = () => {
|
||||
updateUISettings({ show_scenarios_suggestions: false });
|
||||
};
|
||||
|
||||
// Bulk selection & hover state
|
||||
const bulkSelectedIds = ref(new Set());
|
||||
const hoveredCard = ref(null);
|
||||
|
||||
const handleRuleSelect = id => {
|
||||
const selected = new Set(bulkSelectedIds.value);
|
||||
selected[selected.has(id) ? 'delete' : 'add'](id);
|
||||
bulkSelectedIds.value = selected;
|
||||
};
|
||||
|
||||
const buildSelectedCountLabel = computed(() => {
|
||||
const count = scenarios.value.length || 0;
|
||||
const isAllSelected = bulkSelectedIds.value.size === count && count > 0;
|
||||
return isAllSelected
|
||||
? t('CAPTAIN.ASSISTANTS.SCENARIOS.BULK_ACTION.UNSELECT_ALL', { count })
|
||||
: t('CAPTAIN.ASSISTANTS.SCENARIOS.BULK_ACTION.SELECT_ALL', { count });
|
||||
});
|
||||
|
||||
const selectedCountLabel = computed(() => {
|
||||
return t('CAPTAIN.ASSISTANTS.SCENARIOS.BULK_ACTION.SELECTED', {
|
||||
count: bulkSelectedIds.value.size,
|
||||
});
|
||||
});
|
||||
|
||||
const handleRuleHover = (isHovered, id) => {
|
||||
hoveredCard.value = isHovered ? id : null;
|
||||
};
|
||||
|
||||
const getToolsFromInstruction = instruction => [
|
||||
...new Set(
|
||||
[...(instruction?.matchAll(/\(tool:\/\/([^)]+)\)/g) ?? [])].map(m => m[1])
|
||||
),
|
||||
];
|
||||
|
||||
const updateScenario = async scenario => {
|
||||
try {
|
||||
await store.dispatch('captainScenarios/update', {
|
||||
id: scenario.id,
|
||||
assistantId: route.params.assistantId,
|
||||
...scenario,
|
||||
tools: getToolsFromInstruction(scenario.instruction),
|
||||
});
|
||||
useAlert(t('CAPTAIN.ASSISTANTS.SCENARIOS.API.UPDATE.SUCCESS'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.message ||
|
||||
t('CAPTAIN.ASSISTANTS.SCENARIOS.API.UPDATE.ERROR');
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteScenario = async id => {
|
||||
try {
|
||||
await store.dispatch('captainScenarios/delete', {
|
||||
id,
|
||||
assistantId: route.params.assistantId,
|
||||
});
|
||||
useAlert(t('CAPTAIN.ASSISTANTS.SCENARIOS.API.DELETE.SUCCESS'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.message ||
|
||||
t('CAPTAIN.ASSISTANTS.SCENARIOS.API.DELETE.ERROR');
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Add bulk delete endpoint
|
||||
const bulkDeleteScenarios = async ids => {
|
||||
const idsArray = ids || Array.from(bulkSelectedIds.value);
|
||||
await Promise.all(
|
||||
idsArray.map(id =>
|
||||
store.dispatch('captainScenarios/delete', {
|
||||
id,
|
||||
assistantId: route.params.assistantId,
|
||||
})
|
||||
)
|
||||
);
|
||||
bulkSelectedIds.value = new Set();
|
||||
useAlert(t('CAPTAIN.ASSISTANTS.SCENARIOS.API.DELETE.SUCCESS'));
|
||||
};
|
||||
|
||||
const addScenario = async scenario => {
|
||||
try {
|
||||
await store.dispatch('captainScenarios/create', {
|
||||
assistantId: route.params.assistantId,
|
||||
...scenario,
|
||||
tools: getToolsFromInstruction(scenario.instruction),
|
||||
});
|
||||
useAlert(t('CAPTAIN.ASSISTANTS.SCENARIOS.API.ADD.SUCCESS'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.message ||
|
||||
t('CAPTAIN.ASSISTANTS.SCENARIOS.API.ADD.ERROR');
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const addAllExampleScenarios = async () => {
|
||||
try {
|
||||
scenariosExample.forEach(async scenario => {
|
||||
await store.dispatch('captainScenarios/create', {
|
||||
assistantId: route.params.assistantId,
|
||||
...scenario,
|
||||
});
|
||||
});
|
||||
useAlert(t('CAPTAIN.ASSISTANTS.SCENARIOS.API.ADD.SUCCESS'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.message ||
|
||||
t('CAPTAIN.ASSISTANTS.SCENARIOS.API.ADD.ERROR');
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('captainScenarios/get', {
|
||||
assistantId: assistantId,
|
||||
});
|
||||
store.dispatch('captainTools/getTools');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsPageLayout
|
||||
:breadcrumb-items="breadcrumbItems"
|
||||
:is-fetching="isFetching"
|
||||
>
|
||||
<template #body>
|
||||
<SettingsHeader
|
||||
:heading="$t('CAPTAIN.ASSISTANTS.SCENARIOS.TITLE')"
|
||||
:description="$t('CAPTAIN.ASSISTANTS.SCENARIOS.DESCRIPTION')"
|
||||
/>
|
||||
<div v-if="shouldShowSuggestedRules" class="flex mt-7 flex-col gap-4">
|
||||
<SuggestedScenarios
|
||||
:title="$t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.SUGGESTED.TITLE')"
|
||||
:items="scenariosExample"
|
||||
@close="closeSuggestedRules"
|
||||
@add="addAllExampleScenarios"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<div class="flex items-center gap-3 justify-between">
|
||||
<span class="text-sm text-n-slate-12">
|
||||
{{ item.title }}
|
||||
</span>
|
||||
<Button
|
||||
:label="
|
||||
$t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.SUGGESTED.ADD_SINGLE')
|
||||
"
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
class="!text-sm !text-n-slate-11 flex-shrink-0"
|
||||
@click="addScenario(item)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm text-n-slate-11 mt-2">
|
||||
{{ item.description }}
|
||||
</span>
|
||||
<component :is="renderInstruction(item.instruction)" />
|
||||
<span class="text-sm text-n-slate-11 font-medium mb-1">
|
||||
{{ t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.SUGGESTED.TOOLS_USED') }}
|
||||
{{ item.tools?.map(tool => `@${tool}`).join(', ') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</SuggestedScenarios>
|
||||
</div>
|
||||
<div class="flex mt-7 flex-col gap-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<BulkSelectBar
|
||||
v-model="bulkSelectedIds"
|
||||
:all-items="scenarios"
|
||||
:select-all-label="buildSelectedCountLabel"
|
||||
:selected-count-label="selectedCountLabel"
|
||||
:delete-label="
|
||||
$t('CAPTAIN.ASSISTANTS.SCENARIOS.BULK_ACTION.BULK_DELETE_BUTTON')
|
||||
"
|
||||
@bulk-delete="bulkDeleteScenarios"
|
||||
>
|
||||
<template #default-actions>
|
||||
<AddNewScenariosDialog @add="addScenario" />
|
||||
</template>
|
||||
</BulkSelectBar>
|
||||
<div
|
||||
v-if="scenarios.length && bulkSelectedIds.size === 0"
|
||||
class="max-w-[22.5rem] w-full min-w-0"
|
||||
>
|
||||
<Input
|
||||
v-model="searchQuery"
|
||||
:placeholder="
|
||||
t('CAPTAIN.ASSISTANTS.SCENARIOS.LIST.SEARCH_PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="scenarios.length === 0" class="mt-1 mb-2">
|
||||
<span class="text-n-slate-11 text-sm">
|
||||
{{ t('CAPTAIN.ASSISTANTS.SCENARIOS.EMPTY_MESSAGE') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else-if="filteredScenarios.length === 0" class="mt-1 mb-2">
|
||||
<span class="text-n-slate-11 text-sm">
|
||||
{{ t('CAPTAIN.ASSISTANTS.SCENARIOS.SEARCH_EMPTY_MESSAGE') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<ScenariosCard
|
||||
v-for="scenario in filteredScenarios"
|
||||
:id="scenario.id"
|
||||
:key="scenario.id"
|
||||
:title="scenario.title"
|
||||
:description="scenario.description"
|
||||
:instruction="scenario.instruction"
|
||||
:tools="scenario.tools"
|
||||
:is-selected="bulkSelectedIds.has(scenario.id)"
|
||||
:selectable="
|
||||
hoveredCard === scenario.id || bulkSelectedIds.size > 0
|
||||
"
|
||||
@select="handleRuleSelect"
|
||||
@delete="deleteScenario(scenario.id)"
|
||||
@update="updateScenario"
|
||||
@hover="isHovered => handleRuleHover(isHovered, scenario.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</SettingsPageLayout>
|
||||
</template>
|
||||
@@ -41,7 +41,7 @@ const controlItems = computed(() => {
|
||||
description: t(
|
||||
'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.SCENARIOS.DESCRIPTION'
|
||||
),
|
||||
// routeName: 'captain_assistants_scenarios_index',
|
||||
routeName: 'captain_assistants_scenarios_index',
|
||||
},
|
||||
{
|
||||
name: t(
|
||||
|
||||
@@ -7,6 +7,7 @@ import AssistantEdit from './assistants/Edit.vue';
|
||||
import AssistantInboxesIndex from './assistants/inboxes/Index.vue';
|
||||
import AssistantGuardrailsIndex from './assistants/guardrails/Index.vue';
|
||||
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';
|
||||
|
||||
@@ -67,6 +68,21 @@ export const routes = [
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: frontendURL(
|
||||
'accounts/:accountId/captain/assistants/:assistantId/scenarios'
|
||||
),
|
||||
component: AssistantScenariosIndex,
|
||||
name: 'captain_assistants_scenarios_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
||||
installationTypes: [
|
||||
INSTALLATION_TYPES.CLOUD,
|
||||
INSTALLATION_TYPES.ENTERPRISE,
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: frontendURL(
|
||||
'accounts/:accountId/captain/assistants/:assistantId/guidelines'
|
||||
|
||||
@@ -31,7 +31,7 @@ export default {
|
||||
slate
|
||||
xs
|
||||
faded
|
||||
class="flex-shrink-0 rtl:rotate-180 ltr:rotate-0"
|
||||
class="flex-shrink-0 rtl:rotate-180 ltr:rotate-0 md:inline-flex hidden"
|
||||
@click="toggle"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -68,7 +68,7 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col gap-12 sm:gap-16 items-center justify-center py-0 px-4 md:px-0 w-full min-h-screen max-w-full overflow-auto bg-n-background"
|
||||
class="flex flex-col gap-12 sm:gap-16 items-center justify-center py-0 px-4 w-full min-h-screen max-w-full overflow-auto bg-n-background"
|
||||
>
|
||||
<div class="flex flex-col justify-start sm:justify-center gap-6">
|
||||
<div class="flex flex-col gap-1.5 items-start sm:items-center">
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-grow-0 w-full h-full min-h-0 app-wrapper">
|
||||
<div class="flex w-full h-full min-h-0">
|
||||
<section
|
||||
v-if="isHelpCenterEnabled"
|
||||
class="flex flex-1 h-full px-0 overflow-hidden bg-n-background"
|
||||
|
||||
+43
-1
@@ -4,12 +4,16 @@ import { useRoute, useRouter } from 'vue-router';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import PortalSettings from 'dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalSettings.vue';
|
||||
|
||||
const SSL_STATUS_FETCH_INTERVAL = 5000;
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { isOnChatwootCloud } = useAccount();
|
||||
|
||||
const { updateUISettings } = useUISettings();
|
||||
|
||||
@@ -24,6 +28,15 @@ const getDefaultLocale = slug => {
|
||||
return getPortalBySlug.value(slug)?.meta?.default_locale;
|
||||
};
|
||||
|
||||
const fetchSSLStatus = () => {
|
||||
if (!isOnChatwootCloud.value) return;
|
||||
|
||||
const { portalSlug } = route.params;
|
||||
store.dispatch('portals/sslStatus', {
|
||||
portalSlug,
|
||||
});
|
||||
};
|
||||
|
||||
const fetchPortalAndItsCategories = async (slug, locale) => {
|
||||
const selectedPortalParam = { portalSlug: slug, locale };
|
||||
await Promise.all([
|
||||
@@ -106,8 +119,35 @@ const deletePortal = async selectedPortalForDelete => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendCnameInstructions = async payload => {
|
||||
try {
|
||||
await store.dispatch('portals/sendCnameInstructions', payload);
|
||||
useAlert(
|
||||
t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.SEND_CNAME_INSTRUCTIONS.API.SUCCESS_MESSAGE'
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error?.message ||
|
||||
t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.SEND_CNAME_INSTRUCTIONS.API.ERROR_MESSAGE'
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdatePortal = updatePortalSettings;
|
||||
const handleUpdatePortalConfiguration = updatePortalSettings;
|
||||
const handleUpdatePortalConfiguration = portalObj => {
|
||||
updatePortalSettings(portalObj);
|
||||
|
||||
// If custom domain is added or updated, fetch SSL status after a delay of 5 seconds (only on Chatwoot cloud)
|
||||
if (portalObj?.custom_domain && isOnChatwootCloud.value) {
|
||||
setTimeout(() => {
|
||||
fetchSSLStatus();
|
||||
}, SSL_STATUS_FETCH_INTERVAL);
|
||||
}
|
||||
};
|
||||
const handleDeletePortal = deletePortal;
|
||||
</script>
|
||||
|
||||
@@ -118,5 +158,7 @@ const handleDeletePortal = deletePortal;
|
||||
@update-portal="handleUpdatePortal"
|
||||
@update-portal-configuration="handleUpdatePortalConfiguration"
|
||||
@delete-portal="handleDeletePortal"
|
||||
@refresh-status="fetchSSLStatus"
|
||||
@send-cname-instructions="handleSendCnameInstructions"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,221 +1,236 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
<script setup>
|
||||
import { computed, ref, watch, onMounted, nextTick } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
|
||||
import InboxCard from 'dashboard/components-next/Inbox/InboxCard.vue';
|
||||
import InboxListHeader from './components/InboxListHeader.vue';
|
||||
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import IntersectionObserver from 'dashboard/components/IntersectionObserver.vue';
|
||||
import CmdBarConversationSnooze from 'dashboard/routes/dashboard/commands/CmdBarConversationSnooze.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InboxCard,
|
||||
InboxListHeader,
|
||||
IntersectionObserver,
|
||||
CmdBarConversationSnooze,
|
||||
Spinner,
|
||||
},
|
||||
setup() {
|
||||
const { uiSettings } = useUISettings();
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const { uiSettings } = useUISettings();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
infiniteLoaderOptions: {
|
||||
root: this.$refs.notificationList,
|
||||
rootMargin: '100px 0px 100px 0px',
|
||||
},
|
||||
page: 1,
|
||||
status: '',
|
||||
type: '',
|
||||
sortOrder: wootConstants.INBOX_SORT_BY.NEWEST,
|
||||
isInboxContextMenuOpen: false,
|
||||
notificationIdToSnooze: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
meta: 'notifications/getMeta',
|
||||
uiFlags: 'notifications/getUIFlags',
|
||||
notification: 'notifications/getFilteredNotifications',
|
||||
notificationV4: 'notifications/getFilteredNotificationsV4',
|
||||
inboxById: 'inboxes/getInboxById',
|
||||
}),
|
||||
currentNotificationId() {
|
||||
return Number(this.$route.params.notification_id);
|
||||
},
|
||||
inboxFilters() {
|
||||
return {
|
||||
page: this.page,
|
||||
status: this.status,
|
||||
type: this.type,
|
||||
sortOrder: this.sortOrder,
|
||||
};
|
||||
},
|
||||
notifications() {
|
||||
return this.notification(this.inboxFilters);
|
||||
},
|
||||
notificationsV4() {
|
||||
return this.notificationV4(this.inboxFilters);
|
||||
},
|
||||
showEndOfList() {
|
||||
return this.uiFlags.isAllNotificationsLoaded && !this.uiFlags.isFetching;
|
||||
},
|
||||
showEmptyState() {
|
||||
return !this.uiFlags.isFetching && !this.notifications.length;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
inboxFilters(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.$store.dispatch('notifications/updateNotificationFilters', newVal);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setSavedFilter();
|
||||
this.fetchNotifications();
|
||||
},
|
||||
methods: {
|
||||
stateInbox(inboxId) {
|
||||
return this.inboxById(inboxId);
|
||||
},
|
||||
fetchNotifications() {
|
||||
this.page = 1;
|
||||
this.$store.dispatch('notifications/clear');
|
||||
const filter = this.inboxFilters;
|
||||
this.$store.dispatch('notifications/index', filter);
|
||||
},
|
||||
redirectToInbox() {
|
||||
if (this.$route.name === 'inbox_view') return;
|
||||
this.$router.replace({ name: 'inbox_view' });
|
||||
},
|
||||
loadMoreNotifications() {
|
||||
if (this.uiFlags.isAllNotificationsLoaded) return;
|
||||
this.$store.dispatch('notifications/index', {
|
||||
page: this.page + 1,
|
||||
status: this.status,
|
||||
type: this.type,
|
||||
sortOrder: this.sortOrder,
|
||||
});
|
||||
this.page += 1;
|
||||
},
|
||||
markNotificationAsRead(notification) {
|
||||
useTrack(INBOX_EVENTS.MARK_NOTIFICATION_AS_READ);
|
||||
const {
|
||||
id,
|
||||
primary_actor_id: primaryActorId,
|
||||
primary_actor_type: primaryActorType,
|
||||
} = notification;
|
||||
this.$store
|
||||
.dispatch('notifications/read', {
|
||||
id,
|
||||
primaryActorId,
|
||||
primaryActorType,
|
||||
unreadCount: this.meta.unreadCount,
|
||||
})
|
||||
.then(() => {
|
||||
useAlert(this.$t('INBOX.ALERTS.MARK_AS_READ'));
|
||||
this.$store.dispatch('notifications/unReadCount'); // to update the unread count in the store real time
|
||||
});
|
||||
},
|
||||
markNotificationAsUnRead(notification) {
|
||||
useTrack(INBOX_EVENTS.MARK_NOTIFICATION_AS_UNREAD);
|
||||
this.redirectToInbox();
|
||||
const { id } = notification;
|
||||
this.$store
|
||||
.dispatch('notifications/unread', {
|
||||
id,
|
||||
})
|
||||
.then(() => {
|
||||
useAlert(this.$t('INBOX.ALERTS.MARK_AS_UNREAD'));
|
||||
this.$store.dispatch('notifications/unReadCount'); // to update the unread count in the store real time
|
||||
});
|
||||
},
|
||||
deleteNotification(notification) {
|
||||
useTrack(INBOX_EVENTS.DELETE_NOTIFICATION);
|
||||
this.redirectToInbox();
|
||||
this.$store
|
||||
.dispatch('notifications/delete', {
|
||||
notification,
|
||||
unread_count: this.meta.unreadCount,
|
||||
count: this.meta.count,
|
||||
})
|
||||
.then(() => {
|
||||
useAlert(this.$t('INBOX.ALERTS.DELETE'));
|
||||
});
|
||||
},
|
||||
onFilterChange(option) {
|
||||
const { STATUS, TYPE, SORT_ORDER } = wootConstants.INBOX_FILTER_TYPE;
|
||||
if (option.type === STATUS) {
|
||||
this.status = option.selected ? option.key : '';
|
||||
}
|
||||
if (option.type === TYPE) {
|
||||
this.type = option.selected ? option.key : '';
|
||||
}
|
||||
if (option.type === SORT_ORDER) {
|
||||
this.sortOrder = option.key;
|
||||
}
|
||||
this.fetchNotifications();
|
||||
},
|
||||
setSavedFilter() {
|
||||
const { inbox_filter_by: filterBy = {} } = this.uiSettings;
|
||||
const { status, type, sort_by: sortBy } = filterBy;
|
||||
this.status = status;
|
||||
this.type = type;
|
||||
this.sortOrder = sortBy || wootConstants.INBOX_SORT_BY.NEWEST;
|
||||
this.$store.dispatch(
|
||||
'notifications/setNotificationFilters',
|
||||
this.inboxFilters
|
||||
);
|
||||
},
|
||||
openConversation(notification) {
|
||||
const {
|
||||
id,
|
||||
primaryActorId,
|
||||
primaryActorType,
|
||||
primaryActor: { inboxId },
|
||||
notificationType,
|
||||
} = notification;
|
||||
const notificationList = ref(null);
|
||||
const page = ref(1);
|
||||
const status = ref('');
|
||||
const type = ref('');
|
||||
const sortOrder = ref(wootConstants.INBOX_SORT_BY.NEWEST);
|
||||
const isInboxContextMenuOpen = ref(false);
|
||||
|
||||
if (this.$route.params.notification_id !== id) {
|
||||
useTrack(INBOX_EVENTS.OPEN_CONVERSATION_VIA_INBOX, {
|
||||
notificationType,
|
||||
});
|
||||
const infiniteLoaderOptions = computed(() => ({
|
||||
root: notificationList.value,
|
||||
rootMargin: '100px 0px 100px 0px',
|
||||
}));
|
||||
|
||||
this.$store
|
||||
.dispatch('notifications/read', {
|
||||
id,
|
||||
primaryActorId,
|
||||
primaryActorType,
|
||||
unreadCount: this.meta.unreadCount,
|
||||
})
|
||||
.then(() => {
|
||||
this.$store.dispatch('notifications/unReadCount'); // to update the unread count in the store real time
|
||||
});
|
||||
const meta = useMapGetter('notifications/getMeta');
|
||||
const uiFlags = useMapGetter('notifications/getUIFlags');
|
||||
const records = useMapGetter('notifications/getFilteredNotificationsV4');
|
||||
const inboxById = useMapGetter('inboxes/getInboxById');
|
||||
|
||||
this.$router.push({
|
||||
name: 'inbox_view_conversation',
|
||||
params: { inboxId, notification_id: id },
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
const currentConversationId = computed(() => Number(route.params.id));
|
||||
|
||||
const inboxFilters = computed(() => ({
|
||||
page: page.value,
|
||||
status: status.value,
|
||||
type: type.value,
|
||||
sortOrder: sortOrder.value,
|
||||
}));
|
||||
|
||||
const notifications = computed(() => {
|
||||
return records.value(inboxFilters.value);
|
||||
});
|
||||
|
||||
const showEndOfList = computed(() => {
|
||||
return uiFlags.value.isAllNotificationsLoaded && !uiFlags.value.isFetching;
|
||||
});
|
||||
|
||||
const showEmptyState = computed(() => {
|
||||
return !uiFlags.value.isFetching && !notifications.value.length;
|
||||
});
|
||||
|
||||
const stateInbox = inboxId => {
|
||||
return inboxById.value(inboxId);
|
||||
};
|
||||
|
||||
const fetchNotifications = () => {
|
||||
page.value = 1;
|
||||
store.dispatch('notifications/clear');
|
||||
const filter = inboxFilters.value;
|
||||
store.dispatch('notifications/index', filter);
|
||||
};
|
||||
|
||||
const scrollActiveIntoView = () => {
|
||||
const activeEl = notificationList.value?.querySelector('.inbox-card.active');
|
||||
activeEl?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const redirectToInbox = () => {
|
||||
if (route.name === 'inbox_view') return;
|
||||
router.replace({ name: 'inbox_view' });
|
||||
};
|
||||
|
||||
const loadMoreNotifications = () => {
|
||||
if (uiFlags.value.isAllNotificationsLoaded) return;
|
||||
|
||||
page.value += 1;
|
||||
store.dispatch('notifications/index', {
|
||||
page: page.value,
|
||||
status: status.value,
|
||||
type: type.value,
|
||||
sortOrder: sortOrder.value,
|
||||
});
|
||||
};
|
||||
|
||||
const markNotificationAsRead = async notificationItem => {
|
||||
useTrack(INBOX_EVENTS.MARK_NOTIFICATION_AS_READ);
|
||||
|
||||
const {
|
||||
id,
|
||||
primary_actor_id: primaryActorId,
|
||||
primary_actor_type: primaryActorType,
|
||||
} = notificationItem;
|
||||
|
||||
try {
|
||||
await store.dispatch('notifications/read', {
|
||||
id,
|
||||
primaryActorId,
|
||||
primaryActorType,
|
||||
unreadCount: meta.value.unreadCount,
|
||||
});
|
||||
|
||||
useAlert(t('INBOX.ALERTS.MARK_AS_READ'));
|
||||
store.dispatch('notifications/unReadCount');
|
||||
} catch {
|
||||
// error
|
||||
}
|
||||
};
|
||||
|
||||
const markNotificationAsUnRead = async notificationItem => {
|
||||
useTrack(INBOX_EVENTS.MARK_NOTIFICATION_AS_UNREAD);
|
||||
redirectToInbox();
|
||||
|
||||
const { id } = notificationItem;
|
||||
|
||||
try {
|
||||
await store.dispatch('notifications/unread', { id });
|
||||
useAlert(t('INBOX.ALERTS.MARK_AS_UNREAD'));
|
||||
store.dispatch('notifications/unReadCount');
|
||||
} catch {
|
||||
// error
|
||||
}
|
||||
};
|
||||
|
||||
const deleteNotification = async notificationItem => {
|
||||
useTrack(INBOX_EVENTS.DELETE_NOTIFICATION);
|
||||
redirectToInbox();
|
||||
|
||||
try {
|
||||
await store.dispatch('notifications/delete', {
|
||||
notification: notificationItem,
|
||||
unread_count: meta.value.unreadCount,
|
||||
count: meta.value.count,
|
||||
});
|
||||
|
||||
useAlert(t('INBOX.ALERTS.DELETE'));
|
||||
} catch {
|
||||
// error
|
||||
}
|
||||
};
|
||||
|
||||
const onFilterChange = option => {
|
||||
const { STATUS, TYPE, SORT_ORDER } = wootConstants.INBOX_FILTER_TYPE;
|
||||
if (option.type === STATUS) {
|
||||
status.value = option.selected ? option.key : '';
|
||||
}
|
||||
if (option.type === TYPE) {
|
||||
type.value = option.selected ? option.key : '';
|
||||
}
|
||||
if (option.type === SORT_ORDER) {
|
||||
sortOrder.value = option.key;
|
||||
}
|
||||
fetchNotifications();
|
||||
};
|
||||
|
||||
const setSavedFilter = () => {
|
||||
const { inbox_filter_by: filterBy = {} } = uiSettings.value;
|
||||
const { status: savedStatus, type: savedType, sort_by: sortBy } = filterBy;
|
||||
status.value = savedStatus;
|
||||
type.value = savedType;
|
||||
sortOrder.value = sortBy || wootConstants.INBOX_SORT_BY.NEWEST;
|
||||
store.dispatch('notifications/setNotificationFilters', inboxFilters.value);
|
||||
};
|
||||
|
||||
const openConversation = async notificationItem => {
|
||||
const {
|
||||
id,
|
||||
primaryActorId,
|
||||
primaryActorType,
|
||||
primaryActor: { inboxId, id: conversationId },
|
||||
notificationType,
|
||||
} = notificationItem;
|
||||
|
||||
if (route.params.id === String(conversationId)) return;
|
||||
|
||||
useTrack(INBOX_EVENTS.OPEN_CONVERSATION_VIA_INBOX, {
|
||||
notificationType,
|
||||
});
|
||||
|
||||
try {
|
||||
await store.dispatch('notifications/read', {
|
||||
id,
|
||||
primaryActorId,
|
||||
primaryActorType,
|
||||
unreadCount: meta.value.unreadCount,
|
||||
});
|
||||
|
||||
// to update the unread count in the store realtime
|
||||
store.dispatch('notifications/unReadCount');
|
||||
|
||||
router.push({
|
||||
name: 'inbox_view_conversation',
|
||||
params: { inboxId, type: 'conversation', id: conversationId },
|
||||
});
|
||||
} catch {
|
||||
// error
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
inboxFilters,
|
||||
(newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
store.dispatch('notifications/updateNotificationFilters', newVal);
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(currentConversationId, () => {
|
||||
nextTick(scrollActiveIntoView);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
scrollActiveIntoView();
|
||||
setSavedFilter();
|
||||
fetchNotifications();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="flex w-full h-full bg-n-solid-1">
|
||||
<div
|
||||
class="flex flex-col h-full w-full lg:min-w-[400px] lg:max-w-[400px] ltr:border-r rtl:border-l border-n-weak"
|
||||
:class="!currentNotificationId ? 'flex' : 'hidden xl:flex'"
|
||||
class="flex flex-col h-full w-full lg:min-w-[340px] lg:max-w-[340px] ltr:border-r rtl:border-l border-n-weak"
|
||||
:class="!currentConversationId ? 'flex' : 'hidden xl:flex'"
|
||||
>
|
||||
<InboxListHeader
|
||||
:is-context-menu-open="isInboxContextMenuOpen"
|
||||
@@ -224,17 +239,17 @@ export default {
|
||||
/>
|
||||
<div
|
||||
ref="notificationList"
|
||||
class="flex flex-col gap-px w-full h-[calc(100%-56px)] pb-3 overflow-x-hidden px-3 overflow-y-auto divide-y divide-n-weak [&>*:hover]:!border-y-transparent [&>*.active]:!border-y-transparent [&>*:hover+*]:!border-t-transparent [&>*.active+*]:!border-t-transparent"
|
||||
class="flex flex-col gap-0.5 w-full h-[calc(100%-56px)] pb-4 overflow-x-hidden px-2 overflow-y-auto divide-y divide-n-weak [&>*:hover]:!border-y-transparent [&>*.active]:!border-y-transparent [&>*:hover+*]:!border-t-transparent [&>*.active+*]:!border-t-transparent"
|
||||
>
|
||||
<InboxCard
|
||||
v-for="notificationItem in notificationsV4"
|
||||
v-for="notificationItem in notifications"
|
||||
:key="notificationItem.id"
|
||||
:inbox-item="notificationItem"
|
||||
:state-inbox="stateInbox(notificationItem.primaryActor?.inboxId)"
|
||||
class="rounded-none hover:rounded-xl hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3"
|
||||
class="inbox-card rounded-lg hover:rounded-lg hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3"
|
||||
:class="
|
||||
currentNotificationId === notificationItem.id
|
||||
? 'bg-n-alpha-1 dark:bg-n-alpha-3 click-animation rounded-xl active'
|
||||
currentConversationId === notificationItem.primaryActor?.id
|
||||
? 'bg-n-alpha-1 dark:bg-n-alpha-3 rounded-lg active'
|
||||
: ''
|
||||
"
|
||||
@mark-notification-as-read="markNotificationAsRead"
|
||||
@@ -264,23 +279,3 @@ export default {
|
||||
<CmdBarConversationSnooze />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.click-animation {
|
||||
animation: click-animation 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes click-animation {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,183 +1,191 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
<script setup>
|
||||
import { computed, ref, watch, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
import InboxItemHeader from './components/InboxItemHeader.vue';
|
||||
import ConversationBox from 'dashboard/components/widgets/conversation/ConversationBox.vue';
|
||||
import InboxEmptyState from './InboxEmptyState.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import SidepanelSwitch from 'dashboard/components-next/Conversation/SidepanelSwitch.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InboxItemHeader,
|
||||
InboxEmptyState,
|
||||
ConversationBox,
|
||||
Spinner,
|
||||
},
|
||||
setup() {
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
import InboxItemHeader from './components/InboxItemHeader.vue';
|
||||
import ConversationBox from 'dashboard/components/widgets/conversation/ConversationBox.vue';
|
||||
import InboxEmptyState from './InboxEmptyState.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import ConversationSidebar from 'dashboard/components/widgets/conversation/ConversationSidebar.vue';
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isConversationLoading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
notification: 'notifications/getFilteredNotifications',
|
||||
currentChat: 'getSelectedChat',
|
||||
activeNotificationById: 'notifications/getNotificationById',
|
||||
conversationById: 'getConversationById',
|
||||
uiFlags: 'notifications/getUIFlags',
|
||||
meta: 'notifications/getMeta',
|
||||
}),
|
||||
notifications() {
|
||||
return this.notification({
|
||||
sortOrder: this.activeSortOrder,
|
||||
});
|
||||
},
|
||||
inboxId() {
|
||||
return Number(this.$route.params.inboxId);
|
||||
},
|
||||
notificationId() {
|
||||
return Number(this.$route.params.notification_id);
|
||||
},
|
||||
activeNotification() {
|
||||
return this.activeNotificationById(this.notificationId);
|
||||
},
|
||||
conversationId() {
|
||||
return this.activeNotification?.primary_actor?.id;
|
||||
},
|
||||
totalNotificationCount() {
|
||||
return this.meta.count;
|
||||
},
|
||||
showEmptyState() {
|
||||
return (
|
||||
!this.conversationId ||
|
||||
(!this.notifications?.length && this.uiFlags.isFetching)
|
||||
);
|
||||
},
|
||||
activeNotificationIndex() {
|
||||
return this.notifications?.findIndex(n => n.id === this.notificationId);
|
||||
},
|
||||
activeSortOrder() {
|
||||
const { inbox_filter_by: filterBy = {} } = this.uiSettings;
|
||||
const { sort_by: sortBy } = filterBy;
|
||||
return sortBy || 'desc';
|
||||
},
|
||||
isContactPanelOpen() {
|
||||
if (this.currentChat.id) {
|
||||
const { is_contact_sidebar_open: isContactSidebarOpen } =
|
||||
this.uiSettings;
|
||||
return isContactSidebarOpen;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
conversationId: {
|
||||
immediate: true,
|
||||
handler(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.fetchConversationById();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('agents/get');
|
||||
},
|
||||
methods: {
|
||||
async fetchConversationById() {
|
||||
if (!this.notificationId || !this.conversationId) return;
|
||||
this.$store.dispatch('clearSelectedState');
|
||||
const existingChat = this.findConversation();
|
||||
if (existingChat) {
|
||||
this.setActiveChat(existingChat);
|
||||
return;
|
||||
}
|
||||
this.isConversationLoading = true;
|
||||
await this.$store.dispatch('getConversation', this.conversationId);
|
||||
this.setActiveChat();
|
||||
this.isConversationLoading = false;
|
||||
},
|
||||
setActiveChat() {
|
||||
const selectedConversation = this.findConversation();
|
||||
if (!selectedConversation) return;
|
||||
this.$store
|
||||
.dispatch('setActiveChat', { data: selectedConversation })
|
||||
.then(() => {
|
||||
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
});
|
||||
},
|
||||
findConversation() {
|
||||
return this.conversationById(this.conversationId);
|
||||
},
|
||||
navigateToConversation(activeIndex, direction) {
|
||||
let updatedIndex;
|
||||
if (direction === 'prev' && activeIndex) {
|
||||
updatedIndex = activeIndex - 1;
|
||||
} else if (
|
||||
direction === 'next' &&
|
||||
activeIndex < this.totalNotificationCount
|
||||
) {
|
||||
updatedIndex = activeIndex + 1;
|
||||
}
|
||||
const targetNotification = this.notifications[updatedIndex];
|
||||
if (targetNotification) {
|
||||
this.openNotification(targetNotification);
|
||||
}
|
||||
},
|
||||
openNotification(notification) {
|
||||
const {
|
||||
id,
|
||||
primary_actor_id: primaryActorId,
|
||||
primary_actor_type: primaryActorType,
|
||||
primary_actor: { meta: { unreadCount } = {} },
|
||||
notification_type: notificationType,
|
||||
} = notification;
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const { uiSettings } = useUISettings();
|
||||
|
||||
useTrack(INBOX_EVENTS.OPEN_CONVERSATION_VIA_INBOX, {
|
||||
notificationType,
|
||||
});
|
||||
const isConversationLoading = ref(false);
|
||||
|
||||
this.$store.dispatch('notifications/read', {
|
||||
id,
|
||||
primaryActorId,
|
||||
primaryActorType,
|
||||
unreadCount,
|
||||
});
|
||||
const notification = useMapGetter('notifications/getFilteredNotifications');
|
||||
const currentChat = useMapGetter('getSelectedChat');
|
||||
const conversationById = useMapGetter('getConversationById');
|
||||
const uiFlags = useMapGetter('notifications/getUIFlags');
|
||||
const meta = useMapGetter('notifications/getMeta');
|
||||
|
||||
this.$router.push({
|
||||
name: 'inbox_view_conversation',
|
||||
params: { notification_id: id },
|
||||
});
|
||||
},
|
||||
onClickNext() {
|
||||
this.navigateToConversation(this.activeNotificationIndex, 'next');
|
||||
},
|
||||
onClickPrev() {
|
||||
this.navigateToConversation(this.activeNotificationIndex, 'prev');
|
||||
},
|
||||
onToggleContactPanel() {
|
||||
this.updateUISettings({
|
||||
is_contact_sidebar_open: !this.isContactPanelOpen,
|
||||
});
|
||||
},
|
||||
},
|
||||
const inboxId = computed(() => Number(route.params.inboxId));
|
||||
const conversationId = computed(() => Number(route.params.id));
|
||||
|
||||
const activeSortOrder = computed(() => {
|
||||
const { inbox_filter_by: filterBy = {} } = uiSettings.value;
|
||||
const { sort_by: sortBy } = filterBy;
|
||||
return sortBy || 'desc';
|
||||
});
|
||||
|
||||
const notifications = computed(() => {
|
||||
return notification.value({
|
||||
sortOrder: activeSortOrder.value,
|
||||
});
|
||||
});
|
||||
|
||||
const activeNotification = computed(() => {
|
||||
return notifications.value?.find(
|
||||
n => n.primary_actor?.id === conversationId.value
|
||||
);
|
||||
});
|
||||
|
||||
const totalNotificationCount = computed(() => {
|
||||
return meta.value.count;
|
||||
});
|
||||
|
||||
const showEmptyState = computed(() => {
|
||||
return (
|
||||
!conversationId.value ||
|
||||
(!notifications.value?.length && uiFlags.value.isFetching)
|
||||
);
|
||||
});
|
||||
|
||||
const activeNotificationIndex = computed(() => {
|
||||
return notifications.value?.findIndex(
|
||||
n => n.primary_actor?.id === conversationId.value
|
||||
);
|
||||
});
|
||||
|
||||
const isContactPanelOpen = computed(() => {
|
||||
if (currentChat.value.id) {
|
||||
const { is_contact_sidebar_open: isContactSidebarOpen } = uiSettings.value;
|
||||
return isContactSidebarOpen;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const findConversation = () => {
|
||||
return conversationById.value(conversationId.value);
|
||||
};
|
||||
|
||||
const openNotification = async notificationItem => {
|
||||
const {
|
||||
id,
|
||||
primary_actor_id: primaryActorId,
|
||||
primary_actor_type: primaryActorType,
|
||||
primary_actor: {
|
||||
meta: { unreadCount } = {},
|
||||
id: conversationIdFromNotification,
|
||||
},
|
||||
notification_type: notificationType,
|
||||
} = notificationItem;
|
||||
|
||||
useTrack(INBOX_EVENTS.OPEN_CONVERSATION_VIA_INBOX, {
|
||||
notificationType,
|
||||
});
|
||||
|
||||
try {
|
||||
await store.dispatch('notifications/read', {
|
||||
id,
|
||||
primaryActorId,
|
||||
primaryActorType,
|
||||
unreadCount,
|
||||
});
|
||||
|
||||
router.push({
|
||||
name: 'inbox_view_conversation',
|
||||
params: { type: 'conversation', id: conversationIdFromNotification },
|
||||
});
|
||||
} catch {
|
||||
// error
|
||||
}
|
||||
};
|
||||
|
||||
const setActiveChat = async () => {
|
||||
const selectedConversation = findConversation();
|
||||
if (!selectedConversation) return;
|
||||
|
||||
try {
|
||||
await store.dispatch('setActiveChat', { data: selectedConversation });
|
||||
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
} catch {
|
||||
// error
|
||||
}
|
||||
};
|
||||
|
||||
const fetchConversationById = async () => {
|
||||
if (!conversationId.value) return;
|
||||
|
||||
store.dispatch('clearSelectedState');
|
||||
const existingChat = findConversation();
|
||||
|
||||
if (existingChat) {
|
||||
await setActiveChat();
|
||||
return;
|
||||
}
|
||||
|
||||
isConversationLoading.value = true;
|
||||
|
||||
try {
|
||||
await store.dispatch('getConversation', conversationId.value);
|
||||
await setActiveChat();
|
||||
} catch {
|
||||
// error
|
||||
} finally {
|
||||
isConversationLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToConversation = (activeIndex, direction) => {
|
||||
const isValidPrev = direction === 'prev' && activeIndex > 0;
|
||||
const isValidNext =
|
||||
direction === 'next' && activeIndex < totalNotificationCount.value - 1;
|
||||
|
||||
if (!isValidPrev && !isValidNext) return;
|
||||
|
||||
const updatedIndex = direction === 'prev' ? activeIndex - 1 : activeIndex + 1;
|
||||
const targetNotification = notifications.value[updatedIndex];
|
||||
|
||||
if (targetNotification) {
|
||||
openNotification(targetNotification);
|
||||
}
|
||||
};
|
||||
|
||||
const onClickNext = () => {
|
||||
navigateToConversation(activeNotificationIndex.value, 'next');
|
||||
};
|
||||
|
||||
const onClickPrev = () => {
|
||||
navigateToConversation(activeNotificationIndex.value, 'prev');
|
||||
};
|
||||
|
||||
watch(
|
||||
conversationId,
|
||||
(newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
fetchConversationById();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await store.dispatch('agents/get');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full xl:w-[calc(100%-400px)]">
|
||||
<div class="h-full w-full flex-1">
|
||||
<div v-if="showEmptyState" class="flex w-full h-full">
|
||||
<InboxEmptyState
|
||||
:empty-state-message="$t('INBOX.LIST.NO_MESSAGES_AVAILABLE')"
|
||||
@@ -193,19 +201,24 @@ export default {
|
||||
/>
|
||||
<div
|
||||
v-if="isConversationLoading"
|
||||
class="flex items-center h-[calc(100%-56px)] my-4 justify-center bg-n-solid-1"
|
||||
class="flex items-center flex-1 my-4 justify-center bg-n-solid-1"
|
||||
>
|
||||
<Spinner class="text-n-brand" />
|
||||
</div>
|
||||
<ConversationBox
|
||||
v-else
|
||||
class="h-[calc(100%-56px)] [&.conversation-details-wrap]:!border-0"
|
||||
is-inbox-view
|
||||
:inbox-id="inboxId"
|
||||
:is-contact-panel-open="isContactPanelOpen"
|
||||
:is-on-expanded-layout="false"
|
||||
@contact-panel-toggle="onToggleContactPanel"
|
||||
/>
|
||||
<div v-else class="flex h-[calc(100%-48px)] min-w-0">
|
||||
<ConversationBox
|
||||
class="flex-1 [&.conversation-details-wrap]:!border-0"
|
||||
is-inbox-view
|
||||
:inbox-id="inboxId"
|
||||
:is-on-expanded-layout="false"
|
||||
>
|
||||
<SidepanelSwitch v-if="currentChat.id" />
|
||||
</ConversationBox>
|
||||
<ConversationSidebar
|
||||
v-if="isContactPanelOpen"
|
||||
:current-chat="currentChat"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -109,7 +109,7 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between w-full gap-2 border-b ltr:pl-4 rtl:pl-2 h-12 ltr:pr-2 rtl:pr-4 rtl:border-r border-n-weak"
|
||||
class="flex items-center justify-between w-full gap-2 border-b px-3 h-12 rtl:border-r border-n-weak flex-shrink-0"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<BackButton
|
||||
|
||||
@@ -79,9 +79,9 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between w-full gap-1 h-14 px-4 mb-2">
|
||||
<div class="flex items-center justify-between w-full gap-1 h-12 px-3">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<h1 class="min-w-0 text-lg font-medium truncate text-n-slate-12">
|
||||
<h1 class="min-w-0 text-base font-medium truncate text-n-slate-12">
|
||||
{{ $t('INBOX.LIST.TITLE') }}
|
||||
</h1>
|
||||
<div class="relative">
|
||||
|
||||
@@ -21,7 +21,7 @@ export const routes = [
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':notification_id',
|
||||
path: ':type/:id',
|
||||
name: 'inbox_view_conversation',
|
||||
component: InboxDetailView,
|
||||
meta: {
|
||||
|
||||
@@ -1,32 +1,21 @@
|
||||
<script>
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import BackButton from '../../../components/widgets/BackButton.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BackButton,
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
headerTitle: {
|
||||
default: '',
|
||||
type: String,
|
||||
},
|
||||
buttonRoute: {
|
||||
default: '',
|
||||
type: String,
|
||||
},
|
||||
buttonText: {
|
||||
default: '',
|
||||
type: String,
|
||||
},
|
||||
icon: {
|
||||
default: '',
|
||||
type: String,
|
||||
},
|
||||
showBackButton: { type: Boolean, default: false },
|
||||
showNewButton: { type: Boolean, default: false },
|
||||
backUrl: {
|
||||
type: [String, Object],
|
||||
default: '',
|
||||
@@ -67,14 +56,5 @@ export default {
|
||||
{{ headerTitle }}
|
||||
</span>
|
||||
</h1>
|
||||
<!-- TODO: Remove this when we are not using this -->
|
||||
<router-link v-if="showNewButton && isAdmin" :to="buttonRoute">
|
||||
<NextButton
|
||||
teal
|
||||
icon="i-lucide-circle-plus"
|
||||
class="button--fixed-top"
|
||||
:label="buttonText"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -2,13 +2,10 @@
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import SettingsHeader from './SettingsHeader.vue';
|
||||
|
||||
const props = defineProps({
|
||||
headerTitle: { type: String, default: '' },
|
||||
headerButtonText: { type: String, default: '' },
|
||||
icon: { type: String, default: '' },
|
||||
keepAlive: { type: Boolean, default: true },
|
||||
newButtonRoutes: { type: Array, default: () => [] },
|
||||
showBackButton: { type: Boolean, default: false },
|
||||
backUrl: { type: [String, Object], default: '' },
|
||||
fullWidth: { type: Boolean, default: false },
|
||||
@@ -16,16 +13,8 @@ const props = defineProps({
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const showNewButton = computed(
|
||||
() => props.newButtonRoutes.length && !props.showBackButton
|
||||
);
|
||||
|
||||
const showSettingsHeader = computed(
|
||||
() =>
|
||||
props.headerTitle ||
|
||||
props.icon ||
|
||||
props.showBackButton ||
|
||||
showNewButton.value
|
||||
() => props.headerTitle || props.icon || props.showBackButton
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -37,13 +26,10 @@ const showSettingsHeader = computed(
|
||||
>
|
||||
<SettingsHeader
|
||||
v-if="showSettingsHeader"
|
||||
button-route="new"
|
||||
:icon="icon"
|
||||
:header-title="t(headerTitle)"
|
||||
:button-text="t(headerButtonText)"
|
||||
:show-back-button="showBackButton"
|
||||
:back-url="backUrl"
|
||||
:show-new-button="showNewButton"
|
||||
class="sticky top-0 z-20"
|
||||
:class="{ 'max-w-6xl w-full mx-auto': fullWidth }"
|
||||
/>
|
||||
|
||||
@@ -42,9 +42,7 @@ export default {
|
||||
const fullWidth = params.name === 'settings_inbox_show';
|
||||
return {
|
||||
headerTitle: 'INBOX_MGMT.HEADER',
|
||||
headerButtonText: 'SETTINGS.INBOXES.NEW_INBOX',
|
||||
icon: 'mail-inbox-all',
|
||||
newButtonRoutes: ['settings_inbox_list'],
|
||||
showBackButton,
|
||||
fullWidth,
|
||||
};
|
||||
|
||||
@@ -41,9 +41,7 @@ export default {
|
||||
props: () => {
|
||||
return {
|
||||
headerTitle: 'TEAMS_SETTINGS.HEADER',
|
||||
headerButtonText: 'TEAMS_SETTINGS.NEW_TEAM',
|
||||
icon: 'people-team',
|
||||
newButtonRoutes: ['settings_teams_new'],
|
||||
showBackButton: true,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -106,42 +106,44 @@ defineExpose({ shouldShowUpgradePage });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="shouldShowUpgradePage">
|
||||
<div class="mx-auto h-full pt-[clamp(3rem,15vh,12rem)]">
|
||||
<div
|
||||
class="flex flex-col gap-4 max-w-md px-8 py-6 shadow-lg bg-n-solid-1 rounded-xl outline outline-1 outline-n-container"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center w-full gap-2">
|
||||
<span
|
||||
class="flex items-center justify-center w-6 h-6 rounded-full bg-n-solid-blue"
|
||||
>
|
||||
<Icon
|
||||
class="flex-shrink-0 text-n-brand size-[14px]"
|
||||
icon="i-lucide-lock-keyhole"
|
||||
/>
|
||||
</span>
|
||||
<span class="text-base font-medium text-n-slate-12">
|
||||
{{ $t('GENERAL_SETTINGS.UPGRADE') }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-normal text-n-slate-11 mb-3">
|
||||
{{ limitExceededMessage }}
|
||||
</p>
|
||||
<p v-if="!isAdmin">
|
||||
{{ t('GENERAL_SETTINGS.LIMIT_MESSAGES.NON_ADMIN') }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="shouldShowUpgradePage"
|
||||
class="mx-auto h-full pt-[clamp(3rem,15vh,12rem)]"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col gap-4 max-w-md px-8 py-6 shadow-lg bg-n-solid-1 rounded-xl outline outline-1 outline-n-container"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center w-full gap-2">
|
||||
<span
|
||||
class="flex items-center justify-center w-6 h-6 rounded-full bg-n-solid-blue"
|
||||
>
|
||||
<Icon
|
||||
class="flex-shrink-0 text-n-brand size-[14px]"
|
||||
icon="i-lucide-lock-keyhole"
|
||||
/>
|
||||
</span>
|
||||
<span class="text-base font-medium text-n-slate-12">
|
||||
{{ $t('GENERAL_SETTINGS.UPGRADE') }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-normal text-n-slate-11 mb-3">
|
||||
{{ limitExceededMessage }}
|
||||
</p>
|
||||
<p v-if="!isAdmin">
|
||||
{{ t('GENERAL_SETTINGS.LIMIT_MESSAGES.NON_ADMIN') }}
|
||||
</p>
|
||||
</div>
|
||||
<NextButton
|
||||
v-if="isAdmin"
|
||||
:label="$t('GENERAL_SETTINGS.OPEN_BILLING')"
|
||||
icon="i-lucide-credit-card"
|
||||
@click="routeToBilling()"
|
||||
/>
|
||||
</div>
|
||||
<NextButton
|
||||
v-if="isAdmin"
|
||||
:label="$t('GENERAL_SETTINGS.OPEN_BILLING')"
|
||||
icon="i-lucide-credit-card"
|
||||
@click="routeToBilling()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else />
|
||||
<slot />
|
||||
</div>
|
||||
<div v-else />
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user