@imjp/writenex-astro 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/README.md +539 -0
- package/dist/chunk-5PM6EQE5.js +151 -0
- package/dist/chunk-5PM6EQE5.js.map +1 -0
- package/dist/chunk-7XU5X6CW.js +1331 -0
- package/dist/chunk-7XU5X6CW.js.map +1 -0
- package/dist/chunk-AAOQHQPU.js +574 -0
- package/dist/chunk-AAOQHQPU.js.map +1 -0
- package/dist/chunk-CF2XXJFF.js +1410 -0
- package/dist/chunk-CF2XXJFF.js.map +1 -0
- package/dist/chunk-CRPZUUDU.js +52 -0
- package/dist/chunk-CRPZUUDU.js.map +1 -0
- package/dist/chunk-CYLDJ3HZ.js +310 -0
- package/dist/chunk-CYLDJ3HZ.js.map +1 -0
- package/dist/chunk-KIKIPIFA.js +1 -0
- package/dist/chunk-KIKIPIFA.js.map +1 -0
- package/dist/chunk-XNTQTTJU.js +145 -0
- package/dist/chunk-XNTQTTJU.js.map +1 -0
- package/dist/client/index.css +2 -0
- package/dist/client/index.css.map +1 -0
- package/dist/client/index.js +375 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/styles.css +584 -0
- package/dist/client/variables.css +304 -0
- package/dist/config/index.d.ts +54 -0
- package/dist/config/index.js +38 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config-BmEdBDo_.d.ts +220 -0
- package/dist/content-BWR52vD-.d.ts +64 -0
- package/dist/discovery/index.d.ts +310 -0
- package/dist/discovery/index.js +38 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/errors-C0iYiDTv.d.ts +107 -0
- package/dist/filesystem/index.d.ts +1292 -0
- package/dist/filesystem/index.js +203 -0
- package/dist/filesystem/index.js.map +1 -0
- package/dist/image-FP7w5ZIs.d.ts +47 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +151 -0
- package/dist/index.js.map +1 -0
- package/dist/loader-55LWCXHA.js +12 -0
- package/dist/loader-55LWCXHA.js.map +1 -0
- package/dist/loader-CrdnaAWR.d.ts +327 -0
- package/dist/server/index.d.ts +357 -0
- package/dist/server/index.js +37 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +94 -0
- package/src/client/App.tsx +900 -0
- package/src/client/components/ConfigPanel/ConfigPanel.css +553 -0
- package/src/client/components/ConfigPanel/ConfigPanel.tsx +396 -0
- package/src/client/components/ConfigPanel/index.ts +6 -0
- package/src/client/components/CreateContentModal/CreateContentModal.css +327 -0
- package/src/client/components/CreateContentModal/CreateContentModal.tsx +216 -0
- package/src/client/components/CreateContentModal/index.ts +7 -0
- package/src/client/components/Editor/Editor.css +885 -0
- package/src/client/components/Editor/Editor.tsx +484 -0
- package/src/client/components/Editor/ImageDialog.css +344 -0
- package/src/client/components/Editor/ImageDialog.tsx +367 -0
- package/src/client/components/Editor/LinkDialog.css +326 -0
- package/src/client/components/Editor/LinkDialog.tsx +332 -0
- package/src/client/components/Editor/index.ts +6 -0
- package/src/client/components/FrontmatterForm/FrontmatterForm.css +468 -0
- package/src/client/components/FrontmatterForm/FrontmatterForm.tsx +914 -0
- package/src/client/components/FrontmatterForm/index.ts +7 -0
- package/src/client/components/Header/Header.css +300 -0
- package/src/client/components/Header/Header.tsx +300 -0
- package/src/client/components/Header/index.ts +7 -0
- package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.css +239 -0
- package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.tsx +151 -0
- package/src/client/components/KeyboardShortcuts/index.ts +6 -0
- package/src/client/components/LazyEditor.tsx +75 -0
- package/src/client/components/LiveRegion/LiveRegion.css +19 -0
- package/src/client/components/LiveRegion/LiveRegion.tsx +60 -0
- package/src/client/components/LiveRegion/index.ts +7 -0
- package/src/client/components/SearchReplace/SearchReplacePanel.css +300 -0
- package/src/client/components/SearchReplace/SearchReplacePanel.tsx +332 -0
- package/src/client/components/SearchReplace/index.ts +7 -0
- package/src/client/components/SelectCollectionModal/SelectCollectionModal.css +308 -0
- package/src/client/components/SelectCollectionModal/SelectCollectionModal.tsx +223 -0
- package/src/client/components/SelectCollectionModal/index.ts +7 -0
- package/src/client/components/Sidebar/Sidebar.css +570 -0
- package/src/client/components/Sidebar/Sidebar.tsx +617 -0
- package/src/client/components/Sidebar/index.ts +7 -0
- package/src/client/components/SkipLink/SkipLink.css +51 -0
- package/src/client/components/SkipLink/SkipLink.tsx +67 -0
- package/src/client/components/SkipLink/index.ts +7 -0
- package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.css +233 -0
- package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.tsx +160 -0
- package/src/client/components/UnsavedChangesModal/index.ts +1 -0
- package/src/client/components/VersionHistory/DiffViewer.css +430 -0
- package/src/client/components/VersionHistory/DiffViewer.tsx +383 -0
- package/src/client/components/VersionHistory/VersionActions.css +318 -0
- package/src/client/components/VersionHistory/VersionActions.tsx +277 -0
- package/src/client/components/VersionHistory/VersionHistoryPanel.css +369 -0
- package/src/client/components/VersionHistory/VersionHistoryPanel.tsx +469 -0
- package/src/client/components/VersionHistory/index.ts +9 -0
- package/src/client/context/ApiContext.tsx +154 -0
- package/src/client/context/ThemeContext.tsx +172 -0
- package/src/client/hooks/useAnnounce.ts +201 -0
- package/src/client/hooks/useApi.ts +374 -0
- package/src/client/hooks/useArrowNavigation.ts +286 -0
- package/src/client/hooks/useAutosave.ts +241 -0
- package/src/client/hooks/useFocusTrap.ts +178 -0
- package/src/client/hooks/useKeyboardShortcuts.ts +203 -0
- package/src/client/hooks/useSearch.ts +206 -0
- package/src/client/hooks/useVersionHistory.ts +451 -0
- package/src/client/index.tsx +70 -0
- package/src/client/styles.css +584 -0
- package/src/client/utils/focus.ts +57 -0
- package/src/client/utils/openInEditor.ts +130 -0
- package/src/client/variables.css +304 -0
- package/src/config/defaults.ts +109 -0
- package/src/config/index.ts +32 -0
- package/src/config/loader.ts +174 -0
- package/src/config/schema.ts +161 -0
- package/src/core/constants.ts +39 -0
- package/src/core/errors.ts +739 -0
- package/src/core/index.ts +11 -0
- package/src/discovery/collections.ts +216 -0
- package/src/discovery/index.ts +33 -0
- package/src/discovery/patterns.ts +702 -0
- package/src/discovery/schema.ts +453 -0
- package/src/filesystem/images.ts +798 -0
- package/src/filesystem/index.ts +107 -0
- package/src/filesystem/reader.ts +452 -0
- package/src/filesystem/version-config.ts +390 -0
- package/src/filesystem/versions.ts +1339 -0
- package/src/filesystem/watcher.ts +226 -0
- package/src/filesystem/writer.ts +540 -0
- package/src/index.ts +61 -0
- package/src/integration.ts +228 -0
- package/src/server/assets.ts +254 -0
- package/src/server/cache.ts +355 -0
- package/src/server/index.ts +33 -0
- package/src/server/middleware.ts +209 -0
- package/src/server/routes.ts +1428 -0
- package/src/types/api.ts +61 -0
- package/src/types/config.ts +134 -0
- package/src/types/content.ts +64 -0
- package/src/types/image.ts +48 -0
- package/src/types/index.ts +58 -0
- package/src/types/version.ts +117 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Search hook for editor
|
|
3
|
+
*
|
|
4
|
+
* Provides search and replace functionality for markdown content.
|
|
5
|
+
*
|
|
6
|
+
* @module @writenex/astro/client/hooks/useSearch
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useState, useCallback } from "react";
|
|
10
|
+
import type { SearchOptions } from "../components/SearchReplace";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Search state and handlers
|
|
14
|
+
*/
|
|
15
|
+
export interface UseSearchResult {
|
|
16
|
+
/** Whether search panel is open */
|
|
17
|
+
isSearchOpen: boolean;
|
|
18
|
+
/** Open search panel */
|
|
19
|
+
openSearch: () => void;
|
|
20
|
+
/** Close search panel */
|
|
21
|
+
closeSearch: () => void;
|
|
22
|
+
/** Toggle search panel */
|
|
23
|
+
toggleSearch: () => void;
|
|
24
|
+
/** Current search query */
|
|
25
|
+
searchQuery: string;
|
|
26
|
+
/** Array of match positions */
|
|
27
|
+
searchMatches: number[];
|
|
28
|
+
/** Current active match index (1-based) */
|
|
29
|
+
searchActiveIndex: number;
|
|
30
|
+
/** Total number of matches */
|
|
31
|
+
totalMatches: number;
|
|
32
|
+
/** Perform search */
|
|
33
|
+
handleFind: (query: string, options: SearchOptions) => number;
|
|
34
|
+
/** Navigate to next match */
|
|
35
|
+
handleNextMatch: () => void;
|
|
36
|
+
/** Navigate to previous match */
|
|
37
|
+
handlePreviousMatch: () => void;
|
|
38
|
+
/** Replace current match */
|
|
39
|
+
handleReplace: (
|
|
40
|
+
replacement: string,
|
|
41
|
+
content: string,
|
|
42
|
+
setContent: (content: string) => void
|
|
43
|
+
) => void;
|
|
44
|
+
/** Replace all matches */
|
|
45
|
+
handleReplaceAll: (
|
|
46
|
+
replacement: string,
|
|
47
|
+
content: string,
|
|
48
|
+
setContent: (content: string) => void
|
|
49
|
+
) => number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Hook for search and replace functionality
|
|
54
|
+
*
|
|
55
|
+
* @param getContent - Function to get current content
|
|
56
|
+
* @returns Search state and handlers
|
|
57
|
+
*/
|
|
58
|
+
export function useSearch(getContent: () => string): UseSearchResult {
|
|
59
|
+
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
|
60
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
61
|
+
const [searchMatches, setSearchMatches] = useState<number[]>([]);
|
|
62
|
+
const [searchActiveIndex, setSearchActiveIndex] = useState(0);
|
|
63
|
+
|
|
64
|
+
const openSearch = useCallback(() => setIsSearchOpen(true), []);
|
|
65
|
+
const closeSearch = useCallback(() => setIsSearchOpen(false), []);
|
|
66
|
+
const toggleSearch = useCallback(() => setIsSearchOpen((prev) => !prev), []);
|
|
67
|
+
|
|
68
|
+
const handleFind = useCallback(
|
|
69
|
+
(query: string, options: SearchOptions): number => {
|
|
70
|
+
setSearchQuery(query);
|
|
71
|
+
|
|
72
|
+
if (!query) {
|
|
73
|
+
setSearchMatches([]);
|
|
74
|
+
setSearchActiveIndex(0);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const content = getContent();
|
|
79
|
+
let pattern: RegExp;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
let regexPattern = options.regex
|
|
83
|
+
? query
|
|
84
|
+
: query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
85
|
+
|
|
86
|
+
if (options.wholeWord) {
|
|
87
|
+
regexPattern = `\\b${regexPattern}\\b`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
pattern = new RegExp(regexPattern, options.caseSensitive ? "g" : "gi");
|
|
91
|
+
} catch {
|
|
92
|
+
// Invalid regex
|
|
93
|
+
setSearchMatches([]);
|
|
94
|
+
setSearchActiveIndex(0);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const matches: number[] = [];
|
|
99
|
+
let match;
|
|
100
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
101
|
+
matches.push(match.index);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
setSearchMatches(matches);
|
|
105
|
+
setSearchActiveIndex(matches.length > 0 ? 1 : 0);
|
|
106
|
+
return matches.length;
|
|
107
|
+
},
|
|
108
|
+
[getContent]
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const handleNextMatch = useCallback(() => {
|
|
112
|
+
if (searchMatches.length === 0) return;
|
|
113
|
+
setSearchActiveIndex((prev) =>
|
|
114
|
+
prev >= searchMatches.length ? 1 : prev + 1
|
|
115
|
+
);
|
|
116
|
+
}, [searchMatches.length]);
|
|
117
|
+
|
|
118
|
+
const handlePreviousMatch = useCallback(() => {
|
|
119
|
+
if (searchMatches.length === 0) return;
|
|
120
|
+
setSearchActiveIndex((prev) =>
|
|
121
|
+
prev <= 1 ? searchMatches.length : prev - 1
|
|
122
|
+
);
|
|
123
|
+
}, [searchMatches.length]);
|
|
124
|
+
|
|
125
|
+
const handleReplace = useCallback(
|
|
126
|
+
(
|
|
127
|
+
replacement: string,
|
|
128
|
+
content: string,
|
|
129
|
+
setContent: (content: string) => void
|
|
130
|
+
) => {
|
|
131
|
+
if (searchMatches.length === 0) return;
|
|
132
|
+
|
|
133
|
+
const matchIndex = searchMatches[searchActiveIndex - 1];
|
|
134
|
+
if (matchIndex === undefined) return;
|
|
135
|
+
|
|
136
|
+
const beforeMatch = content.substring(0, matchIndex);
|
|
137
|
+
const afterMatch = content.substring(matchIndex + searchQuery.length);
|
|
138
|
+
const newContent = beforeMatch + replacement + afterMatch;
|
|
139
|
+
|
|
140
|
+
setContent(newContent);
|
|
141
|
+
|
|
142
|
+
// Re-run search to update matches
|
|
143
|
+
// Matches after the replacement point need to be adjusted
|
|
144
|
+
const adjustment = replacement.length - searchQuery.length;
|
|
145
|
+
const newMatches = searchMatches
|
|
146
|
+
.filter((_, i) => i !== searchActiveIndex - 1)
|
|
147
|
+
.map((pos, i) => {
|
|
148
|
+
if (i >= searchActiveIndex - 1) {
|
|
149
|
+
return pos + adjustment;
|
|
150
|
+
}
|
|
151
|
+
return pos;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
setSearchMatches(newMatches);
|
|
155
|
+
if (searchActiveIndex > newMatches.length) {
|
|
156
|
+
setSearchActiveIndex(newMatches.length > 0 ? 1 : 0);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
[searchMatches, searchActiveIndex, searchQuery]
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const handleReplaceAll = useCallback(
|
|
163
|
+
(
|
|
164
|
+
replacement: string,
|
|
165
|
+
content: string,
|
|
166
|
+
setContent: (content: string) => void
|
|
167
|
+
): number => {
|
|
168
|
+
if (searchMatches.length === 0) return 0;
|
|
169
|
+
|
|
170
|
+
let newContent = content;
|
|
171
|
+
let count = 0;
|
|
172
|
+
|
|
173
|
+
// Replace from end to start to preserve indices
|
|
174
|
+
[...searchMatches].reverse().forEach((index) => {
|
|
175
|
+
newContent =
|
|
176
|
+
newContent.substring(0, index) +
|
|
177
|
+
replacement +
|
|
178
|
+
newContent.substring(index + searchQuery.length);
|
|
179
|
+
count++;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
setContent(newContent);
|
|
183
|
+
setSearchMatches([]);
|
|
184
|
+
setSearchActiveIndex(0);
|
|
185
|
+
|
|
186
|
+
return count;
|
|
187
|
+
},
|
|
188
|
+
[searchMatches, searchQuery]
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
isSearchOpen,
|
|
193
|
+
openSearch,
|
|
194
|
+
closeSearch,
|
|
195
|
+
toggleSearch,
|
|
196
|
+
searchQuery,
|
|
197
|
+
searchMatches,
|
|
198
|
+
searchActiveIndex,
|
|
199
|
+
totalMatches: searchMatches.length,
|
|
200
|
+
handleFind,
|
|
201
|
+
handleNextMatch,
|
|
202
|
+
handlePreviousMatch,
|
|
203
|
+
handleReplace,
|
|
204
|
+
handleReplaceAll,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Version history hook for Writenex client
|
|
3
|
+
*
|
|
4
|
+
* Custom React hook for managing version history operations.
|
|
5
|
+
* Provides methods for listing, creating, restoring, and deleting versions.
|
|
6
|
+
*
|
|
7
|
+
* @module @writenex/astro/client/hooks/useVersionHistory
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState, useCallback, useMemo } from "react";
|
|
11
|
+
import type { VersionEntry, Version } from "../../types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Version history API client configuration
|
|
15
|
+
*/
|
|
16
|
+
interface VersionApiConfig {
|
|
17
|
+
apiBase: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Version API client type */
|
|
21
|
+
export type VersionApiClient = ReturnType<typeof createVersionApiClient>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Diff data returned from API
|
|
25
|
+
*/
|
|
26
|
+
export interface DiffData {
|
|
27
|
+
version: Version;
|
|
28
|
+
current: {
|
|
29
|
+
content: string;
|
|
30
|
+
frontmatter: Record<string, unknown>;
|
|
31
|
+
body: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create version history API client functions
|
|
37
|
+
*/
|
|
38
|
+
export function createVersionApiClient(config: VersionApiConfig) {
|
|
39
|
+
const { apiBase } = config;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
/**
|
|
43
|
+
* List all versions for a content item
|
|
44
|
+
*/
|
|
45
|
+
async listVersions(
|
|
46
|
+
collection: string,
|
|
47
|
+
contentId: string
|
|
48
|
+
): Promise<VersionEntry[]> {
|
|
49
|
+
const response = await fetch(
|
|
50
|
+
`${apiBase}/versions/${collection}/${contentId}`
|
|
51
|
+
);
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
throw new Error("Failed to fetch versions");
|
|
54
|
+
}
|
|
55
|
+
const data = await response.json();
|
|
56
|
+
return data.versions;
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get a specific version with full content
|
|
61
|
+
*/
|
|
62
|
+
async getVersion(
|
|
63
|
+
collection: string,
|
|
64
|
+
contentId: string,
|
|
65
|
+
versionId: string
|
|
66
|
+
): Promise<Version> {
|
|
67
|
+
const response = await fetch(
|
|
68
|
+
`${apiBase}/versions/${collection}/${contentId}/${versionId}`
|
|
69
|
+
);
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
if (response.status === 404) {
|
|
72
|
+
throw new Error("Version not found");
|
|
73
|
+
}
|
|
74
|
+
throw new Error("Failed to fetch version");
|
|
75
|
+
}
|
|
76
|
+
const data = await response.json();
|
|
77
|
+
return data.version;
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a manual version snapshot
|
|
82
|
+
*/
|
|
83
|
+
async createVersion(
|
|
84
|
+
collection: string,
|
|
85
|
+
contentId: string,
|
|
86
|
+
label?: string
|
|
87
|
+
): Promise<{ success: boolean; version?: VersionEntry; error?: string }> {
|
|
88
|
+
const response = await fetch(
|
|
89
|
+
`${apiBase}/versions/${collection}/${contentId}`,
|
|
90
|
+
{
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { "Content-Type": "application/json" },
|
|
93
|
+
body: JSON.stringify({ label }),
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
return response.json();
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Restore a version to current content
|
|
101
|
+
*/
|
|
102
|
+
async restoreVersion(
|
|
103
|
+
collection: string,
|
|
104
|
+
contentId: string,
|
|
105
|
+
versionId: string
|
|
106
|
+
): Promise<{
|
|
107
|
+
success: boolean;
|
|
108
|
+
content?: string;
|
|
109
|
+
safetySnapshot?: VersionEntry;
|
|
110
|
+
error?: string;
|
|
111
|
+
}> {
|
|
112
|
+
const response = await fetch(
|
|
113
|
+
`${apiBase}/versions/${collection}/${contentId}/${versionId}/restore`,
|
|
114
|
+
{ method: "POST" }
|
|
115
|
+
);
|
|
116
|
+
return response.json();
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get diff data between version and current content
|
|
121
|
+
*/
|
|
122
|
+
async getDiff(
|
|
123
|
+
collection: string,
|
|
124
|
+
contentId: string,
|
|
125
|
+
versionId: string
|
|
126
|
+
): Promise<DiffData> {
|
|
127
|
+
const response = await fetch(
|
|
128
|
+
`${apiBase}/versions/${collection}/${contentId}/${versionId}/diff`
|
|
129
|
+
);
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
if (response.status === 404) {
|
|
132
|
+
throw new Error("Version not found");
|
|
133
|
+
}
|
|
134
|
+
throw new Error("Failed to fetch diff data");
|
|
135
|
+
}
|
|
136
|
+
const data = await response.json();
|
|
137
|
+
return { version: data.version, current: data.current };
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Delete a specific version
|
|
142
|
+
*/
|
|
143
|
+
async deleteVersion(
|
|
144
|
+
collection: string,
|
|
145
|
+
contentId: string,
|
|
146
|
+
versionId: string
|
|
147
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
148
|
+
const response = await fetch(
|
|
149
|
+
`${apiBase}/versions/${collection}/${contentId}/${versionId}`,
|
|
150
|
+
{ method: "DELETE" }
|
|
151
|
+
);
|
|
152
|
+
return response.json();
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Clear all versions for a content item
|
|
157
|
+
*/
|
|
158
|
+
async clearVersions(
|
|
159
|
+
collection: string,
|
|
160
|
+
contentId: string
|
|
161
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
162
|
+
const response = await fetch(
|
|
163
|
+
`${apiBase}/versions/${collection}/${contentId}`,
|
|
164
|
+
{ method: "DELETE" }
|
|
165
|
+
);
|
|
166
|
+
return response.json();
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Hook state for version history
|
|
173
|
+
*/
|
|
174
|
+
export interface UseVersionHistoryState {
|
|
175
|
+
/** List of versions */
|
|
176
|
+
versions: VersionEntry[];
|
|
177
|
+
/** Loading state */
|
|
178
|
+
loading: boolean;
|
|
179
|
+
/** Error message if any */
|
|
180
|
+
error: string | null;
|
|
181
|
+
/** Currently selected version for viewing */
|
|
182
|
+
selectedVersion: Version | null;
|
|
183
|
+
/** Loading state for selected version */
|
|
184
|
+
loadingVersion: boolean;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Hook actions for version history
|
|
189
|
+
*/
|
|
190
|
+
export interface UseVersionHistoryActions {
|
|
191
|
+
/** Refresh the version list */
|
|
192
|
+
refresh: () => Promise<void>;
|
|
193
|
+
/** Get a specific version with full content */
|
|
194
|
+
getVersion: (versionId: string) => Promise<Version | null>;
|
|
195
|
+
/** Create a manual version snapshot */
|
|
196
|
+
createVersion: (label?: string) => Promise<boolean>;
|
|
197
|
+
/** Restore a version to current content */
|
|
198
|
+
restoreVersion: (versionId: string) => Promise<string | null>;
|
|
199
|
+
/** Delete a specific version */
|
|
200
|
+
deleteVersion: (versionId: string) => Promise<boolean>;
|
|
201
|
+
/** Clear all versions */
|
|
202
|
+
clearVersions: () => Promise<boolean>;
|
|
203
|
+
/** Get diff data between version and current */
|
|
204
|
+
getDiff: (versionId: string) => Promise<DiffData | null>;
|
|
205
|
+
/** Clear selected version */
|
|
206
|
+
clearSelectedVersion: () => void;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Hook for managing version history
|
|
211
|
+
*
|
|
212
|
+
* Provides state and actions for version history operations.
|
|
213
|
+
*
|
|
214
|
+
* @param apiBaseOrClient - Base URL for API calls or a pre-created version API client
|
|
215
|
+
* @param collection - Collection name
|
|
216
|
+
* @param contentId - Content ID (slug)
|
|
217
|
+
* @returns State and actions for version history
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```tsx
|
|
221
|
+
* const { versions, loading, refresh, restoreVersion } = useVersionHistory(
|
|
222
|
+
* apiBase,
|
|
223
|
+
* "blog",
|
|
224
|
+
* "my-post"
|
|
225
|
+
* );
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
export function useVersionHistory(
|
|
229
|
+
apiBaseOrClient: string | VersionApiClient,
|
|
230
|
+
collection: string | null,
|
|
231
|
+
contentId: string | null
|
|
232
|
+
): UseVersionHistoryState & UseVersionHistoryActions {
|
|
233
|
+
const client = useMemo(() => {
|
|
234
|
+
if (typeof apiBaseOrClient === "string") {
|
|
235
|
+
return createVersionApiClient({ apiBase: apiBaseOrClient });
|
|
236
|
+
}
|
|
237
|
+
return apiBaseOrClient;
|
|
238
|
+
}, [apiBaseOrClient]);
|
|
239
|
+
|
|
240
|
+
const [versions, setVersions] = useState<VersionEntry[]>([]);
|
|
241
|
+
const [loading, setLoading] = useState(false);
|
|
242
|
+
const [error, setError] = useState<string | null>(null);
|
|
243
|
+
const [selectedVersion, setSelectedVersion] = useState<Version | null>(null);
|
|
244
|
+
const [loadingVersion, setLoadingVersion] = useState(false);
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Refresh the version list
|
|
248
|
+
*/
|
|
249
|
+
const refresh = useCallback(async () => {
|
|
250
|
+
if (!collection || !contentId) {
|
|
251
|
+
setVersions([]);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
setLoading(true);
|
|
256
|
+
setError(null);
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const data = await client.listVersions(collection, contentId);
|
|
260
|
+
setVersions(data);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
setError(err instanceof Error ? err.message : "Failed to fetch versions");
|
|
263
|
+
setVersions([]);
|
|
264
|
+
} finally {
|
|
265
|
+
setLoading(false);
|
|
266
|
+
}
|
|
267
|
+
}, [client, collection, contentId]);
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get a specific version with full content
|
|
271
|
+
*/
|
|
272
|
+
const getVersion = useCallback(
|
|
273
|
+
async (versionId: string): Promise<Version | null> => {
|
|
274
|
+
if (!collection || !contentId) return null;
|
|
275
|
+
|
|
276
|
+
setLoadingVersion(true);
|
|
277
|
+
setError(null);
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const version = await client.getVersion(
|
|
281
|
+
collection,
|
|
282
|
+
contentId,
|
|
283
|
+
versionId
|
|
284
|
+
);
|
|
285
|
+
setSelectedVersion(version);
|
|
286
|
+
return version;
|
|
287
|
+
} catch (err) {
|
|
288
|
+
setError(
|
|
289
|
+
err instanceof Error ? err.message : "Failed to fetch version"
|
|
290
|
+
);
|
|
291
|
+
return null;
|
|
292
|
+
} finally {
|
|
293
|
+
setLoadingVersion(false);
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
[client, collection, contentId]
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Create a manual version snapshot
|
|
301
|
+
*/
|
|
302
|
+
const createVersion = useCallback(
|
|
303
|
+
async (label?: string): Promise<boolean> => {
|
|
304
|
+
if (!collection || !contentId) return false;
|
|
305
|
+
|
|
306
|
+
setError(null);
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const result = await client.createVersion(collection, contentId, label);
|
|
310
|
+
if (result.success) {
|
|
311
|
+
await refresh();
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
setError(result.error ?? "Failed to create version");
|
|
315
|
+
return false;
|
|
316
|
+
} catch (err) {
|
|
317
|
+
setError(
|
|
318
|
+
err instanceof Error ? err.message : "Failed to create version"
|
|
319
|
+
);
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
[client, collection, contentId, refresh]
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Restore a version to current content
|
|
328
|
+
*/
|
|
329
|
+
const restoreVersion = useCallback(
|
|
330
|
+
async (versionId: string): Promise<string | null> => {
|
|
331
|
+
if (!collection || !contentId) return null;
|
|
332
|
+
|
|
333
|
+
setError(null);
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const result = await client.restoreVersion(
|
|
337
|
+
collection,
|
|
338
|
+
contentId,
|
|
339
|
+
versionId
|
|
340
|
+
);
|
|
341
|
+
if (result.success && result.content) {
|
|
342
|
+
await refresh();
|
|
343
|
+
return result.content;
|
|
344
|
+
}
|
|
345
|
+
setError(result.error ?? "Failed to restore version");
|
|
346
|
+
return null;
|
|
347
|
+
} catch (err) {
|
|
348
|
+
setError(
|
|
349
|
+
err instanceof Error ? err.message : "Failed to restore version"
|
|
350
|
+
);
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
[client, collection, contentId, refresh]
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Delete a specific version
|
|
359
|
+
*/
|
|
360
|
+
const deleteVersion = useCallback(
|
|
361
|
+
async (versionId: string): Promise<boolean> => {
|
|
362
|
+
if (!collection || !contentId) return false;
|
|
363
|
+
|
|
364
|
+
setError(null);
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
const result = await client.deleteVersion(
|
|
368
|
+
collection,
|
|
369
|
+
contentId,
|
|
370
|
+
versionId
|
|
371
|
+
);
|
|
372
|
+
if (result.success) {
|
|
373
|
+
await refresh();
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
setError(result.error ?? "Failed to delete version");
|
|
377
|
+
return false;
|
|
378
|
+
} catch (err) {
|
|
379
|
+
setError(
|
|
380
|
+
err instanceof Error ? err.message : "Failed to delete version"
|
|
381
|
+
);
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
[client, collection, contentId, refresh]
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Clear all versions
|
|
390
|
+
*/
|
|
391
|
+
const clearVersions = useCallback(async (): Promise<boolean> => {
|
|
392
|
+
if (!collection || !contentId) return false;
|
|
393
|
+
|
|
394
|
+
setError(null);
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
const result = await client.clearVersions(collection, contentId);
|
|
398
|
+
if (result.success) {
|
|
399
|
+
setVersions([]);
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
setError(result.error ?? "Failed to clear versions");
|
|
403
|
+
return false;
|
|
404
|
+
} catch (err) {
|
|
405
|
+
setError(err instanceof Error ? err.message : "Failed to clear versions");
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
}, [client, collection, contentId]);
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Get diff data between version and current content
|
|
412
|
+
*/
|
|
413
|
+
const getDiff = useCallback(
|
|
414
|
+
async (versionId: string): Promise<DiffData | null> => {
|
|
415
|
+
if (!collection || !contentId) return null;
|
|
416
|
+
|
|
417
|
+
setError(null);
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
return await client.getDiff(collection, contentId, versionId);
|
|
421
|
+
} catch (err) {
|
|
422
|
+
setError(err instanceof Error ? err.message : "Failed to fetch diff");
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
[client, collection, contentId]
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Clear selected version
|
|
431
|
+
*/
|
|
432
|
+
const clearSelectedVersion = useCallback(() => {
|
|
433
|
+
setSelectedVersion(null);
|
|
434
|
+
}, []);
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
versions,
|
|
438
|
+
loading,
|
|
439
|
+
error,
|
|
440
|
+
selectedVersion,
|
|
441
|
+
loadingVersion,
|
|
442
|
+
refresh,
|
|
443
|
+
getVersion,
|
|
444
|
+
createVersion,
|
|
445
|
+
restoreVersion,
|
|
446
|
+
deleteVersion,
|
|
447
|
+
clearVersions,
|
|
448
|
+
getDiff,
|
|
449
|
+
clearSelectedVersion,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Client entry point for Writenex editor
|
|
3
|
+
*
|
|
4
|
+
* This is the main entry point for the React-based editor UI.
|
|
5
|
+
* It bootstraps the application and mounts it to the DOM.
|
|
6
|
+
*
|
|
7
|
+
* @module @writenex/astro/client
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { StrictMode } from "react";
|
|
11
|
+
import { createRoot } from "react-dom/client";
|
|
12
|
+
import { App } from "./App";
|
|
13
|
+
import { ThemeProvider } from "./context/ThemeContext";
|
|
14
|
+
import { ApiProvider } from "./context/ApiContext";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Configuration injected by the server
|
|
18
|
+
*/
|
|
19
|
+
declare global {
|
|
20
|
+
interface Window {
|
|
21
|
+
__WRITENEX_CONFIG__?: {
|
|
22
|
+
basePath: string;
|
|
23
|
+
apiBase: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the configuration from the window object
|
|
30
|
+
*/
|
|
31
|
+
function getConfig() {
|
|
32
|
+
return (
|
|
33
|
+
window.__WRITENEX_CONFIG__ ?? {
|
|
34
|
+
basePath: "/_writenex",
|
|
35
|
+
apiBase: "/_writenex/api",
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initialize and mount the application
|
|
42
|
+
*/
|
|
43
|
+
function mount() {
|
|
44
|
+
const container = document.getElementById("root");
|
|
45
|
+
|
|
46
|
+
if (!container) {
|
|
47
|
+
console.error("[writenex] Root element not found");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const config = getConfig();
|
|
52
|
+
const root = createRoot(container);
|
|
53
|
+
|
|
54
|
+
root.render(
|
|
55
|
+
<StrictMode>
|
|
56
|
+
<ThemeProvider>
|
|
57
|
+
<ApiProvider apiBase={config.apiBase}>
|
|
58
|
+
<App />
|
|
59
|
+
</ApiProvider>
|
|
60
|
+
</ThemeProvider>
|
|
61
|
+
</StrictMode>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Mount when DOM is ready
|
|
66
|
+
if (document.readyState === "loading") {
|
|
67
|
+
document.addEventListener("DOMContentLoaded", mount);
|
|
68
|
+
} else {
|
|
69
|
+
mount();
|
|
70
|
+
}
|