feat: add option to disable calendar
This commit is contained in:
@@ -16,6 +16,7 @@ import { useEmailStore } from "@/stores/email-store";
|
|||||||
import { useSettingsStore } from "@/stores/settings-store";
|
import { useSettingsStore } from "@/stores/settings-store";
|
||||||
import { useIdentityStore } from "@/stores/identity-store";
|
import { useIdentityStore } from "@/stores/identity-store";
|
||||||
import { useAccountStore } from "@/stores/account-store";
|
import { useAccountStore } from "@/stores/account-store";
|
||||||
|
import { usePolicyStore } from "@/stores/policy-store";
|
||||||
import { toast } from "@/stores/toast-store";
|
import { toast } from "@/stores/toast-store";
|
||||||
import { useIsDesktop, useIsMobile } from "@/hooks/use-media-query";
|
import { useIsDesktop, useIsMobile } from "@/hooks/use-media-query";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -97,6 +98,7 @@ export default function CalendarPage() {
|
|||||||
removeCalendar, clearCalendarEvents,
|
removeCalendar, clearCalendarEvents,
|
||||||
refreshAllSubscriptions, icalSubscriptions,
|
refreshAllSubscriptions, icalSubscriptions,
|
||||||
} = useCalendarStore();
|
} = useCalendarStore();
|
||||||
|
const calendarEnabled = usePolicyStore((s) => s.isFeatureEnabled('calendarEnabled'));
|
||||||
const { firstDayOfWeek, timeFormat, showWeekNumbers, enableCalendarTasks, showTasksOnCalendar, calendarHoverPreview, showBirthdayCalendar, birthdayCalendarColor, updateSetting } = useSettingsStore();
|
const { firstDayOfWeek, timeFormat, showWeekNumbers, enableCalendarTasks, showTasksOnCalendar, calendarHoverPreview, showBirthdayCalendar, birthdayCalendarColor, updateSetting } = useSettingsStore();
|
||||||
const sharedCalendarColors = useSettingsStore((s) => s.sharedCalendarColors);
|
const sharedCalendarColors = useSettingsStore((s) => s.sharedCalendarColors);
|
||||||
const setSharedCalendarColor = useSettingsStore((s) => s.setSharedCalendarColor);
|
const setSharedCalendarColor = useSettingsStore((s) => s.setSharedCalendarColor);
|
||||||
@@ -179,10 +181,13 @@ export default function CalendarPage() {
|
|||||||
if (initialCheckDone && !isAuthenticated && !authLoading) {
|
if (initialCheckDone && !isAuthenticated && !authLoading) {
|
||||||
try { sessionStorage.setItem('redirect_after_login', window.location.pathname); } catch { /* ignore */ }
|
try { sessionStorage.setItem('redirect_after_login', window.location.pathname); } catch { /* ignore */ }
|
||||||
redirectToLogin();
|
redirectToLogin();
|
||||||
|
} else if (client && !calendarEnabled) {
|
||||||
|
// Calendar disabled by admin policy - send the user back to mail.
|
||||||
|
router.push("/");
|
||||||
} else if (client && !supportsCalendar && !pendingWebcalAccountChoice && !isProtocolAccountSwitching && !pendingSubscription && !showWebcalActionChoice && !hasPendingWebcal()) {
|
} else if (client && !supportsCalendar && !pendingWebcalAccountChoice && !isProtocolAccountSwitching && !pendingSubscription && !showWebcalActionChoice && !hasPendingWebcal()) {
|
||||||
router.push("/");
|
router.push("/");
|
||||||
}
|
}
|
||||||
}, [initialCheckDone, isAuthenticated, authLoading, client, supportsCalendar, pendingWebcalAccountChoice, isProtocolAccountSwitching, pendingSubscription, showWebcalActionChoice, router]);
|
}, [initialCheckDone, isAuthenticated, authLoading, client, calendarEnabled, supportsCalendar, pendingWebcalAccountChoice, isProtocolAccountSwitching, pendingSubscription, showWebcalActionChoice, router]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -1164,6 +1169,7 @@ export default function CalendarPage() {
|
|||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
if (!isAuthenticated) return null;
|
if (!isAuthenticated) return null;
|
||||||
|
if (!calendarEnabled) return null;
|
||||||
if (!supportsCalendar) return renderWebcalAccountPicker();
|
if (!supportsCalendar) return renderWebcalAccountPicker();
|
||||||
|
|
||||||
const renderView = () => {
|
const renderView = () => {
|
||||||
|
|||||||
@@ -627,7 +627,7 @@ export default function SettingsPage() {
|
|||||||
{ id: 'content_senders', label: t('tabs.content_senders'), icon: tabIcons.content_senders, group: 'privacy' },
|
{ id: 'content_senders', label: t('tabs.content_senders'), icon: tabIcons.content_senders, group: 'privacy' },
|
||||||
|
|
||||||
// Apps
|
// Apps
|
||||||
...(supportsCalendar ? [{ id: 'calendar' as Tab, label: t('tabs.calendar'), icon: tabIcons.calendar, group: 'apps' as TabGroup }] : []),
|
...(supportsCalendar && isFeatureEnabled('calendarEnabled') ? [{ id: 'calendar' as Tab, label: t('tabs.calendar'), icon: tabIcons.calendar, group: 'apps' as TabGroup }] : []),
|
||||||
...(isFeatureEnabled('contactsEnabled') ? [{ id: 'contacts' as Tab, label: t('tabs.contacts'), icon: tabIcons.contacts, group: 'apps' as TabGroup }] : []),
|
...(isFeatureEnabled('contactsEnabled') ? [{ id: 'contacts' as Tab, label: t('tabs.contacts'), icon: tabIcons.contacts, group: 'apps' as TabGroup }] : []),
|
||||||
...(supportsFiles && isFeatureEnabled('filesEnabled') ? [{ id: 'files' as Tab, label: t('tabs.files'), icon: tabIcons.files, group: 'apps' as TabGroup }] : []),
|
...(supportsFiles && isFeatureEnabled('filesEnabled') ? [{ id: 'files' as Tab, label: t('tabs.files'), icon: tabIcons.files, group: 'apps' as TabGroup }] : []),
|
||||||
...(isFeatureEnabled('sidebarAppsEnabled') ? [{ id: 'sidebar_apps' as Tab, label: t('tabs.sidebar_apps'), icon: tabIcons.sidebar_apps, group: 'apps' as TabGroup }] : []),
|
...(isFeatureEnabled('sidebarAppsEnabled') ? [{ id: 'sidebar_apps' as Tab, label: t('tabs.sidebar_apps'), icon: tabIcons.sidebar_apps, group: 'apps' as TabGroup }] : []),
|
||||||
@@ -646,7 +646,7 @@ export default function SettingsPage() {
|
|||||||
? ([
|
? ([
|
||||||
managedAccount.capabilities.sieve && supportsSieve ? 'filters' : null,
|
managedAccount.capabilities.sieve && supportsSieve ? 'filters' : null,
|
||||||
managedAccount.capabilities.mail && supportsVacation ? 'vacation' : null,
|
managedAccount.capabilities.mail && supportsVacation ? 'vacation' : null,
|
||||||
managedAccount.capabilities.calendars && supportsCalendar ? 'calendar' : null,
|
managedAccount.capabilities.calendars && supportsCalendar && isFeatureEnabled('calendarEnabled') ? 'calendar' : null,
|
||||||
managedAccount.capabilities.contacts && isFeatureEnabled('contactsEnabled') ? 'contacts' : null,
|
managedAccount.capabilities.contacts && isFeatureEnabled('contactsEnabled') ? 'contacts' : null,
|
||||||
].filter(Boolean) as Tab[])
|
].filter(Boolean) as Tab[])
|
||||||
: [];
|
: [];
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const FEATURE_GATE_LABELS: Partial<Record<keyof FeatureGates, { label: string; d
|
|||||||
settingsExportEnabled: { label: 'Settings Export/Import', description: 'Allow users to export and import settings JSON' },
|
settingsExportEnabled: { label: 'Settings Export/Import', description: 'Allow users to export and import settings JSON' },
|
||||||
customKeywordsEnabled: { label: 'Custom Keywords', description: 'Allow user-created labels and tags' },
|
customKeywordsEnabled: { label: 'Custom Keywords', description: 'Allow user-created labels and tags' },
|
||||||
templatesEnabled: { label: 'Email Templates', description: 'Allow email template creation and library' },
|
templatesEnabled: { label: 'Email Templates', description: 'Allow email template creation and library' },
|
||||||
|
calendarEnabled: { label: 'Calendar', description: 'Enable calendar features and views' },
|
||||||
calendarTasksEnabled: { label: 'Calendar Tasks', description: 'Show task panel in calendar view' },
|
calendarTasksEnabled: { label: 'Calendar Tasks', description: 'Show task panel in calendar view' },
|
||||||
contactsEnabled: { label: 'Contacts', description: 'Enable contacts/address book features' },
|
contactsEnabled: { label: 'Contacts', description: 'Enable contacts/address book features' },
|
||||||
smimeEnabled: { label: 'S/MIME', description: 'Enable certificate management and email signing' },
|
smimeEnabled: { label: 'S/MIME', description: 'Enable certificate management and email signing' },
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ export function NavigationRail({
|
|||||||
const sidebarAppsEnabled = usePolicyStore((s) => s.isFeatureEnabled('sidebarAppsEnabled'));
|
const sidebarAppsEnabled = usePolicyStore((s) => s.isFeatureEnabled('sidebarAppsEnabled'));
|
||||||
const filesEnabled = usePolicyStore((s) => s.isFeatureEnabled('filesEnabled'));
|
const filesEnabled = usePolicyStore((s) => s.isFeatureEnabled('filesEnabled'));
|
||||||
const contactsEnabled = usePolicyStore((s) => s.isFeatureEnabled('contactsEnabled'));
|
const contactsEnabled = usePolicyStore((s) => s.isFeatureEnabled('contactsEnabled'));
|
||||||
|
const calendarEnabled = usePolicyStore((s) => s.isFeatureEnabled('calendarEnabled'));
|
||||||
const visibleSidebarApps = sidebarAppsEnabled ? sidebarApps : [];
|
const visibleSidebarApps = sidebarAppsEnabled ? sidebarApps : [];
|
||||||
const inboxUnread = mailboxes.find(m => m.role === "inbox")?.unreadEmails || 0;
|
const inboxUnread = mailboxes.find(m => m.role === "inbox")?.unreadEmails || 0;
|
||||||
const [isStalwartAdmin, setIsStalwartAdmin] = useState(false);
|
const [isStalwartAdmin, setIsStalwartAdmin] = useState(false);
|
||||||
@@ -268,7 +269,7 @@ export function NavigationRail({
|
|||||||
|
|
||||||
const navItems: NavItem[] = [
|
const navItems: NavItem[] = [
|
||||||
{ id: "mail", icon: Mail, labelKey: "mail", href: "/", badge: inboxUnread },
|
{ id: "mail", icon: Mail, labelKey: "mail", href: "/", badge: inboxUnread },
|
||||||
{ id: "calendar", icon: Calendar, labelKey: "calendar", href: "/calendar", hidden: !supportsCalendar },
|
{ id: "calendar", icon: Calendar, labelKey: "calendar", href: "/calendar", hidden: !supportsCalendar || !calendarEnabled },
|
||||||
{ id: "contacts", icon: BookUser, labelKey: "contacts", href: "/contacts", hidden: !supportsContacts || !contactsEnabled },
|
{ id: "contacts", icon: BookUser, labelKey: "contacts", href: "/contacts", hidden: !supportsContacts || !contactsEnabled },
|
||||||
{ id: "files", icon: HardDrive, labelKey: "files", href: "/files", hidden: !supportsFiles || !filesEnabled },
|
{ id: "files", icon: HardDrive, labelKey: "files", href: "/files", hidden: !supportsFiles || !filesEnabled },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useAuthStore } from "@/stores/auth-store";
|
|||||||
import { useCalendarStore } from "@/stores/calendar-store";
|
import { useCalendarStore } from "@/stores/calendar-store";
|
||||||
import { useWebDAVStore } from "@/stores/webdav-store";
|
import { useWebDAVStore } from "@/stores/webdav-store";
|
||||||
import { useSettingsStore } from "@/stores/settings-store";
|
import { useSettingsStore } from "@/stores/settings-store";
|
||||||
|
import { usePolicyStore } from "@/stores/policy-store";
|
||||||
import { getTourSteps, type TourStep } from "./tour-steps";
|
import { getTourSteps, type TourStep } from "./tour-steps";
|
||||||
import { TourOverlay } from "./tour-overlay";
|
import { TourOverlay } from "./tour-overlay";
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ export function TourProvider({ children }: { children: ReactNode }) {
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { isDemoMode } = useAuthStore();
|
const { isDemoMode } = useAuthStore();
|
||||||
const { supportsCalendar } = useCalendarStore();
|
const { supportsCalendar } = useCalendarStore();
|
||||||
|
const calendarEnabled = usePolicyStore((s) => s.isFeatureEnabled('calendarEnabled'));
|
||||||
const { supportsWebDAV } = useWebDAVStore();
|
const { supportsWebDAV } = useWebDAVStore();
|
||||||
const tourCompleted = useSettingsStore((s) => s.tourCompleted);
|
const tourCompleted = useSettingsStore((s) => s.tourCompleted);
|
||||||
const showOnboardingOnNewDevices = useSettingsStore((s) => s.showOnboardingOnNewDevices);
|
const showOnboardingOnNewDevices = useSettingsStore((s) => s.showOnboardingOnNewDevices);
|
||||||
@@ -47,7 +49,7 @@ export function TourProvider({ children }: { children: ReactNode }) {
|
|||||||
const [currentStep, setCurrentStep] = useState(0);
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
const [hasCompletedTour, setHasCompletedTour] = useState(false);
|
const [hasCompletedTour, setHasCompletedTour] = useState(false);
|
||||||
|
|
||||||
const steps = getTourSteps({ isDemoMode, supportsCalendar, supportsWebDAV: supportsWebDAV !== false });
|
const steps = getTourSteps({ isDemoMode, supportsCalendar: supportsCalendar && calendarEnabled, supportsWebDAV: supportsWebDAV !== false });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// One-time migration: if the legacy per-device flag is set but the synced
|
// One-time migration: if the legacy per-device flag is set but the synced
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export interface FeatureGates {
|
|||||||
settingsExportEnabled: boolean;
|
settingsExportEnabled: boolean;
|
||||||
customKeywordsEnabled: boolean;
|
customKeywordsEnabled: boolean;
|
||||||
templatesEnabled: boolean;
|
templatesEnabled: boolean;
|
||||||
|
calendarEnabled: boolean;
|
||||||
calendarTasksEnabled: boolean;
|
calendarTasksEnabled: boolean;
|
||||||
smimeEnabled: boolean;
|
smimeEnabled: boolean;
|
||||||
externalContentEnabled: boolean;
|
externalContentEnabled: boolean;
|
||||||
@@ -75,6 +76,7 @@ export const DEFAULT_FEATURE_GATES: FeatureGates = {
|
|||||||
settingsExportEnabled: true,
|
settingsExportEnabled: true,
|
||||||
customKeywordsEnabled: true,
|
customKeywordsEnabled: true,
|
||||||
templatesEnabled: true,
|
templatesEnabled: true,
|
||||||
|
calendarEnabled: true,
|
||||||
calendarTasksEnabled: true,
|
calendarTasksEnabled: true,
|
||||||
smimeEnabled: true,
|
smimeEnabled: true,
|
||||||
externalContentEnabled: true,
|
externalContentEnabled: true,
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ async function readFeatures(): Promise<TelemetryFeatures> {
|
|||||||
return {
|
return {
|
||||||
// Booleans only. We read whether a feature is enabled - never any
|
// Booleans only. We read whether a feature is enabled - never any
|
||||||
// config value beyond a presence check.
|
// config value beyond a presence check.
|
||||||
calendar: gates.calendarTasksEnabled === true,
|
calendar: gates.calendarEnabled === true,
|
||||||
contacts: gates.contactsEnabled === true,
|
contacts: gates.contactsEnabled === true,
|
||||||
files: gates.filesEnabled === true,
|
files: gates.filesEnabled === true,
|
||||||
extensions: gates.pluginsEnabled === true,
|
extensions: gates.pluginsEnabled === true,
|
||||||
|
|||||||
Reference in New Issue
Block a user