From 9c874ec9bc36b50ff6f69c6bfc5c7cb3a1a3ea88 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Sun, 10 Nov 2024 09:41:58 -0500 Subject: [PATCH] refactor: octokit openapi types Signed-off-by: Adam Setch --- package.json | 1 + pnpm-lock.yaml | 8 + .../components/RepositoryNotifications.tsx | 8 +- .../notification/NotificationHeader.tsx | 6 +- src/renderer/typesGitHub.ts | 498 ++++-------------- .../utils/api/__mocks__/response-mocks.ts | 2 +- src/renderer/utils/auth/utils.ts | 4 +- src/renderer/utils/links.test.ts | 16 +- src/renderer/utils/links.ts | 6 +- src/renderer/utils/subject.ts | 13 +- 10 files changed, 122 insertions(+), 440 deletions(-) diff --git a/package.json b/package.json index e0f23311b..fcd46f155 100644 --- a/package.json +++ b/package.json @@ -139,6 +139,7 @@ "@biomejs/biome": "1.9.4", "@discordapp/twemoji": "15.1.0", "@electron/notarize": "2.5.0", + "@octokit/openapi-types": "22.2.0", "@primer/octicons-react": "19.12.0", "@testing-library/react": "16.0.1", "@types/jest": "29.5.14", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15223f80d..e77f494f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,6 +42,9 @@ importers: '@electron/notarize': specifier: 2.5.0 version: 2.5.0 + '@octokit/openapi-types': + specifier: 22.2.0 + version: 22.2.0 '@primer/octicons-react': specifier: 19.12.0 version: 19.12.0(react@18.3.1) @@ -601,6 +604,9 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} deprecated: This functionality has been moved to @npmcli/fs + '@octokit/openapi-types@22.2.0': + resolution: {integrity: sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -4648,6 +4654,8 @@ snapshots: mkdirp: 1.0.4 rimraf: 3.0.2 + '@octokit/openapi-types@22.2.0': {} + '@pkgjs/parseargs@0.11.0': optional: true diff --git a/src/renderer/components/RepositoryNotifications.tsx b/src/renderer/components/RepositoryNotifications.tsx index f48d64377..d536cfd86 100644 --- a/src/renderer/components/RepositoryNotifications.tsx +++ b/src/renderer/components/RepositoryNotifications.tsx @@ -1,14 +1,14 @@ import { CheckIcon, MarkGithubIcon, ReadIcon } from '@primer/octicons-react'; import { type FC, type MouseEvent, useContext, useState } from 'react'; import { AppContext } from '../context/App'; -import { Opacity, Size } from '../types'; +import { type Link, Opacity, Size } from '../types'; import type { Notification } from '../typesGitHub'; import { cn } from '../utils/cn'; +import { openExternalLink } from '../utils/comms'; import { getChevronDetails, isMarkAsDoneFeatureSupported, } from '../utils/helpers'; -import { openRepository } from '../utils/links'; import { HoverGroup } from './HoverGroup'; import { NotificationRow } from './NotificationRow'; import { InteractionButton } from './buttons/InteractionButton'; @@ -67,7 +67,9 @@ export const RepositoryNotifications: FC = ({ onClick={(event: MouseEvent) => { // Don't trigger onClick of parent element. event.stopPropagation(); - openRepository(repoNotifications[0].repository); + openExternalLink( + repoNotifications[0].repository.html_url as Link, + ); }} > {repoName} diff --git a/src/renderer/components/notification/NotificationHeader.tsx b/src/renderer/components/notification/NotificationHeader.tsx index a4c9dd1ff..88fc1a6dc 100644 --- a/src/renderer/components/notification/NotificationHeader.tsx +++ b/src/renderer/components/notification/NotificationHeader.tsx @@ -1,10 +1,10 @@ import { MarkGithubIcon } from '@primer/octicons-react'; import { type FC, type MouseEvent, useContext } from 'react'; import { AppContext } from '../../context/App'; -import { Opacity, Size } from '../../types'; +import { type Link, Opacity, Size } from '../../types'; import type { Notification } from '../../typesGitHub'; import { cn } from '../../utils/cn'; -import { openRepository } from '../../utils/links'; +import { openExternalLink } from '../../utils/comms'; import { AvatarIcon } from '../icons/AvatarIcon'; interface INotificationHeader { @@ -43,7 +43,7 @@ export const NotificationHeader: FC = ({ onClick={(event: MouseEvent) => { // Don't trigger onClick of parent element. event.stopPropagation(); - openRepository(notification.repository); + openExternalLink(notification.repository.html_url as Link); }} > {repoSlug} diff --git a/src/renderer/typesGitHub.ts b/src/renderer/typesGitHub.ts index 5d6486051..017ec9aa2 100644 --- a/src/renderer/typesGitHub.ts +++ b/src/renderer/typesGitHub.ts @@ -1,3 +1,4 @@ +import type { components } from '@octokit/openapi-types'; import type { Account, Link } from './types'; export type Reason = @@ -17,15 +18,6 @@ export type Reason = | 'subscribed' | 'team_mention'; -// Note: ANSWERED and OPEN are not an official discussion state type in the GitHub API -export type DiscussionStateType = - | 'ANSWERED' - | 'DUPLICATE' - | 'OPEN' - | 'OUTDATED' - | 'REOPENED' - | 'RESOLVED'; - export type SubjectType = | 'CheckSuite' | 'Commit' @@ -62,21 +54,6 @@ export type StateType = | IssueStateReasonType | PullRequestStateType; -export type CheckSuiteStatus = - | 'action_required' - | 'cancelled' - | 'completed' - | 'failure' - | 'in_progress' - | 'pending' - | 'queued' - | 'requested' - | 'skipped' - | 'stale' - | 'success' - | 'timed_out' - | 'waiting'; - export type PullRequestReviewState = | 'APPROVED' | 'CHANGES_REQUESTED' @@ -84,175 +61,15 @@ export type PullRequestReviewState = | 'DISMISSED' | 'PENDING'; -export type PullRequestReviewAuthorAssociation = - | 'COLLABORATOR' - | 'CONTRIBUTOR' - | 'FIRST_TIMER' - | 'FIRST_TIME_CONTRIBUTOR' - | 'MANNEQUIN' - | 'MEMBER' - | 'NONE' - | 'OWNER'; - -// TODO: Add explicit types for GitHub API response vs Gitify Notifications object -export type Notification = GitHubNotification & GitifyNotification; - -export interface GitHubNotification { - id: string; - unread: boolean; - reason: Reason; - updated_at: string; - last_read_at: string | null; - subject: Subject; - repository: Repository; - url: Link; - subscription_url: Link; -} - -// Note: This is not in the official GitHub API. We add this to make notification interactions easier. -export interface GitifyNotification { - account: Account; -} +export type GitHubNotification = components['schemas']['thread']; export type UserDetails = User & UserProfile; -export interface UserProfile { - name: string; - company: string; - blog: string; - location: string; - email: string; - hireable: string; - bio: string; - twitter_username: string; - public_repos: number; - public_gists: number; - followers: number; - following: number; - created_at: string; - updated_at: string; - private_gists: number; - total_private_repos: number; - owned_private_repos: number; - disk_usage: number; - collaborators: number; - two_factor_authentication: boolean; - plan: Plan; -} - -export interface Plan { - name: string; - space: number; - private_repos: number; - collaborators: number; -} - -export interface User { - login: string; - id: number; - node_id: string; - avatar_url: Link; - gravatar_url: Link; - url: Link; - html_url: Link; - followers_url: Link; - following_url: Link; - gists_url: Link; - starred_url: Link; - subscriptions_url: Link; - organizations_url: Link; - repos_url: Link; - events_url: Link; - received_events_url: Link; - type: UserType; - site_admin: boolean; -} - -export interface SubjectUser { - login: string; - html_url: Link; - avatar_url: Link; - type: UserType; -} - -export interface DiscussionAuthor { - login: string; - url: Link; - avatar_url: Link; - type: UserType; -} - -export interface Repository { - id: number; - node_id: string; - name: string; - full_name: string; - private: boolean; - owner: Owner; - html_url: Link; - description: string; - fork: boolean; - url: Link; - forks_url: Link; - keys_url: Link; - collaborators_url: Link; - teams_url: Link; - hooks_url: Link; - issue_events_url: Link; - events_url: Link; - assignees_url: Link; - branches_url: Link; - tags_url: Link; - blobs_url: Link; - git_tags_url: Link; - git_refs_url: Link; - trees_url: Link; - statuses_url: Link; - languages_url: Link; - stargazers_url: Link; - contributors_url: Link; - subscribers_url: Link; - subscription_url: Link; - commits_url: Link; - git_commits_url: Link; - comments_url: Link; - issue_comment_url: Link; - contents_url: Link; - compare_url: Link; - merges_url: Link; - archive_url: Link; - downloads_url: Link; - issues_url: Link; - pulls_url: Link; - milestones_url: Link; - notifications_url: Link; - labels_url: Link; - releases_url: Link; - deployments_url: Link; -} +export type User = components['schemas']['simple-user']; -export interface Owner { - login: string; - id: number; - node_id: string; - avatar_url: Link; - gravatar_id: string; - url: Link; - html_url: Link; - followers_url: Link; - following_url: Link; - gists_url: Link; - starred_url: Link; - subscriptions_url: Link; - organizations_url: Link; - repos_url: Link; - events_url: Link; - received_events_url: Link; - type: string; - site_admin: boolean; -} +export type UserProfile = components['schemas']['public-user']; -export type Subject = GitHubSubject & GitifySubject; +export type Repository = components['schemas']['repository']; interface GitHubSubject { title: string; @@ -261,181 +78,15 @@ interface GitHubSubject { type: SubjectType; } -// This is not in the GitHub API, but we add it to the type to make it easier to work with -export interface GitifySubject { - number?: number; - state?: StateType; - user?: SubjectUser; - reviews?: GitifyPullRequestReview[]; - linkedIssues?: string[]; - comments?: number; - labels?: string[]; - milestone?: Milestone; -} - -export interface PullRequest { - url: Link; - id: number; - node_id: string; - html_url: Link; - diff_url: Link; - patch_url: Link; - issue_url: Link; - number: number; - state: PullRequestStateType; - locked: boolean; - title: string; - user: User; - body: string; - created_at: string; - updated_at: string; - closed_at: string | null; - merged_at: string | null; - merge_commit_sha: string | null; - labels: Labels[]; - milestone: Milestone | null; - draft: boolean; - commits_url: Link; - review_comments_url: Link; - review_comment_url: Link; - comments_url: Link; - statuses_url: Link; - author_association: string; - merged: boolean; - mergeable: boolean; - rebaseable: boolean; - comments: number; - review_comments: number; - maintainer_can_modify: boolean; - commits: number; - additions: number; - deletions: number; - changed_files: number; -} - -export interface GitifyPullRequestReview { - state: PullRequestReviewState; - users: string[]; -} - -export interface Labels { - id: number; - node_id: string; - url: Link; - name: string; - color: string; - default: boolean; - description: string; -} - -export interface PullRequestReview { - id: number; - node_id: string; - user: User; - body: string; - state: PullRequestReviewState; - html_url: Link; - pull_request_url: Link; - author_association: PullRequestReviewAuthorAssociation; - submitted_at: string; - commit_id: string; -} - -export interface Commit { - sha: string; - node_id: string; - commit: { - author: CommitUser; - committer: CommitUser; - message: string; - tree: { - sha: string; - url: Link; - }; - url: Link; - comment_count: number; - verification: { - verified: boolean; - reason: string; - signature: string | null; - payload: string | null; - }; - }; - url: Link; - html_url: Link; - comments_url: Link; - author: User; - committer: User; - parents: CommitParent[]; - stats: { - total: number; - additions: number; - deletions: number; - }; - files: CommitFiles[]; -} - -interface CommitUser { - name: string; - email: string; - date: string; -} +export type PullRequest = components['schemas']['pull-request']; -interface CommitParent { - sha: string; - url: Link; - html_url: Link; -} +export type PullRequestReview = components['schemas']['pull-request-review']; -interface CommitFiles { - sha: string; - filename: string; - status: string; - additions: number; - deletions: number; - changes: number; - blob_url: Link; - raw_url: Link; - contents_url: Link; - patch: string; -} +export type Commit = components['schemas']['commit']; -export interface CommitComment { - url: Link; - html_url: Link; - issue_url: Link; - id: number; - node_id: string; - user: User; - created_at: string; - updated_at: string; - body: string; -} +export type CommitComment = components['schemas']['commit-comment']; -export interface Issue { - url: Link; - repository_url: Link; - labels_url: Link; - comments_url: Link; - events_url: Link; - html_url: Link; - id: number; - node_id: string; - number: number; - title: string; - user: User; - state: IssueStateType; - locked: boolean; - labels: Labels[]; - milestone: Milestone | null; - comments: number; - created_at: string; - updated_at: string; - closed_at: string | null; - author_association: string; - body: string; - state_reason: IssueStateReasonType | null; -} +export type Issue = components['schemas']['issue']; export interface IssueOrPullRequestComment { url: Link; @@ -449,45 +100,21 @@ export interface IssueOrPullRequestComment { body: string; } -export interface Milestone { - url: Link; - html_url: Link; - labels_url: Link; - id: number; - node_id: string; - number: number; - title: string; - description: string; - creator: User; - open_issues: number; - closed_issues: number; - state: MilestoneStateType; - created_at: string; - updated_at: string; - due_on: string | null; - closed_at: string | null; -} +export type Milestone = components['schemas']['milestone']; -type MilestoneStateType = 'open' | 'closed'; +export type Release = components['schemas']['release']; -export interface Release { - url: Link; - assets_url: Link; - upload_url: Link; - html_url: Link; - id: number; - author: User; - node_id: string; - tag_name: string; - target_commitish: string; - name: string | null; - body: string | null; - draft: boolean; - prerelease: boolean; - created_at: string; - published_at: string | null; +export interface GitHubRESTError { + message: string; + documentation_url: Link; } +export type NotificationThreadSubscription = + components['schemas']['thread-subscription']; + +/** + * GitHub GraphQL API Types + */ export interface GraphQLSearch { data: { search: { @@ -507,6 +134,22 @@ export interface Discussion { labels: DiscussionLabels | null; } +// Note: ANSWERED and OPEN are not an official discussion state type in the GitHub API +export type DiscussionStateType = + | 'ANSWERED' + | 'DUPLICATE' + | 'OPEN' + | 'OUTDATED' + | 'REOPENED' + | 'RESOLVED'; + +export interface DiscussionAuthor { + login: string; + url: Link; + avatar_url: Link; + type: UserType; +} + export interface DiscussionLabels { nodes: DiscussionLabel[]; } @@ -529,6 +172,46 @@ export interface DiscussionComment { }; } +/** + * Gitify Type Extensions + */ + +// TODO: Add explicit types for GitHub API response vs Gitify Notifications object +export type Notification = GitHubNotification & GitifyNotification; + +// Note: This is not in the official GitHub API. We add this to make notification interactions easier. +export interface GitifyNotification { + account: Account; + reason: Reason; + subject: Subject; +} + +// This is not in the GitHub API, but we add it to the type to make it easier to work with +export type Subject = GitHubSubject & GitifySubject; + +export interface GitifySubject { + number?: number; + state?: StateType; + user?: SubjectUser; + reviews?: GitifyPullRequestReview[]; + linkedIssues?: string[]; + comments?: number; + labels?: string[]; + milestone?: Milestone; +} + +export interface SubjectUser { + login: string; + html_url: Link; + avatar_url: Link; + type: UserType; +} + +export interface GitifyPullRequestReview { + state: PullRequestReviewState; + users: string[]; +} + export interface CheckSuiteAttributes { workflowName: string; attemptNumber?: number; @@ -543,16 +226,17 @@ export interface WorkflowRunAttributes { status: CheckSuiteStatus | null; } -export interface GitHubRESTError { - message: string; - documentation_url: Link; -} - -export interface NotificationThreadSubscription { - subscribed: boolean; - ignored: boolean; - reason: string | null; - created_at: string; - url: Link; - thread_url: Link; -} +export type CheckSuiteStatus = + | 'action_required' + | 'cancelled' + | 'completed' + | 'failure' + | 'in_progress' + | 'pending' + | 'queued' + | 'requested' + | 'skipped' + | 'stale' + | 'success' + | 'timed_out' + | 'waiting'; diff --git a/src/renderer/utils/api/__mocks__/response-mocks.ts b/src/renderer/utils/api/__mocks__/response-mocks.ts index 714635ab5..002f2b05d 100644 --- a/src/renderer/utils/api/__mocks__/response-mocks.ts +++ b/src/renderer/utils/api/__mocks__/response-mocks.ts @@ -19,7 +19,7 @@ export const mockNotificationUser: User = { id: 123456789, node_id: 'MDQ6VXNlcjE=', avatar_url: 'https://avatars.githubusercontent.com/u/583231?v=4' as Link, - gravatar_url: '' as Link, + gravatar_id: '', url: 'https://api.github.com/users/octocat' as Link, html_url: 'https://github.com/octocat' as Link, followers_url: 'https://api.github.com/users/octocat/followers' as Link, diff --git a/src/renderer/utils/auth/utils.ts b/src/renderer/utils/auth/utils.ts index d79fcbce0..d762d9dbe 100644 --- a/src/renderer/utils/auth/utils.ts +++ b/src/renderer/utils/auth/utils.ts @@ -100,7 +100,7 @@ export async function getUserData( id: response.id, login: response.login, name: response.name, - avatar: response.avatar_url, + avatar: response.avatar_url as Link, }; } @@ -162,7 +162,7 @@ export async function refreshAccount(account: Account): Promise { id: res.data.id, login: res.data.login, name: res.data.name, - avatar: res.data.avatar_url, + avatar: res.data.avatar_url as Link, }; // Refresh platform version diff --git a/src/renderer/utils/links.test.ts b/src/renderer/utils/links.test.ts index 7ec918363..6f86b9bc3 100644 --- a/src/renderer/utils/links.test.ts +++ b/src/renderer/utils/links.test.ts @@ -1,7 +1,6 @@ import { partialMockUser } from '../__mocks__/partial-mocks'; import { mockGitHubCloudAccount } from '../__mocks__/state-mocks'; import type { Hostname, Link } from '../types'; -import type { Repository } from '../typesGitHub'; import { mockSingleNotification } from './api/__mocks__/response-mocks'; import * as authUtils from './auth/utils'; import * as comms from './comms'; @@ -17,7 +16,6 @@ import { openGitifyReleaseNotes, openHost, openNotification, - openRepository, openUserProfile, } from './links'; @@ -88,19 +86,9 @@ describe('renderer/utils/links.ts', () => { expect(openExternalLinkMock).toHaveBeenCalledWith(mockSettingsURL); }); - it('openRepository', () => { - const mockHtmlUrl = 'https://github.com/gitify-app/gitify'; - - const repo = { - html_url: mockHtmlUrl, - } as Repository; - - openRepository(repo); - expect(openExternalLinkMock).toHaveBeenCalledWith(mockHtmlUrl); - }); - it('openNotification', async () => { - const mockNotificationUrl = mockSingleNotification.repository.html_url; + const mockNotificationUrl = mockSingleNotification.repository + .html_url as Link; jest .spyOn(helpers, 'generateGitHubWebUrl') .mockResolvedValue(mockNotificationUrl); diff --git a/src/renderer/utils/links.ts b/src/renderer/utils/links.ts index 3ea299d4b..a31975678 100644 --- a/src/renderer/utils/links.ts +++ b/src/renderer/utils/links.ts @@ -1,5 +1,5 @@ import type { Account, Hostname, Link } from '../types'; -import type { Notification, Repository, SubjectUser } from '../typesGitHub'; +import type { Notification, SubjectUser } from '../typesGitHub'; import { getDeveloperSettingsURL } from './auth/utils'; import { openExternalLink } from './comms'; import { Constants } from './constants'; @@ -48,10 +48,6 @@ export function openDeveloperSettings(account: Account) { openExternalLink(url); } -export function openRepository(repository: Repository) { - openExternalLink(repository.html_url); -} - export async function openNotification(notification: Notification) { const url = await generateGitHubWebUrl(notification); openExternalLink(url); diff --git a/src/renderer/utils/subject.ts b/src/renderer/utils/subject.ts index 08d65abcf..e7646cbc2 100644 --- a/src/renderer/utils/subject.ts +++ b/src/renderer/utils/subject.ts @@ -10,9 +10,12 @@ import type { Notification, PullRequest, PullRequestReview, + PullRequestReviewState, PullRequestStateType, + StateType, SubjectUser, User, + UserType, WorkflowRunAttributes, } from '../typesGitHub'; import { @@ -224,7 +227,7 @@ async function getGitifySubjectForIssue( return { number: issue.number, - state: issue.state_reason ?? issue.state, + state: issue.state_reason ?? (issue.state as StateType), user: getSubjectUser([issueCommentUser, issue.user]), comments: issue.comments, labels: issue.labels?.map((label) => label.name) ?? [], @@ -313,7 +316,7 @@ export async function getLatestReviewForReviewers( if (!reviewerFound) { reviewers.push({ - state: prReview.state, + state: prReview.state as PullRequestReviewState, users: [prReview.user.login], }); } else { @@ -421,9 +424,9 @@ export function getSubjectUser(users: User[]): SubjectUser { if (user) { subjectUser = { login: user.login, - html_url: user.html_url, - avatar_url: user.avatar_url, - type: user.type, + html_url: user.html_url as Link, + avatar_url: user.avatar_url as Link, + type: user.type as UserType, }; return subjectUser;