feat: Add visibility checks for installation types (#10773)

This pull request includes multiple changes to the sidebar and route
metas to configure visibility of features on the dashboard.

Here's a summary of the changes

1. Added `installationTypes`, field to routes `meta`, this works along
side `permissions` and `featureFlags`
This allows us to decide weather a particular feature is accessible on a
particular type. For instance, the Billing pages should only be
available on Cloud
2. Updated `usePolicy` and `policy.vue` to use the new
`installationTypes` config
3. Updated Sidebar related components to remove `showOnlyOnCloud` to use
the new policy updates.

Testing the PR

Here's the matrix of cases:
https://docs.google.com/spreadsheets/d/15AAJntJZoyudaby77BOnRcC4435FGuT7PXbUXoTyU50/edit?usp=sharing

---------

Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
Shivam Mishra
2025-02-22 04:18:31 +05:30
committed by GitHub
parent a7e73de8d4
commit 161024db9d
24 changed files with 239 additions and 113 deletions

View File

@@ -36,7 +36,7 @@ class DashboardController < ActionController::Base
'LOGOUT_REDIRECT_LINK',
'DISABLE_USER_PROFILE_UPDATE',
'DEPLOYMENT_ENV',
'CSML_EDITOR_HOST'
'CSML_EDITOR_HOST', 'INSTALLATION_PRICING_PLAN'
).merge(app_config)
end

View File

@@ -1,12 +1,12 @@
<script setup>
import { computed } from 'vue';
import { useAccount } from 'dashboard/composables/useAccount';
import { usePolicy } from 'dashboard/composables/usePolicy';
import Button from 'dashboard/components-next/button/Button.vue';
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import Policy from 'dashboard/components/policy.vue';
const { featureFlag } = defineProps({
const props = defineProps({
currentPage: {
type: Number,
default: 1,
@@ -50,10 +50,10 @@ const { featureFlag } = defineProps({
});
const emit = defineEmits(['click', 'close', 'update:currentPage']);
const { isCloudFeatureEnabled } = useAccount();
const { shouldShowPaywall } = usePolicy();
const showPaywall = computed(() => {
return !isCloudFeatureEnabled(featureFlag);
return shouldShowPaywall(props.featureFlag);
});
const handleButtonClick = () => {
@@ -97,7 +97,7 @@ const handlePageChange = event => {
</header>
<main class="flex-1 px-6 overflow-y-auto xl:px-0">
<div class="w-full max-w-[960px] mx-auto py-4">
<slot name="controls" />
<slot v-if="!showPaywall" name="controls" />
<div
v-if="isFetching"
class="flex items-center justify-center py-10 text-n-slate-11"

View File

@@ -240,24 +240,20 @@ const menuItems = computed(() => {
name: 'Captain',
icon: 'i-woot-captain',
label: t('SIDEBAR.CAPTAIN'),
showOnlyOnCloud: true,
children: [
{
name: 'Assistants',
label: t('SIDEBAR.CAPTAIN_ASSISTANTS'),
showOnlyOnCloud: true,
to: accountScopedRoute('captain_assistants_index'),
},
{
name: 'Documents',
label: t('SIDEBAR.CAPTAIN_DOCUMENTS'),
showOnlyOnCloud: true,
to: accountScopedRoute('captain_documents_index'),
},
{
name: 'Responses',
label: t('SIDEBAR.CAPTAIN_RESPONSES'),
showOnlyOnCloud: true,
to: accountScopedRoute('captain_responses_index'),
},
],
@@ -509,7 +505,6 @@ const menuItems = computed(() => {
name: 'Settings Billing',
label: t('SIDEBAR.BILLING'),
icon: 'i-lucide-credit-card',
showOnlyOnCloud: true,
to: accountScopedRoute('billing_settings_index'),
},
],

View File

@@ -24,7 +24,6 @@ const {
resolvePath,
resolvePermissions,
resolveFeatureFlag,
isOnChatwootCloud,
isAllowed,
} = useSidebarContext();
@@ -43,7 +42,6 @@ const hasChildren = computed(
const accessibleItems = computed(() => {
if (!hasChildren.value) return [];
return props.children.filter(child => {
if (child.showOnlyOnCloud && !isOnChatwootCloud.value) return false;
// If a item has no link, it means it's just a subgroup header
// So we don't need to check for permissions here, because there's nothing to
// access here anyway
@@ -166,7 +164,7 @@ onMounted(async () => {
:active-child="activeChild"
/>
<SidebarGroupLeaf
v-else
v-else-if="isAllowed(child.to)"
v-show="isExpanded || activeChild?.name === child.name"
v-bind="child"
:active="activeChild?.name === child.name"

View File

@@ -9,18 +9,10 @@ const props = defineProps({
to: { type: [String, Object], required: true },
icon: { type: [String, Object], default: null },
active: { type: Boolean, default: false },
showOnlyOnCloud: { type: Boolean, default: false },
component: { type: Function, default: null },
});
const { resolvePermissions, resolveFeatureFlag, isOnChatwootCloud } =
useSidebarContext();
const allowedToShow = computed(() => {
if (props.showOnlyOnCloud && !isOnChatwootCloud.value) return false;
return true;
});
const { resolvePermissions, resolveFeatureFlag } = useSidebarContext();
const shouldRenderComponent = computed(() => {
return typeof props.component === 'function' || isVNode(props.component);
@@ -30,7 +22,6 @@ const shouldRenderComponent = computed(() => {
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template>
<Policy
v-if="allowedToShow"
:permissions="resolvePermissions(to)"
:feature-flag="resolveFeatureFlag(to)"
as="li"

View File

@@ -14,12 +14,11 @@ const props = defineProps({
activeChild: { type: Object, default: undefined },
});
const { isAllowed, isOnChatwootCloud } = useSidebarContext();
const { isAllowed } = useSidebarContext();
const scrollableContainer = ref(null);
const accessibleItems = computed(() =>
props.children.filter(child => {
if (child.showOnlyOnCloud && !isOnChatwootCloud.value) return false;
return child.to && isAllowed(child.to);
})
);

View File

@@ -1,5 +1,4 @@
import { inject, provide } from 'vue';
import { useMapGetter } from 'dashboard/composables/store';
import { usePolicy } from 'dashboard/composables/usePolicy';
import { useRouter } from 'vue-router';
@@ -12,9 +11,8 @@ export function useSidebarContext() {
}
const router = useRouter();
const isOnChatwootCloud = useMapGetter('globalConfig/isOnChatwootCloud');
const { checkFeatureAllowed, checkPermissions } = usePolicy();
const { shouldShow } = usePolicy();
const resolvePath = to => {
if (to) return router.resolve(to)?.path || '/';
@@ -31,11 +29,17 @@ export function useSidebarContext() {
return '';
};
const resolveInstallationType = to => {
if (to) return router.resolve(to)?.meta?.installationTypes || [];
return [];
};
const isAllowed = to => {
const permissions = resolvePermissions(to);
const featureFlag = resolveFeatureFlag(to);
const installationType = resolveInstallationType(to);
return checkPermissions(permissions) && checkFeatureAllowed(featureFlag);
return shouldShow(featureFlag, permissions, installationType);
};
return {
@@ -44,7 +48,6 @@ export function useSidebarContext() {
resolvePermissions,
resolveFeatureFlag,
isAllowed,
isOnChatwootCloud,
};
}

View File

@@ -15,17 +15,22 @@ const props = defineProps({
type: String,
default: null,
},
installationTypes: {
type: Array,
default: null,
},
});
const { checkFeatureAllowed, checkPermissions } = usePolicy();
const { shouldShow } = usePolicy();
const isFeatureAllowed = computed(() => checkFeatureAllowed(props.featureFlag));
const hasPermission = computed(() => checkPermissions(props.permissions));
const show = computed(() =>
shouldShow(props.featureFlag, props.permissions, props.installationTypes)
);
</script>
<!-- eslint-disable vue/no-root-v-if -->
<template>
<component :is="as" v-if="isFeatureAllowed && hasPermission">
<component :is="as" v-if="show">
<slot />
</component>
</template>

View File

@@ -1,20 +1,31 @@
import { computed, unref } from 'vue';
import { useMapGetter } from 'dashboard/composables/store';
import { useAccount } from 'dashboard/composables/useAccount';
import { useConfig } from 'dashboard/composables/useConfig';
import {
getUserPermissions,
hasPermissions,
} from 'dashboard/helper/permissionsHelper';
import { PREMIUM_FEATURES } from 'dashboard/featureFlags';
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
export function usePolicy() {
const user = useMapGetter('getCurrentUser');
const isFeatureEnabled = useMapGetter('accounts/isFeatureEnabledonAccount');
const isOnChatwootCloud = useMapGetter('globalConfig/isOnChatwootCloud');
const isACustomBrandedInstance = useMapGetter(
'globalConfig/isACustomBrandedInstance'
);
const { isEnterprise, enterprisePlanName } = useConfig();
const { accountId } = useAccount();
const getUserPermissionsForAccount = () => {
return getUserPermissions(user.value, accountId.value);
};
const checkFeatureAllowed = featureFlag => {
const isFeatureFlagEnabled = featureFlag => {
if (!featureFlag) return true;
return isFeatureEnabled.value(accountId.value, featureFlag);
};
@@ -25,5 +36,99 @@ export function usePolicy() {
return hasPermissions(requiredPermissions, userPermissions);
};
return { checkFeatureAllowed, checkPermissions };
const checkInstallationType = config => {
if (Array.isArray(config) && config.length > 0) {
const installationCheck = {
[INSTALLATION_TYPES.ENTERPRISE]: isEnterprise,
[INSTALLATION_TYPES.CLOUD]: isOnChatwootCloud.value,
[INSTALLATION_TYPES.COMMUNITY]: true,
};
return config.some(type => installationCheck[type]);
}
return true;
};
const isPremiumFeature = featureFlag => {
if (!featureFlag) return true;
return PREMIUM_FEATURES.includes(featureFlag);
};
const hasPremiumEnterprise = computed(() => {
if (isEnterprise) return enterprisePlanName !== 'community';
return true;
});
const shouldShow = (featureFlag, permissions, installationTypes) => {
const flag = unref(featureFlag);
const perms = unref(permissions);
const installation = unref(installationTypes);
// if the user does not have permissions or installation type is not supported
// return false;
// This supersedes everything
if (!checkPermissions(perms)) return false;
if (!checkInstallationType(installation)) return false;
if (isACustomBrandedInstance.value) {
// if this is a custom branded instance, we just use the feature flag as a reference
return isFeatureFlagEnabled(flag);
}
// if on cloud, we should if the feature is allowed
// or if the feature is a premium one like SLA to show a paywall
// the paywall should be managed by the individual component
if (isOnChatwootCloud.value) {
return isFeatureFlagEnabled(flag) || isPremiumFeature(flag);
}
if (isEnterprise) {
// in enterprise, if the feature is premium but they don't have an enterprise plan
// we should it anyway this is to show upsells on enterprise regardless of the feature flag
// Feature flag is only honored if they have a premium plan
//
// In case they have a premium plan, the check on feature flag alone is enough
// because the second condition will always be false
// That means once subscribed, the feature can be disabled by the admin
//
// the paywall should be managed by the individual component
return (
isFeatureFlagEnabled(flag) ||
(isPremiumFeature(flag) && !hasPremiumEnterprise.value)
);
}
// default to true
return true;
};
const shouldShowPaywall = featureFlag => {
const flag = unref(featureFlag);
if (!flag) return false;
if (isACustomBrandedInstance.value) {
// custom branded instances never show paywall
return false;
}
if (isPremiumFeature(flag)) {
if (isOnChatwootCloud.value) {
return !isFeatureFlagEnabled(flag);
}
if (isEnterprise) {
return !hasPremiumEnterprise.value;
}
}
return false;
};
return {
checkPermissions,
shouldShowPaywall,
shouldShow,
};
}

View File

@@ -0,0 +1,5 @@
export const INSTALLATION_TYPES = {
CLOUD: 'cloud',
ENTERPRISE: 'enterprise',
COMMUNITY: 'community',
};

View File

@@ -36,3 +36,11 @@ export const FEATURE_FLAGS = {
REPORT_V4: 'report_v4',
CONTACT_CHATWOOT_SUPPORT_TEAM: 'contact_chatwoot_support_team',
};
export const PREMIUM_FEATURES = [
FEATURE_FLAGS.SLA,
FEATURE_FLAGS.CAPTAIN,
FEATURE_FLAGS.CUSTOM_ROLES,
FEATURE_FLAGS.AUDIT_LOGS,
FEATURE_FLAGS.HELP_CENTER,
];

View File

@@ -3,6 +3,12 @@ import { frontendURL } from 'dashboard/helper/URLHelper.js';
import CampaignsPageRouteView from './pages/CampaignsPageRouteView.vue';
import LiveChatCampaignsPage from './pages/LiveChatCampaignsPage.vue';
import SMSCampaignsPage from './pages/SMSCampaignsPage.vue';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
const meta = {
featureFlag: FEATURE_FLAGS.CAMPAIGNS,
permissions: ['administrator'],
};
const campaignsRoutes = {
routes: [
@@ -19,9 +25,7 @@ const campaignsRoutes = {
{
path: 'ongoing',
name: 'campaigns_ongoing_index',
meta: {
permissions: ['administrator'],
},
meta,
redirect: to => {
return { name: 'campaigns_livechat_index', params: to.params };
},
@@ -29,9 +33,7 @@ const campaignsRoutes = {
{
path: 'one_off',
name: 'campaigns_one_off_index',
meta: {
permissions: ['administrator'],
},
meta,
redirect: to => {
return { name: 'campaigns_sms_index', params: to.params };
},
@@ -39,17 +41,13 @@ const campaignsRoutes = {
{
path: 'live_chat',
name: 'campaigns_livechat_index',
meta: {
permissions: ['administrator'],
},
meta,
component: LiveChatCampaignsPage,
},
{
path: 'sms',
name: 'campaigns_sms_index',
meta: {
permissions: ['administrator'],
},
meta,
component: SMSCampaignsPage,
},
],

View File

@@ -78,8 +78,8 @@ onMounted(() => store.dispatch('captainAssistants/get'));
:button-policy="['administrator']"
:show-pagination-footer="false"
:is-fetching="isFetching"
:feature-flag="FEATURE_FLAGS.CAPTAIN"
:is-empty="!assistants.length"
:feature-flag="FEATURE_FLAGS.CAPTAIN"
@click="handleCreate"
>
<template #emptyState>

View File

@@ -72,8 +72,8 @@ onMounted(() =>
:button-policy="['administrator']"
:is-fetching="isFetchingAssistant || isFetching"
:is-empty="!captainInboxes.length"
:feature-flag="FEATURE_FLAGS.CAPTAIN"
:show-pagination-footer="false"
:feature-flag="FEATURE_FLAGS.CAPTAIN"
@click="handleCreate"
>
<template v-if="!isFetchingAssistant" #headerTitle>

View File

@@ -1,4 +1,5 @@
// import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
import { frontendURL } from '../../../helper/URLHelper';
import AssistantIndex from './assistants/Index.vue';
import AssistantInboxesIndex from './assistants/inboxes/Index.vue';
@@ -12,6 +13,11 @@ export const routes = [
name: 'captain_assistants_index',
meta: {
permissions: ['administrator', 'agent'],
featureFlag: FEATURE_FLAGS.CAPTAIN,
installationTypes: [
INSTALLATION_TYPES.CLOUD,
INSTALLATION_TYPES.ENTERPRISE,
],
},
},
{
@@ -22,6 +28,11 @@ export const routes = [
name: 'captain_assistants_inboxes_index',
meta: {
permissions: ['administrator', 'agent'],
featureFlag: FEATURE_FLAGS.CAPTAIN,
installationTypes: [
INSTALLATION_TYPES.CLOUD,
INSTALLATION_TYPES.ENTERPRISE,
],
},
},
{
@@ -30,6 +41,11 @@ export const routes = [
name: 'captain_documents_index',
meta: {
permissions: ['administrator', 'agent'],
featureFlag: FEATURE_FLAGS.CAPTAIN,
installationTypes: [
INSTALLATION_TYPES.CLOUD,
INSTALLATION_TYPES.ENTERPRISE,
],
},
},
{
@@ -38,6 +54,11 @@ export const routes = [
name: 'captain_responses_index',
meta: {
permissions: ['administrator', 'agent'],
featureFlag: FEATURE_FLAGS.CAPTAIN,
installationTypes: [
INSTALLATION_TYPES.CLOUD,
INSTALLATION_TYPES.ENTERPRISE,
],
},
},
];

View File

@@ -163,8 +163,8 @@ onMounted(() => {
:button-label="$t('CAPTAIN.RESPONSES.ADD_NEW')"
:is-fetching="isFetching"
:is-empty="!responses.length"
:feature-flag="FEATURE_FLAGS.CAPTAIN"
:show-pagination-footer="!isFetching && !!responses.length"
:feature-flag="FEATURE_FLAGS.CAPTAIN"
@update:current-page="onPageChange"
@click="handleCreate"
>

View File

@@ -1,8 +1,10 @@
import { frontendURL } from '../../../helper/URLHelper';
import ContactsIndex from './pages/ContactsIndex.vue';
import ContactManageView from './pages/ContactManageView.vue';
import { FEATURE_FLAGS } from '../../../featureFlags';
const commonMeta = {
featureFlag: FEATURE_FLAGS.CRM,
permissions: ['administrator', 'agent', 'contact_manage'],
};

View File

@@ -1,3 +1,4 @@
import { FEATURE_FLAGS } from '../../../featureFlags';
import { getPortalRoute } from './helpers/routeHelper';
import HelpCenterPageRouteView from './pages/HelpCenterPageRouteView.vue';
@@ -21,21 +22,21 @@ const PortalsLocalesIndexPage = () =>
const PortalsSettingsIndexPage = () =>
import('./pages/PortalsSettingsIndexPage.vue');
const meta = {
featureFlag: FEATURE_FLAGS.HELP_CENTER,
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
};
const portalRoutes = [
{
path: getPortalRoute(':portalSlug/:locale/:categorySlug?/articles/:tab?'),
name: 'portals_articles_index',
meta: {
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
meta,
component: PortalsArticlesIndexPage,
},
{
path: getPortalRoute(':portalSlug/:locale/:categorySlug?/articles/new'),
name: 'portals_articles_new',
meta: {
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
meta,
component: PortalsArticlesNewPage,
},
{
@@ -43,18 +44,14 @@ const portalRoutes = [
':portalSlug/:locale/:categorySlug?/articles/:tab?/edit/:articleSlug'
),
name: 'portals_articles_edit',
meta: {
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
meta,
component: PortalsArticlesEditPage,
},
{
path: getPortalRoute(':portalSlug/:locale/categories'),
name: 'portals_categories_index',
meta: {
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
meta,
component: PortalsCategoriesIndexPage,
},
{
@@ -62,9 +59,7 @@ const portalRoutes = [
':portalSlug/:locale/categories/:categorySlug/articles'
),
name: 'portals_categories_articles_index',
meta: {
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
meta,
component: PortalsArticlesIndexPage,
},
{
@@ -72,31 +67,26 @@ const portalRoutes = [
':portalSlug/:locale/categories/:categorySlug/articles/:articleSlug'
),
name: 'portals_categories_articles_edit',
meta: {
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
meta,
component: PortalsArticlesEditPage,
},
{
path: getPortalRoute(':portalSlug/locales'),
name: 'portals_locales_index',
meta: {
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
meta,
component: PortalsLocalesIndexPage,
},
{
path: getPortalRoute(':portalSlug/settings'),
name: 'portals_settings_index',
meta: {
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
meta,
component: PortalsSettingsIndexPage,
},
{
path: getPortalRoute('new'),
name: 'portals_new',
meta: {
featureFlag: FEATURE_FLAGS.HELP_CENTER,
permissions: ['administrator', 'knowledge_base_manage'],
},
component: PortalsNew,
@@ -105,6 +95,7 @@ const portalRoutes = [
path: getPortalRoute(':navigationPath'),
name: 'portals_index',
meta: {
featureFlag: FEATURE_FLAGS.HELP_CENTER,
permissions: ['administrator', 'knowledge_base_manage'],
},
component: PortalsIndex,

View File

@@ -1,4 +1,5 @@
import { FEATURE_FLAGS } from '../../../../featureFlags';
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
import { frontendURL } from '../../../../helper/URLHelper';
import SettingsWrapper from '../SettingsWrapper.vue';
@@ -21,6 +22,10 @@ export default {
name: 'auditlogs_list',
meta: {
featureFlag: FEATURE_FLAGS.AUDIT_LOGS,
installationTypes: [
INSTALLATION_TYPES.CLOUD,
INSTALLATION_TYPES.ENTERPRISE,
],
permissions: ['administrator'],
},
component: AuditLogsHome,

View File

@@ -1,4 +1,5 @@
import { frontendURL } from '../../../../helper/URLHelper';
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
import SettingsWrapper from '../SettingsWrapper.vue';
import Index from './Index.vue';
@@ -8,6 +9,7 @@ export default {
path: frontendURL('accounts/:accountId/settings/billing'),
meta: {
permissions: ['administrator'],
installationTypes: [INSTALLATION_TYPES.CLOUD],
},
component: SettingsWrapper,
props: {
@@ -21,6 +23,7 @@ export default {
name: 'billing_settings_index',
component: Index,
meta: {
installationTypes: [INSTALLATION_TYPES.CLOUD],
permissions: ['administrator'],
},
},

View File

@@ -1,4 +1,5 @@
import { FEATURE_FLAGS } from '../../../../featureFlags';
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
import { frontendURL } from 'dashboard/helper/URLHelper';
import SettingsWrapper from '../SettingsWrapper.vue';
@@ -19,6 +20,10 @@ export default {
name: 'custom_roles_list',
meta: {
featureFlag: FEATURE_FLAGS.CUSTOM_ROLES,
installationTypes: [
INSTALLATION_TYPES.CLOUD,
INSTALLATION_TYPES.ENTERPRISE,
],
permissions: ['administrator'],
},
component: CustomRolesHome,

View File

@@ -22,37 +22,34 @@ import BotReports from './BotReports.vue';
import LiveReports from './LiveReports.vue';
import SLAReports from './SLAReports.vue';
const meta = {
featureFlag: FEATURE_FLAGS.REPORTS,
permissions: ['administrator', 'report_manage'],
};
const oldReportRoutes = [
{
path: 'agent',
name: 'agent_reports',
meta: {
permissions: ['administrator', 'report_manage'],
},
meta,
component: AgentReports,
},
{
path: 'inboxes',
name: 'inbox_reports',
meta: {
permissions: ['administrator', 'report_manage'],
},
meta,
component: InboxReports,
},
{
path: 'label',
name: 'label_reports',
meta: {
permissions: ['administrator', 'report_manage'],
},
meta,
component: LabelReports,
},
{
path: 'teams',
name: 'team_reports',
meta: {
permissions: ['administrator', 'report_manage'],
},
meta,
component: TeamReports,
},
];
@@ -124,17 +121,13 @@ export default {
{
path: 'overview',
name: 'account_overview_reports',
meta: {
permissions: ['administrator', 'report_manage'],
},
meta,
component: LiveReports,
},
{
path: 'conversation',
name: 'conversation_reports',
meta: {
permissions: ['administrator', 'report_manage'],
},
meta,
component: Index,
},
...oldReportRoutes,
@@ -142,26 +135,19 @@ export default {
{
path: 'sla',
name: 'sla_reports',
meta: {
permissions: ['administrator', 'report_manage'],
featureFlag: FEATURE_FLAGS.SLA,
},
meta,
component: SLAReports,
},
{
path: 'csat',
name: 'csat_reports',
meta: {
permissions: ['administrator', 'report_manage'],
},
meta,
component: CsatResponses,
},
{
path: 'bot',
name: 'bot_reports',
meta: {
permissions: ['administrator', 'report_manage'],
},
meta,
component: BotReports,
},
],

View File

@@ -1,9 +1,16 @@
import { FEATURE_FLAGS } from '../../../../featureFlags';
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
import { frontendURL } from '../../../../helper/URLHelper';
import SettingsWrapper from '../SettingsWrapper.vue';
import Index from './Index.vue';
const meta = {
featureFlag: FEATURE_FLAGS.SLA,
permissions: ['administrator'],
installationTypes: [INSTALLATION_TYPES.CLOUD, INSTALLATION_TYPES.ENTERPRISE],
};
export default {
routes: [
{
@@ -14,10 +21,7 @@ export default {
{
path: '',
name: 'sla_wrapper',
meta: {
featureFlag: FEATURE_FLAGS.SLA,
permissions: ['administrator'],
},
meta,
redirect: to => {
return { name: 'sla_list', params: to.params };
},
@@ -25,10 +29,7 @@ export default {
{
path: 'list',
name: 'sla_list',
meta: {
featureFlag: FEATURE_FLAGS.SLA,
permissions: ['administrator'],
},
meta,
component: Index,
},
],

View File

@@ -75,10 +75,13 @@
- name: message_reply_to
enabled: false
help_url: https://chwt.app/hc/reply-to
chatwoot_internal: true
- name: insert_article_in_reply
enabled: false
chatwoot_internal: true
- name: inbox_view
enabled: false
chatwoot_internal: true
- name: sla
enabled: false
premium: true
@@ -86,10 +89,12 @@
- name: help_center_embedding_search
enabled: false
premium: true
chatwoot_internal: true
- name: linear_integration
enabled: false
- name: captain_integration
enabled: false
premium: true
- name: custom_roles
enabled: false
premium: true