Skip to content

Commit 52010db

Browse files
committed
folder migration persister etc (#2733)
1 parent ba03897 commit 52010db

File tree

32 files changed

+918
-55
lines changed

32 files changed

+918
-55
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src/components/main/body/sessions/outer-header/shared/folder.tsx

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { FolderIcon } from "lucide-react";
2-
import { type ReactNode, useState } from "react";
2+
import { type ReactNode, useCallback, useState } from "react";
33

4+
import { commands as folderCommands } from "@hypr/plugin-folder";
45
import {
56
Command,
67
CommandEmpty,
@@ -31,13 +32,7 @@ export function SearchableFolderDropdown({
3132
main.STORE_ID,
3233
);
3334

34-
const handleSelectFolder = main.UI.useSetPartialRowCallback(
35-
"sessions",
36-
sessionId,
37-
(folderId: string) => ({ folder_id: folderId }),
38-
[],
39-
main.STORE_ID,
40-
);
35+
const handleSelectFolder = useMoveSessionToFolder(sessionId);
4136

4237
return (
4338
<DropdownMenu open={open} onOpenChange={setOpen}>
@@ -71,13 +66,7 @@ export function SearchableFolderSubmenuContent({
7166
main.STORE_ID,
7267
);
7368

74-
const handleSelectFolder = main.UI.useSetPartialRowCallback(
75-
"sessions",
76-
sessionId,
77-
(folderId: string) => ({ folder_id: folderId }),
78-
[],
79-
main.STORE_ID,
80-
);
69+
const handleSelectFolder = useMoveSessionToFolder(sessionId);
8170

8271
return (
8372
<DropdownMenuSubContent className="w-[200px] p-0">
@@ -102,11 +91,11 @@ function SearchableFolderContent({
10291
setOpen,
10392
}: {
10493
folders: Record<string, any>;
105-
onSelectFolder: (folderId: string) => void;
94+
onSelectFolder: (folderId: string) => Promise<void>;
10695
setOpen?: (open: boolean) => void;
10796
}) {
108-
const handleSelect = (folderId: string) => {
109-
onSelectFolder(folderId);
97+
const handleSelect = async (folderId: string) => {
98+
await onSelectFolder(folderId);
11099
setOpen?.(false);
111100
};
112101

@@ -131,3 +120,21 @@ function SearchableFolderContent({
131120
</Command>
132121
);
133122
}
123+
124+
function useMoveSessionToFolder(sessionId: string) {
125+
const setFolderId = main.UI.useSetPartialRowCallback(
126+
"sessions",
127+
sessionId,
128+
(folderId: string) => ({ folder_id: folderId }),
129+
[],
130+
main.STORE_ID,
131+
);
132+
133+
return useCallback(
134+
async (targetFolderId: string) => {
135+
await folderCommands.moveSession(sessionId, targetFolderId);
136+
setFolderId(targetFolderId);
137+
},
138+
[sessionId, setFolderId],
139+
);
140+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { exists, readDir } from "@tauri-apps/plugin-fs";
2+
import { createCustomPersister } from "tinybase/persisters/with-schemas";
3+
import type {
4+
Content,
5+
MergeableStore,
6+
OptionalSchemas,
7+
} from "tinybase/with-schemas";
8+
9+
import {
10+
commands as notifyCommands,
11+
events as notifyEvents,
12+
} from "@hypr/plugin-notify";
13+
import { commands as path2Commands } from "@hypr/plugin-path2";
14+
import type { FolderStorage } from "@hypr/store";
15+
16+
import { DEFAULT_USER_ID } from "../../../utils";
17+
import { StoreOrMergeableStore } from "../store/shared";
18+
import { getParentFolderPath, type PersisterMode } from "./utils";
19+
20+
type FoldersJson = Record<string, FolderStorage>;
21+
22+
interface ScanResult {
23+
folders: FoldersJson;
24+
sessionFolderMap: Map<string, string>;
25+
}
26+
27+
async function getSessionsDir(): Promise<string> {
28+
const base = await path2Commands.base();
29+
return `${base}/sessions`;
30+
}
31+
32+
async function scanDirectoryRecursively(
33+
sessionsDir: string,
34+
currentPath: string = "",
35+
): Promise<ScanResult> {
36+
const folders: FoldersJson = {};
37+
const sessionFolderMap = new Map<string, string>();
38+
39+
const fullPath = currentPath ? `${sessionsDir}/${currentPath}` : sessionsDir;
40+
41+
try {
42+
const entries = await readDir(fullPath);
43+
44+
for (const entry of entries) {
45+
if (!entry.isDirectory) {
46+
continue;
47+
}
48+
49+
const entryPath = currentPath
50+
? `${currentPath}/${entry.name}`
51+
: entry.name;
52+
53+
const hasMemoMd = await exists(`${sessionsDir}/${entryPath}/_memo.md`);
54+
55+
if (hasMemoMd) {
56+
const folderPath = currentPath === "_default" ? "" : currentPath;
57+
sessionFolderMap.set(entry.name, folderPath);
58+
} else {
59+
if (entry.name !== "_default") {
60+
folders[entryPath] = {
61+
user_id: DEFAULT_USER_ID,
62+
created_at: new Date().toISOString(),
63+
name: entry.name,
64+
parent_folder_id: getParentFolderPath(entryPath),
65+
};
66+
}
67+
68+
const subResult = await scanDirectoryRecursively(
69+
sessionsDir,
70+
entryPath,
71+
);
72+
73+
for (const [id, folder] of Object.entries(subResult.folders)) {
74+
folders[id] = folder;
75+
}
76+
for (const [sessionId, folderPath] of subResult.sessionFolderMap) {
77+
sessionFolderMap.set(sessionId, folderPath);
78+
}
79+
}
80+
}
81+
} catch (error) {
82+
const errorStr = String(error);
83+
if (
84+
!errorStr.includes("No such file or directory") &&
85+
!errorStr.includes("ENOENT") &&
86+
!errorStr.includes("not found")
87+
) {
88+
console.error("[FolderPersister] scan error:", error);
89+
}
90+
}
91+
92+
return { folders, sessionFolderMap };
93+
}
94+
95+
export function jsonToContent<Schemas extends OptionalSchemas>(
96+
data: FoldersJson,
97+
): Content<Schemas> {
98+
return [{ folders: data }, {}] as unknown as Content<Schemas>;
99+
}
100+
101+
export function createFolderPersister<Schemas extends OptionalSchemas>(
102+
store: MergeableStore<Schemas>,
103+
config: { mode: PersisterMode } = { mode: "load-only" },
104+
) {
105+
const loadFn =
106+
config.mode === "save-only"
107+
? async (): Promise<Content<Schemas> | undefined> => undefined
108+
: async (): Promise<Content<Schemas> | undefined> => {
109+
try {
110+
const sessionsDir = await getSessionsDir();
111+
const dirExists = await exists(sessionsDir);
112+
113+
if (!dirExists) {
114+
return jsonToContent<Schemas>({});
115+
}
116+
117+
const { folders, sessionFolderMap } =
118+
await scanDirectoryRecursively(sessionsDir);
119+
120+
for (const [sessionId, folderPath] of sessionFolderMap) {
121+
// @ts-ignore - we're setting cells on the sessions table
122+
if (store.hasRow("sessions", sessionId)) {
123+
// @ts-ignore
124+
store.setCell("sessions", sessionId, "folder_id", folderPath);
125+
}
126+
}
127+
128+
return jsonToContent<Schemas>(folders);
129+
} catch (error) {
130+
console.error("[FolderPersister] load error:", error);
131+
return undefined;
132+
}
133+
};
134+
135+
const saveFn = async () => {};
136+
137+
return createCustomPersister(
138+
store,
139+
loadFn,
140+
saveFn,
141+
(listener) => setInterval(listener, 5000),
142+
(handle) => clearInterval(handle),
143+
(error) => console.error("[FolderPersister]:", error),
144+
StoreOrMergeableStore,
145+
);
146+
}
147+
148+
interface Loadable {
149+
load(): Promise<unknown>;
150+
}
151+
152+
export async function startFolderWatcher(
153+
persister: Loadable,
154+
): Promise<() => void> {
155+
const result = await notifyCommands.start();
156+
if (result.status === "error") {
157+
console.error("[FolderWatcher] Failed to start:", result.error);
158+
return () => {};
159+
}
160+
161+
const unlisten = await notifyEvents.fileChanged.listen(async (event) => {
162+
const path = event.payload.path;
163+
if (path.startsWith("sessions/")) {
164+
try {
165+
await persister.load();
166+
} catch (error) {
167+
console.error("[FolderWatcher] Failed to reload:", error);
168+
}
169+
}
170+
});
171+
172+
return () => {
173+
unlisten();
174+
void notifyCommands.stop();
175+
};
176+
}

apps/desktop/src/store/tinybase/persister/local.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ describe("createNotePersister", () => {
8989
expect(commands.exportTiptapJsonToMdBatch).toHaveBeenCalledWith([
9090
[
9191
{ type: "doc", content: [{ type: "paragraph" }] },
92-
"/mock/data/dir/hyprnote/sessions/session-1/My Template.md",
92+
"/mock/data/dir/hyprnote/sessions/_default/session-1/My Template.md",
9393
],
9494
]);
9595
});
@@ -115,7 +115,7 @@ describe("createNotePersister", () => {
115115
expect(commands.exportTiptapJsonToMdBatch).toHaveBeenCalledWith([
116116
[
117117
{ type: "doc", content: [{ type: "paragraph" }] },
118-
"/mock/data/dir/hyprnote/sessions/session-1/_summary.md",
118+
"/mock/data/dir/hyprnote/sessions/_default/session-1/_summary.md",
119119
],
120120
]);
121121
});
@@ -199,7 +199,7 @@ describe("createNotePersister", () => {
199199
expect(commands.exportTiptapJsonToMdBatch).toHaveBeenCalledWith([
200200
[
201201
expect.any(Object),
202-
"/mock/data/dir/hyprnote/sessions/session-1/My_________Template.md",
202+
"/mock/data/dir/hyprnote/sessions/_default/session-1/My_________Template.md",
203203
],
204204
]);
205205
});
@@ -226,7 +226,7 @@ describe("createNotePersister", () => {
226226
expect(commands.exportTiptapJsonToMdBatch).toHaveBeenCalledWith([
227227
[
228228
expect.any(Object),
229-
"/mock/data/dir/hyprnote/sessions/session-1/unknown-template-id.md",
229+
"/mock/data/dir/hyprnote/sessions/_default/session-1/unknown-template-id.md",
230230
],
231231
]);
232232
});
@@ -292,7 +292,7 @@ describe("createNotePersister", () => {
292292
expect(commands.exportTiptapJsonToMdBatch).toHaveBeenCalledWith([
293293
[
294294
{ type: "doc", content: [{ type: "paragraph" }] },
295-
"/mock/data/dir/hyprnote/sessions/session-1/_memo.md",
295+
"/mock/data/dir/hyprnote/sessions/_default/session-1/_memo.md",
296296
],
297297
]);
298298
});
@@ -338,7 +338,7 @@ describe("createNotePersister", () => {
338338
await persister.save();
339339

340340
expect(mkdir).toHaveBeenCalledWith(
341-
"/mock/data/dir/hyprnote/sessions/session-1",
341+
"/mock/data/dir/hyprnote/sessions/_default/session-1",
342342
{ recursive: true },
343343
);
344344
});

apps/desktop/src/store/tinybase/persister/note.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ describe("createNotePersister", () => {
8989
expect(commands.exportTiptapJsonToMdBatch).toHaveBeenCalledWith([
9090
[
9191
{ type: "doc", content: [{ type: "paragraph" }] },
92-
"/mock/data/dir/hyprnote/sessions/session-1/My Template.md",
92+
"/mock/data/dir/hyprnote/sessions/_default/session-1/My Template.md",
9393
],
9494
]);
9595
});
@@ -115,7 +115,7 @@ describe("createNotePersister", () => {
115115
expect(commands.exportTiptapJsonToMdBatch).toHaveBeenCalledWith([
116116
[
117117
{ type: "doc", content: [{ type: "paragraph" }] },
118-
"/mock/data/dir/hyprnote/sessions/session-1/_summary.md",
118+
"/mock/data/dir/hyprnote/sessions/_default/session-1/_summary.md",
119119
],
120120
]);
121121
});
@@ -199,7 +199,7 @@ describe("createNotePersister", () => {
199199
expect(commands.exportTiptapJsonToMdBatch).toHaveBeenCalledWith([
200200
[
201201
expect.any(Object),
202-
"/mock/data/dir/hyprnote/sessions/session-1/My_________Template.md",
202+
"/mock/data/dir/hyprnote/sessions/_default/session-1/My_________Template.md",
203203
],
204204
]);
205205
});
@@ -226,7 +226,7 @@ describe("createNotePersister", () => {
226226
expect(commands.exportTiptapJsonToMdBatch).toHaveBeenCalledWith([
227227
[
228228
expect.any(Object),
229-
"/mock/data/dir/hyprnote/sessions/session-1/unknown-template-id.md",
229+
"/mock/data/dir/hyprnote/sessions/_default/session-1/unknown-template-id.md",
230230
],
231231
]);
232232
});
@@ -292,7 +292,7 @@ describe("createNotePersister", () => {
292292
expect(commands.exportTiptapJsonToMdBatch).toHaveBeenCalledWith([
293293
[
294294
{ type: "doc", content: [{ type: "paragraph" }] },
295-
"/mock/data/dir/hyprnote/sessions/session-1/_memo.md",
295+
"/mock/data/dir/hyprnote/sessions/_default/session-1/_memo.md",
296296
],
297297
]);
298298
});
@@ -338,7 +338,7 @@ describe("createNotePersister", () => {
338338
await persister.save();
339339

340340
expect(mkdir).toHaveBeenCalledWith(
341-
"/mock/data/dir/hyprnote/sessions/session-1",
341+
"/mock/data/dir/hyprnote/sessions/_default/session-1",
342342
{ recursive: true },
343343
);
344344
});

0 commit comments

Comments
 (0)