Skip to content

Commit 8692516

Browse files
authored
Merge pull request #189 from KubrickCode/develop/shlee/188
feat(button-set): add rename button set feature
2 parents 3b87b19 + b880edb commit 8692516

File tree

11 files changed

+398
-71
lines changed

11 files changed

+398
-71
lines changed

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

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,126 @@ describe("ButtonSetManager", () => {
641641
});
642642
});
643643

644-
describe("updateButtonSet", () => {
644+
describe("renameButtonSet", () => {
645+
it("should return setNotFound error when renaming non-existent set", async () => {
646+
const existingSets = [{ buttons: [], id: "set-1", name: "SetA" }];
647+
const mockConfig = createMockConfig();
648+
mockConfig.get.mockReturnValue(CONFIGURATION_TARGETS.WORKSPACE);
649+
mockConfig.inspect.mockImplementation((key: string) => {
650+
if (key === "buttonSets") {
651+
return { workspaceValue: existingSets };
652+
}
653+
if (key === "activeSet") {
654+
return { workspaceValue: null };
655+
}
656+
return {};
657+
});
658+
jest
659+
.spyOn(vscode.workspace, "getConfiguration")
660+
.mockReturnValue(mockConfig as unknown as vscode.WorkspaceConfiguration);
661+
662+
const configManager = createMockConfigManager();
663+
const configReader = createMockConfigReader();
664+
const buttonSetWriter = createMockButtonSetWriter();
665+
666+
const manager = ButtonSetManager.create(configManager, configReader, buttonSetWriter);
667+
const result = await manager.renameButtonSet("NonExistent", "NewName");
668+
669+
expect(result.success).toBe(false);
670+
if (!result.success) {
671+
expect(result.error).toBe("setNotFound");
672+
}
673+
});
674+
675+
it("should return setNameRequired error when new name is empty", async () => {
676+
const existingSets = [{ buttons: [], id: "set-1", name: "SetA" }];
677+
const mockConfig = createMockConfig();
678+
mockConfig.get.mockReturnValue(CONFIGURATION_TARGETS.WORKSPACE);
679+
mockConfig.inspect.mockImplementation((key: string) => {
680+
if (key === "buttonSets") {
681+
return { workspaceValue: existingSets };
682+
}
683+
if (key === "activeSet") {
684+
return { workspaceValue: null };
685+
}
686+
return {};
687+
});
688+
jest
689+
.spyOn(vscode.workspace, "getConfiguration")
690+
.mockReturnValue(mockConfig as unknown as vscode.WorkspaceConfiguration);
691+
692+
const configManager = createMockConfigManager();
693+
const configReader = createMockConfigReader();
694+
const buttonSetWriter = createMockButtonSetWriter();
695+
696+
const manager = ButtonSetManager.create(configManager, configReader, buttonSetWriter);
697+
const result = await manager.renameButtonSet("SetA", " ");
698+
699+
expect(result.success).toBe(false);
700+
if (!result.success) {
701+
expect(result.error).toBe("setNameRequired");
702+
}
703+
});
704+
705+
it("should update active set when renaming currently active set", async () => {
706+
const existingSets = [{ buttons: [], id: "set-1", name: "SetA" }];
707+
const mockConfig = createMockConfig();
708+
mockConfig.get.mockReturnValue(CONFIGURATION_TARGETS.WORKSPACE);
709+
mockConfig.inspect.mockImplementation((key: string) => {
710+
if (key === "buttonSets") {
711+
return { workspaceValue: existingSets };
712+
}
713+
if (key === "activeSet") {
714+
return { workspaceValue: "SetA" };
715+
}
716+
return {};
717+
});
718+
jest
719+
.spyOn(vscode.workspace, "getConfiguration")
720+
.mockReturnValue(mockConfig as unknown as vscode.WorkspaceConfiguration);
721+
722+
const configManager = createMockConfigManager();
723+
const configReader = createMockConfigReader();
724+
const buttonSetWriter = createMockButtonSetWriter();
725+
726+
const manager = ButtonSetManager.create(configManager, configReader, buttonSetWriter);
727+
const result = await manager.renameButtonSet("SetA", "SetB");
728+
729+
expect(result.success).toBe(true);
730+
expect(buttonSetWriter.writeActiveSet).toHaveBeenCalledWith("SetB", expect.anything());
731+
});
732+
733+
it("should NOT update active set when renaming non-active set", async () => {
734+
const existingSets = [
735+
{ buttons: [], id: "set-1", name: "SetA" },
736+
{ buttons: [], id: "set-2", name: "SetB" },
737+
];
738+
const mockConfig = createMockConfig();
739+
mockConfig.get.mockReturnValue(CONFIGURATION_TARGETS.WORKSPACE);
740+
mockConfig.inspect.mockImplementation((key: string) => {
741+
if (key === "buttonSets") {
742+
return { workspaceValue: existingSets };
743+
}
744+
if (key === "activeSet") {
745+
return { workspaceValue: "SetA" };
746+
}
747+
return {};
748+
});
749+
jest
750+
.spyOn(vscode.workspace, "getConfiguration")
751+
.mockReturnValue(mockConfig as unknown as vscode.WorkspaceConfiguration);
752+
753+
const configManager = createMockConfigManager();
754+
const configReader = createMockConfigReader();
755+
const buttonSetWriter = createMockButtonSetWriter();
756+
757+
const manager = ButtonSetManager.create(configManager, configReader, buttonSetWriter);
758+
const result = await manager.renameButtonSet("SetB", "SetC");
759+
760+
expect(result.success).toBe(true);
761+
expect(buttonSetWriter.writeActiveSet).not.toHaveBeenCalled();
762+
});
763+
645764
it("should detect duplicate when renaming to existing name with different case", async () => {
646765
const existingSets = [
647766
{ buttons: [], id: "set-1", name: "SetA" },
@@ -669,7 +788,7 @@ describe("ButtonSetManager", () => {
669788
const buttonSetWriter = createMockButtonSetWriter();
670789

671790
const manager = ButtonSetManager.create(configManager, configReader, buttonSetWriter);
672-
const result = await manager.updateButtonSet("set-1", { name: "SETB" });
791+
const result = await manager.renameButtonSet("SetA", "SETB");
673792

674793
expect(result.success).toBe(false);
675794
if (!result.success) {
@@ -704,7 +823,7 @@ describe("ButtonSetManager", () => {
704823
const buttonSetWriter = createMockButtonSetWriter();
705824

706825
const manager = ButtonSetManager.create(configManager, configReader, buttonSetWriter);
707-
const result = await manager.updateButtonSet("set-1", { name: "SETA" });
826+
const result = await manager.renameButtonSet("SetA", "SETA");
708827

709828
expect(result.success).toBe(true);
710829
expect(buttonSetWriter.writeButtonSets).toHaveBeenCalled();

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

Lines changed: 35 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,41 @@ export class ButtonSetManager {
174174
return matchingSet?.buttons ?? null;
175175
}
176176

177+
async renameButtonSet(currentName: string, newName: string): Promise<ButtonSetOperationResult> {
178+
const sets = this.getButtonSets();
179+
const setIndex = sets.findIndex((set) => set.name.toLowerCase() === currentName.toLowerCase());
180+
181+
if (setIndex === -1) {
182+
return { error: "setNotFound", success: false };
183+
}
184+
185+
const validation = this.validateSetName(newName, currentName);
186+
if (validation.error) {
187+
return { error: validation.error, success: false };
188+
}
189+
190+
const existingSet = sets[setIndex];
191+
const updatedSet: ButtonSet = {
192+
...existingSet,
193+
name: validation.trimmedName,
194+
};
195+
196+
const updatedSets = [...sets];
197+
updatedSets[setIndex] = updatedSet;
198+
199+
const currentTarget = this.configManager.getCurrentConfigurationTarget();
200+
const activeSet = this.getActiveSet();
201+
const needsActiveSetUpdate = activeSet?.toLowerCase() === currentName.toLowerCase();
202+
203+
await this.writeButtonSets(updatedSets, currentTarget);
204+
205+
if (needsActiveSetUpdate) {
206+
await this.setActiveSet(validation.trimmedName);
207+
}
208+
209+
return { success: true };
210+
}
211+
177212
async saveAsButtonSet(name: string): Promise<ButtonSetOperationResult> {
178213
const validation = this.validateSetName(name);
179214
if (validation.error) {
@@ -242,50 +277,6 @@ export class ButtonSetManager {
242277
return true;
243278
}
244279

245-
async updateButtonSet(
246-
id: string,
247-
updates: { buttons?: ButtonConfigWithOptionalId[]; name?: string }
248-
): Promise<ButtonSetOperationResult> {
249-
const sets = this.getButtonSets();
250-
const setIndex = sets.findIndex((set) => set.id === id);
251-
252-
if (setIndex === -1) {
253-
return { error: "setNotFound", success: false };
254-
}
255-
256-
const existingSet = sets[setIndex];
257-
258-
let validatedName: string | undefined;
259-
if (updates.name !== undefined) {
260-
const validation = this.validateSetName(updates.name, existingSet.name);
261-
if (validation.error) {
262-
return { error: validation.error, success: false };
263-
}
264-
validatedName = validation.trimmedName;
265-
}
266-
267-
const updatedSet: ButtonSet = {
268-
...existingSet,
269-
...(validatedName !== undefined && { name: validatedName }),
270-
...(updates.buttons !== undefined && { buttons: ensureIdsInArray(updates.buttons) }),
271-
};
272-
273-
const updatedSets = [...sets];
274-
updatedSets[setIndex] = updatedSet;
275-
276-
const currentTarget = this.configManager.getCurrentConfigurationTarget();
277-
const activeSet = this.getActiveSet();
278-
const needsActiveSetUpdate = activeSet === existingSet.name && validatedName !== undefined;
279-
280-
await this.writeButtonSets(updatedSets, currentTarget);
281-
282-
if (needsActiveSetUpdate) {
283-
await this.setActiveSet(validatedName!);
284-
}
285-
286-
return { success: true };
287-
}
288-
289280
validateUniqueName(name: string): boolean {
290281
const sets = this.getButtonSets();
291282
return validateUniqueSetName(name, sets);

src/internal/providers/webview-provider.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -377,22 +377,22 @@ export const handleWebviewMessage = async (
377377
});
378378
break;
379379
}
380-
case MESSAGE_TYPE.UPDATE_BUTTON_SET: {
380+
case MESSAGE_TYPE.RENAME_BUTTON_SET: {
381381
if (!buttonSetManager) {
382382
throw new Error(MESSAGES.ERROR.buttonSetManagerNotAvailable);
383383
}
384-
const updateData = message.data as
385-
| { buttons?: ButtonConfigWithOptionalId[]; id?: string; name?: string }
386-
| undefined;
387-
if (!updateData?.id) {
388-
throw new Error("setIdRequired");
389-
}
390-
const updateResult = await buttonSetManager.updateButtonSet(updateData.id, {
391-
buttons: updateData.buttons,
392-
name: updateData.name,
384+
const renameDataSchema = z.object({
385+
currentName: z.string().min(1),
386+
newName: z.string().min(1),
393387
});
394-
if (!updateResult.success) {
395-
throw new Error(updateResult.error || "Failed to update button set");
388+
const parseResult = renameDataSchema.safeParse(message.data);
389+
if (!parseResult.success) {
390+
throw new Error(MESSAGES.ERROR.renameRequiresBothNames);
391+
}
392+
const { currentName, newName } = parseResult.data;
393+
const renameResult = await buttonSetManager.renameButtonSet(currentName, newName);
394+
if (!renameResult.success) {
395+
throw new Error(renameResult.error || MESSAGES.ERROR.renameButtonSetFailed);
396396
}
397397
await vscode.commands.executeCommand(COMMANDS.REFRESH);
398398
webview.postMessage({

src/shared/constants.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ export const MESSAGE_TYPE = {
2727
IMPORT_CONFIGURATION: "importConfiguration",
2828
IMPORT_PREVIEW_RESULT: "importPreviewResult",
2929
PREVIEW_IMPORT: "previewImport",
30+
RENAME_BUTTON_SET: "renameButtonSet",
3031
SAVE_AS_BUTTON_SET: "saveAsButtonSet",
3132
SET_ACTIVE_SET: "setActiveSet",
3233
SET_CONFIG: "setConfig",
3334
SET_CONFIGURATION_TARGET: "setConfigurationTarget",
3435
THEME_CHANGED: "themeChanged",
35-
UPDATE_BUTTON_SET: "updateButtonSet",
3636
} as const;
3737

3838
export const MESSAGES = {
@@ -57,6 +57,8 @@ export const MESSAGES = {
5757
"Invalid import confirmation data. Please restart the import process from the preview dialog.",
5858
invalidSetConfigData: "Invalid data for setConfig: data is not an array.",
5959
noCommands: "No commands found",
60+
renameButtonSetFailed: "Failed to rename button set",
61+
renameRequiresBothNames: "Both current name and new name are required for rename",
6062
unknownError: "Unknown error occurred",
6163
},
6264
INFO: {

src/shared/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,11 @@ export type WebviewMessageType =
6464
| "getConfig"
6565
| "importConfiguration"
6666
| "previewImport"
67+
| "renameButtonSet"
6768
| "saveAsButtonSet"
6869
| "setActiveSet"
6970
| "setConfig"
70-
| "setConfigurationTarget"
71-
| "updateButtonSet";
71+
| "setConfigurationTarget";
7272

7373
export type ExtensionMessageType =
7474
| "configData"
@@ -95,7 +95,7 @@ export type WebviewMessage = {
9595
| { setName?: string | null }
9696
| { strategy?: string; target?: string }
9797
| { buttons?: ButtonConfigWithOptionalId[]; name: string; sourceSetId?: string }
98-
| { buttons: ButtonConfigWithOptionalId[]; id: string; name?: string };
98+
| { currentName: string; newName: string };
9999
requestId?: string;
100100
target?: string;
101101
type: WebviewMessageType;

src/view/src/components/button-set-selector.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Check, Layers, Plus, Trash2 } from "lucide-react";
1+
import { Check, Layers, Pencil, Plus, Trash2 } from "lucide-react";
22
import { useState } from "react";
33
import { useTranslation } from "react-i18next";
44

@@ -16,6 +16,8 @@ import {
1616

1717
import { CreateSetDialog } from "./create-set-dialog";
1818
import { DeleteSetDialog } from "./delete-set-dialog";
19+
import { RenameSetDialog } from "./rename-set-dialog";
20+
import type { ButtonSet } from "../../../shared/types";
1921
import { useVscodeCommand } from "../context/vscode-command-context";
2022

2123
export const ButtonSetSelector = () => {
@@ -24,6 +26,7 @@ export const ButtonSetSelector = () => {
2426
const [isOpen, setIsOpen] = useState(false);
2527
const [showCreateDialog, setShowCreateDialog] = useState(false);
2628
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
29+
const [renameTarget, setRenameTarget] = useState<ButtonSet | null>(null);
2730

2831
const displayName = activeSet ?? t("buttonSets.default");
2932

@@ -32,6 +35,12 @@ export const ButtonSetSelector = () => {
3235
setIsOpen(false);
3336
};
3437

38+
const handleRenameClick = (e: React.MouseEvent, set: ButtonSet) => {
39+
e.stopPropagation();
40+
setIsOpen(false);
41+
setRenameTarget(set);
42+
};
43+
3544
return (
3645
<>
3746
<DropdownMenu onOpenChange={setIsOpen} open={isOpen}>
@@ -73,6 +82,15 @@ export const ButtonSetSelector = () => {
7382
/>
7483
<span className="flex-1 truncate">{set.name}</span>
7584
<span className="ml-2 text-xs text-muted-foreground">{set.buttons.length}</span>
85+
<button
86+
aria-label={t("buttonSets.renameSet")}
87+
className="ml-2 p-1 rounded hover:bg-accent"
88+
data-testid={`rename-set-${set.id}`}
89+
onClick={(e) => handleRenameClick(e, set)}
90+
type="button"
91+
>
92+
<Pencil aria-hidden="true" className="h-3 w-3" />
93+
</button>
7694
</DropdownMenuItem>
7795
))}
7896

@@ -104,6 +122,11 @@ export const ButtonSetSelector = () => {
104122

105123
<CreateSetDialog onOpenChange={setShowCreateDialog} open={showCreateDialog} />
106124
<DeleteSetDialog onOpenChange={setShowDeleteDialog} open={showDeleteDialog} />
125+
<RenameSetDialog
126+
onOpenChange={(open) => !open && setRenameTarget(null)}
127+
open={renameTarget !== null}
128+
targetSet={renameTarget}
129+
/>
107130
</>
108131
);
109132
};

0 commit comments

Comments
 (0)