feat: Update settings pages UI (#13191)
This commit is contained in:
@@ -143,7 +143,7 @@ export default {
|
||||
/>
|
||||
|
||||
<main
|
||||
class="flex flex-1 h-full w-full min-h-0 px-0 overflow-hidden md:rounded-lg md:border md:border-n-weak bg-n-surface-1"
|
||||
class="flex flex-1 h-full w-full relative min-h-0 px-0 overflow-hidden md:rounded-lg md:border md:border-n-weak bg-n-surface-1 after:absolute after:bottom-0 after:inset-x-0 after:h-3 after:bg-gradient-to-t after:from-n-surface-1 after:from-0% after:to-transparent after:z-50 after:pointer-events-none"
|
||||
>
|
||||
<UpgradePage
|
||||
v-show="showUpgradePage"
|
||||
|
||||
@@ -280,11 +280,11 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<LimitBanner class="mb-5 z-20 relative" />
|
||||
<LimitBanner class="mb-5 relative" />
|
||||
<Banner
|
||||
v-if="pendingCount > 0 && bulkSelectedIds.size === 0"
|
||||
color="blue"
|
||||
class="mb-4 -mt-2 z-20 relative"
|
||||
class="mb-4 -mt-2 relative"
|
||||
:action-label="$t('CAPTAIN.RESPONSES.PENDING_BANNER.ACTION')"
|
||||
@action="navigateToPendingFAQs"
|
||||
>
|
||||
|
||||
@@ -154,7 +154,7 @@ onMounted(() => {
|
||||
t('COMPANIES.EMPTY_STATE.TITLE')
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-4 px-6 py-4">
|
||||
<div v-else class="flex flex-col gap-4 py-4">
|
||||
<CompaniesCard
|
||||
v-for="company in companies"
|
||||
:id="company.id"
|
||||
|
||||
@@ -79,9 +79,11 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between w-full gap-1 h-12 px-3">
|
||||
<div
|
||||
class="flex items-center justify-between w-full gap-1 h-[3.25rem] ltr:pl-4 rtl:pr-4 ltr:pr-3 rtl:pl-3"
|
||||
>
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<h1 class="min-w-0 text-base font-medium truncate text-n-slate-12">
|
||||
<h1 class="text-heading-2 truncate text-n-slate-12 min-w-0">
|
||||
{{ $t('INBOX.LIST.TITLE') }}
|
||||
</h1>
|
||||
<div class="relative">
|
||||
@@ -91,7 +93,7 @@ export default {
|
||||
trailing-icon
|
||||
slate
|
||||
xs
|
||||
faded
|
||||
:variant="showInboxDisplayMenu ? 'faded' : 'solid'"
|
||||
@click="openInboxDisplayMenu"
|
||||
/>
|
||||
<InboxDisplayMenu
|
||||
@@ -106,8 +108,8 @@ export default {
|
||||
<NextButton
|
||||
icon="i-lucide-sliders-vertical"
|
||||
slate
|
||||
xs
|
||||
faded
|
||||
sm
|
||||
:variant="showInboxOptionMenu ? 'faded' : 'ghost'"
|
||||
@click="openInboxOptionsMenu"
|
||||
/>
|
||||
<InboxOptionMenu
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
/* eslint arrow-body-style: 0 */
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
import SettingsWrapper from '../settings/Wrapper.vue';
|
||||
import SettingsWrapper from '../settings/SettingsWrapper.vue';
|
||||
import NotificationsView from './components/NotificationsView.vue';
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/notifications'),
|
||||
component: SettingsWrapper,
|
||||
props: {
|
||||
headerTitle: '',
|
||||
icon: '',
|
||||
showNewButton: false,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
|
||||
@@ -41,14 +41,14 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex justify-between items-center h-20 min-h-[3.5rem] px-4 py-2 bg-n-surface-1"
|
||||
class="flex justify-between items-center h-20 min-h-[3.5rem] px-6 py-2 bg-n-surface-1"
|
||||
>
|
||||
<h1 class="flex items-center mb-0 text-2xl text-n-slate-12">
|
||||
<BackButton
|
||||
v-if="showBackButton"
|
||||
:button-label="backButtonLabel"
|
||||
:back-url="backUrl"
|
||||
class="ml-2 mr-4"
|
||||
class="ltr:mr-4 rtl:ml-4"
|
||||
/>
|
||||
|
||||
<slot />
|
||||
|
||||
@@ -20,7 +20,7 @@ defineProps({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col w-full h-full gap-8 font-inter">
|
||||
<div class="flex flex-col w-full h-full gap-4 font-inter">
|
||||
<slot name="header" />
|
||||
<!-- Added to render any templates that should be rendered before body -->
|
||||
<main>
|
||||
|
||||
@@ -8,13 +8,13 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col w-full items-start mb-4">
|
||||
<h2 class="text-xl font-medium mb-1 text-n-slate-12 break-words">
|
||||
<div class="flex flex-col gap-1.5 w-full items-start mb-4">
|
||||
<h2 class="text-heading-1 text-n-slate-12 break-words">
|
||||
{{ headerTitle }}
|
||||
</h2>
|
||||
<p
|
||||
v-dompurify-html="headerContent"
|
||||
class="text-sm w-full text-n-slate-11"
|
||||
class="text-body-main w-full text-n-slate-11"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
<script setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
defineProps({
|
||||
keepAlive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col w-full h-full m-0 p-6 sm:py-8 lg:px-16 overflow-auto bg-n-surface-1 font-inter"
|
||||
class="flex flex-col w-full h-full m-0 pb-8 pt-4 px-6 overflow-auto bg-n-surface-1"
|
||||
>
|
||||
<div class="flex items-start w-full max-w-6xl mx-auto">
|
||||
<div class="flex items-start w-full max-w-5xl mx-auto">
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive v-if="keepAlive">
|
||||
<component :is="Component" />
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
<component :is="Component" v-else />
|
||||
<component :is="Component" v-else :key="route.fullPath" />
|
||||
</router-view>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@ const showSettingsHeader = computed(
|
||||
:class="{ 'max-w-6xl w-full mx-auto': fullWidth }"
|
||||
/>
|
||||
|
||||
<router-view v-slot="{ Component }" class="px-5 flex-1 overflow-hidden">
|
||||
<router-view v-slot="{ Component }" class="px-4 flex-1 overflow-hidden">
|
||||
<component :is="Component" v-if="!keepAlive" :key="$route.fullPath" />
|
||||
<keep-alive v-else>
|
||||
<component :is="Component" :key="$route.fullPath" />
|
||||
|
||||
@@ -154,12 +154,13 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col max-w-2xl mx-auto w-full">
|
||||
<div class="flex flex-col w-full">
|
||||
<BaseSettingsHeader :title="$t('GENERAL_SETTINGS.TITLE')" />
|
||||
<div class="flex-grow flex-shrink min-w-0 mt-3">
|
||||
<SectionLayout
|
||||
:title="$t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.TITLE')"
|
||||
:description="$t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.NOTE')"
|
||||
class="!pt-0"
|
||||
>
|
||||
<form
|
||||
v-if="!uiFlags.isFetchingItem"
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@ const { t } = useI18n();
|
||||
>
|
||||
<header class="grid grid-cols-4">
|
||||
<div class="col-span-3">
|
||||
<h4 class="text-lg font-medium text-n-slate-12 flex items-center gap-2">
|
||||
<h4 class="text-heading-2 text-n-slate-12 flex items-center gap-2">
|
||||
<slot name="title">{{ title }}</slot>
|
||||
<div
|
||||
v-if="beta"
|
||||
@@ -31,7 +31,7 @@ const { t } = useI18n();
|
||||
{{ t('GENERAL.BETA') }}
|
||||
</div>
|
||||
</h4>
|
||||
<p class="text-n-slate-11 text-sm mt-2">
|
||||
<p class="text-n-slate-11 text-body-main mt-2">
|
||||
<slot name="description">{{ description }}</slot>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, computed, onMounted } from 'vue';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
@@ -10,6 +11,11 @@ import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
import AgentBotModal from './components/AgentBotModal.vue';
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import {
|
||||
BaseTable,
|
||||
BaseTableRow,
|
||||
BaseTableCell,
|
||||
} from 'dashboard/components-next/table';
|
||||
|
||||
const MODAL_TYPES = {
|
||||
CREATE: 'create',
|
||||
@@ -23,6 +29,7 @@ const agentBots = useMapGetter('agentBots/getBots');
|
||||
const uiFlags = useMapGetter('agentBots/getUIFlags');
|
||||
|
||||
const selectedBot = ref({});
|
||||
const searchQuery = ref('');
|
||||
const loading = ref({});
|
||||
const modalType = ref(MODAL_TYPES.CREATE);
|
||||
const agentBotModalRef = ref(null);
|
||||
@@ -32,11 +39,18 @@ const tableHeaders = computed(() => {
|
||||
return [
|
||||
t('AGENT_BOTS.LIST.TABLE_HEADER.DETAILS'),
|
||||
t('AGENT_BOTS.LIST.TABLE_HEADER.URL'),
|
||||
t('AGENT_BOTS.LIST.TABLE_HEADER.ACTIONS'),
|
||||
];
|
||||
});
|
||||
|
||||
const selectedBotName = computed(() => selectedBot.value?.name || '');
|
||||
|
||||
const filteredAgentBots = computed(() => {
|
||||
const query = searchQuery.value.trim();
|
||||
if (!query) return agentBots.value;
|
||||
return picoSearch(agentBots.value, query, ['name', 'description']);
|
||||
});
|
||||
|
||||
const openAddModal = () => {
|
||||
modalType.value = MODAL_TYPES.CREATE;
|
||||
selectedBot.value = {};
|
||||
@@ -86,87 +100,89 @@ onMounted(() => {
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
v-model:search-query="searchQuery"
|
||||
:title="t('AGENT_BOTS.HEADER')"
|
||||
:description="t('AGENT_BOTS.DESCRIPTION')"
|
||||
:link-text="t('AGENT_BOTS.LEARN_MORE')"
|
||||
:search-placeholder="t('AGENT_BOTS.SEARCH_PLACEHOLDER')"
|
||||
feature-name="agent_bots"
|
||||
>
|
||||
<template #actions>
|
||||
<Button
|
||||
icon="i-lucide-circle-plus"
|
||||
:label="$t('AGENT_BOTS.ADD.TITLE')"
|
||||
@click="openAddModal"
|
||||
/>
|
||||
<Button :label="$t('AGENT_BOTS.ADD.TITLE')" @click="openAddModal" />
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<table class="min-w-full overflow-x-auto divide-y divide-n-strong">
|
||||
<thead>
|
||||
<th
|
||||
v-for="thHeader in tableHeaders"
|
||||
:key="thHeader"
|
||||
class="py-4 font-semibold text-left ltr:pr-4 rtl:pl-4 text-n-slate-11"
|
||||
>
|
||||
{{ thHeader }}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody class="flex-1 divide-y divide-n-weak text-n-slate-12">
|
||||
<tr v-for="bot in agentBots" :key="bot.id">
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
<div class="flex flex-row items-center gap-4">
|
||||
<Avatar
|
||||
:name="bot.name"
|
||||
:src="bot.thumbnail"
|
||||
:size="40"
|
||||
rounded-full
|
||||
/>
|
||||
<div>
|
||||
<span class="block font-medium break-words">
|
||||
{{ bot.name }}
|
||||
<span
|
||||
v-if="bot.system_bot"
|
||||
class="text-xs text-n-slate-12 bg-n-blue-5 inline-block rounded-md py-0.5 px-1 ltr:ml-1 rtl:mr-1"
|
||||
>
|
||||
{{ $t('AGENT_BOTS.GLOBAL_BOT_BADGE') }}
|
||||
<BaseTable
|
||||
:headers="tableHeaders"
|
||||
:items="filteredAgentBots"
|
||||
:no-data-message="
|
||||
searchQuery ? t('AGENT_BOTS.NO_RESULTS') : t('AGENT_BOTS.LIST.404')
|
||||
"
|
||||
>
|
||||
<template #row="{ items }">
|
||||
<BaseTableRow v-for="bot in items" :key="bot.id" :item="bot">
|
||||
<template #default>
|
||||
<BaseTableCell class="max-w-0">
|
||||
<div class="flex items-center gap-4 min-w-0">
|
||||
<Avatar
|
||||
:name="bot.name"
|
||||
:src="bot.thumbnail"
|
||||
:size="40"
|
||||
rounded-full
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-body-main text-n-slate-12 truncate">
|
||||
{{ bot.name }}
|
||||
</span>
|
||||
<span
|
||||
v-if="bot.system_bot"
|
||||
class="text-xs text-n-slate-12 bg-n-blue-5 rounded-md py-0.5 px-1 flex-shrink-0"
|
||||
>
|
||||
{{ $t('AGENT_BOTS.GLOBAL_BOT_BADGE') }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-body-main text-n-slate-11 block truncate">
|
||||
{{ bot.description }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="text-sm text-n-slate-11">
|
||||
{{ bot.description }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4 text-sm">
|
||||
{{ bot.outgoing_url || bot.bot_config?.webhook_url }}
|
||||
</td>
|
||||
<td class="py-4 min-w-xs">
|
||||
<div class="flex gap-1 justify-end">
|
||||
<Button
|
||||
v-if="!bot.system_bot"
|
||||
v-tooltip.top="t('AGENT_BOTS.EDIT.BUTTON_TEXT')"
|
||||
icon="i-lucide-pen"
|
||||
slate
|
||||
xs
|
||||
faded
|
||||
:is-loading="loading[bot.id]"
|
||||
@click="openEditModal(bot)"
|
||||
/>
|
||||
<Button
|
||||
v-if="!bot.system_bot"
|
||||
v-tooltip.top="t('AGENT_BOTS.DELETE.BUTTON_TEXT')"
|
||||
icon="i-lucide-trash-2"
|
||||
xs
|
||||
ruby
|
||||
faded
|
||||
:is-loading="loading[bot.id]"
|
||||
@click="openDeletePopup(bot)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell class="max-w-0">
|
||||
<span class="text-body-main text-n-slate-11 truncate block">
|
||||
{{ bot.outgoing_url || bot.bot_config?.webhook_url }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell align="end" class="w-24">
|
||||
<div class="flex gap-3 justify-end flex-shrink-0">
|
||||
<Button
|
||||
v-if="!bot.system_bot"
|
||||
v-tooltip.top="t('AGENT_BOTS.EDIT.BUTTON_TEXT')"
|
||||
icon="i-woot-edit-pen"
|
||||
slate
|
||||
sm
|
||||
:is-loading="loading[bot.id]"
|
||||
@click="openEditModal(bot)"
|
||||
/>
|
||||
<Button
|
||||
v-if="!bot.system_bot"
|
||||
v-tooltip.top="t('AGENT_BOTS.DELETE.BUTTON_TEXT')"
|
||||
icon="i-woot-bin"
|
||||
slate
|
||||
sm
|
||||
:is-loading="loading[bot.id]"
|
||||
@click="openDeletePopup(bot)"
|
||||
/>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
</template>
|
||||
</BaseTableRow>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</template>
|
||||
|
||||
<AgentBotModal
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useAlert } from 'dashboard/composables';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
import {
|
||||
useStoreGetters,
|
||||
useStore,
|
||||
@@ -25,6 +26,7 @@ const showDeletePopup = ref(false);
|
||||
const showEditPopup = ref(false);
|
||||
const agentAPI = ref({ message: '' });
|
||||
const currentAgent = ref({});
|
||||
const searchQuery = ref('');
|
||||
|
||||
const deleteConfirmText = computed(
|
||||
() => `${t('AGENT_MGMT.DELETE.CONFIRM.YES')} ${currentAgent.value.name}`
|
||||
@@ -37,6 +39,13 @@ const deleteMessage = computed(() => {
|
||||
});
|
||||
|
||||
const agentList = computed(() => getters['agents/getAgents'].value);
|
||||
|
||||
const filteredAgentList = computed(() => {
|
||||
const query = searchQuery.value.trim();
|
||||
if (!query) return agentList.value;
|
||||
return picoSearch(agentList.value, query, ['name', 'email']);
|
||||
});
|
||||
|
||||
const uiFlags = computed(() => getters['agents/getUIFlags'].value);
|
||||
const currentUserId = computed(() => getters.getCurrentUserID.value);
|
||||
const customRoles = useMapGetter('customRole/getCustomRoles');
|
||||
@@ -144,14 +153,15 @@ const confirmDeletion = () => {
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
v-model:search-query="searchQuery"
|
||||
:title="$t('AGENT_MGMT.HEADER')"
|
||||
:description="$t('AGENT_MGMT.DESCRIPTION')"
|
||||
:link-text="$t('AGENT_MGMT.LEARN_MORE')"
|
||||
:search-placeholder="$t('AGENT_MGMT.SEARCH_PLACEHOLDER')"
|
||||
feature-name="agents"
|
||||
>
|
||||
<template #actions>
|
||||
<Button
|
||||
icon="i-lucide-circle-plus"
|
||||
:label="$t('AGENT_MGMT.HEADER_BTN_TXT')"
|
||||
@click="openAddPopup"
|
||||
/>
|
||||
@@ -159,97 +169,105 @@ const confirmDeletion = () => {
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<table class="divide-y divide-n-weak">
|
||||
<tbody class="divide-y divide-n-weak text-n-slate-11">
|
||||
<tr v-for="(agent, index) in agentList" :key="agent.email">
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
<div class="flex flex-row items-center gap-4">
|
||||
<Avatar
|
||||
:src="agent.thumbnail"
|
||||
:name="agent.name"
|
||||
:status="agent.availability_status"
|
||||
:size="40"
|
||||
hide-offline-status
|
||||
rounded-full
|
||||
/>
|
||||
<div>
|
||||
<span class="block font-medium capitalize">
|
||||
{{ agent.name }}
|
||||
</span>
|
||||
<span>{{ agent.email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="relative py-4 ltr:pr-4 rtl:pl-4">
|
||||
<span
|
||||
class="block font-medium w-fit"
|
||||
:class="{
|
||||
'hover:text-gray-900 group cursor-pointer':
|
||||
agent.custom_role_id,
|
||||
}"
|
||||
>
|
||||
{{ getAgentRoleName(agent) }}
|
||||
|
||||
<div
|
||||
class="absolute left-0 z-10 hidden max-w-[300px] w-auto bg-white rounded-xl border border-n-weak shadow-lg top-14 md:top-12 dark:bg-n-solid-2"
|
||||
:class="{ 'group-hover:block': agent.custom_role_id }"
|
||||
<span
|
||||
v-if="!filteredAgentList.length && searchQuery"
|
||||
class="flex-1 flex items-center justify-center py-20 text-center text-body-main !text-base text-n-slate-11"
|
||||
>
|
||||
{{ $t('AGENT_MGMT.NO_RESULTS') }}
|
||||
</span>
|
||||
<div v-else class="divide-y divide-n-weak">
|
||||
<div
|
||||
v-for="(agent, index) in filteredAgentList"
|
||||
:key="agent.email"
|
||||
class="flex justify-between flex-row items-start gap-4 py-4"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<Avatar
|
||||
:src="agent.thumbnail"
|
||||
:name="agent.name"
|
||||
:status="agent.availability_status"
|
||||
:size="40"
|
||||
hide-offline-status
|
||||
/>
|
||||
<div class="flex flex-col gap-1.5 items-start">
|
||||
<span class="block text-heading-3 text-n-slate-12 capitalize">
|
||||
{{ agent.name }}
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-body-main text-n-slate-11">
|
||||
{{ agent.email }}
|
||||
</span>
|
||||
<div class="w-px h-3 bg-n-strong rounded-lg" />
|
||||
<span
|
||||
class="block w-fit text-body-main text-n-slate-11 relative"
|
||||
:class="{
|
||||
'hover:text-n-slate-12 group cursor-pointer':
|
||||
agent.custom_role_id,
|
||||
}"
|
||||
>
|
||||
<div class="flex flex-col gap-1 p-4">
|
||||
<span class="font-semibold">
|
||||
{{ $t('AGENT_MGMT.LIST.AVAILABLE_CUSTOM_ROLE') }}
|
||||
</span>
|
||||
<ul class="pl-4 mb-0 list-disc">
|
||||
<li
|
||||
v-for="permission in getAgentRolePermissions(agent)"
|
||||
:key="permission"
|
||||
class="font-normal"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
`CUSTOM_ROLE.PERMISSIONS.${permission.toUpperCase()}`
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
</ul>
|
||||
{{ getAgentRoleName(agent) }}
|
||||
|
||||
<div
|
||||
class="absolute ltr:left-0 rtl:right-0 z-10 hidden w-[300px] bg-n-alpha-3 backdrop-blur-[100px] rounded-xl outline outline-1 outline-n-container shadow-lg top-14 md:top-12"
|
||||
:class="{ 'group-hover:block': agent.custom_role_id }"
|
||||
>
|
||||
<div class="flex flex-col gap-1 p-4">
|
||||
<span class="text-heading-3 text-n-slate-12">
|
||||
{{ $t('AGENT_MGMT.LIST.AVAILABLE_CUSTOM_ROLE') }}
|
||||
</span>
|
||||
<ul class="ltr:pl-4 rtl:pr-4 mb-0 list-disc">
|
||||
<li
|
||||
v-for="permission in getAgentRolePermissions(agent)"
|
||||
:key="permission"
|
||||
class="text-body-main text-n-slate-11"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
`CUSTOM_ROLE.PERMISSIONS.${permission.toUpperCase()}`
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
<span v-if="agent.confirmed">
|
||||
{{ $t('AGENT_MGMT.LIST.VERIFIED') }}
|
||||
</span>
|
||||
<span v-if="!agent.confirmed">
|
||||
{{ $t('AGENT_MGMT.LIST.VERIFICATION_PENDING') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-4">
|
||||
<div class="flex justify-end gap-1">
|
||||
<Button
|
||||
v-if="showEditAction(agent)"
|
||||
v-tooltip.top="$t('AGENT_MGMT.EDIT.BUTTON_TEXT')"
|
||||
icon="i-lucide-pen"
|
||||
slate
|
||||
xs
|
||||
faded
|
||||
@click="openEditPopup(agent)"
|
||||
/>
|
||||
<Button
|
||||
v-if="showDeleteAction(agent)"
|
||||
v-tooltip.top="$t('AGENT_MGMT.DELETE.BUTTON_TEXT')"
|
||||
icon="i-lucide-trash-2"
|
||||
xs
|
||||
ruby
|
||||
faded
|
||||
:is-loading="loading[agent.id]"
|
||||
@click="openDeletePopup(agent, index)"
|
||||
/>
|
||||
</span>
|
||||
<div class="w-px h-3 bg-n-strong rounded-lg" />
|
||||
<span
|
||||
v-if="agent.confirmed"
|
||||
class="text-body-main text-n-slate-11"
|
||||
>
|
||||
{{ $t('AGENT_MGMT.LIST.VERIFIED') }}
|
||||
</span>
|
||||
<span
|
||||
v-if="!agent.confirmed"
|
||||
class="text-body-main text-n-slate-11"
|
||||
>
|
||||
{{ $t('AGENT_MGMT.LIST.VERIFICATION_PENDING') }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3">
|
||||
<Button
|
||||
v-if="showEditAction(agent)"
|
||||
v-tooltip.top="$t('AGENT_MGMT.EDIT.BUTTON_TEXT')"
|
||||
icon="i-woot-edit-pen"
|
||||
slate
|
||||
sm
|
||||
@click="openEditPopup(agent)"
|
||||
/>
|
||||
<Button
|
||||
v-if="showDeleteAction(agent)"
|
||||
v-tooltip.top="$t('AGENT_MGMT.DELETE.BUTTON_TEXT')"
|
||||
icon="i-woot-bin"
|
||||
slate
|
||||
sm
|
||||
:is-loading="loading[agent.id]"
|
||||
@click="openDeletePopup(agent, index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<woot-modal v-model:show="showAddPopup" :on-close="hideAddPopup">
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
<script setup>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import EditAttribute from './EditAttribute.vue';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
attributeModel: {
|
||||
type: String,
|
||||
default: 'conversation_attribute',
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const showEditPopup = ref(false);
|
||||
const showDeletePopup = ref(false);
|
||||
const selectedAttribute = ref({});
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const store = useStore();
|
||||
|
||||
const attributes = computed(() =>
|
||||
getters['attributes/getAttributesByModel'].value(props.attributeModel)
|
||||
);
|
||||
const uiFlags = computed(() => getters['attributes/getUIFlags'].value);
|
||||
|
||||
const attributeDisplayName = computed(
|
||||
() => selectedAttribute.value.attribute_display_name
|
||||
);
|
||||
const deleteConfirmText = computed(
|
||||
() =>
|
||||
`${t('ATTRIBUTES_MGMT.DELETE.CONFIRM.YES')} ${attributeDisplayName.value}`
|
||||
);
|
||||
const deleteRejectText = computed(() => t('ATTRIBUTES_MGMT.DELETE.CONFIRM.NO'));
|
||||
const confirmDeleteTitle = computed(() =>
|
||||
t('ATTRIBUTES_MGMT.DELETE.CONFIRM.TITLE', {
|
||||
attributeName: attributeDisplayName.value,
|
||||
})
|
||||
);
|
||||
const confirmPlaceHolderText = computed(
|
||||
() =>
|
||||
`${t('ATTRIBUTES_MGMT.DELETE.CONFIRM.PLACE_HOLDER', {
|
||||
attributeName: attributeDisplayName.value,
|
||||
})}`
|
||||
);
|
||||
|
||||
const deleteAttributes = async ({ id }) => {
|
||||
try {
|
||||
await store.dispatch('attributes/delete', id);
|
||||
useAlert(t('ATTRIBUTES_MGMT.DELETE.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.message || t('ATTRIBUTES_MGMT.DELETE.API.ERROR_MESSAGE');
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
};
|
||||
const openEditPopup = response => {
|
||||
showEditPopup.value = true;
|
||||
selectedAttribute.value = response;
|
||||
};
|
||||
const hideEditPopup = () => {
|
||||
showEditPopup.value = false;
|
||||
};
|
||||
|
||||
const closeDelete = () => {
|
||||
showDeletePopup.value = false;
|
||||
selectedAttribute.value = {};
|
||||
};
|
||||
const confirmDeletion = () => {
|
||||
deleteAttributes(selectedAttribute.value);
|
||||
closeDelete();
|
||||
};
|
||||
const openDelete = value => {
|
||||
showDeletePopup.value = true;
|
||||
selectedAttribute.value = value;
|
||||
};
|
||||
|
||||
const tableHeaders = computed(() => {
|
||||
return [
|
||||
t('ATTRIBUTES_MGMT.LIST.TABLE_HEADER.NAME'),
|
||||
t('ATTRIBUTES_MGMT.LIST.TABLE_HEADER.DESCRIPTION'),
|
||||
t('ATTRIBUTES_MGMT.LIST.TABLE_HEADER.TYPE'),
|
||||
t('ATTRIBUTES_MGMT.LIST.TABLE_HEADER.KEY'),
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<table class="min-w-full overflow-x-auto">
|
||||
<thead>
|
||||
<th
|
||||
v-for="tableHeader in tableHeaders"
|
||||
:key="tableHeader"
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 text-left font-semibold text-n-slate-11"
|
||||
>
|
||||
{{ tableHeader }}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-n-weak flex-1 text-n-slate-12">
|
||||
<tr v-for="attribute in attributes" :key="attribute.attribute_key">
|
||||
<td
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ attribute.attribute_display_name }}
|
||||
</td>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
{{ attribute.attribute_description }}
|
||||
</td>
|
||||
<td
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
`ATTRIBUTES_MGMT.ATTRIBUTE_TYPES.${attribute.attribute_display_type?.toUpperCase()}`
|
||||
)
|
||||
}}
|
||||
</td>
|
||||
<td
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 attribute-key overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ attribute.attribute_key }}
|
||||
</td>
|
||||
<td class="py-4 min-w-xs">
|
||||
<div class="flex gap-1 justify-end">
|
||||
<Button
|
||||
v-tooltip.top="$t('ATTRIBUTES_MGMT.LIST.BUTTONS.EDIT')"
|
||||
icon="i-lucide-pen"
|
||||
slate
|
||||
xs
|
||||
faded
|
||||
@click="openEditPopup(attribute)"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.top="$t('ATTRIBUTES_MGMT.LIST.BUTTONS.DELETE')"
|
||||
icon="i-lucide-trash-2"
|
||||
xs
|
||||
ruby
|
||||
faded
|
||||
@click="openDelete(attribute)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<woot-modal v-model:show="showEditPopup" :on-close="hideEditPopup">
|
||||
<EditAttribute
|
||||
:selected-attribute="selectedAttribute"
|
||||
:is-updating="uiFlags.isUpdating"
|
||||
@on-close="hideEditPopup"
|
||||
/>
|
||||
</woot-modal>
|
||||
<woot-confirm-delete-modal
|
||||
v-if="showDeletePopup"
|
||||
v-model:show="showDeletePopup"
|
||||
:title="confirmDeleteTitle"
|
||||
:message="$t('ATTRIBUTES_MGMT.DELETE.CONFIRM.MESSAGE')"
|
||||
:confirm-text="deleteConfirmText"
|
||||
:reject-text="deleteRejectText"
|
||||
:confirm-value="selectedAttribute.attribute_display_name"
|
||||
:confirm-place-holder-text="confirmPlaceHolderText"
|
||||
@on-confirm="confirmDeletion"
|
||||
@on-close="closeDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.attribute-key {
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import AddAttribute from './AddAttribute.vue';
|
||||
import EditAttribute from './EditAttribute.vue';
|
||||
@@ -26,6 +27,7 @@ const inboxes = useMapGetter('inboxes/getInboxes');
|
||||
|
||||
const [showAddPopup, toggleAddPopup] = useToggle(false);
|
||||
const selectedTabIndex = ref(0);
|
||||
const searchQuery = ref('');
|
||||
const uiFlags = computed(() => getters['attributes/getUIFlags'].value);
|
||||
const [showEditPopup, toggleEditPopup] = useToggle(false);
|
||||
const [showDeletePopup, toggleDeletePopup] = useToggle(false);
|
||||
@@ -77,6 +79,7 @@ const attributes = computed(() =>
|
||||
|
||||
const onClickTabChange = tab => {
|
||||
selectedTabIndex.value = tab.key;
|
||||
searchQuery.value = '';
|
||||
};
|
||||
|
||||
const handleEditAttribute = attribute => {
|
||||
@@ -144,6 +147,16 @@ const derivedAttributes = computed(() =>
|
||||
badges: buildBadges(attribute),
|
||||
}))
|
||||
);
|
||||
|
||||
const filteredAttributes = computed(() => {
|
||||
const query = searchQuery.value.trim();
|
||||
if (!query) return derivedAttributes.value;
|
||||
return picoSearch(derivedAttributes.value, query, [
|
||||
'attribute_display_name',
|
||||
'attribute_key',
|
||||
'attribute_description',
|
||||
]);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -153,14 +166,15 @@ const derivedAttributes = computed(() =>
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
v-model:search-query="searchQuery"
|
||||
:title="$t('ATTRIBUTES_MGMT.HEADER')"
|
||||
:description="$t('ATTRIBUTES_MGMT.DESCRIPTION')"
|
||||
:link-text="$t('ATTRIBUTES_MGMT.LEARN_MORE')"
|
||||
:search-placeholder="$t('ATTRIBUTES_MGMT.SEARCH_PLACEHOLDER')"
|
||||
feature-name="custom_attributes"
|
||||
>
|
||||
<template #actions>
|
||||
<Button
|
||||
icon="i-lucide-circle-plus"
|
||||
:label="$t('ATTRIBUTES_MGMT.HEADER_BTN_TXT')"
|
||||
@click="openAddPopup"
|
||||
/>
|
||||
@@ -168,16 +182,21 @@ const derivedAttributes = computed(() =>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<TabBar
|
||||
:tabs="tabsForTabBar"
|
||||
:initial-active-tab="selectedTabIndex"
|
||||
class="max-w-xl"
|
||||
@tab-changed="onClickTabChange"
|
||||
/>
|
||||
<div v-if="derivedAttributes.length" class="grid gap-3">
|
||||
<span
|
||||
v-if="!filteredAttributes.length && searchQuery"
|
||||
class="flex-1 flex items-center justify-center py-20 text-center text-body-main !text-base text-n-slate-11"
|
||||
>
|
||||
{{ $t('ATTRIBUTES_MGMT.NO_RESULTS') }}
|
||||
</span>
|
||||
<div v-else-if="filteredAttributes.length" class="flex flex-col">
|
||||
<AttributeListItem
|
||||
v-for="attribute in derivedAttributes"
|
||||
v-for="attribute in filteredAttributes"
|
||||
:key="attribute.id"
|
||||
:attribute="attribute"
|
||||
:badges="attribute.badges"
|
||||
|
||||
@@ -2,8 +2,14 @@
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { messageTimestamp } from 'shared/helpers/timeHelper';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
import TableFooter from 'dashboard/components/widgets/TableFooter.vue';
|
||||
import {
|
||||
BaseTable,
|
||||
BaseTableRow,
|
||||
BaseTableCell,
|
||||
} from 'dashboard/components-next/table';
|
||||
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import {
|
||||
generateTranslationPayload,
|
||||
generateLogActionKey,
|
||||
@@ -75,63 +81,68 @@ const tableHeaders = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 overflow-auto">
|
||||
<BaseSettingsHeader
|
||||
:title="$t('AUDIT_LOGS.HEADER')"
|
||||
:description="$t('AUDIT_LOGS.DESCRIPTION')"
|
||||
:link-text="$t('AUDIT_LOGS.LEARN_MORE')"
|
||||
feature-name="audit_logs"
|
||||
/>
|
||||
|
||||
<div class="mt-6 flex-1 text-n-slate-11">
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.fetchingList"
|
||||
:message="$t('AUDIT_LOGS.LOADING')"
|
||||
<SettingsLayout
|
||||
:is-loading="uiFlags.fetchingList"
|
||||
:loading-message="$t('AUDIT_LOGS.LOADING')"
|
||||
:no-records-found="!records.length"
|
||||
:no-records-message="$t('AUDIT_LOGS.LIST.404')"
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
:title="$t('AUDIT_LOGS.HEADER')"
|
||||
:description="$t('AUDIT_LOGS.DESCRIPTION')"
|
||||
:link-text="$t('AUDIT_LOGS.LEARN_MORE')"
|
||||
feature-name="audit_logs"
|
||||
/>
|
||||
<p
|
||||
v-else-if="!records.length"
|
||||
class="flex flex-col items-center justify-center h-full text-base p-8"
|
||||
>
|
||||
{{ $t('AUDIT_LOGS.LIST.404') }}
|
||||
</p>
|
||||
<div v-else class="min-w-full overflow-x-auto">
|
||||
<table class="divide-y divide-n-weak">
|
||||
<thead>
|
||||
<th
|
||||
v-for="thHeader in tableHeaders"
|
||||
:key="thHeader"
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 text-left font-semibold text-n-slate-11"
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<BaseTable :headers="tableHeaders" :items="records">
|
||||
<template #row="{ items }">
|
||||
<BaseTableRow
|
||||
v-for="auditLogItem in items"
|
||||
:key="auditLogItem.id"
|
||||
:item="auditLogItem"
|
||||
>
|
||||
{{ thHeader }}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-n-weak text-n-slate-11">
|
||||
<tr v-for="auditLogItem in records" :key="auditLogItem.id">
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4 break-all whitespace-nowrap">
|
||||
{{ generateLogText(auditLogItem) }}
|
||||
</td>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4 break-all whitespace-nowrap">
|
||||
{{
|
||||
messageTimestamp(
|
||||
auditLogItem.created_at,
|
||||
'MMM dd, yyyy hh:mm a'
|
||||
)
|
||||
}}
|
||||
</td>
|
||||
<td class="py-4 w-[8.75rem]">
|
||||
{{ auditLogItem.remote_address }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<TableFooter
|
||||
<template #default>
|
||||
<BaseTableCell>
|
||||
<span
|
||||
class="text-body-main text-n-slate-12 whitespace-nowrap"
|
||||
>
|
||||
{{ generateLogText(auditLogItem) }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell>
|
||||
<span
|
||||
class="text-body-main text-n-slate-11 whitespace-nowrap"
|
||||
>
|
||||
{{
|
||||
messageTimestamp(
|
||||
auditLogItem.created_at,
|
||||
'MMM dd, yyyy hh:mm a'
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell class="w-36">
|
||||
<span class="text-body-main text-n-slate-11">
|
||||
{{ auditLogItem.remote_address }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
</template>
|
||||
</BaseTableRow>
|
||||
</template>
|
||||
</BaseTable>
|
||||
<PaginationFooter
|
||||
:current-page="Number(meta.currentPage)"
|
||||
:total-count="meta.totalEntries"
|
||||
:page-size="meta.perPage"
|
||||
class="border-n-weak border-t !px-0 py-4"
|
||||
@page-change="onPageChange"
|
||||
:total-items="meta.totalEntries"
|
||||
:items-per-page="meta.perPage"
|
||||
class="!px-0"
|
||||
@update:current-page="onPageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
+55
-43
@@ -3,6 +3,7 @@ import { computed } from 'vue';
|
||||
import { messageStamp } from 'shared/helpers/timeHelper';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ToggleSwitch from 'dashboard/components-next/switch/Switch.vue';
|
||||
import { BaseTableRow, BaseTableCell } from 'dashboard/components-next/table';
|
||||
|
||||
const props = defineProps({
|
||||
automation: {
|
||||
@@ -35,47 +36,58 @@ const automationActive = computed({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4 min-w-[200px]">{{ automation.name }}</td>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">{{ automation.description }}</td>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
<ToggleSwitch v-model="automationActive" />
|
||||
</td>
|
||||
<td
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 min-w-[12px]"
|
||||
:title="readableDateWithTime(automation.created_on)"
|
||||
>
|
||||
{{ readableDate(automation.created_on) }}
|
||||
</td>
|
||||
<td class="py-4 min-w-xs">
|
||||
<div class="flex gap-1 justify-end flex-shrink-0">
|
||||
<Button
|
||||
v-tooltip.top="$t('AUTOMATION.FORM.EDIT')"
|
||||
icon="i-lucide-pen"
|
||||
slate
|
||||
xs
|
||||
faded
|
||||
:is-loading="loading"
|
||||
@click="$emit('edit', automation)"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.top="$t('AUTOMATION.CLONE.TOOLTIP')"
|
||||
icon="i-lucide-copy-plus"
|
||||
xs
|
||||
faded
|
||||
:is-loading="loading"
|
||||
@click="$emit('clone', automation)"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.top="$t('AUTOMATION.FORM.DELETE')"
|
||||
:is-loading="loading"
|
||||
icon="i-lucide-trash-2"
|
||||
xs
|
||||
ruby
|
||||
faded
|
||||
@click="$emit('delete', automation)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<BaseTableRow :item="automation">
|
||||
<template #default>
|
||||
<BaseTableCell class="max-w-0 w-full">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span class="text-body-main text-n-slate-12 truncate">
|
||||
{{ automation.name }}
|
||||
</span>
|
||||
<div class="w-px h-3 rounded-lg bg-n-weak flex-shrink-0" />
|
||||
<span class="text-body-main text-n-slate-11 truncate">
|
||||
{{ automation.description }}
|
||||
</span>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell>
|
||||
<ToggleSwitch v-model="automationActive" />
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell :title="readableDateWithTime(automation.created_on)">
|
||||
<span class="text-body-main text-n-slate-12 whitespace-nowrap">
|
||||
{{ readableDate(automation.created_on) }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell align="end">
|
||||
<div class="flex gap-3 justify-end flex-shrink-0">
|
||||
<Button
|
||||
v-tooltip.top="$t('AUTOMATION.FORM.EDIT')"
|
||||
icon="i-woot-edit-pen"
|
||||
slate
|
||||
sm
|
||||
:is-loading="loading"
|
||||
@click="$emit('edit', automation)"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.top="$t('AUTOMATION.FORM.DELETE')"
|
||||
:is-loading="loading"
|
||||
icon="i-woot-bin"
|
||||
slate
|
||||
sm
|
||||
@click="$emit('delete', automation)"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.top="$t('AUTOMATION.CLONE.TOOLTIP')"
|
||||
icon="i-woot-clone"
|
||||
sm
|
||||
slate
|
||||
:is-loading="loading"
|
||||
@click="$emit('clone', automation)"
|
||||
/>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
</template>
|
||||
</BaseTableRow>
|
||||
</template>
|
||||
|
||||
@@ -7,8 +7,10 @@ import SettingsLayout from '../SettingsLayout.vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
import AutomationRuleRow from './AutomationRuleRow.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { BaseTable } from 'dashboard/components-next/table';
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const store = useStore();
|
||||
@@ -20,12 +22,19 @@ const showAddPopup = ref(false);
|
||||
const showEditPopup = ref(false);
|
||||
const showDeleteConfirmationPopup = ref(false);
|
||||
const selectedAutomation = ref({});
|
||||
const searchQuery = ref('');
|
||||
const toggleModalTitle = ref(t('AUTOMATION.TOGGLE.ACTIVATION_TITLE'));
|
||||
const toggleModalDescription = ref(
|
||||
t('AUTOMATION.TOGGLE.ACTIVATION_DESCRIPTION')
|
||||
);
|
||||
|
||||
const records = computed(() => getters['automations/getAutomations'].value);
|
||||
|
||||
const filteredRecords = computed(() => {
|
||||
const query = searchQuery.value.trim();
|
||||
if (!query) return records.value;
|
||||
return picoSearch(records.value, query, ['name', 'description']);
|
||||
});
|
||||
const uiFlags = computed(() => getters['automations/getUIFlags'].value);
|
||||
const accountId = computed(() => getters.getCurrentAccountId.value);
|
||||
|
||||
@@ -165,9 +174,9 @@ const toggleAutomation = async ({ id, name, status }) => {
|
||||
const tableHeaders = computed(() => {
|
||||
return [
|
||||
t('AUTOMATION.LIST.TABLE_HEADER.NAME'),
|
||||
t('AUTOMATION.LIST.TABLE_HEADER.DESCRIPTION'),
|
||||
t('AUTOMATION.LIST.TABLE_HEADER.ACTIVE'),
|
||||
t('AUTOMATION.LIST.TABLE_HEADER.CREATED_ON'),
|
||||
t('AUTOMATION.LIST.TABLE_HEADER.ACTIONS'),
|
||||
];
|
||||
});
|
||||
</script>
|
||||
@@ -181,14 +190,15 @@ const tableHeaders = computed(() => {
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
v-model:search-query="searchQuery"
|
||||
:title="$t('AUTOMATION.HEADER')"
|
||||
:description="$t('AUTOMATION.DESCRIPTION')"
|
||||
:link-text="$t('AUTOMATION.LEARN_MORE')"
|
||||
:search-placeholder="$t('AUTOMATION.SEARCH_PLACEHOLDER')"
|
||||
feature-name="automation"
|
||||
>
|
||||
<template #actions>
|
||||
<Button
|
||||
icon="i-lucide-circle-plus"
|
||||
:label="$t('AUTOMATION.HEADER_BTN_TXT')"
|
||||
@click="openAddPopup"
|
||||
/>
|
||||
@@ -196,19 +206,16 @@ const tableHeaders = computed(() => {
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<table class="min-w-full divide-y divide-n-weak">
|
||||
<thead>
|
||||
<th
|
||||
v-for="thHeader in tableHeaders"
|
||||
:key="thHeader"
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 rtl:text-right ltr:text-left font-semibold text-n-slate-11"
|
||||
>
|
||||
{{ thHeader }}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-n-weak text-n-slate-11">
|
||||
<BaseTable
|
||||
:headers="tableHeaders"
|
||||
:items="filteredRecords"
|
||||
:no-data-message="
|
||||
searchQuery ? $t('AUTOMATION.NO_RESULTS') : $t('AUTOMATION.LIST.404')
|
||||
"
|
||||
>
|
||||
<template #row="{ items }">
|
||||
<AutomationRuleRow
|
||||
v-for="automation in records"
|
||||
v-for="automation in items"
|
||||
:key="automation.id"
|
||||
:automation="automation"
|
||||
:loading="loading[automation.id]"
|
||||
@@ -217,8 +224,8 @@ const tableHeaders = computed(() => {
|
||||
@edit="openEditPopup"
|
||||
@delete="openDeletePopup"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</template>
|
||||
|
||||
<woot-modal
|
||||
|
||||
@@ -2,13 +2,21 @@
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import AddCanned from './AddCanned.vue';
|
||||
import EditCanned from './EditCanned.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import { computed, onMounted, ref, defineOptions } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import {
|
||||
BaseTable,
|
||||
BaseTableRow,
|
||||
BaseTableCell,
|
||||
} from 'dashboard/components-next/table';
|
||||
|
||||
defineOptions({
|
||||
name: 'CannedResponseSettings',
|
||||
@@ -28,9 +36,17 @@ const activeResponse = ref({});
|
||||
const cannedResponseAPI = ref({ message: '' });
|
||||
|
||||
const sortOrder = ref('asc');
|
||||
const searchQuery = ref('');
|
||||
|
||||
const records = computed(() =>
|
||||
getters.getSortedCannedResponses.value(sortOrder.value)
|
||||
);
|
||||
|
||||
const filteredRecords = computed(() => {
|
||||
const query = searchQuery.value.trim();
|
||||
if (!query) return records.value;
|
||||
return picoSearch(records.value, query, ['short_code', 'content']);
|
||||
});
|
||||
const uiFlags = computed(() => getters.getUIFlags.value);
|
||||
|
||||
const deleteConfirmText = computed(
|
||||
@@ -114,103 +130,112 @@ const confirmDeletion = () => {
|
||||
const tableHeaders = computed(() => {
|
||||
return [
|
||||
t('CANNED_MGMT.LIST.TABLE_HEADER.SHORT_CODE'),
|
||||
t('CANNED_MGMT.LIST.TABLE_HEADER.CONTENT'),
|
||||
t('CANNED_MGMT.LIST.TABLE_HEADER.ACTIONS'),
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 overflow-auto">
|
||||
<BaseSettingsHeader
|
||||
:title="$t('CANNED_MGMT.HEADER')"
|
||||
:description="$t('CANNED_MGMT.DESCRIPTION')"
|
||||
:link-text="$t('CANNED_MGMT.LEARN_MORE')"
|
||||
feature-name="canned_responses"
|
||||
>
|
||||
<template #actions>
|
||||
<Button
|
||||
icon="i-lucide-circle-plus"
|
||||
:label="$t('CANNED_MGMT.HEADER_BTN_TXT')"
|
||||
@click="openAddPopup"
|
||||
/>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
|
||||
<div class="mt-6 flex-1">
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.fetchingList"
|
||||
:message="$t('CANNED_MGMT.LOADING')"
|
||||
/>
|
||||
<p
|
||||
v-else-if="!records.length"
|
||||
class="flex flex-col items-center justify-center h-full text-base text-n-slate-11 py-8"
|
||||
<SettingsLayout
|
||||
:is-loading="uiFlags.fetchingList"
|
||||
:loading-message="$t('CANNED_MGMT.LOADING')"
|
||||
:no-records-found="!records.length"
|
||||
:no-records-message="$t('CANNED_MGMT.LIST.404')"
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
v-model:search-query="searchQuery"
|
||||
:title="$t('CANNED_MGMT.HEADER')"
|
||||
:description="$t('CANNED_MGMT.DESCRIPTION')"
|
||||
:link-text="$t('CANNED_MGMT.LEARN_MORE')"
|
||||
:search-placeholder="$t('CANNED_MGMT.SEARCH_PLACEHOLDER')"
|
||||
feature-name="canned_responses"
|
||||
>
|
||||
{{ $t('CANNED_MGMT.LIST.404') }}
|
||||
</p>
|
||||
<table v-else class="min-w-full overflow-x-auto divide-y divide-n-weak">
|
||||
<thead>
|
||||
<th
|
||||
v-for="thHeader in tableHeaders"
|
||||
:key="thHeader"
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 text-left font-semibold text-n-slate-11 last:text-right"
|
||||
>
|
||||
<span v-if="thHeader !== tableHeaders[0]">
|
||||
{{ thHeader }}
|
||||
</span>
|
||||
<button
|
||||
v-else
|
||||
class="flex items-center p-0 cursor-pointer"
|
||||
@click="toggleSort"
|
||||
>
|
||||
<span class="mb-0">
|
||||
{{ thHeader }}
|
||||
</span>
|
||||
<fluent-icon
|
||||
class="ml-2 size-4"
|
||||
:icon="sortOrder === 'desc' ? 'chevron-up' : 'chevron-down'"
|
||||
/>
|
||||
</button>
|
||||
</th>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-n-weak text-n-slate-11">
|
||||
<tr
|
||||
v-for="(cannedItem, index) in records"
|
||||
:key="cannedItem.short_code"
|
||||
>
|
||||
<td
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 truncate max-w-xs font-medium"
|
||||
:title="cannedItem.short_code"
|
||||
>
|
||||
{{ cannedItem.short_code }}
|
||||
</td>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4 md:break-all whitespace-normal">
|
||||
{{ getPlainText(cannedItem.content) }}
|
||||
</td>
|
||||
<td class="py-4 flex justify-end gap-1">
|
||||
<Button
|
||||
v-tooltip.top="$t('CANNED_MGMT.EDIT.BUTTON_TEXT')"
|
||||
icon="i-lucide-pen"
|
||||
slate
|
||||
xs
|
||||
faded
|
||||
@click="openEditPopup(cannedItem)"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.top="$t('CANNED_MGMT.DELETE.BUTTON_TEXT')"
|
||||
icon="i-lucide-trash-2"
|
||||
xs
|
||||
ruby
|
||||
faded
|
||||
:is-loading="loading[cannedItem.id]"
|
||||
@click="openDeletePopup(cannedItem, index)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<template #actions>
|
||||
<Button
|
||||
:label="$t('CANNED_MGMT.HEADER_BTN_TXT')"
|
||||
@click="openAddPopup"
|
||||
/>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<BaseTable
|
||||
:headers="tableHeaders"
|
||||
:items="filteredRecords"
|
||||
:no-data-message="
|
||||
!records.length
|
||||
? $t('CANNED_MGMT.LIST.404')
|
||||
: searchQuery
|
||||
? $t('CANNED_MGMT.NO_RESULTS')
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<template #header-0>
|
||||
<button
|
||||
class="flex items-center gap-2 p-0 cursor-pointer"
|
||||
@click="toggleSort"
|
||||
>
|
||||
<span class="mb-0">
|
||||
{{ tableHeaders[0] }}
|
||||
</span>
|
||||
<Icon
|
||||
class="size-5 text-n-slate-11 flex-shrink-0"
|
||||
:icon="
|
||||
sortOrder === 'desc'
|
||||
? 'i-woot-sort-descending'
|
||||
: 'i-woot-sort-ascending'
|
||||
"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
<template #header-1>
|
||||
{{ tableHeaders[1] }}
|
||||
</template>
|
||||
|
||||
<template #row="{ items }">
|
||||
<BaseTableRow
|
||||
v-for="cannedItem in items"
|
||||
:key="cannedItem.short_code"
|
||||
:item="cannedItem"
|
||||
>
|
||||
<template #default>
|
||||
<BaseTableCell class="max-w-0">
|
||||
<div class="flex flex-col gap-2 min-w-0">
|
||||
<span class="text-heading-3 text-n-slate-12 truncate block">
|
||||
{{ cannedItem.short_code }}
|
||||
</span>
|
||||
<p class="text-body-main text-n-slate-11 line-clamp-5">
|
||||
{{ getPlainText(cannedItem.content) }}
|
||||
</p>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell align="end" class="w-24">
|
||||
<div class="flex gap-3 justify-end flex-shrink-0">
|
||||
<Button
|
||||
v-tooltip.top="$t('CANNED_MGMT.EDIT.BUTTON_TEXT')"
|
||||
icon="i-woot-edit-pen"
|
||||
slate
|
||||
sm
|
||||
@click="openEditPopup(cannedItem)"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.top="$t('CANNED_MGMT.DELETE.BUTTON_TEXT')"
|
||||
icon="i-woot-bin"
|
||||
slate
|
||||
sm
|
||||
:is-loading="loading[cannedItem.id]"
|
||||
@click="openDeletePopup(cannedItem)"
|
||||
/>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
</template>
|
||||
</BaseTableRow>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</template>
|
||||
<woot-modal v-model:show="showAddPopup" :on-close="hideAddPopup">
|
||||
<AddCanned :on-close="hideAddPopup" />
|
||||
</woot-modal>
|
||||
@@ -235,5 +260,5 @@ const tableHeaders = computed(() => {
|
||||
:confirm-text="deleteConfirmText"
|
||||
:reject-text="deleteRejectText"
|
||||
/>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
+46
-55
@@ -1,9 +1,10 @@
|
||||
<script setup>
|
||||
import { computed, useSlots } from 'vue';
|
||||
import CustomBrandPolicyWrapper from 'dashboard/components/CustomBrandPolicyWrapper.vue';
|
||||
import { getHelpUrlForFeature } from '../../../../helper/featureHelper';
|
||||
import BackButton from '../../../../components/widgets/BackButton.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
@@ -11,10 +12,6 @@ const props = defineProps({
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
iconName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
@@ -30,52 +27,66 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
searchPlaceholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const helpURL = getHelpUrlForFeature(props.featureName);
|
||||
const slots = useSlots();
|
||||
|
||||
const openInNewTab = url => {
|
||||
if (!url) return;
|
||||
window.open(url, '_blank', 'noopener noreferrer');
|
||||
};
|
||||
const searchQuery = defineModel('searchQuery', { type: String, default: '' });
|
||||
|
||||
const hasHeaderActions = computed(
|
||||
() => props.searchPlaceholder || slots.actions
|
||||
);
|
||||
|
||||
const helpURL = getHelpUrlForFeature(props.featureName);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-start w-full gap-2">
|
||||
<div class="flex flex-col items-start w-full">
|
||||
<BackButton
|
||||
v-if="backButtonLabel"
|
||||
compact
|
||||
:button-label="backButtonLabel"
|
||||
class="my-1"
|
||||
/>
|
||||
<div class="flex items-center justify-between w-full gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
v-if="iconName"
|
||||
class="flex items-center w-10 h-10 p-1 rounded-full bg-n-blue-2"
|
||||
<div
|
||||
v-if="title"
|
||||
class="flex items-center justify-between w-full gap-4 min-h-10"
|
||||
:class="!hasHeaderActions ? 'mb-1' : 'mb-3'"
|
||||
>
|
||||
<h1 class="text-heading-1 text-n-slate-12">
|
||||
{{ title }}
|
||||
</h1>
|
||||
<div v-if="searchPlaceholder || slots.actions" class="gap-3 flex">
|
||||
<Input
|
||||
v-if="searchPlaceholder"
|
||||
v-model="searchQuery"
|
||||
:placeholder="searchPlaceholder"
|
||||
autofocus
|
||||
class="w-56 min-w-0 hidden sm:flex [&>input]:ltr:!pl-8 [&>input]:rtl:!pr-8 [&>input]:!rounded-[0.625rem]"
|
||||
size="md"
|
||||
type="search"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center w-full h-full rounded-full bg-n-blue-3"
|
||||
>
|
||||
<fluent-icon
|
||||
size="14"
|
||||
:icon="iconName"
|
||||
type="outline"
|
||||
class="flex-shrink-0 text-n-brand"
|
||||
<template #prefix>
|
||||
<Icon
|
||||
icon="i-lucide-search"
|
||||
class="absolute -translate-y-1/2 text-n-slate-11 size-4 top-1/2 ltr:left-2.5 rtl:right-2.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-xl font-medium tracking-tight text-n-slate-12">
|
||||
{{ title }}
|
||||
</h1>
|
||||
</div>
|
||||
<!-- Slot for additional actions on larger screens -->
|
||||
<div class="hidden gap-2 sm:flex">
|
||||
</template>
|
||||
</Input>
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col w-full gap-3 text-n-slate-11">
|
||||
<div
|
||||
v-if="description || $slots.description || linkText || helpURL"
|
||||
class="flex flex-col w-full gap-2 text-n-slate-11 my-3"
|
||||
>
|
||||
<p
|
||||
class="mb-0 text-sm font-normal line-clamp-5 sm:line-clamp-none max-w-3xl"
|
||||
v-if="description || $slots.description"
|
||||
class="mb-0 line-clamp-5 sm:line-clamp-none max-w-3xl text-body-main"
|
||||
>
|
||||
<slot name="description">{{ description }}</slot>
|
||||
</p>
|
||||
@@ -85,31 +96,11 @@ const openInNewTab = url => {
|
||||
:href="helpURL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="items-center hidden gap-1 text-sm font-medium sm:inline-flex w-fit text-n-blue-text hover:underline"
|
||||
class="items-center gap-1 text-label inline-flex w-fit text-n-blue-11 hover:underline"
|
||||
>
|
||||
{{ linkText }}
|
||||
<Icon
|
||||
icon="i-lucide-chevron-right"
|
||||
class="flex-shrink-0 text-n-blue-text size-4"
|
||||
/>
|
||||
</a>
|
||||
</CustomBrandPolicyWrapper>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-wrap items-start justify-start w-full gap-3 sm:hidden"
|
||||
>
|
||||
<slot name="actions" />
|
||||
<CustomBrandPolicyWrapper :show-on-custom-branded-instance="false">
|
||||
<Button
|
||||
v-if="helpURL && linkText"
|
||||
blue
|
||||
link
|
||||
icon="i-lucide-chevron-right"
|
||||
trailing-icon
|
||||
:label="linkText"
|
||||
@click="openInNewTab(helpURL)"
|
||||
/>
|
||||
</CustomBrandPolicyWrapper>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,6 +9,8 @@ import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
import { BaseTable } from 'dashboard/components-next/table';
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
@@ -19,8 +21,15 @@ const selectedRole = ref(null);
|
||||
const loading = ref({});
|
||||
const showDeleteConfirmationPopup = ref(false);
|
||||
const activeResponse = ref({});
|
||||
const searchQuery = ref('');
|
||||
|
||||
const records = useMapGetter('customRole/getCustomRoles');
|
||||
|
||||
const filteredRecords = computed(() => {
|
||||
const query = searchQuery.value.trim();
|
||||
if (!query) return records.value;
|
||||
return picoSearch(records.value, query, ['name', 'description']);
|
||||
});
|
||||
const uiFlags = useMapGetter('customRole/getUIFlags');
|
||||
|
||||
const deleteConfirmText = computed(
|
||||
@@ -129,14 +138,15 @@ const confirmDeletion = () => {
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
v-model:search-query="searchQuery"
|
||||
:title="$t('CUSTOM_ROLE.HEADER')"
|
||||
:description="$t('CUSTOM_ROLE.DESCRIPTION')"
|
||||
:link-text="$t('CUSTOM_ROLE.LEARN_MORE')"
|
||||
:search-placeholder="$t('CUSTOM_ROLE.SEARCH_PLACEHOLDER')"
|
||||
feature-name="canned_responses"
|
||||
>
|
||||
<template #actions>
|
||||
<Button
|
||||
icon="i-lucide-circle-plus"
|
||||
:label="$t('CUSTOM_ROLE.HEADER_BTN_TXT')"
|
||||
:disabled="isBehindAPaywall"
|
||||
@click="openAddModal"
|
||||
@@ -147,26 +157,25 @@ const confirmDeletion = () => {
|
||||
|
||||
<template #body>
|
||||
<CustomRolePaywall v-if="isBehindAPaywall" />
|
||||
<table v-else class="min-w-full overflow-x-auto divide-y divide-n-weak">
|
||||
<thead>
|
||||
<th
|
||||
v-for="thHeader in tableHeaders"
|
||||
:key="thHeader"
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 font-semibold text-left text-n-slate-11"
|
||||
>
|
||||
<span class="mb-0">
|
||||
{{ thHeader }}
|
||||
</span>
|
||||
</th>
|
||||
</thead>
|
||||
|
||||
<CustomRoleTableBody
|
||||
:roles="records"
|
||||
:loading="loading"
|
||||
@edit="openEditModal"
|
||||
@delete="openDeletePopup"
|
||||
/>
|
||||
</table>
|
||||
<BaseTable
|
||||
v-else
|
||||
:headers="tableHeaders"
|
||||
:items="filteredRecords"
|
||||
:no-data-message="
|
||||
searchQuery
|
||||
? $t('CUSTOM_ROLE.NO_RESULTS')
|
||||
: $t('CUSTOM_ROLE.LIST.404')
|
||||
"
|
||||
>
|
||||
<template #row="{ items }">
|
||||
<CustomRoleTableBody
|
||||
:roles="items"
|
||||
:loading="loading"
|
||||
@edit="openEditModal"
|
||||
@delete="openDeletePopup"
|
||||
/>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</template>
|
||||
|
||||
<woot-modal
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ const tableHeaders = computed(() => {
|
||||
<th
|
||||
v-for="thHeader in tableHeaders"
|
||||
:key="thHeader"
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 font-semibold text-left text-n-slate-11"
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 text-start text-heading-3 text-n-slate-12"
|
||||
>
|
||||
<span class="mb-0">
|
||||
{{ thHeader }}
|
||||
|
||||
+46
-39
@@ -3,6 +3,7 @@ import { useI18n } from 'vue-i18n';
|
||||
import { getI18nKey } from 'dashboard/routes/dashboard/settings/helper/settingsHelper';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { BaseTableRow, BaseTableCell } from 'dashboard/components-next/table';
|
||||
|
||||
defineProps({
|
||||
roles: {
|
||||
@@ -27,43 +28,49 @@ const getFormattedPermissions = role => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tbody class="divide-y divide-n-weak text-n-slate-11">
|
||||
<tr v-for="(customRole, index) in roles" :key="index">
|
||||
<td
|
||||
class="max-w-xs py-4 ltr:pr-4 rtl:pl-4 font-medium truncate align-baseline"
|
||||
:title="customRole.name"
|
||||
>
|
||||
{{ customRole.name }}
|
||||
</td>
|
||||
<td
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 whitespace-normal align-baseline md:break-words"
|
||||
>
|
||||
{{ customRole.description }}
|
||||
</td>
|
||||
<td
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 whitespace-normal align-baseline md:break-words"
|
||||
>
|
||||
{{ getFormattedPermissions(customRole) }}
|
||||
</td>
|
||||
<td class="flex justify-end gap-1 py-4">
|
||||
<Button
|
||||
v-tooltip.top="$t('CUSTOM_ROLE.EDIT.BUTTON_TEXT')"
|
||||
icon="i-lucide-pen"
|
||||
slate
|
||||
xs
|
||||
faded
|
||||
@click="emit('edit', customRole)"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.top="$t('CUSTOM_ROLE.DELETE.BUTTON_TEXT')"
|
||||
icon="i-lucide-trash-2"
|
||||
xs
|
||||
ruby
|
||||
faded
|
||||
:is-loading="loading[customRole.id]"
|
||||
@click="emit('delete', customRole)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<BaseTableRow
|
||||
v-for="customRole in roles"
|
||||
:key="customRole.id"
|
||||
:item="customRole"
|
||||
>
|
||||
<template #default>
|
||||
<BaseTableCell>
|
||||
<span class="text-body-main text-n-slate-12 truncate block">
|
||||
{{ customRole.name }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell>
|
||||
<span class="text-body-main text-n-slate-11 truncate block">
|
||||
{{ customRole.description }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell>
|
||||
<span class="text-body-main text-n-slate-11 block">
|
||||
{{ getFormattedPermissions(customRole) }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell align="end" class="w-24">
|
||||
<div class="flex gap-3 justify-end flex-shrink-0">
|
||||
<Button
|
||||
v-tooltip.top="$t('CUSTOM_ROLE.EDIT.BUTTON_TEXT')"
|
||||
icon="i-woot-edit-pen"
|
||||
slate
|
||||
sm
|
||||
@click="emit('edit', customRole)"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.top="$t('CUSTOM_ROLE.DELETE.BUTTON_TEXT')"
|
||||
icon="i-woot-bin"
|
||||
slate
|
||||
sm
|
||||
:is-loading="loading[customRole.id]"
|
||||
@click="emit('delete', customRole)"
|
||||
/>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
</template>
|
||||
</BaseTableRow>
|
||||
</template>
|
||||
|
||||
@@ -95,7 +95,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-8">
|
||||
<div class="mx-6">
|
||||
<SettingsSection
|
||||
:title="$t('INBOX_MGMT.IMAP.TITLE')"
|
||||
:sub-title="$t('INBOX_MGMT.IMAP.SUBTITLE')"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
@@ -22,6 +23,7 @@ const { isAdmin } = useAdmin();
|
||||
|
||||
const showDeletePopup = ref(false);
|
||||
const selectedInbox = ref({});
|
||||
const searchQuery = ref('');
|
||||
|
||||
const inboxes = useMapGetter('inboxes/getInboxes');
|
||||
|
||||
@@ -29,6 +31,12 @@ const inboxesList = computed(() => {
|
||||
return inboxes.value?.slice().sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
const filteredInboxesList = computed(() => {
|
||||
const query = searchQuery.value.trim();
|
||||
if (!query) return inboxesList.value;
|
||||
return picoSearch(inboxesList.value, query, ['name', 'channel_type']);
|
||||
});
|
||||
|
||||
const uiFlags = computed(() => getters['inboxes/getUIFlags'].value);
|
||||
|
||||
const deleteConfirmText = computed(
|
||||
@@ -80,88 +88,88 @@ const openDelete = inbox => {
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
v-model:search-query="searchQuery"
|
||||
:title="$t('INBOX_MGMT.HEADER')"
|
||||
:description="$t('INBOX_MGMT.DESCRIPTION')"
|
||||
:link-text="$t('INBOX_MGMT.LEARN_MORE')"
|
||||
:search-placeholder="$t('INBOX_MGMT.SEARCH_PLACEHOLDER')"
|
||||
feature-name="inboxes"
|
||||
>
|
||||
<template #actions>
|
||||
<router-link v-if="isAdmin" :to="{ name: 'settings_inbox_new' }">
|
||||
<Button
|
||||
icon="i-lucide-circle-plus"
|
||||
:label="$t('SETTINGS.INBOXES.NEW_INBOX')"
|
||||
/>
|
||||
<Button :label="$t('SETTINGS.INBOXES.NEW_INBOX')" />
|
||||
</router-link>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<table class="min-w-full overflow-x-auto">
|
||||
<tbody class="divide-y divide-n-weak flex-1 text-n-slate-12">
|
||||
<tr v-for="inbox in inboxesList" :key="inbox.id">
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
<div class="flex items-center flex-row gap-4">
|
||||
<div
|
||||
v-if="inbox.avatar_url"
|
||||
class="bg-n-alpha-3 rounded-full size-10 ring ring-n-solid-1 border border-n-strong shadow-sm grid place-items-center"
|
||||
>
|
||||
<Avatar
|
||||
:src="inbox.avatar_url"
|
||||
:name="inbox.name"
|
||||
:size="24"
|
||||
rounded-full
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="size-10 justify-center bg-n-alpha-3 rounded-full ring ring-n-solid-1 border border-n-strong shadow-sm grid place-items-center"
|
||||
>
|
||||
<ChannelIcon class="size-6 text-n-slate-10" :inbox="inbox" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="block font-medium capitalize">
|
||||
{{ inbox.name }}
|
||||
</span>
|
||||
<ChannelName
|
||||
:channel-type="inbox.channel_type"
|
||||
:medium="inbox.medium"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="py-4">
|
||||
<div class="flex gap-1 justify-end">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'settings_inbox_show',
|
||||
params: { inboxId: inbox.id },
|
||||
}"
|
||||
>
|
||||
<Button
|
||||
v-if="isAdmin"
|
||||
v-tooltip.top="$t('INBOX_MGMT.SETTINGS')"
|
||||
icon="i-lucide-settings"
|
||||
slate
|
||||
xs
|
||||
faded
|
||||
/>
|
||||
</router-link>
|
||||
<Button
|
||||
v-if="isAdmin"
|
||||
v-tooltip.top="$t('INBOX_MGMT.DELETE.BUTTON_TEXT')"
|
||||
icon="i-lucide-trash-2"
|
||||
xs
|
||||
ruby
|
||||
faded
|
||||
@click="openDelete(inbox)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<span
|
||||
v-if="!filteredInboxesList.length && searchQuery"
|
||||
class="flex-1 flex items-center justify-center py-20 text-center text-body-main !text-base text-n-slate-11"
|
||||
>
|
||||
{{ $t('INBOX_MGMT.NO_RESULTS') }}
|
||||
</span>
|
||||
<div v-else class="divide-y divide-n-weak">
|
||||
<div
|
||||
v-for="inbox in filteredInboxesList"
|
||||
:key="inbox.id"
|
||||
class="flex justify-between flex-row items-start gap-4 py-4"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
v-if="inbox.avatar_url"
|
||||
class="bg-n-alpha-3 rounded-xl size-10 ring ring-n-solid-1 border border-n-strong shadow-sm grid place-items-center"
|
||||
>
|
||||
<Avatar
|
||||
:src="inbox.avatar_url"
|
||||
:name="inbox.name"
|
||||
:size="24"
|
||||
rounded-full
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="size-10 justify-center bg-n-alpha-3 rounded-xl ring ring-n-solid-1 border border-n-strong shadow-sm grid place-items-center"
|
||||
>
|
||||
<ChannelIcon class="size-6 text-n-slate-10" :inbox="inbox" />
|
||||
</div>
|
||||
<div class="flex flex-col items-start gap-1">
|
||||
<span class="block text-heading-3 text-n-slate-12 capitalize">
|
||||
{{ inbox.name }}
|
||||
</span>
|
||||
<ChannelName
|
||||
:channel-type="inbox.channel_type"
|
||||
:medium="inbox.medium"
|
||||
class="text-body-main text-n-slate-11"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 justify-end">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'settings_inbox_show',
|
||||
params: { inboxId: inbox.id },
|
||||
}"
|
||||
>
|
||||
<Button
|
||||
v-if="isAdmin"
|
||||
v-tooltip.top="$t('INBOX_MGMT.SETTINGS')"
|
||||
icon="i-woot-settings"
|
||||
slate
|
||||
sm
|
||||
/>
|
||||
</router-link>
|
||||
<Button
|
||||
v-if="isAdmin"
|
||||
v-tooltip.top="$t('INBOX_MGMT.DELETE.BUTTON_TEXT')"
|
||||
icon="i-woot-bin"
|
||||
slate
|
||||
sm
|
||||
@click="openDelete(inbox)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<woot-confirm-delete-modal
|
||||
|
||||
@@ -96,7 +96,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-8 my-2 text-base">
|
||||
<div class="mx-6 my-2 text-base">
|
||||
<div class="mx-0 mt-6 mb-3">
|
||||
{{ $t('INBOX_MGMT.PRE_CHAT_FORM.DESCRIPTION') }}
|
||||
</div>
|
||||
|
||||
@@ -509,7 +509,7 @@ export default {
|
||||
:header-title="inboxName"
|
||||
>
|
||||
<woot-tabs
|
||||
class="[&_ul]:p-0"
|
||||
class="[&_ul]:p-0 top-px relative"
|
||||
:index="selectedTabIndex"
|
||||
:border="false"
|
||||
@change="onTabChange"
|
||||
@@ -540,7 +540,7 @@ export default {
|
||||
:content="$t('INBOX_MGMT.ADD.INSTAGRAM.DUPLICATE_INBOX_BANNER')"
|
||||
class="mx-8 mt-5"
|
||||
/>
|
||||
<div v-if="selectedTabKey === 'inbox-settings'" class="mx-8">
|
||||
<div v-if="selectedTabKey === 'inbox-settings'" class="mx-6">
|
||||
<SettingsSection
|
||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_UPDATE_TITLE')"
|
||||
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_UPDATE_SUB_TEXT')"
|
||||
@@ -923,7 +923,7 @@ export default {
|
||||
</SettingsSection>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedTabKey === 'collaborators'" class="mx-8">
|
||||
<div v-if="selectedTabKey === 'collaborators'" class="mx-6">
|
||||
<CollaboratorsPage :inbox="inbox" />
|
||||
</div>
|
||||
<div v-if="selectedTabKey === 'configuration'">
|
||||
|
||||
@@ -155,7 +155,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-8">
|
||||
<div class="mx-6">
|
||||
<SettingsSection
|
||||
:title="$t('INBOX_MGMT.SMTP.TITLE')"
|
||||
:sub-title="$t('INBOX_MGMT.SMTP.SUBTITLE')"
|
||||
|
||||
@@ -271,8 +271,8 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-8">
|
||||
<div class="flex p-2.5">
|
||||
<div class="mx-6">
|
||||
<div class="flex py-6">
|
||||
<div class="w-100 lg:w-[40%]">
|
||||
<div class="min-h-full py-4 overflow-y-scroll px-px">
|
||||
<form @submit.prevent="updateWidget">
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ const getStatusTextColor = status => STATUS_COLORS[status] || 'text-n-slate-12';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gap-4 pt-8 mx-8">
|
||||
<div class="gap-4 pt-8 mx-6">
|
||||
<div
|
||||
class="px-5 py-5 space-y-6 rounded-xl border shadow-sm border-n-weak bg-n-solid-2"
|
||||
>
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-8">
|
||||
<div class="mx-6">
|
||||
<LoadingState v-if="uiFlags.isFetching || uiFlags.isFetchingAgentBot" />
|
||||
<form
|
||||
v-else
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-8">
|
||||
<div class="mx-6">
|
||||
<SettingsSection
|
||||
:title="$t('INBOX_MGMT.BUSINESS_HOURS.TITLE')"
|
||||
:sub-title="$t('INBOX_MGMT.BUSINESS_HOURS.SUBTITLE')"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { frontendURL } from '../../../../helper/URLHelper';
|
||||
import ChannelFactory from './ChannelFactory.vue';
|
||||
|
||||
import SettingsContent from '../Wrapper.vue';
|
||||
import SettingWrapper from '../SettingsWrapper.vue';
|
||||
import SettingsWrapper from '../SettingsWrapper.vue';
|
||||
import InboxHome from './Index.vue';
|
||||
import Settings from './Settings.vue';
|
||||
import InboxChannel from './InboxChannels.vue';
|
||||
@@ -15,7 +15,7 @@ export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/inboxes'),
|
||||
component: SettingWrapper,
|
||||
component: SettingsWrapper,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
|
||||
+7
-7
@@ -149,7 +149,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isATwilioChannel" class="mx-8">
|
||||
<div v-if="isATwilioChannel" class="mx-6">
|
||||
<SettingsSection
|
||||
:title="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.TITLE')"
|
||||
:sub-title="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.SUBTITLE')"
|
||||
@@ -170,7 +170,7 @@ export default {
|
||||
</div>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
<div v-else-if="isAVoiceChannel" class="mx-8">
|
||||
<div v-else-if="isAVoiceChannel" class="mx-6">
|
||||
<SettingsSection
|
||||
:title="$t('INBOX_MGMT.ADD.VOICE.CONFIGURATION.TWILIO_VOICE_URL_TITLE')"
|
||||
:sub-title="
|
||||
@@ -189,7 +189,7 @@ export default {
|
||||
</SettingsSection>
|
||||
</div>
|
||||
|
||||
<div v-else-if="isALineChannel" class="mx-8">
|
||||
<div v-else-if="isALineChannel" class="mx-6">
|
||||
<SettingsSection
|
||||
:title="$t('INBOX_MGMT.ADD.LINE_CHANNEL.API_CALLBACK.TITLE')"
|
||||
:sub-title="$t('INBOX_MGMT.ADD.LINE_CHANNEL.API_CALLBACK.SUBTITLE')"
|
||||
@@ -198,7 +198,7 @@ export default {
|
||||
</SettingsSection>
|
||||
</div>
|
||||
<div v-else-if="isAWebWidgetInbox">
|
||||
<div class="mx-8">
|
||||
<div class="mx-6">
|
||||
<SettingsSection
|
||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_HEADING')"
|
||||
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_SUB_HEAD')"
|
||||
@@ -268,7 +268,7 @@ export default {
|
||||
</SettingsSection>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="isAPIInbox" class="mx-8">
|
||||
<div v-else-if="isAPIInbox" class="mx-6">
|
||||
<SettingsSection
|
||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_IDENTIFIER')"
|
||||
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_IDENTIFIER_SUB_TEXT')"
|
||||
@@ -300,7 +300,7 @@ export default {
|
||||
</SettingsSection>
|
||||
</div>
|
||||
<div v-else-if="isAnEmailChannel">
|
||||
<div class="mx-8">
|
||||
<div class="mx-6">
|
||||
<SettingsSection
|
||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.FORWARD_EMAIL_TITLE')"
|
||||
:sub-title="
|
||||
@@ -327,7 +327,7 @@ export default {
|
||||
<SmtpSettings v-if="inbox.imap_enabled" :inbox="inbox" />
|
||||
</div>
|
||||
<div v-else-if="isAWhatsAppChannel && !isATwilioChannel">
|
||||
<div v-if="inbox.provider_config" class="mx-8">
|
||||
<div v-if="inbox.provider_config" class="mx-6">
|
||||
<!-- Embedded Signup Section -->
|
||||
<template v-if="isEmbeddedSignupWhatsApp">
|
||||
<SettingsSection
|
||||
|
||||
+1
-1
@@ -414,7 +414,7 @@ const handleConfirmTemplateUpdate = async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-8">
|
||||
<div class="mx-6">
|
||||
<SectionLayout
|
||||
:title="$t('INBOX_MGMT.CSAT.TITLE')"
|
||||
:description="$t('INBOX_MGMT.CSAT.SUBTITLE')"
|
||||
|
||||
+45
-36
@@ -1,4 +1,5 @@
|
||||
<script setup>
|
||||
import { BaseTableRow, BaseTableCell } from 'dashboard/components-next/table';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
@@ -12,40 +13,48 @@ defineEmits(['edit', 'delete']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr class="max-w-full py-1">
|
||||
<td
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 text-sm w-40 max-w-[10rem] truncate"
|
||||
:title="app.title"
|
||||
>
|
||||
{{ app.title }}
|
||||
</td>
|
||||
<td
|
||||
class="max-w-lg py-4 ltr:pr-4 rtl:pl-4 text-sm truncate"
|
||||
:title="app.content[0].url"
|
||||
>
|
||||
{{ app.content[0].url }}
|
||||
</td>
|
||||
<td class="flex gap-1 py-4 ltr:pr-4 rtl:pl-4 text-sm sm:pr-0 justify-end">
|
||||
<Button
|
||||
v-tooltip.top="
|
||||
$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.LIST.EDIT_TOOLTIP')
|
||||
"
|
||||
icon="i-lucide-pen"
|
||||
slate
|
||||
xs
|
||||
faded
|
||||
@click="$emit('edit', app)"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.top="
|
||||
$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.LIST.DELETE_TOOLTIP')
|
||||
"
|
||||
icon="i-lucide-trash-2"
|
||||
xs
|
||||
ruby
|
||||
faded
|
||||
@click="$emit('delete', app)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<BaseTableRow :item="app">
|
||||
<template #default>
|
||||
<BaseTableCell>
|
||||
<span
|
||||
class="text-body-main text-n-slate-12 truncate block"
|
||||
:title="app.title"
|
||||
>
|
||||
{{ app.title }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell>
|
||||
<span
|
||||
class="text-body-main text-n-slate-11 truncate block"
|
||||
:title="app.content[0].url"
|
||||
>
|
||||
{{ app.content[0].url }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell align="end" class="w-24">
|
||||
<div class="flex justify-end gap-3 flex-shrink-0">
|
||||
<Button
|
||||
v-tooltip.top="
|
||||
$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.LIST.EDIT_TOOLTIP')
|
||||
"
|
||||
icon="i-woot-edit-pen"
|
||||
slate
|
||||
sm
|
||||
@click="$emit('edit', app)"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.top="
|
||||
$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.LIST.DELETE_TOOLTIP')
|
||||
"
|
||||
icon="i-woot-bin"
|
||||
slate
|
||||
sm
|
||||
@click="$emit('delete', app)"
|
||||
/>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
</template>
|
||||
</BaseTableRow>
|
||||
</template>
|
||||
|
||||
+52
-45
@@ -1,14 +1,19 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
import { BaseTable } from 'dashboard/components-next/table';
|
||||
import DashboardAppModal from './DashboardAppModal.vue';
|
||||
import DashboardAppsRow from './DashboardAppsRow.vue';
|
||||
import BaseSettingsHeader from '../../components/BaseSettingsHeader.vue';
|
||||
import SettingsLayout from '../../SettingsLayout.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseSettingsHeader,
|
||||
SettingsLayout,
|
||||
BaseTable,
|
||||
DashboardAppModal,
|
||||
DashboardAppsRow,
|
||||
NextButton,
|
||||
@@ -20,6 +25,7 @@ export default {
|
||||
showDeleteConfirmationPopup: false,
|
||||
selectedApp: {},
|
||||
mode: 'CREATE',
|
||||
searchQuery: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -27,12 +33,20 @@ export default {
|
||||
records: 'dashboardApps/getRecords',
|
||||
uiFlags: 'dashboardApps/getUIFlags',
|
||||
}),
|
||||
filteredRecords() {
|
||||
const query = this.searchQuery.trim();
|
||||
if (!query) return this.records;
|
||||
return picoSearch(this.records, query, ['title']);
|
||||
},
|
||||
tableHeaders() {
|
||||
return [
|
||||
this.$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.LIST.TABLE_HEADER.NAME'),
|
||||
this.$t(
|
||||
'INTEGRATION_SETTINGS.DASHBOARD_APPS.LIST.TABLE_HEADER.ENDPOINT'
|
||||
),
|
||||
this.$t(
|
||||
'INTEGRATION_SETTINGS.DASHBOARD_APPS.LIST.TABLE_HEADER.ACTIONS'
|
||||
),
|
||||
];
|
||||
},
|
||||
},
|
||||
@@ -84,59 +98,52 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 gap-8 overflow-auto">
|
||||
<BaseSettingsHeader
|
||||
:title="$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.TITLE')"
|
||||
:description="$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.DESCRIPTION')"
|
||||
:link-text="$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.LEARN_MORE')"
|
||||
feature-name="dashboard_apps"
|
||||
:back-button-label="$t('INTEGRATION_SETTINGS.HEADER')"
|
||||
>
|
||||
<template #actions>
|
||||
<NextButton
|
||||
icon="i-lucide-circle-plus"
|
||||
:label="$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.HEADER_BTN_TXT')"
|
||||
@click="openCreatePopup"
|
||||
/>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
<div class="w-full overflow-x-auto text-n-slate-11">
|
||||
<p
|
||||
v-if="!uiFlags.isFetching && !records.length"
|
||||
class="flex flex-col items-center justify-center h-full"
|
||||
<SettingsLayout
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:loading-message="$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.LIST.LOADING')"
|
||||
:no-records-found="!records.length"
|
||||
:no-records-message="$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.LIST.404')"
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
v-model:search-query="searchQuery"
|
||||
:title="$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.TITLE')"
|
||||
:description="$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.DESCRIPTION')"
|
||||
:link-text="$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.LEARN_MORE')"
|
||||
:search-placeholder="
|
||||
$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.SEARCH_PLACEHOLDER')
|
||||
"
|
||||
feature-name="dashboard_apps"
|
||||
:back-button-label="$t('INTEGRATION_SETTINGS.HEADER')"
|
||||
>
|
||||
{{ $t('INTEGRATION_SETTINGS.DASHBOARD_APPS.LIST.404') }}
|
||||
</p>
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.isFetching"
|
||||
:message="$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.LIST.LOADING')"
|
||||
/>
|
||||
<table
|
||||
v-if="!uiFlags.isFetching && records.length"
|
||||
class="min-w-full divide-y divide-n-weak"
|
||||
<template #actions>
|
||||
<NextButton
|
||||
:label="$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.HEADER_BTN_TXT')"
|
||||
@click="openCreatePopup"
|
||||
/>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<span
|
||||
v-if="!filteredRecords.length && searchQuery"
|
||||
class="flex-1 flex items-center justify-center py-20 text-center text-body-main !text-base text-n-slate-11"
|
||||
>
|
||||
<thead>
|
||||
<th
|
||||
v-for="thHeader in tableHeaders"
|
||||
:key="thHeader"
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 font-semibold text-left text-n-slate-11"
|
||||
>
|
||||
{{ thHeader }}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-n-weak">
|
||||
{{ $t('INTEGRATION_SETTINGS.DASHBOARD_APPS.NO_RESULTS') }}
|
||||
</span>
|
||||
<BaseTable v-else :headers="tableHeaders" :items="filteredRecords">
|
||||
<template #row="{ items }">
|
||||
<DashboardAppsRow
|
||||
v-for="(dashboardAppItem, index) in records"
|
||||
v-for="(dashboardAppItem, index) in items"
|
||||
:key="dashboardAppItem.id"
|
||||
:index="index"
|
||||
:app="dashboardAppItem"
|
||||
@edit="editApp"
|
||||
@delete="openDeletePopup"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</BaseTable>
|
||||
</template>
|
||||
<DashboardAppModal
|
||||
v-if="showDashboardAppPopup"
|
||||
:show="showDashboardAppPopup"
|
||||
@@ -160,5 +167,5 @@ export default {
|
||||
"
|
||||
:reject-text="$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.DELETE.CONFIRM_NO')"
|
||||
/>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup>
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useBranding } from 'shared/composables/useBranding';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
import IntegrationItem from './IntegrationItem.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
@@ -10,12 +11,19 @@ const store = useStore();
|
||||
const getters = useStoreGetters();
|
||||
const { replaceInstallationName } = useBranding();
|
||||
|
||||
const searchQuery = ref('');
|
||||
const uiFlags = getters['integrations/getUIFlags'];
|
||||
|
||||
const integrationList = computed(
|
||||
() => getters['integrations/getAppIntegrations'].value
|
||||
);
|
||||
|
||||
const filteredIntegrationList = computed(() => {
|
||||
const query = searchQuery.value.trim();
|
||||
if (!query) return integrationList.value;
|
||||
return picoSearch(integrationList.value, query, ['name', 'description']);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('integrations/get');
|
||||
});
|
||||
@@ -28,19 +36,30 @@ onMounted(() => {
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
v-model:search-query="searchQuery"
|
||||
:title="$t('INTEGRATION_SETTINGS.HEADER')"
|
||||
:description="
|
||||
replaceInstallationName($t('INTEGRATION_SETTINGS.DESCRIPTION'))
|
||||
"
|
||||
:link-text="$t('INTEGRATION_SETTINGS.LEARN_MORE')"
|
||||
:search-placeholder="$t('INTEGRATION_SETTINGS.SEARCH_PLACEHOLDER')"
|
||||
feature-name="integrations"
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="flex-grow flex-shrink overflow-auto">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
<span
|
||||
v-if="!filteredIntegrationList.length && searchQuery"
|
||||
class="flex-1 flex items-center justify-center py-20 text-center text-body-main !text-base text-n-slate-11"
|
||||
>
|
||||
{{ $t('INTEGRATION_SETTINGS.NO_RESULTS') }}
|
||||
</span>
|
||||
<div
|
||||
v-else
|
||||
class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4"
|
||||
>
|
||||
<IntegrationItem
|
||||
v-for="item in integrationList"
|
||||
v-for="item in filteredIntegrationList"
|
||||
:id="item.id"
|
||||
:key="item.id"
|
||||
:logo="item.logo"
|
||||
|
||||
+32
-19
@@ -6,12 +6,16 @@ import { useIntegrationHook } from 'dashboard/composables/useIntegrationHook';
|
||||
import NewHook from './NewHook.vue';
|
||||
import SingleIntegrationHooks from './SingleIntegrationHooks.vue';
|
||||
import MultipleIntegrationHooks from './MultipleIntegrationHooks.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NewHook,
|
||||
SingleIntegrationHooks,
|
||||
MultipleIntegrationHooks,
|
||||
SettingsLayout,
|
||||
BaseSettingsHeader,
|
||||
},
|
||||
props: {
|
||||
integrationId: {
|
||||
@@ -108,26 +112,35 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="overflow-auto p-4 w-full my-auto flex flex-wrap h-full">
|
||||
<div v-if="showIntegrationHooks" class="w-full">
|
||||
<div v-if="isIntegrationMultiple">
|
||||
<MultipleIntegrationHooks
|
||||
:integration-id="integrationId"
|
||||
:show-add-button="showAddButton"
|
||||
@add="openAddHookModal"
|
||||
@delete="openDeletePopup"
|
||||
/>
|
||||
</div>
|
||||
<SettingsLayout :is-loading="uiFlags.isFetching">
|
||||
<template v-if="isIntegrationSingle" #header>
|
||||
<BaseSettingsHeader
|
||||
:title="integration.name || ''"
|
||||
description=""
|
||||
:feature-name="integrationId"
|
||||
:back-button-label="$t('INTEGRATION_SETTINGS.HEADER')"
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<div v-if="showIntegrationHooks" class="w-full">
|
||||
<div v-if="isIntegrationMultiple">
|
||||
<MultipleIntegrationHooks
|
||||
:integration-id="integrationId"
|
||||
:show-add-button="showAddButton"
|
||||
@add="openAddHookModal"
|
||||
@delete="openDeletePopup"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isIntegrationSingle">
|
||||
<SingleIntegrationHooks
|
||||
:integration-id="integrationId"
|
||||
@add="openAddHookModal"
|
||||
@delete="openDeletePopup"
|
||||
/>
|
||||
<div v-if="isIntegrationSingle">
|
||||
<SingleIntegrationHooks
|
||||
:integration-id="integrationId"
|
||||
@add="openAddHookModal"
|
||||
@delete="openDeletePopup"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<woot-modal v-model:show="showAddHookModal" :on-close="hideAddHookModal">
|
||||
<NewHook :integration-id="integrationId" @close="hideAddHookModal" />
|
||||
</woot-modal>
|
||||
@@ -141,5 +154,5 @@ export default {
|
||||
:confirm-text="confirmText"
|
||||
:reject-text="cancelText"
|
||||
/>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
} from 'dashboard/composables/store';
|
||||
|
||||
import Integration from './Integration.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
|
||||
const store = useStore();
|
||||
|
||||
@@ -35,8 +36,16 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-grow flex-shrink p-4 overflow-auto max-w-6xl mx-auto">
|
||||
<div v-if="integrationLoaded && !uiFlags.isCreatingLinear">
|
||||
<SettingsLayout :is-loading="!integrationLoaded || uiFlags.isCreatingLinear">
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
:title="$t('INTEGRATION_SETTINGS.LINEAR.HEADER')"
|
||||
description=""
|
||||
feature-name="linear_integration"
|
||||
:back-button-label="$t('INTEGRATION_SETTINGS.HEADER')"
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<Integration
|
||||
:integration-id="integration.id"
|
||||
:integration-logo="integration.logo"
|
||||
@@ -49,9 +58,6 @@ onMounted(() => {
|
||||
message: $t('INTEGRATION_SETTINGS.LINEAR.DELETE.MESSAGE'),
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="flex items-center justify-center flex-1">
|
||||
<Spinner size="" color-scheme="primary" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
+108
-95
@@ -1,64 +1,80 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useIntegrationHook } from 'dashboard/composables/useIntegrationHook';
|
||||
import {
|
||||
BaseTable,
|
||||
BaseTableRow,
|
||||
BaseTableCell,
|
||||
} from 'dashboard/components-next/table';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import BaseSettingsHeader from 'dashboard/routes/dashboard/settings/components/BaseSettingsHeader.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseSettingsHeader,
|
||||
NextButton,
|
||||
const props = defineProps({
|
||||
integrationId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
integrationId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
showAddButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showAddButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
emits: ['delete', 'add'],
|
||||
setup(props) {
|
||||
const { integration, isHookTypeInbox, hasConnectedHooks } =
|
||||
useIntegrationHook(props.integrationId);
|
||||
return { integration, isHookTypeInbox, hasConnectedHooks };
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
globalConfig: 'globalConfig/get',
|
||||
}),
|
||||
hookHeaders() {
|
||||
return this.integration.visible_properties;
|
||||
},
|
||||
hooks() {
|
||||
if (!this.hasConnectedHooks) {
|
||||
return [];
|
||||
}
|
||||
const { hooks } = this.integration;
|
||||
return hooks.map(hook => ({
|
||||
...hook,
|
||||
id: hook.id,
|
||||
properties: this.hookHeaders.map(property =>
|
||||
hook.settings[property] ? hook.settings[property] : '--'
|
||||
),
|
||||
}));
|
||||
},
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
inboxName(hook) {
|
||||
return hook.inbox ? hook.inbox.name : '';
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
defineEmits(['delete', 'add']);
|
||||
const { t } = useI18n();
|
||||
|
||||
const { integration, isHookTypeInbox, hasConnectedHooks } = useIntegrationHook(
|
||||
props.integrationId
|
||||
);
|
||||
|
||||
const globalConfig = useMapGetter('globalConfig/get');
|
||||
const searchQuery = ref('');
|
||||
|
||||
const hookHeaders = computed(() => {
|
||||
const headers = [...(integration.value.visible_properties || [])];
|
||||
if (isHookTypeInbox.value) {
|
||||
headers.push(t('INTEGRATION_APPS.LIST.INBOX'));
|
||||
}
|
||||
headers.push(t('INTEGRATION_APPS.LIST.ACTIONS'));
|
||||
return headers;
|
||||
});
|
||||
|
||||
const hooks = computed(() => {
|
||||
if (!hasConnectedHooks.value) {
|
||||
return [];
|
||||
}
|
||||
const { hooks: integrationHooks } = integration.value;
|
||||
const visibleProperties = integration.value.visible_properties || [];
|
||||
return integrationHooks.map(hook => ({
|
||||
...hook,
|
||||
id: hook.id,
|
||||
properties: visibleProperties.map(property =>
|
||||
hook.settings[property] ? hook.settings[property] : '--'
|
||||
),
|
||||
}));
|
||||
});
|
||||
|
||||
const filteredHooks = computed(() => {
|
||||
const query = searchQuery.value?.trim() || '';
|
||||
if (!query) return hooks.value;
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return (
|
||||
hooks.value?.filter(hook =>
|
||||
hook.properties?.some(prop => prop?.toLowerCase().includes(lowerQuery))
|
||||
) || []
|
||||
);
|
||||
});
|
||||
|
||||
const inboxName = hook => (hook.inbox ? hook.inbox.name : '');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 gap-8 overflow-auto">
|
||||
<BaseSettingsHeader
|
||||
:title="integration.name"
|
||||
v-model:search-query="searchQuery"
|
||||
:title="integration.name || ''"
|
||||
:description="
|
||||
$t(
|
||||
`INTEGRATION_APPS.SIDEBAR_DESCRIPTION.${integration.name.toUpperCase()}`,
|
||||
@@ -67,61 +83,58 @@ export default {
|
||||
"
|
||||
:feature-name="integrationId"
|
||||
:back-button-label="$t('INTEGRATION_SETTINGS.HEADER')"
|
||||
:search-placeholder="$t('INTEGRATION_APPS.SEARCH_PLACEHOLDER')"
|
||||
>
|
||||
<template #actions>
|
||||
<NextButton
|
||||
v-if="showAddButton"
|
||||
icon="i-lucide-circle-plus"
|
||||
:label="$t('INTEGRATION_APPS.ADD_BUTTON')"
|
||||
@click="$emit('add')"
|
||||
/>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
<div class="w-full">
|
||||
<table v-if="hasConnectedHooks">
|
||||
<thead
|
||||
class="[&>th]:font-semibold [&>th]:tracking-[1px] ltr:[&>th]:text-left rtl:[&>th]:text-right [&>th]:px-2.5 [&>th]:uppercase [&>th]:text-n-slate-12"
|
||||
>
|
||||
<th
|
||||
v-for="hookHeader in hookHeaders"
|
||||
:key="hookHeader"
|
||||
class="ltr:!pl-0 rtl:!pr-0"
|
||||
>
|
||||
{{ hookHeader }}
|
||||
</th>
|
||||
<th v-if="isHookTypeInbox">
|
||||
{{ $t('INTEGRATION_APPS.LIST.INBOX') }}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="hook in hooks"
|
||||
:key="hook.id"
|
||||
class="border-b border-n-weak [&>td]:p-2.5 [&>td]:text-n-slate-12"
|
||||
>
|
||||
<td
|
||||
v-for="property in hook.properties"
|
||||
:key="property"
|
||||
class="ltr:!pl-0 rtl:!pr-0"
|
||||
>
|
||||
{{ property }}
|
||||
</td>
|
||||
<td v-if="isHookTypeInbox" class="break-words">
|
||||
{{ inboxName(hook) }}
|
||||
</td>
|
||||
<td class="flex justify-end gap-1">
|
||||
<NextButton
|
||||
v-tooltip.top="$t('INTEGRATION_APPS.LIST.DELETE.BUTTON_TEXT')"
|
||||
icon="i-lucide-trash-2"
|
||||
xs
|
||||
ruby
|
||||
faded
|
||||
@click="$emit('delete', hook)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<BaseTable
|
||||
v-if="hasConnectedHooks"
|
||||
:headers="hookHeaders"
|
||||
:items="filteredHooks"
|
||||
:no-data-message="searchQuery ? $t('INTEGRATION_APPS.NO_RESULTS') : ''"
|
||||
>
|
||||
<template #row="{ items }">
|
||||
<BaseTableRow v-for="hook in items" :key="hook.id" :item="hook">
|
||||
<template #default>
|
||||
<BaseTableCell
|
||||
v-for="property in hook.properties"
|
||||
:key="property"
|
||||
>
|
||||
<span class="text-body-main text-n-slate-12">
|
||||
{{ property }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell v-if="isHookTypeInbox">
|
||||
<span class="text-body-main text-n-slate-11 break-words">
|
||||
{{ inboxName(hook) }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell align="end" class="w-12">
|
||||
<div class="flex justify-end gap-3 flex-shrink-0">
|
||||
<NextButton
|
||||
v-tooltip.top="
|
||||
$t('INTEGRATION_APPS.LIST.DELETE.BUTTON_TEXT')
|
||||
"
|
||||
icon="i-woot-bin"
|
||||
slate
|
||||
sm
|
||||
@click="$emit('delete', hook)"
|
||||
/>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
</template>
|
||||
</BaseTableRow>
|
||||
</template>
|
||||
</BaseTable>
|
||||
<p v-else class="flex flex-col items-center justify-center h-full">
|
||||
{{
|
||||
$t('INTEGRATION_APPS.NO_HOOK_CONFIGURED', {
|
||||
|
||||
@@ -11,7 +11,8 @@ import ButtonNext from 'next/button/Button.vue';
|
||||
import notionClient from 'dashboard/api/notion_auth.js';
|
||||
|
||||
import Integration from './Integration.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
@@ -49,8 +50,16 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-grow flex-shrink p-4 overflow-auto mx-auto">
|
||||
<div v-if="integrationLoaded && !uiFlags.isCreatingNotion">
|
||||
<SettingsLayout :is-loading="!integrationLoaded || uiFlags.isCreatingNotion">
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
:title="$t('INTEGRATION_SETTINGS.NOTION.HEADER')"
|
||||
description=""
|
||||
feature-name="notion_integration"
|
||||
:back-button-label="$t('INTEGRATION_SETTINGS.HEADER')"
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<Integration
|
||||
:integration-id="integration.id"
|
||||
:integration-logo="integration.logo"
|
||||
@@ -72,9 +81,6 @@ onMounted(() => {
|
||||
/>
|
||||
</template>
|
||||
</Integration>
|
||||
</div>
|
||||
<div v-else class="flex items-center justify-center flex-1">
|
||||
<Spinner size="" color-scheme="primary" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
@@ -5,13 +5,15 @@ import {
|
||||
useMapGetter,
|
||||
useStore,
|
||||
} from 'dashboard/composables/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Integration from './Integration.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import integrationAPI from 'dashboard/api/integrations';
|
||||
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
|
||||
defineProps({
|
||||
error: {
|
||||
@@ -21,6 +23,7 @@ defineProps({
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
const dialogRef = ref(null);
|
||||
const integrationLoaded = ref(false);
|
||||
const storeUrl = ref('');
|
||||
@@ -88,64 +91,67 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-grow flex-shrink p-4 overflow-auto max-w-6xl mx-auto">
|
||||
<div
|
||||
v-if="integrationLoaded && !uiFlags.isCreatingShopify"
|
||||
class="flex flex-col gap-6"
|
||||
>
|
||||
<Integration
|
||||
:integration-id="integration.id"
|
||||
:integration-logo="integration.logo"
|
||||
:integration-name="integration.name"
|
||||
:integration-description="integration.description"
|
||||
:integration-enabled="integration.enabled"
|
||||
:integration-action="integrationAction"
|
||||
:delete-confirmation-text="{
|
||||
title: $t('INTEGRATION_SETTINGS.SHOPIFY.DELETE.TITLE'),
|
||||
message: $t('INTEGRATION_SETTINGS.SHOPIFY.DELETE.MESSAGE'),
|
||||
}"
|
||||
>
|
||||
<template #action>
|
||||
<Button
|
||||
teal
|
||||
:label="$t('INTEGRATION_SETTINGS.CONNECT.BUTTON_TEXT')"
|
||||
@click="openStoreUrlDialog"
|
||||
<SettingsLayout :is-loading="!integrationLoaded || uiFlags.isCreatingShopify">
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
:title="$t('INTEGRATION_SETTINGS.SHOPIFY.HEADER')"
|
||||
description=""
|
||||
feature-name="shopify_integration"
|
||||
:back-button-label="$t('INTEGRATION_SETTINGS.HEADER')"
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="flex flex-col gap-6">
|
||||
<Integration
|
||||
:integration-id="integration.id"
|
||||
:integration-logo="integration.logo"
|
||||
:integration-name="integration.name"
|
||||
:integration-description="integration.description"
|
||||
:integration-enabled="integration.enabled"
|
||||
:integration-action="integrationAction"
|
||||
:delete-confirmation-text="{
|
||||
title: t('INTEGRATION_SETTINGS.SHOPIFY.DELETE.TITLE'),
|
||||
message: t('INTEGRATION_SETTINGS.SHOPIFY.DELETE.MESSAGE'),
|
||||
}"
|
||||
>
|
||||
<template #action>
|
||||
<Button
|
||||
teal
|
||||
:label="t('INTEGRATION_SETTINGS.CONNECT.BUTTON_TEXT')"
|
||||
@click="openStoreUrlDialog"
|
||||
/>
|
||||
</template>
|
||||
</Integration>
|
||||
<div
|
||||
v-if="error"
|
||||
class="flex items-center justify-center flex-1 outline outline-n-container outline-1 bg-n-alpha-3 rounded-md shadow p-6"
|
||||
>
|
||||
<p class="text-n-ruby-9">
|
||||
{{ t('INTEGRATION_SETTINGS.SHOPIFY.ERROR') }}
|
||||
</p>
|
||||
</div>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
:title="t('INTEGRATION_SETTINGS.SHOPIFY.STORE_URL.TITLE')"
|
||||
:is-loading="isSubmitting"
|
||||
@confirm="handleStoreUrlSubmit"
|
||||
@close="hideStoreUrlModal"
|
||||
>
|
||||
<Input
|
||||
v-model="storeUrl"
|
||||
:label="t('INTEGRATION_SETTINGS.SHOPIFY.STORE_URL.LABEL')"
|
||||
:placeholder="
|
||||
t('INTEGRATION_SETTINGS.SHOPIFY.STORE_URL.PLACEHOLDER')
|
||||
"
|
||||
:message="
|
||||
!storeUrlError
|
||||
? t('INTEGRATION_SETTINGS.SHOPIFY.STORE_URL.HELP')
|
||||
: storeUrlError
|
||||
"
|
||||
:message-type="storeUrlError ? 'error' : 'info'"
|
||||
/>
|
||||
</template>
|
||||
</Integration>
|
||||
<div
|
||||
v-if="error"
|
||||
class="flex items-center justify-center flex-1 outline outline-n-container outline-1 bg-n-alpha-3 rounded-md shadow p-6"
|
||||
>
|
||||
<p class="text-n-ruby-9">
|
||||
{{ $t('INTEGRATION_SETTINGS.SHOPIFY.ERROR') }}
|
||||
</p>
|
||||
</Dialog>
|
||||
</div>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
:title="$t('INTEGRATION_SETTINGS.SHOPIFY.STORE_URL.TITLE')"
|
||||
:is-loading="isSubmitting"
|
||||
@confirm="handleStoreUrlSubmit"
|
||||
@close="hideStoreUrlModal"
|
||||
>
|
||||
<Input
|
||||
v-model="storeUrl"
|
||||
:label="$t('INTEGRATION_SETTINGS.SHOPIFY.STORE_URL.LABEL')"
|
||||
:placeholder="
|
||||
$t('INTEGRATION_SETTINGS.SHOPIFY.STORE_URL.PLACEHOLDER')
|
||||
"
|
||||
:message="
|
||||
!storeUrlError
|
||||
? $t('INTEGRATION_SETTINGS.SHOPIFY.STORE_URL.HELP')
|
||||
: storeUrlError
|
||||
"
|
||||
:message-type="storeUrlError ? 'error' : 'info'"
|
||||
/>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex items-center justify-center flex-1">
|
||||
<Spinner size="" color-scheme="primary" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
@@ -6,7 +6,8 @@ import { useI18n } from 'vue-i18n';
|
||||
import Integration from './Integration.vue';
|
||||
import SelectChannelWarning from './Slack/SelectChannelWarning.vue';
|
||||
import SlackIntegrationHelpText from './Slack/SlackIntegrationHelpText.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
|
||||
const props = defineProps({
|
||||
code: { type: String, default: '' },
|
||||
@@ -76,32 +77,42 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="integrationLoaded && !uiFlags.isCreatingSlack"
|
||||
class="flex flex-col flex-1 overflow-auto gap-5 pt-1 pb-10"
|
||||
>
|
||||
<Integration
|
||||
:integration-id="integration.id"
|
||||
:integration-logo="integration.logo"
|
||||
:integration-name="integration.name"
|
||||
:integration-description="integration.description"
|
||||
:integration-enabled="integration.enabled"
|
||||
:integration-action="integrationAction"
|
||||
:action-button-text="$t('INTEGRATION_SETTINGS.SLACK.DELETE')"
|
||||
:delete-confirmation-text="{
|
||||
title: $t('INTEGRATION_SETTINGS.SLACK.DELETE_CONFIRMATION.TITLE'),
|
||||
message: $t('INTEGRATION_SETTINGS.SLACK.DELETE_CONFIRMATION.MESSAGE'),
|
||||
}"
|
||||
/>
|
||||
<div v-if="areHooksAvailable" class="flex-1">
|
||||
<SelectChannelWarning
|
||||
v-if="!isIntegrationHookEnabled"
|
||||
:has-connected-a-channel="hasConnectedAChannel"
|
||||
<SettingsLayout :is-loading="!integrationLoaded || uiFlags.isCreatingSlack">
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
:title="$t('INTEGRATION_SETTINGS.SLACK.HEADER')"
|
||||
description=""
|
||||
feature-name="slack_integration"
|
||||
:back-button-label="$t('INTEGRATION_SETTINGS.HEADER')"
|
||||
/>
|
||||
<SlackIntegrationHelpText :selected-channel-name="selectedChannelName" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex items-center justify-center flex-1">
|
||||
<Spinner size="" color-scheme="primary" />
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="space-y-5">
|
||||
<Integration
|
||||
:integration-id="integration.id"
|
||||
:integration-logo="integration.logo"
|
||||
:integration-name="integration.name"
|
||||
:integration-description="integration.description"
|
||||
:integration-enabled="integration.enabled"
|
||||
:integration-action="integrationAction"
|
||||
:action-button-text="$t('INTEGRATION_SETTINGS.SLACK.DELETE')"
|
||||
:delete-confirmation-text="{
|
||||
title: $t('INTEGRATION_SETTINGS.SLACK.DELETE_CONFIRMATION.TITLE'),
|
||||
message: $t(
|
||||
'INTEGRATION_SETTINGS.SLACK.DELETE_CONFIRMATION.MESSAGE'
|
||||
),
|
||||
}"
|
||||
/>
|
||||
<div v-if="areHooksAvailable" class="flex-1">
|
||||
<SelectChannelWarning
|
||||
v-if="!isIntegrationHookEnabled"
|
||||
:has-connected-a-channel="hasConnectedAChannel"
|
||||
/>
|
||||
<SlackIntegrationHelpText
|
||||
:selected-channel-name="selectedChannelName"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
+24
-15
@@ -2,7 +2,9 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useBranding } from 'shared/composables/useBranding';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import { BaseTable } from 'dashboard/components-next/table';
|
||||
import NewWebhook from './NewWebHook.vue';
|
||||
import EditWebhook from './EditWebHook.vue';
|
||||
import WebhookRow from './WebhookRow.vue';
|
||||
@@ -14,6 +16,7 @@ export default {
|
||||
SettingsLayout,
|
||||
NextButton,
|
||||
BaseSettingsHeader,
|
||||
BaseTable,
|
||||
NewWebhook,
|
||||
EditWebhook,
|
||||
WebhookRow,
|
||||
@@ -29,6 +32,7 @@ export default {
|
||||
showEditPopup: false,
|
||||
showDeleteConfirmationPopup: false,
|
||||
selectedWebHook: {},
|
||||
searchQuery: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -39,6 +43,11 @@ export default {
|
||||
integration() {
|
||||
return this.$store.getters['integrations/getIntegration']('webhook');
|
||||
},
|
||||
filteredRecords() {
|
||||
const query = this.searchQuery.trim();
|
||||
if (!query) return this.records;
|
||||
return picoSearch(this.records, query, ['name', 'url']);
|
||||
},
|
||||
tableHeaders() {
|
||||
return [
|
||||
this.$t(
|
||||
@@ -103,16 +112,19 @@ export default {
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
v-if="integration.name"
|
||||
v-model:search-query="searchQuery"
|
||||
:title="integration.name"
|
||||
:description="replaceInstallationName(integration.description)"
|
||||
:link-text="$t('INTEGRATION_SETTINGS.WEBHOOK.LEARN_MORE')"
|
||||
:search-placeholder="
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.SEARCH_PLACEHOLDER')
|
||||
"
|
||||
feature-name="webhook"
|
||||
:back-button-label="$t('INTEGRATION_SETTINGS.HEADER')"
|
||||
>
|
||||
<template #actions>
|
||||
<NextButton
|
||||
blue
|
||||
icon="i-lucide-circle-plus"
|
||||
:label="$t('INTEGRATION_SETTINGS.WEBHOOK.HEADER_BTN_TXT')"
|
||||
@click="openAddPopup"
|
||||
/>
|
||||
@@ -120,27 +132,24 @@ export default {
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<table class="min-w-full divide-y divide-n-weak">
|
||||
<thead>
|
||||
<th
|
||||
v-for="thHeader in tableHeaders"
|
||||
:key="thHeader"
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 text-left font-semibold text-n-slate-11 last:text-right last:pr-4"
|
||||
>
|
||||
{{ thHeader }}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-n-weak flex-1 text-n-slate-12">
|
||||
<BaseTable
|
||||
:headers="tableHeaders"
|
||||
:items="filteredRecords"
|
||||
:no-data-message="
|
||||
searchQuery ? $t('INTEGRATION_SETTINGS.WEBHOOK.NO_RESULTS') : ''
|
||||
"
|
||||
>
|
||||
<template #row="{ items }">
|
||||
<WebhookRow
|
||||
v-for="(webHookItem, index) in records"
|
||||
v-for="(webHookItem, index) in items"
|
||||
:key="webHookItem.id"
|
||||
:index="index"
|
||||
:webhook="webHookItem"
|
||||
@edit="openEditPopup"
|
||||
@delete="openDeletePopup"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</template>
|
||||
<woot-modal v-model:show="showAddPopup" :on-close="hideAddPopup">
|
||||
<NewWebhook v-if="showAddPopup" :on-close="hideAddPopup" />
|
||||
|
||||
+43
-40
@@ -3,7 +3,7 @@ import { computed } from 'vue';
|
||||
import { getI18nKey } from 'dashboard/routes/dashboard/settings/helper/settingsHelper';
|
||||
import ShowMore from 'dashboard/components/widgets/ShowMore.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { BaseTableRow, BaseTableCell } from 'dashboard/components-next/table';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -35,45 +35,48 @@ const subscribedEvents = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
<div class="flex gap-2 font-medium break-words text-n-slate-12">
|
||||
<template v-if="webhook.name">
|
||||
{{ webhook.name }}
|
||||
<span class="text-n-slate-11">
|
||||
<BaseTableRow :item="webhook">
|
||||
<template #default>
|
||||
<BaseTableCell>
|
||||
<div class="flex gap-2 font-medium break-words text-n-slate-12">
|
||||
<template v-if="webhook.name">
|
||||
{{ webhook.name }}
|
||||
<span class="text-n-slate-11">
|
||||
{{ webhook.url }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ webhook.url }}
|
||||
</template>
|
||||
</div>
|
||||
<div class="block mt-1 text-sm text-n-slate-11">
|
||||
<span class="font-medium">
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.SUBSCRIBED_EVENTS') }}:
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ webhook.url }}
|
||||
</template>
|
||||
</div>
|
||||
<div class="block mt-1 text-sm text-n-slate-11">
|
||||
<span class="font-medium">
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.SUBSCRIBED_EVENTS') }}:
|
||||
</span>
|
||||
<ShowMore :text="subscribedEvents" :limit="60" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 min-w-xs">
|
||||
<div class="flex justify-end gap-1">
|
||||
<Button
|
||||
v-tooltip.top="$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.BUTTON_TEXT')"
|
||||
icon="i-lucide-pen"
|
||||
slate
|
||||
xs
|
||||
faded
|
||||
@click="emit('edit', webhook)"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.top="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.BUTTON_TEXT')"
|
||||
icon="i-lucide-trash-2"
|
||||
xs
|
||||
ruby
|
||||
faded
|
||||
@click="emit('delete', webhook, index)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<ShowMore :text="subscribedEvents" :limit="60" />
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell align="end" class="w-24">
|
||||
<div class="flex justify-end gap-3 flex-shrink-0">
|
||||
<Button
|
||||
v-tooltip.top="$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.BUTTON_TEXT')"
|
||||
icon="i-woot-edit-pen"
|
||||
slate
|
||||
sm
|
||||
@click="emit('edit', webhook)"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.top="
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.BUTTON_TEXT')
|
||||
"
|
||||
icon="i-woot-bin"
|
||||
slate
|
||||
sm
|
||||
@click="emit('delete', webhook, index)"
|
||||
/>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
</template>
|
||||
</BaseTableRow>
|
||||
</template>
|
||||
|
||||
+1
-23
@@ -6,7 +6,6 @@ import Index from './Index.vue';
|
||||
import Webhook from './Webhooks/Index.vue';
|
||||
import DashboardApps from './DashboardApps/Index.vue';
|
||||
import Slack from './Slack.vue';
|
||||
import SettingsContent from '../Wrapper.vue';
|
||||
import Linear from './Linear.vue';
|
||||
import Notion from './Notion.vue';
|
||||
import Shopify from './Shopify.vue';
|
||||
@@ -49,28 +48,7 @@ export default {
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/integrations'),
|
||||
component: SettingsContent,
|
||||
props: params => {
|
||||
const integrationId = params.params?.integration_id;
|
||||
const hideHeader = ['dialogflow'].includes(integrationId);
|
||||
|
||||
// Don't show header
|
||||
if (hideHeader) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const showBackButton = params.name !== 'settings_integrations';
|
||||
const backUrl =
|
||||
params.name === 'settings_integrations_integration'
|
||||
? { name: 'settings_integrations' }
|
||||
: '';
|
||||
return {
|
||||
headerTitle: 'INTEGRATION_SETTINGS.HEADER',
|
||||
icon: 'flash-on',
|
||||
showBackButton,
|
||||
backUrl,
|
||||
};
|
||||
},
|
||||
component: SettingsWrapper,
|
||||
children: [
|
||||
{
|
||||
path: 'slack',
|
||||
|
||||
@@ -3,12 +3,18 @@ import { useAlert } from 'dashboard/composables';
|
||||
import { computed, onBeforeMount, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
|
||||
import AddLabel from './AddLabel.vue';
|
||||
import EditLabel from './EditLabel.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import {
|
||||
BaseTable,
|
||||
BaseTableRow,
|
||||
BaseTableCell,
|
||||
} from 'dashboard/components-next/table';
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const store = useStore();
|
||||
@@ -19,8 +25,15 @@ const showAddPopup = ref(false);
|
||||
const showEditPopup = ref(false);
|
||||
const showDeleteConfirmationPopup = ref(false);
|
||||
const selectedLabel = ref({});
|
||||
const searchQuery = ref('');
|
||||
|
||||
const records = computed(() => getters['labels/getLabels'].value);
|
||||
|
||||
const filteredRecords = computed(() => {
|
||||
const query = searchQuery.value.trim();
|
||||
if (!query) return records.value;
|
||||
return picoSearch(records.value, query, ['title', 'description']);
|
||||
});
|
||||
const uiFlags = computed(() => getters['labels/getUIFlags'].value);
|
||||
|
||||
const deleteMessage = computed(() => ` ${selectedLabel.value.title}?`);
|
||||
@@ -72,6 +85,7 @@ const tableHeaders = computed(() => {
|
||||
t('LABEL_MGMT.LIST.TABLE_HEADER.NAME'),
|
||||
t('LABEL_MGMT.LIST.TABLE_HEADER.DESCRIPTION'),
|
||||
t('LABEL_MGMT.LIST.TABLE_HEADER.COLOR'),
|
||||
t('LABEL_MGMT.LIST.TABLE_HEADER.ACTION'),
|
||||
];
|
||||
});
|
||||
|
||||
@@ -89,14 +103,15 @@ onBeforeMount(() => {
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
v-model:search-query="searchQuery"
|
||||
:title="$t('LABEL_MGMT.HEADER')"
|
||||
:description="$t('LABEL_MGMT.DESCRIPTION')"
|
||||
:link-text="$t('LABEL_MGMT.LEARN_MORE')"
|
||||
:search-placeholder="$t('LABEL_MGMT.SEARCH_PLACEHOLDER')"
|
||||
feature-name="labels"
|
||||
>
|
||||
<template #actions>
|
||||
<Button
|
||||
icon="i-lucide-circle-plus"
|
||||
:label="$t('LABEL_MGMT.HEADER_BTN_TXT')"
|
||||
@click="openAddPopup"
|
||||
/>
|
||||
@@ -104,58 +119,64 @@ onBeforeMount(() => {
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<table class="min-w-full overflow-x-auto divide-y divide-n-weak">
|
||||
<thead>
|
||||
<th
|
||||
v-for="thHeader in tableHeaders"
|
||||
:key="thHeader"
|
||||
class="py-4 font-semibold text-left ltr:pr-4 rtl:pl-4 text-n-slate-11"
|
||||
>
|
||||
{{ thHeader }}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody class="flex-1 divide-y divide-n-weak text-n-slate-12">
|
||||
<tr v-for="(label, index) in records" :key="label.title">
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
<span class="mb-1 font-medium break-words text-n-slate-12">
|
||||
{{ label.title }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">{{ label.description }}</td>
|
||||
<td class="py-4 leading-6 ltr:pr-4 rtl:pl-4">
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
class="w-4 h-4 mr-1 border border-solid rounded rtl:mr-0 rtl:ml-1 border-n-weak"
|
||||
:style="{ backgroundColor: label.color }"
|
||||
/>
|
||||
{{ label.color }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 min-w-xs">
|
||||
<div class="flex gap-1 justify-end">
|
||||
<Button
|
||||
v-tooltip.top="$t('LABEL_MGMT.FORM.EDIT')"
|
||||
icon="i-lucide-pen"
|
||||
slate
|
||||
xs
|
||||
faded
|
||||
:is-loading="loading[label.id]"
|
||||
@click="openEditPopup(label)"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.top="$t('LABEL_MGMT.FORM.DELETE')"
|
||||
icon="i-lucide-trash-2"
|
||||
xs
|
||||
ruby
|
||||
faded
|
||||
:is-loading="loading[label.id]"
|
||||
@click="openDeletePopup(label, index)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<BaseTable
|
||||
:headers="tableHeaders"
|
||||
:items="filteredRecords"
|
||||
:no-data-message="
|
||||
searchQuery ? $t('LABEL_MGMT.NO_RESULTS') : $t('LABEL_MGMT.LIST.404')
|
||||
"
|
||||
>
|
||||
<template #row="{ items }">
|
||||
<BaseTableRow v-for="label in items" :key="label.title" :item="label">
|
||||
<template #default>
|
||||
<BaseTableCell>
|
||||
<span class="text-body-main text-n-slate-12">
|
||||
{{ label.title }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell>
|
||||
<span class="text-body-main text-n-slate-11">
|
||||
{{ label.description }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell>
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
class="w-4 h-4 ltr:mr-2 rtl:ml-2 border border-solid rounded border-n-weak"
|
||||
:style="{ backgroundColor: label.color }"
|
||||
/>
|
||||
<span class="text-body-main text-n-slate-12">
|
||||
{{ label.color }}
|
||||
</span>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell align="end">
|
||||
<div class="flex gap-3 justify-end flex-shrink-0">
|
||||
<Button
|
||||
v-tooltip.top="$t('LABEL_MGMT.FORM.EDIT')"
|
||||
icon="i-woot-edit-pen"
|
||||
slate
|
||||
sm
|
||||
:is-loading="loading[label.id]"
|
||||
@click="openEditPopup(label)"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.top="$t('LABEL_MGMT.FORM.DELETE')"
|
||||
icon="i-woot-bin"
|
||||
slate
|
||||
sm
|
||||
:is-loading="loading[label.id]"
|
||||
@click="openDeletePopup(label)"
|
||||
/>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
</template>
|
||||
</BaseTableRow>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</template>
|
||||
|
||||
<woot-modal v-model:show="showAddPopup" :on-close="hideAddPopup">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
import MacrosTableRow from './MacrosTableRow.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
@@ -7,6 +8,7 @@ import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { BaseTable } from 'dashboard/components-next/table';
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const store = useStore();
|
||||
@@ -14,10 +16,17 @@ const { t } = useI18n();
|
||||
|
||||
const showDeleteConfirmationPopup = ref(false);
|
||||
const selectedMacro = ref({});
|
||||
const searchQuery = ref('');
|
||||
|
||||
const records = computed(() => getters['macros/getMacros'].value);
|
||||
const uiFlags = computed(() => getters['macros/getUIFlags'].value);
|
||||
|
||||
const filteredRecords = computed(() => {
|
||||
const query = searchQuery.value.trim();
|
||||
if (!query) return records.value;
|
||||
return picoSearch(records.value, query, ['name']);
|
||||
});
|
||||
|
||||
const deleteMessage = computed(() => ` ${selectedMacro.value.name}?`);
|
||||
|
||||
onMounted(() => {
|
||||
@@ -53,6 +62,7 @@ const tableHeaders = computed(() => {
|
||||
t('MACROS.LIST.TABLE_HEADER.CREATED BY'),
|
||||
t('MACROS.LIST.TABLE_HEADER.LAST_UPDATED_BY'),
|
||||
t('MACROS.LIST.TABLE_HEADER.VISIBILITY'),
|
||||
t('MACROS.LIST.TABLE_HEADER.ACTIONS'),
|
||||
];
|
||||
});
|
||||
</script>
|
||||
@@ -67,41 +77,37 @@ const tableHeaders = computed(() => {
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
v-model:search-query="searchQuery"
|
||||
:title="$t('MACROS.HEADER')"
|
||||
:description="$t('MACROS.DESCRIPTION')"
|
||||
:link-text="$t('MACROS.LEARN_MORE')"
|
||||
:search-placeholder="$t('MACROS.SEARCH_PLACEHOLDER')"
|
||||
feature-name="macros"
|
||||
>
|
||||
<template #actions>
|
||||
<router-link :to="{ name: 'macros_new' }">
|
||||
<Button
|
||||
icon="i-lucide-circle-plus"
|
||||
:label="$t('MACROS.HEADER_BTN_TXT')"
|
||||
/>
|
||||
<Button :label="$t('MACROS.HEADER_BTN_TXT')" />
|
||||
</router-link>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<table class="min-w-full divide-y divide-n-weak">
|
||||
<thead>
|
||||
<th
|
||||
v-for="thHeader in tableHeaders"
|
||||
:key="thHeader"
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 text-left font-semibold text-n-slate-11"
|
||||
>
|
||||
{{ thHeader }}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-n-weak text-n-slate-11">
|
||||
<BaseTable
|
||||
:headers="tableHeaders"
|
||||
:items="filteredRecords"
|
||||
:no-data-message="
|
||||
searchQuery ? $t('MACROS.NO_RESULTS') : $t('MACROS.LIST.404')
|
||||
"
|
||||
>
|
||||
<template #row="{ items }">
|
||||
<MacrosTableRow
|
||||
v-for="(macro, index) in records"
|
||||
:key="index"
|
||||
v-for="macro in items"
|
||||
:key="macro.id"
|
||||
:macro="macro"
|
||||
@delete="openDeletePopup(macro)"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
</BaseTable>
|
||||
<woot-delete-modal
|
||||
v-model:show="showDeleteConfirmationPopup"
|
||||
:on-close="closeDeletePopup"
|
||||
|
||||
@@ -128,7 +128,7 @@ const saveMacro = async macroData => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 h-full overflow-auto">
|
||||
<div class="flex flex-col flex-1 h-full overflow-auto mx-2">
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.isFetchingItem"
|
||||
:message="t('MACROS.EDITOR.LOADING')"
|
||||
|
||||
@@ -108,9 +108,9 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col w-full h-auto md:flex-row md:h-full">
|
||||
<div class="flex flex-col w-full h-auto lg:flex-row lg:h-full">
|
||||
<div
|
||||
class="flex-1 w-full h-full max-h-full px-12 py-4 overflow-y-auto md:w-auto macro-gradient-radial dark:macro-dark-gradient-radial macro-gradient-radial-size"
|
||||
class="flex-1 w-full h-full max-h-full ltr:pl-12 ltr:pr-6 rtl:pl-6 rtl:pr-12 py-4 overflow-y-auto lg:w-auto macro-gradient-radial dark:macro-dark-gradient-radial macro-gradient-radial-size"
|
||||
>
|
||||
<MacroNodes
|
||||
v-model="macro.actions"
|
||||
@@ -121,7 +121,7 @@ export default {
|
||||
@reset-action="resetNode"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full md:w-1/3 pb-4">
|
||||
<div class="w-full lg:w-1/3 pb-4">
|
||||
<MacroProperties
|
||||
:macro-name="macro.name"
|
||||
:macro-visibility="macro.visibility"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script>
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
Icon,
|
||||
},
|
||||
inject: ['v$'],
|
||||
props: {
|
||||
@@ -53,56 +55,57 @@ export default {
|
||||
</p>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
||||
<button
|
||||
class="p-2 relative rounded-md border border-solid text-left cursor-default"
|
||||
class="p-2 relative rounded-md border border-solid justify-between items-start gap-2 flex flex-col text-start cursor-default"
|
||||
:class="isActive('global')"
|
||||
@click="onUpdateVisibility('global')"
|
||||
>
|
||||
<fluent-icon
|
||||
v-if="macroVisibility === 'global'"
|
||||
icon="checkmark-circle"
|
||||
type="solid"
|
||||
class="absolute text-n-brand top-2 right-2"
|
||||
/>
|
||||
<p
|
||||
class="block m-0 text-sm font-medium leading-[1.8] text-n-slate-12"
|
||||
>
|
||||
{{ $t('MACROS.EDITOR.VISIBILITY.GLOBAL.LABEL') }}
|
||||
</p>
|
||||
<p class="text-xs text-n-slate-11">
|
||||
<div class="flex items-center gap-2 min-w-0 justify-between w-full">
|
||||
<p class="block m-0 text-heading-3 text-n-slate-12 line-clamp-1">
|
||||
{{ $t('MACROS.EDITOR.VISIBILITY.GLOBAL.LABEL') }}
|
||||
</p>
|
||||
<Icon
|
||||
v-if="macroVisibility === 'global'"
|
||||
icon="i-lucide-circle-check-big"
|
||||
class="text-n-brand size-4"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-n-slate-11 text-label-small">
|
||||
{{ $t('MACROS.EDITOR.VISIBILITY.GLOBAL.DESCRIPTION') }}
|
||||
</p>
|
||||
</button>
|
||||
<button
|
||||
class="p-2 relative rounded-md border border-solid text-left cursor-default"
|
||||
class="p-2 relative rounded-md border border-solid justify-between items-start gap-2 flex flex-col text-start cursor-default"
|
||||
:class="isActive('personal')"
|
||||
@click="onUpdateVisibility('personal')"
|
||||
>
|
||||
<fluent-icon
|
||||
v-if="macroVisibility === 'personal'"
|
||||
icon="checkmark-circle"
|
||||
type="solid"
|
||||
class="absolute text-n-brand top-2 right-2"
|
||||
/>
|
||||
<p
|
||||
class="block m-0 text-sm font-medium leading-[1.8] text-n-slate-12"
|
||||
>
|
||||
{{ $t('MACROS.EDITOR.VISIBILITY.PERSONAL.LABEL') }}
|
||||
</p>
|
||||
<p class="text-xs text-n-slate-11">
|
||||
<div class="flex items-center gap-2 min-w-0 justify-between w-full">
|
||||
<p class="block m-0 text-heading-3 text-n-slate-12 line-clamp-1">
|
||||
{{ $t('MACROS.EDITOR.VISIBILITY.PERSONAL.LABEL') }}
|
||||
</p>
|
||||
<Icon
|
||||
v-if="macroVisibility === 'personal'"
|
||||
icon="i-lucide-circle-check-big"
|
||||
class="text-n-brand size-4"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-n-slate-11 text-label-small">
|
||||
{{ $t('MACROS.EDITOR.VISIBILITY.PERSONAL.DESCRIPTION') }}
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mt-2 flex items-start p-2 bg-n-slate-3 dark:bg-n-solid-3 rounded-md"
|
||||
class="mt-2 flex items-start p-2 bg-n-alpha-1 gap-2 dark:bg-n-solid-3 rounded-md"
|
||||
>
|
||||
<fluent-icon icon="info" size="16" class="flex-shrink-0 mt-0.5" />
|
||||
<p class="ml-2 rtl:ml-0 rtl:mr-2 mb-0 text-n-slate-11">
|
||||
<Icon
|
||||
icon="i-lucide-info"
|
||||
class="flex-shrink-0 mt-0.5 size-4 text-n-slate-11"
|
||||
/>
|
||||
<p class="mb-0 text-n-slate-11 text-body-para">
|
||||
{{ $t('MACROS.ORDER_INFO') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-auto w-full">
|
||||
<div class="mt-4 w-full">
|
||||
<NextButton
|
||||
blue
|
||||
solid
|
||||
|
||||
@@ -4,6 +4,7 @@ import Avatar from 'next/avatar/Avatar.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { BaseTableRow, BaseTableCell } from 'dashboard/components-next/table';
|
||||
|
||||
const props = defineProps({
|
||||
macro: {
|
||||
@@ -34,41 +35,71 @@ const visibilityLabel = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4 truncate">{{ macro.name }}</td>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
<div v-if="macro.created_by" class="flex items-center">
|
||||
<Avatar :name="createdByName" :size="24" rounded-full />
|
||||
<span class="mx-2">{{ createdByName }}</span>
|
||||
</div>
|
||||
<div v-else>--</div>
|
||||
</td>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
<div v-if="macro.updated_by" class="flex items-center">
|
||||
<Avatar :name="updatedByName" :size="24" rounded-full />
|
||||
<span class="mx-2">{{ updatedByName }}</span>
|
||||
</div>
|
||||
<div v-else>--</div>
|
||||
</td>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">{{ visibilityLabel }}</td>
|
||||
<td class="py-4 flex justify-end gap-1">
|
||||
<router-link :to="{ name: 'macros_edit', params: { macroId: macro.id } }">
|
||||
<Button
|
||||
v-tooltip.top="$t('MACROS.EDIT.TOOLTIP')"
|
||||
icon="i-lucide-pen"
|
||||
slate
|
||||
xs
|
||||
faded
|
||||
/>
|
||||
</router-link>
|
||||
<Button
|
||||
v-tooltip.top="$t('MACROS.DELETE.TOOLTIP')"
|
||||
icon="i-lucide-trash-2"
|
||||
xs
|
||||
ruby
|
||||
faded
|
||||
@click="$emit('delete')"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<BaseTableRow :item="macro">
|
||||
<template #default>
|
||||
<BaseTableCell class="max-w-0 min-w-0">
|
||||
<span class="text-body-main text-n-slate-12 truncate block">
|
||||
{{ macro.name }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell class="max-w-0">
|
||||
<div v-if="macro.created_by" class="flex items-center gap-2 min-w-0">
|
||||
<Avatar
|
||||
:name="createdByName"
|
||||
:size="24"
|
||||
rounded-full
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<span class="text-body-main text-n-slate-12 truncate">
|
||||
{{ createdByName }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-else class="text-body-main text-n-slate-11">--</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell class="max-w-0">
|
||||
<div v-if="macro.updated_by" class="flex items-center gap-2 min-w-0">
|
||||
<Avatar
|
||||
:name="updatedByName"
|
||||
:size="24"
|
||||
rounded-full
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<span class="text-body-main text-n-slate-12 truncate">
|
||||
{{ updatedByName }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-else class="text-body-main text-n-slate-11">--</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell class="max-w-0">
|
||||
<span class="text-body-main text-n-slate-12 whitespace-nowrap">
|
||||
{{ visibilityLabel }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell align="end" class="w-24">
|
||||
<div class="flex gap-3 justify-end flex-shrink-0">
|
||||
<router-link
|
||||
:to="{ name: 'macros_edit', params: { macroId: macro.id } }"
|
||||
>
|
||||
<Button
|
||||
v-tooltip.top="$t('MACROS.EDIT.TOOLTIP')"
|
||||
icon="i-woot-edit-pen"
|
||||
slate
|
||||
sm
|
||||
/>
|
||||
</router-link>
|
||||
<Button
|
||||
v-tooltip.top="$t('MACROS.DELETE.TOOLTIP')"
|
||||
icon="i-woot-bin"
|
||||
slate
|
||||
sm
|
||||
@click="$emit('delete')"
|
||||
/>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
</template>
|
||||
</BaseTableRow>
|
||||
</template>
|
||||
|
||||
@@ -17,7 +17,8 @@ import HotKeyCard from './HotKeyCard.vue';
|
||||
import ChangePassword from './ChangePassword.vue';
|
||||
import NotificationPreferences from './NotificationPreferences.vue';
|
||||
import AudioNotifications from './AudioNotifications.vue';
|
||||
import FormSection from 'dashboard/components/FormSection.vue';
|
||||
import SectionLayout from '../account/components/SectionLayout.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import AccessToken from './AccessToken.vue';
|
||||
import MfaSettingsCard from './MfaSettingsCard.vue';
|
||||
import Policy from 'dashboard/components/policy.vue';
|
||||
@@ -29,7 +30,7 @@ import {
|
||||
export default {
|
||||
components: {
|
||||
MessageSignature,
|
||||
FormSection,
|
||||
SectionLayout,
|
||||
FontSize,
|
||||
UserLanguageSelect,
|
||||
UserProfilePicture,
|
||||
@@ -41,6 +42,7 @@ export default {
|
||||
AudioNotifications,
|
||||
AccessToken,
|
||||
MfaSettingsCard,
|
||||
BaseSettingsHeader,
|
||||
},
|
||||
setup() {
|
||||
const { isEditorHotKeyEnabled, updateUISettings } = useUISettings();
|
||||
@@ -203,26 +205,27 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid py-16 px-5 font-inter mx-auto gap-16 sm:max-w-screen-md">
|
||||
<div class="flex flex-col gap-6">
|
||||
<h2 class="text-2xl font-medium text-n-slate-12">
|
||||
{{ $t('PROFILE_SETTINGS.TITLE') }}
|
||||
</h2>
|
||||
<UserProfilePicture
|
||||
:src="avatarUrl"
|
||||
:name="name"
|
||||
@change="updateProfilePicture"
|
||||
@delete="deleteProfilePicture"
|
||||
/>
|
||||
<UserBasicDetails
|
||||
:name="name"
|
||||
:display-name="displayName"
|
||||
:email="email"
|
||||
:email-enabled="!globalConfig.disableUserProfileUpdate"
|
||||
@update-user="updateProfile"
|
||||
/>
|
||||
</div>
|
||||
<FormSection
|
||||
<div class="grid">
|
||||
<BaseSettingsHeader :title="$t('PROFILE_SETTINGS.TITLE')" description="" />
|
||||
<SectionLayout title="" description="" class="!pt-0">
|
||||
<div class="flex flex-col gap-6">
|
||||
<UserProfilePicture
|
||||
:src="avatarUrl"
|
||||
:name="name"
|
||||
@change="updateProfilePicture"
|
||||
@delete="deleteProfilePicture"
|
||||
/>
|
||||
<UserBasicDetails
|
||||
:name="name"
|
||||
:display-name="displayName"
|
||||
:email="email"
|
||||
:email-enabled="!globalConfig.disableUserProfileUpdate"
|
||||
@update-user="updateProfile"
|
||||
/>
|
||||
</div>
|
||||
</SectionLayout>
|
||||
<SectionLayout
|
||||
with-border
|
||||
:title="$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.TITLE')"
|
||||
:description="
|
||||
replaceInstallationName(
|
||||
@@ -230,22 +233,25 @@ export default {
|
||||
)
|
||||
"
|
||||
>
|
||||
<FontSize
|
||||
:value="currentFontSize"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.TITLE')"
|
||||
:description="
|
||||
$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.NOTE')
|
||||
"
|
||||
@change="updateFontSize"
|
||||
/>
|
||||
<UserLanguageSelect
|
||||
:label="$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.LANGUAGE.TITLE')"
|
||||
:description="
|
||||
$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.LANGUAGE.NOTE')
|
||||
"
|
||||
/>
|
||||
</FormSection>
|
||||
<FormSection
|
||||
<div class="flex flex-col gap-6 items-start">
|
||||
<FontSize
|
||||
:value="currentFontSize"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.TITLE')"
|
||||
:description="
|
||||
$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.FONT_SIZE.NOTE')
|
||||
"
|
||||
@change="updateFontSize"
|
||||
/>
|
||||
<UserLanguageSelect
|
||||
:label="$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.LANGUAGE.TITLE')"
|
||||
:description="
|
||||
$t('PROFILE_SETTINGS.FORM.INTERFACE_SECTION.LANGUAGE.NOTE')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</SectionLayout>
|
||||
<SectionLayout
|
||||
with-border
|
||||
:title="$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.TITLE')"
|
||||
:description="$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.NOTE')"
|
||||
>
|
||||
@@ -253,8 +259,9 @@ export default {
|
||||
:message-signature="messageSignature"
|
||||
@update-signature="updateSignature"
|
||||
/>
|
||||
</FormSection>
|
||||
<FormSection
|
||||
</SectionLayout>
|
||||
<SectionLayout
|
||||
with-border
|
||||
:title="$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.TITLE')"
|
||||
:description="$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.NOTE')"
|
||||
>
|
||||
@@ -282,36 +289,45 @@ export default {
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</FormSection>
|
||||
<FormSection
|
||||
</SectionLayout>
|
||||
<SectionLayout
|
||||
v-if="!globalConfig.disableUserProfileUpdate"
|
||||
with-border
|
||||
:title="$t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.TITLE')"
|
||||
description=""
|
||||
>
|
||||
<ChangePassword />
|
||||
</FormSection>
|
||||
<FormSection
|
||||
</SectionLayout>
|
||||
<SectionLayout
|
||||
v-if="isMfaEnabled"
|
||||
with-border
|
||||
:title="$t('PROFILE_SETTINGS.FORM.SECURITY_SECTION.TITLE')"
|
||||
:description="$t('PROFILE_SETTINGS.FORM.SECURITY_SECTION.NOTE')"
|
||||
>
|
||||
<MfaSettingsCard />
|
||||
</FormSection>
|
||||
</SectionLayout>
|
||||
<Policy :permissions="audioNotificationPermissions">
|
||||
<FormSection
|
||||
<SectionLayout
|
||||
with-border
|
||||
:title="$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.TITLE')"
|
||||
:description="
|
||||
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.NOTE')
|
||||
"
|
||||
>
|
||||
<AudioNotifications />
|
||||
</FormSection>
|
||||
</SectionLayout>
|
||||
</Policy>
|
||||
<Policy :permissions="notificationPermissions">
|
||||
<FormSection :title="$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TITLE')">
|
||||
<SectionLayout
|
||||
with-border
|
||||
:title="$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TITLE')"
|
||||
description=""
|
||||
>
|
||||
<NotificationPreferences />
|
||||
</FormSection>
|
||||
</SectionLayout>
|
||||
</Policy>
|
||||
<FormSection
|
||||
<SectionLayout
|
||||
with-border
|
||||
:title="$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.TITLE')"
|
||||
:description="
|
||||
replaceInstallationName($t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.NOTE'))
|
||||
@@ -322,6 +338,6 @@ export default {
|
||||
@on-copy="onCopyToken"
|
||||
@on-reset="resetAccessToken"
|
||||
/>
|
||||
</FormSection>
|
||||
</SectionLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useAlert } from 'dashboard/composables';
|
||||
import MfaStatusCard from './MfaStatusCard.vue';
|
||||
import MfaSetupWizard from './MfaSetupWizard.vue';
|
||||
import MfaManagementActions from './MfaManagementActions.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
@@ -130,18 +131,12 @@ const regenerateBackupCodes = async ({ otpCode }) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="grid py-16 px-5 font-inter mx-auto gap-16 sm:max-w-screen-md w-full"
|
||||
>
|
||||
<!-- Page Header -->
|
||||
<div class="flex flex-col gap-6">
|
||||
<h2 class="text-2xl font-medium text-n-slate-12">
|
||||
{{ $t('MFA_SETTINGS.TITLE') }}
|
||||
</h2>
|
||||
<p class="text-sm text-n-slate-11">
|
||||
{{ $t('MFA_SETTINGS.SUBTITLE') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid w-full">
|
||||
<BaseSettingsHeader
|
||||
:title="$t('MFA_SETTINGS.TITLE')"
|
||||
:description="$t('MFA_SETTINGS.SUBTITLE')"
|
||||
:back-button-label="$t('PROFILE_SETTINGS.TITLE')"
|
||||
/>
|
||||
|
||||
<div class="grid gap-4 w-full">
|
||||
<!-- MFA Status Card -->
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
import { parseBoolean } from '@chatwoot/utils';
|
||||
|
||||
import SettingsContent from './Wrapper.vue';
|
||||
import SettingsWrapper from '../SettingsWrapper.vue';
|
||||
import Index from './Index.vue';
|
||||
import MfaSettings from './MfaSettings.vue';
|
||||
|
||||
@@ -13,7 +13,7 @@ export default {
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'custom_role'],
|
||||
},
|
||||
component: SettingsContent,
|
||||
component: SettingsWrapper,
|
||||
children: [
|
||||
{
|
||||
path: 'settings',
|
||||
|
||||
+9
-4
@@ -18,17 +18,22 @@ defineProps({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="flex flex-col gap-1 pt-10 pb-5">
|
||||
<section class="flex flex-col gap-1 pt-6 pb-5">
|
||||
<div v-if="hasBackButton">
|
||||
<BackButton compact />
|
||||
</div>
|
||||
<div class="flex justify-between w-full gap-5">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div>
|
||||
<span class="text-xl font-medium text-n-slate-12">
|
||||
<div class="flex flex-col gap-3 items-start">
|
||||
<span
|
||||
class="text-heading-1 text-n-slate-12 min-h-10 flex items-center"
|
||||
>
|
||||
{{ headerTitle }}
|
||||
</span>
|
||||
<p v-if="headerDescription" class="text-n-slate-12 mt-2">
|
||||
<p
|
||||
v-if="headerDescription"
|
||||
class="text-n-slate-11 mb-0 line-clamp-5 sm:line-clamp-none text-body-main"
|
||||
>
|
||||
{{ headerDescription }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="reports--wrapper overflow-auto bg-n-surface-1 w-full px-6">
|
||||
<div class="max-w-[60rem] mx-auto pb-12">
|
||||
<div class="max-w-5xl mx-auto pb-12">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,10 +31,7 @@ const showPaywall = computed(() => shouldShowPaywall('saml'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsLayout
|
||||
class="max-w-2xl mx-auto"
|
||||
:loading-message="$t('ATTRIBUTES_MGMT.LOADING')"
|
||||
>
|
||||
<SettingsLayout :loading-message="$t('ATTRIBUTES_MGMT.LOADING')">
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
:title="$t('SECURITY_SETTINGS.TITLE')"
|
||||
|
||||
@@ -1,25 +1,34 @@
|
||||
<script>
|
||||
import AddSLA from './AddSLA.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import SLAEmptyState from './components/SLAEmptyState.vue';
|
||||
import SLAHeader from './components/SLAHeader.vue';
|
||||
import SLAListItem from './components/SLAListItem.vue';
|
||||
import SLAListItemLoading from './components/SLAListItemLoading.vue';
|
||||
import SLAPaywallEnterprise from './components/SLAPaywallEnterprise.vue';
|
||||
import BaseSettingsHeader from 'dashboard/routes/dashboard/settings/components/BaseSettingsHeader.vue';
|
||||
import SLAPaywallEnterprise from './SLAPaywallEnterprise.vue';
|
||||
import {
|
||||
BaseTable,
|
||||
BaseTableRow,
|
||||
BaseTableCell,
|
||||
} from 'dashboard/components-next/table';
|
||||
import WootLabel from 'dashboard/components-next/label/Label.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import WootButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
import { mapGetters } from 'vuex';
|
||||
import { convertSecondsToTimeUnit } from '@chatwoot/utils';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddSLA,
|
||||
SettingsLayout,
|
||||
SLAEmptyState,
|
||||
SLAHeader,
|
||||
SLAListItem,
|
||||
SLAListItemLoading,
|
||||
BaseSettingsHeader,
|
||||
SLAPaywallEnterprise,
|
||||
BaseTable,
|
||||
BaseTableRow,
|
||||
BaseTableCell,
|
||||
WootLabel,
|
||||
Icon,
|
||||
WootButton,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -27,6 +36,7 @@ export default {
|
||||
showAddPopup: false,
|
||||
showDeleteConfirmationPopup: false,
|
||||
selectedResponse: {},
|
||||
searchQuery: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -53,6 +63,21 @@ export default {
|
||||
isSuperAdmin() {
|
||||
return this.currentUser.type === 'SuperAdmin';
|
||||
},
|
||||
tableHeaders() {
|
||||
return [
|
||||
this.$t('SLA.LIST.TABLE_HEADER.SLA'),
|
||||
this.$t('SLA.LIST.TABLE_HEADER.BUSINESS_HOURS'),
|
||||
this.$t('SLA.LIST.RESPONSE_TYPES.SHORT_HAND.FRT'),
|
||||
this.$t('SLA.LIST.RESPONSE_TYPES.SHORT_HAND.NRT'),
|
||||
this.$t('SLA.LIST.RESPONSE_TYPES.SHORT_HAND.RT'),
|
||||
this.$t('INTEGRATION_APPS.LIST.ACTIONS'),
|
||||
];
|
||||
},
|
||||
filteredRecords() {
|
||||
const query = this.searchQuery.trim();
|
||||
if (!query) return this.records;
|
||||
return picoSearch(this.records, query, ['name', 'description']);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('sla/get');
|
||||
@@ -117,10 +142,20 @@ export default {
|
||||
:loading-message="$t('SLA.LOADING')"
|
||||
>
|
||||
<template #header>
|
||||
<SLAHeader :show-actions="records.length > 0" @add="openAddPopup" />
|
||||
</template>
|
||||
<template #loading>
|
||||
<SLAListItemLoading v-for="ii in 2" :key="ii" class="mb-3" />
|
||||
<BaseSettingsHeader
|
||||
v-model:search-query="searchQuery"
|
||||
:title="$t('SLA.HEADER')"
|
||||
:description="$t('SLA.DESCRIPTION')"
|
||||
:link-text="$t('SLA.LEARN_MORE')"
|
||||
:search-placeholder="
|
||||
isBehindAPaywall ? '' : $t('SLA.SEARCH_PLACEHOLDER')
|
||||
"
|
||||
feature-name="sla"
|
||||
>
|
||||
<template v-if="!isBehindAPaywall" #actions>
|
||||
<WootButton :label="$t('SLA.ADD_ACTION')" @click="openAddPopup" />
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<SLAPaywallEnterprise
|
||||
@@ -129,24 +164,130 @@ export default {
|
||||
:is-on-chatwoot-cloud="isOnChatwootCloud"
|
||||
@upgrade="onClickCTA"
|
||||
/>
|
||||
<SLAEmptyState
|
||||
v-else-if="!records.length"
|
||||
@primary-action="openAddPopup"
|
||||
/>
|
||||
<div v-else class="flex flex-col w-full h-full gap-3">
|
||||
<SLAListItem
|
||||
v-for="sla in records"
|
||||
:key="sla.title"
|
||||
:sla-name="sla.name"
|
||||
:description="sla.description"
|
||||
:first-response="displayTime(sla.first_response_time_threshold)"
|
||||
:next-response="displayTime(sla.next_response_time_threshold)"
|
||||
:resolution-time="displayTime(sla.resolution_time_threshold)"
|
||||
:has-business-hours="sla.only_during_business_hours"
|
||||
:is-loading="loading[sla.id]"
|
||||
@delete="openDeletePopup(sla)"
|
||||
/>
|
||||
</div>
|
||||
<BaseTable
|
||||
v-else
|
||||
:headers="tableHeaders"
|
||||
:items="filteredRecords"
|
||||
:no-data-message="
|
||||
!records.length
|
||||
? $t('SLA.LIST.404')
|
||||
: searchQuery && !filteredRecords.length
|
||||
? $t('SLA.SEARCH.NO_RESULTS')
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<template #header-2>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-heading-3">
|
||||
{{ $t('SLA.LIST.RESPONSE_TYPES.SHORT_HAND.FRT') }}
|
||||
</span>
|
||||
<Icon
|
||||
v-tooltip.left="$t('SLA.LIST.RESPONSE_TYPES.FRT')"
|
||||
icon="i-lucide-info"
|
||||
class="size-3.5 text-n-slate-10 cursor-help"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #header-3>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-heading-3">
|
||||
{{ $t('SLA.LIST.RESPONSE_TYPES.SHORT_HAND.NRT') }}
|
||||
</span>
|
||||
<Icon
|
||||
v-tooltip.left="$t('SLA.LIST.RESPONSE_TYPES.NRT')"
|
||||
icon="i-lucide-info"
|
||||
class="size-3.5 text-n-slate-10 cursor-help"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #header-4>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-heading-3">
|
||||
{{ $t('SLA.LIST.RESPONSE_TYPES.SHORT_HAND.RT') }}
|
||||
</span>
|
||||
<Icon
|
||||
v-tooltip.left="$t('SLA.LIST.RESPONSE_TYPES.RT')"
|
||||
icon="i-lucide-info"
|
||||
class="size-3.5 text-n-slate-10 cursor-help"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #row="{ items }">
|
||||
<BaseTableRow v-for="sla in items" :key="sla.id" :item="sla">
|
||||
<template #default>
|
||||
<BaseTableCell>
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<span class="text-body-main text-n-slate-12 truncate">
|
||||
{{ sla.name }}
|
||||
</span>
|
||||
<span class="text-body-main text-n-slate-11 line-clamp-1">
|
||||
{{ sla.description }}
|
||||
</span>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell class="w-40">
|
||||
<WootLabel
|
||||
:label="
|
||||
sla.only_during_business_hours
|
||||
? $t('SLA.LIST.BUSINESS_HOURS_ON')
|
||||
: $t('SLA.LIST.BUSINESS_HOURS_OFF')
|
||||
"
|
||||
:color="sla.only_during_business_hours ? 'teal' : 'slate'"
|
||||
compact
|
||||
>
|
||||
<template #icon>
|
||||
<Icon
|
||||
:icon="
|
||||
sla.only_during_business_hours
|
||||
? 'i-lucide-alarm-clock-check'
|
||||
: 'i-lucide-alarm-clock-off'
|
||||
"
|
||||
class="size-3.5"
|
||||
:class="
|
||||
sla.only_during_business_hours
|
||||
? 'text-n-teal-11'
|
||||
: 'text-n-slate-11'
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</WootLabel>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell align="start" class="w-24">
|
||||
<span class="text-body-main text-n-slate-12">
|
||||
{{ displayTime(sla.first_response_time_threshold) }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell align="start" class="w-24">
|
||||
<span class="text-body-main text-n-slate-12">
|
||||
{{ displayTime(sla.next_response_time_threshold) }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell align="start" class="w-24">
|
||||
<span class="text-body-main text-n-slate-12">
|
||||
{{ displayTime(sla.resolution_time_threshold) }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell align="end" class="w-12">
|
||||
<div class="flex justify-end">
|
||||
<WootButton
|
||||
v-tooltip.top="$t('SLA.FORM.DELETE')"
|
||||
icon="i-woot-bin"
|
||||
slate
|
||||
sm
|
||||
:is-loading="loading[sla.id]"
|
||||
@click="openDeletePopup(sla)"
|
||||
/>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
</template>
|
||||
</BaseTableRow>
|
||||
</template>
|
||||
</BaseTable>
|
||||
|
||||
<woot-modal v-model:show="showAddPopup" :on-close="hideAddPopup">
|
||||
<AddSLA @close="hideAddPopup" />
|
||||
|
||||
+2
-3
@@ -1,5 +1,4 @@
|
||||
<script setup>
|
||||
import BaseEmptyState from './BaseEmptyState.vue';
|
||||
import BasePaywallModal from 'dashboard/routes/dashboard/settings/components/BasePaywallModal.vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -18,7 +17,7 @@ const i18nKey = props.isOnChatwootCloud ? 'PAYWALL' : 'ENTERPRISE_PAYWALL';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseEmptyState>
|
||||
<div class="pb-6 pt-10 flex justify-center">
|
||||
<BasePaywallModal
|
||||
feature-prefix="SLA"
|
||||
:i18n-key="i18nKey"
|
||||
@@ -26,5 +25,5 @@ const i18nKey = props.isOnChatwootCloud ? 'PAYWALL' : 'ENTERPRISE_PAYWALL';
|
||||
:is-super-admin="isSuperAdmin"
|
||||
@upgrade="emit('upgrade')"
|
||||
/>
|
||||
</BaseEmptyState>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,33 +0,0 @@
|
||||
<script setup>
|
||||
import SLAListItem from './SLAListItem.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full min-h-[12rem] relative">
|
||||
<div class="w-full space-y-3">
|
||||
<SLAListItem
|
||||
class="opacity-25 dark:opacity-20"
|
||||
:sla-name="$t('SLA.LIST.EMPTY.TITLE_1')"
|
||||
:description="$t('SLA.LIST.EMPTY.DESC_1')"
|
||||
first-response="20m"
|
||||
next-response="1h"
|
||||
resolution-time="24h"
|
||||
has-business-hours
|
||||
/>
|
||||
<SLAListItem
|
||||
class="opacity-25 dark:opacity-20"
|
||||
:sla-name="$t('SLA.LIST.EMPTY.TITLE_2')"
|
||||
:description="$t('SLA.LIST.EMPTY.DESC_2')"
|
||||
first-response="2h"
|
||||
next-response="4h"
|
||||
resolution-time="4d"
|
||||
has-business-hours
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="absolute inset-0 flex flex-col items-center justify-center w-full h-full bg-gradient-to-t from-white dark:from-slate-900 to-transparent"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
hasBusinessHours: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="inline-flex items-center min-w-0 gap-1 px-1.5 sm:px-2 py-1 border border-solid rounded-lg border-n-weak"
|
||||
>
|
||||
<fluent-icon
|
||||
size="14"
|
||||
:icon="hasBusinessHours ? 'alarm-on' : 'alarm-off'"
|
||||
type="outline"
|
||||
class="flex-shrink-0"
|
||||
:class="hasBusinessHours ? 'text-n-slate-11' : 'text-n-slate-10'"
|
||||
/>
|
||||
<span
|
||||
class="hidden text-xs tracking-[0.2%] font-normal truncate sm:block"
|
||||
:class="hasBusinessHours ? 'text-n-slate-11' : 'text-n-slate-10'"
|
||||
>
|
||||
{{
|
||||
hasBusinessHours
|
||||
? $t('SLA.LIST.BUSINESS_HOURS_ON')
|
||||
: $t('SLA.LIST.BUSINESS_HOURS_OFF')
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,21 +0,0 @@
|
||||
<script setup>
|
||||
import BaseEmptyState from './BaseEmptyState.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const emit = defineEmits(['primaryAction']);
|
||||
const primaryAction = () => emit('primaryAction');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseEmptyState>
|
||||
<p class="max-w-xs text-sm font-medium text-center">
|
||||
{{ $t('SLA.LIST.404') }}
|
||||
</p>
|
||||
<NextButton
|
||||
icon="i-lucide-plus"
|
||||
class="mt-4"
|
||||
:label="$t('SLA.ADD_ACTION_LONG')"
|
||||
@click="primaryAction"
|
||||
/>
|
||||
</BaseEmptyState>
|
||||
</template>
|
||||
@@ -1,30 +0,0 @@
|
||||
<script setup>
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import BaseSettingsHeader from '../../components/BaseSettingsHeader.vue';
|
||||
|
||||
defineProps({
|
||||
showActions: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(['add']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseSettingsHeader
|
||||
:title="$t('SLA.HEADER')"
|
||||
:description="$t('SLA.DESCRIPTION')"
|
||||
:link-text="$t('SLA.LEARN_MORE')"
|
||||
feature-name="sla"
|
||||
>
|
||||
<template v-if="showActions" #actions>
|
||||
<Button
|
||||
:label="$t('SLA.ADD_ACTION')"
|
||||
icon="i-lucide-circle-plus"
|
||||
@click="$emit('add')"
|
||||
/>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
@@ -1,71 +0,0 @@
|
||||
<script setup>
|
||||
import BaseSettingsListItem from '../../components/BaseSettingsListItem.vue';
|
||||
import SLAResponseTime from './SLAResponseTime.vue';
|
||||
import SLABusinessHoursLabel from './SLABusinessHoursLabel.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
slaName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
firstResponse: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
nextResponse: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
resolutionTime: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
hasBusinessHours: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['delete']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseSettingsListItem
|
||||
class="sm:divide-x sm:divide-n-weak"
|
||||
:title="slaName"
|
||||
:description="description"
|
||||
>
|
||||
<template #label>
|
||||
<SLABusinessHoursLabel :has-business-hours="hasBusinessHours" />
|
||||
</template>
|
||||
<template #rightSection>
|
||||
<div
|
||||
class="flex items-center divide-x rtl:divide-x-reverse sm:rtl:!border-l-0 sm:rtl:!border-r sm:rtl:border-solid sm:rtl:border-n-weak gap-1.5 w-fit sm:w-full sm:gap-0 sm:justify-between divide-n-weak"
|
||||
>
|
||||
<SLAResponseTime response-type="FRT" :response-time="firstResponse" />
|
||||
<SLAResponseTime response-type="NRT" :response-time="nextResponse" />
|
||||
<SLAResponseTime response-type="RT" :response-time="resolutionTime" />
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<Button
|
||||
v-tooltip.top="$t('SLA.FORM.DELETE')"
|
||||
faded
|
||||
ruby
|
||||
xs
|
||||
icon="i-lucide-trash-2"
|
||||
:is-loading="isLoading"
|
||||
@click="emit('delete')"
|
||||
/>
|
||||
</template>
|
||||
</BaseSettingsListItem>
|
||||
</template>
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
<script setup>
|
||||
import BaseSettingsListItem from '../../components/BaseSettingsListItem.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseSettingsListItem class="opacity-50">
|
||||
<template #title>
|
||||
<div
|
||||
class="w-24 h-[26px] rounded-md bg-n-slate-4 dark:bg-n-slate-6 animate-pulse"
|
||||
/>
|
||||
</template>
|
||||
<template #description>
|
||||
<div
|
||||
class="w-64 h-4 mb-0.5 rounded-md bg-n-slate-4 dark:bg-n-slate-6 animate-pulse"
|
||||
/>
|
||||
<div
|
||||
class="w-48 h-4 rounded-md bg-n-slate-4 dark:bg-n-slate-6 animate-pulse"
|
||||
/>
|
||||
</template>
|
||||
<template #label>
|
||||
<div
|
||||
class="w-32 h-[26px] bg-n-slate-4 dark:bg-n-slate-6 animate-pulse rounded-md"
|
||||
/>
|
||||
</template>
|
||||
<template #rightSection>
|
||||
<div
|
||||
class="flex items-center sm:rtl:!border-l-0 sm:rtl:!border-r border-n-weak gap-1.5 w-fit sm:w-full sm:gap-0 sm:justify-between"
|
||||
>
|
||||
<div
|
||||
v-for="ii in 3"
|
||||
:key="ii"
|
||||
class="flex justify-end w-1/3 h-full px-4"
|
||||
>
|
||||
<div
|
||||
class="w-32 h-full rounded-md bg-n-slate-4 dark:bg-n-slate-6 animate-pulse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BaseSettingsListItem>
|
||||
</template>
|
||||
@@ -1,37 +0,0 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
responseType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
responseTime: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-row items-start w-full h-full gap-1 sm:items-end sm:px-6 sm:py-2 sm:gap-2 sm:flex-col"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center gap-1 tracking-[-0.6%] text-sm ltr:pl-1.5 sm:ltr:pl-0 rtl:pr-1.5 sm:rtl:pr-0 text-n-slate-11"
|
||||
>
|
||||
<fluent-icon
|
||||
v-tooltip.left="$t(`SLA.LIST.RESPONSE_TYPES.${responseType}`)"
|
||||
size="14"
|
||||
icon="information"
|
||||
type="outline"
|
||||
class="flex-shrink-0 hidden text-sm font-normal sm:flex sm:font-medium text-n-slate-11"
|
||||
/>
|
||||
{{ $t(`SLA.LIST.RESPONSE_TYPES.SHORT_HAND.${responseType}`) }}
|
||||
<span class="flex sm:hidden">:</span>
|
||||
</span>
|
||||
<span
|
||||
class="text-sm sm:text-2xl font-medium tracking-[-1.5%] text-n-slate-12"
|
||||
>
|
||||
{{ responseTime }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,120 +1,112 @@
|
||||
<script>
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||
import BaseTable from 'dashboard/components-next/table/BaseTable.vue';
|
||||
import BaseTableRow from 'dashboard/components-next/table/BaseTableRow.vue';
|
||||
import BaseTableCell from 'dashboard/components-next/table/BaseTableCell.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
Avatar,
|
||||
const props = defineProps({
|
||||
agentList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
props: {
|
||||
agentList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedAgents: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
updateSelectedAgents: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
isWorking: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
submitButtonText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
selectedAgents: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
updateSelectedAgents: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
computed: {
|
||||
selectedAgentCount() {
|
||||
return this.selectedAgents.length;
|
||||
},
|
||||
allAgentsSelected() {
|
||||
return this.selectedAgents.length === this.agentList.length;
|
||||
},
|
||||
disableSubmitButton() {
|
||||
return this.selectedAgentCount === 0;
|
||||
},
|
||||
isWorking: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
methods: {
|
||||
isAgentSelected(agentId) {
|
||||
return this.selectedAgents.includes(agentId);
|
||||
},
|
||||
handleSelectAgent(agentId) {
|
||||
const shouldRemove = this.isAgentSelected(agentId);
|
||||
submitButtonText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
let result = [];
|
||||
if (shouldRemove) {
|
||||
result = this.selectedAgents.filter(item => item !== agentId);
|
||||
} else {
|
||||
result = [...this.selectedAgents, agentId];
|
||||
}
|
||||
const { t } = useI18n();
|
||||
|
||||
this.updateSelectedAgents(result);
|
||||
},
|
||||
selectAllAgents() {
|
||||
const result = this.agentList.map(item => item.id);
|
||||
this.updateSelectedAgents(result);
|
||||
},
|
||||
agentRowClass(agentId) {
|
||||
return { 'is-active': this.isAgentSelected(agentId) };
|
||||
},
|
||||
},
|
||||
const selectedAgentCount = computed(() => props.selectedAgents.length);
|
||||
|
||||
const allAgentsSelected = computed(
|
||||
() =>
|
||||
props.selectedAgents.length === props.agentList.length &&
|
||||
props.agentList.length > 0
|
||||
);
|
||||
|
||||
const someAgentsSelected = computed(
|
||||
() => props.selectedAgents.length > 0 && !allAgentsSelected.value
|
||||
);
|
||||
|
||||
const disableSubmitButton = computed(() => selectedAgentCount.value === 0);
|
||||
|
||||
const isAgentSelected = agentId => {
|
||||
return props.selectedAgents.includes(agentId);
|
||||
};
|
||||
|
||||
const handleSelectAgent = agentId => {
|
||||
const shouldRemove = isAgentSelected(agentId);
|
||||
|
||||
let result = [];
|
||||
if (shouldRemove) {
|
||||
result = props.selectedAgents.filter(item => item !== agentId);
|
||||
} else {
|
||||
result = [...props.selectedAgents, agentId];
|
||||
}
|
||||
|
||||
props.updateSelectedAgents(result);
|
||||
};
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
if (allAgentsSelected.value) {
|
||||
props.updateSelectedAgents([]);
|
||||
} else {
|
||||
const result = props.agentList.map(item => item.id);
|
||||
props.updateSelectedAgents(result);
|
||||
}
|
||||
};
|
||||
|
||||
const headers = computed(() => [
|
||||
'',
|
||||
t('TEAMS_SETTINGS.AGENTS.AGENT'),
|
||||
t('TEAMS_SETTINGS.AGENTS.EMAIL'),
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="add-agents__header" />
|
||||
<table>
|
||||
<thead
|
||||
class="[&>th]:font-semibold [&>th]:tracking-[1px] ltr:[&>th]:text-left rtl:[&>th]:text-right [&>th]:px-2.5 [&>th]:uppercase [&>th]:text-n-slate-12"
|
||||
>
|
||||
<tr>
|
||||
<td class="ltr:pl-2.5 rtl:pr-2.5">
|
||||
<BaseTable :headers="headers" :items="agentList">
|
||||
<template #header-0>
|
||||
<div class="flex items-center">
|
||||
<Checkbox
|
||||
:model-value="allAgentsSelected"
|
||||
:indeterminate="someAgentsSelected"
|
||||
:title="$t('TEAMS_SETTINGS.AGENTS.SELECT_ALL')"
|
||||
@change="toggleSelectAll"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #row="{ items }">
|
||||
<BaseTableRow v-for="agent in items" :key="agent.id" :item="agent">
|
||||
<template #default>
|
||||
<BaseTableCell class="w-5">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
name="select-all-agents"
|
||||
type="checkbox"
|
||||
:checked="allAgentsSelected ? 'checked' : ''"
|
||||
:title="$t('TEAMS_SETTINGS.AGENTS.SELECT_ALL')"
|
||||
@click.self="selectAllAgents"
|
||||
<Checkbox
|
||||
:model-value="isAgentSelected(agent.id)"
|
||||
@change="() => handleSelectAgent(agent.id)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-n-slate-12 ltr:pl-2.5 rtl:pr-2.5">
|
||||
{{ $t('TEAMS_SETTINGS.AGENTS.AGENT') }}
|
||||
</td>
|
||||
<td class="text-n-slate-12 ltr:pl-2.5 rtl:pr-2.5">
|
||||
{{ $t('TEAMS_SETTINGS.AGENTS.EMAIL') }}
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="agent in agentList"
|
||||
:key="agent.id"
|
||||
:class="agentRowClass(agent.id)"
|
||||
class="border-b border-n-weak [&>td]:p-2.5 [&>td]:text-n-slate-12"
|
||||
>
|
||||
<td class="w-12">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isAgentSelected(agent.id)"
|
||||
@click.self="() => handleSelectAgent(agent.id)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell class="min-w-0 max-w-40">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<Avatar
|
||||
:src="agent.thumbnail"
|
||||
:name="agent.name"
|
||||
@@ -122,39 +114,38 @@ export default {
|
||||
:size="24"
|
||||
hide-offline-status
|
||||
rounded-full
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<h4 class="text-base mb-0 text-n-slate-12">
|
||||
<h4 class="text-heading-3 mb-0 text-n-slate-12 truncate">
|
||||
{{ agent.name }}
|
||||
</h4>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{{ agent.email || '---' }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="flex items-center justify-between mt-2">
|
||||
<p>
|
||||
{{
|
||||
$t('TEAMS_SETTINGS.AGENTS.SELECTED_COUNT', {
|
||||
selected: selectedAgents.length,
|
||||
total: agentList.length,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<NextButton
|
||||
type="submit"
|
||||
:label="submitButtonText"
|
||||
:disabled="disableSubmitButton"
|
||||
:is-loading="isWorking"
|
||||
/>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
|
||||
<BaseTableCell class="min-w-0">
|
||||
<span class="text-body-main text-n-slate-11 truncate block">
|
||||
{{ agent.email || '---' }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
</template>
|
||||
</BaseTableRow>
|
||||
</template>
|
||||
</BaseTable>
|
||||
|
||||
<div class="flex items-center justify-between mt-4">
|
||||
<p class="text-body-main text-n-slate-11">
|
||||
{{
|
||||
$t('TEAMS_SETTINGS.AGENTS.SELECTED_COUNT', {
|
||||
selected: selectedAgents.length,
|
||||
total: agentList.length,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<NextButton
|
||||
type="submit"
|
||||
:label="submitButtonText"
|
||||
:disabled="disableSubmitButton"
|
||||
:is-loading="isWorking"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
input {
|
||||
@apply mb-0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,7 +4,7 @@ import router from '../../../../index';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import PageHeader from '../../SettingsSubPageHeader.vue';
|
||||
import AgentSelector from '../AgentSelector.vue';
|
||||
|
||||
@@ -131,7 +131,9 @@ export default {
|
||||
$t('TEAMS_SETTINGS.EDIT_FLOW.AGENTS.BUTTON_TEXT')
|
||||
"
|
||||
/>
|
||||
<Spinner v-else />
|
||||
<div v-else class="flex items-center justify-center py-6">
|
||||
<Spinner class="text-n-blue-11" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -2,21 +2,33 @@
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
const getters = useStoreGetters();
|
||||
const teamsList = computed(() => getters['teams/getTeams'].value);
|
||||
const uiFlags = computed(() => getters['teams/getUIFlags'].value);
|
||||
const { isAdmin } = useAdmin();
|
||||
|
||||
const loading = ref({});
|
||||
const searchQuery = ref('');
|
||||
|
||||
const teamsList = computed(() => getters['teams/getTeams'].value);
|
||||
|
||||
const filteredTeamsList = computed(() => {
|
||||
const query = searchQuery.value.trim();
|
||||
if (!query) return teamsList.value;
|
||||
return picoSearch(teamsList.value, query, ['name', 'description']);
|
||||
});
|
||||
|
||||
const uiFlags = computed(() => getters['teams/getUIFlags'].value);
|
||||
|
||||
const deleteTeam = async ({ id }) => {
|
||||
try {
|
||||
@@ -68,74 +80,89 @@ const confirmPlaceHolderText = computed(() =>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 overflow-auto">
|
||||
<BaseSettingsHeader
|
||||
:title="$t('TEAMS_SETTINGS.HEADER')"
|
||||
:description="$t('TEAMS_SETTINGS.DESCRIPTION')"
|
||||
:link-text="$t('TEAMS_SETTINGS.LEARN_MORE')"
|
||||
feature-name="team_management"
|
||||
>
|
||||
<template #actions>
|
||||
<router-link v-if="isAdmin" :to="{ name: 'settings_teams_new' }">
|
||||
<Button
|
||||
icon="i-lucide-circle-plus"
|
||||
:label="$t('TEAMS_SETTINGS.NEW_TEAM')"
|
||||
/>
|
||||
</router-link>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
<div class="mt-6 flex-1 text-n-slate-11">
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.isFetching"
|
||||
:message="$t('TEAMS_SETTINGS.LOADING')"
|
||||
/>
|
||||
<p
|
||||
v-else-if="!teamsList.length"
|
||||
class="flex flex-col items-center justify-center h-full text-base p-8"
|
||||
<SettingsLayout
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:loading-message="$t('TEAMS_SETTINGS.LOADING')"
|
||||
:no-records-found="!teamsList.length"
|
||||
:no-records-message="$t('TEAMS_SETTINGS.LIST.404')"
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
v-model:search-query="searchQuery"
|
||||
:title="$t('TEAMS_SETTINGS.HEADER')"
|
||||
:description="$t('TEAMS_SETTINGS.DESCRIPTION')"
|
||||
:link-text="$t('TEAMS_SETTINGS.LEARN_MORE')"
|
||||
:search-placeholder="$t('TEAMS_SETTINGS.SEARCH_PLACEHOLDER')"
|
||||
feature-name="team_management"
|
||||
>
|
||||
{{ $t('TEAMS_SETTINGS.LIST.404') }}
|
||||
</p>
|
||||
|
||||
<table v-else class="min-w-full divide-y divide-n-weak">
|
||||
<tbody class="divide-y divide-n-weak">
|
||||
<tr v-for="team in teamsList" :key="team.id">
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
<span class="block font-medium capitalize">{{ team.name }}</span>
|
||||
<p class="mb-0">{{ team.description }}</p>
|
||||
</td>
|
||||
|
||||
<td class="py-4 flex justify-end gap-1">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'settings_teams_edit',
|
||||
params: { teamId: team.id },
|
||||
}"
|
||||
>
|
||||
<Button
|
||||
v-if="isAdmin"
|
||||
v-tooltip.top="$t('TEAMS_SETTINGS.LIST.EDIT_TEAM')"
|
||||
icon="i-lucide-settings"
|
||||
slate
|
||||
xs
|
||||
faded
|
||||
/>
|
||||
</router-link>
|
||||
<template #actions>
|
||||
<router-link v-if="isAdmin" :to="{ name: 'settings_teams_new' }">
|
||||
<Button :label="$t('TEAMS_SETTINGS.NEW_TEAM')" />
|
||||
</router-link>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<span
|
||||
v-if="!filteredTeamsList.length && searchQuery"
|
||||
class="flex-1 flex items-center justify-center py-20 text-center text-body-main !text-base text-n-slate-11"
|
||||
>
|
||||
{{ $t('TEAMS_SETTINGS.NO_RESULTS') }}
|
||||
</span>
|
||||
|
||||
<div v-else class="divide-y divide-n-weak">
|
||||
<div
|
||||
v-for="team in filteredTeamsList"
|
||||
:key="team.id"
|
||||
class="flex justify-between flex-row items-start gap-4 py-4"
|
||||
>
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
class="flex items-center flex-shrink-0 size-10 justify-center rounded-xl outline outline-1 outline-n-weak -outline-offset-1"
|
||||
>
|
||||
<Icon
|
||||
icon="i-lucide-users-round"
|
||||
class="size-4 text-n-slate-11"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col items-start gap-1">
|
||||
<span class="block text-heading-3 text-n-slate-12 capitalize">
|
||||
{{ team.name }}
|
||||
</span>
|
||||
<p class="mb-0 text-n-slate-11 text-body-main">
|
||||
{{ team.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'settings_teams_edit',
|
||||
params: { teamId: team.id },
|
||||
}"
|
||||
>
|
||||
<Button
|
||||
v-if="isAdmin"
|
||||
v-tooltip.top="$t('TEAMS_SETTINGS.DELETE.BUTTON_TEXT')"
|
||||
icon="i-lucide-trash-2"
|
||||
xs
|
||||
ruby
|
||||
faded
|
||||
:is-loading="loading[team.id]"
|
||||
@click="openDelete(team)"
|
||||
v-tooltip.top="$t('TEAMS_SETTINGS.LIST.EDIT_TEAM')"
|
||||
icon="i-woot-settings"
|
||||
slate
|
||||
sm
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<Button
|
||||
v-if="isAdmin"
|
||||
v-tooltip.top="$t('TEAMS_SETTINGS.DELETE.BUTTON_TEXT')"
|
||||
icon="i-woot-bin"
|
||||
slate
|
||||
sm
|
||||
:is-loading="loading[team.id]"
|
||||
@click="openDelete(team)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<woot-confirm-delete-modal
|
||||
v-if="showDeletePopup"
|
||||
v-model:show="showDeletePopup"
|
||||
@@ -148,5 +175,5 @@ const confirmPlaceHolderText = computed(() =>
|
||||
@on-confirm="confirmDeletion"
|
||||
@on-close="closeDelete"
|
||||
/>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user