Skip to content
This repository was archived by the owner on Aug 17, 2025. It is now read-only.

Commit b0e3c3c

Browse files
committed
- Extended commands by adding arguments
- Added/Updated tests for ImporterRegistry, ThemeProvider, CommandRegistry and Button - Fixed snippet and node selectors to use createSelector, increasing performance by decreasing re-renders - Organized few imports
1 parent 2ed38f5 commit b0e3c3c

File tree

13 files changed

+269
-25
lines changed

13 files changed

+269
-25
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { vitest } from "vitest";
2+
import { CommandRegistry } from "./CommandRegistry";
3+
4+
describe("Command Registry", () => {
5+
let commandRegistry: CommandRegistry;
6+
beforeEach(() => {
7+
commandRegistry = new CommandRegistry();
8+
});
9+
it("adds commands properly", () => {
10+
commandRegistry.addCommand({
11+
id: "Test Command",
12+
name: "",
13+
description: "",
14+
action: () => {},
15+
});
16+
17+
expect(commandRegistry.hasCommand("Test Command"));
18+
});
19+
20+
it("removes command properly", () => {
21+
commandRegistry.addCommand({
22+
id: "Test Command",
23+
name: "",
24+
description: "",
25+
action: () => {},
26+
});
27+
28+
expect(commandRegistry.hasCommand("Test Command"));
29+
commandRegistry.removeCommand("Test Command");
30+
expect(!commandRegistry.hasCommand("Test Command"));
31+
});
32+
33+
it("executes command properly", () => {
34+
const vn = vitest.fn();
35+
commandRegistry.addCommand({
36+
id: "Test Command",
37+
name: "",
38+
description: "",
39+
action: vn,
40+
});
41+
42+
commandRegistry.executeCommand("Test Command", []);
43+
expect(vn).toHaveBeenCalled();
44+
});
45+
});

src/features/command-palette/CommandRegistry.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,18 @@ export class CommandRegistry implements ICommandRegistry {
3535
this.listeners.get(event)?.delete(listener);
3636
}
3737

38-
async executeCommand(commandId: string): Promise<void> {
38+
async executeCommand(commandId: string, args: string[]): Promise<void> {
3939
const command = this.commands.get(commandId);
4040
if (!command) {
4141
throw new Error(`Command with id ${commandId} does not exist.`);
4242
}
43-
await command.action({
44-
dispatch: store.dispatch,
45-
getState: store.getState,
46-
});
43+
await command.action(
44+
{
45+
dispatch: store.dispatch,
46+
getState: store.getState,
47+
},
48+
args
49+
);
4750
}
4851

4952
addCommand(command: Command): void {
@@ -62,6 +65,10 @@ export class CommandRegistry implements ICommandRegistry {
6265
this.emit("commandRemoved");
6366
}
6467

68+
hasCommand(commandId: string): boolean {
69+
return this.commands.has(commandId);
70+
}
71+
6572
getCommands(): Command[] {
6673
return Array.from(this.commands.values());
6774
}

src/features/command-palette/components/CommandList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export const CommandList = () => {
55
const commands = useCommands();
66

77
function executeCommand(id: string) {
8-
commandRegistry.executeCommand(id);
8+
commandRegistry.executeCommand(id, []);
99
}
1010

1111
return (

src/features/command-palette/types.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,19 @@ export interface Command {
66
description: string;
77
icon?: string;
88
shortcut?: string;
9-
action: (ctx: {
10-
dispatch: AppDispatch;
11-
getState: () => RootState;
12-
}) => void | Promise<void>;
9+
action: (
10+
ctx: {
11+
dispatch: AppDispatch;
12+
getState: () => RootState;
13+
},
14+
args: string[]
15+
) => void | Promise<void>;
1316
}
1417

1518
export interface ICommandRegistry {
1619
addCommand(command: Command): void;
1720
removeCommand(commandId: string): void;
21+
hasCommand(commandId: string): boolean;
1822
getCommands(): Command[];
19-
executeCommand(commandId: string): void;
23+
executeCommand(commandId: string, args: string[]): void;
2024
}

src/features/graph/nodes/components/GraphNode.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import "./node.styles.css";
1+
import { useAppSelector } from "@/store";
22
import { useRef } from "react";
33
import { useDispatch, useSelector } from "react-redux";
4-
import InteractiveRect from "./InteractiveRect";
4+
import { GraphNodePosition, GraphNodeSize } from "../../store";
55
import {
66
clearAllNodeSelections,
77
toggleNodeSelectionStatus,
88
updateNode,
99
} from "../../store/graphSlice";
1010
import { isNodeSelected, selectNodeById } from "../../store/selectors";
11-
import { GraphNodePosition, GraphNodeSize } from "../../store";
12-
import { useAppSelector } from "@/store";
11+
import InteractiveRect from "./InteractiveRect";
12+
import "./node.styles.css";
1313

1414
type GraphNodeProps = {
1515
nodeId: string;

src/features/graph/nodes/components/InteractiveRect.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import "./node.styles.css";
21
import React, {
32
CSSProperties,
43
MouseEventHandler,
54
useCallback,
65
useMemo,
76
} from "react";
8-
import ResizeHandle from "./ResizeHandle";
97
import { useDrag, useResize } from "../../graphview/hooks";
108
import { DragDelta } from "../../graphview/hooks/useDrag";
119
import { GraphNodePosition, GraphNodeSize } from "../../store";
10+
import "./node.styles.css";
11+
import ResizeHandle from "./ResizeHandle";
1212

1313
type InteractiveRectProps = {
1414
zoom: number;

src/features/graph/store/selectors.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,6 @@ export const selectNodesByLayer = (layerId: string) =>
2727
createSelector(graphSelectors.selectAll, (nodes) =>
2828
nodes.filter((node) => node.layerId === layerId)
2929
);
30-
export const selectAllNodesOnCurrentLayer = (state: RootState) =>
31-
graphSelectors
32-
.selectAll(state.graph)
33-
.filter((node) => node.layerId === state.layers.selectedLayerId);
3430

3531
export const selectAllLayers = (state: RootState) =>
3632
layerSelectors.selectAll(state.layers);
@@ -49,3 +45,9 @@ export const selectNodesInCurrentLayer = createSelector(
4945
(nodes, selectedLayerId) =>
5046
nodes.filter((node) => node.layerId === selectedLayerId)
5147
);
48+
49+
export const selectAllNodesOnCurrentLayer = createSelector(
50+
[selectAllNodes, selectCurrentLayer],
51+
(nodes, currentLayer) =>
52+
nodes.filter((node) => node.layerId === currentLayer?.id)
53+
);
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { describe, it, expect, beforeEach, vi } from "vitest";
2+
import { importerRegistry } from "./ImporterRegistry";
3+
import { ImportEvent, IImporter } from "./types";
4+
import { RootState } from "@/store";
5+
6+
const mockState = {} as unknown as RootState;
7+
8+
describe("ImporterRegistry", () => {
9+
let mockFile: unknown & File;
10+
let arrayBufferMock: ArrayBuffer;
11+
let importEvent: ImportEvent;
12+
13+
let mockImporter: IImporter;
14+
let fallbackImporter: IImporter;
15+
16+
beforeEach(() => {
17+
arrayBufferMock = new ArrayBuffer(8);
18+
19+
mockFile = {
20+
name: "test.txt",
21+
arrayBuffer: vi.fn().mockResolvedValue(arrayBufferMock),
22+
} as unknown as File;
23+
24+
vi.spyOn(mockFile, "arrayBuffer").mockResolvedValue(arrayBufferMock);
25+
26+
importEvent = {
27+
file: mockFile,
28+
path: "/mock/path",
29+
position: { x: 10, y: 20 },
30+
dispatch: vi.fn(),
31+
getState: vi.fn(() => mockState),
32+
};
33+
34+
importerRegistry.clearImporters();
35+
36+
mockImporter = {
37+
canHandle: vi.fn().mockResolvedValue(true),
38+
importData: vi.fn().mockResolvedValue({
39+
success: true,
40+
message: "Handled by mockImporter",
41+
}),
42+
};
43+
44+
fallbackImporter = {
45+
canHandle: vi.fn().mockResolvedValue(false),
46+
importData: vi.fn(),
47+
};
48+
});
49+
50+
it("registers and sorts importers by priority", () => {
51+
importerRegistry.registerImporter(mockImporter, 1);
52+
importerRegistry.registerImporter(fallbackImporter, 10);
53+
54+
const importers = importerRegistry.getImporters();
55+
expect(importers.length).toBe(2);
56+
expect(importers[0].importer).toBe(fallbackImporter); // highest priority first
57+
});
58+
59+
it("unregisters an importer", () => {
60+
importerRegistry.registerImporter(mockImporter, 1);
61+
importerRegistry.unregisterImporter(mockImporter);
62+
63+
const importers = importerRegistry.getImporters();
64+
expect(importers.length).toBe(0);
65+
});
66+
67+
it("uses the first importer that can handle the file", async () => {
68+
importerRegistry.registerImporter(fallbackImporter, 1);
69+
importerRegistry.registerImporter(mockImporter, 5);
70+
71+
const result = await importerRegistry.import(importEvent);
72+
73+
expect(mockFile.arrayBuffer).toHaveBeenCalledOnce();
74+
expect(mockImporter.canHandle).toHaveBeenCalledWith(
75+
mockFile,
76+
arrayBufferMock
77+
);
78+
expect(mockImporter.importData).toHaveBeenCalledWith(importEvent);
79+
expect(result).toEqual({
80+
success: true,
81+
message: "Handled by mockImporter",
82+
});
83+
});
84+
85+
it("returns failure when no importer can handle the file", async () => {
86+
importerRegistry.registerImporter(fallbackImporter, 1);
87+
88+
const result = await importerRegistry.import(importEvent);
89+
90+
expect(result.success).toBe(false);
91+
expect(result.message).toBe("No suitable importer found.");
92+
expect(fallbackImporter.importData).not.toHaveBeenCalled();
93+
});
94+
});

src/features/importing/ImporterRegistry.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ class ImporterRegistry {
2121
);
2222
}
2323

24+
clearImporters() {
25+
this.importers = [];
26+
}
27+
28+
getImporters(): ImporterEntry[] {
29+
return this.importers;
30+
}
31+
2432
async import(event: ImportEvent): Promise<ImportResult> {
2533
const fileBuffer = await event.file.arrayBuffer();
2634

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { RootState } from "@/store";
2+
import { createSelector } from "reselect";
23

3-
export function selectEnabledSnippets(state: RootState) {
4-
return state.cssSnippets.snippets.filter((snippet) => snippet.enabled);
5-
}
4+
export const selectEnabledSnippets = createSelector(
5+
(state: RootState) => state.cssSnippets.snippets,
6+
(snippets) => snippets.filter((snippet) => snippet.enabled)
7+
);

0 commit comments

Comments
 (0)