From bad92c6344cf1ddefce5030ca9f03cab2edcfb2e Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Fri, 21 Nov 2025 15:46:13 +0100 Subject: [PATCH 1/2] feat: gate dashboard and grouped status view behind experimental settings --- components/SettingsUI/UISettings.tsx | 37 ++++++++++ constants/defaultSettings.ts | 3 + core/commandsService.ts | 28 +++++--- integrations/commands/commandsIntegration.ts | 4 +- main.tsx | 76 ++++++++++++++++++-- types/pluginSettings.ts | 3 + 6 files changed, 134 insertions(+), 17 deletions(-) diff --git a/components/SettingsUI/UISettings.tsx b/components/SettingsUI/UISettings.tsx index 1a2ac0d..d39a76b 100644 --- a/components/SettingsUI/UISettings.tsx +++ b/components/SettingsUI/UISettings.tsx @@ -322,6 +322,43 @@ export const UISettings: React.FC = ({ settings, onChange }) => { /> +

Experimental features

+ + + + + + + + + + + + +

Behavior & Other

{ - await this.openStatusPane(); - }, - }); - this.registeredCommands.add("open-status-pane"); + // Open Status Pane command (experimental) + if (this.shouldEnableGroupedView()) { + this.plugin.addCommand({ + id: "open-status-pane", + name: "Open status pane", + callback: async () => { + await this.openStatusPane(); + }, + }); + this.registeredCommands.add("open-status-pane"); + } } private async openStatusPane(): Promise { @@ -356,6 +358,14 @@ export class CommandsService { this.registeredCommands.clear(); } + private shouldEnableGroupedView(): boolean { + const settings = settingsService.settings; + return ( + Boolean(settings.enableExperimentalFeatures) && + Boolean(settings.enableGroupedStatusView) + ); + } + public destroy(): void { // Remove all registered commands properly this.removeRegisteredCommands(); diff --git a/integrations/commands/commandsIntegration.ts b/integrations/commands/commandsIntegration.ts index 07d0f0e..2e28690 100644 --- a/integrations/commands/commandsIntegration.ts +++ b/integrations/commands/commandsIntegration.ts @@ -32,7 +32,9 @@ export class CommandsIntegration { if ( key === "quickStatusCommands" || key === "useMultipleStatuses" || - key === "templates" + key === "templates" || + key === "enableExperimentalFeatures" || + key === "enableGroupedStatusView" ) { this.commandsService.destroy(); /// BUG: if removed a command will persist because is not removed, you need the oldStates to send it to be disabled // const oldValue = this.settings[key]; // TODO: Send the old value diff --git a/main.tsx b/main.tsx index 81aa831..6cbbe06 100644 --- a/main.tsx +++ b/main.tsx @@ -27,6 +27,8 @@ export default class NoteStatusPlugin extends Plugin { private fileExplorerIntegration: FileExplorerIntegration; private commandsIntegration: CommandsIntegration; private editorToolbarIntegration: EditorToolbarIntegration; + private groupedViewRibbonEl: HTMLElement | null = null; + private dashboardRibbonEl: HTMLElement | null = null; async onload() { BaseNoteStatusService.initialize(this.app); @@ -53,13 +55,7 @@ export default class NoteStatusPlugin extends Plugin { (leaf) => new StatusDashboardView(leaf), ); - this.addRibbonIcon("list-tree", "Open grouped status view", () => { - this.activateView(); - }); - - this.addRibbonIcon("activity", "Open status dashboard", () => { - this.activateDashboard(); - }); + this.syncExperimentalFeatureShortcuts(); } async onunload() { @@ -70,6 +66,10 @@ export default class NoteStatusPlugin extends Plugin { this.commandsIntegration?.destroy(); this.editorToolbarIntegration?.destroy(); this.pluginSettingsIntegration?.destroy(); + this.groupedViewRibbonEl?.remove(); + this.groupedViewRibbonEl = null; + this.dashboardRibbonEl?.remove(); + this.dashboardRibbonEl = null; // Clean up event subscriptions eventBus.unsubscribe( @@ -148,6 +148,61 @@ export default class NoteStatusPlugin extends Plugin { } } + private canUseGroupedStatusView(): boolean { + const settings = settingsService.settings; + return ( + Boolean(settings.enableExperimentalFeatures) && + Boolean(settings.enableGroupedStatusView) + ); + } + + private canUseStatusDashboard(): boolean { + const settings = settingsService.settings; + return ( + Boolean(settings.enableExperimentalFeatures) && + Boolean(settings.enableStatusDashboard) + ); + } + + private syncExperimentalFeatureShortcuts(): void { + this.ensureGroupedViewRibbon(); + this.ensureDashboardRibbon(); + } + + private ensureGroupedViewRibbon(): void { + if (this.canUseGroupedStatusView()) { + if (!this.groupedViewRibbonEl) { + this.groupedViewRibbonEl = this.addRibbonIcon( + "list-tree", + "Open grouped status view", + () => { + this.activateView(); + }, + ); + } + } else if (this.groupedViewRibbonEl) { + this.groupedViewRibbonEl.remove(); + this.groupedViewRibbonEl = null; + } + } + + private ensureDashboardRibbon(): void { + if (this.canUseStatusDashboard()) { + if (!this.dashboardRibbonEl) { + this.dashboardRibbonEl = this.addRibbonIcon( + "activity", + "Open status dashboard", + () => { + this.activateDashboard(); + }, + ); + } + } else if (this.dashboardRibbonEl) { + this.dashboardRibbonEl.remove(); + this.dashboardRibbonEl = null; + } + } + private async loadEventBus() { // Propagate to custom event bus the new active file this.app.workspace.on( @@ -179,6 +234,13 @@ export default class NoteStatusPlugin extends Plugin { if (key === "enableStatusOverviewPopup" && value === false) { StatusesInfoPopup.close(); } + if ( + key === "enableExperimentalFeatures" || + key === "enableStatusDashboard" || + key === "enableGroupedStatusView" + ) { + this.syncExperimentalFeatureShortcuts(); + } }, "main-status-popup-setting-subscriptor", ); diff --git a/types/pluginSettings.ts b/types/pluginSettings.ts index c8e9a52..90af6f0 100644 --- a/types/pluginSettings.ts +++ b/types/pluginSettings.ts @@ -28,6 +28,9 @@ export type PluginSettings = { fileExplorerLeftBorder: boolean; // Whether to display a colored border on the explorer item fileExplorerStatusDot: boolean; // Whether to append a small colored dot next to the filename fileExplorerUnderlineFileName: boolean; // Whether to underline the filename using the status color + enableExperimentalFeatures: boolean; // Gate for experimental features + enableStatusDashboard: boolean; // Toggle for the status dashboard experiment + enableGroupedStatusView: boolean; // Toggle for the grouped status view experiment enabledTemplates: string[]; // IDs of enabled templates useCustomStatusesOnly: boolean; // Whether to use only custom statuses or include templates useMultipleStatuses: boolean; // Whether to allow multiple statuses per note From c3659363e8d04eb55d295c0b086f48c3543da05b Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Fri, 21 Nov 2025 16:02:23 +0100 Subject: [PATCH 2/2] chore: clean-up --- core/commandsService.ts | 7 +-- main.tsx | 101 ++++++++++++++++------------------ utils/experimentalFeatures.ts | 23 ++++++++ 3 files changed, 73 insertions(+), 58 deletions(-) create mode 100644 utils/experimentalFeatures.ts diff --git a/core/commandsService.ts b/core/commandsService.ts index 7076eb3..9a1ea49 100644 --- a/core/commandsService.ts +++ b/core/commandsService.ts @@ -3,6 +3,7 @@ import { NoteStatusService, BaseNoteStatusService } from "./noteStatusService"; import settingsService from "./settingsService"; import eventBus from "./eventBus"; import { VIEW_TYPE_EXAMPLE } from "../integrations/views/grouped-status-view"; +import { isExperimentalFeatureEnabled } from "@/utils/experimentalFeatures"; export class CommandsService { private plugin: Plugin; @@ -359,11 +360,7 @@ export class CommandsService { } private shouldEnableGroupedView(): boolean { - const settings = settingsService.settings; - return ( - Boolean(settings.enableExperimentalFeatures) && - Boolean(settings.enableGroupedStatusView) - ); + return isExperimentalFeatureEnabled("groupedStatusView"); } public destroy(): void { diff --git a/main.tsx b/main.tsx index 6cbbe06..2ea5ed9 100644 --- a/main.tsx +++ b/main.tsx @@ -19,6 +19,7 @@ import { } from "./integrations/views/status-dashboard-view"; import { StatusesInfoPopup } from "./integrations/popups/statusesInfoPopupIntegration"; import statusStoreManager from "./core/statusStoreManager"; +import { isExperimentalFeatureEnabled } from "@/utils/experimentalFeatures"; export default class NoteStatusPlugin extends Plugin { private statusBarIntegration: StatusBarIntegration; @@ -27,8 +28,7 @@ export default class NoteStatusPlugin extends Plugin { private fileExplorerIntegration: FileExplorerIntegration; private commandsIntegration: CommandsIntegration; private editorToolbarIntegration: EditorToolbarIntegration; - private groupedViewRibbonEl: HTMLElement | null = null; - private dashboardRibbonEl: HTMLElement | null = null; + private experimentalRibbonShortcuts: Map = new Map(); async onload() { BaseNoteStatusService.initialize(this.app); @@ -66,10 +66,8 @@ export default class NoteStatusPlugin extends Plugin { this.commandsIntegration?.destroy(); this.editorToolbarIntegration?.destroy(); this.pluginSettingsIntegration?.destroy(); - this.groupedViewRibbonEl?.remove(); - this.groupedViewRibbonEl = null; - this.dashboardRibbonEl?.remove(); - this.dashboardRibbonEl = null; + this.experimentalRibbonShortcuts.forEach((el) => el.remove()); + this.experimentalRibbonShortcuts.clear(); // Clean up event subscriptions eventBus.unsubscribe( @@ -148,58 +146,55 @@ export default class NoteStatusPlugin extends Plugin { } } - private canUseGroupedStatusView(): boolean { - const settings = settingsService.settings; - return ( - Boolean(settings.enableExperimentalFeatures) && - Boolean(settings.enableGroupedStatusView) - ); - } - - private canUseStatusDashboard(): boolean { - const settings = settingsService.settings; - return ( - Boolean(settings.enableExperimentalFeatures) && - Boolean(settings.enableStatusDashboard) - ); - } - private syncExperimentalFeatureShortcuts(): void { - this.ensureGroupedViewRibbon(); - this.ensureDashboardRibbon(); + const shortcuts = this.getExperimentalShortcuts(); + Object.entries(shortcuts).forEach(([key, config]) => { + this.toggleExperimentalRibbon(key, config); + }); } - private ensureGroupedViewRibbon(): void { - if (this.canUseGroupedStatusView()) { - if (!this.groupedViewRibbonEl) { - this.groupedViewRibbonEl = this.addRibbonIcon( - "list-tree", - "Open grouped status view", - () => { - this.activateView(); - }, - ); - } - } else if (this.groupedViewRibbonEl) { - this.groupedViewRibbonEl.remove(); - this.groupedViewRibbonEl = null; + private getExperimentalShortcuts() { + return { + groupedStatusView: { + feature: "groupedStatusView" as const, + icon: "list-tree", + title: "Open grouped status view", + handler: () => this.activateView(), + }, + statusDashboard: { + feature: "statusDashboard" as const, + icon: "activity", + title: "Open status dashboard", + handler: () => this.activateDashboard(), + }, + }; + } + + private toggleExperimentalRibbon( + key: string, + config: { + feature: Parameters[0]; + icon: string; + title: string; + handler: () => void; + }, + ): void { + const isEnabled = isExperimentalFeatureEnabled(config.feature); + const existingIcon = this.experimentalRibbonShortcuts.get(key); + + if (isEnabled && !existingIcon) { + const ribbonEl = this.addRibbonIcon( + config.icon, + config.title, + config.handler, + ); + this.experimentalRibbonShortcuts.set(key, ribbonEl); + return; } - } - private ensureDashboardRibbon(): void { - if (this.canUseStatusDashboard()) { - if (!this.dashboardRibbonEl) { - this.dashboardRibbonEl = this.addRibbonIcon( - "activity", - "Open status dashboard", - () => { - this.activateDashboard(); - }, - ); - } - } else if (this.dashboardRibbonEl) { - this.dashboardRibbonEl.remove(); - this.dashboardRibbonEl = null; + if (!isEnabled && existingIcon) { + existingIcon.remove(); + this.experimentalRibbonShortcuts.delete(key); } } diff --git a/utils/experimentalFeatures.ts b/utils/experimentalFeatures.ts new file mode 100644 index 0000000..98e04a7 --- /dev/null +++ b/utils/experimentalFeatures.ts @@ -0,0 +1,23 @@ +import settingsService from "@/core/settingsService"; + +export type ExperimentalFeature = "statusDashboard" | "groupedStatusView"; + +const FEATURE_SETTING_MAP: Record< + ExperimentalFeature, + keyof typeof settingsService.settings +> = { + statusDashboard: "enableStatusDashboard", + groupedStatusView: "enableGroupedStatusView", +}; + +export function isExperimentalFeatureEnabled( + feature: ExperimentalFeature, +): boolean { + const settings = settingsService.settings; + if (!settings.enableExperimentalFeatures) { + return false; + } + + const flagKey = FEATURE_SETTING_MAP[feature]; + return Boolean(settings[flagKey]); +}