react-embed-docs 0.1.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/LICENSE +21 -0
- package/README.md +422 -0
- package/dist/client/components/Breadcrumbs.d.ts +21 -0
- package/dist/client/components/Breadcrumbs.d.ts.map +1 -0
- package/dist/client/components/Breadcrumbs.js +123 -0
- package/dist/client/components/DocsLayout.d.ts +20 -0
- package/dist/client/components/DocsLayout.d.ts.map +1 -0
- package/dist/client/components/DocsLayout.js +387 -0
- package/dist/client/components/DocumentContent.d.ts +5 -0
- package/dist/client/components/DocumentContent.d.ts.map +1 -0
- package/dist/client/components/DocumentContent.js +15 -0
- package/dist/client/components/DocumentEdit.d.ts +6 -0
- package/dist/client/components/DocumentEdit.d.ts.map +1 -0
- package/dist/client/components/DocumentEdit.js +153 -0
- package/dist/client/components/DocumentList.d.ts +5 -0
- package/dist/client/components/DocumentList.d.ts.map +1 -0
- package/dist/client/components/DocumentList.js +39 -0
- package/dist/client/components/DocumentProvider.d.ts +42 -0
- package/dist/client/components/DocumentProvider.d.ts.map +1 -0
- package/dist/client/components/DocumentProvider.js +47 -0
- package/dist/client/components/DocumentView.d.ts +6 -0
- package/dist/client/components/DocumentView.d.ts.map +1 -0
- package/dist/client/components/DocumentView.js +58 -0
- package/dist/client/components/DragOverlayItem.d.ts +5 -0
- package/dist/client/components/DragOverlayItem.d.ts.map +1 -0
- package/dist/client/components/DragOverlayItem.js +9 -0
- package/dist/client/components/EmojiPicker.d.ts +8 -0
- package/dist/client/components/EmojiPicker.d.ts.map +1 -0
- package/dist/client/components/EmojiPicker.js +48 -0
- package/dist/client/components/ExportButton.d.ts +22 -0
- package/dist/client/components/ExportButton.d.ts.map +1 -0
- package/dist/client/components/ExportButton.js +97 -0
- package/dist/client/components/Layout.d.ts +7 -0
- package/dist/client/components/Layout.d.ts.map +1 -0
- package/dist/client/components/Layout.js +172 -0
- package/dist/client/components/ReactEmbedDocs.d.ts +8 -0
- package/dist/client/components/ReactEmbedDocs.d.ts.map +1 -0
- package/dist/client/components/ReactEmbedDocs.js +8 -0
- package/dist/client/components/SearchInput.d.ts +2 -0
- package/dist/client/components/SearchInput.d.ts.map +1 -0
- package/dist/client/components/SearchInput.js +7 -0
- package/dist/client/components/Sidebar.d.ts +10 -0
- package/dist/client/components/Sidebar.d.ts.map +1 -0
- package/dist/client/components/Sidebar.js +176 -0
- package/dist/client/components/SortableTreeItem.d.ts +13 -0
- package/dist/client/components/SortableTreeItem.d.ts.map +1 -0
- package/dist/client/components/SortableTreeItem.js +24 -0
- package/dist/client/components/VersionHistory.d.ts +14 -0
- package/dist/client/components/VersionHistory.d.ts.map +1 -0
- package/dist/client/components/VersionHistory.js +102 -0
- package/dist/client/hooks/useCollaboration.d.ts +99 -0
- package/dist/client/hooks/useCollaboration.d.ts.map +1 -0
- package/dist/client/hooks/useCollaboration.js +180 -0
- package/dist/client/hooks/useDocsQuery.d.ts +84 -0
- package/dist/client/hooks/useDocsQuery.d.ts.map +1 -0
- package/dist/client/hooks/useDocsQuery.js +241 -0
- package/dist/client/hooks/useExport.d.ts +31 -0
- package/dist/client/hooks/useExport.d.ts.map +1 -0
- package/dist/client/hooks/useExport.js +66 -0
- package/dist/client/hooks/useFileUpload.d.ts +44 -0
- package/dist/client/hooks/useFileUpload.d.ts.map +1 -0
- package/dist/client/hooks/useFileUpload.js +193 -0
- package/dist/client/hooks/useSystemTheme.d.ts +2 -0
- package/dist/client/hooks/useSystemTheme.d.ts.map +1 -0
- package/dist/client/hooks/useSystemTheme.js +19 -0
- package/dist/client/hooks/useVersions.d.ts +105 -0
- package/dist/client/hooks/useVersions.d.ts.map +1 -0
- package/dist/client/hooks/useVersions.js +129 -0
- package/dist/client/index.d.ts +23 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +18 -0
- package/dist/client/lib/blocknoteTheme.d.ts +13 -0
- package/dist/client/lib/blocknoteTheme.d.ts.map +1 -0
- package/dist/client/lib/blocknoteTheme.js +76 -0
- package/dist/client/lib/path.d.ts +8 -0
- package/dist/client/lib/path.d.ts.map +1 -0
- package/dist/client/lib/path.js +30 -0
- package/dist/client/providers/DocumentProvider.d.ts +1 -0
- package/dist/client/providers/DocumentProvider.d.ts.map +1 -0
- package/dist/client/providers/DocumentProvider.js +1 -0
- package/dist/server/CollaborationService.d.ts +134 -0
- package/dist/server/CollaborationService.d.ts.map +1 -0
- package/dist/server/CollaborationService.js +307 -0
- package/dist/server/DocsService.d.ts +115 -0
- package/dist/server/DocsService.d.ts.map +1 -0
- package/dist/server/DocsService.js +512 -0
- package/dist/server/ExportService.d.ts +106 -0
- package/dist/server/ExportService.d.ts.map +1 -0
- package/dist/server/ExportService.js +501 -0
- package/dist/server/FilesService.d.ts +44 -0
- package/dist/server/FilesService.d.ts.map +1 -0
- package/dist/server/FilesService.js +78 -0
- package/dist/server/VersioningService.d.ts +112 -0
- package/dist/server/VersioningService.d.ts.map +1 -0
- package/dist/server/VersioningService.js +264 -0
- package/dist/server/db.d.ts +7 -0
- package/dist/server/db.d.ts.map +1 -0
- package/dist/server/db.js +22 -0
- package/dist/server/index.d.ts +55 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +36 -0
- package/dist/server/routes.d.ts +9 -0
- package/dist/server/routes.d.ts.map +1 -0
- package/dist/server/routes.js +483 -0
- package/dist/server/schema.d.ts +587 -0
- package/dist/server/schema.d.ts.map +1 -0
- package/dist/server/schema.js +126 -0
- package/dist/shared/types.d.ts +314 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +48 -0
- package/drizzle/migrations/0000_gray_monster_badoon.sql +88 -0
- package/drizzle/migrations/meta/0000_snapshot.json +574 -0
- package/drizzle/migrations/meta/_journal.json +13 -0
- package/package.json +109 -0
- package/styles/docs.css +981 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Document, DocumentSummary, ListDocumentsFilters, ListDocumentsOptions, ListDocumentsResult, UpdateDocument } from '../../shared/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Query key factory for documents
|
|
4
|
+
* Provides consistent query keys for caching and invalidation
|
|
5
|
+
*/
|
|
6
|
+
export declare const docsQueryKeys: {
|
|
7
|
+
all: readonly ["docs"];
|
|
8
|
+
lists: () => readonly ["docs", "list"];
|
|
9
|
+
list: (filters?: ListDocumentsFilters) => readonly ["docs", "list", ListDocumentsFilters];
|
|
10
|
+
trees: () => readonly ["docs", "tree"];
|
|
11
|
+
tree: () => readonly ["docs", "tree"];
|
|
12
|
+
details: () => readonly ["docs", "detail"];
|
|
13
|
+
detail: (id: string) => readonly ["docs", "detail", string];
|
|
14
|
+
slug: (slug: string) => readonly ["docs", "detail", "slug", string];
|
|
15
|
+
children: (parentId: string) => readonly ["docs", "children", string];
|
|
16
|
+
search: (query: string) => readonly ["docs", "search", string];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Hook to list/search documents with filters
|
|
20
|
+
* Supports pagination, sorting, and filtering by parent, published status, and search query
|
|
21
|
+
*/
|
|
22
|
+
export declare function useDocumentsQuery(filters?: ListDocumentsFilters, options?: ListDocumentsOptions, queryOptions?: {
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
}): import("@tanstack/react-query").UseQueryResult<ListDocumentsResult, Error>;
|
|
25
|
+
/**
|
|
26
|
+
* Hook to get a single document by ID
|
|
27
|
+
*/
|
|
28
|
+
export declare function useDocumentQuery(id: string | null | undefined, options?: {
|
|
29
|
+
enabled?: boolean;
|
|
30
|
+
}): import("@tanstack/react-query").UseQueryResult<Document, Error>;
|
|
31
|
+
/**
|
|
32
|
+
* Hook to get a document by slug
|
|
33
|
+
*/
|
|
34
|
+
export declare function useDocumentBySlugQuery(slug: string, options?: {
|
|
35
|
+
enabled?: boolean;
|
|
36
|
+
}): import("@tanstack/react-query").UseQueryResult<Document, Error>;
|
|
37
|
+
/**
|
|
38
|
+
* Hook to create a new document
|
|
39
|
+
* Invalidates list queries on success
|
|
40
|
+
*/
|
|
41
|
+
export declare function useCreateDocumentMutation(): import("@tanstack/react-query").UseMutationResult<Document, Error, {
|
|
42
|
+
slug: string;
|
|
43
|
+
title: string;
|
|
44
|
+
isPublished: boolean;
|
|
45
|
+
content?: any;
|
|
46
|
+
emoji?: string | undefined;
|
|
47
|
+
cover?: string | null | undefined;
|
|
48
|
+
authorId?: number | undefined;
|
|
49
|
+
parentSlugs?: string[] | undefined;
|
|
50
|
+
}, unknown>;
|
|
51
|
+
/**
|
|
52
|
+
* Hook to update an existing document
|
|
53
|
+
* Invalidates the specific document and list queries on success
|
|
54
|
+
*/
|
|
55
|
+
export declare function useUpdateDocumentMutation(): import("@tanstack/react-query").UseMutationResult<Document, Error, {
|
|
56
|
+
id: string;
|
|
57
|
+
data: UpdateDocument;
|
|
58
|
+
}, unknown>;
|
|
59
|
+
/**
|
|
60
|
+
* Hook to soft delete a document
|
|
61
|
+
* Removes from cache and invalidates lists on success
|
|
62
|
+
*/
|
|
63
|
+
export declare function useDeleteDocumentMutation(): import("@tanstack/react-query").UseMutationResult<void, Error, string, unknown>;
|
|
64
|
+
/**
|
|
65
|
+
* Hook to reorder a document (move to new parent and/or position)
|
|
66
|
+
* Implements optimistic updates for immediate UI feedback
|
|
67
|
+
*/
|
|
68
|
+
export declare function useReorderDocumentMutation(): import("@tanstack/react-query").UseMutationResult<DocumentSummary, Error, {
|
|
69
|
+
id: string;
|
|
70
|
+
parentId: string | null;
|
|
71
|
+
order: number;
|
|
72
|
+
}, {
|
|
73
|
+
previousLists: [unknown, ListDocumentsResult | undefined][];
|
|
74
|
+
previousTrees: [unknown, DocumentSummary[] | undefined][];
|
|
75
|
+
}>;
|
|
76
|
+
/**
|
|
77
|
+
* Hook to update just the order field of a document
|
|
78
|
+
* Useful for drag-and-drop sorting within the same parent
|
|
79
|
+
*/
|
|
80
|
+
export declare function useUpdateOrderMutation(): import("@tanstack/react-query").UseMutationResult<DocumentSummary, Error, {
|
|
81
|
+
id: string;
|
|
82
|
+
order: number;
|
|
83
|
+
}, unknown>;
|
|
84
|
+
//# sourceMappingURL=useDocsQuery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useDocsQuery.d.ts","sourceRoot":"","sources":["../../../src/client/hooks/useDocsQuery.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,QAAQ,EACR,eAAe,EACf,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,cAAc,EACf,MAAM,uBAAuB,CAAA;AAqF9B;;;GAGG;AACH,eAAO,MAAM,aAAa;;;qBAGR,oBAAoB;;;;iBAIvB,MAAM;iBACN,MAAM;yBACE,MAAM;oBACX,MAAM;CACvB,CAAA;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,GAAE,oBAAyB,EAClC,OAAO,GAAE,oBAAyB,EAClC,YAAY,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,8EAOzC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,mEAMlG;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,mEAMvF;AAED;;;GAGG;AACH,wBAAgB,yBAAyB;;;;;;;;;YAUxC;AAED;;;GAGG;AACH,wBAAgB,yBAAyB;QAKS,MAAM;UAAQ,cAAc;YAM7E;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,oFAUxC;AAED;;;GAGG;AACH,wBAAgB,0BAA0B;QAgClB,MAAM;cAAY,MAAM,GAAG,IAAI;WAAS,MAAM;;mBACtC,CAAC,OAAO,EAAE,mBAAmB,GAAG,SAAS,CAAC,EAAE;mBAAiB,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,SAAS,CAAC,EAAE;GAkBtI;AAED;;;GAGG;AACH,wBAAgB,sBAAsB;QAKmB,MAAM;WAAS,MAAM;YAM7E"}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
/**
|
|
3
|
+
* API client for documents
|
|
4
|
+
*/
|
|
5
|
+
const api = {
|
|
6
|
+
list: async (filters, options) => {
|
|
7
|
+
const params = new URLSearchParams();
|
|
8
|
+
if (filters?.search)
|
|
9
|
+
params.set('search', filters.search);
|
|
10
|
+
if (filters?.parentId !== undefined)
|
|
11
|
+
params.set('parentId', filters.parentId ?? 'null');
|
|
12
|
+
if (filters?.isPublished !== undefined)
|
|
13
|
+
params.set('isPublished', String(filters.isPublished));
|
|
14
|
+
if (options?.offset)
|
|
15
|
+
params.set('offset', String(options.offset));
|
|
16
|
+
if (options?.limit)
|
|
17
|
+
params.set('limit', String(options.limit));
|
|
18
|
+
if (options?.sortBy)
|
|
19
|
+
params.set('sortBy', String(options.sortBy));
|
|
20
|
+
if (options?.sortDir)
|
|
21
|
+
params.set('sortDir', options.sortDir);
|
|
22
|
+
const res = await fetch(`/api/docs?${params}`);
|
|
23
|
+
if (!res.ok)
|
|
24
|
+
throw new Error('Failed to fetch documents');
|
|
25
|
+
const data = await res.json();
|
|
26
|
+
return {
|
|
27
|
+
documents: data.items,
|
|
28
|
+
total: data.total,
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
getById: async (id) => {
|
|
32
|
+
const res = await fetch(`/api/docs/${id}`);
|
|
33
|
+
if (!res.ok)
|
|
34
|
+
throw new Error('Failed to fetch document');
|
|
35
|
+
return res.json();
|
|
36
|
+
},
|
|
37
|
+
getBySlug: async (slug) => {
|
|
38
|
+
const res = await fetch(`/api/docs/slug/${slug}`);
|
|
39
|
+
if (!res.ok)
|
|
40
|
+
throw new Error('Failed to fetch document');
|
|
41
|
+
return res.json();
|
|
42
|
+
},
|
|
43
|
+
create: async (data) => {
|
|
44
|
+
const res = await fetch('/api/docs', {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
body: JSON.stringify(data),
|
|
48
|
+
});
|
|
49
|
+
if (!res.ok)
|
|
50
|
+
throw new Error('Failed to create document');
|
|
51
|
+
return res.json();
|
|
52
|
+
},
|
|
53
|
+
update: async (id, data) => {
|
|
54
|
+
const res = await fetch(`/api/docs/${id}`, {
|
|
55
|
+
method: 'PUT',
|
|
56
|
+
headers: { 'Content-Type': 'application/json' },
|
|
57
|
+
body: JSON.stringify(data),
|
|
58
|
+
});
|
|
59
|
+
if (!res.ok)
|
|
60
|
+
throw new Error('Failed to update document');
|
|
61
|
+
return res.json();
|
|
62
|
+
},
|
|
63
|
+
delete: async (id) => {
|
|
64
|
+
const res = await fetch(`/api/docs/${id}`, {
|
|
65
|
+
method: 'DELETE',
|
|
66
|
+
});
|
|
67
|
+
if (!res.ok)
|
|
68
|
+
throw new Error('Failed to delete document');
|
|
69
|
+
},
|
|
70
|
+
reorder: async (id, parentId, order) => {
|
|
71
|
+
const res = await fetch(`/api/docs/${id}/reorder`, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
body: JSON.stringify({ parentId, order }),
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok)
|
|
77
|
+
throw new Error('Failed to reorder document');
|
|
78
|
+
return res.json();
|
|
79
|
+
},
|
|
80
|
+
updateOrder: async (id, order) => {
|
|
81
|
+
const res = await fetch(`/api/docs/${id}/order`, {
|
|
82
|
+
method: 'PATCH',
|
|
83
|
+
headers: { 'Content-Type': 'application/json' },
|
|
84
|
+
body: JSON.stringify({ order }),
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok)
|
|
87
|
+
throw new Error('Failed to update document order');
|
|
88
|
+
return res.json();
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Query key factory for documents
|
|
93
|
+
* Provides consistent query keys for caching and invalidation
|
|
94
|
+
*/
|
|
95
|
+
export const docsQueryKeys = {
|
|
96
|
+
all: ['docs'],
|
|
97
|
+
lists: () => [...docsQueryKeys.all, 'list'],
|
|
98
|
+
list: (filters = {}) => [...docsQueryKeys.lists(), filters],
|
|
99
|
+
trees: () => [...docsQueryKeys.all, 'tree'],
|
|
100
|
+
tree: () => [...docsQueryKeys.trees()],
|
|
101
|
+
details: () => [...docsQueryKeys.all, 'detail'],
|
|
102
|
+
detail: (id) => [...docsQueryKeys.details(), id],
|
|
103
|
+
slug: (slug) => [...docsQueryKeys.details(), 'slug', slug],
|
|
104
|
+
children: (parentId) => [...docsQueryKeys.all, 'children', parentId],
|
|
105
|
+
search: (query) => [...docsQueryKeys.all, 'search', query],
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Hook to list/search documents with filters
|
|
109
|
+
* Supports pagination, sorting, and filtering by parent, published status, and search query
|
|
110
|
+
*/
|
|
111
|
+
export function useDocumentsQuery(filters = {}, options = {}, queryOptions = {}) {
|
|
112
|
+
return useQuery({
|
|
113
|
+
queryKey: docsQueryKeys.list(filters),
|
|
114
|
+
queryFn: () => api.list(filters, options),
|
|
115
|
+
...queryOptions,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Hook to get a single document by ID
|
|
120
|
+
*/
|
|
121
|
+
export function useDocumentQuery(id, options = {}) {
|
|
122
|
+
return useQuery({
|
|
123
|
+
queryKey: docsQueryKeys.detail(id),
|
|
124
|
+
queryFn: () => api.getById(id),
|
|
125
|
+
enabled: !!id && options.enabled !== false,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Hook to get a document by slug
|
|
130
|
+
*/
|
|
131
|
+
export function useDocumentBySlugQuery(slug, options = {}) {
|
|
132
|
+
return useQuery({
|
|
133
|
+
queryKey: docsQueryKeys.slug(slug),
|
|
134
|
+
queryFn: () => api.getBySlug(slug),
|
|
135
|
+
enabled: !!slug && options.enabled !== false,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Hook to create a new document
|
|
140
|
+
* Invalidates list queries on success
|
|
141
|
+
*/
|
|
142
|
+
export function useCreateDocumentMutation() {
|
|
143
|
+
const queryClient = useQueryClient();
|
|
144
|
+
return useMutation({
|
|
145
|
+
mutationFn: api.create,
|
|
146
|
+
onSuccess: () => {
|
|
147
|
+
queryClient.invalidateQueries({ queryKey: docsQueryKeys.lists() });
|
|
148
|
+
queryClient.invalidateQueries({ queryKey: docsQueryKeys.trees() });
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Hook to update an existing document
|
|
154
|
+
* Invalidates the specific document and list queries on success
|
|
155
|
+
*/
|
|
156
|
+
export function useUpdateDocumentMutation() {
|
|
157
|
+
const queryClient = useQueryClient();
|
|
158
|
+
return useMutation({
|
|
159
|
+
mutationFn: ({ id, data }) => api.update(id, data),
|
|
160
|
+
onSuccess: (_data, variables) => {
|
|
161
|
+
queryClient.invalidateQueries({ queryKey: docsQueryKeys.detail(variables.id) });
|
|
162
|
+
queryClient.invalidateQueries({ queryKey: docsQueryKeys.lists() });
|
|
163
|
+
queryClient.invalidateQueries({ queryKey: docsQueryKeys.trees() });
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Hook to soft delete a document
|
|
169
|
+
* Removes from cache and invalidates lists on success
|
|
170
|
+
*/
|
|
171
|
+
export function useDeleteDocumentMutation() {
|
|
172
|
+
const queryClient = useQueryClient();
|
|
173
|
+
return useMutation({
|
|
174
|
+
mutationFn: api.delete,
|
|
175
|
+
onSuccess: () => {
|
|
176
|
+
queryClient.invalidateQueries({ queryKey: docsQueryKeys.lists() });
|
|
177
|
+
queryClient.invalidateQueries({ queryKey: docsQueryKeys.trees() });
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Hook to reorder a document (move to new parent and/or position)
|
|
183
|
+
* Implements optimistic updates for immediate UI feedback
|
|
184
|
+
*/
|
|
185
|
+
export function useReorderDocumentMutation() {
|
|
186
|
+
const queryClient = useQueryClient();
|
|
187
|
+
return useMutation({
|
|
188
|
+
mutationFn: ({ id, parentId, order }) => api.reorder(id, parentId, order),
|
|
189
|
+
onMutate: async (variables) => {
|
|
190
|
+
const { id, parentId, order } = variables;
|
|
191
|
+
// Cancel outgoing refetches
|
|
192
|
+
await queryClient.cancelQueries({ queryKey: docsQueryKeys.lists() });
|
|
193
|
+
await queryClient.cancelQueries({ queryKey: docsQueryKeys.trees() });
|
|
194
|
+
// Snapshot previous values
|
|
195
|
+
const previousLists = queryClient.getQueriesData({ queryKey: docsQueryKeys.lists() });
|
|
196
|
+
const previousTrees = queryClient.getQueriesData({ queryKey: docsQueryKeys.trees() });
|
|
197
|
+
// Optimistically update all list caches
|
|
198
|
+
queryClient.setQueriesData({ queryKey: docsQueryKeys.lists() }, (old) => {
|
|
199
|
+
if (!old)
|
|
200
|
+
return old;
|
|
201
|
+
return {
|
|
202
|
+
...old,
|
|
203
|
+
documents: old.documents.map((doc) => doc.id === id ? { ...doc, parentId, order } : doc),
|
|
204
|
+
};
|
|
205
|
+
});
|
|
206
|
+
// Return context for rollback
|
|
207
|
+
return { previousLists, previousTrees };
|
|
208
|
+
},
|
|
209
|
+
onError: (_err, _variables, context) => {
|
|
210
|
+
// Rollback on error
|
|
211
|
+
if (context) {
|
|
212
|
+
context.previousLists.forEach(([key, value]) => {
|
|
213
|
+
queryClient.setQueryData(key, value);
|
|
214
|
+
});
|
|
215
|
+
context.previousTrees.forEach(([key, value]) => {
|
|
216
|
+
queryClient.setQueryData(key, value);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
onSettled: () => {
|
|
221
|
+
// Always refetch after error or success
|
|
222
|
+
queryClient.invalidateQueries({ queryKey: docsQueryKeys.lists() });
|
|
223
|
+
queryClient.invalidateQueries({ queryKey: docsQueryKeys.trees() });
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Hook to update just the order field of a document
|
|
229
|
+
* Useful for drag-and-drop sorting within the same parent
|
|
230
|
+
*/
|
|
231
|
+
export function useUpdateOrderMutation() {
|
|
232
|
+
const queryClient = useQueryClient();
|
|
233
|
+
return useMutation({
|
|
234
|
+
mutationFn: ({ id, order }) => api.updateOrder(id, order),
|
|
235
|
+
onSuccess: (_data, variables) => {
|
|
236
|
+
queryClient.invalidateQueries({ queryKey: docsQueryKeys.detail(variables.id) });
|
|
237
|
+
queryClient.invalidateQueries({ queryKey: docsQueryKeys.lists() });
|
|
238
|
+
queryClient.invalidateQueries({ queryKey: docsQueryKeys.trees() });
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type ExportFormat = 'docx' | 'pdf' | 'html' | 'markdown';
|
|
2
|
+
export interface UseExportOptions {
|
|
3
|
+
/** Base URL for API requests (default: '/api') */
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface UseExportReturn {
|
|
7
|
+
exportDocument: (documentId: string, format: ExportFormat, filename?: string) => Promise<void>;
|
|
8
|
+
isExporting: boolean;
|
|
9
|
+
error: Error | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Hook for exporting documents to various formats
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* function DocumentView({ docId }: { docId: string }) {
|
|
17
|
+
* const { exportDocument, isExporting } = useExport()
|
|
18
|
+
*
|
|
19
|
+
* return (
|
|
20
|
+
* <button
|
|
21
|
+
* onClick={() => exportDocument(docId, 'docx')}
|
|
22
|
+
* disabled={isExporting}
|
|
23
|
+
* >
|
|
24
|
+
* Export to Word
|
|
25
|
+
* </button>
|
|
26
|
+
* )
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function useExport(options?: UseExportOptions): UseExportReturn;
|
|
31
|
+
//# sourceMappingURL=useExport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useExport.d.ts","sourceRoot":"","sources":["../../../src/client/hooks/useExport.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,UAAU,CAAA;AAE/D,MAAM,WAAW,gBAAgB;IAC/B,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9F,WAAW,EAAE,OAAO,CAAA;IACpB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;CACpB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,SAAS,CAAC,OAAO,GAAE,gBAAqB,GAAG,eAAe,CAwDzE"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Hook for exporting documents to various formats
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```tsx
|
|
7
|
+
* function DocumentView({ docId }: { docId: string }) {
|
|
8
|
+
* const { exportDocument, isExporting } = useExport()
|
|
9
|
+
*
|
|
10
|
+
* return (
|
|
11
|
+
* <button
|
|
12
|
+
* onClick={() => exportDocument(docId, 'docx')}
|
|
13
|
+
* disabled={isExporting}
|
|
14
|
+
* >
|
|
15
|
+
* Export to Word
|
|
16
|
+
* </button>
|
|
17
|
+
* )
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function useExport(options = {}) {
|
|
22
|
+
const { baseUrl = '/api' } = options;
|
|
23
|
+
const [isExporting, setIsExporting] = useState(false);
|
|
24
|
+
const [error, setError] = useState(null);
|
|
25
|
+
const exportDocument = useCallback(async (documentId, format, filename) => {
|
|
26
|
+
setIsExporting(true);
|
|
27
|
+
setError(null);
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(`${baseUrl}/docs/${documentId}/export?format=${format}`);
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
const errorData = await response.json().catch(() => ({}));
|
|
32
|
+
throw new Error(errorData.message || `Export failed: ${response.statusText}`);
|
|
33
|
+
}
|
|
34
|
+
// Get the blob
|
|
35
|
+
const blob = await response.blob();
|
|
36
|
+
// Determine filename from Content-Disposition header or use default
|
|
37
|
+
const contentDisposition = response.headers.get('Content-Disposition');
|
|
38
|
+
const downloadedFilename = contentDisposition
|
|
39
|
+
?.match(/filename="([^"]+)"/)?.[1]
|
|
40
|
+
|| filename
|
|
41
|
+
|| `document.${format === 'markdown' ? 'md' : format}`;
|
|
42
|
+
// Create download link
|
|
43
|
+
const url = window.URL.createObjectURL(blob);
|
|
44
|
+
const link = document.createElement('a');
|
|
45
|
+
link.href = url;
|
|
46
|
+
link.download = downloadedFilename;
|
|
47
|
+
document.body.appendChild(link);
|
|
48
|
+
link.click();
|
|
49
|
+
document.body.removeChild(link);
|
|
50
|
+
window.URL.revokeObjectURL(url);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
const exportError = err instanceof Error ? err : new Error('Export failed');
|
|
54
|
+
setError(exportError);
|
|
55
|
+
throw exportError;
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
setIsExporting(false);
|
|
59
|
+
}
|
|
60
|
+
}, [baseUrl]);
|
|
61
|
+
return {
|
|
62
|
+
exportDocument,
|
|
63
|
+
isExporting,
|
|
64
|
+
error,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface FileUploadOptions {
|
|
2
|
+
maxSize?: number;
|
|
3
|
+
acceptedTypes?: string[];
|
|
4
|
+
onProgress?: (progress: number) => void;
|
|
5
|
+
}
|
|
6
|
+
export interface FileUploadState {
|
|
7
|
+
isUploading: boolean;
|
|
8
|
+
progress: number;
|
|
9
|
+
error: Error | null;
|
|
10
|
+
}
|
|
11
|
+
export interface UploadResult {
|
|
12
|
+
id: string;
|
|
13
|
+
filename: string;
|
|
14
|
+
mimeType: string;
|
|
15
|
+
size: number;
|
|
16
|
+
url: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Hook for handling file uploads with base64 conversion
|
|
20
|
+
* Features:
|
|
21
|
+
* - File size and type validation
|
|
22
|
+
* - Upload progress tracking
|
|
23
|
+
* - Error handling
|
|
24
|
+
* - Base64 conversion
|
|
25
|
+
*/
|
|
26
|
+
export declare function useFileUpload(options?: FileUploadOptions): {
|
|
27
|
+
isUploading: boolean;
|
|
28
|
+
progress: number;
|
|
29
|
+
error: Error | null;
|
|
30
|
+
uploadFile: (file: File) => Promise<UploadResult>;
|
|
31
|
+
reset: () => void;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Hook for handling multiple file uploads
|
|
35
|
+
*/
|
|
36
|
+
export declare function useMultiFileUpload(options?: FileUploadOptions): {
|
|
37
|
+
isUploading: boolean;
|
|
38
|
+
progress: Record<string, number>;
|
|
39
|
+
errors: Record<string, Error>;
|
|
40
|
+
results: Record<string, UploadResult>;
|
|
41
|
+
uploadFiles: (files: File[]) => Promise<UploadResult[]>;
|
|
42
|
+
reset: () => void;
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=useFileUpload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFileUpload.d.ts","sourceRoot":"","sources":["../../../src/client/hooks/useFileUpload.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;CACxC;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,OAAO,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;CACZ;AAuCD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,iBAAsB;;;;uBAQ5C,IAAI,KAAG,OAAO,CAAC,YAAY,CAAC;;EA+E5C;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,iBAAsB;;;;;yBAShD,IAAI,EAAE,KAAG,OAAO,CAAC,YAAY,EAAE,CAAC;;EA8EjD"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Converts a File to base64 string
|
|
4
|
+
*/
|
|
5
|
+
function fileToBase64(file) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const reader = new FileReader();
|
|
8
|
+
reader.onload = () => {
|
|
9
|
+
const result = reader.result;
|
|
10
|
+
const base64 = result.split(',')[1];
|
|
11
|
+
resolve(base64);
|
|
12
|
+
};
|
|
13
|
+
reader.onerror = reject;
|
|
14
|
+
reader.readAsDataURL(file);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Validates file size
|
|
19
|
+
*/
|
|
20
|
+
function validateFileSize(file, maxSize) {
|
|
21
|
+
return file.size <= maxSize;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Validates file type
|
|
25
|
+
*/
|
|
26
|
+
function validateFileType(file, acceptedTypes) {
|
|
27
|
+
if (!acceptedTypes || acceptedTypes.length === 0)
|
|
28
|
+
return true;
|
|
29
|
+
return acceptedTypes.some((type) => {
|
|
30
|
+
if (type.includes('*')) {
|
|
31
|
+
const [category] = type.split('/');
|
|
32
|
+
return file.type.startsWith(`${category}/`);
|
|
33
|
+
}
|
|
34
|
+
return file.type === type;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Hook for handling file uploads with base64 conversion
|
|
39
|
+
* Features:
|
|
40
|
+
* - File size and type validation
|
|
41
|
+
* - Upload progress tracking
|
|
42
|
+
* - Error handling
|
|
43
|
+
* - Base64 conversion
|
|
44
|
+
*/
|
|
45
|
+
export function useFileUpload(options = {}) {
|
|
46
|
+
const { maxSize = 5 * 1024 * 1024, acceptedTypes, onProgress } = options;
|
|
47
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
48
|
+
const [progress, setProgress] = useState(0);
|
|
49
|
+
const [error, setError] = useState(null);
|
|
50
|
+
const uploadFile = useCallback(async (file) => {
|
|
51
|
+
setIsUploading(true);
|
|
52
|
+
setProgress(0);
|
|
53
|
+
setError(null);
|
|
54
|
+
try {
|
|
55
|
+
// Validate file size
|
|
56
|
+
if (!validateFileSize(file, maxSize)) {
|
|
57
|
+
throw new Error(`File too large (max ${(maxSize / 1024 / 1024).toFixed(1)}MB)`);
|
|
58
|
+
}
|
|
59
|
+
// Validate file type
|
|
60
|
+
if (!validateFileType(file, acceptedTypes)) {
|
|
61
|
+
throw new Error(`File type not accepted. Allowed: ${acceptedTypes?.join(', ') ?? 'all'}`);
|
|
62
|
+
}
|
|
63
|
+
// Simulate progress updates
|
|
64
|
+
const progressInterval = setInterval(() => {
|
|
65
|
+
setProgress((prev) => {
|
|
66
|
+
const next = prev + Math.random() * 20;
|
|
67
|
+
if (next >= 90) {
|
|
68
|
+
clearInterval(progressInterval);
|
|
69
|
+
return 90;
|
|
70
|
+
}
|
|
71
|
+
onProgress?.(next);
|
|
72
|
+
return next;
|
|
73
|
+
});
|
|
74
|
+
}, 100);
|
|
75
|
+
// Convert to base64
|
|
76
|
+
const base64Content = await fileToBase64(file);
|
|
77
|
+
// Upload to server
|
|
78
|
+
const res = await fetch('/api/docs/files', {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: { 'Content-Type': 'application/json' },
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
filename: file.name,
|
|
83
|
+
mimeType: file.type,
|
|
84
|
+
content: base64Content,
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
clearInterval(progressInterval);
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
const errorData = await res.json().catch(() => ({ message: 'Upload failed' }));
|
|
90
|
+
throw new Error(errorData.message || `Upload failed: ${res.statusText}`);
|
|
91
|
+
}
|
|
92
|
+
setProgress(100);
|
|
93
|
+
onProgress?.(100);
|
|
94
|
+
const result = await res.json();
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
99
|
+
setError(error);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
setIsUploading(false);
|
|
104
|
+
}
|
|
105
|
+
}, [maxSize, acceptedTypes, onProgress]);
|
|
106
|
+
const reset = useCallback(() => {
|
|
107
|
+
setIsUploading(false);
|
|
108
|
+
setProgress(0);
|
|
109
|
+
setError(null);
|
|
110
|
+
}, []);
|
|
111
|
+
return {
|
|
112
|
+
isUploading,
|
|
113
|
+
progress,
|
|
114
|
+
error,
|
|
115
|
+
uploadFile,
|
|
116
|
+
reset,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Hook for handling multiple file uploads
|
|
121
|
+
*/
|
|
122
|
+
export function useMultiFileUpload(options = {}) {
|
|
123
|
+
const { maxSize = 5 * 1024 * 1024, acceptedTypes } = options;
|
|
124
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
125
|
+
const [progress, setProgress] = useState({});
|
|
126
|
+
const [errors, setErrors] = useState({});
|
|
127
|
+
const [results, setResults] = useState({});
|
|
128
|
+
const uploadFiles = useCallback(async (files) => {
|
|
129
|
+
setIsUploading(true);
|
|
130
|
+
setProgress({});
|
|
131
|
+
setErrors({});
|
|
132
|
+
setResults({});
|
|
133
|
+
const uploadPromises = files.map(async (file) => {
|
|
134
|
+
try {
|
|
135
|
+
// Validate file size
|
|
136
|
+
if (file.size > maxSize) {
|
|
137
|
+
throw new Error(`File "${file.name}" too large (max ${(maxSize / 1024 / 1024).toFixed(1)}MB)`);
|
|
138
|
+
}
|
|
139
|
+
// Validate file type
|
|
140
|
+
if (acceptedTypes && !validateFileType(file, acceptedTypes)) {
|
|
141
|
+
throw new Error(`File "${file.name}" type not accepted`);
|
|
142
|
+
}
|
|
143
|
+
setProgress((prev) => ({ ...prev, [file.name]: 0 }));
|
|
144
|
+
// Convert to base64
|
|
145
|
+
const base64Content = await fileToBase64(file);
|
|
146
|
+
setProgress((prev) => ({ ...prev, [file.name]: 50 }));
|
|
147
|
+
// Upload to server
|
|
148
|
+
const res = await fetch('/api/files', {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: { 'Content-Type': 'application/json' },
|
|
151
|
+
body: JSON.stringify({
|
|
152
|
+
filename: file.name,
|
|
153
|
+
mimeType: file.type,
|
|
154
|
+
content: base64Content,
|
|
155
|
+
}),
|
|
156
|
+
});
|
|
157
|
+
if (!res.ok) {
|
|
158
|
+
throw new Error(`Failed to upload "${file.name}"`);
|
|
159
|
+
}
|
|
160
|
+
const result = await res.json();
|
|
161
|
+
setProgress((prev) => ({ ...prev, [file.name]: 100 }));
|
|
162
|
+
setResults((prev) => ({ ...prev, [file.name]: result }));
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
167
|
+
setErrors((prev) => ({ ...prev, [file.name]: error }));
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
try {
|
|
172
|
+
const allResults = await Promise.all(uploadPromises);
|
|
173
|
+
return allResults;
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
setIsUploading(false);
|
|
177
|
+
}
|
|
178
|
+
}, [maxSize, acceptedTypes]);
|
|
179
|
+
const reset = useCallback(() => {
|
|
180
|
+
setIsUploading(false);
|
|
181
|
+
setProgress({});
|
|
182
|
+
setErrors({});
|
|
183
|
+
setResults({});
|
|
184
|
+
}, []);
|
|
185
|
+
return {
|
|
186
|
+
isUploading,
|
|
187
|
+
progress,
|
|
188
|
+
errors,
|
|
189
|
+
results,
|
|
190
|
+
uploadFiles,
|
|
191
|
+
reset,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSystemTheme.d.ts","sourceRoot":"","sources":["../../../src/client/hooks/useSystemTheme.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,cAAc,wBAqB1B,CAAA"}
|