mirror of
https://github.com/chatwoot/chatwoot.git
synced 2026-06-28 21:01:02 +08:00
Adds a platform-wide status banner system to notify all users about external service outages. Super Admins can create, edit, and manage banners via the Super Admin console. Banners support markdown for links and are dismissible by users. <img width="1099" height="236" alt="image" src="https://github.com/user-attachments/assets/047a7994-d885-4a8a-b9c4-aeb32f15474a" /> ## How to test 1. Set `ENABLE_PLATFORM_BANNERS=true` in your environment 2. Go to Super Admin → Platform Banners 3. Create a banner with a message like: `Elevated error rates from Meta APIs. [Check status](https://metastatus.com)` 4. Select a banner type: `info` (blue), `warning` (amber), or `error` (red) 5. Visit the dashboard — the banner should appear at the top 6. Click "Dismiss" — the banner hides and stays dismissed across page reloads 7. Deactivate the banner in Super Admin — it disappears on next page load ## What changed - New `PlatformBanner` model with `banner_message`, `banner_type` (info/warning/error), and `active` flag - Super Admin CRUD via Administrate (controller, dashboard, routes, sidebar icon) - `DashboardController` serves active banners via `globalConfig` - `StatusBanner.vue` component renders banners with markdown support and per-banner localStorage dismiss - Feature gated behind `ENABLE_PLATFORM_BANNERS` env var 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
173 lines
5.0 KiB
Vue
173 lines
5.0 KiB
Vue
<script>
|
|
import { mapGetters } from 'vuex';
|
|
import LoadingState from './components/widgets/LoadingState.vue';
|
|
import NetworkNotification from './components/NetworkNotification.vue';
|
|
import UpdateBanner from './components/app/UpdateBanner.vue';
|
|
import StatusBanner from './components/app/StatusBanner.vue';
|
|
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
|
|
import PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue';
|
|
import vueActionCable from './helper/actionCable';
|
|
import { useRouter } from 'vue-router';
|
|
import { useStore } from 'dashboard/composables/store';
|
|
import WootSnackbarBox from './components/SnackbarContainer.vue';
|
|
import { setColorTheme } from './helper/themeHelper';
|
|
import { isOnOnboardingView } from 'v3/helpers/RouteHelper';
|
|
import { useAccount } from 'dashboard/composables/useAccount';
|
|
import { useFontSize } from 'dashboard/composables/useFontSize';
|
|
import {
|
|
registerSubscription,
|
|
verifyServiceWorkerExistence,
|
|
} from './helper/pushHelper';
|
|
import ReconnectService from 'dashboard/helper/ReconnectService';
|
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
|
|
|
export default {
|
|
name: 'App',
|
|
|
|
components: {
|
|
LoadingState,
|
|
NetworkNotification,
|
|
UpdateBanner,
|
|
StatusBanner,
|
|
PaymentPendingBanner,
|
|
WootSnackbarBox,
|
|
PendingEmailVerificationBanner,
|
|
},
|
|
setup() {
|
|
const router = useRouter();
|
|
const store = useStore();
|
|
const { accountId } = useAccount();
|
|
// Use the font size composable (it automatically sets up the watcher)
|
|
const { currentFontSize } = useFontSize();
|
|
const { uiSettings } = useUISettings();
|
|
|
|
return {
|
|
router,
|
|
store,
|
|
currentAccountId: accountId,
|
|
currentFontSize,
|
|
uiSettings,
|
|
};
|
|
},
|
|
data() {
|
|
return {
|
|
latestChatwootVersion: null,
|
|
reconnectService: null,
|
|
};
|
|
},
|
|
computed: {
|
|
...mapGetters({
|
|
getAccount: 'accounts/getAccount',
|
|
isRTL: 'accounts/isRTL',
|
|
currentUser: 'getCurrentUser',
|
|
authUIFlags: 'getAuthUIFlags',
|
|
}),
|
|
hideOnOnboardingView() {
|
|
return !isOnOnboardingView(this.$route);
|
|
},
|
|
},
|
|
|
|
watch: {
|
|
currentAccountId: {
|
|
immediate: true,
|
|
handler() {
|
|
if (this.currentAccountId) {
|
|
this.initializeAccount();
|
|
}
|
|
},
|
|
},
|
|
},
|
|
mounted() {
|
|
this.initializeColorTheme();
|
|
this.listenToThemeChanges();
|
|
// If user locale is set, use it; otherwise use account locale
|
|
this.setLocale(
|
|
this.uiSettings?.locale || window.chatwootConfig.selectedLocale
|
|
);
|
|
},
|
|
unmounted() {
|
|
if (this.reconnectService) {
|
|
this.reconnectService.disconnect();
|
|
}
|
|
},
|
|
methods: {
|
|
initializeColorTheme() {
|
|
setColorTheme(window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
},
|
|
listenToThemeChanges() {
|
|
const mql = window.matchMedia('(prefers-color-scheme: dark)');
|
|
mql.onchange = e => setColorTheme(e.matches);
|
|
},
|
|
setLocale(locale) {
|
|
if (locale) {
|
|
this.$root.$i18n.locale = locale;
|
|
}
|
|
},
|
|
async initializeAccount() {
|
|
await this.$store.dispatch('accounts/get');
|
|
this.$store.dispatch('setActiveAccount', {
|
|
accountId: this.currentAccountId,
|
|
});
|
|
const account = this.getAccount(this.currentAccountId);
|
|
const { locale, latest_chatwoot_version: latestChatwootVersion } =
|
|
account;
|
|
const { pubsub_token: pubsubToken } = this.currentUser || {};
|
|
// If user locale is set, use it; otherwise use account locale
|
|
this.setLocale(this.uiSettings?.locale || locale);
|
|
this.latestChatwootVersion = latestChatwootVersion;
|
|
vueActionCable.init(this.store, pubsubToken);
|
|
this.reconnectService = new ReconnectService(this.store, this.router);
|
|
window.reconnectService = this.reconnectService;
|
|
|
|
verifyServiceWorkerExistence(registration =>
|
|
registration.pushManager.getSubscription().then(subscription => {
|
|
if (subscription) {
|
|
registerSubscription();
|
|
}
|
|
})
|
|
);
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
v-if="!authUIFlags.isFetching"
|
|
id="app"
|
|
class="flex flex-col w-full h-screen min-h-0 bg-n-background"
|
|
:dir="isRTL ? 'rtl' : 'ltr'"
|
|
>
|
|
<UpdateBanner :latest-chatwoot-version="latestChatwootVersion" />
|
|
<StatusBanner />
|
|
<template v-if="currentAccountId">
|
|
<PendingEmailVerificationBanner v-if="hideOnOnboardingView" />
|
|
<PaymentPendingBanner v-if="hideOnOnboardingView" />
|
|
</template>
|
|
<router-view v-slot="{ Component }">
|
|
<transition name="fade" mode="out-in">
|
|
<component :is="Component" />
|
|
</transition>
|
|
</router-view>
|
|
<WootSnackbarBox />
|
|
<NetworkNotification />
|
|
</div>
|
|
<LoadingState v-else />
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
@import './assets/scss/app';
|
|
|
|
.v-popper--theme-tooltip .v-popper__inner {
|
|
background: black !important;
|
|
font-size: 0.75rem;
|
|
padding: 4px 8px !important;
|
|
border-radius: 6px;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.v-popper--theme-tooltip .v-popper__arrow-container {
|
|
display: none;
|
|
}
|
|
</style>
|