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', 'LOGOUT_REDIRECT_LINK',
'DISABLE_USER_PROFILE_UPDATE', 'DISABLE_USER_PROFILE_UPDATE',
'DEPLOYMENT_ENV', 'DEPLOYMENT_ENV',
'CSML_EDITOR_HOST' 'CSML_EDITOR_HOST', 'INSTALLATION_PRICING_PLAN'
).merge(app_config) ).merge(app_config)
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,20 +1,31 @@
import { computed, unref } from 'vue';
import { useMapGetter } from 'dashboard/composables/store'; import { useMapGetter } from 'dashboard/composables/store';
import { useAccount } from 'dashboard/composables/useAccount'; import { useAccount } from 'dashboard/composables/useAccount';
import { useConfig } from 'dashboard/composables/useConfig';
import { import {
getUserPermissions, getUserPermissions,
hasPermissions, hasPermissions,
} from 'dashboard/helper/permissionsHelper'; } from 'dashboard/helper/permissionsHelper';
import { PREMIUM_FEATURES } from 'dashboard/featureFlags';
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
export function usePolicy() { export function usePolicy() {
const user = useMapGetter('getCurrentUser'); const user = useMapGetter('getCurrentUser');
const isFeatureEnabled = useMapGetter('accounts/isFeatureEnabledonAccount'); const isFeatureEnabled = useMapGetter('accounts/isFeatureEnabledonAccount');
const isOnChatwootCloud = useMapGetter('globalConfig/isOnChatwootCloud');
const isACustomBrandedInstance = useMapGetter(
'globalConfig/isACustomBrandedInstance'
);
const { isEnterprise, enterprisePlanName } = useConfig();
const { accountId } = useAccount(); const { accountId } = useAccount();
const getUserPermissionsForAccount = () => { const getUserPermissionsForAccount = () => {
return getUserPermissions(user.value, accountId.value); return getUserPermissions(user.value, accountId.value);
}; };
const checkFeatureAllowed = featureFlag => { const isFeatureFlagEnabled = featureFlag => {
if (!featureFlag) return true; if (!featureFlag) return true;
return isFeatureEnabled.value(accountId.value, featureFlag); return isFeatureEnabled.value(accountId.value, featureFlag);
}; };
@@ -25,5 +36,99 @@ export function usePolicy() {
return hasPermissions(requiredPermissions, userPermissions); 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', REPORT_V4: 'report_v4',
CONTACT_CHATWOOT_SUPPORT_TEAM: 'contact_chatwoot_support_team', 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 CampaignsPageRouteView from './pages/CampaignsPageRouteView.vue';
import LiveChatCampaignsPage from './pages/LiveChatCampaignsPage.vue'; import LiveChatCampaignsPage from './pages/LiveChatCampaignsPage.vue';
import SMSCampaignsPage from './pages/SMSCampaignsPage.vue'; import SMSCampaignsPage from './pages/SMSCampaignsPage.vue';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
const meta = {
featureFlag: FEATURE_FLAGS.CAMPAIGNS,
permissions: ['administrator'],
};
const campaignsRoutes = { const campaignsRoutes = {
routes: [ routes: [
@@ -19,9 +25,7 @@ const campaignsRoutes = {
{ {
path: 'ongoing', path: 'ongoing',
name: 'campaigns_ongoing_index', name: 'campaigns_ongoing_index',
meta: { meta,
permissions: ['administrator'],
},
redirect: to => { redirect: to => {
return { name: 'campaigns_livechat_index', params: to.params }; return { name: 'campaigns_livechat_index', params: to.params };
}, },
@@ -29,9 +33,7 @@ const campaignsRoutes = {
{ {
path: 'one_off', path: 'one_off',
name: 'campaigns_one_off_index', name: 'campaigns_one_off_index',
meta: { meta,
permissions: ['administrator'],
},
redirect: to => { redirect: to => {
return { name: 'campaigns_sms_index', params: to.params }; return { name: 'campaigns_sms_index', params: to.params };
}, },
@@ -39,17 +41,13 @@ const campaignsRoutes = {
{ {
path: 'live_chat', path: 'live_chat',
name: 'campaigns_livechat_index', name: 'campaigns_livechat_index',
meta: { meta,
permissions: ['administrator'],
},
component: LiveChatCampaignsPage, component: LiveChatCampaignsPage,
}, },
{ {
path: 'sms', path: 'sms',
name: 'campaigns_sms_index', name: 'campaigns_sms_index',
meta: { meta,
permissions: ['administrator'],
},
component: SMSCampaignsPage, component: SMSCampaignsPage,
}, },
], ],

View File

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

View File

@@ -72,8 +72,8 @@ onMounted(() =>
:button-policy="['administrator']" :button-policy="['administrator']"
:is-fetching="isFetchingAssistant || isFetching" :is-fetching="isFetchingAssistant || isFetching"
:is-empty="!captainInboxes.length" :is-empty="!captainInboxes.length"
:feature-flag="FEATURE_FLAGS.CAPTAIN"
:show-pagination-footer="false" :show-pagination-footer="false"
:feature-flag="FEATURE_FLAGS.CAPTAIN"
@click="handleCreate" @click="handleCreate"
> >
<template v-if="!isFetchingAssistant" #headerTitle> <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 { frontendURL } from '../../../helper/URLHelper';
import AssistantIndex from './assistants/Index.vue'; import AssistantIndex from './assistants/Index.vue';
import AssistantInboxesIndex from './assistants/inboxes/Index.vue'; import AssistantInboxesIndex from './assistants/inboxes/Index.vue';
@@ -12,6 +13,11 @@ export const routes = [
name: 'captain_assistants_index', name: 'captain_assistants_index',
meta: { meta: {
permissions: ['administrator', 'agent'], 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', name: 'captain_assistants_inboxes_index',
meta: { meta: {
permissions: ['administrator', 'agent'], 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', name: 'captain_documents_index',
meta: { meta: {
permissions: ['administrator', 'agent'], 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', name: 'captain_responses_index',
meta: { meta: {
permissions: ['administrator', 'agent'], 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')" :button-label="$t('CAPTAIN.RESPONSES.ADD_NEW')"
:is-fetching="isFetching" :is-fetching="isFetching"
:is-empty="!responses.length" :is-empty="!responses.length"
:feature-flag="FEATURE_FLAGS.CAPTAIN"
:show-pagination-footer="!isFetching && !!responses.length" :show-pagination-footer="!isFetching && !!responses.length"
:feature-flag="FEATURE_FLAGS.CAPTAIN"
@update:current-page="onPageChange" @update:current-page="onPageChange"
@click="handleCreate" @click="handleCreate"
> >

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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