Skip to content

Commit 379a859

Browse files
committed
feat(ui): add set indicator toggle with consolidated options menu
Add configurable toggle for button set indicator visibility in the status bar. Evolved ImportExportMenu (backup-only) into OptionsMenu, consolidating Display settings and Backup sections into a single utility menu. Designed as an extensible structure to accommodate future display settings like refresh button visibility. - Backend: setIndicator config schema, adapter, StatusBarManager visibility control, webview message handler - Frontend: new OptionsMenu (⋮ icon), toggle via DropdownMenuCheckboxItem - ButtonSetSelector remains dedicated to set selection/management (separation of concerns)
1 parent 5c3b550 commit 379a859

25 files changed

+374
-57
lines changed

package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,20 @@
345345
}
346346
},
347347
"type": "object"
348+
},
349+
"quickCommandButtons.setIndicator": {
350+
"default": {
351+
"enabled": true
352+
},
353+
"description": "%config.setIndicator.description%",
354+
"properties": {
355+
"enabled": {
356+
"default": true,
357+
"description": "%config.setIndicator.enabled.description%",
358+
"type": "boolean"
359+
}
360+
},
361+
"type": "object"
348362
}
349363
}
350364
}

package.nls.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
"config.refreshButton.description": "Configuration for the refresh button",
3535
"config.refreshButton.enabled.description": "Enable/disable the refresh button",
3636
"config.refreshButton.icon.description": "Icon for the refresh button (supports $(icon-name) syntax)",
37+
"config.setIndicator.description": "Configuration for the set indicator button",
38+
"config.setIndicator.enabled.description": "Enable/disable the set indicator button in the status bar",
3739
"config.title": "Quick Command Buttons",
3840
"view.commands": "Commands",
3941
"viewsContainer.title": "Quick Commands"

package.nls.ko.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
"config.refreshButton.description": "새로고침 버튼 구성",
3535
"config.refreshButton.enabled.description": "새로고침 버튼 활성화/비활성화",
3636
"config.refreshButton.icon.description": "새로고침 버튼 아이콘 ($(icon-name) 구문 지원)",
37+
"config.setIndicator.description": "세트 표시 버튼 구성",
38+
"config.setIndicator.enabled.description": "상태 표시줄에서 세트 표시 버튼 활성화/비활성화",
3739
"config.title": "Quick Command Buttons",
3840
"view.commands": "Commands",
3941
"viewsContainer.title": "Quick Command Buttons"

src/internal/adapters.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,35 @@ describe("adapters", () => {
234234
});
235235
});
236236

237+
describe("getSetIndicatorConfig", () => {
238+
it("should return set indicator config from workspace configuration", () => {
239+
const mockConfig = {
240+
get: vi.fn((key: string) => (key === "setIndicator" ? { enabled: false } : undefined)),
241+
};
242+
243+
(vscode.workspace.getConfiguration as vi.Mock).mockReturnValue(mockConfig);
244+
245+
const configReader = createVSCodeConfigReader();
246+
const result = configReader.getSetIndicatorConfig();
247+
248+
expect(result).toEqual({ enabled: false });
249+
expect(mockConfig.get).toHaveBeenCalledWith("setIndicator");
250+
});
251+
252+
it("should return default config when no set indicator config exists", () => {
253+
const mockConfig = {
254+
get: vi.fn(() => undefined),
255+
};
256+
257+
(vscode.workspace.getConfiguration as vi.Mock).mockReturnValue(mockConfig);
258+
259+
const configReader = createVSCodeConfigReader();
260+
const result = configReader.getSetIndicatorConfig();
261+
262+
expect(result).toEqual({ enabled: true });
263+
});
264+
});
265+
237266
describe("createProjectLocalStorage", () => {
238267
it("should return empty array when no buttons are stored", () => {
239268
const mockContext = {

src/internal/adapters.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as vscode from "vscode";
22
import { CONFIG_SECTION } from "../pkg/config-constants";
3-
import { ButtonConfig, RefreshButtonConfig } from "../pkg/types";
3+
import { ButtonConfig, RefreshButtonConfig, SetIndicatorConfig } from "../pkg/types";
44
import { ButtonConfigWithOptionalId, ensureIdsInArray, stripIdsInArray } from "./utils/ensure-id";
55
import { validateButtonConfigs, ValidationResult } from "./utils/validate-button-config";
66

@@ -10,6 +10,10 @@ const DEFAULT_REFRESH_CONFIG: RefreshButtonConfig = {
1010
icon: "$(refresh)",
1111
};
1212

13+
const DEFAULT_SET_INDICATOR_CONFIG: SetIndicatorConfig = {
14+
enabled: true,
15+
};
16+
1317
export const LOCAL_BUTTONS_STORAGE_KEY = "quickCommandButtons.localButtons";
1418

1519
export type TerminalExecutor = (
@@ -26,6 +30,7 @@ export type ConfigReader = {
2630
getButtonsFromScope: (target: vscode.ConfigurationTarget) => ButtonConfig[];
2731
getRawButtonsFromScope: (target: vscode.ConfigurationTarget) => ButtonConfigWithOptionalId[];
2832
getRefreshConfig: () => RefreshButtonConfig;
33+
getSetIndicatorConfig: () => SetIndicatorConfig;
2934
onConfigChange: (listener: () => void) => vscode.Disposable;
3035
validateButtons: () => ValidationResult;
3136
};
@@ -40,6 +45,7 @@ export type QuickPickCreator = <T extends vscode.QuickPickItem>() => vscode.Quic
4045
export type ConfigWriter = {
4146
writeButtons: (buttons: ButtonConfig[], target: vscode.ConfigurationTarget) => Promise<void>;
4247
writeConfigurationTarget: (target: string) => Promise<void>;
48+
writeSetIndicatorConfig: (config: SetIndicatorConfig) => Promise<void>;
4349
};
4450

4551
export type ProjectLocalStorage = {
@@ -54,6 +60,10 @@ const getButtonsFromConfig = (
5460
const getRefreshConfigFromConfig = (config: vscode.WorkspaceConfiguration): RefreshButtonConfig =>
5561
config.get("refreshButton") || DEFAULT_REFRESH_CONFIG;
5662

63+
const getSetIndicatorConfigFromConfig = (
64+
config: vscode.WorkspaceConfiguration
65+
): SetIndicatorConfig => config.get("setIndicator") || DEFAULT_SET_INDICATOR_CONFIG;
66+
5767
const isQuickCommandButtonsConfigChange = (event: vscode.ConfigurationChangeEvent): boolean =>
5868
event.affectsConfiguration(CONFIG_SECTION);
5969

@@ -92,6 +102,10 @@ export const createVSCodeConfigReader = (): ConfigReader => ({
92102
const config = vscode.workspace.getConfiguration(CONFIG_SECTION);
93103
return getRefreshConfigFromConfig(config);
94104
},
105+
getSetIndicatorConfig: () => {
106+
const config = vscode.workspace.getConfiguration(CONFIG_SECTION);
107+
return getSetIndicatorConfigFromConfig(config);
108+
},
95109
onConfigChange: (listener: () => void) =>
96110
vscode.workspace.onDidChangeConfiguration((event) => {
97111
if (!isQuickCommandButtonsConfigChange(event)) return;
@@ -122,6 +136,11 @@ export const createVSCodeConfigWriter = (): ConfigWriter => ({
122136
const config = vscode.workspace.getConfiguration(CONFIG_SECTION);
123137
await config.update("configurationTarget", target, vscode.ConfigurationTarget.Global);
124138
},
139+
// Global scope: set indicator is a user-level display preference, not project-scoped
140+
writeSetIndicatorConfig: async (indicatorConfig: SetIndicatorConfig) => {
141+
const config = vscode.workspace.getConfiguration(CONFIG_SECTION);
142+
await config.update("setIndicator", indicatorConfig, vscode.ConfigurationTarget.Global);
143+
},
125144
});
126145

127146
export const createProjectLocalStorage = (

src/internal/managers/button-set-manager.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ describe("ButtonSetManager", () => {
2828
const mockConfigWriter = {
2929
writeButtons: vi.fn(),
3030
writeConfigurationTarget: vi.fn(),
31+
writeSetIndicatorConfig: vi.fn(),
3132
};
3233
return ConfigManager.create({ configWriter: mockConfigWriter });
3334
};
@@ -39,6 +40,7 @@ describe("ButtonSetManager", () => {
3940
getRefreshConfig: vi
4041
.fn()
4142
.mockReturnValue({ color: "#00BCD4", enabled: true, icon: "$(refresh)" }),
43+
getSetIndicatorConfig: vi.fn().mockReturnValue({ enabled: true }),
4244
onConfigChange: vi.fn().mockReturnValue({ dispose: vi.fn() }),
4345
validateButtons: vi.fn().mockReturnValue({ errors: [], hasErrors: false }),
4446
});

src/internal/managers/config-manager.spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as vscode from "vscode";
22
import { CONFIGURATION_TARGETS } from "../../pkg/config-constants";
33
import { ConfigWriter, ProjectLocalStorage } from "../adapters";
4+
import { EventBus } from "../event-bus";
45
import { ConfigManager } from "./config-manager";
56

67
describe("ConfigManager", () => {
@@ -12,6 +13,7 @@ describe("ConfigManager", () => {
1213
const createMockConfigWriter = (): ConfigWriter => ({
1314
writeButtons: vi.fn(),
1415
writeConfigurationTarget: vi.fn(),
16+
writeSetIndicatorConfig: vi.fn(),
1517
});
1618

1719
const createMockLocalStorage = (): ProjectLocalStorage => ({
@@ -229,6 +231,28 @@ describe("ConfigManager", () => {
229231
});
230232
});
231233

234+
describe("updateSetIndicatorConfig", () => {
235+
it("should delegate to configWriter.writeSetIndicatorConfig", async () => {
236+
const mockConfigWriter = createMockConfigWriter();
237+
const configManager = ConfigManager.create({ configWriter: mockConfigWriter });
238+
await configManager.updateSetIndicatorConfig({ enabled: false });
239+
240+
expect(mockConfigWriter.writeSetIndicatorConfig).toHaveBeenCalledWith({ enabled: false });
241+
});
242+
243+
it("should emit config:changed event via EventBus", async () => {
244+
const mockConfigWriter = createMockConfigWriter();
245+
const eventBus = new EventBus();
246+
const listener = vi.fn();
247+
eventBus.on("config:changed", listener);
248+
249+
const configManager = ConfigManager.create({ configWriter: mockConfigWriter, eventBus });
250+
await configManager.updateSetIndicatorConfig({ enabled: true });
251+
252+
expect(listener).toHaveBeenCalledWith({ scope: "global" });
253+
});
254+
});
255+
232256
describe("getButtonsWithFallback", () => {
233257
it("should return local buttons when local scope is selected and has buttons", () => {
234258
const localButtons = [{ command: "echo local", id: "local-btn", name: "Local Command" }];

src/internal/managers/config-manager.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
VS_CODE_CONFIGURATION_TARGETS,
77
ConfigurationTargetType,
88
} from "../../pkg/config-constants";
9-
import { ButtonConfig } from "../../pkg/types";
9+
import { ButtonConfig, SetIndicatorConfig } from "../../pkg/types";
1010
import { ButtonConfigWithOptionalId, ValidationError } from "../../shared/types";
1111
import { ConfigWriter, ProjectLocalStorage } from "../adapters";
1212
import { EventBus } from "../event-bus";
@@ -137,4 +137,9 @@ export class ConfigManager {
137137
await this.configWriter.writeConfigurationTarget(target);
138138
this.eventBus?.emit("configTarget:changed", { target });
139139
}
140+
141+
async updateSetIndicatorConfig(indicatorConfig: SetIndicatorConfig): Promise<void> {
142+
await this.configWriter.writeSetIndicatorConfig(indicatorConfig);
143+
this.eventBus?.emit("config:changed", { scope: CONFIGURATION_TARGETS.GLOBAL });
144+
}
140145
}

src/internal/managers/import-export-manager.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,15 @@ describe("ImportExportManager", () => {
6262
getButtonsFromScope: vi.fn(),
6363
getRawButtonsFromScope: vi.fn().mockReturnValue([]),
6464
getRefreshConfig: vi.fn(),
65+
getSetIndicatorConfig: vi.fn().mockReturnValue({ enabled: true }),
6566
onConfigChange: vi.fn(),
6667
validateButtons: vi.fn().mockReturnValue({ errors: [], hasErrors: false }),
6768
};
6869

6970
mockConfigWriter = {
7071
writeButtons: vi.fn().mockResolvedValue(undefined),
7172
writeConfigurationTarget: vi.fn().mockResolvedValue(undefined),
73+
writeSetIndicatorConfig: vi.fn().mockResolvedValue(undefined),
7274
};
7375

7476
mockLocalStorage = {

src/internal/managers/status-bar-manager.spec.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,9 @@ describe("status-bar-manager", () => {
365365
enabled: false, // Disable refresh button to simplify tests
366366
icon: "🔄",
367367
}),
368+
getSetIndicatorConfig: vi.fn().mockReturnValue({
369+
enabled: true,
370+
}),
368371
};
369372

370373
mockStatusBarCreator = vi.fn().mockImplementation(() => ({
@@ -570,5 +573,91 @@ describe("status-bar-manager", () => {
570573
consoleSpy.mockRestore();
571574
});
572575
});
576+
577+
describe("set indicator visibility", () => {
578+
type MockStatusBarItem = {
579+
color: string | null;
580+
command: string;
581+
dispose: ReturnType<typeof vi.fn>;
582+
show: ReturnType<typeof vi.fn>;
583+
text: string;
584+
tooltip: string;
585+
};
586+
587+
const getCreatedItems = (): MockStatusBarItem[] =>
588+
mockStatusBarCreator.mock.results.map(
589+
(r: { value: MockStatusBarItem }) => r.value
590+
);
591+
592+
it("should not create set indicator when enabled is false", () => {
593+
mockConfigReader.getSetIndicatorConfig.mockReturnValue({ enabled: false });
594+
595+
statusBarManager = StatusBarManager.create({
596+
configReader: mockConfigReader,
597+
statusBarCreator: mockStatusBarCreator,
598+
store: mockStore,
599+
});
600+
601+
mockStatusBarCreator.mockClear();
602+
statusBarManager.refreshButtons();
603+
604+
const setIndicator = getCreatedItems().find((item) =>
605+
item.text.startsWith("$(layers)")
606+
);
607+
expect(setIndicator).toBeUndefined();
608+
});
609+
610+
it("should create set indicator when enabled is true", () => {
611+
mockConfigReader.getSetIndicatorConfig.mockReturnValue({ enabled: true });
612+
613+
statusBarManager = StatusBarManager.create({
614+
configReader: mockConfigReader,
615+
statusBarCreator: mockStatusBarCreator,
616+
store: mockStore,
617+
});
618+
619+
mockStatusBarCreator.mockClear();
620+
statusBarManager.refreshButtons();
621+
622+
const setIndicator = getCreatedItems().find((item) =>
623+
item.text.startsWith("$(layers)")
624+
);
625+
expect(setIndicator).toBeDefined();
626+
});
627+
628+
it("should create set indicator by default", () => {
629+
statusBarManager = StatusBarManager.create({
630+
configReader: mockConfigReader,
631+
statusBarCreator: mockStatusBarCreator,
632+
store: mockStore,
633+
});
634+
635+
mockStatusBarCreator.mockClear();
636+
statusBarManager.refreshButtons();
637+
638+
const setIndicator = getCreatedItems().find((item) =>
639+
item.text.startsWith("$(layers)")
640+
);
641+
expect(setIndicator).toBeDefined();
642+
});
643+
644+
it("should still create command buttons when set indicator is disabled", () => {
645+
mockConfigReader.getSetIndicatorConfig.mockReturnValue({ enabled: false });
646+
647+
statusBarManager = StatusBarManager.create({
648+
configReader: mockConfigReader,
649+
statusBarCreator: mockStatusBarCreator,
650+
store: mockStore,
651+
});
652+
653+
mockStatusBarCreator.mockClear();
654+
statusBarManager.refreshButtons();
655+
656+
const commandButton = getCreatedItems().find(
657+
(item) => item.text === "Test Button 1"
658+
);
659+
expect(commandButton).toBeDefined();
660+
});
661+
});
573662
});
574663
});

0 commit comments

Comments
 (0)