jsoneditor
This commit is contained in:
@@ -46,6 +46,8 @@
|
||||
"@types/three": "^0.182.0",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260107.1",
|
||||
"@uiw/react-textarea-code-editor": "^3.1.1",
|
||||
"@visual-json/core": "^0.1.1",
|
||||
"@visual-json/react": "^0.1.1",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"argon2": "^0.44.0",
|
||||
@@ -1051,6 +1053,10 @@
|
||||
|
||||
"@use-gesture/react": ["@use-gesture/react@10.3.1", "", { "dependencies": { "@use-gesture/core": "10.3.1" }, "peerDependencies": { "react": ">= 16.8.0" } }, "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g=="],
|
||||
|
||||
"@visual-json/core": ["@visual-json/core@0.1.1", "", {}, "sha512-VLARpQnjJU1SnnYk1NiXScGfp2FyAwyZS2kA8hIJV5OzkaH2oS1dTHsaB8vOiyi14/L7jsbpA4g/EwzRWbxQmg=="],
|
||||
|
||||
"@visual-json/react": ["@visual-json/react@0.1.1", "", { "dependencies": { "@visual-json/core": "0.1.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-KCpFYmBhH5F+xctfvxxVX7mVZK/iwvDwh/ITNkGPzEKXTIJQ16fau+hrG7m7QoTAOxvsdhHtHoCUiEjNHXboUQ=="],
|
||||
|
||||
"@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="],
|
||||
|
||||
"@xterm/addon-fit": ["@xterm/addon-fit@0.11.0", "", {}, "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g=="],
|
||||
|
||||
+3
-1
@@ -65,6 +65,8 @@
|
||||
"@types/three": "^0.182.0",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260107.1",
|
||||
"@uiw/react-textarea-code-editor": "^3.1.1",
|
||||
"@visual-json/core": "^0.1.1",
|
||||
"@visual-json/react": "^0.1.1",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"argon2": "^0.44.0",
|
||||
@@ -100,6 +102,7 @@
|
||||
"node-pty": "^1.1.0",
|
||||
"nodemailer": "^7.0.12",
|
||||
"officerdb": "workspace:*",
|
||||
"officerdev": "workspace:*",
|
||||
"pg": "^8.16.3",
|
||||
"postgres": "^3.4.5",
|
||||
"react": "^19",
|
||||
@@ -125,7 +128,6 @@
|
||||
"three": "^0.182.0",
|
||||
"types": "workspace:*",
|
||||
"vaul": "^1.1.2",
|
||||
"officerdev": "workspace:*",
|
||||
"widgets": "workspace:*",
|
||||
"ws": "^8.18.1",
|
||||
"zod": "^4.2.1"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef } from 'react';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { Loader2, FolderArchive } from 'lucide-react';
|
||||
import { getLang, getRawUrl, getTranscodeUrl, needsTranscode, getArchiveBaseName } from './file-types';
|
||||
import { useFileViewer } from './FileViewerContext';
|
||||
@@ -11,6 +11,7 @@ import { MarkdownRenderer } from './renderers/MarkdownRenderer';
|
||||
import { TextRenderer } from './renderers/TextRenderer';
|
||||
import { ScrollToTopButton } from './renderers/ScrollToTopButton';
|
||||
import { EditorRenderer } from './renderers/EditorRenderer';
|
||||
import { JsonEditorRenderer } from './renderers/JsonEditorRenderer';
|
||||
|
||||
export const FileViewerBody = () => {
|
||||
const { filePath, fileName, root, fileType, content, loading, error, autoPlay, editing, setContent } = useFileViewer();
|
||||
@@ -19,6 +20,17 @@ export const FileViewerBody = () => {
|
||||
const videoSrc =
|
||||
fileType === 'video' ? (needsTranscode(fileName) ? getTranscodeUrl(filePath, root) : getRawUrl(filePath, root)) : '';
|
||||
|
||||
const isJson = useMemo(() => {
|
||||
if (!content) return false;
|
||||
if (/\.jsonc?$/i.test(fileName)) return true;
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
return typeof parsed === 'object' && parsed !== null;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, [content, fileName]);
|
||||
|
||||
return (
|
||||
<div ref={scrollRef} className="h-full overflow-auto">
|
||||
{loading ? (
|
||||
@@ -43,8 +55,15 @@ export const FileViewerBody = () => {
|
||||
<VideoRenderer src={videoSrc} fileName={fileName} />
|
||||
) : fileType === 'audio' ? (
|
||||
<AudioRenderer src={getRawUrl(filePath, root)} fileName={fileName} autoPlay={autoPlay} />
|
||||
) : content !== null && editing && isJson ? (
|
||||
<JsonEditorRenderer content={content} filePath={filePath} root={root} onSave={setContent} />
|
||||
) : content !== null && editing ? (
|
||||
<EditorRenderer content={content} fileName={fileName} filePath={filePath} root={root} onSave={setContent} />
|
||||
) : content !== null && isJson ? (
|
||||
<div>
|
||||
<CodeRenderer content={content} lang="json" />
|
||||
<ScrollToTopButton scrollContainer={scrollRef} />
|
||||
</div>
|
||||
) : content !== null ? (
|
||||
fileType === 'code' ? (
|
||||
<div>
|
||||
|
||||
@@ -16,6 +16,7 @@ export type FileViewerContextValue = {
|
||||
extractAudioLoading: boolean;
|
||||
extractLoading: boolean;
|
||||
autoPlay: boolean;
|
||||
editable: boolean;
|
||||
editing: boolean;
|
||||
setEditing: (editing: boolean) => void;
|
||||
setContent: (content: string) => void;
|
||||
|
||||
@@ -22,6 +22,7 @@ export const FileViewerHeader = () => {
|
||||
handleDownload,
|
||||
handleSaveResult,
|
||||
saveResultLoading,
|
||||
editable,
|
||||
editing,
|
||||
setEditing,
|
||||
} = useFileViewer();
|
||||
@@ -48,7 +49,7 @@ export const FileViewerHeader = () => {
|
||||
<span className="text-[10px] font-mono uppercase tracking-wider shrink-0 opacity-60">
|
||||
{fileType === 'code' ? getLang(fileName) : fileType}
|
||||
</span>
|
||||
{(fileType === 'markdown' || fileType === 'code' || fileType === 'text') && content && (
|
||||
{editable && content && (
|
||||
<button
|
||||
onClick={() => setEditing(!editing)}
|
||||
className="p-1 rounded hover:bg-current/10 transition-colors cursor-pointer"
|
||||
|
||||
@@ -36,6 +36,7 @@ export const FileViewerProvider = ({
|
||||
const [editing, setEditing] = useState(false);
|
||||
const files = useFilesAPI(root);
|
||||
const fileType = getFileType(fileName);
|
||||
const editable = fileType === 'markdown' || fileType === 'code' || fileType === 'text';
|
||||
|
||||
useEffect(() => {
|
||||
if (directContent !== undefined) {
|
||||
@@ -162,6 +163,7 @@ export const FileViewerProvider = ({
|
||||
extractAudioLoading,
|
||||
extractLoading,
|
||||
autoPlay,
|
||||
editable,
|
||||
editing,
|
||||
setEditing,
|
||||
setContent,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { JsonEditor } from '@visual-json/react';
|
||||
import type { JsonValue } from '@visual-json/core';
|
||||
import { toast } from 'sonner';
|
||||
import { useFilesAPI } from '../../../hooks/useFilesAPI';
|
||||
|
||||
type JsonEditorRendererProps = {
|
||||
content: string;
|
||||
filePath: string;
|
||||
root: string;
|
||||
onSave?: (content: string) => void;
|
||||
};
|
||||
|
||||
export const JsonEditorRenderer = ({ content, filePath, root, onSave }: JsonEditorRendererProps) => {
|
||||
const { writeFile } = useFilesAPI(root);
|
||||
const valueRef = useRef<JsonValue>(JSON.parse(content));
|
||||
|
||||
const handleChange = useCallback((value: JsonValue) => {
|
||||
valueRef.current = value;
|
||||
}, []);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
const newContent = JSON.stringify(valueRef.current, null, 2);
|
||||
try {
|
||||
await writeFile(filePath, newContent);
|
||||
onSave?.(newContent);
|
||||
toast.success('File saved');
|
||||
} catch {
|
||||
toast.error('Failed to save file');
|
||||
}
|
||||
}, [filePath, writeFile, onSave]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-full"
|
||||
onKeyDown={(ev) => {
|
||||
if ((ev.ctrlKey || ev.metaKey) && ev.key === 's') {
|
||||
ev.preventDefault();
|
||||
handleSave();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<JsonEditor
|
||||
defaultValue={valueRef.current}
|
||||
onChange={handleChange}
|
||||
height="100%"
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user