From 917f7c8cf79e09b886ab7a0386355399245c984e Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 15 Jun 2026 14:19:13 -0300 Subject: [PATCH 01/12] feat: Adds search by note title --- .../Runtime/Display/Search/SearchUtilities.ts | 35 +++++++++++++------ .../Domain/Runtime/Display/Search/Types.ts | 1 + .../SearchOptions/SearchOptions.tsx | 4 ++- .../ItemList/ItemListController.ts | 2 ++ .../Controllers/SearchOptionsController.ts | 7 ++++ 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/packages/models/src/Domain/Runtime/Display/Search/SearchUtilities.ts b/packages/models/src/Domain/Runtime/Display/Search/SearchUtilities.ts index 0f265ec8758..37d64dce798 100644 --- a/packages/models/src/Domain/Runtime/Display/Search/SearchUtilities.ts +++ b/packages/models/src/Domain/Runtime/Display/Search/SearchUtilities.ts @@ -38,18 +38,33 @@ export function itemMatchesQuery( searchQuery: SearchQuery, collection: ReferenceLookupCollection, ): boolean { - const shouldCheckForSomeTagMatches = searchQuery.shouldCheckForSomeTagMatches ?? true - const itemTags = collection.elementsReferencingElement(itemToMatch, ContentType.TYPES.Tag) as SNTag[] - const someTagsMatches = - shouldCheckForSomeTagMatches && - itemTags.some((tag) => matchResultForStringQuery(tag, searchQuery.query) !== MatchResult.None) - - if (itemToMatch.protected && !searchQuery.includeProtectedNoteText) { - const match = matchResultForStringQuery(itemToMatch, searchQuery.query) - return match === MatchResult.Title || match === MatchResult.TitleAndText || someTagsMatches + const { query, includeProtectedNoteText, shouldCheckForSomeTagMatches = true, noteTitleOnly = false } = searchQuery + + if (query.length === 0) { + return true + } + + const itemMatch = matchResultForStringQuery(itemToMatch, query) + + if (noteTitleOnly) { + return itemMatch === MatchResult.Title + } + + if (shouldCheckForSomeTagMatches) { + const itemTags = collection.elementsReferencingElement(itemToMatch, ContentType.TYPES.Tag) as SNTag[] + const tagMatches = itemTags.map((tag) => matchResultForStringQuery(tag, query)) + const someTagsMatches = tagMatches.some((match) => match !== MatchResult.None) + + if (someTagsMatches) { + return true + } + } + + if (itemToMatch.protected && !includeProtectedNoteText) { + return itemMatch === MatchResult.Title || itemMatch === MatchResult.TitleAndText } - return matchResultForStringQuery(itemToMatch, searchQuery.query) !== MatchResult.None || someTagsMatches + return itemMatch !== MatchResult.None } function matchResultForStringQuery(item: SearchableItem, searchString: string): MatchResult { diff --git a/packages/models/src/Domain/Runtime/Display/Search/Types.ts b/packages/models/src/Domain/Runtime/Display/Search/Types.ts index e574abcaae3..fffab2e73ef 100644 --- a/packages/models/src/Domain/Runtime/Display/Search/Types.ts +++ b/packages/models/src/Domain/Runtime/Display/Search/Types.ts @@ -6,6 +6,7 @@ export type SearchQuery = { query: string includeProtectedNoteText: boolean shouldCheckForSomeTagMatches?: boolean + noteTitleOnly?: boolean } export interface ReferenceLookupCollection { diff --git a/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx b/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx index 89829a3eac0..667267be297 100644 --- a/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx +++ b/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx @@ -8,7 +8,7 @@ type Props = { } const SearchOptions = ({ searchOptions }: Props) => { - const { includeProtectedContents, includeArchived, includeTrashed } = searchOptions + const { includeProtectedContents, includeArchived, includeTrashed, noteTitleOnly } = searchOptions const toggleIncludeProtectedContents = useCallback(async () => { await searchOptions.toggleIncludeProtectedContents() @@ -25,6 +25,8 @@ const SearchOptions = ({ searchOptions }: Props) => { + + ) } diff --git a/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts b/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts index e407a8262b4..41b6f43f72e 100644 --- a/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts +++ b/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts @@ -227,6 +227,7 @@ export class ItemListController this.searchOptionsController.includeProtectedContents, this.searchOptionsController.includeArchived, this.searchOptionsController.includeTrashed, + this.searchOptionsController.noteTitleOnly, ], () => { this.reloadNotesDisplayOptions() @@ -642,6 +643,7 @@ export class ItemListController searchQuery: { query: searchText, includeProtectedNoteText: this.searchOptionsController.includeProtectedContents, + noteTitleOnly: this.searchOptionsController.noteTitleOnly, }, } diff --git a/packages/web/src/javascripts/Controllers/SearchOptionsController.ts b/packages/web/src/javascripts/Controllers/SearchOptionsController.ts index 0ae8ccf8cad..944def1a9c7 100644 --- a/packages/web/src/javascripts/Controllers/SearchOptionsController.ts +++ b/packages/web/src/javascripts/Controllers/SearchOptionsController.ts @@ -12,6 +12,7 @@ export class SearchOptionsController extends AbstractViewController implements I includeProtectedContents = false includeArchived = false includeTrashed = false + noteTitleOnly = false constructor( private protections: ProtectionsClientInterface, @@ -23,11 +24,13 @@ export class SearchOptionsController extends AbstractViewController implements I includeProtectedContents: observable, includeTrashed: observable, includeArchived: observable, + noteTitleOnly: observable, toggleIncludeArchived: action, toggleIncludeTrashed: action, toggleIncludeProtectedContents: action, refreshIncludeProtectedContents: action, + toggleNoteTitleOnly: action, }) eventBus.addEventHandler(this, ApplicationEvent.UnprotectedSessionBegan) @@ -64,4 +67,8 @@ export class SearchOptionsController extends AbstractViewController implements I }) } } + + toggleNoteTitleOnly = (): void => { + this.noteTitleOnly = !this.noteTitleOnly + } } From 093e76d777841b511c6b420f7026605f9f3485ac Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 15 Jun 2026 21:22:50 -0300 Subject: [PATCH 02/12] feat: Adds search enhancement feature flag --- .../src/Domain/InternalFeatures/InternalFeature.ts | 1 + packages/web/src/javascripts/Application/DevMode.ts | 1 + .../Components/ContentListView/ContentListView.tsx | 1 + .../src/javascripts/Components/SearchBar/SearchBar.tsx | 10 ++++++++-- .../Components/SearchOptions/SearchOptions.tsx | 7 +++++-- .../src/javascripts/Controllers/FeaturesController.ts | 9 ++++++++- packages/web/src/javascripts/FeatureTrunk.ts | 4 ++++ 7 files changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/services/src/Domain/InternalFeatures/InternalFeature.ts b/packages/services/src/Domain/InternalFeatures/InternalFeature.ts index 1dfa14b229d..6b86bd41679 100644 --- a/packages/services/src/Domain/InternalFeatures/InternalFeature.ts +++ b/packages/services/src/Domain/InternalFeatures/InternalFeature.ts @@ -1,3 +1,4 @@ export enum InternalFeature { Vaults = 'vaults', + SearchEnhancements = 'search-enhancements', } diff --git a/packages/web/src/javascripts/Application/DevMode.ts b/packages/web/src/javascripts/Application/DevMode.ts index ead3f49c5f6..1a4ad0466b6 100644 --- a/packages/web/src/javascripts/Application/DevMode.ts +++ b/packages/web/src/javascripts/Application/DevMode.ts @@ -4,6 +4,7 @@ import { WebApplicationInterface } from '@standardnotes/ui-services' export class DevMode { constructor(private application: WebApplicationInterface) { InternalFeatureService.get().enableFeature(InternalFeature.Vaults) + InternalFeatureService.get().enableFeature(InternalFeature.SearchEnhancements) } /** Valid only when running a mock event publisher on port 3124 */ diff --git a/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx b/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx index c66d0e2424e..e1a118b2578 100644 --- a/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx @@ -332,6 +332,7 @@ const ContentListView = forwardRef( itemListController={itemListController} searchOptionsController={searchOptionsController} hideOptions={shouldUseTableView} + showNoteTitleOnlyOption={application.featuresController.isSearchEnhancementsEnabled()} /> )} { +const SearchBar = ({ + itemListController, + searchOptionsController, + hideOptions = false, + showNoteTitleOnlyOption = false, +}: Props) => { const searchBarRef = useRef(null) const searchInputRef = useRef(null) @@ -68,7 +74,7 @@ const SearchBar = ({ itemListController, searchOptionsController, hideOptions = hideOptions ? 'hidden' : !noteFilterText && 'hidden group-focus-within:flex', )} > - + ) diff --git a/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx b/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx index 667267be297..bf4174c05e1 100644 --- a/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx +++ b/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx @@ -5,9 +5,10 @@ import { SearchOptionsController } from '@/Controllers/SearchOptionsController' type Props = { searchOptions: SearchOptionsController + showNoteTitleOnlyOption?: boolean } -const SearchOptions = ({ searchOptions }: Props) => { +const SearchOptions = ({ searchOptions, showNoteTitleOnlyOption = false }: Props) => { const { includeProtectedContents, includeArchived, includeTrashed, noteTitleOnly } = searchOptions const toggleIncludeProtectedContents = useCallback(async () => { @@ -26,7 +27,9 @@ const SearchOptions = ({ searchOptions }: Props) => { - + {showNoteTitleOnlyOption && ( + + )} ) } diff --git a/packages/web/src/javascripts/Controllers/FeaturesController.ts b/packages/web/src/javascripts/Controllers/FeaturesController.ts index 1ce26bac6f5..a251d32e24a 100644 --- a/packages/web/src/javascripts/Controllers/FeaturesController.ts +++ b/packages/web/src/javascripts/Controllers/FeaturesController.ts @@ -13,7 +13,7 @@ import { import { action, makeObservable, observable, runInAction, when } from 'mobx' import { AbstractViewController } from './Abstract/AbstractViewController' import { CrossControllerEvent } from './CrossControllerEvent' -import { featureTrunkVaultsEnabled } from '@/FeatureTrunk' +import { featureTrunkSearchEnhancementsEnabled, featureTrunkVaultsEnabled } from '@/FeatureTrunk' export class FeaturesController extends AbstractViewController implements InternalEventHandlerInterface { hasFolders: boolean @@ -152,4 +152,11 @@ export class FeaturesController extends AbstractViewController implements Intern return featureTrunkVaultsEnabled() || isEntitledToFeature } + + isSearchEnhancementsEnabled(): boolean { + return ( + featureTrunkSearchEnhancementsEnabled() || + this.features.hasRole(RoleName.create(RoleName.NAMES.InternalTeamUser).getValue()) + ) + } } diff --git a/packages/web/src/javascripts/FeatureTrunk.ts b/packages/web/src/javascripts/FeatureTrunk.ts index b8e6128c128..92875daa7e9 100644 --- a/packages/web/src/javascripts/FeatureTrunk.ts +++ b/packages/web/src/javascripts/FeatureTrunk.ts @@ -11,3 +11,7 @@ export function featureTrunkEnabled(trunk: FeatureTrunkName): boolean { export function featureTrunkVaultsEnabled(): boolean { return InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults) } + +export function featureTrunkSearchEnhancementsEnabled(): boolean { + return InternalFeatureService.get().isFeatureEnabled(InternalFeature.SearchEnhancements) +} From e800bc4710a1563e428f7122c1f3abccb538269d Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Thu, 18 Jun 2026 23:56:45 -0300 Subject: [PATCH 03/12] feat: Improve UX --- .../SearchOptions/SearchOptions.tsx | 47 +++++++++++++++---- .../Controllers/SearchOptionsController.ts | 6 +-- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx b/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx index bf4174c05e1..b5c7085b4a0 100644 --- a/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx +++ b/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx @@ -1,35 +1,62 @@ import { observer } from 'mobx-react-lite' import Bubble from '@/Components/Bubble/Bubble' +import Checkbox from '@/Components/Checkbox/Checkbox' import { useCallback } from 'react' import { SearchOptionsController } from '@/Controllers/SearchOptionsController' +import { classNames } from '@standardnotes/snjs' -type Props = { +type SearchBubblesProps = { searchOptions: SearchOptionsController - showNoteTitleOnlyOption?: boolean } -const SearchOptions = ({ searchOptions, showNoteTitleOnlyOption = false }: Props) => { - const { includeProtectedContents, includeArchived, includeTrashed, noteTitleOnly } = searchOptions +const SearchBubbles = observer(({ searchOptions }: SearchBubblesProps) => { + const { includeProtectedContents, includeArchived, includeTrashed } = searchOptions const toggleIncludeProtectedContents = useCallback(async () => { await searchOptions.toggleIncludeProtectedContents() }, [searchOptions]) return ( -
e.preventDefault()}> + <> - - + + ) +}) + +type Props = { + searchOptions: SearchOptionsController + showNoteTitleOnlyOption?: boolean +} + +const SearchOptions = ({ searchOptions, showNoteTitleOnlyOption = false }: Props) => { + const { noteTitleOnly } = searchOptions + + if (!showNoteTitleOnlyOption) { + return ( +
e.preventDefault()}> + +
+ ) + } + + return ( +
e.preventDefault()}> + searchOptions.setNoteTitleOnly(event.target.checked)} + /> - {showNoteTitleOnlyOption && ( - - )} +
+ +
) } diff --git a/packages/web/src/javascripts/Controllers/SearchOptionsController.ts b/packages/web/src/javascripts/Controllers/SearchOptionsController.ts index 944def1a9c7..7be5f905210 100644 --- a/packages/web/src/javascripts/Controllers/SearchOptionsController.ts +++ b/packages/web/src/javascripts/Controllers/SearchOptionsController.ts @@ -30,7 +30,7 @@ export class SearchOptionsController extends AbstractViewController implements I toggleIncludeTrashed: action, toggleIncludeProtectedContents: action, refreshIncludeProtectedContents: action, - toggleNoteTitleOnly: action, + setNoteTitleOnly: action, }) eventBus.addEventHandler(this, ApplicationEvent.UnprotectedSessionBegan) @@ -68,7 +68,7 @@ export class SearchOptionsController extends AbstractViewController implements I } } - toggleNoteTitleOnly = (): void => { - this.noteTitleOnly = !this.noteTitleOnly + setNoteTitleOnly = (value: boolean): void => { + this.noteTitleOnly = value } } From 3f14c9620be41401256254a816dfe87fe2c3e633 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Fri, 19 Jun 2026 00:03:30 -0300 Subject: [PATCH 04/12] fix: Fixes checkboxes cursor --- .../src/javascripts/Components/Checkbox/Checkbox.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/web/src/javascripts/Components/Checkbox/Checkbox.tsx b/packages/web/src/javascripts/Components/Checkbox/Checkbox.tsx index e2ace706a64..daa7cbb1d83 100644 --- a/packages/web/src/javascripts/Components/Checkbox/Checkbox.tsx +++ b/packages/web/src/javascripts/Components/Checkbox/Checkbox.tsx @@ -1,3 +1,4 @@ +import { classNames } from '@standardnotes/snjs' import { ChangeEventHandler, FunctionComponent } from 'react' type CheckboxProps = { @@ -10,9 +11,15 @@ type CheckboxProps = { const Checkbox: FunctionComponent = ({ name, checked, onChange, disabled, label }) => { return ( -