Skip to content

fix(web): OSearch Favorites i18n, first-frame race, and edit - #268

Open
buke wants to merge 5 commits into
mainfrom
fix/web-osearch-favorites-i18n
Open

fix(web): OSearch Favorites i18n, first-frame race, and edit#268
buke wants to merge 5 commits into
mainfrom
fix/web-osearch-favorites-i18n

Conversation

@buke

@buke buke commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

User description

Summary

  • Translate OSearch Favorites / UserFilter chrome strings in zh_CN (menu, save/share/delete dialogs, model fields).
  • Fix first-frame race and request inflation: single UserFilter load via OSearch defaults-ready, defer OListView mount apply like Kanban, and guard useUserFilters.load with a generation.
  • Add favorite metadata edit (name / default / share) without rewriting Condition; pencil entry next to delete.

Test plan

  • Open a list with a saved IsDefault favorite; first paint applies it (no missing default on fast nav).
  • First open Network: ~1× web.UserFilter/Search + 1× model Search + 1× Count (no triple UserFilter / double Search).
  • UI language zh_CN: Favorites menu and save dialog show Chinese chrome (saved Name text may stay English).
  • Edit a favorite: rename / toggle default / share; Condition unchanged; toast 「收藏已更新」.
  • Delete / save paths still emit defaults-ready; shared favorites owned by others have no edit/delete.
  • ./choysum test unit web --fe
  • ./choysum i18n status web --lang zh_CN (orphan-only OK)

Made with Cursor


PR Type

Bug fix, Enhancement, Tests


Description

  • Fix first-frame race and double UserFilter search: load favorites once in OSearch, guard with generation, defer list/kanban mount

  • Add edit favorite metadata (name, default, share) without changing Condition, using pencil button in menu

  • Provide Chinese (zh_CN) translations for all OSearch favorites UI strings, dialogs, and model fields

  • Add tests: OListView first-frame defer, edit metadata dialog, useUserFilters generation, and rewrite OSearchView tests


File Walkthrough

Relevant files
Bug fix
4 files
OSearchView.vue
Refactor to await OSearch defaults-ready, remove own UserFilter
Search, defer first emit
+28/-79 
OListView.vue
Defer mount apply when searchView is OSearchView using
shouldDeferViewFirstFrame
+7/-0     
OKanbanView.vue
Switch to shouldDeferViewFirstFrame for deferring first frame in
kanban view
+2/-2     
useUserFilters.ts
Add load generation guard to ignore stale responses; add updateMeta
for editing favorites
+36/-2   
Enhancement
1 files
OSearch.vue
Add edit favorite metadata (pencil icon, dialog, updateMeta), import
EditPen icon
+54/-8   
Refactoring
2 files
kanbanFirstFrame.ts
Rename to shouldDeferViewFirstFrame, deprecate old name, document
usage for list/kanban
+5/-2     
OSearchView.scopeKey.test.ts
Remove scope-key test file (logic moved out of OSearchView)
+0/-87   
I18n
2 files
web.pot
Extract OSearch favorites and UserFilter model strings into
translation template
+194/-33
zh_CN.po
Provide Chinese translations for OSearch favorites UI and model fields
+193/-33
Tests
7 files
OSearchView.test.ts
Rewrite tests to cover single-load defaults-ready flow and initialEmit
behavior
+49/-211
OListView.firstframe.test.ts
Add first-frame defer test for OListView, verify mount apply and
deferral
+155/-0 
OKanbanView.firstframe.test.ts
Update kanban first-frame test to use shouldDeferViewFirstFrame mock
+1/-0     
OKanbanView.readonly.test.ts
Adjust source check for shouldDeferViewFirstFrame in kanban view test
+1/-1     
kanbanFirstFrame.test.ts
Add tests for the renamed shouldDeferViewFirstFrame function
+6/-5     
useUserFilters.test.ts
Add tests for load generation, updateMeta, and edge cases
+99/-0   
OSearch.behavior.test.ts
Add test for edit favorite dialog, metadata update, and defaults-ready
count
+57/-2   

Summary by CodeRabbit

  • New Features
    • Added the ability to edit saved search favorites, including their name, default status, and sharing settings.
    • Improved search initialization to apply filters more consistently and avoid duplicate updates.
  • Bug Fixes
    • Prevented outdated search requests from overwriting newer results or loading states.
    • Improved first-frame loading behavior for list and Kanban views.
  • Localization
    • Added and updated Simplified Chinese translations for filtering, search, favorites, sharing, and related validation messages.

buke and others added 4 commits August 10, 2026 21:33
- Extract UserFilter and Favorites menu msgids into web.pot / zh_CN.po.

- Provide Chinese msgstr for Favorites, save/share/delete dialogs, and model fields.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Drop mount-time defaults-ready so OSearchView IsDefault load is not superseded.

- Defer OListView mount apply when searchView is OSearchView, matching Kanban.

- Guard useUserFilters.load with a generation so stale responses cannot overwrite.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Let OSearch own the single Search and emit defaults-ready after load.

- Drop OSearchView's duplicate IsDefault Search; wait on defaults-ready for first-frame emit.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Add useUserFilters.updateMeta via UpdateById without rewriting Condition.

- Reuse the save dialog for edit mode with a pencil entry next to delete.

- Extract and translate Edit favorite strings in zh_CN.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 55 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3b4104a5-dfb9-4892-b8ae-dd9b6e5fb537

📥 Commits

Reviewing files that changed from the base of the PR and between a8b8ed9 and d033f5b.

📒 Files selected for processing (2)
  • modules/core/i18n/core.pot
  • modules/core/i18n/zh_CN.po
📝 Walkthrough

Walkthrough

This change generalizes first-frame deferral to List and Kanban views, moves search-default synchronization into OSearch, adds favorite metadata editing and stale-load protection, and updates web and Simplified Chinese translation catalogs.

Changes

Search view updates

Layer / File(s) Summary
Shared first-frame deferral
modules/web/web/components/view/kanbanFirstFrame.ts, modules/web/web/components/view/OKanbanView.vue, modules/web/web/components/view/OListView.vue, modules/web/web/components/view/*firstframe.test.ts
The deferral helper now supports both List and Kanban views. List mount initialization exits before field waiting and controller application when deferral is enabled.
Child-provided search defaults
modules/web/web/components/view/OSearchView.vue, modules/web/web/components/view/OSearchView.test.ts
OSearchView passes code defaults to OSearch, stores defaults-ready results, and emits the initial query once after the child provides defaults.
Favorite metadata updates
modules/web/web/composables/search/useUserFilters.ts, modules/web/web/components/view/search/OSearch.vue, modules/web/web/composables/search/useUserFilters.test.ts, modules/web/web/components/view/search/OSearch.behavior.test.ts
useUserFilters suppresses stale loads and exposes updateMeta. OSearch adds favorite editing for name, default status, and sharing settings.
Translation catalog updates
modules/web/i18n/web.pot, modules/web/i18n/zh_CN.po
The catalogs add UserFilter and search-related messages and update source references for moved view and terminology-editor messages.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OSearchView
  participant OSearch
  participant useUserFilters
  participant UserFilterAPI
  OSearchView->>OSearch: pass codeDefaultFilters
  OSearch->>useUserFilters: load favorites
  useUserFilters->>UserFilterAPI: request favorites
  UserFilterAPI-->>useUserFilters: return favorites
  useUserFilters-->>OSearch: emit defaults-ready
  OSearch-->>OSearchView: provide merged defaults
  OSearchView-->>OSearchView: emit initial query-update once
Loading

Possibly related PRs

Suggested labels: Review effort 2/5

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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
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.
Title check ✅ Passed The title clearly summarizes the main changes to OSearch Favorites, i18n, first-frame behavior, and favorite editing.
Description check ✅ Passed The description provides a clear summary, test plan, change categories, and file walkthrough aligned with the pull request objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/web-osearch-favorites-i18n

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

@github-actions

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ No major issues detected

- Add Created By / Updated By / Deleted By to core.pot and zh_CN.po.

- Unblock CI i18n status pot-dirty after audit uid columns landed.

Co-authored-by: Cursor <cursoragent@cursor.com>

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
modules/web/web/composables/search/useUserFilters.ts (1)

56-66: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clear request state for an invalid load context.

At Line 60, this branch invalidates any earlier request but leaves loading true. The stale request cannot clear it at Lines 116-118. The favorites menu can then show a permanent loading state. Clear loading and loadError with favorites before returning. Add a regression test that starts a load, then removes the application or model before the first request resolves.

Proposed fix
     if (!app || !model) {
       if (gen === loadGeneration) {
         favorites.value = [];
+        loading.value = false;
+        loadError.value = null;
       }
       return;
     }
🤖 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 `@modules/web/web/composables/search/useUserFilters.ts` around lines 56 - 66,
Update the invalid-context branch in the load function around loadGeneration to
clear loading and loadError alongside favorites before returning, while
retaining the generation guard. Add a regression test that starts a load,
removes the application or model before the request resolves, and verifies the
loading state is cleared.
🤖 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 `@modules/web/web/components/view/OListView.firstframe.test.ts`:
- Around line 45-48: Update the kanbanFirstFrame mock’s
shouldDeferViewFirstFrame and shouldDeferKanbanFirstFrame implementations to
honor their searchView and OSearchView arguments, matching production behavior
by deferring only when the references are identical and the configured defer
state applies. Preserve the existing deferState-driven behavior for matching
references so SearchStub and custom search views exercise the correct fallback
mount path.

In `@modules/web/web/components/view/OSearchView.vue`:
- Around line 111-120: Prevent duplicate first-frame emissions in
emitFirstFrameIfNeeded by rechecking mounted.value after nextTick or setting a
pending latch before awaiting, so concurrent defaults-ready events produce only
one query-update. In modules/web/web/components/view/OSearchView.test.ts lines
142-155, emit defaults-ready twice before awaiting Vue updates and assert
exactly one query-update; both sites require changes.

---

Outside diff comments:
In `@modules/web/web/composables/search/useUserFilters.ts`:
- Around line 56-66: Update the invalid-context branch in the load function
around loadGeneration to clear loading and loadError alongside favorites before
returning, while retaining the generation guard. Add a regression test that
starts a load, removes the application or model before the request resolves, and
verifies the loading state is cleared.
🪄 Autofix

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: da0cd229-6719-44d5-a150-4eaa836b8d39

📥 Commits

Reviewing files that changed from the base of the PR and between bd780ea and a8b8ed9.

📒 Files selected for processing (16)
  • modules/web/i18n/web.pot
  • modules/web/i18n/zh_CN.po
  • modules/web/web/components/view/OKanbanView.firstframe.test.ts
  • modules/web/web/components/view/OKanbanView.readonly.test.ts
  • modules/web/web/components/view/OKanbanView.vue
  • modules/web/web/components/view/OListView.firstframe.test.ts
  • modules/web/web/components/view/OListView.vue
  • modules/web/web/components/view/OSearchView.scopeKey.test.ts
  • modules/web/web/components/view/OSearchView.test.ts
  • modules/web/web/components/view/OSearchView.vue
  • modules/web/web/components/view/kanbanFirstFrame.test.ts
  • modules/web/web/components/view/kanbanFirstFrame.ts
  • modules/web/web/components/view/search/OSearch.behavior.test.ts
  • modules/web/web/components/view/search/OSearch.vue
  • modules/web/web/composables/search/useUserFilters.test.ts
  • modules/web/web/composables/search/useUserFilters.ts
💤 Files with no reviewable changes (1)
  • modules/web/web/components/view/OSearchView.scopeKey.test.ts

Comment on lines +45 to +48
vi.mock('@/web/web/components/view/kanbanFirstFrame', () => ({
shouldDeferViewFirstFrame: () => deferState.defer,
shouldDeferKanbanFirstFrame: () => deferState.defer,
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the mock preserve the component-reference contract.

The mock ignores searchView and OSearchView. Lines 115-130 therefore defer for SearchStub, but production defers only when both references are identical. This test can pass if custom search views incorrectly skip their fallback mount apply.

Proposed fix
 vi.mock('`@/web/web/components/view/kanbanFirstFrame`', () => ({
-  shouldDeferViewFirstFrame: () => deferState.defer,
+  shouldDeferViewFirstFrame: (searchView: unknown, oSearchView: unknown) =>
+    deferState.defer && searchView === oSearchView,
   shouldDeferKanbanFirstFrame: () => deferState.defer,
 }));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
vi.mock('@/web/web/components/view/kanbanFirstFrame', () => ({
shouldDeferViewFirstFrame: () => deferState.defer,
shouldDeferKanbanFirstFrame: () => deferState.defer,
}));
vi.mock('`@/web/web/components/view/kanbanFirstFrame`', () => ({
shouldDeferViewFirstFrame: (searchView: unknown, oSearchView: unknown) =>
deferState.defer && searchView === oSearchView,
shouldDeferKanbanFirstFrame: () => deferState.defer,
}));
🤖 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 `@modules/web/web/components/view/OListView.firstframe.test.ts` around lines 45
- 48, Update the kanbanFirstFrame mock’s shouldDeferViewFirstFrame and
shouldDeferKanbanFirstFrame implementations to honor their searchView and
OSearchView arguments, matching production behavior by deferring only when the
references are identical and the configured defer state applies. Preserve the
existing deferState-driven behavior for matching references so SearchStub and
custom search views exercise the correct fallback mount path.

Comment on lines +111 to +120
async function emitFirstFrameIfNeeded(): Promise<void> {
if (mounted.value || !props.initialEmit) return;
await nextTick();
const filtersAtFirstEmit = appliedFiltersForChild.value || [];
const groupsAtFirstEmit = appliedGroupsForChild.value || [];
const payload = buildQueryUpdatePayload<T>(keywordForChild.value, filtersAtFirstEmit, groupsAtFirstEmit, {
explicitGroups: false,
});
emit('query-update', payload);
mounted.value = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Prevent duplicate first-frame query emissions.

At Lines 111-113, each defaults-ready handler checks mounted before awaiting nextTick(). If two events arrive in the same tick, both handlers pass that check and both emit query-update after the tick. This reintroduces redundant first-frame requests.

  • modules/web/web/components/view/OSearchView.vue#L111-L120: Recheck mounted after await nextTick(), or set a pending latch before awaiting.
  • modules/web/web/components/view/OSearchView.test.ts#L142-L155: Emit defaults-ready twice before awaiting Vue updates, then assert that exactly one query-update occurs.
Proposed fix
 async function emitFirstFrameIfNeeded(): Promise<void> {
   if (mounted.value || !props.initialEmit) return;
   await nextTick();
+  if (mounted.value || !props.initialEmit) return;
   const filtersAtFirstEmit = appliedFiltersForChild.value || [];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function emitFirstFrameIfNeeded(): Promise<void> {
if (mounted.value || !props.initialEmit) return;
await nextTick();
const filtersAtFirstEmit = appliedFiltersForChild.value || [];
const groupsAtFirstEmit = appliedGroupsForChild.value || [];
const payload = buildQueryUpdatePayload<T>(keywordForChild.value, filtersAtFirstEmit, groupsAtFirstEmit, {
explicitGroups: false,
});
emit('query-update', payload);
mounted.value = true;
async function emitFirstFrameIfNeeded(): Promise<void> {
if (mounted.value || !props.initialEmit) return;
await nextTick();
if (mounted.value || !props.initialEmit) return;
const filtersAtFirstEmit = appliedFiltersForChild.value || [];
const groupsAtFirstEmit = appliedGroupsForChild.value || [];
const payload = buildQueryUpdatePayload<T>(keywordForChild.value, filtersAtFirstEmit, groupsAtFirstEmit, {
explicitGroups: false,
});
emit('query-update', payload);
mounted.value = true;
📍 Affects 2 files
  • modules/web/web/components/view/OSearchView.vue#L111-L120 (this comment)
  • modules/web/web/components/view/OSearchView.test.ts#L142-L155
🤖 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 `@modules/web/web/components/view/OSearchView.vue` around lines 111 - 120,
Prevent duplicate first-frame emissions in emitFirstFrameIfNeeded by rechecking
mounted.value after nextTick or setting a pending latch before awaiting, so
concurrent defaults-ready events produce only one query-update. In
modules/web/web/components/view/OSearchView.test.ts lines 142-155, emit
defaults-ready twice before awaiting Vue updates and assert exactly one
query-update; both sites require changes.

@github-actions

Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Fall back to code defaults when favorites emit empty

When favoritesDefaults.value is an empty array, the ?? operator treats it as truthy,

skipping the fallback to mergeUserFilterDefaults. This means that when the child
OSearch
emits no favorites defaults (e.g., no favorites exist), the search tag will
be empty instead
of showing the code-level default filters. Explicitly check for an
empty array so that
code defaults are shown when no favorites defaults exist.

modules/web/web/components/view/OSearchView.vue [78-84]

-const mergedDefaultFilters = computed(
-  () =>
-    (favoritesDefaults.value ??
-      mergeUserFilterDefaults({
-        codeDefaults: codeDefaultFilters.value as any,
-      })) as NamedFilter<T>[]
-);
+const mergedDefaultFilters = computed(() => {
+  const favs = favoritesDefaults.value;
+  if (favs && favs.length > 0) return favs as NamedFilter<T>[];
+  return mergeUserFilterDefaults({
+    codeDefaults: codeDefaultFilters.value as any,
+  }) as NamedFilter<T>[];
+});
Suggestion importance[1-10]: 8

__

Why: Prevents an empty favoritesDefaults array from overriding the code‑level defaults. Without this fallback, when OSearch emits no favorites defaults, the merged default list would be empty, breaking the fallback to code defaults and potentially causing the view to have no default filters.

Medium
Possible issue
Reload favorites after saving a new default

After saving a new favorite with isDefault set, the favorites list is not reloaded,

so defaultsForOpen remains stale and does not include the new default. This prevents
the
default from being applied immediately, which is a regression since OSearchView
no longer
re-fetches server defaults. Reload the favorites only when the user sets
the new favorite
as default, then emit defaults-ready.

modules/web/web/components/view/search/OSearch.vue [487-496]

 await saveFavoriteCurrent({
   name,
   isDefault: saveFavoriteIsDefault.value,
   shared: saveFavoriteShared.value,
 });
 saveFavoriteOpen.value = false;
+// Reload so that the new default appears in defaultsForOpen
+if (saveFavoriteIsDefault.value) {
+  await loadFavorites();
+}
 emit('defaults-ready', defaultsForOpen.value as NamedFilter[]);
 ElMessage.success(_t('Favorite saved'));
Suggestion importance[1-10]: 8

__

Why: defaultsForOpen is computed from the current favorites list, which is not updated after saveFavoriteCurrent. If the new favorite is set as default, emitting the stale defaultsForOpen prevents the newly saved default from being applied immediately. Reloading only when isDefault is true fixes this regression.

Medium

@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.28571% with 6 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
modules/web/web/components/view/OSearchView.vue 75.0% 0 Missing and 3 partials ⚠️
...dules/web/web/composables/search/useUserFilters.ts 85.0% 0 Missing and 3 partials ⚠️

📢 Thoughts on this report? Let us know!

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.

1 participant