From ca02b442dd9aebdc3fc6851285751c12559f565d Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Sat, 22 Feb 2025 03:18:43 +0530 Subject: [PATCH 01/15] fix: Reports chart by removing y-axis numeric labels (#10941) # Pull Request Template ## Description This PR fixes showing y-axis numeric labels in reports charts. It occurs after merging this PR. https://github.com/chatwoot/chatwoot/pull/10938 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? #### **Screenshots** #### **Before** image #### **After** image ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --- .../settings/reports/ReportContainer.vue | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue index c831612e0..da4cc0167 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue @@ -112,33 +112,28 @@ export default { }; }, getChartOptions(metric) { - const baseOptions = METRIC_CHART[metric.KEY].scales; + const options = { + scales: METRIC_CHART[metric.KEY].scales, + }; - // If not an average metric type, return base options early - if (!this.isAverageMetricType(metric.KEY)) { - return baseOptions; - } - - // Only create tooltip config for time-based metrics - return { - ...baseOptions, - plugins: { + // Only add tooltip configuration for time-based metrics + if (this.isAverageMetricType(metric.KEY)) { + options.plugins = { tooltip: { callbacks: { label: ({ raw, dataIndex }) => { - const value = raw || 0; - const count = - this.accountReport.data[metric.KEY][dataIndex]?.count || 0; - return this.$t(metric.TOOLTIP_TEXT, { - metricValue: formatTime(value), - conversationCount: count, + metricValue: formatTime(raw || 0), + conversationCount: + this.accountReport.data[metric.KEY][dataIndex]?.count || 0, }); }, }, }, - }, - }; + }; + } + + return options; }, }, }; From a7e73de8d4db7882629cdef6d4b481c0fe06a0ee Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sat, 22 Feb 2025 03:20:30 +0530 Subject: [PATCH 02/15] feat: Don't hide thumbnail on hover (#10935) ### Old behavior https://github.com/user-attachments/assets/5d7524c1-b1ca-454f-8e89-843ca25d017e ### New behavior https://github.com/user-attachments/assets/49814190-d6b4-4553-bced-3987be3583e2 --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin --- .../components/widgets/Thumbnail.vue | 3 +- .../widgets/conversation/ConversationCard.vue | 53 ++++++++++--------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/Thumbnail.vue b/app/javascript/dashboard/components/widgets/Thumbnail.vue index 7b222aa7e..5998dd338 100644 --- a/app/javascript/dashboard/components/widgets/Thumbnail.vue +++ b/app/javascript/dashboard/components/widgets/Thumbnail.vue @@ -156,13 +156,14 @@ export default { Badge
diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue index 7e8ad507c..a5695b22c 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue @@ -95,9 +95,6 @@ export default { activeInbox: 'getSelectedInbox', accountId: 'getCurrentAccountId', }), - bulkActionCheck() { - return !this.hideThumbnail && !this.hovered && !this.selected; - }, chatMetadata() { return this.chat.meta || {}; }, @@ -182,10 +179,10 @@ export default { router.push({ path }); }, - onCardHover() { + onThumbnailHover() { this.hovered = !this.hideThumbnail; }, - onCardLeave() { + onThumbnailLeave() { this.hovered = false; }, onSelectConversation(checked) { @@ -249,28 +246,36 @@ export default { 'has-inbox-name': showInboxName, 'conversation-selected': selected, }" - @mouseenter="onCardHover" - @mouseleave="onCardLeave" @click="onCardClick" @contextmenu="openContextMenu($event)" > - - +
@@ -400,7 +405,7 @@ export default { } .checkbox-wrapper { - @apply h-10 w-10 flex items-center justify-center rounded-full cursor-pointer mt-4 hover:bg-woot-100 dark:hover:bg-woot-800; + @apply h-10 w-10 flex items-center justify-center rounded-full cursor-pointer mt-4; input[type='checkbox'] { @apply m-0 cursor-pointer; From 161024db9df8e804efa652ac07cc95558868672d Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sat, 22 Feb 2025 04:18:31 +0530 Subject: [PATCH 03/15] 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 Co-authored-by: Sojan Jose Co-authored-by: Pranav --- app/controllers/dashboard_controller.rb | 2 +- .../components-next/captain/PageLayout.vue | 10 +- .../components-next/sidebar/Sidebar.vue | 5 - .../components-next/sidebar/SidebarGroup.vue | 4 +- .../sidebar/SidebarGroupLeaf.vue | 11 +- .../sidebar/SidebarSubGroup.vue | 3 +- .../components-next/sidebar/provider.js | 13 ++- .../dashboard/components/policy.vue | 13 ++- .../dashboard/composables/usePolicy.js | 109 +++++++++++++++++- .../dashboard/constants/installationTypes.js | 5 + app/javascript/dashboard/featureFlags.js | 8 ++ .../dashboard/campaigns/campaigns.routes.js | 22 ++-- .../dashboard/captain/assistants/Index.vue | 2 +- .../captain/assistants/inboxes/Index.vue | 2 +- .../dashboard/captain/captain.routes.js | 23 +++- .../dashboard/captain/responses/Index.vue | 2 +- .../routes/dashboard/contacts/routes.js | 2 + .../dashboard/helpcenter/helpcenter.routes.js | 39 +++---- .../settings/auditlogs/audit.routes.js | 5 + .../settings/billing/billing.routes.js | 3 + .../settings/customRoles/customRole.routes.js | 5 + .../settings/reports/reports.routes.js | 42 +++---- .../dashboard/settings/sla/sla.routes.js | 17 +-- config/features.yml | 5 + 24 files changed, 239 insertions(+), 113 deletions(-) create mode 100644 app/javascript/dashboard/constants/installationTypes.js diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index f63e8f6bf..0f915ab8a 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -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 diff --git a/app/javascript/dashboard/components-next/captain/PageLayout.vue b/app/javascript/dashboard/components-next/captain/PageLayout.vue index 1ce03cec5..dd76f1b62 100644 --- a/app/javascript/dashboard/components-next/captain/PageLayout.vue +++ b/app/javascript/dashboard/components-next/captain/PageLayout.vue @@ -1,12 +1,12 @@ diff --git a/app/javascript/dashboard/composables/usePolicy.js b/app/javascript/dashboard/composables/usePolicy.js index 0653003ec..dc203cbac 100644 --- a/app/javascript/dashboard/composables/usePolicy.js +++ b/app/javascript/dashboard/composables/usePolicy.js @@ -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, + }; } diff --git a/app/javascript/dashboard/constants/installationTypes.js b/app/javascript/dashboard/constants/installationTypes.js new file mode 100644 index 000000000..87709e98d --- /dev/null +++ b/app/javascript/dashboard/constants/installationTypes.js @@ -0,0 +1,5 @@ +export const INSTALLATION_TYPES = { + CLOUD: 'cloud', + ENTERPRISE: 'enterprise', + COMMUNITY: 'community', +}; diff --git a/app/javascript/dashboard/featureFlags.js b/app/javascript/dashboard/featureFlags.js index 0695be473..f7430bdba 100644 --- a/app/javascript/dashboard/featureFlags.js +++ b/app/javascript/dashboard/featureFlags.js @@ -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, +]; diff --git a/app/javascript/dashboard/routes/dashboard/campaigns/campaigns.routes.js b/app/javascript/dashboard/routes/dashboard/campaigns/campaigns.routes.js index a4f0a94f5..e0c1f3a17 100644 --- a/app/javascript/dashboard/routes/dashboard/campaigns/campaigns.routes.js +++ b/app/javascript/dashboard/routes/dashboard/campaigns/campaigns.routes.js @@ -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, }, ], diff --git a/app/javascript/dashboard/routes/dashboard/captain/assistants/Index.vue b/app/javascript/dashboard/routes/dashboard/captain/assistants/Index.vue index 3ea78e74a..bcc551966 100644 --- a/app/javascript/dashboard/routes/dashboard/captain/assistants/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/captain/assistants/Index.vue @@ -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" >