From 32e4dae8697a98fe64422239c94fa096bb37a009 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Mon, 6 Oct 2025 15:13:00 -0400 Subject: [PATCH 1/6] refactor: make layout constants file for shared use --- src/constants/layoutConstants.ts | 16 ++++++++++++++++ src/contexts/PreferencesContext.tsx | 5 +++++ src/hooks/useLayoutPrefs.ts | 22 ++++++++-------------- 3 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 src/constants/layoutConstants.ts diff --git a/src/constants/layoutConstants.ts b/src/constants/layoutConstants.ts new file mode 100644 index 00000000..963bc5ce --- /dev/null +++ b/src/constants/layoutConstants.ts @@ -0,0 +1,16 @@ +// Shared constants for layout management between PreferencesContext and useLayoutPrefs + +// Name is set by the autosaveId prop in PanelGroup +export const LAYOUT_NAME = 'react-resizable-panels:layout'; + +// Layout keys for the different panel combinations +// Confusingly, the names are in alphabetical order, but the order of the sizes is set by the order prop +// in the respective Panel components +export const WITH_PROPERTIES_AND_SIDEBAR = 'main,properties,sidebar'; +export const ONLY_SIDEBAR = 'main,sidebar'; +export const ONLY_PROPERTIES = 'main,properties'; + +export const DEFAULT_LAYOUT = + '{"main,properties,sidebar":{"expandToSizes":{},"layout":[24,50,26]}}'; +export const DEFAULT_LAYOUT_SMALL_SCREENS = + '{"main":{"expandToSizes":{},"layout":[100]}}'; diff --git a/src/contexts/PreferencesContext.tsx b/src/contexts/PreferencesContext.tsx index b50e829a..ee96f18e 100644 --- a/src/contexts/PreferencesContext.tsx +++ b/src/contexts/PreferencesContext.tsx @@ -8,6 +8,11 @@ import { useFileBrowserContext } from './FileBrowserContext'; import { sendFetchRequest, makeMapKey, HTTPError } from '@/utils'; import { createSuccess, handleError, toHttpError } from '@/utils/errorHandling'; import type { Result } from '@/shared.types'; +import { + LAYOUT_NAME, + WITH_PROPERTIES_AND_SIDEBAR, + ONLY_PROPERTIES +} from '@/constants/layoutConstants'; export type FolderFavorite = { type: 'folder'; diff --git a/src/hooks/useLayoutPrefs.ts b/src/hooks/useLayoutPrefs.ts index 2a6e5fe5..afe36a4d 100644 --- a/src/hooks/useLayoutPrefs.ts +++ b/src/hooks/useLayoutPrefs.ts @@ -3,6 +3,14 @@ import React from 'react'; import { usePreferencesContext } from '@/contexts/PreferencesContext'; import { useCentralServerHealthContext } from '@/contexts/CentralServerHealthContext'; import logger from '@/logger'; +import { + LAYOUT_NAME, + WITH_PROPERTIES_AND_SIDEBAR, + ONLY_SIDEBAR, + ONLY_PROPERTIES, + DEFAULT_LAYOUT, + DEFAULT_LAYOUT_SMALL_SCREENS +} from '@/constants/layoutConstants'; /** * Custom hook that provides storage interface for react-resizable-panels * with debounced updates to reduce API calls when resizing panels @@ -12,20 +20,6 @@ import logger from '@/logger'; const DEBOUNCE_MS = 500; -// Name is set by the autosaveId prop in PanelGroup -const LAYOUT_NAME = 'react-resizable-panels:layout'; -// Confusingly, the names are in alphabetical order, but the order of the sizes is set by the order prop -// in the respective Panel components -const DEFAULT_LAYOUT = - '{"main,properties,sidebar":{"expandToSizes":{},"layout":[24,50,24]}}'; -const DEFAULT_LAYOUT_SMALL_SCREENS = - '{"main":{"expandToSizes":{},"layout":[100]}}'; - -// Layout keys for the two possible panel combinations -const WITH_PROPERTIES_AND_SIDEBAR = 'main,properties,sidebar'; -const ONLY_SIDEBAR = 'main,sidebar'; -const ONLY_PROPERTIES = 'main,properties'; - export default function useLayoutPrefs() { const [showPropertiesDrawer, setShowPropertiesDrawer] = React.useState(false); From f4b3ec1bcf3f85aec5b18db4d2854101bc8dc99a Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Mon, 6 Oct 2025 15:13:46 -0400 Subject: [PATCH 2/6] feat: add func to PreferencesContext to add prop panel to layout if not there --- src/contexts/PreferencesContext.tsx | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/contexts/PreferencesContext.tsx b/src/contexts/PreferencesContext.tsx index ee96f18e..dcb6c66c 100644 --- a/src/contexts/PreferencesContext.tsx +++ b/src/contexts/PreferencesContext.tsx @@ -58,6 +58,7 @@ type PreferencesContextType = { recentlyViewedFolders: FolderPreference[]; layout: string; handleUpdateLayout: (layout: string) => Promise; + setLayoutWithPropertiesOpen: () => Promise>; loadingRecentlyViewedFolders: boolean; isLayoutLoadedFromDB: boolean; handleContextMenuFavorite: () => Promise>; @@ -228,6 +229,34 @@ export const PreferencesProvider = ({ setLayout(layout); }; + const setLayoutWithPropertiesOpen = async (): Promise> => { + try { + // Keep sidebar in new layout if it is currently present + const hasSidebar = layout.includes('sidebar'); + + const layoutKey = hasSidebar + ? WITH_PROPERTIES_AND_SIDEBAR + : ONLY_PROPERTIES; + + const layoutSizes = hasSidebar ? [24, 50, 26] : [75, 25]; + + const newLayout = { + [LAYOUT_NAME]: { + [layoutKey]: { + expandToSizes: {}, + layout: layoutSizes + } + } + }; + const newLayoutString = JSON.stringify(newLayout); + await savePreferencesToBackend('layout', newLayoutString); + setLayout(newLayoutString); + return createSuccess(undefined); + } catch (error) { + return handleError(error); + } + }; + const handlePathPreferenceSubmit = React.useCallback( async ( localPathPreference: ['linux_path'] | ['windows_path'] | ['mac_path'] @@ -755,6 +784,7 @@ export const PreferencesProvider = ({ recentlyViewedFolders, layout, handleUpdateLayout, + setLayoutWithPropertiesOpen, loadingRecentlyViewedFolders, isLayoutLoadedFromDB, handleContextMenuFavorite From 2ef42e0466987d49d9116faf031febe1b280b0ce Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Mon, 6 Oct 2025 15:14:26 -0400 Subject: [PATCH 3/6] refactor: add onClick as prop to FgLink --- src/components/ui/Table/jobsColumns.tsx | 5 ++++- src/components/ui/widgets/FgLink.tsx | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/ui/Table/jobsColumns.tsx b/src/components/ui/Table/jobsColumns.tsx index eb803b14..fc35f6cb 100644 --- a/src/components/ui/Table/jobsColumns.tsx +++ b/src/components/ui/Table/jobsColumns.tsx @@ -28,7 +28,10 @@ function FilePathCell({ item }: { readonly item: Ticket }) { return (
- + {displayPath}
diff --git a/src/components/ui/widgets/FgLink.tsx b/src/components/ui/widgets/FgLink.tsx index 582e07d2..1b82fb91 100644 --- a/src/components/ui/widgets/FgLink.tsx +++ b/src/components/ui/widgets/FgLink.tsx @@ -9,6 +9,7 @@ type StyledLinkProps = { readonly target?: string; readonly rel?: string; readonly textSize?: 'default' | 'large' | 'small'; + readonly onClick?: (e: React.MouseEvent) => void; }; export function FgStyledLink({ @@ -17,7 +18,8 @@ export function FgStyledLink({ className = '', target, rel, - textSize = 'default' + textSize = 'default', + onClick }: StyledLinkProps) { const baseClasses = 'text-primary-light hover:underline focus:underline'; const textClasses = { @@ -29,6 +31,7 @@ export function FgStyledLink({ return ( Date: Mon, 6 Oct 2025 15:15:31 -0400 Subject: [PATCH 4/6] feat: add func to navigate to link with property panel open and state for convert tab --- src/components/ui/Table/jobsColumns.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/ui/Table/jobsColumns.tsx b/src/components/ui/Table/jobsColumns.tsx index fc35f6cb..0f789343 100644 --- a/src/components/ui/Table/jobsColumns.tsx +++ b/src/components/ui/Table/jobsColumns.tsx @@ -1,5 +1,6 @@ import { Typography } from '@material-tailwind/react'; import { type ColumnDef } from '@tanstack/react-table'; +import { useNavigate } from 'react-router'; import { useZoneAndFspMapContext } from '@/contexts/ZonesAndFspMapContext'; import { usePreferencesContext } from '@/contexts/PreferencesContext'; @@ -12,10 +13,13 @@ import { } from '@/utils'; import { FileSharePath } from '@/shared.types'; import { FgStyledLink } from '../widgets/FgLink'; +import toast from 'react-hot-toast'; function FilePathCell({ item }: { readonly item: Ticket }) { const { zonesAndFileSharePathsMap } = useZoneAndFspMapContext(); - const { pathPreference } = usePreferencesContext(); + const { pathPreference, setLayoutWithPropertiesOpen } = + usePreferencesContext(); + const navigate = useNavigate(); const itemFsp = zonesAndFileSharePathsMap[ makeMapKey('fsp', item.fsp_name) @@ -26,6 +30,18 @@ function FilePathCell({ item }: { readonly item: Ticket }) { item.path ); + const handleClick = async (e: React.MouseEvent) => { + e.preventDefault(); + const result = await setLayoutWithPropertiesOpen(); + if (!result.success) { + toast.error(`Error opening properties for file: ${result.error}`); + return; + } + navigate(makeBrowseLink(item.fsp_name, item.path), { + state: { openConvertTab: true } + }); + }; + return (
Date: Mon, 6 Oct 2025 15:16:05 -0400 Subject: [PATCH 5/6] feat: set active tab in properties drawer to convert if state is provided --- .../ui/PropertiesDrawer/PropertiesDrawer.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx b/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx index 4a98b61c..1e017e3a 100644 --- a/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx +++ b/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx @@ -10,6 +10,7 @@ import { import toast from 'react-hot-toast'; import { HiOutlineDocument, HiOutlineDuplicate, HiX } from 'react-icons/hi'; import { HiOutlineFolder } from 'react-icons/hi2'; +import { useLocation } from 'react-router'; import PermissionsTable from '@/components/ui/PropertiesDrawer/PermissionsTable'; import OverviewTable from '@/components/ui/PropertiesDrawer/OverviewTable'; @@ -80,8 +81,10 @@ export default function PropertiesDrawer({ setShowPermissionsDialog, setShowConvertFileDialog }: PropertiesDrawerProps): JSX.Element { + const location = useLocation(); const [showDataLinkDialog, setShowDataLinkDialog] = React.useState(false); + const [activeTab, setActiveTab] = React.useState('overview'); const { fileBrowserState } = useFileBrowserContext(); const { pathPreference, areDataLinksAutomatic } = usePreferencesContext(); @@ -95,6 +98,13 @@ export default function PropertiesDrawer({ handleDeleteDataLink } = useDataToolLinks(setShowDataLinkDialog); + // Set active tab to 'convert' when navigating from jobs page + React.useEffect(() => { + if (location.state?.openConvertTab) { + setActiveTab('convert'); + } + }, [location.state]); + const fullPath = getPreferredPathForDisplay( pathPreference, fileBrowserState.currentFileSharePath, @@ -145,8 +155,9 @@ export default function PropertiesDrawer({ {fileBrowserState.propertiesTarget ? ( Date: Mon, 6 Oct 2025 15:16:33 -0400 Subject: [PATCH 6/6] fix: prettier --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a713564..43975483 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ See the [Fileglancer User Guide](https://janeliascicomp.github.io/fileglancer-us ## Software Architecture -Fileglancer is built on top of JuptyerHub, which provides the infrastructure for allowing users to login and interact directly with their files on mounted network file systems. JupyterHub runs a "single user server" for each user who logs in, in a process owned by that user. The Fileglancer plugin for JupyterHub replaces the UI with a new SPA webapp that connects back to a custom backend running inside the single user server. We also added a "central server" to serve shared data and to manage connections to a shared database for saving preferences, data links, and other persistent information. +Fileglancer is built on top of JuptyerHub, which provides the infrastructure for allowing users to login and interact directly with their files on mounted network file systems. JupyterHub runs a "single user server" for each user who logs in, in a process owned by that user. The Fileglancer plugin for JupyterHub replaces the UI with a new SPA webapp that connects back to a custom backend running inside the single user server. We also added a "central server" to serve shared data and to manage connections to a shared database for saving preferences, data links, and other persistent information. Fileglancer architecture diagram