Conversation
- Add title filter to issue filters (filterPropTypes, PanelIssues, Filters UI) - Add ONYXKEYS.SAVED_SEARCHES and SavedSearchesStorage (chrome.storage.local) - Add SavedSearches actions: get, save, apply, delete, rename - Add Save search button and Saved searches list (Apply/Rename/Delete) in Filters - Re-enable Filters component in dashboard ListIssues - Fix applySavedSearch to wait for saveFilters when async - Fix componentDidUpdate to sync form fields when filters change (not only milestones) Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
Adds a “saved searches” feature to the K2 dashboard so users can persist and re-apply filter presets (including a new title search filter) across sessions.
Changes:
- Re-enabled the dashboard
FiltersUI and added a new “Search by title” field plus saved-search CRUD controls. - Introduced
ONYXKEYS.SAVED_SEARCHESand new saved-search actions/storage for persistence via extension storage. - Added title-based filtering into
PanelIssues(case-insensitive substring match).
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/js/module/dashboard/ListIssues.js | Re-adds Filters to the dashboard layout. |
| src/js/module/dashboard/Filters.js | Adds title filter input and saved-search UI + Onyx connection. |
| src/js/lib/filterPropTypes.js | Extends filter shape with optional titleFilter. |
| src/js/lib/actions/SavedSearches.js | Implements saved-search CRUD and applying saved filters. |
| src/js/lib/SavedSearchesStorage.js | Adds persistence layer over extension storage. |
| src/js/component/panel/PanelIssues.js | Applies titleFilter when ordering/filtering issues. |
| src/js/ONYXKEYS.js | Adds SAVED_SEARCHES Onyx key. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import getBrowser from './browser'; | ||
|
|
||
| const STORAGE_KEY = 'k2_saved_searches'; | ||
|
|
||
| /** | ||
| * @returns {Promise<Array<{id: string, name: string, filters: Object, createdAt: number}>>} | ||
| */ | ||
| function getSavedSearches() { | ||
| const browser = getBrowser(); | ||
| return new Promise((resolve) => { | ||
| browser.storage.local.get(STORAGE_KEY, (result) => { | ||
| const list = result && result[STORAGE_KEY]; |
There was a problem hiding this comment.
./browser default export is the browser/chrome object (see other imports like messenger.js), but this file imports it as getBrowser and calls it as a function. That will throw at runtime. Import the browser object directly (e.g. import ksBrowser from './browser') and remove the function call, or change browser.js to export a function (but that would require updating existing call sites).
| // Apply title filter first (case-insensitive includes) | ||
| if (filters && filters.titleFilter && typeof filters.titleFilter === 'string') { | ||
| const query = filters.titleFilter.trim().toLowerCase(); | ||
| if (query) { | ||
| preparedIssues = _.filter(preparedIssues, (item) => ( | ||
| item.title && item.title.toLowerCase().indexOf(query) !== -1 | ||
| )); | ||
| } | ||
| } | ||
|
|
||
| // Apply filters | ||
| if (applyFilters && filters && !_.isEmpty(filters)) { |
There was a problem hiding this comment.
titleFilter is applied even when applyFilters is false. applyFilters is documented as the switch to ignore dashboard filters for certain views, so this change makes those views still subject to filtering. Consider moving the title filter logic inside the existing if (applyFilters && filters ...) block (or otherwise explicitly tie it to applyFilters) so the "ignore filters" behavior remains consistent.
| // Apply title filter first (case-insensitive includes) | |
| if (filters && filters.titleFilter && typeof filters.titleFilter === 'string') { | |
| const query = filters.titleFilter.trim().toLowerCase(); | |
| if (query) { | |
| preparedIssues = _.filter(preparedIssues, (item) => ( | |
| item.title && item.title.toLowerCase().indexOf(query) !== -1 | |
| )); | |
| } | |
| } | |
| // Apply filters | |
| if (applyFilters && filters && !_.isEmpty(filters)) { | |
| // Apply filters (including optional title filter) | |
| if (applyFilters && filters && !_.isEmpty(filters)) { | |
| // Apply title filter first (case-insensitive includes) | |
| if (filters.titleFilter && typeof filters.titleFilter === 'string') { | |
| const query = filters.titleFilter.trim().toLowerCase(); | |
| if (query) { | |
| preparedIssues = _.filter(preparedIssues, (item) => ( | |
| item.title && item.title.toLowerCase().indexOf(query) !== -1 | |
| )); | |
| } | |
| } |
| @@ -50,6 +69,9 @@ class Filters extends React.Component { | |||
| this.fieldTask.checked = this.props.filters.task; | |||
| this.fieldFeature.checked = this.props.filters.feature; | |||
| $(this.fieldMilestone).val(this.props.filters.milestone); | |||
| if (this.fieldTitleFilter) { | |||
| this.fieldTitleFilter.value = this.props.filters.titleFilter || ''; | |||
| } | |||
| } | |||
There was a problem hiding this comment.
The form inputs are uncontrolled and default to unchecked/empty, but componentDidUpdate bails out when filters is empty. With Filters now rendered on the dashboard, the UI will never reflect the default filter state (e.g. improvement/task/feature should likely start checked) until something writes to ONYXKEYS.ISSUES.FILTER. Consider initializing defaults on mount when filters are empty, or updating the fields with fallback defaults instead of returning early.
|
@neil-marcellini I have completely vibe-coded this and have no way to know if it's total rubbish. Feel free to pick it apart! |
neil-marcellini
left a comment
There was a problem hiding this comment.
Hey @johncschuster, I'm glad to see you taking a shot at this! The first thing I would recommend is making sure that you test it out locally. It should be pretty easy to do that and it doesn't require setting up the VM.
Here are some instructions. They are AI generated, but hopefully they are pretty clear and you should be able to pass these to your AI to help you get set up.
Setup (one-time)
-
Install Node.js v20 if you don't have it. The easiest way is via nvm. Open Terminal and run:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bashClose and reopen Terminal, then run:
nvm install 20 -
Clone the repo and check out your branch:
git clone https://github.com/Expensify/k2-extension.git cd k2-extension git checkout saved-searches-feature -
Install dependencies:
npm install
Build the extension
Run the development build:
npm run web
This will create a dist/ folder with the built extension and keep watching for changes.
Load it in Chrome
- Open Chrome and go to
chrome://extensions - Enable Developer mode (toggle in the top-right corner)
- Click Load unpacked
- Select the
dist/folder inside yourk2-extensiondirectory - The K2 extension should now appear in your extensions list
Test the saved searches feature
- Navigate to a GitHub repo's issues page (e.g., https://github.com/Expensify/App/issues)
- Open the K2 extension panel
- Test the title filter by typing in the "Search by title" field and clicking Apply
- Save a search by clicking "Save search" and entering a name
- Verify the saved search appears in the list
- Click "Apply" on a saved search and verify filters update
- Test "Rename" and "Delete" buttons
- Close and reopen the panel — verify saved searches persist
|
Converting to a draft until it's ready for my review again. |
Summary
Adds a saved searches feature to the K2 extension so users can save the current filter state (including title search), name it, and re-apply it later.
Changes
titleFilterto the filter shape; filtering by issue title (case-insensitive) inPanelIssuesand a "Search by title" input in the Filters form.ONYXKEYS.SAVED_SEARCHESandSavedSearchesStorage.jsusingchrome.storage.localfor persistence.SavedSearches.jswithgetSavedSearches,saveSavedSearch,applySavedSearch,deleteSavedSearch,renameSavedSearch.ListIssuesso the filter form and saved searches are visible.applySavedSearchnow waits forsaveFilterswhen it returns a promise;componentDidUpdatein Filters syncs form fields whenfilterschange (not onlymilestones) so applied saved searches update the UI correctly.Made with Cursor