From 0e4ee1a3445e661387265b8cc250e8e19f385718 Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 21 May 2024 21:11:54 +0200 Subject: [PATCH] Add the ability to star pull requests Closes #4 --- src/components/DashboardSection.tsx | 12 +++---- src/components/PullTable.tsx | 26 ++++++++++++--- src/components/Sidebar.tsx | 1 + src/config.ts | 4 ++- src/main.tsx | 38 ++++++++++++---------- src/queries.ts | 27 ++++++++++++++++ src/routes/dashboard.tsx | 40 +++++++---------------- src/routes/stars.tsx | 49 +++++++++++++++++++++++++++++ src/styles/index.less | 12 ++++--- src/styles/utils.less | 4 +++ 10 files changed, 152 insertions(+), 61 deletions(-) create mode 100644 src/queries.ts create mode 100644 src/routes/stars.tsx diff --git a/src/components/DashboardSection.tsx b/src/components/DashboardSection.tsx index 701850f..164d22d 100644 --- a/src/components/DashboardSection.tsx +++ b/src/components/DashboardSection.tsx @@ -35,7 +35,7 @@ export default function DashboardSection({isLoading, section, isFirst, isLast, d
{section.label} - {!isLoading && (count > 0) && ( + {(count > 0) && ( {count} )}
@@ -56,11 +56,11 @@ export default function DashboardSection({isLoading, section, isFirst, isLast, d onSubmit={onChange} onDelete={onDelete}/> - {isLoading && } - - {!isLoading && - {count > 0 ? :

No results

} -
} + {isLoading + ? + : + {count > 0 ? :

No results

} +
} ) } \ No newline at end of file diff --git a/src/components/PullTable.tsx b/src/components/PullTable.tsx index 57015df..ec75b9e 100644 --- a/src/components/PullTable.tsx +++ b/src/components/PullTable.tsx @@ -1,7 +1,10 @@ +import { useContext } from "react" import { HTMLTable, Tooltip, Tag, Icon } from "@blueprintjs/core" -import ReactTimeAgo from 'react-time-ago' +import ReactTimeAgo from "react-time-ago" + import { PullList, computeSize } from "../model" import IconWithTooltip from "./IconWithTooltip" +import { ConfigContext } from "../config" export type Props = { @@ -19,10 +22,20 @@ const formatDate = (d: string) => { } export default function PullTable({data}: Props) { + const { config, setConfig } = useContext(ConfigContext) + + const stars = new Set(config.stars) + + const handleStar = (number: number) => { + setConfig(prev => prev.stars.indexOf(number) > -1 + ? {...prev, stars: prev.stars.filter(s => s != number)} + : {...prev, stars: prev.stars.concat([number])}) + } return ( +   Author Status Last Action @@ -31,9 +44,14 @@ export default function PullTable({data}: Props) { - {data.flatMap((pulls, idx) => ( - pulls.pulls.map((pull, idx2) => ( + {data.map((pullList, idx) => ( + pullList.pulls.map((pull, idx2) => ( + handleStar(pull.number)}> + {stars.has(pull.number) + ? + : } +
@@ -75,7 +93,7 @@ export default function PullTable({data}: Props) {
{pull.title}
- {pulls.host}:{pull.repository.nameWithOwner} #{pull.number} + {pullList.host}:{pull.repository.nameWithOwner} #{pull.number}
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index b00740e..bac36fb 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -46,6 +46,7 @@ export default function Sidebar({ isDark, onDarkChange }: Props) {
+
{ return localforage.getItem(configKey) - .then(config => (config === null) ? defaultConfig : config) + .then(config => (config === null) ? defaultConfig : {...defaultConfig, ...config}) } export function writeConfig(config: Config): Promise { diff --git a/src/main.tsx b/src/main.tsx index bfcc8b6..a4659d4 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,14 +1,11 @@ import React from 'react' import ReactDOM from 'react-dom/client' -import { - createBrowserRouter, - RouterProvider, -} from 'react-router-dom' +import {createBrowserRouter, RouterProvider} from 'react-router-dom' import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import TimeAgo from 'javascript-time-ago' import timeAgoEnLocale from 'javascript-time-ago/locale/en.json' -import { Intent } from '@blueprintjs/core' +import { Intent, BlueprintProvider } from '@blueprintjs/core' import { AppToaster} from './toaster' import App from './App.tsx' @@ -20,6 +17,7 @@ import 'normalize.css/normalize.css' import '@blueprintjs/icons/lib/css/blueprint-icons.css' import '@blueprintjs/core/lib/css/blueprint.css' import './styles/index.less' +import Stars from './routes/stars.tsx' TimeAgo.addDefaultLocale(timeAgoEnLocale) @@ -29,14 +27,18 @@ const router = createBrowserRouter([ element: , errorElement: , children: [ - { - index: true, - element: , - }, - { - path: "/settings", - element: , - }, + { + index: true, + element: , + }, + { + path: "/stars", + element: , + }, + { + path: "/settings", + element: , + }, ] }, ]) @@ -52,9 +54,11 @@ const queryClient = new QueryClient({ ReactDOM.createRoot(document.getElementById('root')!).render( - - - - + + + + + + , ) diff --git a/src/queries.ts b/src/queries.ts new file mode 100644 index 0000000..c0ff650 --- /dev/null +++ b/src/queries.ts @@ -0,0 +1,27 @@ +import { UseQueryResult, useQueries } from "@tanstack/react-query"; +import { Config } from "./config"; +import { getPulls, getViewer } from "./github"; +import { Pull } from "./model"; + +export const usePullRequests = (config: Config): UseQueryResult[] => { + const viewers = useQueries({ + queries: config.connections.map(connection => ({ + queryKey: ['viewer', connection.host], + queryFn: () => getViewer(connection), + staleTime: Infinity, + })), + }) + return useQueries({ + queries: config.sections.flatMap(section => { + return config.connections.map((connection, idx) => ({ + queryKey: ['pulls', connection.host, connection.auth, section.search], + queryFn: () => getPulls(connection, section.search, viewers[idx].data?.login || ""), + refetchInterval: 300_000, + refetchIntervalInBackground: true, + // refetchOnWindowFocus: false, + staleTime: 60_000, + enabled: viewers[idx].data !== undefined, + })) + }), + }) +} \ No newline at end of file diff --git a/src/routes/dashboard.tsx b/src/routes/dashboard.tsx index 192a99e..2e68d12 100644 --- a/src/routes/dashboard.tsx +++ b/src/routes/dashboard.tsx @@ -1,13 +1,13 @@ -import { useCallback, useContext, useEffect, useState } from "react"; -import { Section, emptySectionConfig, ConfigContext } from "../config"; -import DashboardSection from "../components/DashboardSection"; -import { Button } from "@blueprintjs/core"; -import SectionDialog from "../components/SectionDialog"; -import { useQueries } from "@tanstack/react-query"; -import { useSearchParams } from "react-router-dom"; -import { getPulls, getViewer } from "../github"; -import { Pull } from "../model"; -import SearchInput from "../components/SearchInput"; +import { useCallback, useContext, useEffect, useState } from "react" +import { useSearchParams } from "react-router-dom" +import { Button } from "@blueprintjs/core" + +import { Section, emptySectionConfig, ConfigContext } from "../config" +import SectionDialog from "../components/SectionDialog" +import DashboardSection from "../components/DashboardSection" +import { Pull } from "../model" +import SearchInput from "../components/SearchInput" +import { usePullRequests } from "../queries" function matches(pull: Pull, tokens: string[]): boolean { return tokens.length === 0 || tokens.every(tok => pull.title.toLowerCase().indexOf(tok) > -1 || pull.repository.nameWithOwner.indexOf(tok) > -1) @@ -24,25 +24,7 @@ export default function Dashboard() { const [ searchParams, setSearchParams ] = useSearchParams() const [ newSection, setNewSection ] = useState(emptySectionConfig) - const viewers = useQueries({ - queries: config.connections.map(connection => ({ - queryKey: ['viewer', connection.host], - queryFn: () => getViewer(connection), - staleTime: Infinity, - })), - }) - const results = useQueries({ - queries: config.sections.flatMap(section => { - return config.connections.map((connection, idx) => ({ - queryKey: ['pulls', connection.host, connection.auth, section.search], - queryFn: () => getPulls(connection, section.search, viewers[idx].data?.login || ""), - refetchInterval: 300_000, - refetchIntervalInBackground: true, - refetchOnWindowFocus: false, - enabled: viewers[idx].data !== undefined, - })) - }), - }) + const results = usePullRequests(config) const refetchAll = useCallback(async () => { await Promise.all(results.map(res => res.refetch())); diff --git a/src/routes/stars.tsx b/src/routes/stars.tsx new file mode 100644 index 0000000..8dce011 --- /dev/null +++ b/src/routes/stars.tsx @@ -0,0 +1,49 @@ +import { useCallback, useContext } from "react"; +import { Button, Card, H3, Spinner } from "@blueprintjs/core"; + +import { ConfigContext } from "../config"; +import PullTable from "../components/PullTable"; +import { usePullRequests } from "../queries"; + + +export default function Stars() { + const { config } = useContext(ConfigContext) + const results = usePullRequests(config) + + const stars = new Set(config.stars) + + const isLoading = results.some(res => res.isLoading) + const isFetching = results.some(res => res.isFetching) + + const data = config.connections.map((connection, idx) => ({ + host: connection.host, + pulls: config.sections.flatMap((_, idx2) => results[idx + config.connections.length * idx2].data || []).filter(v => stars.has(v.number)) + })) + const count = data.map(res => res.pulls.length).reduce((acc, v) => acc + v, 0) + + const refetchAll = useCallback(async () => { + await Promise.all(results.map(res => res.refetch())); + }, [results]); + + return ( + <> +
+

Starred pull requests

+
+ + + {isLoading + ? + : count > 0 + ? + :

No results

} +
+ + ) +} diff --git a/src/styles/index.less b/src/styles/index.less index fa84ec8..652e831 100644 --- a/src/styles/index.less +++ b/src/styles/index.less @@ -104,18 +104,22 @@ footer { overflow: hidden; vertical-align: middle; } - tbody td:nth-child(1) { /* Author */ + tbody td:nth-child(1) { /* Star */ + text-align: center; + width: 25px; + } + tbody td:nth-child(2) { /* Author */ text-align: center; width: 50px; } - tbody td:nth-child(2) { /* Status */ + tbody td:nth-child(3) { /* Status */ text-align: center; width: 50px; } - tbody td:nth-child(3) { /* Last Action */ + tbody td:nth-child(4) { /* Last Action */ width: 200px; } - tbody td:nth-child(4) { /* Size */ + tbody td:nth-child(5) { /* Size */ text-align: center; width: 50px; } diff --git a/src/styles/utils.less b/src/styles/utils.less index 89413f0..f331228 100644 --- a/src/styles/utils.less +++ b/src/styles/utils.less @@ -15,6 +15,10 @@ flex-grow: 1; } +.cursor-pointer { + cursor: pointer; +} + .text-sm { font-size: @pt-font-size-small; }