Skip to content

Commit b32ceb4

Browse files
committed
feat: search notifications
Signed-off-by: Adam Setch <[email protected]>
1 parent 4ac7d00 commit b32ceb4

File tree

3 files changed

+107
-79
lines changed

3 files changed

+107
-79
lines changed

src/renderer/components/filters/SearchFilter.tsx

Lines changed: 59 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -33,27 +33,30 @@ type InputToken = { id: number; text: string };
3333
type Qualifier = {
3434
key: string; // the qualifier prefix shown to user (author, org, repo)
3535
description: string;
36-
example: string;
3736
};
3837

3938
const QUALIFIERS: Qualifier[] = [
40-
{
41-
key: 'author:',
42-
description: 'Filter by notification author',
43-
example: 'author:octocat',
44-
},
45-
{
46-
key: 'org:',
47-
description: 'Filter by organization owner',
48-
example: 'org:microsoft',
49-
},
50-
{
51-
key: 'repo:',
52-
description: 'Filter by repository full name',
53-
example: 'repo:gitify-app/gitify',
54-
},
39+
{ key: 'author:', description: 'Filter by notification author' },
40+
{ key: 'org:', description: 'Filter by organization owner' },
41+
{ key: 'repo:', description: 'Filter by repository full name' },
5542
];
5643

44+
const INCLUDE_EXAMPLES: Record<string, string> = {
45+
'author:': 'author:octocat',
46+
'org:': 'org:gitify-app',
47+
'repo:': 'repo:gitify-app/gitify',
48+
};
49+
50+
const EXCLUDE_EXAMPLES: Record<string, string> = {
51+
'author:': 'author:spambot',
52+
'org:': 'org:hooli',
53+
'repo:': 'repo:hooli/nucleas',
54+
};
55+
56+
function getExample(key: string, mode: 'include' | 'exclude') {
57+
return (mode === 'include' ? INCLUDE_EXAMPLES : EXCLUDE_EXAMPLES)[key] || '';
58+
}
59+
5760
const tokenEvents = ['Enter', 'Tab', ' ', ','];
5861

5962
function parseRawValue(raw: string): string | null {
@@ -209,7 +212,7 @@ export const SearchFilter: FC = () => {
209212
direction="horizontal"
210213
gap="condensed"
211214
>
212-
<Box className="font-medium text-gitify-font w-28">
215+
<Box className="font-medium text-gitify-font w-20">
213216
<Stack align="center" direction="horizontal" gap="condensed">
214217
<CheckCircleFillIcon className={IconColor.GREEN} />
215218
<Text>Include:</Text>
@@ -236,9 +239,17 @@ export const SearchFilter: FC = () => {
236239
setShowIncludeSuggestions(false);
237240
}
238241
}}
242+
onFocus={(e) => {
243+
if (
244+
!hasExcludeSearchFilters(settings) &&
245+
!!settings.detailedNotifications &&
246+
(e.target as HTMLInputElement).value.trim() === ''
247+
) {
248+
setShowIncludeSuggestions(true);
249+
}
250+
}}
239251
onKeyDown={includeSearchTokensKeyDown}
240252
onTokenRemove={removeIncludeSearchToken}
241-
placeholder="author:octocat org:microsoft repo:gitify"
242253
size="small"
243254
title="Include searches"
244255
tokens={includeSearchTokens}
@@ -271,11 +282,18 @@ export const SearchFilter: FC = () => {
271282
setShowIncludeSuggestions(false);
272283
}}
273284
>
274-
<Stack direction="vertical" gap="none">
275-
<Text>{q.key}</Text>
276-
<Text className="text-xs opacity-70">
285+
<Stack
286+
className="text-xs"
287+
direction="vertical"
288+
gap="none"
289+
>
290+
<Text className="text-xs font-semibold">{q.key}</Text>
291+
<Text className="text-[10px] opacity-70">
277292
{q.description}
278293
</Text>
294+
<Text className="text-[10px] font-mono opacity-80">
295+
{getExample(q.key, 'include')}
296+
</Text>
279297
</Stack>
280298
</ActionList.Item>
281299
))}
@@ -292,7 +310,7 @@ export const SearchFilter: FC = () => {
292310
direction="horizontal"
293311
gap="condensed"
294312
>
295-
<Box className="font-medium text-gitify-font w-28">
313+
<Box className="font-medium text-gitify-font w-20">
296314
<Stack align="center" direction="horizontal" gap="condensed">
297315
<NoEntryFillIcon className={IconColor.RED} />
298316
<Text>Exclude:</Text>
@@ -318,9 +336,17 @@ export const SearchFilter: FC = () => {
318336
setShowExcludeSuggestions(false);
319337
}
320338
}}
339+
onFocus={(e) => {
340+
if (
341+
!hasIncludeSearchFilters(settings) &&
342+
!!settings.detailedNotifications &&
343+
(e.target as HTMLInputElement).value.trim() === ''
344+
) {
345+
setShowExcludeSuggestions(true);
346+
}
347+
}}
321348
onKeyDown={excludeSearchTokensKeyDown}
322349
onTokenRemove={removeExcludeSearchToken}
323-
placeholder="author:spambot org:legacycorp repo:oldrepo"
324350
size="small"
325351
title="Exclude searches"
326352
tokens={excludeSearchTokens}
@@ -353,11 +379,18 @@ export const SearchFilter: FC = () => {
353379
setShowExcludeSuggestions(false);
354380
}}
355381
>
356-
<Stack direction="vertical" gap="none">
357-
<Text>{q.key}</Text>
358-
<Text className="text-xs opacity-70">
382+
<Stack
383+
className="text-xs"
384+
direction="vertical"
385+
gap="none"
386+
>
387+
<Text className="text-xs font-semibold">{q.key}</Text>
388+
<Text className="text-[10px] opacity-70">
359389
{q.description}
360390
</Text>
391+
<Text className="text-[10px] font-mono opacity-80">
392+
{getExample(q.key, 'exclude')}
393+
</Text>
361394
</Stack>
362395
</ActionList.Item>
363396
))}

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

Lines changed: 47 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { partialMockNotification } from '../../../__mocks__/partial-mocks';
22
import { mockSettings } from '../../../__mocks__/state-mocks';
33
import { defaultSettings } from '../../../context/defaults';
44
import type { Link, SearchToken, SettingsState } from '../../../types';
5-
import type { Notification } from '../../../typesGitHub';
5+
import type { Repository } from '../../../typesGitHub';
66
import {
77
filterBaseNotifications,
88
filterDetailedNotifications,
@@ -125,53 +125,47 @@ describe('renderer/utils/notifications/filters/filter.ts', () => {
125125
});
126126

127127
it('should filter notifications that match include organization', async () => {
128-
// Initialize repository owner structure if it doesn't exist
129-
// @ts-expect-error augment mock notification repository shape
130-
if (!mockNotifications[0].repository)
131-
mockNotifications[0].repository = {};
132-
// @ts-expect-error augment mock notification repository owner
133-
if (!mockNotifications[0].repository.owner)
134-
mockNotifications[0].repository.owner = {};
135-
// @ts-expect-error augment mock notification repository shape
136-
if (!mockNotifications[1].repository)
137-
mockNotifications[1].repository = {};
138-
// @ts-expect-error augment mock notification repository owner
139-
if (!mockNotifications[1].repository.owner)
140-
mockNotifications[1].repository.owner = {};
141-
142-
mockNotifications[0].repository.owner.login = 'microsoft';
143-
mockNotifications[1].repository.owner.login = 'github';
128+
mockNotifications[1].repository = {
129+
owner: {
130+
login: 'gitify-app',
131+
},
132+
} as Repository;
133+
134+
mockNotifications[1].repository = {
135+
owner: {
136+
login: 'github',
137+
},
138+
} as Repository;
144139

145140
// Apply base filtering first (where organization filtering now happens)
146141
let result = filterBaseNotifications(mockNotifications, {
147142
...mockSettings,
148-
filterIncludeSearchTokens: ['org:microsoft' as SearchToken],
143+
filterIncludeSearchTokens: ['org:gitify-app' as SearchToken],
149144
});
150145

151146
// Then apply detailed filtering
152147
result = filterDetailedNotifications(result, {
153148
...mockSettings,
154149
detailedNotifications: true,
155-
filterIncludeSearchTokens: ['org:microsoft' as SearchToken],
150+
filterIncludeSearchTokens: ['org:gitify-app' as SearchToken],
156151
});
157152

158153
expect(result.length).toBe(1);
159154
expect(result).toEqual([mockNotifications[0]]);
160155
});
161156

162157
it('should filter notifications that match exclude organization', async () => {
163-
// Initialize repository owner structure if it doesn't exist
164-
if (!mockNotifications[0].repository)
165-
mockNotifications[0].repository = {};
166-
if (!mockNotifications[0].repository.owner)
167-
mockNotifications[0].repository.owner = {};
168-
if (!mockNotifications[1].repository)
169-
mockNotifications[1].repository = {};
170-
if (!mockNotifications[1].repository.owner)
171-
mockNotifications[1].repository.owner = {};
172-
173-
mockNotifications[0].repository.owner.login = 'microsoft';
174-
mockNotifications[1].repository.owner.login = 'github';
158+
mockNotifications[1].repository = {
159+
owner: {
160+
login: 'gitify-app',
161+
},
162+
} as Repository;
163+
164+
mockNotifications[1].repository = {
165+
owner: {
166+
login: 'github',
167+
},
168+
} as Repository;
175169

176170
// Apply base filtering first (where organization filtering now happens)
177171
let result = filterBaseNotifications(mockNotifications, {
@@ -191,50 +185,51 @@ describe('renderer/utils/notifications/filters/filter.ts', () => {
191185
});
192186

193187
it('should filter notifications that match include repository', async () => {
194-
// Ensure repository name structure
195-
// @ts-expect-error augment mock shape for repository filtering
196-
if (!mockNotifications[0].repository)
197-
mockNotifications[0].repository = {};
198-
// @ts-expect-error augment mock shape for repository filtering
199-
if (!mockNotifications[1].repository)
200-
mockNotifications[1].repository = {};
201-
mockNotifications[0].repository.name = 'gitify';
202-
mockNotifications[1].repository.name = 'other';
188+
mockNotifications[1].repository = {
189+
full_name: 'gitify-app/gitify',
190+
} as Repository;
191+
192+
mockNotifications[1].repository = {
193+
full_name: 'other/other',
194+
} as Repository;
203195

204196
let result = filterBaseNotifications(mockNotifications, {
205197
...mockSettings,
206-
filterIncludeSearchTokens: ['repo:gitify' as SearchToken],
198+
filterIncludeSearchTokens: ['repo:gitify-app/gitify' as SearchToken],
207199
});
208200

209201
result = filterDetailedNotifications(result, {
210202
...mockSettings,
211203
detailedNotifications: true,
212-
filterIncludeSearchTokens: ['repo:gitify' as SearchToken],
204+
filterIncludeSearchTokens: ['repo:gitify-app/gitify' as SearchToken],
213205
});
214206

215207
expect(result.length).toBe(1);
216208
expect(result).toEqual([mockNotifications[0]]);
217209
});
218210

219211
it('should filter notifications that match exclude repository', async () => {
220-
// @ts-expect-error augment mock shape for repository filtering
221-
if (!mockNotifications[0].repository)
222-
mockNotifications[0].repository = {};
223-
// @ts-expect-error augment mock shape for repository filtering
224-
if (!mockNotifications[1].repository)
225-
mockNotifications[1].repository = {};
226-
mockNotifications[0].repository.name = 'gitify';
227-
mockNotifications[1].repository.name = 'other';
212+
mockNotifications[1].repository = {
213+
id: 1,
214+
name: 'gitify',
215+
full_name: 'gitify-app/gitify',
216+
} as Repository;
217+
218+
mockNotifications[1].repository = {
219+
id: 2,
220+
name: 'other',
221+
full_name: 'other/other',
222+
} as Repository;
228223

229224
let result = filterBaseNotifications(mockNotifications, {
230225
...mockSettings,
231-
filterExcludeSearchTokens: ['repo:other'],
226+
filterExcludeSearchTokens: ['repo:other/other' as SearchToken],
232227
});
233228

234229
result = filterDetailedNotifications(result, {
235230
...mockSettings,
236231
detailedNotifications: true,
237-
filterExcludeSearchTokens: ['repo:other'],
232+
filterExcludeSearchTokens: ['repo:other/other' as SearchToken],
238233
});
239234

240235
expect(result.length).toBe(1);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function filterNotificationBySearchTerm(
4848

4949
if (isRepoToken(token)) {
5050
const repo = stripPrefix(token);
51-
const name = notification.repository?.name;
51+
const name = notification.repository?.full_name;
5252
return name?.toLowerCase() === repo.toLowerCase();
5353
}
5454

0 commit comments

Comments
 (0)