Skip to content

Commit c0d2ecc

Browse files
committed
feat: search notifications
Signed-off-by: Adam Setch <[email protected]>
1 parent 236a624 commit c0d2ecc

File tree

4 files changed

+37
-62
lines changed

4 files changed

+37
-62
lines changed

src/renderer/components/filters/SearchFilterSuggestions.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { Box, Popover, Stack, Text } from '@primer/react';
55
import { Opacity } from '../../types';
66
import { cn } from '../../utils/cn';
77
import {
8-
BASE_QUALIFIERS,
9-
QUALIFIERS,
8+
BASE_SEARCH_QUALIFIERS,
9+
ALL_SEARCH_QUALIFIERS,
1010
SEARCH_DELIMITER,
1111
} from '../../utils/notifications/filters/search';
1212

@@ -28,7 +28,7 @@ export const SearchFilterSuggestions: FC<SearchFilterSuggestionsProps> = ({
2828
}
2929

3030
const lower = inputValue.toLowerCase();
31-
const base = isDetailedNotificationsEnabled ? QUALIFIERS : BASE_QUALIFIERS;
31+
const base = isDetailedNotificationsEnabled ? ALL_SEARCH_QUALIFIERS : BASE_SEARCH_QUALIFIERS;
3232
const suggestions = base.filter(
3333
(q) => q.prefix.startsWith(lower) || inputValue === '',
3434
);

src/renderer/utils/notifications/filters/filter.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import {
1313
subjectTypeFilter,
1414
userTypeFilter,
1515
type SearchQualifier,
16-
BASE_QUALIFIERS,
17-
DETAILED_ONLY_QUALIFIERS,
16+
BASE_SEARCH_QUALIFIERS,
17+
DETAILED_ONLY_SEARCH_QUALIFIERS,
1818
} from '.';
1919

2020

@@ -27,7 +27,7 @@ export function filterBaseNotifications(
2727
let passesFilters = true;
2828

2929
// Apply base qualifier include/exclude filters (org, repo, etc.)
30-
for (const qualifier of BASE_QUALIFIERS) {
30+
for (const qualifier of BASE_SEARCH_QUALIFIERS) {
3131
if (!passesFilters) break;
3232
passesFilters =
3333
passesFilters &&
@@ -139,7 +139,7 @@ function passesUserFilters(
139139
}
140140

141141
// Apply detailed-only qualifier search token filters (e.g. author)
142-
for (const qualifier of DETAILED_ONLY_QUALIFIERS) {
142+
for (const qualifier of DETAILED_ONLY_SEARCH_QUALIFIERS) {
143143
if (!passesFilters) break;
144144
passesFilters =
145145
passesFilters &&

src/renderer/utils/notifications/filters/search.test.ts

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,40 @@
11
import { partialMockNotification } from '../../../__mocks__/partial-mocks';
22
import type { Link } from '../../../types';
33
import type { Owner } from '../../../typesGitHub';
4-
import {
5-
filterNotificationBySearchTerm,
6-
matchQualifierByPrefix,
7-
QUALIFIERS,
8-
} from './search';
4+
import { filterNotificationBySearchTerm, parseSearchToken, ALL_SEARCH_QUALIFIERS } from './search';
95

106
// (helper removed – no longer used)
117

128
describe('renderer/utils/notifications/filters/search.ts', () => {
13-
describe('matchQualifierByPrefix', () => {
9+
describe('parseSearchToken (prefix matching behavior)', () => {
1410
it('returns null for empty string', () => {
15-
expect(matchQualifierByPrefix('')).toBeNull();
11+
expect(parseSearchToken('')).toBeNull();
1612
});
1713

1814
it('returns null when no qualifier prefix matches', () => {
19-
expect(matchQualifierByPrefix('unknown:value')).toBeNull();
20-
expect(matchQualifierByPrefix('auth:foo')).toBeNull(); // near miss
15+
expect(parseSearchToken('unknown:value')).toBeNull();
16+
expect(parseSearchToken('auth:foo')).toBeNull(); // near miss
2117
});
2218

2319
it('matches each known qualifier by its exact prefix and additional value', () => {
24-
for (const q of QUALIFIERS) {
20+
for (const q of ALL_SEARCH_QUALIFIERS) {
2521
const token = q.prefix + 'someValue';
26-
const qualifier = matchQualifierByPrefix(token);
27-
expect(qualifier).not.toBeNull();
28-
expect(qualifier).toBe(q);
22+
const parsed = parseSearchToken(token);
23+
expect(parsed).not.toBeNull();
24+
expect(parsed?.qualifier).toBe(q);
2925
}
3026
});
3127

3228
it('is case-sensitive (does not match mismatched casing)', () => {
3329
// Intentionally alter case of prefix characters
34-
expect(matchQualifierByPrefix('Author:foo')).toBeNull();
35-
expect(matchQualifierByPrefix('ORG:bar')).toBeNull();
36-
expect(matchQualifierByPrefix('Repo:baz')).toBeNull();
30+
expect(parseSearchToken('Author:foo')).toBeNull();
31+
expect(parseSearchToken('ORG:bar')).toBeNull();
32+
expect(parseSearchToken('Repo:baz')).toBeNull();
3733
});
3834

3935
it('does not match when prefix appears later in the token', () => {
40-
expect(matchQualifierByPrefix('xauthor:foo')).toBeNull();
41-
expect(matchQualifierByPrefix('xxorg:bar')).toBeNull();
36+
expect(parseSearchToken('xauthor:foo')).toBeNull();
37+
expect(parseSearchToken('xxorg:bar')).toBeNull();
4238
});
4339
});
4440

src/renderer/utils/notifications/filters/search.ts

Lines changed: 16 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -29,37 +29,20 @@ export type SearchQualifierKey = keyof typeof SEARCH_QUALIFIERS;
2929
export type SearchQualifier = (typeof SEARCH_QUALIFIERS)[SearchQualifierKey];
3030
export type SearchPrefix = SearchQualifier['prefix'];
3131

32-
export const QUALIFIERS: readonly SearchQualifier[] = Object.values(
32+
export const ALL_SEARCH_QUALIFIERS: readonly SearchQualifier[] = Object.values(
3333
SEARCH_QUALIFIERS,
3434
) as readonly SearchQualifier[];
3535

3636

37-
export const BASE_QUALIFIERS: readonly SearchQualifier[] = QUALIFIERS.filter(
37+
export const BASE_SEARCH_QUALIFIERS: readonly SearchQualifier[] = ALL_SEARCH_QUALIFIERS.filter(
3838
(q) => !q.requiresDetailsNotifications,
3939
);
4040

41-
export const DETAILED_ONLY_QUALIFIERS: readonly SearchQualifier[] = QUALIFIERS.filter(
41+
export const DETAILED_ONLY_SEARCH_QUALIFIERS: readonly SearchQualifier[] = ALL_SEARCH_QUALIFIERS.filter(
4242
(q) => q.requiresDetailsNotifications,
4343
);
4444

4545

46-
// Qualifier selection helpers (centralized to avoid duplicating logic in UI components)
47-
export function getAvailableSearchQualifiers(
48-
detailedNotificationsEnabled: boolean,
49-
): readonly SearchQualifier[] {
50-
const all = Object.values(SEARCH_QUALIFIERS) as readonly SearchQualifier[];
51-
if (detailedNotificationsEnabled) {
52-
return all;
53-
}
54-
55-
return all.filter((q) => !q.requiresDetailsNotifications);
56-
}
57-
58-
export const BASE_SEARCH_QUALIFIERS: readonly SearchQualifier[] =
59-
getAvailableSearchQualifiers(false);
60-
export const ALL_SEARCH_QUALIFIERS: readonly SearchQualifier[] =
61-
getAvailableSearchQualifiers(true);
62-
6346
export function hasIncludeSearchFilters(settings: SettingsState) {
6447
return settings.filterIncludeSearchTokens.length > 0;
6548
}
@@ -68,15 +51,6 @@ export function hasExcludeSearchFilters(settings: SettingsState) {
6851
return settings.filterExcludeSearchTokens.length > 0;
6952
}
7053

71-
export function matchQualifierByPrefix(token: string) {
72-
for (const qualifier of QUALIFIERS) {
73-
if (token.startsWith(qualifier.prefix)) {
74-
return qualifier;
75-
}
76-
}
77-
return null;
78-
}
79-
8054
function stripPrefix(token: string, qualifier: SearchQualifier) {
8155
return token.slice(qualifier.prefix.length).trim();
8256
}
@@ -88,17 +62,22 @@ export interface ParsedSearchToken {
8862
}
8963

9064
export function parseSearchToken(token: string): ParsedSearchToken | null {
91-
const qualifier = matchQualifierByPrefix(token);
92-
if (!qualifier) {
65+
if (!token) {
9366
return null;
9467
}
9568

96-
const value = stripPrefix(token, qualifier);
97-
if (!value) {
98-
return null;
69+
for (const qualifier of ALL_SEARCH_QUALIFIERS) {
70+
if (token.startsWith(qualifier.prefix)) {
71+
const value = stripPrefix(token, qualifier);
72+
73+
if (!value) {
74+
return null; // prefix only
75+
}
76+
77+
return { qualifier, value, valueLower: value.toLowerCase() };
78+
}
9979
}
100-
101-
return { qualifier, value, valueLower: value.toLowerCase() };
80+
return null;
10281
}
10382

10483
// Normalize raw user input from the token text field into a SearchToken (string)
@@ -110,7 +89,7 @@ export function normalizeSearchInputToToken(raw: string): string | null {
11089
}
11190

11291
const lower = value.toLowerCase();
113-
const matchedQualifier = QUALIFIERS.find((q) => lower.startsWith(q.prefix));
92+
const matchedQualifier = ALL_SEARCH_QUALIFIERS.find((q) => lower.startsWith(q.prefix));
11493

11594
if (!matchedQualifier) {
11695
return null;

0 commit comments

Comments
 (0)