Skip to content

feat: folder view (browse library by folder structure) - #61

Open
Majorfi wants to merge 6 commits into
mainfrom
feat/folder-view
Open

feat: folder view (browse library by folder structure)#61
Majorfi wants to merge 6 commits into
mainfrom
feat/folder-view

Conversation

@Majorfi

@Majorfi Majorfi commented Jul 28, 2026

Copy link
Copy Markdown
Owner

What

Adds a Folders view mode (alongside Timeline and Album) that organizes the library by its original directory structure as an expandable tree, drilling into a paginated asset grid. Requested in #29.

Draft — the core browse→geotag flow works end to end; a few integrations are still deferred (see Known limitations).

Design

The tree is derived from each asset's originalPath (no new tables — always consistent with the asset set, respects filters). A live probe against a real Immich v3 server showed that uploaded assets carry an internal path, not user folders:

/usr/src/app/upload/library/<userUUID>/2026/2026-07-24/L1002720.jpg   (libraryId=null)

So originalPath is normalized at sync time:

  • Uploaded assets (no libraryID) get the internal <UPLOAD_LOCATION>/library/<uuid>/ prefix stripped, so they surface as clean year folders (2016/…, 2017/…).
  • External-library assets keep their real filesystem path (/mnt/media/Photos/2023/Vacation/…).

Because the tree and the asset query both read the same stored originalPath, matching stays consistent with no per-request prefix logic.

Backend

  • Migration 017: originalPath column + (userID, originalPath) index.
  • normalizeOriginalPath applied in the sync pipeline, with a one-shot backfill (originalPathBackfillDone, mirroring libraryIDBackfill) so existing installs repopulate on the next sync.
  • GET /folders (tree, counts summed over descendants) and GET /folders/assets (recursive prefix match, paginated). The path LIKE pattern escapes %/_ so my_photos cannot match a sibling myXphotos.
  • All three global filters apply: GPS, hidden, and date range (startDate/endDate).

Frontend

  • New Folders entry in the view-mode selector → FolderTree (expand/collapse, per-folder counts) → click a folder → asset grid + breadcrumb back to the tree.
  • selectedFolderPath persisted in the URL, mutually exclusive with the album selection; tree and detail react to the GPS/hidden/date-range filters. Mirrors the album view throughout.

Tests / verification

  • Backend: go build / go vet / go test ./... green. Tests cover path normalization, upload+external tree building, the LIKE-escape guard, recursion + pagination, and the backfill flag.
  • Frontend: tsc --noEmit, eslint, and next build all green.
  • Exercised locally against a real Immich v3 server: full re-sync normalizes ~20k upload paths to year folders; the tree and date-range filter behave.

Known limitations (follow-ups)

These need folder-filter support on endpoints that don't have it yet:

  1. The map is not scoped to the selected folder (the grid is).
  2. No map→grid focus in folder detail (page-info has no folderPath).
  3. The tag filter is disabled in folder detail (/folders/assets takes no tagId).

Refs #29

Capture d’écran 2026-07-29 à 13 19 12

Majorfi added 2 commits July 28, 2026 22:48
Add a "Folders" view derived from each asset's originalPath, scoped to
external-library assets (libraryID NOT NULL). Uploaded assets carry an
internal Immich path (/usr/src/app/upload/library/<uuid>/YYYY/MM-DD/) that
is not a user-meaningful folder structure, so they are excluded.

- migration 017: originalPath column + (userID, originalPath) index
- sync originalPath through the asset pipeline (response -> row -> upsert
  -> scan -> map) plus a one-shot backfill (originalPathBackfillDone)
  mirroring the existing libraryID backfill
- GET /folders (tree) and GET /folders/assets (recursive, paginated); the
  path LIKE pattern escapes % and _ so "my_photos" cannot match "myXphotos"

Refs #29
Add a "Folders" view mode alongside timeline/album, mirroring the album
view: an expandable folder tree that drills into a paginated asset grid
with a breadcrumb back to the tree. selectedFolderPath is persisted in the
URL (mutually exclusive with the album selection) and the tree/detail react
to the GPS and hidden filters.

Refs #29
@DjunaPix

Copy link
Copy Markdown

Yippee! I'll give this a test drive tonight :)

Thanks so much!!

Majorfi added 2 commits July 29, 2026 12:52
Two follow-ups from testing on an upload-only account (the external-only
scope left the folder view empty):

- Drop the external-only restriction. Uploaded assets are now included,
  with their internal Immich prefix (<UPLOAD_LOCATION>/library/<uuid>/)
  stripped at sync time so they surface as year folders; external-library
  assets keep their real filesystem path.
- Thread startDate/endDate through getFolderTree/getFolderAssets and the
  handlers so the folder tree and detail honor the date-range filter.

Refs #29
getFolders/getFolderAssets now forward startDate/endDate; the folder tree
refetches when the date range changes and the folder detail passes it too.

Refs #29
@Majorfi

Majorfi commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

@DjunaPix ran a few more test and fixed a few more things. Please take a look and if all good for you, I merge :)

@Majorfi
Majorfi marked this pull request as ready for review July 29, 2026 11:19
@Majorfi
Majorfi requested a review from Copilot July 29, 2026 11:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Folders view mode to browse the catalog by original directory structure, backed by a new originalPath field in the backend and new /folders + /folders/assets endpoints. This integrates into existing filter/view state and routing so users can drill from an expandable tree into a paginated asset grid.

Changes:

  • Introduces a new folders view mode with URL state (folder param) and UI components (FolderTree → folder detail grid).
  • Adds backend support for folder browsing: originalPath persistence + normalization during sync, migration + index, and folder tree/assets endpoints.
  • Wires folder data into shared context/domain hooks and asset loading to support paginated folder detail queries.

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/utils/view.ts Adds folder URL param constant.
src/shared/types/view.ts Extends view mode union to include folders.
src/shared/types/folder.ts Adds shared folder tree/node types.
src/shared/types/context.ts Extends view/catalog context with folder selection and folder tree loading state.
src/shared/services/backendApi.ts Adds /folders and /folders/assets API clients.
src/shared/services/backendApi.guards.ts Adds runtime guards for folder tree payloads.
src/shared/context/useViewDomain.ts Adds folder selection to view domain and URL syncing.
src/shared/context/useProviderValues.ts Plumbs new folder-related context values through memoization.
src/shared/context/useCatalogDomain.ts Loads folder tree on-demand and passes folder filter into asset loading.
src/shared/context/useAppProviderState.ts Exposes folder state/actions from the app provider.
src/shared/components/PhotoListContainer.tsx Adds folder selection/back behavior and view-mode switching interactions.
src/shared/components/PhotoList.tsx Renders folder tree vs grid, adds folder breadcrumb naming and disables tag filter in folders mode.
src/features/photoGrid/useAssets.ts Routes asset paging to /folders/assets when a folder path is active.
src/features/folders/useFolders.ts Adds hook to fetch/reset the folder tree with filter dependencies.
src/features/folders/FolderTree.tsx Implements the expandable folder tree UI.
src/features/filterBar/useURLState.ts Persists/restores folder selection via URL state.
src/features/filterBar/constant.ts Adds Folders to view-mode selector labels/options.
src/app/page.tsx Treats folders tree as a “main catalog view” route state.
backend/types.go Adds OriginalPath to asset types and introduces folder tree response types.
backend/syncService.go Persists normalized originalPath during sync and adds backfill trigger flag logic.
backend/migrations/017_add_original_path.sql Adds originalPath column and index.
backend/migrations_test.go Updates stamped migration version to 17.
backend/main.go Registers /folders and /folders/assets routes.
backend/interfaces.go Extends store interfaces for folder queries and backfill detection.
backend/handlersFolders.go Adds folder endpoints with filter/date/pagination validation.
backend/databaseFolders.go Implements folder tree building, recursive folder asset querying, path normalization, and LIKE escaping.
backend/databaseFolders_test.go Adds tests for normalization, tree building, LIKE escaping, recursion, pagination, and backfill state.
backend/database.go Adds originalPath to asset selects/scans and upsert SQL.
Comments suppressed due to low confidence (1)

src/shared/components/PhotoListContainer.tsx:137

  • Switching into the new folders view disables the tag filter UI, but selectedTagID is only cleared when switching to album. This can leave a hidden active tag filter that still affects other data sources (e.g., map markers) while the folders grid ignores tags.

Clear selectedTagID when switching to folders as well (and do it in both the “detail open” and “no detail open” branches).

		if (selectedAlbumID || selectedFolderPath) {
			closeAlbumAndClearPending(() => {
				closeLightboxAction();
				selectAlbumAction(null);
				selectFolderAction(null);
				setViewModeAction(mode);
			});
			return;
		}
		closeLightboxAction();
		if (mode === 'album') {
			selectTagAction(null);
		}
		setViewModeAction(mode);

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/features/folders/FolderTree.tsx
Comment thread src/features/filterBar/useURLState.ts
Majorfi added 2 commits July 29, 2026 13:49
- Support the tag filter in the folder view: thread tagID through
  getFolderTree/getFolderAssets, handling the aliased JOIN that
  buildAssetFilter produces when a tag is set, and through both handlers.
- Dedupe date-range parsing: share parseDateRangeParams between
  handleGetAssets and the folder handlers, using errors.New.
- Document the unbounded originalPath fetch in getFolderTree.
- Add a tag-filter DB test and handler tests for /folders and
  /folders/assets (auth 401, invalid date 400, missing path 400).

Refs #29
- Send tagID from getFolders/getFolderAssets and thread it through useFolders
  and the folder-detail asset load; enable the tag filter UI in the folder view.
- FolderTree accessibility: keyboard support on tree rows (role/tabIndex,
  Enter/Space) and type + aria-label/aria-expanded on the expand toggle.
- Add a Retry button to the folder-tree load-error state.

Refs #29
@DjunaPix

Copy link
Copy Markdown

I've been testing some with Immich 3.10. All running smoothly and many thanks for these developments!

A few observations...

Image counts on folder view -- it would be nice if those could show the filtered counts of 'Missing location' or 'With location'.

When clicking between 'Missing location' and 'With location', it would be nice if folder tree view stayed at same level rather than jumping to the very top of the folder tree.

I'm confused by the number chip over 'Missing location' and 'With location' -- the values don't reflect my ~182,000 image collection.

A helpful add-on would be the addition of a 'Go to Folder' link when right-clicking an image on the map.

I'm generally confused about the functions of the Markers and Visibility settings, but I believe that predates folder view.

Thanks again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants