Skip to content

Commit 0d2beca

Browse files
committed
Extract dashboard mutations/handlers/menu into dashboard-actions (§6.8)
createDashboardActions(st) owns the six mutations and every handler; installDashboardMenu moves the native-menu table out of the component. Dashboard.svelte is now 165 lines: title/theme/shortcut effects, the card-actions provider, and layout.
1 parent bfb8683 commit 0d2beca

2 files changed

Lines changed: 131 additions & 91 deletions

File tree

Lines changed: 19 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<script lang="ts">
22
/** Project dashboard orchestrator. */
3-
import { push } from "svelte-spa-router";
43
import { Command as CommandIcon, Plus, Upload } from "@lucide/svelte";
54
import Button from "$lib/ui/Button.svelte";
65
import TitleBar from "$lib/components/TitleBar.svelte";
@@ -10,44 +9,27 @@
109
import DashboardStates from "@/features/dashboard/DashboardStates.svelte";
1110
import ProjectGrid from "@/features/dashboard/ProjectGrid.svelte";
1211
import ConfirmDialog from "$lib/ui/ConfirmDialog.svelte";
13-
import {
14-
createProjectMutation,
15-
duplicateProjectMutation,
16-
deleteProjectMutation,
17-
exportProjectMutation,
18-
importProjectMutation,
19-
renameProjectMutation,
20-
} from "$lib/queries";
2112
import { createRenameState } from "$lib/lib/rename-state.svelte";
2213
import { isModKey, isTypingTarget } from "$lib/lib/keyboard";
2314
import { modKeyLabel } from "$lib/lib/platform";
2415
import { setWindowTitle } from "$lib/lib/window-title";
25-
import {
26-
subscribeToAppMenu,
27-
type AppMenuHandlers,
28-
} from "$lib/lib/app-menu.svelte";
2916
import {
3017
ui,
3118
applyUiTheme,
3219
setIsCommandOpen,
3320
setIsShortcutsOpen,
34-
toggleTheme,
3521
} from "$lib/stores/ui-state.svelte";
36-
import { api } from "$lib/lib/tauri-api";
3722
import { createDashboardState } from "./dashboard-state.svelte";
23+
import {
24+
createDashboardActions,
25+
installDashboardMenu,
26+
} from "./dashboard-actions";
3827
import { provideProjectCardActions } from "./project-card-actions.svelte";
39-
import { notify } from "$lib/lib/toast";
4028
4129
const st = createDashboardState();
4230
const listQuery = st.query;
4331
const projects = $derived(st.projects);
44-
const createMutation = createProjectMutation();
45-
const duplicateMutation = duplicateProjectMutation();
46-
const deleteMutation = deleteProjectMutation();
47-
const exportMutation = exportProjectMutation();
48-
const importMutation = importProjectMutation();
49-
const renameMutation = renameProjectMutation();
50-
32+
const actions = createDashboardActions(st);
5133
5234
// Native + document window title for the dashboard route.
5335
$effect(() => {
@@ -69,20 +51,8 @@
6951
return () => window.removeEventListener("keydown", onKey);
7052
});
7153
72-
const handleOpen = (id: string) => void push(`/editor/${id}`);
73-
const handleDuplicate = (id: string) => duplicateMutation.mutate(id);
74-
const handleExport = (id: string) => exportMutation.mutate(id);
75-
const handleDelete = (id: string, name: string) => (st.deleteTarget = { id, name });
76-
77-
function handleConfirmDelete() {
78-
if (st.deleteTarget) {
79-
deleteMutation.mutate(st.deleteTarget.id);
80-
st.deleteTarget = null;
81-
}
82-
}
83-
8454
const rename = createRenameState(async (id: string, name: string) => {
85-
await renameMutation.mutateAsync({
55+
await actions.renameMutation.mutateAsync({
8656
projectId: id,
8757
name: name || "Untitled Presentation",
8858
});
@@ -98,64 +68,22 @@
9868
return rename.value;
9969
},
10070
get duplicateBusy() {
101-
return duplicateMutation.isPending;
71+
return actions.duplicateMutation.isPending;
10272
},
10373
get commitBusy() {
104-
return renameMutation.isPending;
74+
return actions.renameMutation.isPending;
10575
},
10676
setRenameValue: (v: string) => (rename.value = v),
10777
commitRename: rename.commit,
10878
cancelRename: rename.cancel,
10979
startRename: rename.start,
110-
open: handleOpen,
111-
duplicate: handleDuplicate,
112-
exportProject: handleExport,
113-
remove: handleDelete,
80+
open: actions.open,
81+
duplicate: actions.duplicate,
82+
exportProject: actions.exportProject,
83+
remove: actions.requestDelete,
11484
});
11585
116-
async function handleCreate() {
117-
try {
118-
const project = await createMutation.mutateAsync(
119-
st.newName.trim() || "Untitled Presentation",
120-
);
121-
if (st.selectedTheme && st.selectedTheme !== "dark-plus" && st.selectedTheme !== project.theme) {
122-
try {
123-
await api.updateProjectTheme(project.id, st.selectedTheme);
124-
} catch {
125-
// The deck itself exists and is ready to edit even if its optional
126-
// theme update fails, so do not strand the user on the dashboard.
127-
notify.error("Presentation created, but the selected theme could not be applied");
128-
}
129-
}
130-
st.resetForm();
131-
void push(`/editor/${project.id}`);
132-
} catch {
133-
// The mutation presents the creation error.
134-
}
135-
}
136-
137-
async function handleImport() {
138-
try {
139-
const project = await importMutation.mutateAsync();
140-
void push(`/editor/${project.id}`);
141-
} catch {
142-
/* the mutation owns the error toast */
143-
}
144-
}
145-
146-
const menuHandlers: AppMenuHandlers = {
147-
"menu://new-project": () => (st.creating = true),
148-
"menu://open-dashboard": () => void push("/"),
149-
"menu://command-palette": () => setIsCommandOpen(true),
150-
"menu://toggle-theme": () => toggleTheme(),
151-
"menu://shortcuts-app": () => setIsShortcutsOpen(true),
152-
"menu://shortcuts-help": () => setIsShortcutsOpen(true),
153-
"menu://export": () => {
154-
const first = projects[0];
155-
if (first) exportMutation.mutate(first.id);
156-
},
157-
};
158-
subscribeToAppMenu(() => menuHandlers);
86+
installDashboardMenu(st, actions);
15987
16088
const mod = modKeyLabel();
16189
</script>
@@ -182,8 +110,8 @@
182110
variant="outline"
183111
size="sm"
184112
class="gap-1.5"
185-
onclick={() => void handleImport()}
186-
disabled={importMutation.isPending}
113+
onclick={() => void actions.importProject()}
114+
disabled={actions.importMutation.isPending}
187115
>
188116
<Upload class="h-4 w-4" />Import
189117
</Button>
@@ -202,8 +130,8 @@
202130
onNameChange={(v) => (st.newName = v)}
203131
selectedTheme={st.selectedTheme}
204132
onThemeChange={(t) => (st.selectedTheme = t)}
205-
onCreate={() => void handleCreate()}
206-
isPending={createMutation.isPending}
133+
onCreate={() => void actions.create()}
134+
isPending={actions.createMutation.isPending}
207135
isStandalone={true}
208136
/>
209137
</div>
@@ -215,7 +143,7 @@
215143
error={(listQuery.error as Error | null) ?? null}
216144
projectCount={projects.length}
217145
onCreate={() => (st.creating = true)}
218-
onImport={() => void handleImport()}
146+
onImport={() => void actions.importProject()}
219147
showEmptyState={!st.creating}
220148
>
221149
{#if !listQuery.isLoading && !listQuery.isError && projects.length > 0}
@@ -231,7 +159,7 @@
231159
description="This cannot be undone."
232160
confirmLabel="Delete"
233161
destructive
234-
onConfirm={handleConfirmDelete}
162+
onConfirm={actions.confirmDelete}
235163
onCancel={() => (st.deleteTarget = null)}
236164
/>
237165
</div>
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/**
2+
* Dashboard mutations, handlers and native-menu wiring (§6.8). The
3+
* component keeps layout + effects; every mutation and cross-feature
4+
* handler is created here so they stay in one testable place.
5+
*/
6+
import { push } from "svelte-spa-router";
7+
import {
8+
createProjectMutation,
9+
duplicateProjectMutation,
10+
deleteProjectMutation,
11+
exportProjectMutation,
12+
importProjectMutation,
13+
renameProjectMutation,
14+
} from "$lib/queries";
15+
import { api } from "$lib/lib/tauri-api";
16+
import { notify } from "$lib/lib/toast";
17+
import {
18+
setIsCommandOpen,
19+
setIsShortcutsOpen,
20+
toggleTheme,
21+
} from "$lib/stores/ui-state.svelte";
22+
import {
23+
subscribeToAppMenu,
24+
type AppMenuHandlers,
25+
} from "$lib/lib/app-menu.svelte";
26+
import type { createDashboardState } from "./dashboard-state.svelte";
27+
28+
export function createDashboardActions(st: ReturnType<typeof createDashboardState>) {
29+
const createMutation = createProjectMutation();
30+
const duplicateMutation = duplicateProjectMutation();
31+
const deleteMutation = deleteProjectMutation();
32+
const exportMutation = exportProjectMutation();
33+
const importMutation = importProjectMutation();
34+
const renameMutation = renameProjectMutation();
35+
36+
const open = (id: string) => void push(`/editor/${id}`);
37+
const duplicate = (id: string) => duplicateMutation.mutate(id);
38+
const exportProject = (id: string) => exportMutation.mutate(id);
39+
const requestDelete = (id: string, name: string) => (st.deleteTarget = { id, name });
40+
41+
function confirmDelete() {
42+
if (st.deleteTarget) {
43+
deleteMutation.mutate(st.deleteTarget.id);
44+
st.deleteTarget = null;
45+
}
46+
}
47+
48+
async function create() {
49+
try {
50+
const project = await createMutation.mutateAsync(
51+
st.newName.trim() || "Untitled Presentation",
52+
);
53+
if (st.selectedTheme && st.selectedTheme !== "dark-plus" && st.selectedTheme !== project.theme) {
54+
try {
55+
await api.updateProjectTheme(project.id, st.selectedTheme);
56+
} catch {
57+
// The deck itself exists and is ready to edit even if its optional
58+
// theme update fails, so do not strand the user on the dashboard.
59+
notify.error("Presentation created, but the selected theme could not be applied");
60+
}
61+
}
62+
st.resetForm();
63+
void push(`/editor/${project.id}`);
64+
} catch {
65+
// The mutation presents the creation error.
66+
}
67+
}
68+
69+
async function importProject() {
70+
try {
71+
const project = await importMutation.mutateAsync();
72+
void push(`/editor/${project.id}`);
73+
} catch {
74+
/* the mutation owns the error toast */
75+
}
76+
}
77+
78+
return {
79+
createMutation,
80+
duplicateMutation,
81+
exportMutation,
82+
importMutation,
83+
renameMutation,
84+
open,
85+
duplicate,
86+
exportProject,
87+
requestDelete,
88+
confirmDelete,
89+
create,
90+
importProject,
91+
};
92+
}
93+
94+
/** Native menu bar wiring for the dashboard route. */
95+
export function installDashboardMenu(
96+
st: ReturnType<typeof createDashboardState>,
97+
actions: ReturnType<typeof createDashboardActions>,
98+
): void {
99+
const handlers: AppMenuHandlers = {
100+
"menu://new-project": () => (st.creating = true),
101+
"menu://open-dashboard": () => void push("/"),
102+
"menu://command-palette": () => setIsCommandOpen(true),
103+
"menu://toggle-theme": () => toggleTheme(),
104+
"menu://shortcuts-app": () => setIsShortcutsOpen(true),
105+
"menu://shortcuts-help": () => setIsShortcutsOpen(true),
106+
"menu://export": () => {
107+
const first = st.projects[0];
108+
if (first) actions.exportMutation.mutate(first.id);
109+
},
110+
};
111+
subscribeToAppMenu(() => handlers);
112+
}

0 commit comments

Comments
 (0)