Skip to content

Add saved searches feature#304

Draft
johncschuster wants to merge 1 commit intomainfrom
saved-searches-feature
Draft

Add saved searches feature#304
johncschuster wants to merge 1 commit intomainfrom
saved-searches-feature

Conversation

@johncschuster
Copy link

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

  • Title filter: Added optional titleFilter to the filter shape; filtering by issue title (case-insensitive) in PanelIssues and a "Search by title" input in the Filters form.
  • Storage: New ONYXKEYS.SAVED_SEARCHES and SavedSearchesStorage.js using chrome.storage.local for persistence.
  • Actions: New SavedSearches.js with getSavedSearches, saveSavedSearch, applySavedSearch, deleteSavedSearch, renameSavedSearch.
  • UI: In the Filters area: "Save search" button (prompts for name), and a "Saved searches" list with Apply, Rename, and Delete per item.
  • Dashboard: Re-enabled the Filters component in ListIssues so the filter form and saved searches are visible.
  • Fixes: applySavedSearch now waits for saveFilters when it returns a promise; componentDidUpdate in Filters syncs form fields when filters change (not only milestones) so applied saved searches update the UI correctly.

Made with Cursor

- 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>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 Filters UI and added a new “Search by title” field plus saved-search CRUD controls.
  • Introduced ONYXKEYS.SAVED_SEARCHES and 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.

Comment on lines +1 to +12
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];
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

./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).

Copilot uses AI. Check for mistakes.
Comment on lines +73 to 84
// 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)) {
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
// 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
));
}
}

Copilot uses AI. Check for mistakes.
Comment on lines 58 to 75
@@ -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 || '';
}
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
@johncschuster
Copy link
Author

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

Copy link
Contributor

@neil-marcellini neil-marcellini left a comment

Choose a reason for hiding this comment

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

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)

  1. 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 | bash
    

    Close and reopen Terminal, then run:

    nvm install 20
    
  2. 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
    
  3. 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

  1. Open Chrome and go to chrome://extensions
  2. Enable Developer mode (toggle in the top-right corner)
  3. Click Load unpacked
  4. Select the dist/ folder inside your k2-extension directory
  5. The K2 extension should now appear in your extensions list

Test the saved searches feature

  1. Navigate to a GitHub repo's issues page (e.g., https://github.com/Expensify/App/issues)
  2. Open the K2 extension panel
  3. Test the title filter by typing in the "Search by title" field and clicking Apply
  4. Save a search by clicking "Save search" and entering a name
  5. Verify the saved search appears in the list
  6. Click "Apply" on a saved search and verify filters update
  7. Test "Rename" and "Delete" buttons
  8. Close and reopen the panel — verify saved searches persist

@neil-marcellini neil-marcellini marked this pull request as draft February 12, 2026 18:56
@neil-marcellini
Copy link
Contributor

Converting to a draft until it's ready for my review again.

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.

2 participants