Skip to content

Commit bb4d377

Browse files
committed
feat: search notifications
Signed-off-by: Adam Setch <[email protected]>
1 parent 4860d46 commit bb4d377

File tree

5 files changed

+82
-37
lines changed

5 files changed

+82
-37
lines changed

src/main/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import path from 'path';
1+
import path from 'node:path';
22

33
import {
44
app,

src/renderer/components/filters/SearchFilter.tsx

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ import { Box, Stack, Text } from '@primer/react';
1212

1313
import { AppContext } from '../../context/App';
1414
import { IconColor, type SearchToken, Size } from '../../types';
15-
import { hasExcludeSearchFilters, hasIncludeSearchFilters } from '../../utils/notifications/filters/search';
15+
import { cn } from '../../utils/cn';
16+
import {
17+
hasExcludeSearchFilters,
18+
hasIncludeSearchFilters,
19+
} from '../../utils/notifications/filters/search';
1620
import { Title } from '../primitives/Title';
1721
import { RequiresDetailedNotificationWarning } from './RequiresDetailedNotificationsWarning';
1822
import { TokenSearchInput } from './TokenSearchInput';
19-
import { cn } from '../../utils/cn';
2023

2124
type InputToken = { id: number; text: string };
2225

@@ -37,40 +40,50 @@ export const SearchFilter: FC = () => {
3740
const mapValuesToTokens = (values: string[]): InputToken[] =>
3841
values.map((value, index) => ({ id: index, text: value }));
3942

40-
const [includeSearchTokens, setIncludeSearchTokens] = useState<InputToken[]>(mapValuesToTokens(settings.filterIncludeSearchTokens));
43+
const [includeSearchTokens, setIncludeSearchTokens] = useState<InputToken[]>(
44+
mapValuesToTokens(settings.filterIncludeSearchTokens),
45+
);
4146

4247
const addIncludeSearchToken = (value: string) => {
4348
if (!value || includeSearchTokens.some((v) => v.text === value)) return;
44-
const nextId = includeSearchTokens.reduce((m, t) => Math.max(m, t.id), -1) + 1;
45-
setIncludeSearchTokens([...includeSearchTokens, { id: nextId, text: value }]);
49+
const nextId =
50+
includeSearchTokens.reduce((m, t) => Math.max(m, t.id), -1) + 1;
51+
setIncludeSearchTokens([
52+
...includeSearchTokens,
53+
{ id: nextId, text: value },
54+
]);
4655
updateFilter('filterIncludeSearchTokens', value as SearchToken, true);
4756
};
4857

4958
const removeIncludeSearchToken = (tokenId: string | number) => {
5059
const value = includeSearchTokens.find((v) => v.id === tokenId)?.text || '';
51-
if (value) updateFilter('filterIncludeSearchTokens', value as SearchToken, false);
60+
if (value)
61+
updateFilter('filterIncludeSearchTokens', value as SearchToken, false);
5262
setIncludeSearchTokens(includeSearchTokens.filter((v) => v.id !== tokenId));
5363
};
5464

55-
// now handled inside TokenSearchInput
56-
57-
const [excludeSearchTokens, setExcludeSearchTokens] = useState<InputToken[]>(mapValuesToTokens(settings.filterExcludeSearchTokens));
65+
const [excludeSearchTokens, setExcludeSearchTokens] = useState<InputToken[]>(
66+
mapValuesToTokens(settings.filterExcludeSearchTokens),
67+
);
5868

5969
const addExcludeSearchToken = (value: string) => {
6070
if (!value || excludeSearchTokens.some((v) => v.text === value)) return;
61-
const nextId = excludeSearchTokens.reduce((m, t) => Math.max(m, t.id), -1) + 1;
62-
setExcludeSearchTokens([...excludeSearchTokens, { id: nextId, text: value }]);
71+
const nextId =
72+
excludeSearchTokens.reduce((m, t) => Math.max(m, t.id), -1) + 1;
73+
setExcludeSearchTokens([
74+
...excludeSearchTokens,
75+
{ id: nextId, text: value },
76+
]);
6377
updateFilter('filterExcludeSearchTokens', value as SearchToken, true);
6478
};
6579

6680
const removeExcludeSearchToken = (tokenId: string | number) => {
6781
const value = excludeSearchTokens.find((v) => v.id === tokenId)?.text || '';
68-
if (value) updateFilter('filterExcludeSearchTokens', value as SearchToken, false);
82+
if (value)
83+
updateFilter('filterExcludeSearchTokens', value as SearchToken, false);
6984
setExcludeSearchTokens(excludeSearchTokens.filter((v) => v.id !== tokenId));
7085
};
7186

72-
// handled by TokenSearchInput
73-
7487
// Basic suggestions for prefixes
7588
const fieldsetId = useId();
7689

@@ -85,7 +98,14 @@ export const SearchFilter: FC = () => {
8598
<Stack direction="vertical" gap="condensed">
8699
<Stack direction="horizontal" gap="condensed">
87100
<PersonIcon size={Size.SMALL} />
88-
<Text className={cn("text-gitify-caution", !settings.detailedNotifications && "line-through")}>Author (author:handle)</Text>
101+
<Text
102+
className={cn(
103+
'text-gitify-caution',
104+
!settings.detailedNotifications && 'line-through',
105+
)}
106+
>
107+
Author (author:handle)
108+
</Text>
89109
</Stack>
90110
<Stack direction="horizontal" gap="condensed">
91111
<OrganizationIcon size={Size.SMALL} />
@@ -111,9 +131,7 @@ export const SearchFilter: FC = () => {
111131
label="Include"
112132
onAdd={addIncludeSearchToken}
113133
onRemove={removeIncludeSearchToken}
114-
showSuggestionsOnFocusIfEmpty={
115-
!hasIncludeSearchFilters(settings)
116-
}
134+
showSuggestionsOnFocusIfEmpty={!hasIncludeSearchFilters(settings)}
117135
tokens={includeSearchTokens}
118136
/>
119137
<TokenSearchInput
@@ -122,9 +140,7 @@ export const SearchFilter: FC = () => {
122140
label="Exclude"
123141
onAdd={addExcludeSearchToken}
124142
onRemove={removeExcludeSearchToken}
125-
showSuggestionsOnFocusIfEmpty={
126-
!hasExcludeSearchFilters(settings)
127-
}
143+
showSuggestionsOnFocusIfEmpty={!hasExcludeSearchFilters(settings)}
128144
tokens={excludeSearchTokens}
129145
/>
130146
</Stack>

src/renderer/components/filters/SearchFilterSuggestions.tsx

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { Box, Popover, Stack, Text } from '@primer/react';
44

55
import { Opacity } from '../../types';
66
import { cn } from '../../utils/cn';
7-
import { SEARCH_QUALIFIERS } from '../../utils/notifications/filters/search';
7+
import {
8+
SEARCH_DELIMITER,
9+
SEARCH_QUALIFIERS,
10+
} from '../../utils/notifications/filters/search';
811

912
const QUALIFIERS = Object.values(SEARCH_QUALIFIERS);
1013

@@ -23,24 +26,42 @@ export const SearchFilterSuggestions: FC<SearchFilterSuggestionsProps> = ({
2326
return null;
2427
}
2528

29+
const lower = inputValue.toLowerCase();
30+
const suggestions = QUALIFIERS.filter(
31+
(q) => q.prefix.startsWith(lower) || inputValue === '',
32+
);
33+
const beginsWithKnownQualifier = QUALIFIERS.some((q) =>
34+
lower.startsWith(q.prefix),
35+
);
36+
2637
return (
2738
<Popover caret={false} onOpenChange={onClose} open>
2839
<Popover.Content sx={{ p: 2, mt: 2, width: '100%' }}>
2940
<Stack direction="vertical" gap="condensed">
30-
{QUALIFIERS.filter(
31-
(q) =>
32-
q.prefix.startsWith(inputValue.toLowerCase()) ||
33-
inputValue === '',
34-
).map((q) => (
35-
<Box key={q.prefix}>
36-
<Stack direction="vertical" gap="none">
37-
<Text className="text-xs font-semibold">{q.prefix}</Text>
41+
{suggestions.length > 0 &&
42+
suggestions.map((q) => (
43+
<Box key={q.prefix}>
44+
<Stack direction="vertical" gap="none">
45+
<Text className="text-xs font-semibold">{q.prefix}</Text>
46+
<Text className={cn('text-xs', Opacity.HIGH)}>
47+
{q.description}
48+
</Text>
49+
</Stack>
50+
</Box>
51+
))}
52+
{inputValue !== '' &&
53+
suggestions.length === 0 &&
54+
!beginsWithKnownQualifier && (
55+
<Box>
3856
<Text className={cn('text-xs', Opacity.HIGH)}>
39-
{q.description}
57+
Please use one of the supported filters [
58+
{QUALIFIERS.map((q) =>
59+
q.prefix.replace(SEARCH_DELIMITER, ''),
60+
).join(', ')}
61+
]
4062
</Text>
41-
</Stack>
42-
</Box>
43-
))}
63+
</Box>
64+
)}
4465
</Stack>
4566
</Popover.Content>
4667
</Popover>

src/renderer/components/filters/TokenSearchInput.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { type FC, useState } from 'react';
33
import { Box, Stack, Text, TextInputWithTokens } from '@primer/react';
44

55
import type { SearchToken } from '../../types';
6-
import { normalizeSearchInputToToken } from '../../utils/notifications/filters/search';
6+
import {
7+
normalizeSearchInputToToken,
8+
SEARCH_DELIMITER,
9+
} from '../../utils/notifications/filters/search';
710
import { SearchFilterSuggestions } from './SearchFilterSuggestions';
811

912
export interface TokenInputItem {
@@ -81,7 +84,10 @@ export const TokenSearchInput: FC<TokenSearchInputProps> = ({
8184
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
8285
setInputValue(e.target.value);
8386
const val = e.target.value.trim();
84-
if (!val.includes(':') || val.endsWith(':')) {
87+
if (
88+
!val.includes(SEARCH_DELIMITER) ||
89+
val.endsWith(SEARCH_DELIMITER)
90+
) {
8591
setShowSuggestions(true);
8692
} else {
8793
setShowSuggestions(false);

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { SettingsState } from '../../../types';
22
import type { Notification } from '../../../typesGitHub';
33

4+
export const SEARCH_DELIMITER = ':';
5+
46
export const SEARCH_QUALIFIERS = {
57
author: {
68
prefix: 'author:',

0 commit comments

Comments
 (0)