@gmickel/gno 0.33.4 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/package.json +1 -1
- package/src/serve/browse-tree.ts +201 -0
- package/src/serve/public/app.tsx +38 -14
- package/src/serve/public/components/BacklinksPanel.tsx +10 -10
- package/src/serve/public/components/BrowseDetailPane.tsx +194 -0
- package/src/serve/public/components/BrowseOverview.tsx +81 -0
- package/src/serve/public/components/BrowseTreeSidebar.tsx +281 -0
- package/src/serve/public/components/BrowseWorkspaceCard.tsx +96 -0
- package/src/serve/public/components/FrontmatterDisplay.tsx +164 -65
- package/src/serve/public/components/OutgoingLinksPanel.tsx +8 -12
- package/src/serve/public/components/RelatedNotesSidebar.tsx +42 -76
- package/src/serve/public/components/editor/MarkdownPreview.tsx +61 -2
- package/src/serve/public/components/ui/button.tsx +1 -1
- package/src/serve/public/components/ui/dialog.tsx +1 -1
- package/src/serve/public/hooks/useWorkspace.tsx +38 -0
- package/src/serve/public/lib/browse.ts +110 -0
- package/src/serve/public/lib/workspace-tabs.ts +59 -1
- package/src/serve/public/pages/Browse.tsx +334 -344
- package/src/serve/public/pages/DocView.tsx +484 -296
- package/src/serve/public/pages/DocumentEditor.tsx +1 -11
- package/src/serve/public/pages/GraphView.tsx +5 -4
- package/src/serve/routes/api.ts +47 -0
- package/src/serve/server.ts +5 -0
- package/src/store/sqlite/adapter.ts +240 -241
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ArrowLeft,
|
|
3
|
-
ChevronRight,
|
|
4
|
-
FileText,
|
|
5
3
|
FolderOpen,
|
|
6
4
|
HomeIcon,
|
|
7
5
|
RefreshCw,
|
|
8
6
|
StarIcon,
|
|
9
7
|
} from "lucide-react";
|
|
10
|
-
import {
|
|
8
|
+
import { useEffect, useMemo, useState } from "react";
|
|
11
9
|
|
|
10
|
+
import {
|
|
11
|
+
createBrowseNodeId,
|
|
12
|
+
findBrowseNode,
|
|
13
|
+
getBrowseAncestorIds,
|
|
14
|
+
getImmediateChildFolders,
|
|
15
|
+
} from "../../browse-tree";
|
|
12
16
|
import { Loader } from "../components/ai-elements/loader";
|
|
13
|
-
import {
|
|
17
|
+
import { BrowseDetailPane } from "../components/BrowseDetailPane";
|
|
18
|
+
import { BrowseOverview } from "../components/BrowseOverview";
|
|
19
|
+
import { BrowseTreeSidebar } from "../components/BrowseTreeSidebar";
|
|
20
|
+
import { BrowseWorkspaceCard } from "../components/BrowseWorkspaceCard";
|
|
14
21
|
import { Badge } from "../components/ui/badge";
|
|
15
22
|
import { Button } from "../components/ui/button";
|
|
23
|
+
import {
|
|
24
|
+
Dialog,
|
|
25
|
+
DialogContent,
|
|
26
|
+
DialogHeader,
|
|
27
|
+
DialogTitle,
|
|
28
|
+
} from "../components/ui/dialog";
|
|
16
29
|
import {
|
|
17
30
|
Select,
|
|
18
31
|
SelectContent,
|
|
@@ -20,16 +33,18 @@ import {
|
|
|
20
33
|
SelectTrigger,
|
|
21
34
|
SelectValue,
|
|
22
35
|
} from "../components/ui/select";
|
|
23
|
-
import {
|
|
24
|
-
Table,
|
|
25
|
-
TableBody,
|
|
26
|
-
TableCell,
|
|
27
|
-
TableHead,
|
|
28
|
-
TableHeader,
|
|
29
|
-
TableRow,
|
|
30
|
-
} from "../components/ui/table";
|
|
31
36
|
import { apiFetch } from "../hooks/use-api";
|
|
32
37
|
import { useDocEvents } from "../hooks/use-doc-events";
|
|
38
|
+
import { useWorkspace } from "../hooks/useWorkspace";
|
|
39
|
+
import {
|
|
40
|
+
buildBrowseCrumbs,
|
|
41
|
+
buildBrowseLocation,
|
|
42
|
+
formatDateFieldLabel,
|
|
43
|
+
parseBrowseLocation,
|
|
44
|
+
type BrowseDocument,
|
|
45
|
+
type BrowseTreeResponse,
|
|
46
|
+
type DocsResponse,
|
|
47
|
+
} from "../lib/browse";
|
|
33
48
|
import {
|
|
34
49
|
loadFavoriteCollections,
|
|
35
50
|
loadFavoriteDocuments,
|
|
@@ -39,30 +54,7 @@ import {
|
|
|
39
54
|
|
|
40
55
|
interface PageProps {
|
|
41
56
|
navigate: (to: string | number) => void;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
interface Collection {
|
|
45
|
-
name: string;
|
|
46
|
-
path: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface Document {
|
|
50
|
-
docid: string;
|
|
51
|
-
uri: string;
|
|
52
|
-
title: string | null;
|
|
53
|
-
collection: string;
|
|
54
|
-
relPath: string;
|
|
55
|
-
sourceExt: string;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
interface DocsResponse {
|
|
59
|
-
documents: Document[];
|
|
60
|
-
total: number;
|
|
61
|
-
limit: number;
|
|
62
|
-
offset: number;
|
|
63
|
-
availableDateFields: string[];
|
|
64
|
-
sortField: string;
|
|
65
|
-
sortOrder: "asc" | "desc";
|
|
57
|
+
location?: string;
|
|
66
58
|
}
|
|
67
59
|
|
|
68
60
|
interface SyncResponse {
|
|
@@ -71,14 +63,26 @@ interface SyncResponse {
|
|
|
71
63
|
|
|
72
64
|
type SyncTarget = { kind: "all" } | { kind: "collection"; name: string } | null;
|
|
73
65
|
|
|
74
|
-
export default function Browse({ navigate }: PageProps) {
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
const
|
|
66
|
+
export default function Browse({ navigate, location }: PageProps) {
|
|
67
|
+
const { activeTab, updateActiveTabBrowseState } = useWorkspace();
|
|
68
|
+
const latestDocEvent = useDocEvents();
|
|
69
|
+
const resolvedLocation =
|
|
70
|
+
location ?? `${window.location.pathname}${window.location.search}`;
|
|
71
|
+
const selection = useMemo(
|
|
72
|
+
() =>
|
|
73
|
+
parseBrowseLocation(
|
|
74
|
+
resolvedLocation.includes("?")
|
|
75
|
+
? `?${resolvedLocation.split("?")[1] ?? ""}`
|
|
76
|
+
: ""
|
|
77
|
+
),
|
|
78
|
+
[resolvedLocation]
|
|
79
|
+
);
|
|
80
|
+
const [tree, setTree] = useState<BrowseTreeResponse | null>(null);
|
|
81
|
+
const [docs, setDocs] = useState<BrowseDocument[]>([]);
|
|
78
82
|
const [total, setTotal] = useState(0);
|
|
79
83
|
const [offset, setOffset] = useState(0);
|
|
80
|
-
const [
|
|
81
|
-
const [
|
|
84
|
+
const [treeLoading, setTreeLoading] = useState(true);
|
|
85
|
+
const [docsLoading, setDocsLoading] = useState(false);
|
|
82
86
|
const [availableDateFields, setAvailableDateFields] = useState<string[]>([]);
|
|
83
87
|
const [sortField, setSortField] = useState("modified");
|
|
84
88
|
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
|
@@ -88,24 +92,58 @@ export default function Browse({ navigate }: PageProps) {
|
|
|
88
92
|
const [refreshToken, setRefreshToken] = useState(0);
|
|
89
93
|
const [favoriteDocHrefs, setFavoriteDocHrefs] = useState<string[]>([]);
|
|
90
94
|
const [favoriteCollections, setFavoriteCollections] = useState<string[]>([]);
|
|
91
|
-
const
|
|
95
|
+
const [mobileTreeOpen, setMobileTreeOpen] = useState(false);
|
|
92
96
|
const limit = 25;
|
|
93
97
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
const selectedCollection = selection.collection;
|
|
99
|
+
const selectedPath = selection.path;
|
|
100
|
+
const selectedNodeId = selectedCollection
|
|
101
|
+
? createBrowseNodeId(selectedCollection, selectedPath)
|
|
102
|
+
: null;
|
|
103
|
+
const selectedNode = tree
|
|
104
|
+
? findBrowseNode(tree.collections, selectedCollection, selectedPath)
|
|
105
|
+
: null;
|
|
106
|
+
|
|
107
|
+
const expandedNodeIds = useMemo(() => {
|
|
108
|
+
const persisted = activeTab?.browseState?.expandedNodeIds ?? [];
|
|
109
|
+
const ancestors = selectedPath
|
|
110
|
+
? getBrowseAncestorIds(selectedCollection, selectedPath)
|
|
111
|
+
: [];
|
|
112
|
+
return [...new Set([...persisted, ...ancestors])];
|
|
113
|
+
}, [
|
|
114
|
+
activeTab?.browseState?.expandedNodeIds,
|
|
115
|
+
selectedCollection,
|
|
116
|
+
selectedPath,
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
const childFolders = useMemo(() => {
|
|
120
|
+
if (!tree) {
|
|
121
|
+
return [];
|
|
100
122
|
}
|
|
101
|
-
|
|
123
|
+
if (!selectedCollection) {
|
|
124
|
+
return tree.collections;
|
|
125
|
+
}
|
|
126
|
+
return getImmediateChildFolders(
|
|
127
|
+
tree.collections,
|
|
128
|
+
selectedCollection,
|
|
129
|
+
selectedPath
|
|
130
|
+
);
|
|
131
|
+
}, [selectedCollection, selectedPath, tree]);
|
|
132
|
+
|
|
133
|
+
const crumbs = useMemo(
|
|
134
|
+
() =>
|
|
135
|
+
selectedCollection
|
|
136
|
+
? buildBrowseCrumbs(selectedCollection, selectedPath)
|
|
137
|
+
: [],
|
|
138
|
+
[selectedCollection, selectedPath]
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
setOffset(0);
|
|
143
|
+
setDocs([]);
|
|
144
|
+
}, [selectedCollection, selectedPath]);
|
|
102
145
|
|
|
103
146
|
useEffect(() => {
|
|
104
|
-
void apiFetch<Collection[]>("/api/collections").then(({ data }) => {
|
|
105
|
-
if (data) {
|
|
106
|
-
setCollections(data);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
147
|
setFavoriteDocHrefs(loadFavoriteDocuments().map((entry) => entry.href));
|
|
110
148
|
setFavoriteCollections(
|
|
111
149
|
loadFavoriteCollections().map((entry) => entry.name)
|
|
@@ -113,22 +151,68 @@ export default function Browse({ navigate }: PageProps) {
|
|
|
113
151
|
}, []);
|
|
114
152
|
|
|
115
153
|
useEffect(() => {
|
|
116
|
-
|
|
154
|
+
void apiFetch<BrowseTreeResponse>("/api/browse/tree").then(({ data }) => {
|
|
155
|
+
setTree(
|
|
156
|
+
data ?? { collections: [], totalCollections: 0, totalDocuments: 0 }
|
|
157
|
+
);
|
|
158
|
+
setTreeLoading(false);
|
|
159
|
+
});
|
|
160
|
+
}, [refreshToken]);
|
|
161
|
+
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!tree) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!selectedCollection) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const node = findBrowseNode(
|
|
172
|
+
tree.collections,
|
|
173
|
+
selectedCollection,
|
|
174
|
+
selectedPath
|
|
175
|
+
);
|
|
176
|
+
if (node) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const collectionRoot = findBrowseNode(tree.collections, selectedCollection);
|
|
181
|
+
if (collectionRoot) {
|
|
182
|
+
navigate(buildBrowseLocation(selectedCollection));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
navigate("/browse");
|
|
187
|
+
}, [navigate, selectedCollection, selectedPath, tree]);
|
|
188
|
+
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (!selectedCollection) {
|
|
191
|
+
setDocs([]);
|
|
192
|
+
setTotal(0);
|
|
193
|
+
setOffset(0);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
setDocsLoading(true);
|
|
117
198
|
const params = new URLSearchParams({
|
|
199
|
+
collection: selectedCollection,
|
|
118
200
|
limit: String(limit),
|
|
119
201
|
offset: String(offset),
|
|
120
202
|
sortField,
|
|
121
203
|
sortOrder,
|
|
204
|
+
directChildrenOnly: "true",
|
|
122
205
|
});
|
|
123
|
-
if (
|
|
124
|
-
params.set("
|
|
206
|
+
if (selectedPath) {
|
|
207
|
+
params.set("pathPrefix", selectedPath);
|
|
125
208
|
}
|
|
126
|
-
const url = `/api/docs?${params.toString()}`;
|
|
127
209
|
|
|
128
|
-
void apiFetch<DocsResponse>(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
210
|
+
void apiFetch<DocsResponse>(`/api/docs?${params.toString()}`).then(
|
|
211
|
+
({ data }) => {
|
|
212
|
+
setDocsLoading(false);
|
|
213
|
+
if (!data) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
132
216
|
setAvailableDateFields(data.availableDateFields ?? []);
|
|
133
217
|
setSortField(data.sortField);
|
|
134
218
|
setSortOrder(data.sortOrder);
|
|
@@ -137,42 +221,77 @@ export default function Browse({ navigate }: PageProps) {
|
|
|
137
221
|
);
|
|
138
222
|
setTotal(data.total);
|
|
139
223
|
}
|
|
140
|
-
|
|
141
|
-
}, [
|
|
224
|
+
);
|
|
225
|
+
}, [
|
|
226
|
+
limit,
|
|
227
|
+
offset,
|
|
228
|
+
refreshToken,
|
|
229
|
+
selectedCollection,
|
|
230
|
+
selectedPath,
|
|
231
|
+
sortField,
|
|
232
|
+
sortOrder,
|
|
233
|
+
]);
|
|
142
234
|
|
|
143
235
|
useEffect(() => {
|
|
144
|
-
if (
|
|
236
|
+
if (!latestDocEvent?.changedAt) {
|
|
145
237
|
return;
|
|
146
238
|
}
|
|
147
|
-
setSortField("modified");
|
|
148
|
-
setSortOrder("desc");
|
|
149
239
|
setOffset(0);
|
|
150
240
|
setDocs([]);
|
|
151
|
-
|
|
241
|
+
setRefreshToken((current) => current + 1);
|
|
242
|
+
}, [latestDocEvent?.changedAt]);
|
|
152
243
|
|
|
153
244
|
useEffect(() => {
|
|
154
|
-
|
|
245
|
+
const persisted = activeTab?.browseState?.expandedNodeIds ?? [];
|
|
246
|
+
const ancestors = selectedPath
|
|
247
|
+
? getBrowseAncestorIds(selectedCollection, selectedPath)
|
|
248
|
+
: [];
|
|
249
|
+
const merged = [...new Set([...persisted, ...ancestors])];
|
|
250
|
+
if (merged.length === persisted.length) {
|
|
155
251
|
return;
|
|
156
252
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}, [
|
|
253
|
+
updateActiveTabBrowseState({
|
|
254
|
+
expandedNodeIds: merged,
|
|
255
|
+
});
|
|
256
|
+
}, [
|
|
257
|
+
activeTab?.browseState?.expandedNodeIds,
|
|
258
|
+
selectedCollection,
|
|
259
|
+
selectedPath,
|
|
260
|
+
updateActiveTabBrowseState,
|
|
261
|
+
]);
|
|
161
262
|
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
263
|
+
const handleLoadMore = () => {
|
|
264
|
+
setOffset((current) => current + limit);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const handleSelectNode = (collection: string, path?: string) => {
|
|
166
268
|
setDocs([]);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
: "/browse";
|
|
171
|
-
window.history.pushState({}, "", url);
|
|
269
|
+
setOffset(0);
|
|
270
|
+
setMobileTreeOpen(false);
|
|
271
|
+
navigate(buildBrowseLocation(collection, path));
|
|
172
272
|
};
|
|
173
273
|
|
|
174
|
-
const
|
|
175
|
-
|
|
274
|
+
const handleToggleNode = (nodeId: string) => {
|
|
275
|
+
updateActiveTabBrowseState((current) => {
|
|
276
|
+
const next = new Set(current.expandedNodeIds);
|
|
277
|
+
if (next.has(nodeId)) {
|
|
278
|
+
next.delete(nodeId);
|
|
279
|
+
} else {
|
|
280
|
+
next.add(nodeId);
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
expandedNodeIds: [...next],
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const handleToggleFavoriteCollection = (collection: string) => {
|
|
289
|
+
const next = toggleFavoriteCollection({
|
|
290
|
+
name: collection,
|
|
291
|
+
href: buildBrowseLocation(collection),
|
|
292
|
+
label: collection,
|
|
293
|
+
});
|
|
294
|
+
setFavoriteCollections(next.map((entry) => entry.name));
|
|
176
295
|
};
|
|
177
296
|
|
|
178
297
|
const handleSortChange = (value: string) => {
|
|
@@ -186,25 +305,9 @@ export default function Browse({ navigate }: PageProps) {
|
|
|
186
305
|
setDocs([]);
|
|
187
306
|
};
|
|
188
307
|
|
|
189
|
-
const navigateToCollection = (collection: string) => {
|
|
190
|
-
const nextValue = collection.trim();
|
|
191
|
-
if (!nextValue) {
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
setSelected(nextValue);
|
|
195
|
-
setOffset(0);
|
|
196
|
-
setDocs([]);
|
|
197
|
-
window.history.pushState(
|
|
198
|
-
{},
|
|
199
|
-
"",
|
|
200
|
-
`/browse?collection=${encodeURIComponent(nextValue)}`
|
|
201
|
-
);
|
|
202
|
-
};
|
|
203
|
-
|
|
204
308
|
const handleReindex = async () => {
|
|
205
309
|
setSyncError(null);
|
|
206
|
-
|
|
207
|
-
const body = selected ? { collection: selected } : {};
|
|
310
|
+
const body = selectedCollection ? { collection: selectedCollection } : {};
|
|
208
311
|
const { data, error } = await apiFetch<SyncResponse>("/api/sync", {
|
|
209
312
|
method: "POST",
|
|
210
313
|
body: JSON.stringify(body),
|
|
@@ -218,36 +321,27 @@ export default function Browse({ navigate }: PageProps) {
|
|
|
218
321
|
if (data?.jobId) {
|
|
219
322
|
setSyncJobId(data.jobId);
|
|
220
323
|
setSyncTarget(
|
|
221
|
-
|
|
324
|
+
selectedCollection
|
|
325
|
+
? { kind: "collection", name: selectedCollection }
|
|
326
|
+
: { kind: "all" }
|
|
222
327
|
);
|
|
223
328
|
}
|
|
224
329
|
};
|
|
225
330
|
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
return "default";
|
|
238
|
-
case ".pdf":
|
|
239
|
-
return "destructive";
|
|
240
|
-
case ".docx":
|
|
241
|
-
case ".doc":
|
|
242
|
-
return "secondary";
|
|
243
|
-
default:
|
|
244
|
-
return "outline";
|
|
245
|
-
}
|
|
246
|
-
};
|
|
331
|
+
const renderSidebar = () => (
|
|
332
|
+
<BrowseTreeSidebar
|
|
333
|
+
collections={tree?.collections ?? []}
|
|
334
|
+
expandedNodeIds={expandedNodeIds}
|
|
335
|
+
favoriteCollections={favoriteCollections}
|
|
336
|
+
onSelect={handleSelectNode}
|
|
337
|
+
onToggle={handleToggleNode}
|
|
338
|
+
onToggleFavoriteCollection={handleToggleFavoriteCollection}
|
|
339
|
+
selectedNodeId={selectedNodeId}
|
|
340
|
+
/>
|
|
341
|
+
);
|
|
247
342
|
|
|
248
343
|
return (
|
|
249
344
|
<div className="min-h-screen">
|
|
250
|
-
{/* Header */}
|
|
251
345
|
<header className="glass sticky top-0 z-10 border-border/50 border-b">
|
|
252
346
|
<div className="flex flex-wrap items-center justify-between gap-4 px-8 py-4">
|
|
253
347
|
<div className="flex items-center gap-4">
|
|
@@ -272,23 +366,15 @@ export default function Browse({ navigate }: PageProps) {
|
|
|
272
366
|
<h1 className="font-semibold text-xl">Browse</h1>
|
|
273
367
|
</div>
|
|
274
368
|
<div className="flex flex-wrap items-center justify-end gap-3">
|
|
275
|
-
<
|
|
276
|
-
|
|
277
|
-
|
|
369
|
+
<Button
|
|
370
|
+
className="gap-2 lg:hidden"
|
|
371
|
+
onClick={() => setMobileTreeOpen(true)}
|
|
372
|
+
size="sm"
|
|
373
|
+
variant="outline"
|
|
278
374
|
>
|
|
279
|
-
<
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
</SelectTrigger>
|
|
283
|
-
<SelectContent>
|
|
284
|
-
<SelectItem value="all">All Collections</SelectItem>
|
|
285
|
-
{collections.map((c) => (
|
|
286
|
-
<SelectItem key={c.name} value={c.name}>
|
|
287
|
-
{c.name}
|
|
288
|
-
</SelectItem>
|
|
289
|
-
))}
|
|
290
|
-
</SelectContent>
|
|
291
|
-
</Select>
|
|
375
|
+
<FolderOpen className="size-4" />
|
|
376
|
+
Tree
|
|
377
|
+
</Button>
|
|
292
378
|
<Select
|
|
293
379
|
onValueChange={handleSortChange}
|
|
294
380
|
value={`${sortField}:${sortOrder}`}
|
|
@@ -300,19 +386,22 @@ export default function Browse({ navigate }: PageProps) {
|
|
|
300
386
|
<SelectItem value="modified:desc">Newest Modified</SelectItem>
|
|
301
387
|
<SelectItem value="modified:asc">Oldest Modified</SelectItem>
|
|
302
388
|
{availableDateFields.map((field) => (
|
|
303
|
-
<
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
</
|
|
389
|
+
<SelectItem key={`${field}:desc`} value={`${field}:desc`}>
|
|
390
|
+
{`Newest by ${formatDateFieldLabel(field)}`}
|
|
391
|
+
</SelectItem>
|
|
392
|
+
))}
|
|
393
|
+
{availableDateFields.map((field) => (
|
|
394
|
+
<SelectItem key={`${field}:asc`} value={`${field}:asc`}>
|
|
395
|
+
{`Oldest by ${formatDateFieldLabel(field)}`}
|
|
396
|
+
</SelectItem>
|
|
311
397
|
))}
|
|
312
398
|
</SelectContent>
|
|
313
399
|
</Select>
|
|
314
400
|
<Badge className="font-mono" variant="outline">
|
|
315
|
-
{
|
|
401
|
+
{selectedNode?.documentCount ??
|
|
402
|
+
tree?.totalDocuments.toLocaleString() ??
|
|
403
|
+
0}{" "}
|
|
404
|
+
docs
|
|
316
405
|
</Badge>
|
|
317
406
|
<Button
|
|
318
407
|
className="gap-2"
|
|
@@ -323,29 +412,25 @@ export default function Browse({ navigate }: PageProps) {
|
|
|
323
412
|
<FolderOpen className="size-4" />
|
|
324
413
|
Collections
|
|
325
414
|
</Button>
|
|
326
|
-
{
|
|
415
|
+
{selectedCollection && (
|
|
327
416
|
<Button
|
|
328
417
|
className="gap-2"
|
|
329
418
|
onClick={() =>
|
|
330
|
-
|
|
331
|
-
toggleFavoriteCollection({
|
|
332
|
-
name: selected,
|
|
333
|
-
href: `/browse?collection=${encodeURIComponent(selected)}`,
|
|
334
|
-
label: selected,
|
|
335
|
-
}).map((entry) => entry.name)
|
|
336
|
-
)
|
|
419
|
+
handleToggleFavoriteCollection(selectedCollection)
|
|
337
420
|
}
|
|
338
421
|
size="sm"
|
|
339
422
|
variant="outline"
|
|
340
423
|
>
|
|
341
424
|
<StarIcon
|
|
342
425
|
className={`size-4 ${
|
|
343
|
-
favoriteCollections.includes(
|
|
426
|
+
favoriteCollections.includes(selectedCollection)
|
|
344
427
|
? "fill-current text-secondary"
|
|
345
428
|
: ""
|
|
346
429
|
}`}
|
|
347
430
|
/>
|
|
348
|
-
{favoriteCollections.includes(
|
|
431
|
+
{favoriteCollections.includes(selectedCollection)
|
|
432
|
+
? "Pinned"
|
|
433
|
+
: "Pin"}
|
|
349
434
|
</Button>
|
|
350
435
|
)}
|
|
351
436
|
<Button
|
|
@@ -355,203 +440,108 @@ export default function Browse({ navigate }: PageProps) {
|
|
|
355
440
|
size="sm"
|
|
356
441
|
>
|
|
357
442
|
<RefreshCw className="size-4" />
|
|
358
|
-
{
|
|
443
|
+
{selectedCollection ? "Re-index This Collection" : "Re-index All"}
|
|
359
444
|
</Button>
|
|
360
445
|
</div>
|
|
361
446
|
</div>
|
|
362
447
|
</header>
|
|
363
448
|
|
|
364
|
-
<
|
|
365
|
-
<
|
|
366
|
-
|
|
367
|
-
<div className="
|
|
368
|
-
<
|
|
369
|
-
Collection Controls
|
|
370
|
-
</p>
|
|
371
|
-
<p className="max-w-2xl text-muted-foreground text-sm">
|
|
372
|
-
Add folders, remove sources, and re-index after external edits
|
|
373
|
-
from the collections view.
|
|
374
|
-
</p>
|
|
375
|
-
{selected && (
|
|
376
|
-
<div className="flex items-center gap-2">
|
|
377
|
-
<span className="text-muted-foreground text-sm">
|
|
378
|
-
Current collection:
|
|
379
|
-
</span>
|
|
380
|
-
<Badge className="font-mono text-xs" variant="secondary">
|
|
381
|
-
{selected}
|
|
382
|
-
</Badge>
|
|
383
|
-
</div>
|
|
384
|
-
)}
|
|
449
|
+
<div className="flex min-h-[calc(100vh-73px)]">
|
|
450
|
+
<aside className="hidden w-[320px] shrink-0 border-border/40 border-r bg-card/30 lg:block">
|
|
451
|
+
{treeLoading ? (
|
|
452
|
+
<div className="flex h-full items-center justify-center">
|
|
453
|
+
<Loader className="text-primary" size={24} />
|
|
385
454
|
</div>
|
|
455
|
+
) : (
|
|
456
|
+
renderSidebar()
|
|
457
|
+
)}
|
|
458
|
+
</aside>
|
|
386
459
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
</span>
|
|
415
|
-
.
|
|
460
|
+
<main className="min-w-0 flex-1 p-8">
|
|
461
|
+
<div className="mx-auto max-w-6xl space-y-6">
|
|
462
|
+
<BrowseWorkspaceCard
|
|
463
|
+
crumbs={crumbs}
|
|
464
|
+
navigate={navigate}
|
|
465
|
+
onSyncComplete={() => {
|
|
466
|
+
setSyncError(null);
|
|
467
|
+
setSyncJobId(null);
|
|
468
|
+
setSyncTarget(null);
|
|
469
|
+
setRefreshToken((current) => current + 1);
|
|
470
|
+
}}
|
|
471
|
+
onSyncError={(error) => {
|
|
472
|
+
setSyncError(error);
|
|
473
|
+
setSyncJobId(null);
|
|
474
|
+
setSyncTarget(null);
|
|
475
|
+
}}
|
|
476
|
+
selectedCollection={selectedCollection}
|
|
477
|
+
syncError={syncError}
|
|
478
|
+
syncJobId={syncJobId}
|
|
479
|
+
syncTarget={syncTarget}
|
|
480
|
+
/>
|
|
481
|
+
|
|
482
|
+
{treeLoading ? (
|
|
483
|
+
<div className="flex flex-col items-center justify-center gap-4 py-20">
|
|
484
|
+
<Loader className="text-primary" size={32} />
|
|
485
|
+
<p className="text-muted-foreground">
|
|
486
|
+
Loading workspace tree...
|
|
416
487
|
</p>
|
|
417
|
-
|
|
418
|
-
|
|
488
|
+
</div>
|
|
489
|
+
) : !selectedCollection ? (
|
|
490
|
+
<BrowseOverview
|
|
491
|
+
collections={tree?.collections ?? []}
|
|
492
|
+
favoriteCollections={favoriteCollections}
|
|
493
|
+
onSelectCollection={(collection) =>
|
|
494
|
+
handleSelectNode(collection)
|
|
495
|
+
}
|
|
496
|
+
onToggleFavoriteCollection={handleToggleFavoriteCollection}
|
|
497
|
+
/>
|
|
498
|
+
) : (
|
|
499
|
+
<BrowseDetailPane
|
|
500
|
+
childFolders={childFolders}
|
|
501
|
+
docs={docs}
|
|
502
|
+
docsLoading={docsLoading}
|
|
503
|
+
favoriteDocHrefs={favoriteDocHrefs}
|
|
504
|
+
onLoadMore={handleLoadMore}
|
|
505
|
+
onOpenDoc={(uri) =>
|
|
506
|
+
navigate(`/doc?uri=${encodeURIComponent(uri)}`)
|
|
507
|
+
}
|
|
508
|
+
onSelectCollection={(collection) =>
|
|
509
|
+
handleSelectNode(collection)
|
|
510
|
+
}
|
|
511
|
+
onSelectFolder={handleSelectNode}
|
|
512
|
+
onToggleFavoriteDocument={(doc) => {
|
|
513
|
+
const next = toggleFavoriteDocument({
|
|
514
|
+
uri: doc.uri,
|
|
515
|
+
href: `/doc?uri=${encodeURIComponent(doc.uri)}`,
|
|
516
|
+
label: doc.title || doc.relPath,
|
|
517
|
+
});
|
|
518
|
+
setFavoriteDocHrefs(next.map((entry) => entry.href));
|
|
519
|
+
}}
|
|
520
|
+
selectedNode={selectedNode}
|
|
521
|
+
selectedPath={selectedPath}
|
|
522
|
+
total={total}
|
|
523
|
+
/>
|
|
524
|
+
)}
|
|
419
525
|
</div>
|
|
420
|
-
</
|
|
526
|
+
</main>
|
|
527
|
+
</div>
|
|
421
528
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
<
|
|
425
|
-
<
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
{!loading && docs.length === 0 && (
|
|
432
|
-
<div className="py-20 text-center">
|
|
433
|
-
<FileText className="mx-auto mb-4 size-12 text-muted-foreground" />
|
|
434
|
-
<h3 className="mb-2 font-medium text-lg">No documents found</h3>
|
|
435
|
-
<p className="text-muted-foreground">
|
|
436
|
-
{selected
|
|
437
|
-
? "This collection is empty"
|
|
438
|
-
: "Index some documents to get started"}
|
|
439
|
-
</p>
|
|
440
|
-
</div>
|
|
441
|
-
)}
|
|
442
|
-
|
|
443
|
-
{/* Document Table */}
|
|
444
|
-
{docs.length > 0 && (
|
|
445
|
-
<div className="animate-fade-in opacity-0">
|
|
446
|
-
<Table className="table-fixed">
|
|
447
|
-
<TableHeader>
|
|
448
|
-
<TableRow>
|
|
449
|
-
<TableHead className="w-[68%]">Document</TableHead>
|
|
450
|
-
<TableHead className="w-[220px]">Collection</TableHead>
|
|
451
|
-
<TableHead className="w-[72px] text-right">Type</TableHead>
|
|
452
|
-
</TableRow>
|
|
453
|
-
</TableHeader>
|
|
454
|
-
<TableBody>
|
|
455
|
-
{docs.map((doc, _i) => (
|
|
456
|
-
<TableRow
|
|
457
|
-
className="group cursor-pointer"
|
|
458
|
-
key={doc.docid}
|
|
459
|
-
onClick={() =>
|
|
460
|
-
navigate(`/doc?uri=${encodeURIComponent(doc.uri)}`)
|
|
461
|
-
}
|
|
462
|
-
>
|
|
463
|
-
<TableCell className="align-top whitespace-normal">
|
|
464
|
-
<div className="flex items-center gap-2">
|
|
465
|
-
<FileText className="size-4 shrink-0 text-muted-foreground" />
|
|
466
|
-
<div className="min-w-0">
|
|
467
|
-
<div className="break-words font-medium leading-tight transition-colors group-hover:text-primary">
|
|
468
|
-
{doc.title || doc.relPath}
|
|
469
|
-
</div>
|
|
470
|
-
<div className="break-all font-mono text-muted-foreground text-xs leading-relaxed">
|
|
471
|
-
{doc.relPath}
|
|
472
|
-
</div>
|
|
473
|
-
</div>
|
|
474
|
-
<Button
|
|
475
|
-
onClick={(event) => {
|
|
476
|
-
event.stopPropagation();
|
|
477
|
-
const next = toggleFavoriteDocument({
|
|
478
|
-
uri: doc.uri,
|
|
479
|
-
href: `/doc?uri=${encodeURIComponent(doc.uri)}`,
|
|
480
|
-
label: doc.title || doc.relPath,
|
|
481
|
-
});
|
|
482
|
-
setFavoriteDocHrefs(
|
|
483
|
-
next.map((entry) => entry.href)
|
|
484
|
-
);
|
|
485
|
-
}}
|
|
486
|
-
size="icon-sm"
|
|
487
|
-
variant="ghost"
|
|
488
|
-
>
|
|
489
|
-
<StarIcon
|
|
490
|
-
className={`size-4 ${
|
|
491
|
-
favoriteDocHrefs.includes(
|
|
492
|
-
`/doc?uri=${encodeURIComponent(doc.uri)}`
|
|
493
|
-
)
|
|
494
|
-
? "fill-current text-secondary"
|
|
495
|
-
: "text-muted-foreground"
|
|
496
|
-
}`}
|
|
497
|
-
/>
|
|
498
|
-
</Button>
|
|
499
|
-
<ChevronRight className="ml-auto size-4 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100" />
|
|
500
|
-
</div>
|
|
501
|
-
</TableCell>
|
|
502
|
-
<TableCell className="align-top whitespace-normal">
|
|
503
|
-
<Badge
|
|
504
|
-
className="inline-flex min-h-[2.5rem] max-w-[180px] cursor-pointer items-center px-3 py-1 text-center whitespace-normal break-words font-mono text-xs leading-tight transition-colors hover:border-primary hover:text-primary"
|
|
505
|
-
onClick={(event) => {
|
|
506
|
-
event.stopPropagation();
|
|
507
|
-
navigateToCollection(doc.collection);
|
|
508
|
-
}}
|
|
509
|
-
variant="outline"
|
|
510
|
-
>
|
|
511
|
-
{doc.collection}
|
|
512
|
-
</Badge>
|
|
513
|
-
</TableCell>
|
|
514
|
-
<TableCell className="align-top text-right">
|
|
515
|
-
<Badge
|
|
516
|
-
className="font-mono text-xs"
|
|
517
|
-
variant={getExtBadgeVariant(doc.sourceExt)}
|
|
518
|
-
>
|
|
519
|
-
{doc.sourceExt}
|
|
520
|
-
</Badge>
|
|
521
|
-
</TableCell>
|
|
522
|
-
</TableRow>
|
|
523
|
-
))}
|
|
524
|
-
</TableBody>
|
|
525
|
-
</Table>
|
|
526
|
-
|
|
527
|
-
{/* Load More */}
|
|
528
|
-
{offset + limit < total && (
|
|
529
|
-
<div className="mt-8 text-center">
|
|
530
|
-
<Button
|
|
531
|
-
className="gap-2"
|
|
532
|
-
disabled={loading}
|
|
533
|
-
onClick={handleLoadMore}
|
|
534
|
-
variant="outline"
|
|
535
|
-
>
|
|
536
|
-
{loading ? (
|
|
537
|
-
<>
|
|
538
|
-
<Loader size={16} />
|
|
539
|
-
Loading...
|
|
540
|
-
</>
|
|
541
|
-
) : (
|
|
542
|
-
<>
|
|
543
|
-
Load More
|
|
544
|
-
<Badge className="ml-1" variant="secondary">
|
|
545
|
-
{Math.min(limit, total - docs.length)} remaining
|
|
546
|
-
</Badge>
|
|
547
|
-
</>
|
|
548
|
-
)}
|
|
549
|
-
</Button>
|
|
529
|
+
<Dialog onOpenChange={setMobileTreeOpen} open={mobileTreeOpen}>
|
|
530
|
+
<DialogContent className="max-w-xl p-0">
|
|
531
|
+
<DialogHeader className="px-6 pt-6">
|
|
532
|
+
<DialogTitle>Workspace Tree</DialogTitle>
|
|
533
|
+
</DialogHeader>
|
|
534
|
+
<div className="h-[70vh] border-border/40 border-t">
|
|
535
|
+
{treeLoading ? (
|
|
536
|
+
<div className="flex h-full items-center justify-center">
|
|
537
|
+
<Loader className="text-primary" size={24} />
|
|
550
538
|
</div>
|
|
539
|
+
) : (
|
|
540
|
+
renderSidebar()
|
|
551
541
|
)}
|
|
552
542
|
</div>
|
|
553
|
-
|
|
554
|
-
</
|
|
543
|
+
</DialogContent>
|
|
544
|
+
</Dialog>
|
|
555
545
|
</div>
|
|
556
546
|
);
|
|
557
547
|
}
|