mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-24 07:37:10 +00:00
Project tree view and creation (#203)
* Implement creating and importing new projects * Add downloading a zip of a project * Project validation (WIP) * Add project side panel, remove project pages * Project file saving * Add file tree actions to rename and delete * Fix file creation auto focus * Add button to save file from menu * Add project creation * Fix specificity on version switcher button * Update default version to 1.19 * List project files by type, remember project and delete project
This commit is contained in:
96
src/app/components/generator/ProjectCreation.tsx
Normal file
96
src/app/components/generator/ProjectCreation.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
|
||||
import { Btn, BtnMenu, FileUpload, Octicon, TextInput } from '..'
|
||||
import config from '../../../config.json'
|
||||
import type { Project } from '../../contexts'
|
||||
import { disectFilePath, useLocale, useProject } from '../../contexts'
|
||||
import type { VersionId } from '../../services'
|
||||
import { DEFAULT_VERSION, parseSource } from '../../services'
|
||||
import { message, readZip } from '../../Utils'
|
||||
import { Modal } from '../Modal'
|
||||
|
||||
interface Props {
|
||||
onClose: () => unknown,
|
||||
}
|
||||
export function ProjectCreation({ onClose }: Props) {
|
||||
const { locale } = useLocale()
|
||||
const { projects, createProject, changeProject, updateProject } = useProject()
|
||||
|
||||
const [name, setName] = useState('')
|
||||
const [namespace, setNamespace] = useState('')
|
||||
const [version, setVersion] = useState(DEFAULT_VERSION)
|
||||
const [file, setFile] = useState<File | undefined>(undefined)
|
||||
const [creating, setCreating] = useState(false)
|
||||
|
||||
const onUpload = (file: File) => {
|
||||
if (file.type.match(/^application\/(x-)?zip(-compressed)?$/)) {
|
||||
if (name.length === 0) {
|
||||
setName(file.name
|
||||
.replace(/\.zip$/, '')
|
||||
.replaceAll(/[ _-]+/g, ' '))
|
||||
}
|
||||
setFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
const projectUpdater = useRef(updateProject)
|
||||
useEffect(() => {
|
||||
projectUpdater.current = updateProject
|
||||
}, [updateProject])
|
||||
|
||||
const onCreate = () => {
|
||||
setCreating(true)
|
||||
createProject(name, namespace || undefined, version)
|
||||
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])
|
||||
if (file) {
|
||||
try {
|
||||
const data = await parseSource(entry[1], 'json')
|
||||
project.files!.push({ ...file, data })
|
||||
} catch (e) {
|
||||
console.error(`Failed parsing ${file.type} ${file.id}: ${message(e)}`)
|
||||
}
|
||||
}
|
||||
}))
|
||||
projectUpdater.current(project)
|
||||
onClose()
|
||||
}).catch(() => {
|
||||
onClose()
|
||||
})
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
const invalidName = useMemo(() => {
|
||||
return projects.map(p => p.name.trim().toLowerCase()).includes(name.trim().toLowerCase())
|
||||
}, [projects, name])
|
||||
|
||||
const invalidNamespace = useMemo(() => {
|
||||
return !(namespace.length === 0 || namespace.match(/^(?:[a-z0-9._-]+:)?[a-z0-9/._-]+$/))
|
||||
}, [namespace])
|
||||
|
||||
const versions = config.versions.map(v => v.id as VersionId).reverse()
|
||||
|
||||
return <Modal class="project-creation" onDismiss={onClose}>
|
||||
<p>{locale('project.create')}</p>
|
||||
<div class="input-group">
|
||||
<TextInput autofocus class={`btn btn-input${!creating && (invalidName || name.length === 0) ? ' invalid': ''}`} placeholder={locale('project.name')} value={name} onChange={setName} />
|
||||
{!creating && invalidName && <div class="status-icon danger tooltipped tip-e" aria-label={locale('project.name.already_exists')}>{Octicon.issue_opened}</div>}
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<TextInput class={`btn btn-input${!creating && invalidNamespace ? ' invalid' : ''}`} placeholder={locale('project.namespace')} value={namespace} onChange={setNamespace} />
|
||||
{!creating && invalidNamespace && <div class="status-icon danger tooltipped tip-e" aria-label={locale('project.namespace.invalid')}>{Octicon.issue_opened}</div>}
|
||||
</div>
|
||||
<BtnMenu icon="tag" label={version} tooltip={locale('switch_version')} data-cy="version-switcher">
|
||||
{versions.map(v =>
|
||||
<Btn label={v} active={v === version} onClick={() => setVersion(v)} />
|
||||
)}
|
||||
</BtnMenu>
|
||||
<FileUpload value={file} onChange={onUpload} label={locale('choose_zip_file')} accept=".zip"/>
|
||||
<Btn icon="rocket" label="Create!" disabled={creating || invalidName || name.length === 0 || invalidNamespace} onClick={onCreate} />
|
||||
</Modal>
|
||||
}
|
||||
Reference in New Issue
Block a user