@tscircuit/fake-snippets 0.0.71 → 0.0.72
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/bun-tests/fake-snippets-api/routes/package_files/create_or_update.test.ts +26 -0
- package/bun.lock +30 -46
- package/dist/bundle.js +4 -4
- package/fake-snippets-api/routes/api/package_files/create_or_update.ts +3 -3
- package/package.json +7 -7
- package/src/components/FileSidebar.tsx +111 -37
- package/src/components/package-port/CodeAndPreview.tsx +78 -267
- package/src/components/package-port/CodeEditor.tsx +29 -18
- package/src/components/package-port/CodeEditorHeader.tsx +7 -6
- package/src/components/ui/tree-view.tsx +3 -3
- package/src/hooks/useFileManagement.ts +257 -38
- package/src/hooks/usePackageFilesLoader.ts +2 -2
- package/src/hooks/useUpdatePackageFilesMutation.ts +50 -24
|
@@ -1,59 +1,278 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useMemo, useState, useCallback } from "react"
|
|
2
2
|
import { isValidFileName } from "@/lib/utils/isValidFileName"
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
DEFAULT_CODE,
|
|
5
|
+
generateRandomPackageName,
|
|
6
|
+
PackageFile,
|
|
6
7
|
} from "../components/package-port/CodeAndPreview"
|
|
8
|
+
import { Package } from "fake-snippets-api/lib/db/schema"
|
|
9
|
+
import {
|
|
10
|
+
usePackageFile,
|
|
11
|
+
usePackageFileById,
|
|
12
|
+
usePackageFiles,
|
|
13
|
+
} from "./use-package-files"
|
|
14
|
+
import { decodeUrlHashToText } from "@/lib/decodeUrlHashToText"
|
|
15
|
+
import { usePackageFilesLoader } from "./usePackageFilesLoader"
|
|
16
|
+
import { useGlobalStore } from "./use-global-store"
|
|
17
|
+
import { useToast } from "@/components/ViewPackagePage/hooks/use-toast"
|
|
18
|
+
import { useUpdatePackageFilesMutation } from "./useUpdatePackageFilesMutation"
|
|
19
|
+
import { useCreatePackageReleaseMutation } from "./use-create-package-release-mutation"
|
|
20
|
+
import { useCreatePackageMutation } from "./use-create-package-mutation"
|
|
21
|
+
import { findTargetFile } from "@/lib/utils/findTargetFile"
|
|
22
|
+
|
|
23
|
+
export interface ICreateFileProps {
|
|
24
|
+
newFileName: string
|
|
25
|
+
onError: (error: Error) => void
|
|
26
|
+
}
|
|
27
|
+
export interface ICreateFileResult {
|
|
28
|
+
newFileCreated: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface IDeleteFileResult {
|
|
32
|
+
fileDeleted: boolean
|
|
33
|
+
}
|
|
34
|
+
export interface IDeleteFileProps {
|
|
35
|
+
filename: string
|
|
36
|
+
onError: (error: Error) => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function useFileManagement({
|
|
40
|
+
templateCode,
|
|
41
|
+
currentPackage,
|
|
42
|
+
fileChoosen,
|
|
43
|
+
openNewPackageSaveDialog,
|
|
44
|
+
updateLastUpdated,
|
|
45
|
+
}: {
|
|
46
|
+
templateCode?: string
|
|
47
|
+
currentPackage?: Package
|
|
48
|
+
fileChoosen: string | null
|
|
49
|
+
openNewPackageSaveDialog: () => void
|
|
50
|
+
updateLastUpdated: () => void
|
|
51
|
+
}) {
|
|
52
|
+
const [localFiles, setLocalFiles] = useState<PackageFile[]>([])
|
|
53
|
+
const [initialFiles, setInitialFiles] = useState<PackageFile[]>([])
|
|
54
|
+
const [currentFile, setCurrentFile] = useState<string | null>(null)
|
|
55
|
+
const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
|
|
56
|
+
const loggedInUser = useGlobalStore((s) => s.session)
|
|
57
|
+
const { toast } = useToast()
|
|
58
|
+
const {
|
|
59
|
+
data: packageFilesWithContent,
|
|
60
|
+
isLoading: isLoadingPackageFilesWithContent,
|
|
61
|
+
} = usePackageFilesLoader(currentPackage)
|
|
62
|
+
const { data: packageFilesMeta, isLoading: isLoadingPackageFiles } =
|
|
63
|
+
usePackageFiles(currentPackage?.latest_package_release_id)
|
|
7
64
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
)
|
|
12
|
-
|
|
65
|
+
const initialCodeContent = useMemo(() => {
|
|
66
|
+
return (
|
|
67
|
+
templateCode ??
|
|
68
|
+
decodeUrlHashToText(window.location.toString()) ??
|
|
69
|
+
DEFAULT_CODE
|
|
70
|
+
)
|
|
71
|
+
}, [templateCode, currentPackage])
|
|
72
|
+
const manualEditsFileContent = useMemo(() => {
|
|
73
|
+
return (
|
|
74
|
+
localFiles?.find((file) => file.path === "manual-edits.json")?.content ||
|
|
75
|
+
"{}"
|
|
76
|
+
)
|
|
77
|
+
}, [localFiles])
|
|
78
|
+
|
|
79
|
+
const updatePackageFilesMutation = useUpdatePackageFilesMutation({
|
|
80
|
+
currentPackage,
|
|
81
|
+
localFiles,
|
|
82
|
+
initialFiles,
|
|
83
|
+
packageFilesMeta: packageFilesMeta || [],
|
|
84
|
+
})
|
|
85
|
+
const { mutate: createRelease, isLoading: isCreatingRelease } =
|
|
86
|
+
useCreatePackageReleaseMutation({
|
|
87
|
+
onSuccess: () => {
|
|
88
|
+
toast({
|
|
89
|
+
title: "Package released",
|
|
90
|
+
description: "Your package has been released successfully.",
|
|
91
|
+
})
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
const createPackageMutation = useCreatePackageMutation()
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!currentPackage || isLoadingPackageFilesWithContent) {
|
|
98
|
+
setLocalFiles([
|
|
99
|
+
{
|
|
100
|
+
path: "index.tsx",
|
|
101
|
+
content: initialCodeContent || "",
|
|
102
|
+
},
|
|
103
|
+
])
|
|
104
|
+
setInitialFiles([])
|
|
105
|
+
setCurrentFile("index.tsx")
|
|
106
|
+
return
|
|
107
|
+
} else {
|
|
108
|
+
const targetFile = findTargetFile(
|
|
109
|
+
packageFilesWithContent || [],
|
|
110
|
+
fileChoosen,
|
|
111
|
+
)
|
|
112
|
+
setLocalFiles(packageFilesWithContent || [])
|
|
113
|
+
setInitialFiles(packageFilesWithContent || [])
|
|
114
|
+
setCurrentFile(targetFile?.path || null)
|
|
115
|
+
}
|
|
116
|
+
}, [currentPackage, isLoadingPackageFilesWithContent])
|
|
117
|
+
|
|
118
|
+
const isLoading = useMemo(() => {
|
|
119
|
+
return (
|
|
120
|
+
isLoadingPackageFilesWithContent || isLoadingPackageFiles || !localFiles
|
|
121
|
+
)
|
|
122
|
+
}, [isLoadingPackageFilesWithContent, localFiles, isLoadingPackageFiles])
|
|
123
|
+
|
|
124
|
+
const fsMap = useMemo(() => {
|
|
125
|
+
const map = localFiles.reduce(
|
|
126
|
+
(acc, file) => {
|
|
127
|
+
acc[file.path] = file.content || ""
|
|
128
|
+
return acc
|
|
129
|
+
},
|
|
130
|
+
{} as Record<string, string>,
|
|
131
|
+
)
|
|
132
|
+
return map
|
|
133
|
+
}, [localFiles, manualEditsFileContent])
|
|
134
|
+
|
|
135
|
+
const onFileSelect = (fileName: string) => {
|
|
136
|
+
if (localFiles.some((file) => file.path === fileName)) {
|
|
137
|
+
setCurrentFile(fileName)
|
|
138
|
+
} else {
|
|
139
|
+
setCurrentFile(null)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const createFile = ({
|
|
13
144
|
newFileName,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
setNewFileName,
|
|
17
|
-
setIsCreatingFile,
|
|
18
|
-
}: CreateFileProps) => {
|
|
145
|
+
onError,
|
|
146
|
+
}: ICreateFileProps): ICreateFileResult => {
|
|
19
147
|
newFileName = newFileName.trim()
|
|
20
148
|
if (!newFileName) {
|
|
21
|
-
|
|
22
|
-
return
|
|
149
|
+
onError(new Error("File name cannot be empty"))
|
|
150
|
+
return {
|
|
151
|
+
newFileCreated: false,
|
|
152
|
+
}
|
|
23
153
|
}
|
|
24
154
|
if (!isValidFileName(newFileName)) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
155
|
+
onError(new Error("Invalid file name"))
|
|
156
|
+
return {
|
|
157
|
+
newFileCreated: false,
|
|
158
|
+
}
|
|
29
159
|
}
|
|
30
|
-
setErrorMessage("")
|
|
31
|
-
|
|
32
|
-
const fileExists = state.pkgFilesWithContent.some(
|
|
33
|
-
(file) => file.path === newFileName,
|
|
34
|
-
)
|
|
35
160
|
|
|
161
|
+
const fileExists = localFiles?.some((file) => file.path === newFileName)
|
|
36
162
|
if (fileExists) {
|
|
37
|
-
|
|
38
|
-
return
|
|
163
|
+
onError(new Error("File already exists"))
|
|
164
|
+
return {
|
|
165
|
+
newFileCreated: false,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const updatedFiles = [
|
|
169
|
+
...(localFiles || []),
|
|
170
|
+
{ path: newFileName, content: "" },
|
|
171
|
+
]
|
|
172
|
+
setLocalFiles(updatedFiles)
|
|
173
|
+
onFileSelect(newFileName)
|
|
174
|
+
return {
|
|
175
|
+
newFileCreated: true,
|
|
39
176
|
}
|
|
177
|
+
}
|
|
40
178
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
179
|
+
const deleteFile = ({
|
|
180
|
+
filename,
|
|
181
|
+
onError,
|
|
182
|
+
}: IDeleteFileProps): IDeleteFileResult => {
|
|
183
|
+
const fileExists = localFiles?.some((file) => file.path === filename)
|
|
184
|
+
if (!fileExists) {
|
|
185
|
+
onError(new Error("File does not exist"))
|
|
46
186
|
return {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
187
|
+
fileDeleted: false,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const updatedFiles = localFiles.filter((file) => file.path !== filename)
|
|
191
|
+
setLocalFiles(updatedFiles)
|
|
192
|
+
onFileSelect(updatedFiles[0]?.path || "")
|
|
193
|
+
return {
|
|
194
|
+
fileDeleted: true,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const savePackage = async (isPrivate: boolean) => {
|
|
199
|
+
if (!isLoggedIn) {
|
|
200
|
+
toast({
|
|
201
|
+
title: "Not Logged In",
|
|
202
|
+
description: "You must be logged in to save your package.",
|
|
203
|
+
})
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const newPackage = await createPackageMutation.mutateAsync({
|
|
208
|
+
name: `${loggedInUser?.github_username}/${generateRandomPackageName()}`,
|
|
209
|
+
is_private: isPrivate,
|
|
50
210
|
})
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
211
|
+
|
|
212
|
+
if (newPackage) {
|
|
213
|
+
createRelease(
|
|
214
|
+
{
|
|
215
|
+
package_name_with_version: `${newPackage.name}@latest`,
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
onSuccess: () => {
|
|
219
|
+
updatePackageFilesMutation.mutate({
|
|
220
|
+
package_name_with_version: `${newPackage.name}@latest`,
|
|
221
|
+
...newPackage,
|
|
222
|
+
})
|
|
223
|
+
updateLastUpdated()
|
|
224
|
+
setInitialFiles([...localFiles])
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
updateLastUpdated()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const saveFiles = () => {
|
|
233
|
+
if (!isLoggedIn) {
|
|
234
|
+
toast({
|
|
235
|
+
title: "Not Logged In",
|
|
236
|
+
description: "You must be logged in to save your package.",
|
|
237
|
+
})
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
if (!currentPackage) {
|
|
241
|
+
openNewPackageSaveDialog()
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
updatePackageFilesMutation.mutate({
|
|
245
|
+
package_name_with_version: `${currentPackage.name}@latest`,
|
|
246
|
+
...currentPackage,
|
|
247
|
+
})
|
|
248
|
+
updateLastUpdated()
|
|
249
|
+
setInitialFiles([...localFiles])
|
|
54
250
|
}
|
|
55
251
|
|
|
252
|
+
const isSaving = useMemo(() => {
|
|
253
|
+
return (
|
|
254
|
+
updatePackageFilesMutation.isLoading ||
|
|
255
|
+
createPackageMutation.isLoading ||
|
|
256
|
+
isCreatingRelease
|
|
257
|
+
)
|
|
258
|
+
}, [
|
|
259
|
+
updatePackageFilesMutation.isLoading,
|
|
260
|
+
createPackageMutation.isLoading,
|
|
261
|
+
isCreatingRelease,
|
|
262
|
+
])
|
|
263
|
+
|
|
56
264
|
return {
|
|
57
|
-
|
|
265
|
+
fsMap,
|
|
266
|
+
createFile,
|
|
267
|
+
deleteFile,
|
|
268
|
+
saveFiles,
|
|
269
|
+
localFiles,
|
|
270
|
+
initialFiles,
|
|
271
|
+
currentFile,
|
|
272
|
+
setLocalFiles,
|
|
273
|
+
onFileSelect,
|
|
274
|
+
isLoading,
|
|
275
|
+
isSaving,
|
|
276
|
+
savePackage,
|
|
58
277
|
}
|
|
59
278
|
}
|
|
@@ -33,8 +33,8 @@ export function usePackageFilesLoader(pkg?: Package) {
|
|
|
33
33
|
const response = await axios.post(`/package_files/get`, {
|
|
34
34
|
package_file_id: file.package_file_id,
|
|
35
35
|
})
|
|
36
|
-
const content = response.data.package_file?.content_text
|
|
37
|
-
return
|
|
36
|
+
const content = response.data.package_file?.content_text
|
|
37
|
+
return { path: file.file_path, content: content ?? "" }
|
|
38
38
|
},
|
|
39
39
|
staleTime: 2,
|
|
40
40
|
})) ?? [],
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useMutation } from "react-query"
|
|
2
2
|
import type { Package } from "fake-snippets-api/lib/db/schema"
|
|
3
|
+
import { useAxios } from "./use-axios"
|
|
4
|
+
import { useToast } from "@/components/ViewPackagePage/hooks/use-toast"
|
|
3
5
|
|
|
4
6
|
interface PackageFile {
|
|
5
7
|
path: string
|
|
@@ -7,47 +9,49 @@ interface PackageFile {
|
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
interface UseUpdatePackageFilesMutationProps {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
currentPackage: Package | undefined
|
|
13
|
+
localFiles: PackageFile[]
|
|
14
|
+
initialFiles: PackageFile[]
|
|
15
|
+
packageFilesMeta: {
|
|
16
|
+
created_at: string
|
|
17
|
+
file_path: string
|
|
18
|
+
package_file_id: string
|
|
19
|
+
package_release_id: string
|
|
20
|
+
}[]
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
export function useUpdatePackageFilesMutation({
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
axios,
|
|
24
|
-
toast,
|
|
24
|
+
currentPackage,
|
|
25
|
+
localFiles,
|
|
26
|
+
initialFiles,
|
|
27
|
+
packageFilesMeta,
|
|
25
28
|
}: UseUpdatePackageFilesMutationProps) {
|
|
29
|
+
const axios = useAxios()
|
|
30
|
+
const { toast } = useToast()
|
|
26
31
|
return useMutation({
|
|
27
32
|
mutationFn: async (
|
|
28
|
-
|
|
33
|
+
newPackage: Pick<Package, "package_id" | "name"> & {
|
|
29
34
|
package_name_with_version: string
|
|
30
35
|
},
|
|
31
36
|
) => {
|
|
32
|
-
if (
|
|
33
|
-
|
|
37
|
+
if (currentPackage) {
|
|
38
|
+
newPackage = { ...currentPackage, ...newPackage }
|
|
34
39
|
}
|
|
35
|
-
if (!
|
|
40
|
+
if (!newPackage) throw new Error("No package to update")
|
|
36
41
|
|
|
37
42
|
let updatedFilesCount = 0
|
|
38
43
|
|
|
39
|
-
for (const file of
|
|
40
|
-
const initialFile =
|
|
41
|
-
if (file.content
|
|
44
|
+
for (const file of localFiles) {
|
|
45
|
+
const initialFile = initialFiles.find((x) => x.path === file.path)
|
|
46
|
+
if (file.content !== initialFile?.content) {
|
|
42
47
|
const updatePkgFilePayload = {
|
|
43
48
|
package_file_id:
|
|
44
|
-
|
|
49
|
+
packageFilesMeta.find((x) => x.file_path === file.path)
|
|
45
50
|
?.package_file_id ?? null,
|
|
46
51
|
content_text: file.content,
|
|
47
52
|
file_path: file.path,
|
|
48
|
-
package_name_with_version: `${
|
|
53
|
+
package_name_with_version: `${newPackage.name}`,
|
|
49
54
|
}
|
|
50
|
-
|
|
51
55
|
const response = await axios.post(
|
|
52
56
|
"/package_files/create_or_update",
|
|
53
57
|
updatePkgFilePayload,
|
|
@@ -58,10 +62,33 @@ export function useUpdatePackageFilesMutation({
|
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
64
|
}
|
|
65
|
+
|
|
66
|
+
for (const initialFile of initialFiles) {
|
|
67
|
+
const fileStillExists = localFiles.some(
|
|
68
|
+
(x) => x.path === initialFile.path,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if (!fileStillExists) {
|
|
72
|
+
const fileToDelete = packageFilesMeta.find(
|
|
73
|
+
(x) => x.file_path === initialFile.path,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if (fileToDelete?.package_file_id) {
|
|
77
|
+
const response = await axios.post("/package_files/delete", {
|
|
78
|
+
package_name_with_version: `${newPackage.name}`,
|
|
79
|
+
file_path: initialFile.path,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
if (response.status === 200) {
|
|
83
|
+
updatedFilesCount++
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
61
88
|
return updatedFilesCount
|
|
62
89
|
},
|
|
63
90
|
onSuccess: (updatedFilesCount) => {
|
|
64
|
-
if (updatedFilesCount) {
|
|
91
|
+
if (updatedFilesCount > 0) {
|
|
65
92
|
toast({
|
|
66
93
|
title: `Package's ${updatedFilesCount} files saved`,
|
|
67
94
|
description: "Your changes have been saved successfully.",
|
|
@@ -69,7 +96,6 @@ export function useUpdatePackageFilesMutation({
|
|
|
69
96
|
}
|
|
70
97
|
},
|
|
71
98
|
onError: (error: any) => {
|
|
72
|
-
console.error("Error updating pkg files:", error)
|
|
73
99
|
toast({
|
|
74
100
|
title: "Error",
|
|
75
101
|
description:
|