From 797cb45e2df5cc5ab556b85e87913c76162bf5a4 Mon Sep 17 00:00:00 2001 From: Weaver Goldman Date: Wed, 7 Aug 2024 19:58:21 -0400 Subject: [PATCH] Add drag and drop support for neuroglancer json files. --- .../components/FileExplorer/FileExplorer.tsx | 31 ++++++++- .../iframe.tsx => contexts/IFrameContext.tsx} | 33 ++++++--- src/renderer/src/routes/Root/Root.tsx | 17 ++--- .../src/routes/SlicesPage/SlicesPage.tsx | 69 ++++++++----------- .../src/schemas/iframe-message-schema.ts | 9 +++ .../src/schemas/neuroglancer-json-schema.ts | 28 +++++++- 6 files changed, 125 insertions(+), 62 deletions(-) rename src/renderer/src/{interfaces/iframe.tsx => contexts/IFrameContext.tsx} (84%) diff --git a/src/renderer/src/components/MenuPanel/components/FileExplorer/FileExplorer.tsx b/src/renderer/src/components/MenuPanel/components/FileExplorer/FileExplorer.tsx index d88eea0..0e78a1c 100644 --- a/src/renderer/src/components/MenuPanel/components/FileExplorer/FileExplorer.tsx +++ b/src/renderer/src/components/MenuPanel/components/FileExplorer/FileExplorer.tsx @@ -15,12 +15,17 @@ import { newFolder, renameFSItem, isPathFile, - directory + directory, + readFile } from '@renderer/interfaces/file' +import { parseNeuroglancerJSON } from '@renderer/schemas/neuroglancer-json-schema' +import { IFrameContext } from '@renderer/contexts/IFrameContext' +import { SendNeuroglancerJSON } from '@renderer/schemas/iframe-message-schema' function FileExplorer(): JSX.Element { const { clearDragEvent, parentChildData, active } = useContext(DragContext) const { nodes, directoryName, directoryPath, setDirectory } = useContext(DirectoryContext) + const { broadcast } = useContext(IFrameContext) const { point, clicked, data, handleContextMenu } = useContextMenu() const [renamePath, setRenamePath] = useState(null) @@ -161,6 +166,26 @@ function FileExplorer(): JSX.Element { const [customDragOver, setCustomDragOver] = useState(false) + const trySendFileToIframe = useCallback( + async (path: string): Promise => { + const neuroglancerJSONContent = await readFile('', path) + + const { error } = parseNeuroglancerJSON(neuroglancerJSONContent) + + if (error) return + + const message: SendNeuroglancerJSON = { + type: 'send-neuroglancer-json', + data: { + contents: neuroglancerJSONContent + } + } + + broadcast(message) + }, + [broadcast] + ) + useEffect(() => { const handleDrop = async (event: DragEvent): Promise => { event.preventDefault() @@ -184,6 +209,8 @@ function FileExplorer(): JSX.Element { const folderPath = directory(item.path) setDirectory(folderPath) + + trySendFileToIframe(item.path) } } @@ -209,7 +236,7 @@ function FileExplorer(): JSX.Element { dropFileRef.current.removeEventListener('dragleave', handleDragLeave) } } - }, [directoryPath, dropFileRef]) + }, [directoryPath, dropFileRef, trySendFileToIframe]) return ( <> diff --git a/src/renderer/src/interfaces/iframe.tsx b/src/renderer/src/contexts/IFrameContext.tsx similarity index 84% rename from src/renderer/src/interfaces/iframe.tsx rename to src/renderer/src/contexts/IFrameContext.tsx index d743712..af47377 100644 --- a/src/renderer/src/interfaces/iframe.tsx +++ b/src/renderer/src/contexts/IFrameContext.tsx @@ -8,11 +8,17 @@ import { SendDirectoryContents } from '@renderer/schemas/iframe-message-schema' import { safeParse } from 'valibot' -import { readFile, writeFile } from './file' -import { useContext, useEffect, useState } from 'react' +import { readFile, writeFile } from '../interfaces/file' +import { createContext, useCallback, useContext, useEffect, useState } from 'react' import { DirectoryContext } from '@renderer/contexts/DirectoryContext' -export function IFrameManager(): JSX.Element { +export type IFrameContextValue = { + broadcast: (message: IFrameMessage) => void +} + +export const IFrameContext = createContext(null as never) + +export function IFrameProvider({ children }: { children: React.ReactNode }): JSX.Element { // Define allowed origins const allowedOrigins: string[] = ['http://localhost', 'http://127.0.0.1', 'http://0.0.0.0'] @@ -44,6 +50,17 @@ export function IFrameManager(): JSX.Element { // Access the directory context const data = useContext(DirectoryContext) + const broadcast = useCallback( + (message: IFrameMessage): void => { + iframes.forEach((iframe) => { + iframe.postMessage(message, { + targetOrigin: '*' + }) + }) + }, + [iframes] + ) + useEffect(() => { if (!data) return @@ -57,12 +74,8 @@ export function IFrameManager(): JSX.Element { } // Send the directory info to the iframes - iframes.forEach((iframe) => { - iframe.postMessage(message, { - targetOrigin: '*' - }) - }) - }, [data, iframes]) + broadcast(message) + }, [data, broadcast]) useEffect(() => { const listener = (event: MessageEvent): void => { @@ -96,7 +109,7 @@ export function IFrameManager(): JSX.Element { } }, []) - return <> + return {children} } async function handleReadFileRequest( diff --git a/src/renderer/src/routes/Root/Root.tsx b/src/renderer/src/routes/Root/Root.tsx index 31ad134..deccc5f 100644 --- a/src/renderer/src/routes/Root/Root.tsx +++ b/src/renderer/src/routes/Root/Root.tsx @@ -7,7 +7,7 @@ import { Outlet } from 'react-router-dom' import styles from './Root.module.css' import TestingPluginProvider from '@renderer/contexts/TestingPluginContext' -import { IFrameManager } from '@renderer/interfaces/iframe' +import { IFrameProvider } from '@renderer/contexts/IFrameContext' function Root(): JSX.Element { return ( @@ -16,13 +16,14 @@ function Root(): JSX.Element { - -
- - - -
-
+ + +
+ + +
+
+
diff --git a/src/renderer/src/routes/SlicesPage/SlicesPage.tsx b/src/renderer/src/routes/SlicesPage/SlicesPage.tsx index 654afb2..ca33bd1 100644 --- a/src/renderer/src/routes/SlicesPage/SlicesPage.tsx +++ b/src/renderer/src/routes/SlicesPage/SlicesPage.tsx @@ -18,7 +18,7 @@ import VisualizeSlicing from './components/VisualizeSlicing/VisualizeSlicing' import SliceResultSchema from '@renderer/schemas/slice-result-schema' import { safeParse } from 'valibot' import SliceStatusResultSchema from '@renderer/schemas/slice-status-result-schema' -import NeuroglancerJSONSchema from '@renderer/schemas/neuroglancer-json-schema' +import { parseNeuroglancerJSON } from '@renderer/schemas/neuroglancer-json-schema' import SliceVisualizationResultSchema from '@renderer/schemas/slice-visualization-result-schema' import ConfigurationJSONSchema, { ConfigurationJSON @@ -300,56 +300,43 @@ function useSlicePageState(): SlicePageState { const neuroglancerJSONContent = await readFile('', entry.value as string) - if (!neuroglancerJSONContent || neuroglancerJSONContent === '') { - addAlert('Invalid Neuroglancer JSON', 'error') - return - } - - let json = '' + const { result, error } = parseNeuroglancerJSON(neuroglancerJSONContent) - try { - json = JSON.parse(neuroglancerJSONContent) - } catch (e) { - addAlert('Invalid Neuroglancer JSON', 'error') + if (error) { + addAlert(error, 'error') return } - const jsonResult = safeParse(NeuroglancerJSONSchema, json) + const imageLayers: { type: string; name: string }[] = [] + const annotationLayers: { type: string; name: string }[] = [] - if (jsonResult.success) { - const imageLayers: { type: string; name: string }[] = [] - const annotationLayers: { type: string; name: string }[] = [] - - // Read all image and annotation layers from the Neuroglancer JSON - for (const layer of jsonResult.output['layers']) { - if (layer.type === 'image' && layer.name !== '') { - imageLayers.push(layer) - } else if (layer.type === 'annotation' && layer.name !== '') { - annotationLayers.push(layer) - } + // Read all image and annotation layers from the Neuroglancer JSON + for (const layer of result['layers']) { + if (layer.type === 'image' && layer.name !== '') { + imageLayers.push(layer) + } else if (layer.type === 'annotation' && layer.name !== '') { + annotationLayers.push(layer) } + } - // Update the options for the image and annotation layer entries - if (entries[0] instanceof CompoundEntry) { - entries[0].getEntries().forEach((entry) => { - if (entry.name === 'neuroglancer_image_layer' && entry instanceof Entry) { - entry.options = imageLayers.map((layer) => layer.name) + // Update the options for the image and annotation layer entries + if (entries[0] instanceof CompoundEntry) { + entries[0].getEntries().forEach((entry) => { + if (entry.name === 'neuroglancer_image_layer' && entry instanceof Entry) { + entry.options = imageLayers.map((layer) => layer.name) - if (imageLayers.length > 0) entry.value = imageLayers[0].name - } else if ( - entry.name === 'neuroglancer_annotation_layer' && - entry instanceof Entry - ) { - entry.options = annotationLayers.map((layer) => layer.name) + if (imageLayers.length > 0) entry.value = imageLayers[0].name + } else if ( + entry.name === 'neuroglancer_annotation_layer' && + entry instanceof Entry + ) { + entry.options = annotationLayers.map((layer) => layer.name) - if (annotationLayers.length > 0) entry.value = annotationLayers[0].name - } - }) + if (annotationLayers.length > 0) entry.value = annotationLayers[0].name + } + }) - setEntries([...entries]) - } - } else { - addAlert('Invalid Neuroglancer JSON', 'error') + setEntries([...entries]) } } diff --git a/src/renderer/src/schemas/iframe-message-schema.ts b/src/renderer/src/schemas/iframe-message-schema.ts index b9be5fe..733ac5a 100644 --- a/src/renderer/src/schemas/iframe-message-schema.ts +++ b/src/renderer/src/schemas/iframe-message-schema.ts @@ -57,3 +57,12 @@ export const SaveFileRequestSchema = object({ }) export type SaveFileRequest = InferOutput + +export const SendNeuroglancerJSONSchema = object({ + type: pipe(string(), includes('send-neuroglancer-json')), + data: object({ + contents: string() + }) +}) + +export type SendNeuroglancerJSON = InferOutput diff --git a/src/renderer/src/schemas/neuroglancer-json-schema.ts b/src/renderer/src/schemas/neuroglancer-json-schema.ts index 4a86d77..22ec799 100644 --- a/src/renderer/src/schemas/neuroglancer-json-schema.ts +++ b/src/renderer/src/schemas/neuroglancer-json-schema.ts @@ -1,4 +1,4 @@ -import { array, object, string } from 'valibot' +import { array, InferOutput, object, safeParse, string } from 'valibot' const NeuroglancerJSONSchema = object({ layers: array( @@ -9,4 +9,30 @@ const NeuroglancerJSONSchema = object({ ) }) +export type NeuroglancerJSON = InferOutput + +export const parseNeuroglancerJSON = ( + jsonString: string | null +): { result: NeuroglancerJSON; error: string | null } => { + const errorResult = { result: {} as NeuroglancerJSON, error: 'Invalid Neuroglancer JSON' } + + if (!jsonString || jsonString === '') return errorResult + + let parsedJSON = '' + + try { + parsedJSON = JSON.parse(jsonString) + } catch (e) { + return errorResult + } + + const jsonResult = safeParse(NeuroglancerJSONSchema, parsedJSON) + + if (jsonResult.success) { + return { result: jsonResult.output, error: null } + } else { + return errorResult + } +} + export default NeuroglancerJSONSchema