Skip to content

Open editable catalog workspaces by default#44

Merged
Fredoq merged 3 commits into
mainfrom
release-ui-import-polish
May 29, 2026
Merged

Open editable catalog workspaces by default#44
Fredoq merged 3 commits into
mainfrom
release-ui-import-polish

Conversation

@Fredoq
Copy link
Copy Markdown
Owner

@Fredoq Fredoq commented May 29, 2026

Summary

  • load full editable workspaces by default for entity routes instead of the compact server search view
  • keep /catalog as the server-backed global search surface
  • remove the Load editable view transition and keep catalog graph details readable
  • update routing and workspace tests for the new default behavior
  • address review feedback for stale cover state, duplicate role badges, and brittle navigation assertions

Verification

  • npm run format:check
  • npm run lint
  • npm run typecheck
  • npm run file-size:check
  • npm test -- src/App.catalog-actions.test.tsx src/App.catalog-server-detail.test.tsx src/App.server-navigation.test.tsx
  • npm test
  • npm run build

Summary by CodeRabbit

Release Notes

  • Tests

    • Updated test coverage for editable workspace navigation and catalog loading flows
  • New Features

    • Enhanced release detail panels with improved artist credit display and cover management controls
  • Refactor

    • Improved formatting for role labels in search results and detail views
    • Streamlined catalog navigation and workspace transition behavior

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

📝 Walkthrough

Walkthrough

This PR enhances the catalog workspace UI to support dictionary-aware role formatting, release cover upload/removal, and full-catalog loading while migrating app tests from server-backed to editable-workspace patterns. New display label utilities enable consistent dictionary-backed formatting across role facets and graph relations.

Changes

Catalog UI Enhancement: Dictionaries, Release Covers, and Editable Workspaces

Layer / File(s) Summary
Display labels and formatting utilities
src/features/catalog/catalogDisplayLabels.ts
New module provides dictionary-backed formatting for matched fields, role facets, graph relations, and artist-role detection. Helpers normalize labels for consistent equality checks and humanize unknown identifiers.
Release client API for cover loading
src/features/catalog/api/releaseClient.ts
Adds loadRelease(releaseId) function to fetch release details by ID, supporting async cover image loading in the graph detail panel.
Component contracts and type updates
src/features/catalog/FilterSelect.tsx, src/app/renderWorkspace.tsx
FilterSelect gains optional options array for pre-computed option objects. Release cover callbacks updated to return `Promise
Workspace component props threading
src/features/catalog/CatalogWorkspace.tsx, src/features/catalog/ServerCatalogWorkspace.tsx, src/features/catalog/ServerEntityWorkspace.tsx
Components updated to accept optional dictionaries and onRemoveReleaseCover/onUploadReleaseCover callbacks, threading them down to child components (FilterBar, Table, GraphDetailPanel).
Server catalog controls with dictionary-aware roles
src/features/catalog/ServerCatalogControls.tsx
FilterBar and Table updated to use dictionaries for formatting role facet options. Introduces uniqueRoleOptions helper to build role option objects using display label formatters. Table conditionally renders Type/Context columns based on provided flags.
Graph detail panel with release covers and artist credits
src/features/catalog/CatalogGraphDetailPanel.tsx
GraphDetailPanel refactored to accept dictionaries and cover callbacks. Renders new ServerReleaseCoverPanel for releases with async cover loading and refresh-on-mutation. Introduces ArtistCreditsSection to merge and deduplicate artist credits with role badges. Enhanced GraphSection with dictionary-aware link grouping, redundant header suppression, and formatted subtitles.
Full catalog loading state management
src/app/AuthenticatedApp.tsx
Introduces fullCatalogPending state for routes requiring full catalog. Workspace rendering conditionally shows error panel or loading status. onRemoveReleaseCover handler returns promise. fullCatalogRoutes set to manualEntryRoutes to qualify more routes.
ReleaseCoverPanel export and prop tightening
src/features/releases/ReleaseDetail.tsx
ReleaseCoverPanel exported from module with tightened prop type (`Pick<ReleaseRecord, 'coverImage'
Test suite migration to editable-workspace flows
src/App.*.test.tsx
App integration tests refocused from server-backed to editable-workspace patterns. Added requestUrls helpers to normalize fetch mock call inspection. Artists/relations/releases navigation now validates editable API endpoints (/api/artists, /api/artist-relations, /api/releases) without /api/search calls. Catalog-actions test validates merged artist credits and release details rendering. Owned-items test validates full catalog hydration and linked details.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • Fredoq/cratebase-web#37: Updates editable/workspace navigation flow and full-catalog loading behavior alignment across test files and core app logic.
  • Fredoq/cratebase-web#23: Implements release cover UI and thumbnails; this PR wires cover upload/removal callbacks through the workspace component tree and implements ServerReleaseCoverPanel.
  • Fredoq/cratebase-web#41: Updates search v1 UI filtering and catalog search controls that this PR builds upon with dictionary-aware role facet rendering.

Poem

🐰 Through dictionaries we thread the way,
Covers load when artists come to play,
Deduplicated credits shine so bright,
Editable workspaces take their flight! 🎨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: switching to opening editable catalog workspaces by default instead of the server-backed view.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description follows the required template structure with Summary, Verification sections, and all specified verification checks listed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release-ui-import-polish

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 584e660b2e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +287 to +290
queueMicrotask(() => {
if (isCurrent) {
setCoverLoadStatus('loading')
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear stale cover state when loading another release

When the selected release changes, this effect only flips the status to loading and leaves the previous coverImage in state until loadRelease settles; if the new request is slow or fails, the detail panel for Release B can show Release A's cover under B's title. Clear coverImage when starting a load (or key this panel by releaseId) so release detail panels never display a stale cover.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
src/features/catalog/CatalogGraphDetailPanel.tsx (3)

453-453: 💤 Low value

Redundant special-case check: mediacoverage === mediacoverage is always true.

Line 453 compares normalizedTitle === 'mediacoverage' and normalizedLabel === 'mediacoverage', which is equivalent to the general case already checked on line 449. This special case has no effect and can be removed.

♻️ Proposed cleanup
     (normalizedTitle === 'tracks' && normalizedLabel === 'tracklist') ||
     (normalizedTitle === 'labels' && normalizedLabel === 'label') ||
-    (normalizedTitle === 'ownedcopies' && normalizedLabel === 'ownedcopy') ||
-    (normalizedTitle === 'mediacoverage' && normalizedLabel === 'mediacoverage')
+    (normalizedTitle === 'ownedcopies' && normalizedLabel === 'ownedcopy')
   )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/catalog/CatalogGraphDetailPanel.tsx` at line 453, Remove the
redundant special-case comparison in CatalogGraphDetailPanel: the conditional
fragment "(normalizedTitle === 'mediacoverage' && normalizedLabel ===
'mediacoverage')" is always true when the general check already matches and
should be deleted; update the conditional/boolean expression in the render logic
(where normalizedTitle and normalizedLabel are used) to omit this redundant
clause so behavior remains unchanged, leaving only the general case handling.

344-349: ⚡ Quick win

Potential duplicate loading and error messages.

ReleaseCoverPanel already manages and displays loading/error states (lines 386-387 in ReleaseDetail.tsx). This section adds additional status messages for cover loading, which could result in two concurrent "Loading cover..." or error messages on screen.

♻️ Consider removing duplicate status UI

If ReleaseCoverPanel already handles upload/removal feedback, you can likely remove lines 344-349 or conditionally show them only during initial load (not during mutation pending states).

       />
-      {coverLoadStatus === 'loading' ? (
-        <p role="status">Loading cover...</p>
-      ) : null}
-      {coverLoadStatus === 'error' ? (
-        <p role="alert">Cover image could not be loaded.</p>
-      ) : null}
     </section>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/catalog/CatalogGraphDetailPanel.tsx` around lines 344 - 349,
Remove the duplicate cover loading/error messages in CatalogGraphDetailPanel by
deleting or gating the coverLoadStatus conditional block (the JSX that renders
"<p role='status'>Loading cover...</p>" and "<p role='alert'>Cover image could
not be loaded.</p>") since ReleaseCoverPanel already presents those states;
alternatively, if you need an initial-load-only message, change the condition to
show those messages only when coverLoadStatus indicates initial fetch (e.g., not
when a mutation/upload is pending) so ReleaseCoverPanel remains the single
source of truth for upload/removal feedback.

390-396: ⚡ Quick win

Inconsistent "Artist" default role replacement logic.

The code removes the default "Artist" role only when roles has exactly one entry and that entry is "Artist" (line 390). If an artist already has ["Artist", "Vocalist"] and a new role like "Producer" is detected, the result becomes ["Artist", "Vocalist", "Producer"], keeping the generic "Artist" alongside more specific roles. This inconsistency could confuse users.

♻️ Proposed fix for consistent default-role handling

Remove the default "Artist" role whenever a more specific role is added, not just when it's the only role:

       const role = formatRoleFacet(candidate ?? '', dictionaries)
       if (!existing.roleSet.has(role)) {
-        if (existing.roles.length === 1 && existing.roles[0] === 'Artist') {
+        if (existing.roles.includes('Artist')) {
           existing.roles = []
+          existing.roleSet.clear()
         }
         existing.roleSet.add(role)
         existing.roles.push(role)
       }
     }

Alternatively, only add the default "Artist" role at line 398-400 if no other roles were ever added during the loop.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/catalog/CatalogGraphDetailPanel.tsx` around lines 390 - 396, The
current logic in CatalogGraphDetailPanel (inside the block that updates
existing.roleSet and existing.roles) only removes the default "Artist" role when
it's the sole entry; change it so that whenever you add a new role that is not
"Artist" you remove any existing "Artist" entry (e.g., adjust the code around
existing.roleSet.add(role) / existing.roles.push(role) to check if role !==
'Artist' and then remove 'Artist' from both existing.roleSet and
existing.roles), or alternatively defer adding the default "Artist" role until
after the loop and only add it if existing.roles is empty; update the logic in
the same function to maintain consistency between existing.roleSet and
existing.roles.
src/App.server-navigation.test.tsx (1)

73-81: ⚡ Quick win

Make hydration assertion less brittle and await post-navigation UI deterministically.

Hard-coding 13 ties this test to internal request fan-out and can break on harmless API call changes. Also prefer awaiting the Tracks heading after click.

Proposed test hardening
-    await h.waitFor(() => {
-      expect(fetchMock.mock.calls).toHaveLength(13)
-    })
+    await h.waitFor(() => {
+      expect(fetchMock.mock.calls.length).toBeGreaterThan(0)
+    })
+    const callCountAfterReleases = fetchMock.mock.calls.length

     await user.click(h.screen.getByRole('link', { name: 'Tracks' }))

-    expect(
-      h.screen.getByRole('heading', { name: 'Track records' }),
-    ).toBeInTheDocument()
-    expect(fetchMock.mock.calls).toHaveLength(13)
+    expect(
+      await h.screen.findByRole('heading', { name: 'Track records' }),
+    ).toBeInTheDocument()
+    expect(fetchMock.mock.calls).toHaveLength(callCountAfterReleases)

As per coding guidelines, "Use Testing Library for user-observable behavior; avoid tests that only verify implementation details".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/App.server-navigation.test.tsx` around lines 73 - 81, Replace the brittle
hard-coded fetch call count check and the synchronous heading assertion with a
deterministic wait for the post-navigation UI: after calling user.click(...) on
the 'Tracks' link (the user.click call in this test), remove or stop asserting
expect(fetchMock.mock.calls).toHaveLength(13) and instead await the UI change
using Testing Library (e.g., await h.screen.findByRole('heading', { name: 'Track
records' }) or await waitFor(() => expect(h.screen.getByRole('heading', { name:
'Track records' })).toBeInTheDocument())), keeping fetchMock usage only for
meaningful, behavior-level assertions if needed; reference fetchMock.mock.calls,
user.click, and the 'Track records' heading to locate the lines to edit.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/App.catalog-actions.test.tsx`:
- Around line 647-760: The test file exceeded the size guideline because three
large helper fixtures (searchResponseWithRelease,
graphResponseForReleaseWithDuplicateArtists, releaseDetailWithoutCover) are
defined inline; extract these functions into a new shared test fixtures module
(e.g., create test/catalogFixtures.ts), export them, then replace the inline
definitions by importing the exported helpers into
src/App.catalog-actions.test.tsx and update any references to call the imported
functions; ensure the new module exports the exact function names and that any
helper utilities used inside them (like h.jsonResponse) are available or
imported in the new file.

In `@src/features/catalog/ServerCatalogControls.tsx`:
- Around line 369-376: The rendered badges can produce duplicate keys because
formatRoleFacet(result.facets.roles) may map different raw roles to the same
display string; fix by deduplicating the formatted labels before passing them
into BadgeList. In the ServerCatalogControls.tsx cell that uses
result.facets.roles and formatRoleFacet, transform roles to formatted strings,
remove duplicates (e.g., via a Set while preserving order), then pass the
deduped array into <BadgeList values={...} ... /> so each badge key/value is
unique.

In `@src/features/catalog/ServerEntityWorkspace.tsx`:
- Around line 506-521: Extract the duplicate uniqueRoleOptions helper from
ServerEntityWorkspace.tsx (and the copy in ServerCatalogControls.tsx) into a
shared module (e.g. catalogDisplayLabels.ts) and update both files to import it;
keep the implementation using a Map and iterate over roles (drop the redundant
uniqueValues call). Also move the URL/param helpers parseEntitySearchParams,
buildEntityUrl and pushSelectionUrl into a separate module to reduce file size
and eliminate duplication, and update ServerEntityWorkspace.tsx to import those
helpers instead of defining them inline.

---

Nitpick comments:
In `@src/App.server-navigation.test.tsx`:
- Around line 73-81: Replace the brittle hard-coded fetch call count check and
the synchronous heading assertion with a deterministic wait for the
post-navigation UI: after calling user.click(...) on the 'Tracks' link (the
user.click call in this test), remove or stop asserting
expect(fetchMock.mock.calls).toHaveLength(13) and instead await the UI change
using Testing Library (e.g., await h.screen.findByRole('heading', { name: 'Track
records' }) or await waitFor(() => expect(h.screen.getByRole('heading', { name:
'Track records' })).toBeInTheDocument())), keeping fetchMock usage only for
meaningful, behavior-level assertions if needed; reference fetchMock.mock.calls,
user.click, and the 'Track records' heading to locate the lines to edit.

In `@src/features/catalog/CatalogGraphDetailPanel.tsx`:
- Line 453: Remove the redundant special-case comparison in
CatalogGraphDetailPanel: the conditional fragment "(normalizedTitle ===
'mediacoverage' && normalizedLabel === 'mediacoverage')" is always true when the
general check already matches and should be deleted; update the
conditional/boolean expression in the render logic (where normalizedTitle and
normalizedLabel are used) to omit this redundant clause so behavior remains
unchanged, leaving only the general case handling.
- Around line 344-349: Remove the duplicate cover loading/error messages in
CatalogGraphDetailPanel by deleting or gating the coverLoadStatus conditional
block (the JSX that renders "<p role='status'>Loading cover...</p>" and "<p
role='alert'>Cover image could not be loaded.</p>") since ReleaseCoverPanel
already presents those states; alternatively, if you need an initial-load-only
message, change the condition to show those messages only when coverLoadStatus
indicates initial fetch (e.g., not when a mutation/upload is pending) so
ReleaseCoverPanel remains the single source of truth for upload/removal
feedback.
- Around line 390-396: The current logic in CatalogGraphDetailPanel (inside the
block that updates existing.roleSet and existing.roles) only removes the default
"Artist" role when it's the sole entry; change it so that whenever you add a new
role that is not "Artist" you remove any existing "Artist" entry (e.g., adjust
the code around existing.roleSet.add(role) / existing.roles.push(role) to check
if role !== 'Artist' and then remove 'Artist' from both existing.roleSet and
existing.roles), or alternatively defer adding the default "Artist" role until
after the loop and only add it if existing.roles is empty; update the logic in
the same function to maintain consistency between existing.roleSet and
existing.roles.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2377b17b-93ce-4b57-9fed-2be48a522223

📥 Commits

Reviewing files that changed from the base of the PR and between ed85e81 and 584e660.

📒 Files selected for processing (17)
  • src/App.auth.test.tsx
  • src/App.catalog-actions.test.tsx
  • src/App.owned-items-inventory.test.tsx
  • src/App.relation-credit-navigation.test.tsx
  • src/App.search-v1.test.tsx
  • src/App.server-navigation.test.tsx
  • src/app/AuthenticatedApp.tsx
  • src/app/renderWorkspace.tsx
  • src/features/catalog/CatalogGraphDetailPanel.tsx
  • src/features/catalog/CatalogWorkspace.tsx
  • src/features/catalog/FilterSelect.tsx
  • src/features/catalog/ServerCatalogControls.tsx
  • src/features/catalog/ServerCatalogWorkspace.tsx
  • src/features/catalog/ServerEntityWorkspace.tsx
  • src/features/catalog/api/releaseClient.ts
  • src/features/catalog/catalogDisplayLabels.ts
  • src/features/releases/ReleaseDetail.tsx

Comment thread src/App.catalog-actions.test.tsx Outdated
Comment on lines +647 to +760
function searchResponseWithRelease() {
return h.jsonResponse({
items: [
{
id: 'release-stripped',
type: 'release',
title: 'Stripped',
subtitle: 'Mute',
summary: 'Imported single release.',
matchedFields: ['title', 'credit.role'],
snippets: ['Depeche Mode · Stripped'],
facets: {
roles: ['mainArtist'],
media: [],
statuses: [],
tags: [],
labelId: 'label-mute',
collectorSignals: [],
},
rank: 1,
},
],
limit: 100,
offset: 0,
total: 1,
})
}

function graphResponseForReleaseWithDuplicateArtists() {
return h.jsonResponse({
entity: {
id: 'release-stripped',
type: 'release',
title: 'Stripped',
subtitle: 'Mute',
summary: 'Imported single release.',
},
sections: {
artists: [
{
id: 'artist-depeche-mode',
type: 'artist',
title: 'Depeche Mode',
subtitle: 'Group',
relation: 'mainArtist',
},
],
credits: [
{
id: 'artist-depeche-mode',
type: 'artist',
title: 'Depeche Mode',
subtitle: 'mainArtist',
relation: 'credit',
},
],
releases: [],
tracks: [
{
id: 'track-stripped',
type: 'track',
title: 'Stripped',
subtitle: '1',
relation: 'tracklist',
},
],
ownedCopies: [],
labels: [
{
id: 'label-mute',
type: 'label',
title: 'Mute',
subtitle: 'BONG 010',
relation: 'label',
},
],
playlists: [],
relations: [],
media: [],
},
collectorSignals: [],
})
}

function releaseDetailWithoutCover() {
return h.jsonResponse({
id: 'release-stripped',
title: 'Stripped',
type: 'standalone',
year: 1986,
releaseDate: '1986-02-10',
genres: [],
tags: [],
coverImage: null,
isVariousArtists: false,
notOnLabel: false,
artistCredits: [
{
artistId: 'artist-depeche-mode',
artistName: 'Depeche Mode',
role: 'mainArtist',
},
],
labels: [
{
labelId: 'label-mute',
name: 'Mute',
catalogNumber: 'BONG 010',
hasNoCatalogNumber: false,
},
],
tracklist: [],
})
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

File exceeds size guideline.

The pipeline reports this file has grown to 760 lines, exceeding the 600-line limit. Consider extracting the new helper functions (searchResponseWithRelease, graphResponseForReleaseWithDuplicateArtists, releaseDetailWithoutCover) into a shared test fixtures module (e.g., test/catalogFixtures.ts) to keep test files focused and maintainable.

🧰 Tools
🪛 GitHub Actions: CI / 0_Verify.txt

[error] 760-760: file-size:check failed. File exceeds 600 lines (760 lines reported).

🪛 GitHub Actions: CI / Verify

[error] 760-760: file-size:check failed. File exceeds 600 lines (760).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/App.catalog-actions.test.tsx` around lines 647 - 760, The test file
exceeded the size guideline because three large helper fixtures
(searchResponseWithRelease, graphResponseForReleaseWithDuplicateArtists,
releaseDetailWithoutCover) are defined inline; extract these functions into a
new shared test fixtures module (e.g., create test/catalogFixtures.ts), export
them, then replace the inline definitions by importing the exported helpers into
src/App.catalog-actions.test.tsx and update any references to call the imported
functions; ensure the new module exports the exact function names and that any
helper utilities used inside them (like h.jsonResponse) are available or
imported in the new file.

Comment thread src/features/catalog/ServerCatalogControls.tsx
Comment thread src/features/catalog/ServerEntityWorkspace.tsx Outdated
@Fredoq Fredoq merged commit 48f68b0 into main May 29, 2026
2 checks passed
@Fredoq Fredoq deleted the release-ui-import-polish branch May 29, 2026 10:23
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.

1 participant