Refactor projects to use indexeddb

This commit is contained in:
Misode
2024-11-13 05:29:06 +01:00
parent 26079d1188
commit 2366716cae
17 changed files with 265 additions and 501 deletions

View File

@@ -1,46 +1,56 @@
import type { DocAndNode } from '@spyglassmc/core'
import { useState } from 'preact/hooks'
import { Identifier } from 'deepslate'
import { useCallback, useState } from 'preact/hooks'
import type { Method } from '../../Analytics.js'
import { Analytics } from '../../Analytics.js'
import { useLocale, useProject } from '../../contexts/index.js'
import { safeJsonParse } from '../../Utils.js'
import type { ConfigGenerator } from '../../Config.js'
import { getProjectRoot, useLocale, useProject, useVersion } from '../../contexts/index.js'
import { useSpyglass } from '../../contexts/Spyglass.jsx'
import { genPath, message } from '../../Utils.js'
import { Btn } from '../Btn.js'
import { TextInput } from '../forms/index.js'
import { Modal } from '../Modal.js'
interface Props {
docAndNode: DocAndNode,
id: string,
method: string,
gen: ConfigGenerator,
method: Method,
onCreate: (uri: string) => void,
onClose: () => void,
}
export function FileCreation({ docAndNode, id, method, onClose }: Props) {
export function FileCreation({ docAndNode, gen, method, onCreate, onClose }: Props) {
const { locale } = useLocale()
const { projects, project, updateFile } = useProject()
const [fileId, setFileId] = useState(id === 'pack_mcmeta' ? 'pack' : '')
const [error, setError] = useState<string>()
const { version } = useVersion()
const { project } = useProject()
const { client } = useSpyglass()
const [fileId, setFileId] = useState(gen.id === 'pack_mcmeta' ? 'pack' : '')
const [error, setError] = useState<string>()
const changeFileId = (str: string) => {
setError(undefined)
setFileId(str)
}
const doSave = () => {
const doSave = useCallback(() => {
if (!fileId.match(/^([a-z0-9_.-]+:)?[a-z0-9/_.-]+$/)) {
setError('Invalid resource location')
return
}
Analytics.saveProjectFile(id, projects.length, project.files.length, method as any)
const id = Identifier.parse(fileId.includes(':') || project.namespace === undefined ? fileId : `${project.namespace}:${fileId}`)
const uri = `${getProjectRoot(project)}data/${id.namespace}/${genPath(gen, version)}/${id.path}.json`
Analytics.saveProjectFile(method)
const text = docAndNode.doc.getText()
const data = safeJsonParse(text)
if (data !== undefined) {
updateFile(id, undefined, { type: id, id: fileId, data })
}
onClose()
}
client.fs.writeFile(uri, text).then(() => {
onCreate(uri)
}).catch((e) => {
setError(message(e))
})
}, [version, project, client, fileId ])
return <Modal class="file-modal" onDismiss={onClose}>
<p>{locale('project.save_current_file')}</p>
<TextInput autofocus={id !== 'pack_mcmeta'} class="btn btn-input" value={fileId} onChange={changeFileId} onEnter={doSave} onCancel={onClose} placeholder={locale('resource_location')} spellcheck={false} readOnly={id === 'pack_mcmeta'} />
<TextInput autofocus={gen.id !== 'pack_mcmeta'} class="btn btn-input" value={fileId} onChange={changeFileId} onEnter={doSave} onCancel={onClose} placeholder={locale('resource_location')} spellcheck={false} readOnly={gen.id === 'pack_mcmeta'} />
{error !== undefined && <span class="invalid">{error}</span>}
<Btn icon="file" label={locale('project.save')} onClick={doSave} />
</Modal>

View File

@@ -1,19 +1,17 @@
import { useState } from 'preact/hooks'
import { Analytics } from '../../Analytics.js'
import { useLocale, useProject } from '../../contexts/index.js'
import { useLocale } from '../../contexts/index.js'
import { Btn } from '../Btn.js'
import { TextInput } from '../forms/index.js'
import { Modal } from '../Modal.js'
interface Props {
id: string,
name: string,
uri: string,
onClose: () => void,
}
export function FileRenaming({ id, name, onClose }: Props) {
export function FileRenaming({ uri, onClose }: Props) {
const { locale } = useLocale()
const { projects, project, updateFile } = useProject()
const [fileId, setFileId] = useState(name)
const [fileId, setFileId] = useState(uri) // TODO: get original file id
const [error, setError] = useState<string>()
const changeFileId = (str: string) => {
@@ -26,8 +24,8 @@ export function FileRenaming({ id, name, onClose }: Props) {
setError('Invalid resource location')
return
}
Analytics.renameProjectFile(id, projects.length, project.files.length, 'menu')
updateFile(id, name, { type: id, id: fileId })
Analytics.renameProjectFile('menu')
// TODO: rename file
onClose()
}

View File

@@ -1,10 +1,11 @@
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
import { useCallback, useMemo, useState } from 'preact/hooks'
import config from '../../Config.js'
import type { Project } from '../../contexts/index.js'
import { disectFilePath, useLocale, useProject } from '../../contexts/index.js'
import { useLocale, useProject } from '../../contexts/index.js'
import { useSpyglass } from '../../contexts/Spyglass.jsx'
import type { VersionId } from '../../services/index.js'
import { DEFAULT_VERSION, parseSource } from '../../services/index.js'
import { message, readZip } from '../../Utils.js'
import { DEFAULT_VERSION } from '../../services/index.js'
import { PROJECTS_URI } from '../../services/Spyglass.js'
import { hexId, readZip } from '../../Utils.js'
import { Btn, BtnMenu, FileUpload, Octicon, TextInput } from '../index.js'
import { Modal } from '../Modal.js'
@@ -13,7 +14,8 @@ interface Props {
}
export function ProjectCreation({ onClose }: Props) {
const { locale } = useLocale()
const { projects, createProject, changeProject, updateProject } = useProject()
const { projects, createProject, changeProject } = useProject()
const { client } = useSpyglass()
const [name, setName] = useState('')
const [namespace, setNamespace] = useState('')
@@ -32,36 +34,17 @@ export function ProjectCreation({ onClose }: Props) {
}
}
const projectUpdater = useRef(updateProject)
useEffect(() => {
projectUpdater.current = updateProject
}, [updateProject])
const onCreate = () => {
const onCreate = useCallback(() => {
setCreating(true)
createProject(name, namespace || undefined, version)
const rootUri = `${PROJECTS_URI}${hexId()}/`
createProject({ name, namespace, version, storage: { type: 'indexeddb', rootUri } })
changeProject(name)
if (file) {
readZip(file).then(async (entries) => {
const project: Partial<Project> = { files: [] }
await Promise.all(entries.map(async (entry) => {
const file = disectFilePath(entry[0], version)
if (file) {
try {
const text = await parseSource(entry[1], 'json')
const data = JSON.parse(text)
project.files!.push({ ...file, data })
return
} catch (e) {
console.warn(`Failed parsing ${file.type} ${file.id}: ${message(e)}`)
}
}
if (project.unknownFiles === undefined) {
project.unknownFiles = []
}
project.unknownFiles.push({ path: entry[0], data: entry[1] })
await Promise.all(entries.map((entry) => {
const path = entry[0].startsWith('/') ? entry[0].slice(1) : entry[0]
return client.fs.writeFile(rootUri + path, entry[1])
}))
projectUpdater.current(project)
onClose()
}).catch(() => {
onClose()
@@ -69,7 +52,7 @@ export function ProjectCreation({ onClose }: Props) {
} else {
onClose()
}
}
}, [createProject, changeProject, client, version, name, namespace, file])
const invalidName = useMemo(() => {
return projects.map(p => p.name.trim().toLowerCase()).includes(name.trim().toLowerCase())

View File

@@ -1,3 +1,4 @@
import { useCallback } from 'preact/hooks'
import { Analytics } from '../../Analytics.js'
import { useLocale, useProject } from '../../contexts/index.js'
import { Btn } from '../Btn.js'
@@ -8,13 +9,13 @@ interface Props {
}
export function ProjectDeletion({ onClose }: Props) {
const { locale } = useLocale()
const { projects, project, deleteProject } = useProject()
const { project, deleteProject } = useProject()
const doSave = () => {
Analytics.deleteProject(projects.length, project.files.length, 'menu')
const doSave = useCallback(() => {
Analytics.deleteProject('menu')
deleteProject(project.name)
onClose()
}
}, [onClose, deleteProject])
return <Modal class="file-modal" onDismiss={onClose}>
<p>{locale('project.delete_confirm.1', project.name)}</p>

View File

@@ -1,11 +1,11 @@
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
import { Analytics } from '../../Analytics.js'
import { route } from 'preact-router'
import { useCallback, useMemo, useRef } from 'preact/hooks'
import config from '../../Config.js'
import { disectFilePath, DRAFT_PROJECT, getFilePath, useLocale, useProject, useVersion } from '../../contexts/index.js'
import { DRAFT_PROJECT, getProjectRoot, useLocale, useProject } from '../../contexts/index.js'
import { useSpyglass } from '../../contexts/Spyglass.jsx'
import { useAsync } from '../../hooks/useAsync.js'
import { useFocus } from '../../hooks/useFocus.js'
import { stringifySource } from '../../services/index.js'
import { Store } from '../../Store.js'
import { writeZip } from '../../Utils.js'
import { cleanUrl, writeZip } from '../../Utils.js'
import { Btn } from '../Btn.js'
import { BtnMenu } from '../BtnMenu.js'
import { Octicon } from '../Octicon.jsx'
@@ -13,73 +13,35 @@ import type { TreeViewGroupRenderer, TreeViewLeafRenderer } from '../TreeView.js
import { TreeView } from '../TreeView.js'
interface Props {
onError: (message: string) => unknown,
onRename: (file: { type: string, id: string }) => unknown,
onCreate: () => unknown,
onDeleteProject: () => unknown,
onError: (message: string) => void,
onRename: (uri: string) => void,
onCreateProject: () => void,
onDeleteProject: () => void,
}
export function ProjectPanel({ onRename, onCreate, onDeleteProject }: Props) {
export function ProjectPanel({ onRename, onCreateProject, onDeleteProject}: Props) {
const { locale } = useLocale()
const { version } = useVersion()
const { projects, project, changeProject, file, openFile, updateFile } = useProject()
const { projects, project, projectUri, setProjectUri, changeProject } = useProject()
const { client, service } = useSpyglass()
const [treeViewMode, setTreeViewMode] = useState(Store.getTreeViewMode())
const projectRoot = getProjectRoot(project)
const changeTreeViewMode = useCallback((mode: string) => {
Store.setTreeViewMode(mode)
Analytics.setTreeViewMode(mode)
setTreeViewMode(mode)
}, [])
const disectEntry = useCallback((entry: string) => {
if (treeViewMode === 'resources' && entry !== 'pack.mcmeta') {
const [type, id] = entry.split('/')
return {
type: type.replaceAll('\u2215', '/'),
id: id.replaceAll('\u2215', '/'),
}
}
return disectFilePath(entry, version)
}, [treeViewMode, version])
const entries = useMemo(() => project.files.flatMap(f => {
const path = getFilePath(f, version)
if (!path) return []
if (f.type === 'pack_mcmeta') return 'pack.mcmeta'
if (treeViewMode === 'resources') {
return [`${f.type.replaceAll('/', '\u2215')}/${f.id.replaceAll('/', '\u2215')}`]
}
return [path]
}), [treeViewMode, version, ...project.files])
const selected = useMemo(() => file && getFilePath(file, version), [file, version])
const selectFile = useCallback((entry: string) => {
const file = disectEntry(entry)
if (file) {
openFile(file.type, file.id)
}
}, [disectEntry])
const { value: entries } = useAsync(async () => {
const entries = await client.fs.readdir(projectRoot)
return entries.flatMap(e => {
return e.name.startsWith(projectRoot) ? [e.name.slice(projectRoot.length)] : []
})
}, [project])
const download = useRef<HTMLAnchorElement>(null)
const onDownload = async () => {
if (!download.current) return
let hasPack = false
const entries = project.files.flatMap(file => {
const path = getFilePath(file, version)
if (path === undefined) return []
if (path === 'pack.mcmeta') hasPack = true
return [[path, stringifySource(JSON.stringify(file.data))]] as [string, string][]
})
project.unknownFiles?.forEach(({ path, data }) => {
entries.push([path, data])
})
if (!hasPack) {
const pack_format = config.versions.find(v => v.id === version)!.pack_format
entries.push(['pack.mcmeta', stringifySource(JSON.stringify({ pack: { pack_format, description: '' } }, null, 2))])
}
const url = await writeZip(entries)
if (!download.current || entries === undefined) return
const zipEntries = await Promise.all(entries.map(async e => {
const data = await client.fs.readFile(projectRoot + e)
const text = new TextDecoder().decode(data)
return [e, text] as [string, string]
}))
const url = await writeZip(zipEntries)
download.current.setAttribute('href', url)
download.current.setAttribute('download', `${project.name.replaceAll(' ', '_')}.zip`)
download.current.click()
@@ -89,25 +51,20 @@ export function ProjectPanel({ onRename, onCreate, onDeleteProject }: Props) {
{
icon: 'pencil',
label: locale('project.rename_file'),
onAction: (entry: string) => {
const file = disectEntry(entry)
if (file) {
onRename(file)
}
onAction: (uri: string) => {
onRename(uri)
},
},
{
icon: 'trashcan',
label: locale('project.delete_file'),
onAction: (entry: string) => {
const file = disectEntry(entry)
if (file) {
Analytics.deleteProjectFile(file.type, projects.length, project.files.length, 'menu')
updateFile(file.type, file.id, {})
}
onAction: (uri: string) => {
client.fs.unlink(uri).then(() => {
setProjectUri(undefined)
})
},
},
], [disectEntry, updateFile, onRename])
], [client, onRename, projectRoot])
const FolderEntry: TreeViewGroupRenderer = useCallback(({ name, open, onClick }) => {
return <div class="entry" onClick={onClick} >
@@ -118,23 +75,34 @@ export function ProjectPanel({ onRename, onCreate, onDeleteProject }: Props) {
const FileEntry: TreeViewLeafRenderer<string> = useCallback(({ entry }) => {
const [focused, setFocus] = useFocus()
const uri = projectRoot + entry
const onContextMenu = (evt: MouseEvent) => {
evt.preventDefault()
setFocus()
}
const file = disectEntry(entry)
const onClick = () => {
const category = uri.endsWith('/pack.mcmeta')
? 'pack_mcmeta'
: service?.dissectUri(uri)?.category
const gen = config.generators.find(g => g.id === category)
if (!gen) {
throw new Error(`Cannot find generator for uri ${uri}`)
}
route(cleanUrl(gen.url))
setProjectUri(uri)
}
return <div class={`entry ${file && getFilePath(file, version) === selected ? 'active' : ''} ${focused ? 'focused' : ''}`} onClick={() => selectFile(entry)} onContextMenu={onContextMenu} >
return <div class={`entry ${uri === projectUri ? 'active' : ''} ${focused ? 'focused' : ''}`} onClick={onClick} onContextMenu={onContextMenu} >
{Octicon.file}
<span>{entry.split('/').at(-1)}</span>
{focused && <div class="entry-menu">
{actions?.map(a => <div class="action [&>svg]:inline" onClick={e => { a.onAction(entry); e.stopPropagation(); setFocus(false) }}>
{actions?.map(a => <div class="action [&>svg]:inline" onClick={e => { a.onAction(uri); e.stopPropagation(); setFocus(false) }}>
{(Octicon as any)[a.icon]}
<span>{a.label}</span>
</div>)}
</div>}
</div>
}, [actions, disectEntry])
}, [service, actions, projectRoot, projectUri])
return <>
<div class="project-controls">
@@ -143,15 +111,16 @@ export function ProjectPanel({ onRename, onCreate, onDeleteProject }: Props) {
</BtnMenu>
<BtnMenu icon="kebab_horizontal" >
<Btn icon="file_zip" label={locale('project.download')} onClick={onDownload} />
<Btn icon="plus_circle" label={locale('project.new')} onClick={onCreate} />
<Btn icon={treeViewMode === 'resources' ? 'three_bars' : 'rows'} label={locale(treeViewMode === 'resources' ? 'project.show_file_paths' : 'project.show_resources')} onClick={() => changeTreeViewMode(treeViewMode === 'resources' ? 'files' : 'resources')} />
<Btn icon="plus_circle" label={locale('project.new')} onClick={onCreateProject} />
{project.name !== DRAFT_PROJECT.name && <Btn icon="trashcan" label={locale('project.delete')} onClick={onDeleteProject} />}
</BtnMenu>
</div>
<div class="file-view">
{entries.length === 0
? <span>{locale('project.no_files')}</span>
: <TreeView entries={entries} split={path => path.split('/')} group={FolderEntry} leaf={FileEntry} />}
{entries === undefined
? <></>
: entries.length === 0
? <span>{locale('project.no_files')}</span>
: <TreeView entries={entries} split={path => path.split('/')} group={FolderEntry} leaf={FileEntry} />}
</div>
<a ref={download} style="display: none;"></a>
</>

View File

@@ -1,5 +1,6 @@
import { route } from 'preact-router'
import { useCallback, useEffect, useErrorBoundary, useMemo, useRef, useState } from 'preact/hooks'
import type { Method } from '../../Analytics.js'
import { Analytics } from '../../Analytics.js'
import type { ConfigGenerator } from '../../Config.js'
import config from '../../Config.js'
@@ -8,8 +9,9 @@ import { useSpyglass, watchSpyglassUri } from '../../contexts/Spyglass.jsx'
import { AsyncCancel, useActiveTimeout, useAsync, useSearchParam } from '../../hooks/index.js'
import type { VersionId } from '../../services/index.js'
import { checkVersion, fetchDependencyMcdoc, fetchPreset, fetchRegistries, getSnippet, shareSnippet } from '../../services/index.js'
import { DEPENDENCY_URI } from '../../services/Spyglass.js'
import { Store } from '../../Store.js'
import { cleanUrl, genPath, safeJsonParse } from '../../Utils.js'
import { cleanUrl, genPath } from '../../Utils.js'
import { Ad, Btn, BtnMenu, ErrorPanel, FileCreation, FileRenaming, Footer, HasPreview, Octicon, PreviewPanel, ProjectCreation, ProjectDeletion, ProjectPanel, SearchList, SourcePanel, TextInput, Tree, VersionSwitcher } from '../index.js'
import { getRootDefault } from './McdocHelpers.js'
@@ -23,7 +25,7 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
const { locale } = useLocale()
const { version, changeVersion, changeTargetVersion } = useVersion()
const { service } = useSpyglass()
const { projects, project, file, updateProject, updateFile, closeFile } = useProject()
const { project, projectUri, setProjectUri, updateProject } = useProject()
const [error, setError] = useState<Error | string | null>(null)
const [errorBoundary, errorRetry] = useErrorBoundary()
if (errorBoundary) {
@@ -34,9 +36,21 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
useEffect(() => Store.visitGenerator(gen.id), [gen.id])
const uri = useMemo(() => {
// TODO: return different uri when project file is open
return service?.getUnsavedFileUri(gen)
}, [service, version, gen])
if (!service) {
return undefined
}
if (projectUri) {
const category = projectUri.endsWith('/pack.mcmeta')
? 'pack_mcmeta'
: service.dissectUri(projectUri)?.category
if (category === gen.id) {
return projectUri
} else {
setProjectUri(undefined)
}
}
return service.getUnsavedFileUri(gen)
}, [service, version, gen, projectUri])
const [currentPreset, setCurrentPreset] = useSearchParam('preset')
const [sharedSnippetId, setSharedSnippetId] = useSearchParam(SHARE_KEY)
@@ -75,20 +89,13 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
Analytics.openSnippet(gen.id, sharedSnippetId, version)
ignoreChange.current = true
text = snippet.text
} else if (file) {
if (project.version && project.version !== version) {
changeVersion(project.version, false)
return AsyncCancel
}
ignoreChange.current = true
text = JSON.stringify(file.data, null, 2)
}
if (!service || !uri) {
return AsyncCancel
}
if (gen.dependency) {
const dependency = await fetchDependencyMcdoc(gen.dependency)
const dependencyUri = `file:///project/mcdoc/${gen.dependency}.mcdoc`
const dependencyUri = `${DEPENDENCY_URI}${gen.dependency}.mcdoc`
await service.writeFile(dependencyUri, dependency)
}
if (text !== undefined) {
@@ -104,24 +111,18 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
const docAndNode = await service.openFile(uri)
Analytics.setGenerator(gen.id)
return docAndNode
}, [gen.id, version, sharedSnippetId, currentPreset, project.name, file?.id, service])
}, [gen.id, version, sharedSnippetId, currentPreset, project.name, service, uri])
const { doc } = docAndNode ?? {}
watchSpyglassUri(uri, ({ doc }) => {
watchSpyglassUri(uri, () => {
if (!ignoreChange.current) {
setCurrentPreset(undefined, true)
setSharedSnippetId(undefined, true)
}
if (file) {
const data = safeJsonParse(doc.getText())
if (data !== undefined) {
updateFile(gen.id, file.id, { id: file.id, data })
}
}
ignoreChange.current = false
setError(null)
}, [updateFile])
}, [])
const reset = async () => {
if (!service || !uri) {
@@ -304,9 +305,9 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
const [projectShown, setProjectShown] = useState(Store.getProjectPanelOpen() ?? window.innerWidth > 1000)
const toggleProjectShown = useCallback(() => {
if (projectShown) {
Analytics.hideProject(gen.id, projects.length, project.files.length, 'menu')
Analytics.hideProject('menu')
} else {
Analytics.showProject(gen.id, projects.length, project.files.length, 'menu')
Analytics.showProject('menu')
}
Store.setProjectPanelOpen(!projectShown)
setProjectShown(!projectShown)
@@ -314,13 +315,13 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
const [projectCreating, setProjectCreating] = useState(false)
const [projectDeleting, setprojectDeleting] = useState(false)
const [fileSaving, setFileSaving] = useState<string | undefined>(undefined)
const [fileRenaming, setFileRenaming] = useState<{ type: string, id: string } | undefined>(undefined)
const [fileSaving, setFileSaving] = useState<Method | undefined>(undefined)
const [fileRenaming, setFileRenaming] = useState<string | undefined>(undefined)
const onNewFile = useCallback(() => {
closeFile()
setProjectUri(undefined)
// TODO: create new file with default contents
}, [closeFile])
}, [setProjectUri])
return <>
<main class={`${previewShown ? 'has-preview' : ''} ${projectShown ? 'has-project' : ''}`}>
@@ -379,11 +380,11 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
</div>
</div>
<div class={`popup-project${projectShown ? ' shown' : ''}`}>
<ProjectPanel onError={setError} onDeleteProject={() => setprojectDeleting(true)} onRename={setFileRenaming} onCreate={() => setProjectCreating(true)} />
<ProjectPanel onError={setError} onRename={setFileRenaming} onDeleteProject={() => setprojectDeleting(true)} onCreateProject={() => setProjectCreating(true)} />
</div>
{projectCreating && <ProjectCreation onClose={() => setProjectCreating(false)} />}
{projectDeleting && <ProjectDeletion onClose={() => setprojectDeleting(false)} />}
{docAndNode && fileSaving && <FileCreation id={gen.id} docAndNode={docAndNode} method={fileSaving} onClose={() => setFileSaving(undefined)} />}
{fileRenaming && <FileRenaming id={fileRenaming.type } name={fileRenaming.id} onClose={() => setFileRenaming(undefined)} />}
{docAndNode && fileSaving && <FileCreation gen={gen} docAndNode={docAndNode} method={fileSaving} onCreate={(uri) => {setFileSaving(undefined); setProjectUri(uri)}} onClose={() => setFileSaving(undefined)} />}
{fileRenaming && <FileRenaming uri={fileRenaming} onClose={() => setFileRenaming(undefined)} />}
</>
}

View File

@@ -1,8 +1,9 @@
import type { DocAndNode, Range } from '@spyglassmc/core'
import { dissectUri } from '@spyglassmc/java-edition/lib/binder/index.js'
import type { JsonNode } from '@spyglassmc/json'
import { JsonFileNode } from '@spyglassmc/json'
import { useCallback, useErrorBoundary, useMemo } from 'preact/hooks'
import { disectFilePath, useLocale, useVersion } from '../../contexts/index.js'
import { useLocale } from '../../contexts/index.js'
import { useDocAndNode, useSpyglass } from '../../contexts/Spyglass.jsx'
import { getRootType, simplifyType } from './McdocHelpers.js'
import type { McdocContext } from './McdocRenderer.jsx'
@@ -14,7 +15,6 @@ type TreePanelProps = {
}
export function Tree({ docAndNode: original, onError }: TreePanelProps) {
const { lang } = useLocale()
const { version } = useVersion()
const { service } = useSpyglass()
if (lang === 'none') return <></>
@@ -61,19 +61,23 @@ export function Tree({ docAndNode: original, onError }: TreePanelProps) {
}, [docAndNode, service])
const resourceType = useMemo(() => {
const path = original.doc.uri
.replace(/^file:\/\/\/project\//, '')
.replace(/\.json$/, '')
const res = disectFilePath(path, version)
return res?.type
}, [original, version])
if (original.doc.uri.endsWith('/pack.mcmeta')) {
return 'pack_mcmeta'
}
if (ctx === undefined) {
return undefined
}
const res = dissectUri(original.doc.uri, ctx)
return res?.category
}, [original, ctx])
const mcdocType = useMemo(() => {
if (!ctx || !resourceType) {
return undefined
}
const rootType = getRootType(resourceType)
return simplifyType(rootType, ctx)
const type = simplifyType(rootType, ctx)
return type
}, [resourceType, ctx])
return <div class="tree node-root" data-cy="tree" data-category={getCategory(resourceType)}>