Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d2e6889
feat: add reusable dialog button components
allison-truhlar Dec 9, 2025
84dd4ee
feat: add dialog with code snippets and instructions for data links
allison-truhlar Dec 9, 2025
dde3020
refactor: convert navigation and folder btns to use DialogIconBtn
allison-truhlar Dec 9, 2025
ed5dc87
feat: add data link usage dialog across app
allison-truhlar Dec 9, 2025
5b40748
fix: increase tooltip z-index to prevent overlap with dialogs
allison-truhlar Dec 9, 2025
e657a1a
style: organize example languages/tools in tabs
allison-truhlar Dec 12, 2025
169bb0b
refactor: extract dark mode logic for code blocks
allison-truhlar Dec 12, 2025
2b5eaf6
feat: add SyntaxHighlighter to data link usage dialog
allison-truhlar Dec 12, 2025
207d1d6
style: add padding to FileViewer so scroll bar doesn't cover last lin…
allison-truhlar Dec 12, 2025
93c31cc
chore: change text for data link usage btn in properties panel
allison-truhlar Dec 12, 2025
2507b3a
Merge branch 'main' into data-link-code-snippets-dialog
allison-truhlar Dec 12, 2025
0cfb5ef
refactor: map over data to remove duplicate styling on tabs
allison-truhlar Dec 12, 2025
38da78c
Merge branch 'main' into data-link-code-snippets-dialog
allison-truhlar Jan 5, 2026
033d82e
fix: show 'copied' msg for data link copy tooltip
allison-truhlar Jan 5, 2026
f5ad35d
feat: add fiji instructions to DataLinkUsageDialog
allison-truhlar Jan 5, 2026
cdf17aa
feat: add VVDViewer instructions to DataLinkUsageDialog
allison-truhlar Jan 5, 2026
c0448bd
Merge branch 'main' into data-link-code-snippets-dialog
allison-truhlar Jan 21, 2026
d7442f5
feat: add python sample code
allison-truhlar Jan 21, 2026
f9c26d3
Merge branch 'add-fiji-and-vvd-instructions' into data-link-code-snip…
allison-truhlar Jan 22, 2026
9209142
style: limit height of data link usage dialog
allison-truhlar Jan 22, 2026
1e4b96b
refactor: remove java code block until we have a working example
allison-truhlar Jan 22, 2026
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
28 changes: 7 additions & 21 deletions frontend/src/components/ui/BrowsePage/FileViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { Switch, Typography } from '@material-tailwind/react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import {
Expand All @@ -10,6 +10,7 @@ import { useFileBrowserContext } from '@/contexts/FileBrowserContext';
import { formatFileSize, formatUnixTimestamp } from '@/utils';
import type { FileOrFolder } from '@/shared.types';
import { useFileContentQuery } from '@/queries/fileContentQueries';
import useDarkMode from '@/hooks/useDarkMode';

type FileViewerProps = {
readonly file: FileOrFolder;
Expand Down Expand Up @@ -76,30 +77,12 @@ const getLanguageFromExtension = (filename: string): string => {

export default function FileViewer({ file }: FileViewerProps) {
const { fspName } = useFileBrowserContext();

const [isDarkMode, setIsDarkMode] = useState<boolean>(false);
const isDarkMode = useDarkMode();
const [formatJson, setFormatJson] = useState<boolean>(true);

const contentQuery = useFileContentQuery(fspName, file.path);
const language = getLanguageFromExtension(file.name);
const isJsonFile = language === 'json';

// Detect dark mode from document
useEffect(() => {
const checkDarkMode = () => {
setIsDarkMode(document.documentElement.classList.contains('dark'));
};

checkDarkMode();
const observer = new MutationObserver(checkDarkMode);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});

return () => observer.disconnect();
}, []);

const renderViewer = () => {
if (contentQuery.isLoading) {
return (
Expand Down Expand Up @@ -150,7 +133,10 @@ export default function FileViewer({ file }: FileViewerProps) {
codeTagProps={mergedCodeTagProps}
customStyle={{
margin: 0,
padding: '1rem',
paddingTop: '1em',
paddingRight: '1em',
paddingBottom: '0',
paddingLeft: '1em',
fontSize: '14px',
lineHeight: '1.5',
overflow: 'visible',
Expand Down
42 changes: 13 additions & 29 deletions frontend/src/components/ui/BrowsePage/NavigationButton.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { useState } from 'react';
import type { MouseEvent } from 'react';
import { IoNavigateCircleSharp } from 'react-icons/io5';

import FgTooltip from '@/components/ui/widgets/FgTooltip';
import DialogIconBtn from '@/components/ui/buttons/DialogIconBtn';
import NavigationInput from '@/components/ui/BrowsePage/NavigateInput';
import FgDialog from '@/components/ui/Dialogs/FgDialog';
import { IconButton } from '@material-tailwind/react';

type NavigationButtonProps = {
readonly triggerClasses: string;
Expand All @@ -14,30 +10,18 @@ type NavigationButtonProps = {
export default function NavigationButton({
triggerClasses
}: NavigationButtonProps) {
const [showNavigationDialog, setShowNavigationDialog] = useState(false);

return (
<>
<FgTooltip
as={IconButton}
icon={IoNavigateCircleSharp}
label="Navigate to a path"
onClick={(e: MouseEvent<HTMLButtonElement>) => {
setShowNavigationDialog(true);
}}
triggerClasses={triggerClasses}
/>
{showNavigationDialog ? (
<FgDialog
onClose={() => setShowNavigationDialog(false)}
open={showNavigationDialog}
>
<NavigationInput
location="dialog"
setShowNavigationDialog={setShowNavigationDialog}
/>
</FgDialog>
) : null}
</>
<DialogIconBtn
icon={IoNavigateCircleSharp}
label="Navigate to a path"
triggerClasses={triggerClasses}
>
{closeDialog => (
<NavigationInput
location="dialog"
setShowNavigationDialog={closeDialog}
/>
)}
</DialogIconBtn>
);
}
130 changes: 59 additions & 71 deletions frontend/src/components/ui/BrowsePage/NewFolderButton.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { useState } from 'react';
import type { ChangeEvent, MouseEvent } from 'react';
import type { ChangeEvent } from 'react';
import { Button, Typography } from '@material-tailwind/react';
import { HiFolderAdd } from 'react-icons/hi';
import toast from 'react-hot-toast';

import FgTooltip from '@/components/ui/widgets/FgTooltip';
import FgDialog from '@/components/ui/Dialogs/FgDialog';
import DialogIconBtn from '@/components/ui/buttons/DialogIconBtn';
import { Spinner } from '@/components/ui/widgets/Loaders';
import useNewFolderDialog from '@/hooks/useNewFolderDialog';
import { useFileBrowserContext } from '@/contexts/FileBrowserContext';
Expand All @@ -17,15 +15,17 @@ type NewFolderButtonProps = {
export default function NewFolderButton({
triggerClasses
}: NewFolderButtonProps) {
const [showNewFolderDialog, setShowNewFolderDialog] = useState(false);
const { fspName, mutations } = useFileBrowserContext();
const { handleNewFolderSubmit, newName, setNewName, isDuplicateName } =
useNewFolderDialog();

const isSubmitDisabled =
!newName.trim() || isDuplicateName || mutations.createFolder.isPending;

const formSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
const formSubmit = async (
event: React.FormEvent<HTMLFormElement>,
closeDialog: () => void
) => {
event.preventDefault();
const result = await handleNewFolderSubmit();
if (result.success) {
Expand All @@ -34,74 +34,62 @@ export default function NewFolderButton({
} else {
toast.error(`Error creating folder: ${result.error}`);
}
setShowNewFolderDialog(false);
};

const handleClose = () => {
setNewName('');
setShowNewFolderDialog(false);
closeDialog();
};

return (
<>
<FgTooltip
as="button"
disabledCondition={!fspName}
icon={HiFolderAdd}
label="New folder"
onClick={(e: MouseEvent<HTMLButtonElement>) => {
setShowNewFolderDialog(true);
}}
triggerClasses={triggerClasses}
/>
{showNewFolderDialog ? (
<FgDialog onClose={handleClose} open={showNewFolderDialog}>
<form onSubmit={formSubmit}>
<div className="mt-8 flex flex-col gap-2">
<Typography
as="label"
className="text-foreground font-semibold"
htmlFor="new_name"
>
Create a New Folder
<DialogIconBtn
disabled={!fspName}
icon={HiFolderAdd}
label="New folder"
triggerClasses={triggerClasses}
>
{closeDialog => (
<form onSubmit={e => formSubmit(e, closeDialog)}>
<div className="mt-8 flex flex-col gap-2">
<Typography
as="label"
className="text-foreground font-semibold"
htmlFor="new_name"
>
Create a New Folder
</Typography>
<input
autoFocus
className="mb-4 p-2 text-foreground text-lg border border-primary-light rounded-sm focus:outline-none focus:border-primary bg-background"
id="new_name"
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setNewName(event.target.value);
}}
placeholder="Folder name ..."
type="text"
value={newName}
/>
</div>
<div className="flex items-center gap-2">
<Button
className="!rounded-md"
disabled={isSubmitDisabled}
type="submit"
>
{mutations.createFolder.isPending ? (
<Spinner customClasses="border-white" text="Creating..." />
) : (
'Submit'
)}
</Button>
{!newName.trim() ? (
<Typography className="text-sm text-gray-500">
Please enter a folder name
</Typography>
) : newName.trim() && isDuplicateName ? (
<Typography className="text-sm text-red-500">
A file or folder with this name already exists
</Typography>
<input
autoFocus
className="mb-4 p-2 text-foreground text-lg border border-primary-light rounded-sm focus:outline-none focus:border-primary bg-background"
id="new_name"
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setNewName(event.target.value);
}}
placeholder="Folder name ..."
type="text"
value={newName}
/>
</div>
<div className="flex items-center gap-2">
<Button
className="!rounded-md"
disabled={isSubmitDisabled}
type="submit"
>
{mutations.createFolder.isPending ? (
<Spinner customClasses="border-white" text="Creating..." />
) : (
'Submit'
)}
</Button>
{!newName.trim() ? (
<Typography className="text-sm text-gray-500">
Please enter a folder name
</Typography>
) : newName.trim() && isDuplicateName ? (
<Typography className="text-sm text-red-500">
A file or folder with this name already exists
</Typography>
) : null}
</div>
</form>
</FgDialog>
) : null}
</>
) : null}
</div>
</form>
)}
</DialogIconBtn>
);
}
27 changes: 21 additions & 6 deletions frontend/src/components/ui/BrowsePage/ZarrPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { UseQueryResult } from '@tanstack/react-query';
import zarrLogo from '@/assets/zarr.jpg';
import ZarrMetadataTable from '@/components/ui/BrowsePage/ZarrMetadataTable';
import DataLinkDialog from '@/components/ui/Dialogs/DataLink';
import DataLinkUsageDialog from '@/components/ui/Dialogs/DataLinkUsageDialog';
import TextDialogBtn from '@/components/ui/buttons/DialogTextBtn';
import DataToolLinks from './DataToolLinks';
import type {
OpenWithToolUrls,
Expand Down Expand Up @@ -91,12 +93,25 @@ export default function ZarrPreview({
</div>

{openWithToolUrls ? (
<DataToolLinks
onToolClick={handleToolClick}
showCopiedTooltip={showCopiedTooltip}
title="Open with:"
urls={openWithToolUrls as OpenWithToolUrls}
/>
<>
<DataToolLinks
onToolClick={handleToolClick}
showCopiedTooltip={showCopiedTooltip}
title="Open with:"
urls={openWithToolUrls as OpenWithToolUrls}
/>
{openWithToolUrls.copy ? (
<TextDialogBtn label="More ways to open" variant="solid">
{closeDialog => (
<DataLinkUsageDialog
dataLinkUrl={openWithToolUrls.copy}
onClose={closeDialog}
open={true}
/>
)}
</TextDialogBtn>
) : null}
</>
) : null}

{showDataLinkDialog ? (
Expand Down
Loading
Loading