Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1cb3217
chore: install shepherd.js and react-shepherd for user tour
allison-truhlar Jan 9, 2026
e20a8be
chore: add shepherd journey provider
allison-truhlar Jan 12, 2026
40b2342
feat: create tour for 3 documented fileglancer workflows
allison-truhlar Jan 12, 2026
1e28d2b
feat: add card to help page to trigger tours
allison-truhlar Jan 12, 2026
749b8fd
chore: add data ids to elements for tour anchors
allison-truhlar Jan 12, 2026
3dd1d1a
style: add custom styles to tour components to match existing styles
allison-truhlar Jan 12, 2026
b7e31cd
fix: tour step for non-janelia file system zarr/n5 description
allison-truhlar Jan 12, 2026
1b344dd
refactor: simplify StartTour by removing link options
allison-truhlar Jan 13, 2026
9e575fc
feat: add WelcomeCard and preference to show card
allison-truhlar Jan 13, 2026
71482ae
tests: add WelcomeCard and showTutorial preference tests
allison-truhlar Jan 13, 2026
fab3245
feat: add tour preference control to preference page
allison-truhlar Jan 13, 2026
0d2049f
style: reorganize and add section headers to preference pg
allison-truhlar Jan 13, 2026
d923fdc
refactor: export tour button configs for use in StartTour
allison-truhlar Jan 13, 2026
329524c
feat: provide a sample path to copy in the navigation tour
allison-truhlar Jan 13, 2026
d923370
refactor: enhance dashboard card styling flexibility
allison-truhlar Jan 13, 2026
c7dc5b2
chore: prettier formatting
allison-truhlar Jan 13, 2026
2752fb6
refactor: change dashboard to show all cards with msgs if empty
allison-truhlar Jan 13, 2026
68f180e
tests: update to match new wording on WelcomeTutorialCard
allison-truhlar Jan 13, 2026
c645527
chore: prettier formatting
allison-truhlar Jan 13, 2026
72785ce
style: make tour arrow more visible
allison-truhlar Jan 15, 2026
58d6996
style: add padding around tour target in overlay
allison-truhlar Jan 15, 2026
1b3a7aa
fix: remove conflicting min-width so that max-width:600px takes effect
allison-truhlar Jan 15, 2026
f31306c
style: change arrow color to purple
allison-truhlar Jan 16, 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
47 changes: 46 additions & 1 deletion frontend/package-lock.json

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

2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
"react-resizable-panels": "^3.0.2",
"react-router": "^7.12.0",
"react-router-dom": "^7.12.0",
"react-shepherd": "^6.1.9",
"react-syntax-highlighter": "^16.1.0",
"shepherd.js": "^14.5.1",
"tailwindcss": "^3.4.17",
"zarrita": "^0.5.1"
},
Expand Down
62 changes: 62 additions & 0 deletions frontend/src/__tests__/componentTests/Browse.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { waitFor } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { render, screen } from '@/__tests__/test-utils';
import Browse from '@/components/Browse';

// Mock the StartTour component to avoid ShepherdJourneyProvider dependency
vi.mock('@/components/tours/StartTour', () => ({
default: ({ children }: { children: React.ReactNode }) => (
<button>{children}</button>
)
}));

// Mock useOutletContext since Browse requires it
vi.mock('react-router', async () => {
const actual = await vi.importActual('react-router');
return {
...actual,
useOutletContext: () => ({
setShowPermissionsDialog: vi.fn(),
togglePropertiesDrawer: vi.fn(),
toggleSidebar: vi.fn(),
setShowConvertFileDialog: vi.fn(),
showPermissionsDialog: false,
showPropertiesDrawer: false,
showSidebar: false,
showConvertFileDialog: false
})
};
});

describe('Browse - WelcomeTutorialCard visibility', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('shows WelcomeTutorialCard on dashboard when showTutorial is true', async () => {
render(<Browse />, { initialEntries: ['/browse'] });

await waitFor(() => {
expect(screen.getByText('Welcome to Fileglancer!')).toBeInTheDocument();
});
});

it('does not show WelcomeTutorialCard after the user selects to hide it', async () => {
render(<Browse />, { initialEntries: ['/browse'] });
await waitFor(() => {
expect(screen.getByText('Welcome to Fileglancer!')).toBeInTheDocument();
});

const user = userEvent.setup();
const checkbox = screen.getByRole('checkbox', { name: /hide this card/i });
expect(checkbox).not.toBeChecked();
await user.click(checkbox);

await waitFor(() => {
expect(
screen.queryByText('Welcome to Fileglancer!')
).not.toBeInTheDocument();
});
});
});
102 changes: 102 additions & 0 deletions frontend/src/__tests__/componentTests/WelcomeTutorialCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { waitFor } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import toast from 'react-hot-toast';
import { render, screen } from '@/__tests__/test-utils';
import WelcomeTutorialCard from '@/components/ui/BrowsePage/Dashboard/WelcomeTutorialCard';

// Mock the StartTour component to avoid ShepherdJourneyProvider dependency
vi.mock('@/components/tours/StartTour', () => ({
default: ({ children }: { children: React.ReactNode }) => (
<button>{children}</button>
)
}));

describe('WelcomeTutorialCard', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('renders welcome message and tour button', async () => {
render(<WelcomeTutorialCard />, { initialEntries: ['/browse'] });

await waitFor(() => {
expect(screen.getByText('Welcome to Fileglancer!')).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /start tour/i })
).toBeInTheDocument();
});
});

it('displays checkbox with correct label and helper text', async () => {
render(<WelcomeTutorialCard />, { initialEntries: ['/browse'] });

await waitFor(() => {
expect(
screen.getByLabelText(/hide this card.*help page/i)
).toBeInTheDocument();
expect(
screen.getByText(/you can always access tutorials from the help page/i)
).toBeInTheDocument();
});
});

it('checkbox reflects showTutorial preference state', async () => {
render(<WelcomeTutorialCard />, { initialEntries: ['/browse'] });

const checkbox = await screen.findByLabelText(/hide this card.*help page/i);

// showTutorial is true by default, so checkbox should be unchecked (inverse)
expect(checkbox).not.toBeChecked();
});

it('toggles preference when checkbox is clicked and shows success toast', async () => {
render(<WelcomeTutorialCard />, { initialEntries: ['/browse'] });

const user = userEvent.setup();
const checkbox = await screen.findByLabelText(/hide this card.*help page/i);

await user.click(checkbox);

await waitFor(() => {
expect(toast.success).toHaveBeenCalledWith(
'Welcome card hidden. Access Tutorials from the Help page.'
);
});
});

it('shows error toast when preference update fails', async () => {
const { server } = await import('@/__tests__/mocks/node');
const { http, HttpResponse } = await import('msw');

server.use(
http.put('/api/preference/showTutorial', () => {
return HttpResponse.json({ error: 'Update failed' }, { status: 500 });
})
);

render(<WelcomeTutorialCard />, { initialEntries: ['/browse'] });

const user = userEvent.setup();
const checkbox = await screen.findByLabelText(/hide this card.*help page/i);

await user.click(checkbox);

await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith(`500 Internal Server Error:
Update failed`);
});
});

it('contains descriptive text about FileGlancer', async () => {
render(<WelcomeTutorialCard />, { initialEntries: ['/browse'] });

await waitFor(() => {
expect(
screen.getByText(
/fileglancer helps you browse.*visualize.*share.*scientific imaging data/i
)
).toBeInTheDocument();
});
});
});
1 change: 1 addition & 0 deletions frontend/src/__tests__/mocks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const handlers = [
disableNeuroglancerStateGeneration: { value: false },
useLegacyMultichannelApproach: { value: false },
isFilteredByGroups: { value: true },
showTutorial: { value: true },
layout: { value: '' },
zone: { value: [] },
fileSharePath: { value: [] },
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/assets/arrow-up.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions frontend/src/components/Browse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useOutletContext } from 'react-router';
import { default as log } from '@/logger';
import type { OutletContextType } from '@/layouts/BrowseLayout';
import { useFileBrowserContext } from '@/contexts/FileBrowserContext';
import { usePreferencesContext } from '@/contexts/PreferencesContext';
import FileBrowser from './ui/BrowsePage/FileBrowser';
import Toolbar from './ui/BrowsePage/Toolbar';
import RenameDialog from './ui/Dialogs/Rename';
Expand All @@ -11,6 +12,7 @@ import ChangePermissions from './ui/Dialogs/ChangePermissions';
import ConvertFileDialog from './ui/Dialogs/ConvertFile';
import RecentDataLinksCard from './ui/BrowsePage/Dashboard/RecentDataLinksCard';
import RecentlyViewedCard from './ui/BrowsePage/Dashboard/RecentlyViewedCard';
import WelcomeTutorialCard from './ui/BrowsePage/Dashboard/WelcomeTutorialCard';
import NavigationInput from './ui/BrowsePage/NavigateInput';
import FgDialog from './ui/Dialogs/FgDialog';

Expand All @@ -27,6 +29,7 @@ export default function Browse() {
} = useOutletContext<OutletContextType>();

const { fspName } = useFileBrowserContext();
const { showTutorial } = usePreferencesContext();

const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [showRenameDialog, setShowRenameDialog] = useState(false);
Expand Down Expand Up @@ -114,10 +117,12 @@ export default function Browse() {
{!fspName ? (
<div className="flex flex-col max-w-full gap-6 px-6">
<NavigationInput location="dashboard" />
{showTutorial ? <WelcomeTutorialCard /> : null}
<div
className={`flex gap-6 ${componentWidth > 800 ? '' : 'flex-col'}`}
>
<RecentlyViewedCard />

<RecentDataLinksCard />
</div>
</div>
Expand Down
22 changes: 20 additions & 2 deletions frontend/src/components/Help.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { SiClickup, SiSlack } from 'react-icons/si';
import { IconType } from 'react-icons/lib';
import { LuBookOpenText } from 'react-icons/lu';
import { HiExternalLink } from 'react-icons/hi';
import { MdTour } from 'react-icons/md';

import useVersionQuery from '@/queries/versionQuery';
import { buildUrl } from '@/utils';
import StartTour from '@/components/tours/StartTour';

type HelpLink = {
icon: IconType;
Expand Down Expand Up @@ -86,10 +88,26 @@ export default function Help() {
</Typography>
</div>
<div className="grid grid-cols-2 gap-10">
{/* Tour Card */}
<Card
as={StartTour}
className="group min-h-44 p-8 md:p-12 flex flex-col gap-2 text-left w-full hover:bg-surface-light dark:hover:bg-surface hover:border-surface"
>
<div className="flex items-center justify-start gap-2 w-full">
<MdTour className="hidden md:block icon-default lg:icon-large text-primary" />
<Typography className="text-base md:text-lg lg:text-xl text-primary font-semibold group-hover:underline">
Take a Tutorial
</Typography>
</div>
<Typography className="text-sm md:text-base text-foreground w-full">
Guided tours of common Fileglancer workflows
</Typography>
</Card>

{helpLinks.map(({ icon: Icon, title, description, url }) => (
<Card
as={Link}
className="group min-h-44 p-8 md:p-12 flex flex-col gap-2 text-left w-full hover:shadow-lg transition-shadow duration-200"
className="group min-h-44 p-8 md:p-12 flex flex-col gap-2 text-left w-full hover:shadow-lg transition-shadow duration-200 hover:bg-surface-light dark:hover:bg-surface"
key={url}
rel="noopener noreferrer"
target="_blank"
Expand All @@ -104,7 +122,7 @@ export default function Help() {
<HiExternalLink className="icon-xsmall md:icon-small text-primary" />
</div>
</div>
<Typography className="text-sm md:text-base text-muted-foreground">
<Typography className="text-sm md:text-base text-foreground">
{description}
</Typography>
</Card>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/Jobs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { jobsColumns } from './ui/Table/jobsColumns';
export default function Jobs() {
const { allTicketsQuery } = useTicketContext();
return (
<>
<div data-tour="tasks-page">
<Typography className="mb-6 text-foreground font-bold" type="h5">
Tasks
</Typography>
Expand All @@ -25,6 +25,6 @@ export default function Jobs() {
gridColsClass="grid-cols-[3fr_3fr_1fr_2fr]"
loadingState={allTicketsQuery.isPending}
/>
</>
</div>
);
}
Loading