diff --git a/src/app/Main.tsx b/src/app/Main.tsx index f5b52c05..78c5344c 100644 --- a/src/app/Main.tsx +++ b/src/app/Main.tsx @@ -4,6 +4,7 @@ import '../styles/main.css' import '../styles/nodes.css' import { App } from './App.js' import { LocaleProvider, ProjectProvider, StoreProvider, ThemeProvider, TitleProvider, VersionProvider } from './contexts/index.js' +import { ModalProvider } from './contexts/Modal.jsx' import { SpyglassProvider } from './contexts/Spyglass.jsx' function Main() { @@ -15,7 +16,9 @@ function Main() { - + + + diff --git a/src/app/components/Modal.tsx b/src/app/components/Modal.tsx index a5741e72..cfd5763c 100644 --- a/src/app/components/Modal.tsx +++ b/src/app/components/Modal.tsx @@ -1,21 +1,22 @@ import type { JSX } from 'preact' import { useCallback, useEffect } from 'preact/hooks' +import { useModal } from '../contexts/Modal.jsx' import { LOSE_FOCUS } from '../hooks/index.js' const MODALS_KEY = 'data-modals' -interface Props extends JSX.HTMLAttributes { - onDismiss: () => void, -} +interface Props extends JSX.HTMLAttributes {} export function Modal(props: Props) { + const { hideModal } = useModal() + useEffect(() => { addCurrentModals(1) - window.addEventListener('click', props.onDismiss) + window.addEventListener('click', hideModal) return () => { addCurrentModals(-1) - window.removeEventListener('click', props.onDismiss) + window.removeEventListener('click', hideModal) } - }) + }, [hideModal]) const onClick = useCallback((e: MouseEvent) => { e.stopPropagation() diff --git a/src/app/components/generator/FileCreation.tsx b/src/app/components/generator/FileCreation.tsx index b5952b9c..062a8450 100644 --- a/src/app/components/generator/FileCreation.tsx +++ b/src/app/components/generator/FileCreation.tsx @@ -5,6 +5,7 @@ import type { Method } from '../../Analytics.js' import { Analytics } from '../../Analytics.js' import type { ConfigGenerator } from '../../Config.js' import { getProjectRoot, useLocale, useProject, useVersion } from '../../contexts/index.js' +import { useModal } from '../../contexts/Modal.jsx' import { useSpyglass } from '../../contexts/Spyglass.jsx' import { genPath, message } from '../../Utils.js' import { Btn } from '../Btn.js' @@ -15,12 +16,11 @@ interface Props { docAndNode: DocAndNode, gen: ConfigGenerator, method: Method, - onCreate: (uri: string) => void, - onClose: () => void, } -export function FileCreation({ docAndNode, gen, method, onCreate, onClose }: Props) { +export function FileCreation({ docAndNode, gen, method }: Props) { const { locale } = useLocale() const { version } = useVersion() + const { hideModal } = useModal() const { project } = useProject() const { client } = useSpyglass() @@ -42,15 +42,15 @@ export function FileCreation({ docAndNode, gen, method, onCreate, onClose }: Pro Analytics.saveProjectFile(method) const text = docAndNode.doc.getText() client.fs.writeFile(uri, text).then(() => { - onCreate(uri) + hideModal() }).catch((e) => { setError(message(e)) }) }, [version, project, client, fileId ]) - return + return

{locale('project.save_current_file')}

- + {error !== undefined && {error}}
diff --git a/src/app/components/generator/FileRenaming.tsx b/src/app/components/generator/FileRenaming.tsx index 019c12d9..96a29233 100644 --- a/src/app/components/generator/FileRenaming.tsx +++ b/src/app/components/generator/FileRenaming.tsx @@ -1,16 +1,17 @@ import { useState } from 'preact/hooks' import { Analytics } from '../../Analytics.js' import { useLocale } from '../../contexts/index.js' +import { useModal } from '../../contexts/Modal.jsx' import { Btn } from '../Btn.js' import { TextInput } from '../forms/index.js' import { Modal } from '../Modal.js' interface Props { uri: string, - onClose: () => void, } -export function FileRenaming({ uri, onClose }: Props) { +export function FileRenaming({ uri }: Props) { const { locale } = useLocale() + const { hideModal } = useModal() const [fileId, setFileId] = useState(uri) // TODO: get original file id const [error, setError] = useState() @@ -26,12 +27,12 @@ export function FileRenaming({ uri, onClose }: Props) { } Analytics.renameProjectFile('menu') // TODO: rename file - onClose() + hideModal() } - return + return

{locale('project.rename_file')}

- + {error !== undefined && {error}}
diff --git a/src/app/components/generator/ProjectCreation.tsx b/src/app/components/generator/ProjectCreation.tsx index 6287ddd7..d88e4af8 100644 --- a/src/app/components/generator/ProjectCreation.tsx +++ b/src/app/components/generator/ProjectCreation.tsx @@ -1,6 +1,7 @@ import { useCallback, useMemo, useState } from 'preact/hooks' import config from '../../Config.js' import { useLocale, useProject } from '../../contexts/index.js' +import { useModal } from '../../contexts/Modal.jsx' import { useSpyglass } from '../../contexts/Spyglass.jsx' import type { VersionId } from '../../services/index.js' import { DEFAULT_VERSION } from '../../services/index.js' @@ -9,11 +10,9 @@ import { hexId, readZip } from '../../Utils.js' import { Btn, BtnMenu, FileUpload, Octicon, TextInput } from '../index.js' import { Modal } from '../Modal.js' -interface Props { - onClose: () => unknown, -} -export function ProjectCreation({ onClose }: Props) { +export function ProjectCreation() { const { locale } = useLocale() + const { hideModal } = useModal() const { projects, createProject, changeProject } = useProject() const { client } = useSpyglass() @@ -45,12 +44,13 @@ export function ProjectCreation({ onClose }: Props) { const path = entry[0].startsWith('/') ? entry[0].slice(1) : entry[0] return client.fs.writeFile(rootUri + path, entry[1]) })) - onClose() + hideModal() }).catch(() => { - onClose() + // TODO: handle errors + hideModal() }) } else { - onClose() + hideModal() } }, [createProject, changeProject, client, version, name, namespace, file]) @@ -64,7 +64,7 @@ export function ProjectCreation({ onClose }: Props) { const versions = config.versions.map(v => v.id as VersionId).reverse() - return + return

{locale('project.create')}

diff --git a/src/app/components/generator/ProjectDeletion.tsx b/src/app/components/generator/ProjectDeletion.tsx index 04856d33..7145b5a5 100644 --- a/src/app/components/generator/ProjectDeletion.tsx +++ b/src/app/components/generator/ProjectDeletion.tsx @@ -1,28 +1,27 @@ import { useCallback } from 'preact/hooks' import { Analytics } from '../../Analytics.js' import { useLocale, useProject } from '../../contexts/index.js' +import { useModal } from '../../contexts/Modal.jsx' import { Btn } from '../Btn.js' import { Modal } from '../Modal.js' -interface Props { - onClose: () => void, -} -export function ProjectDeletion({ onClose }: Props) { +export function ProjectDeletion() { const { locale } = useLocale() + const { hideModal } = useModal() const { project, deleteProject } = useProject() const doSave = useCallback(() => { Analytics.deleteProject('menu') deleteProject(project.name) - onClose() - }, [onClose, deleteProject]) + hideModal() + }, [deleteProject, hideModal]) - return + return

{locale('project.delete_confirm.1', project.name)}

{locale('project.delete_confirm.2')}

- +
} diff --git a/src/app/components/generator/ProjectPanel.tsx b/src/app/components/generator/ProjectPanel.tsx index e43d545a..1b9b4993 100644 --- a/src/app/components/generator/ProjectPanel.tsx +++ b/src/app/components/generator/ProjectPanel.tsx @@ -2,6 +2,7 @@ import { route } from 'preact-router' import { useCallback, useMemo, useRef } from 'preact/hooks' import config from '../../Config.js' import { DRAFT_PROJECT, getProjectRoot, useLocale, useProject } from '../../contexts/index.js' +import { useModal } from '../../contexts/Modal.jsx' import { useSpyglass } from '../../contexts/Spyglass.jsx' import { useAsync } from '../../hooks/useAsync.js' import { useFocus } from '../../hooks/useFocus.js' @@ -11,15 +12,13 @@ import { BtnMenu } from '../BtnMenu.js' import { Octicon } from '../Octicon.jsx' import type { TreeViewGroupRenderer, TreeViewLeafRenderer } from '../TreeView.js' import { TreeView } from '../TreeView.js' +import { FileRenaming } from './FileRenaming.jsx' +import { ProjectCreation } from './ProjectCreation.jsx' +import { ProjectDeletion } from './ProjectDeletion.jsx' -interface Props { - onError: (message: string) => void, - onRename: (uri: string) => void, - onCreateProject: () => void, - onDeleteProject: () => void, -} -export function ProjectPanel({ onRename, onCreateProject, onDeleteProject}: Props) { +export function ProjectPanel() { const { locale } = useLocale() + const { showModal } = useModal() const { projects, project, projectUri, setProjectUri, changeProject } = useProject() const { client, service } = useSpyglass() @@ -47,12 +46,20 @@ export function ProjectPanel({ onRename, onCreateProject, onDeleteProject}: Prop download.current.click() } + const onDeleteProject = useCallback(() => { + showModal(() => ) + }, []) + + const onCreateProject = useCallback(() => { + showModal(() => ) + }, []) + const actions = useMemo(() => [ { icon: 'pencil', label: locale('project.rename_file'), onAction: (uri: string) => { - onRename(uri) + showModal(() => ) }, }, { @@ -64,7 +71,7 @@ export function ProjectPanel({ onRename, onCreateProject, onDeleteProject}: Prop }) }, }, - ], [client, onRename, projectRoot]) + ], [client, projectRoot, showModal]) const FolderEntry: TreeViewGroupRenderer = useCallback(({ name, open, onClick }) => { return
diff --git a/src/app/components/generator/SchemaGenerator.tsx b/src/app/components/generator/SchemaGenerator.tsx index 8c6d8fae..300782da 100644 --- a/src/app/components/generator/SchemaGenerator.tsx +++ b/src/app/components/generator/SchemaGenerator.tsx @@ -5,6 +5,7 @@ import { Analytics } from '../../Analytics.js' import type { ConfigGenerator } from '../../Config.js' import config from '../../Config.js' import { DRAFT_PROJECT, useLocale, useProject, useVersion } from '../../contexts/index.js' +import { useModal } from '../../contexts/Modal.jsx' import { useSpyglass, watchSpyglassUri } from '../../contexts/Spyglass.jsx' import { AsyncCancel, useActiveTimeout, useAsync, useSearchParam } from '../../hooks/index.js' import type { VersionId } from '../../services/index.js' @@ -12,7 +13,7 @@ import { checkVersion, fetchDependencyMcdoc, fetchPreset, fetchRegistries, getSn import { DEPENDENCY_URI } from '../../services/Spyglass.js' import { Store } from '../../Store.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 { Ad, Btn, BtnMenu, ErrorPanel, FileCreation, Footer, HasPreview, Octicon, PreviewPanel, ProjectPanel, SearchList, SourcePanel, TextInput, Tree, VersionSwitcher } from '../index.js' import { getRootDefault } from './McdocHelpers.js' export const SHARE_KEY = 'share' @@ -25,6 +26,7 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { const { locale } = useLocale() const { version, changeVersion, changeTargetVersion } = useVersion() const { service } = useSpyglass() + const { showModal } = useModal() const { project, projectUri, setProjectUri, updateProject } = useProject() const [error, setError] = useState(null) const [errorBoundary, errorRetry] = useErrorBoundary() @@ -164,7 +166,7 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { Analytics.redoGenerator(gen.id, 1, 'hotkey') await service.redoEdit(uri) } else if (e.ctrlKey && e.key === 's') { - setFileSaving('hotkey') + saveFile('hotkey') e.preventDefault() e.stopPropagation() } @@ -302,7 +304,7 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { } } - const [projectShown, setProjectShown] = useState(Store.getProjectPanelOpen() ?? window.innerWidth > 1000) + const [projectShown, setProjectShown] = useState(Store.getProjectPanelOpen() ?? false) const toggleProjectShown = useCallback(() => { if (projectShown) { Analytics.hideProject('menu') @@ -313,15 +315,22 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { setProjectShown(!projectShown) }, [projectShown]) - const [projectCreating, setProjectCreating] = useState(false) - const [projectDeleting, setprojectDeleting] = useState(false) - const [fileSaving, setFileSaving] = useState(undefined) - const [fileRenaming, setFileRenaming] = useState(undefined) + const saveFile = useCallback((method: Method) => { + if (!docAndNode) { + return + } + showModal(() => ) + }, [showModal, gen, docAndNode]) - const onNewFile = useCallback(() => { + const newEmptyFile = useCallback(async () => { + if (service) { + const unsavedUri = service.getUnsavedFileUri(gen) + const node = getRootDefault(gen.id, service.getCheckerContext()) + const text = service.formatNode(node, unsavedUri) + await service.writeFile(unsavedUri, text) + } setProjectUri(undefined) - // TODO: create new file with default contents - }, [setProjectUri]) + }, [showModal]) return <>
@@ -339,8 +348,8 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { - - setFileSaving('menu')} /> + + saveFile('menu')} />
{error && setError(null)} />} @@ -380,11 +389,7 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
- setprojectDeleting(true)} onCreateProject={() => setProjectCreating(true)} /> +
- {projectCreating && setProjectCreating(false)} />} - {projectDeleting && setprojectDeleting(false)} />} - {docAndNode && fileSaving && {setFileSaving(undefined); setProjectUri(uri)}} onClose={() => setFileSaving(undefined)} />} - {fileRenaming && setFileRenaming(undefined)} />} } diff --git a/src/app/contexts/Modal.tsx b/src/app/contexts/Modal.tsx new file mode 100644 index 00000000..806db1c9 --- /dev/null +++ b/src/app/contexts/Modal.tsx @@ -0,0 +1,40 @@ +import type { ComponentChildren, FunctionComponent } from 'preact' +import { createContext } from 'preact' +import { useCallback, useContext, useState } from 'preact/hooks' + +interface ModalContext { + showModal: (component: FunctionComponent) => void + hideModal: () => void +} + +const ModalContext = createContext(undefined) + +export function useModal() { + const context = useContext(ModalContext) + if (context === undefined) { + throw new Error('Cannot use modal context') + } + return context +} + +export function ModalProvider({ children }: { children: ComponentChildren }) { + const [modal, setModal] = useState() + + const showModal = useCallback((component: FunctionComponent) => { + setModal(component) + }, []) + + const hideModal = useCallback(() => { + setModal(undefined) + }, []) + + const value: ModalContext = { + showModal, + hideModal, + } + + return + {children} + {modal !== undefined && modal} + +} diff --git a/src/app/contexts/Project.tsx b/src/app/contexts/Project.tsx index 7462e8ed..12efe92b 100644 --- a/src/app/contexts/Project.tsx +++ b/src/app/contexts/Project.tsx @@ -63,7 +63,7 @@ export function useProject() { export function ProjectProvider({ children }: { children: ComponentChildren }) { const [projects, setProjects] = useState(Store.getProjects()) - const [openProject, setProjectName] = useState() + const [openProject, setOpenProject] = useState() useEffect(() => { const initialProject = Store.getOpenProject() @@ -72,7 +72,7 @@ export function ProjectProvider({ children }: { children: ComponentChildren }) { return } if (project.storage !== undefined) { - setProjectName(initialProject) + setOpenProject(initialProject) return } // TODO: Import legacy projects @@ -96,11 +96,12 @@ export function ProjectProvider({ children }: { children: ComponentChildren }) { const deleteProject = useCallback((name: string) => { if (name === DRAFT_PROJECT.name) return changeProjects(projects.filter(p => p.name !== name)) + setOpenProject(undefined) }, [projects]) const changeProject = useCallback((name: string) => { Store.setOpenProject(name) - setProjectName(name) + setOpenProject(name) }, []) const updateProject = useCallback((edits: Partial) => {