Skip to content

Manager: URL-based tag filter state + filter-aware initial story selection#34283

Merged
valentinpalkovic merged 11 commits into
nextfrom
valentin/url-based-tag-state
Mar 26, 2026
Merged

Manager: URL-based tag filter state + filter-aware initial story selection#34283
valentinpalkovic merged 11 commits into
nextfrom
valentin/url-based-tag-state

Conversation

@valentinpalkovic
Copy link
Copy Markdown
Contributor

@valentinpalkovic valentinpalkovic commented Mar 23, 2026

Closes #34252

What I did

Replaces localStorage persistence for tag filters with URL-based state. All filter state is now encoded in a single tags URL parameter (e.g. ?tags=a11y;$changed;!experimental). Also fixes the bug where initial story selection ignored active tag filters when navigating to Storybook without a story path.

Bildschirmaufnahme.2026-03-23.um.16.59.50.mov

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

Caution

This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!

  1. Run a sandbox: yarn task --task sandbox --start-from auto --template react-vite/default-ts
  2. Add a tag filter via the sidebar filter UI (e.g. include play)
  3. Verify the URL updates to ?tags=$play (or the appropriate tag encoding)
  4. Refresh the page — verify the same filter is restored from the URL
  5. Navigate to http://localhost:6006/?tags=play directly — verify only stories tagged play are shown and the first story/docs is auto-selected
  6. Clear all filters — verify the tags param is removed from the URL
  7. Click between sidebar entries repeatedly — verify tags param is preserved in the URL throughout

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update
    MIGRATION.MD

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Make sure this PR contains one of the labels below:

    Available labels - `bug`: Internal changes that fixes incorrect behavior. - `maintenance`: User-facing maintenance tasks. - `dependencies`: Upgrading (sometimes downgrading) dependencies. - `build`: Internal-facing build tooling & test updates. Will not show up in release changelog. - `cleanup`: Minor cleanup style change. Will not show up in release changelog. - `documentation`: Documentation **only** changes. Will not show up in release changelog. - `feature request`: Introducing a new feature. - `BREAKING CHANGE`: Changes that break compatibility in some way with current major version. - `other`: Changes that don't fit in the above categories.

🦋 Canary release

This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

core team members can create a canary release here or locally with gh workflow run --repo storybookjs/storybook publish.yml --field pr=<PR_NUMBER>

Summary by CodeRabbit

  • New Features

    • Tag filters now persist in the URL (bookmarkable/shareable) and are initialized from the URL on load
    • Query parameters are preserved during story navigation so filters/selections remain intact
    • Story selection respects active tag filters and will navigate to the first matching story/docs entry when needed
    • URL-backed persistence supports explicit adding/removing of query params (null clears a param)
    • Safer URL/query handling on load and navigation (handles missing location/search)
  • Tests

    • Added tests for tag filter parsing/serialization and URL query-parameter behavior, including navigation expectations

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Mar 23, 2026

View your CI Pipeline Execution ↗ for commit 17b4191

Command Status Duration Result
nx run-many -t compile,check,knip,test,lint,fmt... ✅ Succeeded 7m 17s View ↗

☁️ Nx Cloud last updated this comment at 2026-03-26 10:24:29 UTC

@valentinpalkovic valentinpalkovic marked this pull request as ready for review March 23, 2026 15:56
@valentinpalkovic valentinpalkovic self-assigned this Mar 23, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds URL-backed tag-filter persistence and query-parameter-aware navigation: new tag parse/serialize utilities, URL persistence handler registration, nullable/deletable query-param semantics, initial tag filters derived from the URL, and navigation updated to preserve and validate against URL tag filters.

Changes

Cohort / File(s) Summary
Tag parsing & serialization
code/core/src/manager-api/modules/tags.ts
New module mapping built-in URL tag names to internal tags and providing parseTagsParam(tagsParam) and serializeTagsParam(included, excluded) for URL ↔ internal tag translation.
Stories: selection & navigation
code/core/src/manager-api/modules/stories.ts
Added navigateWithQueryParams() to preserve custom query params during navigation; initialize tag filters from URL via parseTagsParam(); persist tag-filter updates with persistence: 'url'; adjust selectFirstStory and STORY_SPECIFIED to prefer filteredIndex and redirect when the requested item is excluded by active filters.
URL param handling
code/core/src/manager-api/modules/url.ts
Introduced QueryParamInput (`string
Store persistence plumbing
code/core/src/manager-api/store.ts
Added PersistenceHandler type, serialize option in Options, registerPersistenceHandler() and persistenceHandlers registry; accept persistence: 'url' and route patches to a registered URL handler; initial migration strips tag-filter keys from persisted local/session state.
Router util
code/core/src/router/utils.ts
Made queryFromLocation(location?) optional and defensively read location.search (falls back to empty string) to support calls without a full Location.
Tests: tags & url
code/core/src/manager-api/tests/tags.test.js, code/core/src/manager-api/tests/url.test.js
Added tests for parseTagsParam/serializeTagsParam (including built-in tag mappings and ignored empty segments); added test verifying applyQueryParams({ tags: null }) removes tags from customQueryParams and it is not reintroduced.
Tests: navigation expectations & store mocks
code/core/src/manager-api/tests/stories.test.ts, code/core/src/manager-api/tests/store.test.js
Updated navigation assertions to expect a second navigate argument (e.g., navigate('/path', undefined)); adjusted store tests to mock two read passes for local/session storage during initial migration/merge.

Sequence Diagram

sequenceDiagram
    participant Browser as Browser Location
    participant Stories as Stories Module
    participant URL as URL Module
    participant Store as Store/Persistence
    participant Navigation as Navigator

    Browser->>Stories: App load (with ?tags=...)
    Stories->>URL: queryFromLocation(location)
    URL-->>Stories: parsed query (tags param)
    Stories->>Stories: parseTagsParam(tags)
    Stories->>Store: init state with parsed tag filters

    Note over Stories,Store: user updates tag filters
    Stories->>Store: setState(patch, persistence: 'url')
    Store->>Store: invoke registered 'url' handler with delta + serialize
    Store->>URL: handler.applyQueryParams(serialized)
    URL->>Navigation: navigate(updated URL with tags)
    Navigation-->>Browser: URL updated

    Note over Stories: user selects a story
    Stories->>Stories: navigateWithQueryParams(path)
    Stories->>URL: merge current customQueryParams into path
    URL->>Navigation: navigate(path + preserved params)
    Navigation-->>Browser: URL includes story route + tags
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs


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

Copy link
Copy Markdown
Contributor

@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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code/core/src/manager-api/modules/stories.ts`:
- Around line 1179-1183: The URL-parsed tag filters (computed via
queryFromLocation and parseTagsParam into initialIncluded/initialExcluded when
hasTagsParam is true) are being overridden by legacy persisted keys merged by
Store.getInitialState (store.local / store.session) so the active filters stay
out-of-sync; modify startup behavior so when hasTagsParam is true you ignore or
scrub legacy persisted includedTagFilters/excludedTagFilters instead of letting
Store.getInitialState merge them: either (A) update Store.getInitialState to
skip merging those two keys from store.local/session when a tags param is
present, or (B) add a one-time migration in the stories module startup to
delete/clear store.local.storeKeys for includedTagFilters/excludedTagFilters
before state merge, or (C) ensure the module’s initial state explicitly sets
includedTagFilters/excludedTagFilters to parsedTags.included/parsedTags.excluded
after Store.getInitialState runs when hasTagsParam is true so URL-derived values
win.
- Around line 967-975: The fallback redirect uses viewMode blindly which can
misroute a docs entry; change the fallback to derive the route from
filteredIndex[firstFiltered]. Retrieve const entry =
filteredIndex[firstFiltered]; determine routeMode = entry && entry.type ===
'docs' ? 'docs' : 'story' (or only allow 'story' if you prefer restricting),
then call navigateWithQueryParams(`/${routeMode}/${firstFiltered}`) instead of
using viewMode; ensure you guard for entry existence before navigating.

In `@code/core/src/manager-api/modules/tags.ts`:
- Around line 39-42: The serialization currently returns undefined when both
included and excluded are empty which collapses an explicit “no tags” selection
into an absent tags param; change serializeTagsParam (in
code/core/src/manager-api/modules/tags.ts) to return an explicit empty string
('') when included.length===0 && excluded.length===0 so the URL-writing path can
preserve the empty value instead of dropping the param, and ensure this aligns
with parseTagsParam so parseTagsParam('') continues to mean “no filters” while
undefined remains “no tags param / use presets” (verify any callers in
stories.ts that write the tags param do not treat '' as missing).

In `@code/core/src/manager-api/tests/url.test.js`:
- Around line 126-143: The test seeds store.state with only customQueryParams
causing applyQueryParams (api.applyQueryParams) to call store.getState() and
access location.path and location.hash on an undefined location; fix by
initializing the store with the full module state shape (including location: {
path, search, hash }) before calling initURL/api.applyQueryParams so getState()
returns a location object—update the test's store or initial state passed to
initURL to include location.path and location.hash alongside customQueryParams.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0d4be47d-677e-4e55-b652-7d025048af77

📥 Commits

Reviewing files that changed from the base of the PR and between c25004a and e73920d.

📒 Files selected for processing (7)
  • code/core/src/manager-api/modules/stories.ts
  • code/core/src/manager-api/modules/tags.ts
  • code/core/src/manager-api/modules/url.ts
  • code/core/src/manager-api/store.ts
  • code/core/src/manager-api/tests/tags.test.js
  • code/core/src/manager-api/tests/url.test.js
  • code/core/src/router/utils.ts

Comment thread code/core/src/manager-api/modules/stories.ts
Comment thread code/core/src/manager-api/modules/stories.ts
Comment thread code/core/src/manager-api/modules/tags.ts Outdated
Comment thread code/core/src/manager-api/tests/url.test.js
Copy link
Copy Markdown
Contributor

@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: 1

🧹 Nitpick comments (1)
code/core/src/manager-api/store.ts (1)

142-150: Consider logging when URL persistence handler is missing.

If the 'url' handler isn't registered (e.g., due to initialization order issues), the persistence operation silently no-ops. This could make debugging difficult. A warning log would surface misconfiguration early.

💡 Optional: Add a warning when handler is missing
       if (persistence === 'url') {
         const handler = this.persistenceHandlers.get('url');
         if (handler) {
           await handler(delta, (options as Options | undefined)?.serialize);
+        } else if (process.env.NODE_ENV !== 'production') {
+          console.warn('URL persistence handler not registered');
         }
       } else {

Note: Per coding guidelines, consider using @storybook/internal/client-logger instead of console.warn if readily available in this module.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/manager-api/store.ts` around lines 142 - 150, When persistence
=== 'url' and this.persistenceHandlers.get('url') returns undefined, add a
warning log so the missing handler doesn't silently no-op; update the block
around persistenceHandlers/get('url') (the handler variable and its conditional)
to log a warning that the 'url' persistence handler is not registered and
include context (e.g., persistence value and any relevant options). Use
`@storybook/internal/client-logger.warn` if that module is available in this file,
otherwise fall back to console.warn; do not change the existing behavior of
calling handler(delta, serialize) when present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code/core/src/manager-api/store.ts`:
- Around line 81-94: The migration removes tag-filter keys after taking
snapshots (local and session), so the returned initial state still includes the
old includedTagFilters/excludedTagFilters; update the logic in the store
initialization to either (A) re-read storage after the cleanup loop (call
get(store.local) and get(store.session) again and use those for the returned
merge) or (B) explicitly strip includedTagFilters/excludedTagFilters from the
previously-captured local and session objects before returning; reference the
existing symbols local, session, store.local, store.session, get, set, and the
final return that merges base, local, session to locate where to apply the fix.

---

Nitpick comments:
In `@code/core/src/manager-api/store.ts`:
- Around line 142-150: When persistence === 'url' and
this.persistenceHandlers.get('url') returns undefined, add a warning log so the
missing handler doesn't silently no-op; update the block around
persistenceHandlers/get('url') (the handler variable and its conditional) to log
a warning that the 'url' persistence handler is not registered and include
context (e.g., persistence value and any relevant options). Use
`@storybook/internal/client-logger.warn` if that module is available in this file,
otherwise fall back to console.warn; do not change the existing behavior of
calling handler(delta, serialize) when present.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5dd03e6e-802b-48c6-8699-7f72beecafcf

📥 Commits

Reviewing files that changed from the base of the PR and between e73920d and 0d52e76.

📒 Files selected for processing (5)
  • code/core/src/manager-api/modules/stories.ts
  • code/core/src/manager-api/modules/tags.ts
  • code/core/src/manager-api/modules/url.ts
  • code/core/src/manager-api/store.ts
  • code/core/src/manager-api/tests/tags.test.js
🚧 Files skipped from review as they are similar to previous changes (4)
  • code/core/src/manager-api/tests/tags.test.js
  • code/core/src/manager-api/modules/tags.ts
  • code/core/src/manager-api/modules/url.ts
  • code/core/src/manager-api/modules/stories.ts

Comment thread code/core/src/manager-api/store.ts Outdated
@valentinpalkovic valentinpalkovic force-pushed the valentin/url-based-tag-state branch from 0d52e76 to 2f80eb0 Compare March 24, 2026 17:42
Copy link
Copy Markdown
Contributor

@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.

♻️ Duplicate comments (1)
code/core/src/manager-api/modules/stories.ts (1)

967-980: ⚠️ Potential issue | 🟠 Major

Use the matched entry type for the filtered fallback.

This block finds firstFiltered by ID but then navigates using the preview-emitted viewMode. If firstFiltered is a docs entry but viewMode is 'story', this produces an incorrect route like /story/<docsId> instead of /docs/<docsId>.

Derive the route from the matched entry's type instead of using viewMode.

Proposed fix
-              const firstFiltered = filteredIndex
-                ? Object.keys(filteredIndex).find((id) => {
-                    const entry = filteredIndex[id];
-                    return entry.type === 'story' || entry.type === 'docs';
-                  })
+              const firstFilteredEntry = filteredIndex
+                ? Object.values(filteredIndex).find(
+                    (entry) => entry.type === 'story' || entry.type === 'docs'
+                  )
                 : undefined;
 
-              if (firstFiltered) {
-                navigateWithQueryParams(`/${viewMode}/${firstFiltered}`);
+              if (firstFilteredEntry) {
+                navigateWithQueryParams(`/${firstFilteredEntry.type}/${firstFilteredEntry.id}`);
               }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/manager-api/modules/stories.ts` around lines 967 - 980, The
fallback navigation uses viewMode instead of the matched entry's type, causing
routes like `/story/<docsId>`; change the logic that computes firstFiltered to
also capture the matched entry (e.g., find both id and entry via filteredIndex)
and derive the routeType from entry.type (map entry.type 'docs' => 'docs',
otherwise 'story'), then call
navigateWithQueryParams(`/${routeType}/${firstFiltered}`) instead of using
viewMode; keep the existing early return and preserve filteredIndex checks
around this change.
🧹 Nitpick comments (4)
code/core/src/manager-api/modules/stories.ts (2)

447-455: Clarify the serialize callback parameter type.

The serialize function parameter s uses a type assertion via ReturnType<typeof store.getState>. Consider adding explicit typing to the persistFilters function signature for better readability and maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/manager-api/modules/stories.ts` around lines 447 - 455, The
serialize callback currently types its parameter `s` via ReturnType<typeof
store.getState>, which is unclear; update the persistFilters signature to accept
an explicit state type (e.g., define or import the store state interface and
annotate persistFilters or the serialize param) so that the `serialize` function
parameter is strongly typed instead of using ReturnType<typeof store.getState>;
locate the persistFilters function, the store.setState call, and the serialize
callback (and types referenced: store.getState, serializeTagsParam,
includedTagFilters, excludedTagFilters) and replace the assertion with the
explicit state type in the function signature or serialize param.

437-445: Consider extracting URL building logic to reduce duplication.

navigateWithQueryParams duplicates the query string building logic from navigateTo in url.ts (lines 227-231). Consider refactoring to share this logic, perhaps by exposing a utility function or having the stories module call into the URL module's API.

Alternative approach

If the URL module's api.applyQueryParams is available, you could potentially use it instead. However, I understand this may be a deliberate design choice to avoid circular dependencies or timing issues during module initialization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/manager-api/modules/stories.ts` around lines 437 - 445,
navigateWithQueryParams duplicates query-string construction already implemented
in navigateTo (url.ts) — refactor to reuse that logic: extract the
query-building logic into a shared utility (e.g., buildQueryString or reuse
api.applyQueryParams) and have navigateWithQueryParams call that helper instead
of rebuilding params; update navigateWithQueryParams to accept the path and
options, call the shared buildQueryString([...]) or api.applyQueryParams(path,
customQueryParams) to produce the final URL, then call navigate(to, options).
Ensure the helper exports are used to avoid circular imports (or move the helper
to a neutral util module) and keep the existing function signature of
navigateWithQueryParams and the behavior filtering null/undefined and sorting
keys.
code/core/src/manager-api/modules/url.ts (1)

321-326: Consider the order of operations in applyQueryParams.

The function navigates first (line 324) then updates query params state (line 325). If setQueryParams triggers any side effects, they would happen after navigation. This seems intentional, but verify that this ordering is correct for the URL persistence use case where the persistence handler calls applyQueryParams.

Also, line 324 casts { ...queryParams, ...input } to any, which loses type safety. Consider using a more precise type.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/manager-api/modules/url.ts` around lines 321 - 326,
applyQueryParams currently calls navigateTo with merged params then calls
api.setQueryParams, which can invert expected side-effect ordering for URL
persistence handlers; decide and enforce the correct order: if
persistence/handlers must run before navigation, call api.setQueryParams(input)
first, otherwise leave order but document intent. Also remove the unsafe cast to
any on `{ ...queryParams, ...input }` and replace it with a precise type (e.g.,
the same type as api.getUrlState().queryParams or a defined
QueryParams/Record<string, string|undefined> type) so navigateTo receives a
correctly typed object; update references in applyQueryParams, navigateTo call
site, and types returned by api.getUrlState() accordingly.
code/core/src/manager-api/tests/stories.test.ts (1)

959-992: Add test coverage for filter-aware STORY_SPECIFIED behavior.

The STORY_SPECIFIED handler now includes logic for filter-aware navigation (lines 960-972 in stories.ts):

  • Navigates to the first filtered story/docs when the specified story doesn't pass active tag filters
  • Suppresses navigation when no entries pass the current filters
  • Selects docs entries as fallback when stories are unavailable

The existing tests in this block (lines 709-746) cover basic navigation only and don't test these filter-aware scenarios. Adding tests for these cases would improve confidence in the tag filter functionality.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/manager-api/tests/stories.test.ts` around lines 959 - 992, Add
unit tests around the STORY_SPECIFIED handler in stories.test.ts (the tests near
the existing navigation block that use api.setIndex and api.jumpToStory) to
cover filter-aware behavior: (1) when the specified storyId is filtered out by
active tag filters, assert that navigate/jumpToStory goes to the first filtered
story or docs entry (depending on available entries); (2) when no entries pass
the current filters, assert that navigation is suppressed (navigate not called);
and (3) when stories are filtered out but docs entries exist, assert the docs
entry is selected as a fallback. Use the same helpers (createMockModuleArgs,
initStories, navigationEntries, api.setIndex, api.jumpToStory and the mocked
navigate) and the STORY_SPECIFIED handler in stories.ts to locate where to add
these cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@code/core/src/manager-api/modules/stories.ts`:
- Around line 967-980: The fallback navigation uses viewMode instead of the
matched entry's type, causing routes like `/story/<docsId>`; change the logic
that computes firstFiltered to also capture the matched entry (e.g., find both
id and entry via filteredIndex) and derive the routeType from entry.type (map
entry.type 'docs' => 'docs', otherwise 'story'), then call
navigateWithQueryParams(`/${routeType}/${firstFiltered}`) instead of using
viewMode; keep the existing early return and preserve filteredIndex checks
around this change.

---

Nitpick comments:
In `@code/core/src/manager-api/modules/stories.ts`:
- Around line 447-455: The serialize callback currently types its parameter `s`
via ReturnType<typeof store.getState>, which is unclear; update the
persistFilters signature to accept an explicit state type (e.g., define or
import the store state interface and annotate persistFilters or the serialize
param) so that the `serialize` function parameter is strongly typed instead of
using ReturnType<typeof store.getState>; locate the persistFilters function, the
store.setState call, and the serialize callback (and types referenced:
store.getState, serializeTagsParam, includedTagFilters, excludedTagFilters) and
replace the assertion with the explicit state type in the function signature or
serialize param.
- Around line 437-445: navigateWithQueryParams duplicates query-string
construction already implemented in navigateTo (url.ts) — refactor to reuse that
logic: extract the query-building logic into a shared utility (e.g.,
buildQueryString or reuse api.applyQueryParams) and have navigateWithQueryParams
call that helper instead of rebuilding params; update navigateWithQueryParams to
accept the path and options, call the shared buildQueryString([...]) or
api.applyQueryParams(path, customQueryParams) to produce the final URL, then
call navigate(to, options). Ensure the helper exports are used to avoid circular
imports (or move the helper to a neutral util module) and keep the existing
function signature of navigateWithQueryParams and the behavior filtering
null/undefined and sorting keys.

In `@code/core/src/manager-api/modules/url.ts`:
- Around line 321-326: applyQueryParams currently calls navigateTo with merged
params then calls api.setQueryParams, which can invert expected side-effect
ordering for URL persistence handlers; decide and enforce the correct order: if
persistence/handlers must run before navigation, call api.setQueryParams(input)
first, otherwise leave order but document intent. Also remove the unsafe cast to
any on `{ ...queryParams, ...input }` and replace it with a precise type (e.g.,
the same type as api.getUrlState().queryParams or a defined
QueryParams/Record<string, string|undefined> type) so navigateTo receives a
correctly typed object; update references in applyQueryParams, navigateTo call
site, and types returned by api.getUrlState() accordingly.

In `@code/core/src/manager-api/tests/stories.test.ts`:
- Around line 959-992: Add unit tests around the STORY_SPECIFIED handler in
stories.test.ts (the tests near the existing navigation block that use
api.setIndex and api.jumpToStory) to cover filter-aware behavior: (1) when the
specified storyId is filtered out by active tag filters, assert that
navigate/jumpToStory goes to the first filtered story or docs entry (depending
on available entries); (2) when no entries pass the current filters, assert that
navigation is suppressed (navigate not called); and (3) when stories are
filtered out but docs entries exist, assert the docs entry is selected as a
fallback. Use the same helpers (createMockModuleArgs, initStories,
navigationEntries, api.setIndex, api.jumpToStory and the mocked navigate) and
the STORY_SPECIFIED handler in stories.ts to locate where to add these cases.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fd63282c-9d33-4fe6-b65e-1276259c0679

📥 Commits

Reviewing files that changed from the base of the PR and between 2f80eb0 and 0e26cda.

📒 Files selected for processing (3)
  • code/core/src/manager-api/modules/stories.ts
  • code/core/src/manager-api/modules/url.ts
  • code/core/src/manager-api/tests/stories.test.ts

Copy link
Copy Markdown
Contributor

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code/core/src/manager-api/tests/store.test.js`:
- Around line 24-30: Update the test to use vi.mock(..., { spy: true }) for the
store2 mock, and move the inline mockReturnValueOnce setup into a beforeEach
where you reset mocks (e.g., vi.resetAllMocks or vi.clearAllMocks) and then
configure behaviors via vi.mocked(...) wrappers; specifically replace direct
calls to store2.session.get.mockReturnValueOnce and
store2.local.get.mockReturnValueOnce with
vi.mocked(store2.session.get).mockReturnValueOnce(...) and
vi.mocked(store2.local.get).mockReturnValueOnce(...), doing both the
migration-check and merge returns inside beforeEach so tests use the typed
vi.mocked accessors and mocks are reset between tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c70e553f-563b-41e5-941f-e7e95d794d91

📥 Commits

Reviewing files that changed from the base of the PR and between 0e26cda and f1256e9.

📒 Files selected for processing (1)
  • code/core/src/manager-api/tests/store.test.js

Comment thread code/core/src/manager-api/tests/store.test.js
Comment thread code/core/src/manager-api/modules/stories.ts
Comment thread code/core/src/manager-api/modules/tags.ts
Comment thread code/core/src/manager-api/modules/url.ts Outdated
Comment thread code/core/src/manager-api/tests/tags.test.js Outdated
Comment thread code/core/src/manager-api/tests/tags.test.js Outdated
Comment thread code/core/src/manager-api/store.ts
@storybook-app-bot
Copy link
Copy Markdown

storybook-app-bot Bot commented Mar 26, 2026

Package Benchmarks

Commit: 17b4191, ran on 26 March 2026 at 10:28:11 UTC

No significant changes detected, all good. 👏

@valentinpalkovic valentinpalkovic merged commit f4e4a53 into next Mar 26, 2026
122 of 124 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Review Changes]: T2 - URL-based tag filter encoding + filter-aware initial story selection

2 participants