Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions app/hooks/use-debounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { debounce } from 'lodash-es';
import { useEffect, useMemo, useRef } from 'react';

export const useDebounce = (callback: () => void) => {
const ref = useRef<() => void>();

useEffect(() => {
ref.current = callback;
}, [callback]);

const debouncedCallback = useMemo(() => {
const func = () => {
ref.current?.();
};

return debounce(func, 500);
}, []);

return debouncedCallback;
};
25 changes: 23 additions & 2 deletions components/editor/editor-pane.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { useState } from "react"
import dynamic from "next/dynamic"
import { SchemaState } from "@/store/main"
import { SchemaState, useMainStore } from "@/store/main"

import { parse, serialize } from "@/lib/json"

import { ImportDialog } from "./import-dialog"
import { EditorMenu } from "./menu"

export interface EditorPane {
Expand All @@ -24,15 +28,25 @@ export const EditorPane = ({
setValueString,
...props
}: EditorPane) => {
const [openImportDialog, setOpenImportDialog] = useState(false)
const editorMode = useMainStore(
(state) => state.editors[editorKey].mode ?? state.userSettings.mode
)
return (
<>
<div className="flex items-center justify-between rounded-lg">
<h3 className="text-md pl-2 font-medium w-full">{heading}</h3>
<h3 className="text-md w-full pl-2 font-medium">{heading}</h3>
<EditorMenu
heading={heading}
editorKey={editorKey}
value={value}
setValueString={setValueString}
onOpenImportDialog={() => setOpenImportDialog(true)}
onFormat={() => {
setValueString(
serialize(editorMode, parse(editorMode, value ?? ""))
)
}}
/>
</div>
<JSONEditor
Expand All @@ -46,6 +60,13 @@ export const EditorPane = ({
value={value}
{...props}
/>
<ImportDialog
heading={heading}
setValueString={setValueString}
editorKey={editorKey}
open={openImportDialog}
onOpenChange={setOpenImportDialog}
/>
</>
)
}
146 changes: 146 additions & 0 deletions components/editor/import-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { useState } from 'react'
import { Button } from "../ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogPortal,
} from "../ui/dialog"
import { Input } from "../ui/input"
import { Label } from "../ui/label"
import { Separator } from "../ui/separator"
import { JSONModes } from '@/types/editor'
import json5 from 'json5'
import { load as parseYaml } from "js-yaml"
import { Check } from 'lucide-react'
import { serialize } from '@/lib/json'
import { SchemaState, useMainStore } from '@/store/main'
import { useDebounce } from '@/app/hooks/use-debounce'
import { isValidUrl } from '@/lib/utils'

export interface ImportDialogProps {
open: boolean
onOpenChange?: (open: boolean) => void
heading: string
editorKey: keyof SchemaState["editors"]
setValueString: (val: string) => void
}
export const ImportDialog = ({ open, onOpenChange, heading, editorKey, setValueString }: ImportDialogProps) => {
const [imported, setImported] = useState<unknown>(undefined)
const [importUrl, setImportUrl] = useState('');

const editorMode = useMainStore(
(state) =>
state.editors[editorKey].mode ??
state.userSettings.mode
)

const debouncedImportUrlRequest = useDebounce(() => {
console.log('fetch import url', importUrl);
if (isValidUrl(importUrl)) {
fetch(importUrl)
.then((res) => res.text())
.then((text) => {
if (importUrl.includes(JSONModes.JSON5)) {
setImported(json5.parse(text))
} else if (importUrl.includes("json")) {
setImported(JSON.parse(text))
}

if (importUrl.includes("yaml")) {
setImported(parseYaml(text))
}
})
.catch((err) => {
console.error(err)
})
}
});

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogPortal>
<DialogContent>
<DialogHeader className="text-lg">
<div className="mb-2">Import {heading} File...</div>
<Separator className="h-[1px] bg-ring" />
</DialogHeader>
<div>
<Label htmlFor="importFile">From your device</Label>
<Input
name="importFile"
type="file"
accept=".json,.yml,.yaml,.json5,.jsonc,.json5c,.json-ld"
// to undo the above stopPropogation() within this scope
onClick={(e) => e.stopPropagation()}
onChange={async (e) => {
// TODO: move to zustand
const file = e?.target?.files?.[0]
if (file) {
const fileText = await file.text()
if (file.type.includes(JSONModes.JSON5)) {
setImported(json5.parse(fileText))
} else if (file.type.includes("json")) {
setImported(JSON.parse(fileText))
}

if (file.type.includes("yaml")) {
setImported(parseYaml(fileText))
}
}
}}
/>

<Label htmlFor={"urlImport"}>From a URL</Label>
<Input
id="urlImport"
name="urlImport"
type="url"
placeholder="https://example.com/schema.json"
onChange={(e) => {
console.log(e.target.value)
setImportUrl(e.target.value)
debouncedImportUrlRequest()
}}
value={importUrl}
/>
{imported ? (
<div className="flex items-center">
<Check className="mr-2 h-5 w-5 text-green-500" />
This file can be imported{" "}
</div>
) : null}
</div>
<DialogFooter>
<DialogClose asChild>
<Button
onClick={(e) => {
// e.stopPropagation()
setValueString(
serialize(editorMode, imported as Record<string, unknown>)
)
setImported(undefined)
}}
type="submit"
disabled={!imported}
>
Import
</Button>
</DialogClose>
<DialogClose asChild>
<Button
variant="ghost"
className="mr-2"
onClick={() => setImported(undefined)}
>
Cancel
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</DialogPortal>
</Dialog>
)
}
12 changes: 7 additions & 5 deletions components/editor/json-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import { JSONModes } from "@/types/editor"
import { serialize } from "@/lib/json"

// import { debounce } from "@/lib/utils"
import { jsonDark, jsonDarkTheme } from "./theme"
import { jsonDark, jsonDarkTheme, jsonLight, jsonLightTheme, lightHighlightStyle } from "./theme"
import { useTheme } from 'next-themes'

/**
* none of these are required for json4 or 5
Expand All @@ -30,10 +31,8 @@ import { jsonDark, jsonDarkTheme } from "./theme"
const commonExtensions = [
history(),
autocompletion(),
jsonDark,
EditorView.lineWrapping,
EditorState.tabSize.of(2),
syntaxHighlighting(oneDarkHighlightStyle),
]

const languageExtensions = {
Expand All @@ -59,7 +58,10 @@ export const JSONEditor = ({
state.editors[editorKey as keyof SchemaState["editors"]].mode ??
state.userSettings.mode
)
const {theme} = useTheme();
const languageExtension = languageExtensions[editorMode](schema)
const themeExtensions = theme === 'light' ? jsonLight : jsonDark;

const editorRef = useRef<ReactCodeMirrorRef>(null)

useEffect(() => {
Expand All @@ -73,9 +75,9 @@ export const JSONEditor = ({
return (
<CodeMirror
value={value ?? "{}"}
extensions={[...commonExtensions, languageExtension]}
extensions={[...commonExtensions, themeExtensions, languageExtension]}
onChange={onValueChange}
theme={jsonDarkTheme}
theme={theme === 'light' ? jsonLightTheme : jsonDarkTheme}
ref={editorRef}
contextMenu="true"
{...rest}
Expand Down
Loading