Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8e79468
fix(views): cart rows expand without a Data Link
allison-truhlar Aug 19, 2026
15b423e
feat(views): compact embedded viewer header into a single row
allison-truhlar Aug 19, 2026
58f12f1
feat(views): NG Views table gets a resizable Sources column and horiz…
allison-truhlar Aug 19, 2026
5ce819c
fix(views): cart list scrolls internally instead of overflowing the s…
allison-truhlar Aug 20, 2026
a3e1c85
feat(views): add pure dimension-signature comparison for cart layers
allison-truhlar Aug 20, 2026
f2b7f72
feat(views): fetch and compare cart layer dimensions via useQueries
allison-truhlar Aug 20, 2026
95a6801
feat(views): warn on cart rows whose dimensions differ from the first…
allison-truhlar Aug 20, 2026
6c92a61
feat(views): gate Create View on acknowledging mismatched dimensions
allison-truhlar Aug 20, 2026
0ba3f3a
feat(views): make the view name open the embedded viewer
allison-truhlar Aug 20, 2026
1b25f83
refactor(views): rename user-facing "NG View(s)"/"Neuroglancer View(s…
allison-truhlar Aug 20, 2026
d4463d9
test(views): mock useCartDimensionCheck in cart/create-view tests to …
allison-truhlar Aug 20, 2026
13695b2
feat(views): add a Create view tile to the zarr metadata preview
allison-truhlar Aug 20, 2026
c65f2a8
fix(views): drop stray 'Neuroglancer view' tooltip copy; fail open on…
allison-truhlar Aug 20, 2026
bc76d96
feat(views): show full source paths in the Views table
allison-truhlar Aug 21, 2026
6e25c23
feat(views): explain the empty expansion for non-OME datasets in the …
allison-truhlar Aug 21, 2026
d3205e8
fix(views): fit the Views table columns without horizontal scroll
allison-truhlar Aug 21, 2026
84d1ec3
fix(ui-tests): scope data link confirm buttons to dialog
allison-truhlar Aug 24, 2026
a3dadb5
test(views): scope the data-link confirm locator to the dialog
allison-truhlar Aug 24, 2026
b7ee3c6
chore: prettier formatting
allison-truhlar Aug 24, 2026
24619ce
feat(viewer): give the embedded Neuroglancer view its own top bar
allison-truhlar Aug 25, 2026
99a74c7
fix(tests): update viewer tests for navbar-suppression and icon-butto…
allison-truhlar Aug 25, 2026
589708a
chore: bump version to 3.3.0a0 for test release on dev
allison-truhlar Sep 10, 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
4 changes: 2 additions & 2 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "fileglancer",
"type": "module",
"version": "3.2.0",
"version": "3.3.0-a0",
"description": "Browse, share, and publish files on the Janelia file system",
"keywords": [
"ngff",
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/__tests__/componentTests/CartList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@ vi.mock('@/contexts/CartContext', () => ({
clearCart
})
}));
vi.mock('@/queries/proxiedPathQueries', () => ({
useAllProxiedPathsQuery: () => ({ data: [] })
}));
vi.mock('@/components/ui/Views/CartDatasetRow', () => ({
default: ({ label }: { label: string }) => (
<div data-testid="row">{label}</div>
Expand All @@ -49,6 +46,12 @@ vi.mock('@/components/ui/Views/CreateViewButton', () => ({
</button>
)
}));
vi.mock('@/hooks/useCartDimensionCheck', () => ({
useCartDimensionCheck: () => ({
mismatchedKeys: new Set(),
hasMismatch: false
})
}));

import CartList from '@/components/ui/Views/CartList';

Expand Down
69 changes: 41 additions & 28 deletions frontend/src/__tests__/componentTests/CartTab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ const view: View = {
layers: []
};

// Dataset A has an existing Data Link (channel expansion enabled) and TWO
// cart entries (a base entry + an already-checked "GFP" channel entry), to
// exercise the multi-entry "Remove" batch path.
// Dataset B has no Data Link (channel expansion disabled + hint).
// Dataset A has TWO cart entries (a base entry + an already-checked "GFP"
// channel entry), to exercise the multi-entry "Remove" batch path.
// Dataset B is a plain single-entry dataset - both expand identically now
// that metadata is fetched from the internal /api/content URL rather than
// a Data Link.
const cartABase: CartItem = {
fsp_name: 'fsp1',
path: '/a',
Expand Down Expand Up @@ -92,25 +93,17 @@ vi.mock('@/omezarr-helper', () => ({
getResolvedScales: () => [1, 0.65, 0.65],
translateUnitToNeuroglancer: (unit?: string) => unit ?? ''
}));
vi.mock('@/queries/proxiedPathQueries', () => ({
useAllProxiedPathsQuery: () => ({
data: [
{
fsp_name: 'fsp1',
path: '/a',
url: 'https://data.example/a',
sharing_key: 'k1'
}
],
error: null,
isPending: false
})
}));
vi.mock('@/components/ui/Views/CreateViewButton', () => ({
default: ({ label }: { label?: string }) => (
<button type="button">{label ?? 'Create View'}</button>
)
}));
vi.mock('@/hooks/useCartDimensionCheck', () => ({
useCartDimensionCheck: () => ({
mismatchedKeys: new Set(),
hasMismatch: false
})
}));

import CartList from '@/components/ui/Views/CartList';

Expand Down Expand Up @@ -140,18 +133,20 @@ describe('Layer Cart tab', () => {
expect(screen.getByText('Dataset B')).toBeInTheDocument();
});

it('lazy-loads and shows channels when expanding a dataset with a Data Link', async () => {
it('lazy-loads and shows channels when expanding a dataset', async () => {
const user = await renderCartTab();
await user.click(screen.getByRole('button', { name: 'Dataset A' }));

await waitFor(() => {
expect(getOmeZarrChannels).toHaveBeenCalledWith('https://data.example/a');
expect(getOmeZarrChannels).toHaveBeenCalledWith(
expect.stringContaining('/api/content/fsp1/a')
);
});
expect(await screen.findByText('DAPI')).toBeInTheDocument();
expect(screen.getByText('GFP')).toBeInTheDocument();
});

it('lazy-loads and shows the axis table when expanding a dataset with a Data Link', async () => {
it('lazy-loads and shows the axis table when expanding a dataset', async () => {
getOmeZarrMetadata.mockResolvedValueOnce({
shapes: [[3, 2048, 2048]],
arr: { chunks: [1, 512, 512] },
Expand All @@ -172,7 +167,9 @@ describe('Layer Cart tab', () => {
await user.click(screen.getByRole('button', { name: /Dataset A/ }));

await waitFor(() => {
expect(getOmeZarrMetadata).toHaveBeenCalledWith('https://data.example/a');
expect(getOmeZarrMetadata).toHaveBeenCalledWith(
expect.stringContaining('/api/content/fsp1/a')
);
});
expect(await screen.findByText('Chunk Size')).toBeInTheDocument();
});
Expand All @@ -193,14 +190,30 @@ describe('Layer Cart tab', () => {
expect(screen.queryByText(/×/)).not.toBeInTheDocument();
});

it('disables expansion and shows a hint for a dataset with no Data Link', async () => {
await renderCartTab();
const expandButton = screen.getByRole('button', { name: 'Dataset B' });
expect(expandButton).toBeDisabled();
it('shows a "no OME-Zarr metadata" message for a plain (non-OME) array', async () => {
// Plain Zarr array: no channels and getOmeZarrMetadata throws (no
// multiscale group), so the expanded body has nothing to show.
getOmeZarrChannels.mockResolvedValueOnce([]);
getOmeZarrMetadata.mockRejectedValueOnce(new Error('not ome-zarr'));
const user = await renderCartTab();
await user.click(screen.getByRole('button', { name: 'Dataset B' }));

expect(
screen.getByText(/channels load after the view is created/i)
await screen.findByText('No OME-Zarr metadata to display.')
).toBeInTheDocument();
expect(getOmeZarrChannels).not.toHaveBeenCalled();
});

it('expands a dataset that has no Data Link (metadata fetched via /api/content)', async () => {
const user = await renderCartTab();
const expandButton = screen.getByRole('button', { name: 'Dataset B' });
expect(expandButton).not.toBeDisabled();
await user.click(expandButton);

await waitFor(() => {
expect(getOmeZarrChannels).toHaveBeenCalledWith(
expect.stringContaining('/api/content/fsp2/b')
);
});
});

it('toggling a channel checkbox adds a channel-specific CartItem', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ vi.mock('react-router', () => ({ useNavigate: () => vi.fn() }));
vi.mock('@/contexts/CartContext', () => ({
useCartContext: () => ({ clearCart: vi.fn().mockResolvedValue(undefined) })
}));
vi.mock('@/hooks/useCartDimensionCheck', () => ({
useCartDimensionCheck: () => ({
mismatchedKeys: new Set(),
hasMismatch: false
})
}));

import CreateViewButton from '@/components/ui/Views/CreateViewButton';

Expand Down
6 changes: 3 additions & 3 deletions frontend/src/__tests__/componentTests/MainLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { ReactNode } from 'react';

// MainLayout composes ~a dozen context providers unrelated to this test;
// stub them all as passthroughs so we can assert on the one thing that
// changed: the navbar is no longer skipped for /view/:readKey.
// changed: the navbar is now skipped for /view/:readKey.
// vi.mock factories are hoisted above imports, so the shared stub must be
// created via vi.hoisted rather than a plain top-level const.
const { passthrough } = vi.hoisted(() => ({
Expand Down Expand Up @@ -69,7 +69,7 @@ vi.mock('@/contexts/ViewersContext', () => ({ ViewersProvider: passthrough }));
import { MainLayout } from '@/layouts/MainLayout';

describe('MainLayout', () => {
it('renders the navbar on the embedded viewer route (/view/:readKey)', () => {
it('suppresses the navbar on the embedded viewer route (/view/:readKey)', () => {
render(
<MemoryRouter initialEntries={['/view/abc123']}>
<Routes>
Expand All @@ -79,7 +79,7 @@ describe('MainLayout', () => {
</Routes>
</MemoryRouter>
);
expect(screen.getByTestId('navbar')).toBeInTheDocument();
expect(screen.queryByTestId('navbar')).not.toBeInTheDocument();
});

it('still renders the navbar on an ordinary route', () => {
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/__tests__/componentTests/NGViews.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ vi.mock('@/queries/proxiedPathQueries', () => ({
vi.mock('@/components/ui/Views/CreateViewButton', () => ({
default: () => <button type="button">Create View</button>
}));
vi.mock('@/contexts/PreferencesContext', () => ({
usePreferencesContext: () => ({ pathPreference: ['linux_path'] })
}));
vi.mock('@/contexts/ZonesAndFspMapContext', () => ({
useZoneAndFspMapContext: () => ({ zonesAndFspQuery: { data: {} } })
}));

import NGViews from '@/components/NGViews';

Expand All @@ -53,7 +59,7 @@ describe('NGViews page', () => {
<NGViews />
</MemoryRouter>
);
expect(screen.getByText('Neuroglancer Views')).toBeInTheDocument();
expect(screen.getByText('Views')).toBeInTheDocument();
expect(screen.getByText('Seeded View')).toBeInTheDocument();
});
});
8 changes: 0 additions & 8 deletions frontend/src/__tests__/componentTests/NavbarBadge.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,6 @@ vi.mock('@/hooks/useCartCount', () => ({
useCartCount: () => 0
}));

vi.mock('@/hooks/useTheme', () => ({
default: vi.fn(() => ({
toggleTheme: vi.fn(),
isLightTheme: true,
setIsLightTheme: vi.fn()
}))
}));

vi.mock('@/utils/fathom', () => ({
trackEvent: vi.fn()
}));
Expand Down
10 changes: 7 additions & 3 deletions frontend/src/__tests__/componentTests/NeuroglancerView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ const { copyToClipboard } = vi.hoisted(() => ({
}));
vi.mock('@/utils/copyText', () => ({ copyToClipboard }));

vi.mock('@/components/ui/Navbar/ProfileMenu', () => ({
default: () => <div data-testid="profile-menu" />
}));

import NeuroglancerView from '@/components/NeuroglancerView';

describe('NeuroglancerView', () => {
Expand Down Expand Up @@ -73,18 +77,18 @@ describe('NeuroglancerView', () => {
screen.getByRole('button', { name: /download json/i })
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /open external/i })
screen.getByRole('button', { name: /open in neuroglancer/i })
).toBeInTheDocument();
});

it('shows a breadcrumb linking back to the NG Views list', () => {
it('shows a breadcrumb linking back to the Views list', () => {
useViewStateByReadKey.mockReturnValue({
data: { title: 'My View', layers: [{ name: 'L0' }] },
isPending: false,
isError: false
});
render(<NeuroglancerView />);
const crumbLink = screen.getByRole('link', { name: /ng views/i });
const crumbLink = screen.getByRole('link', { name: /^views$/i });
expect(crumbLink).toHaveAttribute('href', '/ngviews');
});

Expand Down
35 changes: 32 additions & 3 deletions frontend/src/__tests__/componentTests/ngViewsColumns.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,28 @@ vi.mock('@/queries/proxiedPathQueries', () => ({
]
})
}));
vi.mock('@/contexts/PreferencesContext', () => ({
usePreferencesContext: () => ({ pathPreference: ['linux_path'] })
}));
vi.mock('@/contexts/ZonesAndFspMapContext', () => ({
useZoneAndFspMapContext: () => ({
zonesAndFspQuery: {
// key format is `fsp_<name>` (see makeMapKey)
data: {
fsp_nrs: {
zone: 'z',
name: 'nrs',
group: '',
storage: '',
mount_path: '/nrs',
linux_path: '/nrs',
mac_path: null,
windows_path: null
}
}
}
})
}));

const view: View = {
short_key: 'k1',
Expand Down Expand Up @@ -66,7 +88,13 @@ function TableProbe({
}) {
// ponytail: TableProbe is already a component, so call the hook directly
// rather than nesting renderHook inside a component under render().
const columns = useNGViewsColumns(onRename, onDelete, 'https://ng.example/');
const columns = useNGViewsColumns(
onRename,
onDelete,
'https://ng.example/',
320,
() => {}
);
const table = useReactTable({
data: [view],
columns,
Expand Down Expand Up @@ -110,10 +138,11 @@ describe('useNGViewsColumns', () => {
<TableProbe onDelete={vi.fn()} onRename={vi.fn()} />
</MemoryRouter>
);
const link = screen.getByText('dudman/reg.zarr/g1_r0');
// Sources show the full path (file share path + subpath), not just the subpath.
const link = screen.getByText('/nrs/dudman/reg.zarr/g1_r0');
expect(link).toBeInTheDocument();
expect(link.closest('a')).toHaveAttribute('href');
expect(screen.getByText('dudman/reg.zarr/g1_r1')).toBeInTheDocument();
expect(screen.getByText('/nrs/dudman/reg.zarr/g1_r1')).toBeInTheDocument();
});

it('fires onRename and onDelete from the actions menu', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ vi.mock('@/queries/proxiedPathQueries', () => ({
useAllProxiedPathsQuery: () => ({ data: [] })
}));
vi.mock('react-router', () => ({ useNavigate: () => vi.fn() }));
vi.mock('@/hooks/useCartDimensionCheck', () => ({
useCartDimensionCheck: () => ({
mismatchedKeys: new Set(),
hasMismatch: false
})
}));

import { useCreateViewFlow } from '@/hooks/useCreateViewFlow';

Expand Down
Loading
Loading