Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
544b7be
feat(api): implement GraphQL query merging for notification enrichment
afonsojramos Dec 30, 2025
27e5321
refactor: clean up query merging implementation
afonsojramos Dec 30, 2025
f00cb42
test: add coverage for graphql utils and enrichNotifications
afonsojramos Dec 30, 2025
ba8bb0a
test: remove skipped integration test for detailed notifications
afonsojramos Dec 30, 2025
9c4227a
refactor: discussion gql types and common types. user-mocks reuse
setchy Dec 30, 2025
3c30d3d
refactor: rename arg to lastComments to lastThreadedComment to avoid …
setchy Dec 30, 2025
858b208
refactor: codegen args withing INDEX suffix. simplify handler types
setchy Dec 30, 2025
1ad9f63
refactor: graphql utils for fragment parsing
setchy Dec 30, 2025
d510677
refactor: restore batch fetching
setchy Dec 30, 2025
ce5f7fe
refactor: rename query as template. add todos for future refactoring…
setchy Dec 30, 2025
0451483
refactor: simplify handler setup
setchy Dec 31, 2025
0fa5c83
refactor: graphql args
setchy Dec 31, 2025
80b17d2
refactor: generate types for builder pattern
setchy Jan 1, 2026
33bfe90
refactor: generate types for builder pattern
setchy Jan 1, 2026
2135e0f
refactor: move builder logic into client method
setchy Jan 1, 2026
b763394
refactor: move builder logic into client method
setchy Jan 1, 2026
e1e764f
refactor: move builder logic into client method
setchy Jan 1, 2026
d7aadd6
refactor: move builder logic into client method
setchy Jan 1, 2026
9ccc964
add test coverage
setchy Jan 1, 2026
2d1a5c9
add test coverage
setchy Jan 1, 2026
a159567
index template vars
setchy Jan 1, 2026
9a60c1c
address sonarcloud feedback
setchy Jan 1, 2026
2d061f3
Update utils.test.ts
setchy Jan 1, 2026
73f753f
refactor: rename to align with industry terminology for graphql query…
setchy Jan 1, 2026
a9b2d3b
refactor: rename to align with industry terminology for graphql query…
setchy Jan 1, 2026
e84a14d
refactor: internalize index. rename builder options
setchy Jan 1, 2026
07c9eb9
refactor: internalize index. rename builder options
setchy Jan 1, 2026
bf13af8
refactor: simplify add node builder method
setchy Jan 1, 2026
0554507
test: add unit test
setchy Jan 1, 2026
e7f356a
test: add unit test
setchy Jan 1, 2026
35baa2e
remove unused builder fn
setchy Jan 1, 2026
8c41624
handle no supported notifications
setchy Jan 2, 2026
fbbdf1b
Merge branch 'main' into feat/query-merging
setchy Jan 2, 2026
20acf3d
lint
setchy Jan 2, 2026
6ae4640
test: add coverage for client
setchy Jan 2, 2026
3f9462a
test: add coverage for client
setchy Jan 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,4 @@
"*": "biome check --fix --no-errors-on-unmatched",
"*.{js,ts,tsx}": "pnpm test --findRelatedTests --passWithNoTests --updateSnapshot"
}
}
}
13 changes: 12 additions & 1 deletion src/renderer/__mocks__/user-mocks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { GitifyNotificationUser, GitifyUser, Link } from '../types';
import type { AuthorFieldsFragment } from '../utils/api/graphql/generated/graphql';
import type { RawUser } from '../utils/api/types';

export const mockGitifyUser: GitifyUser = {
Expand All @@ -19,7 +20,7 @@ export function createPartialMockUser(login: string): RawUser {
return mockUser as RawUser;
}

export function createMockNotificationUser(
export function createMockGitifyNotificationUser(
login: string,
): GitifyNotificationUser {
return {
Expand All @@ -29,3 +30,13 @@ export function createMockNotificationUser(
type: 'User',
};
}

/**
* Creates a mock author for use in GraphQL response mocks.
*/
export function createMockGraphQLAuthor(login: string): AuthorFieldsFragment {
return {
...createMockGitifyNotificationUser(login),
__typename: 'User',
} as AuthorFieldsFragment;
}
10 changes: 10 additions & 0 deletions src/renderer/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ export const Constants = {

REFRESH_ACCOUNTS_INTERVAL_MS: 60 * 60 * 1000, // 1 hour

// GraphQL Argument Defaults
GRAPHQL_ARGS: {
FIRST_LABELS: 100,
FIRST_CLOSING_ISSUES: 100,
LAST_COMMENTS: 1,
LAST_THREADED_COMMENTS: 10,
LAST_REPLIES: 10,
LAST_REVIEWS: 100,
},

// GitHub Docs
GITHUB_DOCS: {
OAUTH_URL:
Expand Down
204 changes: 1 addition & 203 deletions src/renderer/hooks/useNotifications.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@ import { act, renderHook, waitFor } from '@testing-library/react';
import axios, { AxiosError } from 'axios';
import nock from 'nock';

import { mockGitHubCloudAccount } from '../__mocks__/account-mocks';
import { mockAuth, mockSettings, mockState } from '../__mocks__/state-mocks';
import {
mockNotificationUser,
mockSingleNotification,
} from '../utils/api/__mocks__/response-mocks';
import { mockSingleNotification } from '../utils/api/__mocks__/response-mocks';
import { Errors } from '../utils/errors';
import * as logger from '../utils/logger';
import { useNotifications } from './useNotifications';
Expand Down Expand Up @@ -118,204 +114,6 @@ describe('renderer/hooks/useNotifications.ts', () => {
expect(result.current.notifications[1].notifications.length).toBe(2);
});

it('should fetch detailed notifications with success', async () => {
const mockRepository = {
name: 'notifications-test',
full_name: 'gitify-app/notifications-test',
html_url: 'https://github.com/gitify-app/notifications-test',
owner: {
login: 'gitify-app',
avatar_url: 'https://avatar.url',
type: 'Organization',
},
};

const mockNotifications = [
{
id: '1',
unread: true,
updated_at: '2024-01-01T00:00:00Z',
reason: 'subscribed',
subject: {
title: 'This is a check suite workflow.',
type: 'CheckSuite',
url: null,
latest_comment_url: null,
},
repository: mockRepository,
},
{
id: '2',
unread: true,
updated_at: '2024-02-26T00:00:00Z',
reason: 'subscribed',
subject: {
title: 'This is a Discussion.',
type: 'Discussion',
url: null,
latest_comment_url: null,
},
repository: mockRepository,
},
{
id: '3',
unread: true,
updated_at: '2024-01-01T00:00:00Z',
reason: 'subscribed',
subject: {
title: 'This is an Issue.',
type: 'Issue',
url: 'https://api.github.com/repos/gitify-app/notifications-test/issues/3',
latest_comment_url:
'https://api.github.com/repos/gitify-app/notifications-test/issues/3/comments',
},
repository: mockRepository,
},
{
id: '4',
unread: true,
updated_at: '2024-01-01T00:00:00Z',
reason: 'subscribed',
subject: {
title: 'This is a Pull Request.',
type: 'PullRequest',
url: 'https://api.github.com/repos/gitify-app/notifications-test/pulls/4',
latest_comment_url:
'https://api.github.com/repos/gitify-app/notifications-test/issues/4/comments',
},
repository: mockRepository,
},
{
id: '5',
unread: true,
updated_at: '2024-01-01T00:00:00Z',
reason: 'subscribed',
subject: {
title: 'This is an invitation.',
type: 'RepositoryInvitation',
url: null,
latest_comment_url: null,
},
repository: mockRepository,
},
{
id: '6',
unread: true,
updated_at: '2024-01-01T00:00:00Z',
reason: 'subscribed',
subject: {
title: 'This is a workflow run.',
type: 'WorkflowRun',
url: null,
latest_comment_url: null,
},
repository: mockRepository,
},
];

nock('https://api.github.com')
.get('/notifications?participating=false')
.reply(200, mockNotifications);

nock('https://api.github.com')
.post('/graphql')
.reply(200, {
data: {
search: {
nodes: [
{
title: 'This is a Discussion.',
stateReason: null,
isAnswered: true,
url: 'https://github.com/gitify-app/notifications-test/discussions/612',
author: {
login: 'discussion-creator',
url: 'https://github.com/discussion-creator',
avatar_url:
'https://avatars.githubusercontent.com/u/133795385?s=200&v=4',
type: 'User',
},
comments: {
nodes: [
{
databaseId: 2297637,
createdAt: '2022-03-04T20:39:44Z',
author: {
login: 'comment-user',
url: 'https://github.com/comment-user',
avatar_url:
'https://avatars.githubusercontent.com/u/1?v=4',
type: 'User',
},
replies: {
nodes: [],
},
},
],
},
labels: null,
},
],
},
},
});

nock('https://api.github.com')
.get('/repos/gitify-app/notifications-test/issues/3')
.reply(200, {
state: 'closed',
merged: true,
user: mockNotificationUser,
labels: [],
});
nock('https://api.github.com')
.get('/repos/gitify-app/notifications-test/issues/3/comments')
.reply(200, {
user: mockNotificationUser,
});
nock('https://api.github.com')
.get('/repos/gitify-app/notifications-test/pulls/4')
.reply(200, {
state: 'closed',
merged: false,
user: mockNotificationUser,
labels: [],
});
nock('https://api.github.com')
.get('/repos/gitify-app/notifications-test/pulls/4/reviews')
.reply(200, {});
nock('https://api.github.com')
.get('/repos/gitify-app/notifications-test/issues/4/comments')
.reply(200, {
user: mockNotificationUser,
});

const { result } = renderHook(() => useNotifications());

act(() => {
result.current.fetchNotifications({
auth: {
accounts: [mockGitHubCloudAccount],
},
settings: {
...mockSettings,
detailedNotifications: true,
},
});
});

expect(result.current.status).toBe('loading');

await waitFor(() => {
expect(result.current.status).toBe('success');
});

expect(result.current.notifications[0].account.hostname).toBe(
'github.com',
);
expect(result.current.notifications[0].notifications.length).toBe(6);
});

it('should fetch notifications with same failures', async () => {
const code = AxiosError.ERR_BAD_REQUEST;
const status = 401;
Expand Down
Loading