feat: folder view (browse library by folder structure) - #61
Conversation
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
|
Yippee! I'll give this a test drive tonight :) Thanks so much!! |
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
|
@DjunaPix ran a few more test and fixed a few more things. Please take a look and if all good for you, I merge :) |
There was a problem hiding this comment.
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
foldersview mode with URL state (folderparam) and UI components (FolderTree→ folder detail grid). - Adds backend support for folder browsing:
originalPathpersistence + 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
foldersview disables the tag filter UI, butselectedTagIDis only cleared when switching toalbum. 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.
- 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
|
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! |
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.
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:So
originalPathis normalized at sync time:libraryID) get the internal<UPLOAD_LOCATION>/library/<uuid>/prefix stripped, so they surface as clean year folders (2016/…,2017/…)./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
017:originalPathcolumn +(userID, originalPath)index.normalizeOriginalPathapplied in the sync pipeline, with a one-shot backfill (originalPathBackfillDone, mirroringlibraryIDBackfill) so existing installs repopulate on the next sync.GET /folders(tree, counts summed over descendants) andGET /folders/assets(recursive prefix match, paginated). The pathLIKEpattern escapes%/_somy_photoscannot match a siblingmyXphotos.startDate/endDate).Frontend
FolderTree(expand/collapse, per-folder counts) → click a folder → asset grid + breadcrumb back to the tree.selectedFolderPathpersisted 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
go build/go vet/go test ./...green. Tests cover path normalization, upload+external tree building, theLIKE-escape guard, recursion + pagination, and the backfill flag.tsc --noEmit,eslint, andnext buildall green.Known limitations (follow-ups)
These need folder-filter support on endpoints that don't have it yet:
folderPath)./folders/assetstakes notagId).Refs #29