@tscircuit/fake-snippets 0.0.70 → 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/api/generated-index.js +77 -3
- package/bun-tests/fake-snippets-api/routes/accounts/get.test.ts +53 -0
- 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 +17 -6
- package/fake-snippets-api/routes/api/accounts/get.ts +16 -3
- package/fake-snippets-api/routes/api/package_files/create_or_update.ts +3 -3
- package/index.html +3 -0
- package/package.json +7 -7
- package/src/ContextProviders.tsx +2 -0
- package/src/components/FileSidebar.tsx +111 -37
- package/src/components/HeaderLogin.tsx +2 -2
- 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
- package/src/lib/populate-query-cache-with-ssr-data.ts +52 -0
- package/src/pages/dashboard.tsx +2 -2
- package/src/pages/landing.tsx +14 -3
|
@@ -1,28 +1,18 @@
|
|
|
1
1
|
import { CodeEditor } from "@/components/package-port/CodeEditor"
|
|
2
2
|
import { usePackageVisibilitySettingsDialog } from "@/components/dialogs/package-visibility-settings-dialog"
|
|
3
|
-
import { useAxios } from "@/hooks/use-axios"
|
|
4
|
-
import { useGlobalStore } from "@/hooks/use-global-store"
|
|
5
3
|
import { useToast } from "@/hooks/use-toast"
|
|
6
4
|
import { useUrlParams } from "@/hooks/use-url-params"
|
|
7
5
|
import useWarnUserOnPageChange from "@/hooks/use-warn-user-on-page-change"
|
|
8
|
-
import { decodeUrlHashToText } from "@/lib/decodeUrlHashToText"
|
|
9
6
|
import { getSnippetTemplate } from "@/lib/get-snippet-template"
|
|
10
7
|
import { cn } from "@/lib/utils"
|
|
11
8
|
import type { Package } from "fake-snippets-api/lib/db/schema"
|
|
12
9
|
import { Loader2 } from "lucide-react"
|
|
13
|
-
import {
|
|
10
|
+
import { useMemo, useState } from "react"
|
|
14
11
|
import EditorNav from "@/components/package-port/EditorNav"
|
|
15
12
|
import { SuspenseRunFrame } from "../SuspenseRunFrame"
|
|
16
13
|
import { applyEditEventsToManualEditsFile } from "@tscircuit/core"
|
|
17
|
-
import { usePackageFileById, usePackageFiles } from "@/hooks/use-package-files"
|
|
18
|
-
import { useCreatePackageMutation } from "@/hooks/use-create-package-mutation"
|
|
19
|
-
import { useCreatePackageReleaseMutation } from "@/hooks/use-create-package-release-mutation"
|
|
20
|
-
import { useUpdatePackageFilesMutation } from "@/hooks/useUpdatePackageFilesMutation"
|
|
21
|
-
import { usePackageFilesLoader } from "@/hooks/usePackageFilesLoader"
|
|
22
|
-
import { findTargetFile } from "@/lib/utils/findTargetFile"
|
|
23
14
|
import { toastManualEditConflicts } from "@/lib/utils/toastManualEditConflicts"
|
|
24
15
|
import { ManualEditEvent } from "@tscircuit/props"
|
|
25
|
-
import { isValidFileName } from "@/lib/utils/isValidFileName"
|
|
26
16
|
import { useFileManagement } from "@/hooks/useFileManagement"
|
|
27
17
|
|
|
28
18
|
interface Props {
|
|
@@ -34,29 +24,17 @@ export interface PackageFile {
|
|
|
34
24
|
content: string
|
|
35
25
|
}
|
|
36
26
|
|
|
37
|
-
export interface CreateFileProps {
|
|
38
|
-
newFileName: string
|
|
39
|
-
setErrorMessage: (message: string) => void
|
|
40
|
-
onFileSelect: (fileName: string) => void
|
|
41
|
-
setNewFileName: (fileName: string) => void
|
|
42
|
-
setIsCreatingFile: (isCreatingFile: boolean) => void
|
|
43
|
-
}
|
|
44
|
-
|
|
45
27
|
export interface CodeAndPreviewState {
|
|
46
|
-
pkgFilesWithContent: PackageFile[]
|
|
47
|
-
initialFilesLoad: PackageFile[]
|
|
48
28
|
showPreview: boolean
|
|
49
29
|
fullScreen: boolean
|
|
50
30
|
lastSavedAt: number
|
|
51
31
|
circuitJson: null | any
|
|
52
32
|
isPrivate: boolean
|
|
53
33
|
lastRunCode: string
|
|
54
|
-
pkgFilesLoaded: boolean
|
|
55
|
-
currentFile: string
|
|
56
34
|
defaultComponentFile?: string
|
|
57
35
|
}
|
|
58
36
|
|
|
59
|
-
const DEFAULT_CODE = `
|
|
37
|
+
export const DEFAULT_CODE = `
|
|
60
38
|
export default () => (
|
|
61
39
|
<board width="10mm" height="10mm">
|
|
62
40
|
{/* write your code here! */}
|
|
@@ -65,236 +43,76 @@ export default () => (
|
|
|
65
43
|
`.trim()
|
|
66
44
|
|
|
67
45
|
export const generateRandomPackageName = () =>
|
|
68
|
-
`untitled-package-${Math.floor(Math.random() *
|
|
46
|
+
`untitled-package-${Math.floor(Math.random() * 900) + 100}`
|
|
69
47
|
|
|
70
48
|
export function CodeAndPreview({ pkg }: Props) {
|
|
71
49
|
const { toast } = useToast()
|
|
72
|
-
const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
|
|
73
|
-
const loggedInUser = useGlobalStore((s) => s.session)
|
|
74
50
|
const urlParams = useUrlParams()
|
|
75
51
|
|
|
76
52
|
const templateFromUrl = useMemo(
|
|
77
53
|
() => (urlParams.template ? getSnippetTemplate(urlParams.template) : null),
|
|
78
54
|
[urlParams.template],
|
|
79
55
|
)
|
|
80
|
-
|
|
81
|
-
const pkgFiles = usePackageFiles(pkg?.latest_package_release_id)
|
|
82
|
-
const indexFileFromHook = usePackageFileById(
|
|
83
|
-
pkgFiles.data?.find((x) => x.file_path === "index.tsx")?.package_file_id ??
|
|
84
|
-
null,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
const defaultCode = useMemo(() => {
|
|
88
|
-
if (indexFileFromHook.data?.content_text)
|
|
89
|
-
return indexFileFromHook.data.content_text
|
|
90
|
-
return (
|
|
91
|
-
templateFromUrl?.code ??
|
|
92
|
-
decodeUrlHashToText(window.location.toString()) ??
|
|
93
|
-
(urlParams.package_id ? "" : DEFAULT_CODE)
|
|
94
|
-
)
|
|
95
|
-
}, [indexFileFromHook.data, templateFromUrl, urlParams.package_id])
|
|
96
|
-
|
|
97
56
|
const [state, setState] = useState<CodeAndPreviewState>({
|
|
98
|
-
pkgFilesWithContent: !pkg
|
|
99
|
-
? [{ path: "index.tsx", content: defaultCode }]
|
|
100
|
-
: [],
|
|
101
|
-
initialFilesLoad: [],
|
|
102
57
|
showPreview: true,
|
|
103
58
|
fullScreen: false,
|
|
104
59
|
lastSavedAt: Date.now(),
|
|
105
60
|
circuitJson: null,
|
|
106
61
|
isPrivate: false,
|
|
107
|
-
lastRunCode:
|
|
108
|
-
pkgFilesLoaded: !urlParams.package_id,
|
|
109
|
-
currentFile: "",
|
|
62
|
+
lastRunCode: "",
|
|
110
63
|
})
|
|
111
64
|
|
|
112
|
-
const manualEditsFileContent = useMemo(() => {
|
|
113
|
-
return (
|
|
114
|
-
state.pkgFilesWithContent.find((x) => x.path === "manual-edits.json")
|
|
115
|
-
?.content ?? ""
|
|
116
|
-
)
|
|
117
|
-
}, [state.pkgFilesWithContent])
|
|
118
|
-
|
|
119
|
-
const entryPointCode = useMemo(() => {
|
|
120
|
-
const defaultComponentFile = findTargetFile(state.pkgFilesWithContent, null)
|
|
121
|
-
if (defaultComponentFile?.content) {
|
|
122
|
-
setState((prev) => ({
|
|
123
|
-
...prev,
|
|
124
|
-
defaultComponentFile: defaultComponentFile.path,
|
|
125
|
-
}))
|
|
126
|
-
return defaultComponentFile.content
|
|
127
|
-
}
|
|
128
|
-
return (
|
|
129
|
-
state.pkgFilesWithContent.find((x) => x.path === "index.tsx")?.content ??
|
|
130
|
-
defaultCode
|
|
131
|
-
)
|
|
132
|
-
}, [state.pkgFilesWithContent, defaultCode])
|
|
133
|
-
|
|
134
65
|
const packageType =
|
|
135
66
|
pkg?.snippet_type ?? templateFromUrl?.type ?? urlParams.snippet_type
|
|
136
67
|
|
|
137
68
|
const { Dialog: NewPackageSaveDialog, openDialog: openNewPackageSaveDialog } =
|
|
138
69
|
usePackageVisibilitySettingsDialog()
|
|
139
70
|
|
|
140
|
-
const {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
setState((prev) => ({
|
|
160
|
-
...prev,
|
|
161
|
-
pkgFilesWithContent: processedResults,
|
|
162
|
-
pkgFilesLoaded: true,
|
|
163
|
-
initialFilesLoad: processedResults,
|
|
164
|
-
lastRunCode:
|
|
165
|
-
processedResults.find((x) => x.path === "index.tsx")?.content ??
|
|
166
|
-
defaultCode,
|
|
167
|
-
}))
|
|
168
|
-
}
|
|
169
|
-
}, [isLoadingFiles, pkg, pkgFiles.data, defaultCode])
|
|
170
|
-
|
|
171
|
-
const createPackageMutation = useCreatePackageMutation()
|
|
172
|
-
const { mutate: createRelease } = useCreatePackageReleaseMutation({
|
|
173
|
-
onSuccess: () => {
|
|
174
|
-
toast({
|
|
175
|
-
title: "Package released",
|
|
176
|
-
description: "Your package has been released successfully.",
|
|
177
|
-
})
|
|
71
|
+
const {
|
|
72
|
+
savePackage,
|
|
73
|
+
isSaving,
|
|
74
|
+
currentFile,
|
|
75
|
+
fsMap,
|
|
76
|
+
isLoading,
|
|
77
|
+
createFile,
|
|
78
|
+
deleteFile,
|
|
79
|
+
onFileSelect,
|
|
80
|
+
saveFiles,
|
|
81
|
+
setLocalFiles,
|
|
82
|
+
localFiles,
|
|
83
|
+
initialFiles,
|
|
84
|
+
} = useFileManagement({
|
|
85
|
+
templateCode: templateFromUrl?.code,
|
|
86
|
+
currentPackage: pkg,
|
|
87
|
+
fileChoosen: urlParams.file_path ?? null,
|
|
88
|
+
openNewPackageSaveDialog,
|
|
89
|
+
updateLastUpdated: () => {
|
|
90
|
+
setState((prev) => ({ ...prev, lastSavedAt: Date.now() }))
|
|
178
91
|
},
|
|
179
92
|
})
|
|
180
93
|
|
|
181
|
-
const axios = useAxios()
|
|
182
|
-
|
|
183
|
-
const updatePackageFilesMutation = useUpdatePackageFilesMutation({
|
|
184
|
-
pkg,
|
|
185
|
-
pkgFilesWithContent: state.pkgFilesWithContent,
|
|
186
|
-
initialFilesLoad: state.initialFilesLoad,
|
|
187
|
-
pkgFiles,
|
|
188
|
-
axios,
|
|
189
|
-
toast,
|
|
190
|
-
})
|
|
191
|
-
|
|
192
94
|
const hasUnsavedChanges = useMemo(
|
|
193
95
|
() =>
|
|
194
|
-
!
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
[
|
|
203
|
-
state.pkgFilesWithContent,
|
|
204
|
-
state.initialFilesLoad,
|
|
205
|
-
updatePackageFilesMutation.isLoading,
|
|
206
|
-
state.lastSavedAt,
|
|
207
|
-
],
|
|
96
|
+
(!isSaving &&
|
|
97
|
+
Date.now() - state.lastSavedAt > 1000 &&
|
|
98
|
+
localFiles.some((file) => {
|
|
99
|
+
const initialFile = initialFiles.find((x) => x.path === file.path)
|
|
100
|
+
return initialFile?.content !== file.content
|
|
101
|
+
})) ||
|
|
102
|
+
localFiles.length !== initialFiles.length,
|
|
103
|
+
[localFiles, initialFiles, isSaving, state.lastSavedAt],
|
|
208
104
|
)
|
|
209
105
|
|
|
210
106
|
useWarnUserOnPageChange({ hasUnsavedChanges })
|
|
211
107
|
|
|
212
|
-
const handleNewPackageSaveRequest = async (isPrivate: boolean) => {
|
|
213
|
-
setState((prev) => ({ ...prev, lastSavedAt: Date.now() }))
|
|
214
|
-
const newPackage = await createPackageMutation.mutateAsync({
|
|
215
|
-
name: `${loggedInUser?.github_username}/${generateRandomPackageName()}`,
|
|
216
|
-
is_private: isPrivate,
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
if (newPackage) {
|
|
220
|
-
createRelease(
|
|
221
|
-
{
|
|
222
|
-
package_name_with_version: `${newPackage.name}@latest`,
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
onSuccess: () => {
|
|
226
|
-
updatePackageFilesMutation.mutate({
|
|
227
|
-
package_name_with_version: `${newPackage.name}@latest`,
|
|
228
|
-
...newPackage,
|
|
229
|
-
})
|
|
230
|
-
},
|
|
231
|
-
},
|
|
232
|
-
)
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const handleSave = async () => {
|
|
237
|
-
if (!isLoggedIn) {
|
|
238
|
-
toast({
|
|
239
|
-
title: "Not Logged In",
|
|
240
|
-
description: "You must be logged in to save your package.",
|
|
241
|
-
variant: "destructive",
|
|
242
|
-
})
|
|
243
|
-
return
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (!pkg) {
|
|
247
|
-
openNewPackageSaveDialog()
|
|
248
|
-
return
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
setState((prev) => ({ ...prev, lastSavedAt: Date.now() }))
|
|
252
|
-
|
|
253
|
-
if (pkg) {
|
|
254
|
-
updatePackageFilesMutation.mutate(
|
|
255
|
-
{
|
|
256
|
-
package_name_with_version: `${pkg.name}@latest`,
|
|
257
|
-
...pkg,
|
|
258
|
-
},
|
|
259
|
-
{
|
|
260
|
-
onSuccess: () => {
|
|
261
|
-
setState((prev) => ({
|
|
262
|
-
...prev,
|
|
263
|
-
initialFilesLoad: [...prev.pkgFilesWithContent],
|
|
264
|
-
}))
|
|
265
|
-
pkgFiles.refetch()
|
|
266
|
-
},
|
|
267
|
-
},
|
|
268
|
-
)
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
108
|
const currentFileCode = useMemo(
|
|
273
109
|
() =>
|
|
274
|
-
|
|
275
|
-
?.content ??
|
|
110
|
+
localFiles.find((x) => x.path === currentFile)?.content ??
|
|
276
111
|
state.defaultComponentFile ??
|
|
277
112
|
DEFAULT_CODE,
|
|
278
|
-
[
|
|
113
|
+
[localFiles, currentFile],
|
|
279
114
|
)
|
|
280
115
|
|
|
281
|
-
const fsMap = useMemo(() => {
|
|
282
|
-
return {
|
|
283
|
-
"manual-edits.json": manualEditsFileContent || "{}",
|
|
284
|
-
...state.pkgFilesWithContent.reduce(
|
|
285
|
-
(acc, file) => {
|
|
286
|
-
acc[file.path] = file.content
|
|
287
|
-
return acc
|
|
288
|
-
},
|
|
289
|
-
{} as Record<string, string>,
|
|
290
|
-
),
|
|
291
|
-
}
|
|
292
|
-
}, [
|
|
293
|
-
manualEditsFileContent,
|
|
294
|
-
entryPointCode,
|
|
295
|
-
packageType,
|
|
296
|
-
state.pkgFilesWithContent,
|
|
297
|
-
])
|
|
298
116
|
const mainComponentPath = useMemo(() => {
|
|
299
117
|
const isReactComponentExported =
|
|
300
118
|
/export function\s+\w+/.test(currentFileCode) ||
|
|
@@ -303,52 +121,50 @@ export function CodeAndPreview({ pkg }: Props) {
|
|
|
303
121
|
/export default\s+function\s*(\w*)\s*\(/.test(currentFileCode) ||
|
|
304
122
|
/export default\s*\(\s*\)\s*=>/.test(currentFileCode)
|
|
305
123
|
|
|
306
|
-
return (
|
|
307
|
-
|
|
308
|
-
!!state.pkgFilesWithContent.some((x) => x.path == state.currentFile) &&
|
|
124
|
+
return (currentFile?.endsWith(".tsx") || currentFile?.endsWith(".ts")) &&
|
|
125
|
+
!!localFiles.some((x) => x.path == currentFile) &&
|
|
309
126
|
isReactComponentExported
|
|
310
|
-
?
|
|
127
|
+
? currentFile
|
|
311
128
|
: state.defaultComponentFile
|
|
312
|
-
}, [
|
|
129
|
+
}, [currentFile, localFiles, currentFileCode])
|
|
313
130
|
|
|
314
131
|
const handleEditEvent = (event: ManualEditEvent) => {
|
|
315
|
-
const parsedManualEdits = JSON.parse(
|
|
132
|
+
const parsedManualEdits = JSON.parse(
|
|
133
|
+
localFiles.find((x) => x.path === "manual-edits.json")?.content || "{}",
|
|
134
|
+
)
|
|
316
135
|
const newManualEditsFileContent = applyEditEventsToManualEditsFile({
|
|
317
136
|
circuitJson: state.circuitJson,
|
|
318
137
|
editEvents: [event],
|
|
319
138
|
manualEditsFile: parsedManualEdits,
|
|
320
139
|
})
|
|
321
140
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const updatedFiles = [...prev.pkgFilesWithContent]
|
|
141
|
+
setLocalFiles(
|
|
142
|
+
(() => {
|
|
143
|
+
const manualEditsIndex = localFiles.findIndex(
|
|
144
|
+
(file) => file.path === "manual-edits.json",
|
|
145
|
+
)
|
|
328
146
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
147
|
+
const updatedFiles = [...localFiles]
|
|
148
|
+
|
|
149
|
+
if (manualEditsIndex !== -1) {
|
|
150
|
+
// Update existing manual-edits.json
|
|
151
|
+
updatedFiles[manualEditsIndex] = {
|
|
152
|
+
...updatedFiles[manualEditsIndex],
|
|
153
|
+
content: JSON.stringify(newManualEditsFileContent, null, 2),
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
// Add new manual-edits.json
|
|
157
|
+
updatedFiles.push({
|
|
158
|
+
path: "manual-edits.json",
|
|
159
|
+
content: JSON.stringify(newManualEditsFileContent, null, 2),
|
|
160
|
+
})
|
|
334
161
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
path: "manual-edits.json",
|
|
339
|
-
content: JSON.stringify(newManualEditsFileContent, null, 2),
|
|
340
|
-
})
|
|
341
|
-
}
|
|
342
|
-
return {
|
|
343
|
-
...prev,
|
|
344
|
-
pkgFilesWithContent: updatedFiles,
|
|
345
|
-
}
|
|
346
|
-
})
|
|
162
|
+
return updatedFiles
|
|
163
|
+
})(),
|
|
164
|
+
)
|
|
347
165
|
}
|
|
348
166
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if ((!pkg && urlParams.package_id) || pkgFiles.isLoading || isLoadingFiles) {
|
|
167
|
+
if ((!pkg && urlParams.package_id) || isLoading) {
|
|
352
168
|
return (
|
|
353
169
|
<div className="flex items-center justify-center h-64">
|
|
354
170
|
<div className="flex flex-col items-center justify-center">
|
|
@@ -366,9 +182,9 @@ export function CodeAndPreview({ pkg }: Props) {
|
|
|
366
182
|
pkg={pkg}
|
|
367
183
|
packageType={packageType}
|
|
368
184
|
code={String(currentFileCode)}
|
|
369
|
-
isSaving={
|
|
185
|
+
isSaving={isSaving}
|
|
370
186
|
hasUnsavedChanges={hasUnsavedChanges}
|
|
371
|
-
onSave={
|
|
187
|
+
onSave={saveFiles}
|
|
372
188
|
onTogglePreview={() =>
|
|
373
189
|
setState((prev) => ({ ...prev, showPreview: !prev.showPreview }))
|
|
374
190
|
}
|
|
@@ -384,24 +200,22 @@ export function CodeAndPreview({ pkg }: Props) {
|
|
|
384
200
|
)}
|
|
385
201
|
>
|
|
386
202
|
<CodeEditor
|
|
387
|
-
handleCreateFile={
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
392
|
-
files={state.pkgFilesWithContent}
|
|
203
|
+
handleCreateFile={createFile}
|
|
204
|
+
handleDeleteFile={deleteFile}
|
|
205
|
+
currentFile={currentFile}
|
|
206
|
+
onFileSelect={onFileSelect}
|
|
207
|
+
files={localFiles}
|
|
393
208
|
onCodeChange={(newCode, filename) => {
|
|
394
|
-
const targetFilename = filename ??
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
pkgFilesWithContent: prev.pkgFilesWithContent.map((file) =>
|
|
209
|
+
const targetFilename = filename ?? currentFile
|
|
210
|
+
setLocalFiles((prev) =>
|
|
211
|
+
prev.map((file) =>
|
|
398
212
|
file.path === targetFilename
|
|
399
213
|
? { ...file, content: newCode }
|
|
400
214
|
: file,
|
|
401
215
|
),
|
|
402
|
-
|
|
216
|
+
)
|
|
403
217
|
}}
|
|
404
|
-
pkgFilesLoaded={
|
|
218
|
+
pkgFilesLoaded={!isLoading}
|
|
405
219
|
/>
|
|
406
220
|
</div>
|
|
407
221
|
{state.showPreview && (
|
|
@@ -427,15 +241,12 @@ export function CodeAndPreview({ pkg }: Props) {
|
|
|
427
241
|
onEditEvent={(event) => {
|
|
428
242
|
handleEditEvent(event)
|
|
429
243
|
}}
|
|
430
|
-
fsMap={fsMap}
|
|
244
|
+
fsMap={fsMap ?? {}}
|
|
431
245
|
/>
|
|
432
246
|
</div>
|
|
433
247
|
)}
|
|
434
248
|
</div>
|
|
435
|
-
<NewPackageSaveDialog
|
|
436
|
-
initialIsPrivate={false}
|
|
437
|
-
onSave={handleNewPackageSaveRequest}
|
|
438
|
-
/>
|
|
249
|
+
<NewPackageSaveDialog initialIsPrivate={false} onSave={savePackage} />
|
|
439
250
|
</div>
|
|
440
251
|
)
|
|
441
252
|
}
|
|
@@ -24,12 +24,20 @@ import {
|
|
|
24
24
|
import { EditorView } from "codemirror"
|
|
25
25
|
import { useEffect, useMemo, useRef, useState } from "react"
|
|
26
26
|
import tsModule from "typescript"
|
|
27
|
-
import CodeEditorHeader
|
|
27
|
+
import CodeEditorHeader, {
|
|
28
|
+
FileName,
|
|
29
|
+
} from "@/components/package-port/CodeEditorHeader"
|
|
28
30
|
import { useCodeCompletionApi } from "@/hooks/use-code-completion-ai-api"
|
|
29
31
|
import FileSidebar from "../FileSidebar"
|
|
30
32
|
import { findTargetFile } from "@/lib/utils/findTargetFile"
|
|
31
|
-
import type {
|
|
33
|
+
import type { PackageFile } from "./CodeAndPreview"
|
|
32
34
|
import { useShikiHighlighter } from "@/hooks/use-shiki-highlighter"
|
|
35
|
+
import {
|
|
36
|
+
ICreateFileProps,
|
|
37
|
+
ICreateFileResult,
|
|
38
|
+
IDeleteFileProps,
|
|
39
|
+
IDeleteFileResult,
|
|
40
|
+
} from "@/hooks/useFileManagement"
|
|
33
41
|
|
|
34
42
|
const defaultImports = `
|
|
35
43
|
import React from "@types/react/jsx-runtime"
|
|
@@ -46,19 +54,21 @@ export const CodeEditor = ({
|
|
|
46
54
|
onFileContentChanged,
|
|
47
55
|
pkgFilesLoaded,
|
|
48
56
|
currentFile,
|
|
49
|
-
|
|
57
|
+
onFileSelect,
|
|
50
58
|
handleCreateFile,
|
|
59
|
+
handleDeleteFile,
|
|
51
60
|
}: {
|
|
52
61
|
onCodeChange: (code: string, filename?: string) => void
|
|
53
62
|
files: PackageFile[]
|
|
54
|
-
handleCreateFile: (props:
|
|
63
|
+
handleCreateFile: (props: ICreateFileProps) => ICreateFileResult
|
|
64
|
+
handleDeleteFile: (props: IDeleteFileProps) => IDeleteFileResult
|
|
55
65
|
readOnly?: boolean
|
|
56
66
|
isStreaming?: boolean
|
|
57
67
|
pkgFilesLoaded?: boolean
|
|
58
68
|
showImportAndFormatButtons?: boolean
|
|
59
69
|
onFileContentChanged?: (path: string, content: string) => void
|
|
60
|
-
currentFile: string
|
|
61
|
-
|
|
70
|
+
currentFile: string | null
|
|
71
|
+
onFileSelect: (path: string) => void
|
|
62
72
|
}) => {
|
|
63
73
|
const editorRef = useRef<HTMLDivElement>(null)
|
|
64
74
|
const viewRef = useRef<EditorView | null>(null)
|
|
@@ -85,7 +95,6 @@ export const CodeEditor = ({
|
|
|
85
95
|
if (files.length === 0 || !pkgFilesLoaded || currentFile) return
|
|
86
96
|
|
|
87
97
|
const targetFile = findTargetFile(files, filePathFromUrl)
|
|
88
|
-
|
|
89
98
|
if (targetFile) {
|
|
90
99
|
handleFileChange(targetFile.path)
|
|
91
100
|
setCode(targetFile.content)
|
|
@@ -229,7 +238,7 @@ export const CodeEditor = ({
|
|
|
229
238
|
// Set up base extensions
|
|
230
239
|
const baseExtensions = [
|
|
231
240
|
basicSetup,
|
|
232
|
-
currentFile
|
|
241
|
+
currentFile?.endsWith(".json")
|
|
233
242
|
? json()
|
|
234
243
|
: javascript({ typescript: true, jsx: true }),
|
|
235
244
|
keymap.of([indentWithTab]),
|
|
@@ -237,7 +246,7 @@ export const CodeEditor = ({
|
|
|
237
246
|
EditorView.updateListener.of((update) => {
|
|
238
247
|
if (update.docChanged) {
|
|
239
248
|
const newContent = update.state.doc.toString()
|
|
240
|
-
|
|
249
|
+
if (!currentFile) return
|
|
241
250
|
if (newContent === lastFilesEventContent[currentFile]) return
|
|
242
251
|
lastFilesEventContent[currentFile] = newContent
|
|
243
252
|
|
|
@@ -272,12 +281,12 @@ export const CodeEditor = ({
|
|
|
272
281
|
|
|
273
282
|
// Add TypeScript-specific extensions and handlers
|
|
274
283
|
const tsExtensions =
|
|
275
|
-
currentFile
|
|
284
|
+
currentFile?.endsWith(".tsx") || currentFile?.endsWith(".ts")
|
|
276
285
|
? [
|
|
277
286
|
tsFacet.of({
|
|
278
287
|
env,
|
|
279
|
-
path: currentFile
|
|
280
|
-
? currentFile
|
|
288
|
+
path: currentFile?.endsWith(".ts")
|
|
289
|
+
? currentFile?.replace(/\.ts$/, ".tsx")
|
|
281
290
|
: currentFile,
|
|
282
291
|
}),
|
|
283
292
|
tsSync(),
|
|
@@ -421,7 +430,7 @@ export const CodeEditor = ({
|
|
|
421
430
|
: []
|
|
422
431
|
|
|
423
432
|
const state = EditorState.create({
|
|
424
|
-
doc: fileMap[currentFile] || "",
|
|
433
|
+
doc: fileMap[currentFile || ""] || "",
|
|
425
434
|
extensions: [...baseExtensions, ...tsExtensions],
|
|
426
435
|
})
|
|
427
436
|
|
|
@@ -432,7 +441,7 @@ export const CodeEditor = ({
|
|
|
432
441
|
|
|
433
442
|
viewRef.current = view
|
|
434
443
|
|
|
435
|
-
if (currentFile
|
|
444
|
+
if (currentFile?.endsWith(".tsx") || currentFile?.endsWith(".ts")) {
|
|
436
445
|
ata(`${defaultImports}${code}`)
|
|
437
446
|
}
|
|
438
447
|
|
|
@@ -459,7 +468,7 @@ export const CodeEditor = ({
|
|
|
459
468
|
}
|
|
460
469
|
|
|
461
470
|
const updateEditorToMatchCurrentFile = () => {
|
|
462
|
-
const currentContent = fileMap[currentFile] || ""
|
|
471
|
+
const currentContent = fileMap[currentFile || ""] || ""
|
|
463
472
|
updateCurrentEditorContent(currentContent)
|
|
464
473
|
}
|
|
465
474
|
|
|
@@ -468,14 +477,14 @@ export const CodeEditor = ({
|
|
|
468
477
|
useEffect(() => {
|
|
469
478
|
if (
|
|
470
479
|
ataRef.current &&
|
|
471
|
-
(currentFile
|
|
480
|
+
(currentFile?.endsWith(".tsx") || currentFile?.endsWith(".ts"))
|
|
472
481
|
) {
|
|
473
482
|
ataRef.current(`${defaultImports}${code}`)
|
|
474
483
|
}
|
|
475
484
|
}, [codeImports])
|
|
476
485
|
|
|
477
486
|
const handleFileChange = (path: string) => {
|
|
478
|
-
|
|
487
|
+
onFileSelect(path)
|
|
479
488
|
try {
|
|
480
489
|
// Set url query to file path
|
|
481
490
|
const urlParams = new URLSearchParams(window.location.search)
|
|
@@ -484,7 +493,8 @@ export const CodeEditor = ({
|
|
|
484
493
|
} catch {}
|
|
485
494
|
}
|
|
486
495
|
|
|
487
|
-
const updateFileContent = (path:
|
|
496
|
+
const updateFileContent = (path: FileName | null, newContent: string) => {
|
|
497
|
+
if (!path) return
|
|
488
498
|
if (currentFile === path) {
|
|
489
499
|
setCode(newContent)
|
|
490
500
|
onCodeChange(newContent, path)
|
|
@@ -523,6 +533,7 @@ export const CodeEditor = ({
|
|
|
523
533
|
}
|
|
524
534
|
onFileSelect={handleFileChange}
|
|
525
535
|
handleCreateFile={handleCreateFile}
|
|
536
|
+
handleDeleteFile={handleDeleteFile}
|
|
526
537
|
/>
|
|
527
538
|
<div className="flex flex-col flex-1 w-full min-w-0 h-full">
|
|
528
539
|
{showImportAndFormatButtons && (
|
|
@@ -23,9 +23,9 @@ import { isHiddenFile } from "../ViewPackagePage/utils/is-hidden-file"
|
|
|
23
23
|
export type FileName = string
|
|
24
24
|
|
|
25
25
|
interface CodeEditorHeaderProps {
|
|
26
|
-
currentFile: FileName
|
|
26
|
+
currentFile: FileName | null
|
|
27
27
|
files: Record<FileName, string>
|
|
28
|
-
updateFileContent: (filename: FileName, content: string) => void
|
|
28
|
+
updateFileContent: (filename: FileName | null, content: string) => void
|
|
29
29
|
fileSidebarState: ReturnType<typeof useState<boolean>>
|
|
30
30
|
handleFileChange: (filename: FileName) => void
|
|
31
31
|
entrypointFileName?: string
|
|
@@ -46,6 +46,7 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
|
|
|
46
46
|
|
|
47
47
|
const handleFormatFile = useCallback(() => {
|
|
48
48
|
if (!window.prettier || !window.prettierPlugins) return
|
|
49
|
+
if (!currentFile) return
|
|
49
50
|
try {
|
|
50
51
|
const currentContent = files[currentFile]
|
|
51
52
|
let fileExtension = currentFile.split(".").pop()?.toLowerCase()
|
|
@@ -148,9 +149,9 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
|
|
|
148
149
|
</div>
|
|
149
150
|
</button>
|
|
150
151
|
<div>
|
|
151
|
-
<Select value={currentFile} onValueChange={handleFileChange}>
|
|
152
|
+
<Select value={currentFile || ""} onValueChange={handleFileChange}>
|
|
152
153
|
<SelectTrigger
|
|
153
|
-
className={`h-7 px-3 bg-white select-none transition-[margin] duration-300 ease-in-out ${
|
|
154
|
+
className={`h-7 w-24 px-3 bg-white select-none transition-[margin] duration-300 ease-in-out ${
|
|
154
155
|
sidebarOpen ? "-ml-2" : "-ml-1"
|
|
155
156
|
}`}
|
|
156
157
|
>
|
|
@@ -197,7 +198,7 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
|
|
|
197
198
|
</div>
|
|
198
199
|
|
|
199
200
|
<div className="flex items-center overflow-x-hidden gap-2 px-2 py-1 ml-auto">
|
|
200
|
-
{checkIfManualEditsImported(files, currentFile) && (
|
|
201
|
+
{checkIfManualEditsImported(files, currentFile || "") && (
|
|
201
202
|
<DropdownMenu>
|
|
202
203
|
<DropdownMenuTrigger asChild>
|
|
203
204
|
<Button
|
|
@@ -235,7 +236,7 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
|
|
|
235
236
|
</div>
|
|
236
237
|
<ImportSnippetDialog
|
|
237
238
|
onSnippetSelected={(snippet: any) => {
|
|
238
|
-
const newContent = `import {} from "@tsci/${snippet.owner_name}.${snippet.unscoped_name}"\n${files[currentFile]}`
|
|
239
|
+
const newContent = `import {} from "@tsci/${snippet.owner_name}.${snippet.unscoped_name}"\n${files[currentFile || ""]}`
|
|
239
240
|
updateFileContent(currentFile, newContent)
|
|
240
241
|
}}
|
|
241
242
|
/>
|
|
@@ -404,9 +404,9 @@ const TreeLeaf = React.forwardRef<
|
|
|
404
404
|
default={defaultLeafIcon}
|
|
405
405
|
/>
|
|
406
406
|
<span className="flex-grow text-sm truncate">{item.name}</span>
|
|
407
|
-
<
|
|
408
|
-
{item.actions}
|
|
409
|
-
</
|
|
407
|
+
<div onClick={(e) => e.stopPropagation()}>
|
|
408
|
+
<TreeActions isSelected={true}>{item.actions}</TreeActions>
|
|
409
|
+
</div>
|
|
410
410
|
</div>
|
|
411
411
|
)
|
|
412
412
|
},
|