@tscircuit/fake-snippets 0.0.107 → 0.0.109
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 +82 -22
- package/biome.json +7 -1
- package/bun-tests/fake-snippets-api/routes/package_builds/get.test.ts +0 -15
- package/bun-tests/fake-snippets-api/routes/package_builds/list.test.ts +0 -12
- package/bun.lock +62 -19
- package/dist/bundle.js +25 -24
- package/dist/index.d.ts +26 -15
- package/dist/index.js +19 -18
- package/dist/schema.d.ts +32 -24
- package/dist/schema.js +7 -6
- package/fake-snippets-api/lib/db/db-client.ts +10 -1
- package/fake-snippets-api/lib/db/schema.ts +4 -3
- package/fake-snippets-api/lib/db/seed.ts +6 -9
- package/fake-snippets-api/lib/public-mapping/public-map-package-build.ts +0 -3
- package/fake-snippets-api/lib/public-mapping/public-map-package-release.ts +3 -0
- package/package.json +7 -8
- package/src/App.tsx +12 -11
- package/src/components/DownloadButtonAndMenu.tsx +133 -35
- package/src/components/FileSidebar.tsx +45 -193
- package/src/components/Footer.tsx +0 -1
- package/src/components/HeaderLogin.tsx +1 -1
- package/src/components/HiddenFilesDropdown.tsx +0 -2
- package/src/components/PackageBreadcrumb.tsx +1 -1
- package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +0 -2
- package/src/components/PackageBuildsPage/build-preview-content.tsx +34 -5
- package/src/components/PackageCard.tsx +0 -1
- package/src/components/ViewPackagePage/components/ShikiCodeViewer.tsx +20 -11
- package/src/components/ViewPackagePage/components/important-files-view.tsx +75 -59
- package/src/components/ViewPackagePage/components/main-content-header.tsx +4 -4
- package/src/components/ViewPackagePage/components/main-content-view-selector.tsx +0 -1
- package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +1 -2
- package/src/components/ViewPackagePage/components/package-header.tsx +14 -17
- package/src/components/ViewPackagePage/components/preview-image-squares.tsx +0 -1
- package/src/components/ViewPackagePage/components/repo-page-content.tsx +21 -20
- package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +18 -2
- package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +1 -1
- package/src/components/ViewPackagePage/components/sidebar.tsx +0 -2
- package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +18 -17
- package/src/components/ViewPackagePage/components/theme-toggle.tsx +0 -2
- package/src/components/ViewPackagePage/hooks/use-toast.tsx +0 -1
- package/src/components/package-port/CodeAndPreview.tsx +23 -40
- package/src/components/package-port/CodeEditor.tsx +24 -1
- package/src/components/package-port/CodeEditorHeader.tsx +5 -2
- package/src/components/preview/BuildsList.tsx +20 -9
- package/src/components/preview/ConnectedPackagesList.tsx +73 -60
- package/src/components/preview/ConnectedRepoOverview.tsx +160 -154
- package/src/components/preview/PackageReleasesDashboard.tsx +41 -30
- package/src/components/preview/index.tsx +16 -153
- package/src/hooks/use-current-package-id.ts +5 -30
- package/src/hooks/use-current-package-info.ts +29 -5
- package/src/hooks/use-global-store.ts +1 -1
- package/src/hooks/useFileManagement.ts +153 -34
- package/src/hooks/useOptimizedPackageFilesLoader.ts +149 -0
- package/src/hooks/useUpdatePackageFilesMutation.ts +2 -0
- package/src/index.css +24 -0
- package/src/lib/download-fns/download-circuit-png.ts +11 -3
- package/src/lib/download-fns/download-gltf-from-circuit-json.ts +44 -0
- package/src/lib/utils/isComponentExported.ts +9 -0
- package/src/lib/utils/transformFilesToTreeData.tsx +195 -0
- package/src/pages/404.tsx +3 -5
- package/src/pages/authorize.tsx +0 -2
- package/src/pages/landing.tsx +0 -1
- package/src/pages/preview-release.tsx +279 -0
- package/src/pages/release-builds.tsx +0 -8
- package/src/pages/release-detail.tsx +17 -15
- package/src/pages/releases.tsx +5 -1
- package/src/pages/view-package.tsx +14 -13
- package/src/components/Footer2.tsx +0 -100
- package/src/components/ShippingInformationForm.tsx +0 -423
- package/src/components/StaticViewSnippetHeader.tsx +0 -70
- package/src/components/ViewPackagePage/components/file-explorer.tsx +0 -67
- package/src/components/ViewPackagePage/components/readme-view.tsx +0 -58
- package/src/components/ViewPackagePage/components/repo-header-button.tsx +0 -36
- package/src/components/ViewPackagePage/components/repo-header.tsx +0 -4
- package/src/components/ViewPackagePage/components/sidebar-contributors-section.tsx +0 -31
- package/src/components/ViewSnippetHeader.tsx +0 -181
- package/src/components/ui/input-otp.tsx +0 -69
- package/src/hooks/use-snippets-base-api-url.ts +0 -3
- package/src/pages/preview-build.tsx +0 -380
- package/src/pages/settings.tsx +0 -25
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { useQuery, useQueries } from "react-query"
|
|
2
|
+
import { useAxios } from "@/hooks/use-axios"
|
|
3
|
+
import { usePackageFiles } from "@/hooks/use-package-files"
|
|
4
|
+
import { usePackageFileById } from "@/hooks/use-package-files"
|
|
5
|
+
import type { Package } from "fake-snippets-api/lib/db/schema"
|
|
6
|
+
import { useState, useMemo } from "react"
|
|
7
|
+
import { findTargetFile } from "@/lib/utils/findTargetFile"
|
|
8
|
+
|
|
9
|
+
export interface PackageFile {
|
|
10
|
+
path: string
|
|
11
|
+
content: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface OptimizedLoadingState {
|
|
15
|
+
priorityFile: PackageFile | null
|
|
16
|
+
allFiles: PackageFile[]
|
|
17
|
+
isPriorityLoading: boolean
|
|
18
|
+
areAllFilesLoading: boolean
|
|
19
|
+
error: any
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useOptimizedPackageFilesLoader(
|
|
23
|
+
pkg?: Package,
|
|
24
|
+
priorityFilePath?: string | null,
|
|
25
|
+
packageId?: string | null,
|
|
26
|
+
) {
|
|
27
|
+
const axios = useAxios()
|
|
28
|
+
const [loadedFiles, setLoadedFiles] = useState<Map<string, PackageFile>>(
|
|
29
|
+
new Map(),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const pkgFilesRaw = usePackageFiles(pkg?.latest_package_release_id)
|
|
33
|
+
|
|
34
|
+
// Filter out dist/ files to avoid downloading build artifacts
|
|
35
|
+
const pkgFiles = useMemo(
|
|
36
|
+
() => ({
|
|
37
|
+
...pkgFilesRaw,
|
|
38
|
+
data: pkgFilesRaw.data?.filter(
|
|
39
|
+
(file) => !file.file_path.startsWith("dist/"),
|
|
40
|
+
),
|
|
41
|
+
}),
|
|
42
|
+
[pkgFilesRaw.data, pkgFilesRaw.isLoading, pkgFilesRaw.error],
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const targetFilePath = useMemo(() => {
|
|
46
|
+
if (!pkgFiles.data) return priorityFilePath
|
|
47
|
+
|
|
48
|
+
if (priorityFilePath) {
|
|
49
|
+
const exactMatch = pkgFiles.data.find(
|
|
50
|
+
(f) => f.file_path === priorityFilePath,
|
|
51
|
+
)
|
|
52
|
+
if (exactMatch) return exactMatch.file_path
|
|
53
|
+
|
|
54
|
+
const partialMatch = pkgFiles.data.find(
|
|
55
|
+
(f) =>
|
|
56
|
+
f.file_path.includes(priorityFilePath) ||
|
|
57
|
+
priorityFilePath.includes(f.file_path),
|
|
58
|
+
)
|
|
59
|
+
if (partialMatch) return partialMatch.file_path
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check for index.tsx first
|
|
63
|
+
const indexFile = pkgFiles.data.find((f) => f.file_path === "index.tsx")
|
|
64
|
+
if (indexFile) return indexFile.file_path
|
|
65
|
+
|
|
66
|
+
// Fallback to first file
|
|
67
|
+
return pkgFiles.data[0]?.file_path || null
|
|
68
|
+
}, [pkgFiles.data, priorityFilePath])
|
|
69
|
+
|
|
70
|
+
const priorityFileData = pkgFiles.data?.find(
|
|
71
|
+
(file) => file.file_path === targetFilePath,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const priorityFileQuery = useQuery({
|
|
75
|
+
queryKey: ["priorityPackageFile", priorityFileData?.package_file_id],
|
|
76
|
+
queryFn: async () => {
|
|
77
|
+
if (!priorityFileData) return null
|
|
78
|
+
|
|
79
|
+
const response = await axios.post(`/package_files/get`, {
|
|
80
|
+
package_file_id: priorityFileData.package_file_id,
|
|
81
|
+
})
|
|
82
|
+
const content = response.data.package_file?.content_text
|
|
83
|
+
const file = { path: priorityFileData.file_path, content: content ?? "" }
|
|
84
|
+
|
|
85
|
+
setLoadedFiles((prev) => {
|
|
86
|
+
const newMap = new Map(prev)
|
|
87
|
+
newMap.set(file.path, file)
|
|
88
|
+
return newMap
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
return file
|
|
92
|
+
},
|
|
93
|
+
enabled: !!priorityFileData,
|
|
94
|
+
refetchOnWindowFocus: false,
|
|
95
|
+
refetchOnMount: false,
|
|
96
|
+
staleTime: Infinity,
|
|
97
|
+
cacheTime: Infinity,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const remainingFilesQueries = useQueries(
|
|
101
|
+
pkgFiles.data
|
|
102
|
+
?.filter((file) => file.file_path !== targetFilePath)
|
|
103
|
+
?.map((file) => ({
|
|
104
|
+
queryKey: ["packageFile", file.package_file_id],
|
|
105
|
+
queryFn: async () => {
|
|
106
|
+
const response = await axios.post(`/package_files/get`, {
|
|
107
|
+
package_file_id: file.package_file_id,
|
|
108
|
+
})
|
|
109
|
+
const content = response.data.package_file?.content_text
|
|
110
|
+
const fileData = { path: file.file_path, content: content ?? "" }
|
|
111
|
+
|
|
112
|
+
setLoadedFiles((prev) => {
|
|
113
|
+
const newMap = new Map(prev)
|
|
114
|
+
newMap.set(fileData.path, fileData)
|
|
115
|
+
return newMap
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
return fileData
|
|
119
|
+
},
|
|
120
|
+
refetchOnWindowFocus: false,
|
|
121
|
+
refetchOnMount: false,
|
|
122
|
+
staleTime: Infinity,
|
|
123
|
+
cacheTime: Infinity,
|
|
124
|
+
})) ?? [],
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const allFiles = useMemo(() => {
|
|
128
|
+
return Array.from(loadedFiles.values())
|
|
129
|
+
}, [loadedFiles])
|
|
130
|
+
|
|
131
|
+
const areAllFilesLoading =
|
|
132
|
+
remainingFilesQueries.some((q) => q.isLoading) ||
|
|
133
|
+
priorityFileQuery.isLoading
|
|
134
|
+
const error =
|
|
135
|
+
priorityFileQuery.error || remainingFilesQueries.find((q) => q.error)?.error
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
priorityFile: priorityFileQuery.data || null,
|
|
139
|
+
priorityFileFetched: priorityFileQuery.isFetched,
|
|
140
|
+
allFiles,
|
|
141
|
+
isPriorityLoading: priorityFileQuery.isLoading,
|
|
142
|
+
areAllFilesLoading,
|
|
143
|
+
error,
|
|
144
|
+
isMetaLoading: pkgFiles.isLoading,
|
|
145
|
+
totalFilesCount: pkgFiles.data?.length || 0,
|
|
146
|
+
loadedFilesCount: allFiles.length,
|
|
147
|
+
isPriorityFileFetched: priorityFileQuery.isFetched,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -2,6 +2,7 @@ import { useMutation } from "react-query"
|
|
|
2
2
|
import type { Package } from "fake-snippets-api/lib/db/schema"
|
|
3
3
|
import { useAxios } from "./use-axios"
|
|
4
4
|
import { useToast } from "@/components/ViewPackagePage/hooks/use-toast"
|
|
5
|
+
import { isHiddenFile } from "@/components/ViewPackagePage/utils/is-hidden-file"
|
|
5
6
|
|
|
6
7
|
interface PackageFile {
|
|
7
8
|
path: string
|
|
@@ -42,6 +43,7 @@ export function useUpdatePackageFilesMutation({
|
|
|
42
43
|
let updatedFilesCount = 0
|
|
43
44
|
|
|
44
45
|
for (const file of localFiles) {
|
|
46
|
+
if (isHiddenFile(file.path)) continue
|
|
45
47
|
const initialFile = initialFiles.find((x) => x.path === file.path)
|
|
46
48
|
if (file.content !== initialFile?.content) {
|
|
47
49
|
const updatePkgFilePayload = {
|
package/src/index.css
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
@tailwind base;
|
|
2
2
|
@tailwind components;
|
|
3
3
|
@tailwind utilities;
|
|
4
|
+
|
|
4
5
|
@layer base {
|
|
5
6
|
:root {
|
|
6
7
|
--radius: 0.5rem;
|
|
@@ -18,6 +19,10 @@
|
|
|
18
19
|
-ms-overflow-style: none; /* IE and Edge */
|
|
19
20
|
scrollbar-width: none; /* Firefox */
|
|
20
21
|
}
|
|
22
|
+
|
|
23
|
+
.shiki {
|
|
24
|
+
@apply no-scrollbar;
|
|
25
|
+
}
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
.shiki {
|
|
@@ -82,3 +87,22 @@
|
|
|
82
87
|
body {
|
|
83
88
|
overflow-x: hidden;
|
|
84
89
|
}
|
|
90
|
+
|
|
91
|
+
/* Subtle scrollbar styling for html and body */
|
|
92
|
+
html::-webkit-scrollbar,
|
|
93
|
+
body::-webkit-scrollbar {
|
|
94
|
+
width: 6px;
|
|
95
|
+
background-color: transparent;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
html::-webkit-scrollbar-thumb,
|
|
99
|
+
body::-webkit-scrollbar-thumb {
|
|
100
|
+
background-color: rgba(215, 215, 215, 0.564);
|
|
101
|
+
border-radius: 8px;
|
|
102
|
+
transition: background-color 0.2s;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
html::-webkit-scrollbar-thumb:hover,
|
|
106
|
+
body::-webkit-scrollbar-thumb:hover {
|
|
107
|
+
background-color: rgba(193, 193, 193, 0.455);
|
|
108
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { convertCircuitJsonToSimple3dSvg } from "circuit-json-to-simple-3d"
|
|
1
2
|
import { AnyCircuitElement } from "circuit-json"
|
|
2
3
|
import {
|
|
3
4
|
convertCircuitJsonToAssemblySvg,
|
|
@@ -6,7 +7,7 @@ import {
|
|
|
6
7
|
} from "circuit-to-svg"
|
|
7
8
|
import { saveAs } from "file-saver"
|
|
8
9
|
|
|
9
|
-
export type ImageFormat = "schematic" | "pcb" | "assembly"
|
|
10
|
+
export type ImageFormat = "schematic" | "pcb" | "assembly" | "3d"
|
|
10
11
|
|
|
11
12
|
interface DownloadCircuitPngOptions {
|
|
12
13
|
format: ImageFormat
|
|
@@ -65,7 +66,7 @@ export const downloadCircuitPng = async (
|
|
|
65
66
|
if (options.width) svgOptions.width = options.width
|
|
66
67
|
if (options.height) svgOptions.height = options.height
|
|
67
68
|
|
|
68
|
-
switch (options.format) {
|
|
69
|
+
switch (options.format.toLowerCase()) {
|
|
69
70
|
case "schematic":
|
|
70
71
|
svg = convertCircuitJsonToSchematicSvg(circuitJson, svgOptions)
|
|
71
72
|
break
|
|
@@ -76,13 +77,20 @@ export const downloadCircuitPng = async (
|
|
|
76
77
|
svg = convertCircuitJsonToAssemblySvg(circuitJson, svgOptions)
|
|
77
78
|
break
|
|
78
79
|
default:
|
|
79
|
-
|
|
80
|
+
svg = await convertCircuitJsonToSimple3dSvg(circuitJson, {
|
|
81
|
+
background: {
|
|
82
|
+
color: "#fff",
|
|
83
|
+
opacity: 0.0,
|
|
84
|
+
},
|
|
85
|
+
defaultZoomMultiplier: 1.1,
|
|
86
|
+
})
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
blob = await convertSvgToPng(svg)
|
|
83
90
|
const downloadFileName = `${fileName}_${options.format}.png`
|
|
84
91
|
saveAs(blob, downloadFileName)
|
|
85
92
|
} catch (error) {
|
|
93
|
+
console.error(error)
|
|
86
94
|
throw new Error(`Failed to download ${options.format} PNG: ${error}`)
|
|
87
95
|
}
|
|
88
96
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { CircuitJson } from "circuit-json"
|
|
2
|
+
import { saveAs } from "file-saver"
|
|
3
|
+
import {
|
|
4
|
+
convertCircuitJsonToGltf,
|
|
5
|
+
type ConversionOptions,
|
|
6
|
+
} from "circuit-json-to-gltf"
|
|
7
|
+
export const downloadGltfFromCircuitJson = async (
|
|
8
|
+
circuitJson: CircuitJson,
|
|
9
|
+
fileName: string,
|
|
10
|
+
options?: ConversionOptions,
|
|
11
|
+
) => {
|
|
12
|
+
const result = await convertCircuitJsonToGltf(circuitJson, options)
|
|
13
|
+
|
|
14
|
+
let blob: Blob
|
|
15
|
+
let extension = options?.format === "glb" ? ".glb" : ".gltf"
|
|
16
|
+
|
|
17
|
+
if (result instanceof ArrayBuffer) {
|
|
18
|
+
blob = new Blob([result], { type: "model/gltf-binary" })
|
|
19
|
+
extension = options?.format
|
|
20
|
+
? options.format === "glb"
|
|
21
|
+
? ".glb"
|
|
22
|
+
: ".gltf"
|
|
23
|
+
: ".glb"
|
|
24
|
+
} else if (
|
|
25
|
+
typeof ArrayBuffer !== "undefined" &&
|
|
26
|
+
result &&
|
|
27
|
+
typeof (result as any).buffer === "object" &&
|
|
28
|
+
(result as any).byteLength !== undefined
|
|
29
|
+
) {
|
|
30
|
+
const view = result as ArrayBufferView
|
|
31
|
+
blob = new Blob([view], { type: "model/gltf-binary" })
|
|
32
|
+
extension = options?.format
|
|
33
|
+
? options.format === "glb"
|
|
34
|
+
? ".glb"
|
|
35
|
+
: ".gltf"
|
|
36
|
+
: ".glb"
|
|
37
|
+
} else if (typeof result === "string") {
|
|
38
|
+
blob = new Blob([result], { type: "model/gltf+json" })
|
|
39
|
+
} else {
|
|
40
|
+
blob = new Blob([JSON.stringify(result)], { type: "model/gltf+json" })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
saveAs(blob, fileName + extension)
|
|
44
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const isComponentExported = (code: string) => {
|
|
2
|
+
return (
|
|
3
|
+
/export function\s+\w+/.test(code) ||
|
|
4
|
+
/export const\s+\w+\s*=/.test(code) ||
|
|
5
|
+
/export default\s+\w+/.test(code) ||
|
|
6
|
+
/export default\s+function\s*(\w*)\s*\(/.test(code) ||
|
|
7
|
+
/export default\s*\(\s*\)\s*=>/.test(code)
|
|
8
|
+
)
|
|
9
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { TreeDataItem } from "@/components/ui/tree-view"
|
|
2
|
+
import type {
|
|
3
|
+
IRenameFileProps,
|
|
4
|
+
IDeleteFileProps,
|
|
5
|
+
} from "@/hooks/useFileManagement"
|
|
6
|
+
import { isHiddenFile } from "@/components/ViewPackagePage/utils/is-hidden-file"
|
|
7
|
+
import { File, Folder, MoreVertical, Pencil, Trash2 } from "lucide-react"
|
|
8
|
+
import {
|
|
9
|
+
DropdownMenu,
|
|
10
|
+
DropdownMenuContent,
|
|
11
|
+
DropdownMenuGroup,
|
|
12
|
+
DropdownMenuItem,
|
|
13
|
+
DropdownMenuTrigger,
|
|
14
|
+
} from "@/components/ui/dropdown-menu"
|
|
15
|
+
import { useToast } from "@/hooks/use-toast"
|
|
16
|
+
|
|
17
|
+
type FileName = string
|
|
18
|
+
type TreeNode = Omit<TreeDataItem, "children"> & {
|
|
19
|
+
children?: Record<string, TreeNode>
|
|
20
|
+
}
|
|
21
|
+
interface TransformFilesToTreeDataProps {
|
|
22
|
+
files: Record<FileName, string>
|
|
23
|
+
currentFile: FileName | null
|
|
24
|
+
renamingFile: string | null
|
|
25
|
+
handleRenameFile: (props: IRenameFileProps) => { fileRenamed: boolean }
|
|
26
|
+
handleDeleteFile: (props: IDeleteFileProps) => { fileDeleted: boolean }
|
|
27
|
+
setRenamingFile: (filename: string | null) => void
|
|
28
|
+
onFileSelect: (filename: FileName) => void
|
|
29
|
+
onFolderSelect: (folderPath: string) => void
|
|
30
|
+
canModifyFiles: boolean
|
|
31
|
+
setErrorMessage: (message: string) => void
|
|
32
|
+
setSelectedFolderForCreation: (folder: string | null) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const transformFilesToTreeData = ({
|
|
36
|
+
files,
|
|
37
|
+
currentFile,
|
|
38
|
+
renamingFile,
|
|
39
|
+
handleRenameFile,
|
|
40
|
+
handleDeleteFile,
|
|
41
|
+
setRenamingFile,
|
|
42
|
+
onFileSelect,
|
|
43
|
+
onFolderSelect,
|
|
44
|
+
canModifyFiles,
|
|
45
|
+
setErrorMessage,
|
|
46
|
+
setSelectedFolderForCreation,
|
|
47
|
+
}: TransformFilesToTreeDataProps): TreeDataItem[] => {
|
|
48
|
+
const { toast } = useToast()
|
|
49
|
+
const root: Record<string, TreeNode> = {}
|
|
50
|
+
|
|
51
|
+
Object.keys(files).forEach((filePath) => {
|
|
52
|
+
const hasLeadingSlash = filePath.startsWith("/")
|
|
53
|
+
const pathSegments = (hasLeadingSlash ? filePath.slice(1) : filePath)
|
|
54
|
+
.trim()
|
|
55
|
+
.split("/")
|
|
56
|
+
let currentNode: Record<string, TreeNode> = root
|
|
57
|
+
|
|
58
|
+
pathSegments.forEach((segment, segmentIndex) => {
|
|
59
|
+
const isLeafNode = segmentIndex === pathSegments.length - 1
|
|
60
|
+
const ancestorPath = pathSegments.slice(0, segmentIndex).join("/")
|
|
61
|
+
const relativePath = ancestorPath ? `${ancestorPath}/${segment}` : segment
|
|
62
|
+
const absolutePath = hasLeadingSlash ? `/${relativePath}` : relativePath
|
|
63
|
+
const itemId = absolutePath
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
!currentNode[segment] &&
|
|
67
|
+
(!isHiddenFile(relativePath) ||
|
|
68
|
+
isHiddenFile(
|
|
69
|
+
currentFile?.startsWith("/")
|
|
70
|
+
? currentFile.slice(1)
|
|
71
|
+
: currentFile || "",
|
|
72
|
+
))
|
|
73
|
+
) {
|
|
74
|
+
currentNode[segment] = {
|
|
75
|
+
id: itemId,
|
|
76
|
+
name: segment,
|
|
77
|
+
isRenaming: renamingFile === itemId,
|
|
78
|
+
onRename: (newFilename: string) => {
|
|
79
|
+
const oldPath = itemId
|
|
80
|
+
const pathParts = oldPath.split("/").filter((part) => part !== "")
|
|
81
|
+
let newPath: string
|
|
82
|
+
|
|
83
|
+
if (pathParts.length > 1) {
|
|
84
|
+
const folderPath = pathParts.slice(0, -1).join("/")
|
|
85
|
+
newPath = folderPath + "/" + newFilename
|
|
86
|
+
} else {
|
|
87
|
+
newPath = newFilename
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (oldPath.startsWith("/") && !newPath.startsWith("/")) {
|
|
91
|
+
newPath = "/" + newPath
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { fileRenamed } = handleRenameFile({
|
|
95
|
+
oldFilename: itemId,
|
|
96
|
+
newFilename: newPath,
|
|
97
|
+
onError: (error) => {
|
|
98
|
+
toast({
|
|
99
|
+
title: `Error renaming file`,
|
|
100
|
+
description: error.message,
|
|
101
|
+
variant: "destructive",
|
|
102
|
+
})
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
if (fileRenamed) {
|
|
106
|
+
setRenamingFile(null)
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
onCancelRename: () => {
|
|
110
|
+
setRenamingFile(null)
|
|
111
|
+
},
|
|
112
|
+
icon: isLeafNode ? File : Folder,
|
|
113
|
+
onClick: isLeafNode
|
|
114
|
+
? () => {
|
|
115
|
+
onFileSelect(absolutePath)
|
|
116
|
+
setSelectedFolderForCreation(null)
|
|
117
|
+
}
|
|
118
|
+
: () => onFolderSelect(absolutePath),
|
|
119
|
+
draggable: false,
|
|
120
|
+
droppable: !isLeafNode,
|
|
121
|
+
children: isLeafNode ? undefined : {},
|
|
122
|
+
actions: canModifyFiles ? (
|
|
123
|
+
<>
|
|
124
|
+
<DropdownMenu key={itemId}>
|
|
125
|
+
<DropdownMenuTrigger asChild>
|
|
126
|
+
<MoreVertical className="w-4 h-4 text-gray-500 hover:text-gray-700" />
|
|
127
|
+
</DropdownMenuTrigger>
|
|
128
|
+
<DropdownMenuContent
|
|
129
|
+
className="w-fit bg-white shadow-lg rounded-md border-4 z-[100] border-white"
|
|
130
|
+
style={{
|
|
131
|
+
position: "absolute",
|
|
132
|
+
top: "100%",
|
|
133
|
+
left: "0",
|
|
134
|
+
marginTop: "0.5rem",
|
|
135
|
+
width: "8rem",
|
|
136
|
+
padding: "0.01rem",
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
<DropdownMenuGroup>
|
|
140
|
+
{isLeafNode && (
|
|
141
|
+
<DropdownMenuItem
|
|
142
|
+
onClick={() => {
|
|
143
|
+
setRenamingFile(itemId)
|
|
144
|
+
}}
|
|
145
|
+
className="flex items-center px-3 py-1 text-xs text-black hover:bg-gray-100 cursor-pointer"
|
|
146
|
+
>
|
|
147
|
+
<Pencil className="mr-2 h-3 w-3" />
|
|
148
|
+
Rename
|
|
149
|
+
</DropdownMenuItem>
|
|
150
|
+
)}
|
|
151
|
+
<DropdownMenuItem
|
|
152
|
+
onClick={() => {
|
|
153
|
+
const { fileDeleted } = handleDeleteFile({
|
|
154
|
+
filename: itemId,
|
|
155
|
+
onError: (error) => {
|
|
156
|
+
toast({
|
|
157
|
+
title: `Error deleting file ${itemId}`,
|
|
158
|
+
description: error.message,
|
|
159
|
+
})
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
if (fileDeleted) {
|
|
163
|
+
setErrorMessage("")
|
|
164
|
+
}
|
|
165
|
+
}}
|
|
166
|
+
className="flex items-center px-3 py-1 text-xs text-red-600 hover:bg-gray-100 cursor-pointer"
|
|
167
|
+
>
|
|
168
|
+
<Trash2 className="mr-2 h-3 w-3" />
|
|
169
|
+
Delete
|
|
170
|
+
</DropdownMenuItem>
|
|
171
|
+
</DropdownMenuGroup>
|
|
172
|
+
</DropdownMenuContent>
|
|
173
|
+
</DropdownMenu>
|
|
174
|
+
</>
|
|
175
|
+
) : undefined,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!isLeafNode && currentNode[segment]?.children) {
|
|
180
|
+
currentNode = currentNode[segment].children
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const convertToArray = (items: Record<string, TreeNode>): TreeDataItem[] => {
|
|
186
|
+
return Object.values(items).map((item) => ({
|
|
187
|
+
...item,
|
|
188
|
+
children: item.children ? convertToArray(item.children) : undefined,
|
|
189
|
+
}))
|
|
190
|
+
}
|
|
191
|
+
return convertToArray(root).filter((x) => {
|
|
192
|
+
if (x.children?.length === 0) return false
|
|
193
|
+
return true
|
|
194
|
+
})
|
|
195
|
+
}
|
package/src/pages/404.tsx
CHANGED
|
@@ -5,15 +5,13 @@ import { NotFound } from "@/components/NotFound"
|
|
|
5
5
|
|
|
6
6
|
export function NotFoundPage({
|
|
7
7
|
heading = "Page Not Found",
|
|
8
|
-
|
|
8
|
+
subtitle = "The page you're looking for doesn't exist.",
|
|
9
|
+
}: { heading?: string; subtitle?: string }) {
|
|
9
10
|
return (
|
|
10
11
|
<div className="flex min-h-screen flex-col">
|
|
11
12
|
<Helmet>
|
|
12
13
|
<title>404 - {heading} | tscircuit</title>
|
|
13
|
-
<meta
|
|
14
|
-
name="description"
|
|
15
|
-
content="The page you're looking for doesn't exist."
|
|
16
|
-
/>
|
|
14
|
+
<meta name="description" content={subtitle} />
|
|
17
15
|
</Helmet>
|
|
18
16
|
<Header2 />
|
|
19
17
|
<NotFound heading={heading} />
|
package/src/pages/authorize.tsx
CHANGED