Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 dev/package-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const assertCacheServiceOpens = async () => {
const cached = await Effect.runPromise(
Effect.gen(function* () {
const cache = yield* CacheService
return yield* cache.readQueue("smoke", { _tag: "Queue", mode: "authored", repository: null })
return yield* cache.readQueue("smoke", { _tag: "Queue", mode: "authored", repository: null, stateFilter: "open" })
}).pipe(Effect.provide(CacheService.layerSqliteFile(join(dir, "cache.sqlite")))),
)
assert(cached === null, "New cache database should start empty")
Expand Down
100 changes: 91 additions & 9 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ import {
type PullRequestMergeAction,
type PullRequestMergeMethod,
type PullRequestReviewComment,
type PullRequestStateFilter,
type RepositoryMergeMethods,
type SubmitPullRequestReviewInput,
} from "./domain.js"
import { allowedMergeMethodList, pullRequestMergeMethods } from "./domain.js"
import { allowedMergeMethodList, pullRequestMergeMethods, pullRequestStateFilters } from "./domain.js"
import { formatShortDate, formatTimestamp } from "./date.js"
import { errorMessage } from "./errors.js"
import { getMergeKindDefinition, mergeInfoFromPullRequest, requiresMarkReady, visibleMergeKinds } from "./mergeActions.js"
Expand All @@ -45,6 +46,8 @@ import {
viewLabel,
viewMode,
viewRepository,
viewStateKey,
viewStateFilter,
} from "./pullRequestViews.js"
import { BrowserOpener } from "./services/BrowserOpener.js"
import { CacheService, type PullRequestCacheKey } from "./services/CacheService.js"
Expand Down Expand Up @@ -130,13 +133,15 @@ import {
initialMergeModalState,
initialModal,
initialOpenRepositoryModalState,
initialPullRequestFilterModalState,
initialPullRequestStateModalState,
initialSubmitReviewModalState,
initialThemeModalState,
LabelModal,
MergeModal,
Modal,
OpenRepositoryModal,
PullRequestFilterModal,
PullRequestStateModal,
submitReviewOptions,
SubmitReviewModal,
Expand All @@ -152,6 +157,7 @@ import {
type ModalState,
type ModalTag,
type OpenRepositoryModalState,
type PullRequestFilterModalState,
type PullRequestStateModalState,
type SubmitReviewModalState,
type ThemeModalState,
Expand Down Expand Up @@ -201,8 +207,11 @@ interface DetailPlaceholderInput {
readonly loadingIndicator: string
readonly visibleCount: number
readonly filterText: string
readonly stateFilter: PullRequestStateFilter
}

const pullRequestStatePhrase = (stateFilter: PullRequestStateFilter) => `${stateFilter} pull requests`

type DiffLineColorConfig = {
readonly gutter: string
readonly content: string
Expand Down Expand Up @@ -265,6 +274,7 @@ const retryProgressAtom = Atom.make<RetryProgress>(initialRetryProgress).pipe(At
const activeViewAtom = Atom.make<PullRequestView>(initialPullRequestView()).pipe(Atom.keepAlive)
const queueLoadCacheAtom = Atom.make<Partial<Record<string, PullRequestLoad>>>({}).pipe(Atom.keepAlive)
const queueSelectionAtom = Atom.make<Partial<Record<string, number>>>({}).pipe(Atom.keepAlive)
const stateFiltersByViewAtom = Atom.make<Partial<Record<string, PullRequestStateFilter>>>({}).pipe(Atom.keepAlive)
const trimQueueLoadCache = (cache: Partial<Record<string, PullRequestLoad>>) => {
const repositoryKeys = Object.keys(cache).filter((key) => key.startsWith("repository:"))
if (repositoryKeys.length <= MAX_REPOSITORY_CACHE_ENTRIES) return cache
Expand All @@ -279,6 +289,7 @@ const pullRequestsAtom = githubRuntime
const view = yield* Atom.get(activeViewAtom)
const queueMode = viewMode(view)
const repository = viewRepository(view)
const stateFilter = viewStateFilter(view)
const cacheKey = viewCacheKey(view)
const cacheUsername = view._tag === "Repository" ? null : yield* github.getAuthenticatedUser().pipe(Effect.catch(() => Effect.succeed(null)))
const cacheViewer = cacheViewerFor(view, cacheUsername)
Expand All @@ -294,6 +305,7 @@ const pullRequestsAtom = githubRuntime
.listOpenPullRequestPage({
mode: queueMode,
repository,
stateFilter,
cursor: null,
pageSize: Math.min(pullRequestPageSize, config.prFetchLimit),
})
Expand Down Expand Up @@ -656,11 +668,12 @@ const setDiffCommentLineColor = (diff: DiffRenderable, entry: AppliedDiffLineCol
}
}

const getDetailPlaceholderContent = ({ status, retryProgress, loadingIndicator, visibleCount, filterText }: DetailPlaceholderInput): DetailPlaceholderContent => {
const getDetailPlaceholderContent = ({ status, retryProgress, loadingIndicator, visibleCount, filterText, stateFilter }: DetailPlaceholderInput): DetailPlaceholderContent => {
const statePhrase = pullRequestStatePhrase(stateFilter)
if (status === "loading") {
return {
title: `${loadingIndicator} Loading pull requests`,
hint: retryProgress._tag === "Retrying" ? `Retry ${retryProgress.attempt}/${retryProgress.max}` : "Fetching latest open PRs",
hint: retryProgress._tag === "Retrying" ? `Retry ${retryProgress.attempt}/${retryProgress.max}` : `Fetching latest ${statePhrase}`,
}
}

Expand All @@ -680,7 +693,7 @@ const getDetailPlaceholderContent = ({ status, retryProgress, loadingIndicator,

if (visibleCount === 0) {
return {
title: "No open pull requests",
title: `No ${statePhrase}`,
hint: "Press r to refresh",
}
}
Expand All @@ -698,6 +711,7 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
const pullRequestResult = useAtomValue(pullRequestsAtom)
const refreshPullRequestsAtom = useAtomRefresh(pullRequestsAtom)
const [activeView, setActiveView] = useAtom(activeViewAtom)
const [stateFiltersByView, setStateFiltersByView] = useAtom(stateFiltersByViewAtom)
const setQueueLoadCache = useAtomSet(queueLoadCacheAtom)
const setQueueSelection = useAtomSet(queueSelectionAtom)
const [selectedIndex, setSelectedIndex] = useAtom(selectedIndexAtom)
Expand Down Expand Up @@ -731,6 +745,7 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
const labelModalActive = Modal.$is("Label")(activeModal)
const closeModalActive = Modal.$is("Close")(activeModal)
const pullRequestStateModalActive = Modal.$is("PullRequestState")(activeModal)
const pullRequestFilterModalActive = Modal.$is("PullRequestFilter")(activeModal)
const mergeModalActive = Modal.$is("Merge")(activeModal)
const commentModalActive = Modal.$is("Comment")(activeModal)
const deleteCommentModalActive = Modal.$is("DeleteComment")(activeModal)
Expand All @@ -743,6 +758,7 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
const labelModal: LabelModalState = labelModalActive ? activeModal : initialLabelModalState
const closeModal: CloseModalState = closeModalActive ? activeModal : initialCloseModalState
const pullRequestStateModal: PullRequestStateModalState = pullRequestStateModalActive ? activeModal : initialPullRequestStateModalState
const pullRequestFilterModal: PullRequestFilterModalState = pullRequestFilterModalActive ? activeModal : initialPullRequestFilterModalState
const mergeModal: MergeModalState = mergeModalActive ? activeModal : initialMergeModalState
const commentModal: CommentModalState = commentModalActive ? activeModal : initialCommentModalState
const deleteCommentModal: DeleteCommentModalState = deleteCommentModalActive ? activeModal : initialDeleteCommentModalState
Expand All @@ -767,6 +783,7 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
const setLabelModal = makeModalSetter("Label")
const setCloseModal = makeModalSetter("Close")
const setPullRequestStateModal = makeModalSetter("PullRequestState")
const setPullRequestFilterModal = makeModalSetter("PullRequestFilter")
const setMergeModal = makeModalSetter("Merge")
const setCommentModal = makeModalSetter("Comment")
const setDeleteCommentModal = makeModalSetter("DeleteComment")
Expand Down Expand Up @@ -935,7 +952,10 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
const pullRequestComments = useAtomValue(pullRequestCommentsAtom)
const pullRequestCommentsLoaded = useAtomValue(pullRequestCommentsLoadedAtom)
const selectedRepository = viewRepository(activeView)
const activeViews = activePullRequestViews(activeView)
const activeViews = activePullRequestViews(activeView).map((view) => {
const override = stateFiltersByView[viewStateKey(view)]
return override ? { ...view, stateFilter: override } : view
})
const currentQueueCacheKey = viewCacheKey(activeView)
const loadedPullRequestCount = pullRequestLoad?.data.length ?? 0
const hasMorePullRequests = Boolean(pullRequestLoad?.hasNextPage && loadedPullRequestCount < config.prFetchLimit)
Expand All @@ -948,11 +968,23 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
error: pullRequestError,
filterText: visibleFilterText,
showFilterBar: filterMode || filterQuery.length > 0,
stateFilter: activeView.stateFilter,
loadedCount: loadedPullRequestCount,
hasMore: hasMorePullRequests,
isLoadingMore: isLoadingMorePullRequests,
}),
[visibleGroups, pullRequestStatus, pullRequestError, visibleFilterText, filterMode, filterQuery, loadedPullRequestCount, hasMorePullRequests, isLoadingMorePullRequests],
[
visibleGroups,
pullRequestStatus,
pullRequestError,
visibleFilterText,
filterMode,
filterQuery,
activeView.stateFilter,
loadedPullRequestCount,
hasMorePullRequests,
isLoadingMorePullRequests,
],
)
const selectedPullRequestRowIndex = pullRequestListRowIndex(pullRequestListRows, selectedPullRequest?.url ?? null)
const selectedDiffKey = useAtomValue(selectedDiffKeyAtom)
Expand Down Expand Up @@ -1089,6 +1121,11 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
if (viewEquals(view, activeView)) return
refreshGenerationRef.current += 1
setQueueSelection((current) => ({ ...current, [currentQueueCacheKey]: selectedIndex }))
setStateFiltersByView((current) => ({
...current,
[viewStateKey(activeView)]: activeView.stateFilter,
[viewStateKey(view)]: view.stateFilter,
}))
setActiveView(view)
setSelectedIndex(registry.get(queueSelectionAtom)[viewCacheKey(view)] ?? 0)
setRecentlyCompletedPullRequests({})
Expand Down Expand Up @@ -1116,6 +1153,7 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
void loadPullRequestPage({
mode: viewMode(activeView),
repository: selectedRepository,
stateFilter: viewStateFilter(activeView),
cursor: pullRequestLoad.endCursor,
pageSize: Math.min(pullRequestPageSize, remaining),
})
Expand Down Expand Up @@ -1471,6 +1509,13 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
setStartupLoadComplete(true)
}, [startupLoadComplete, pullRequestStatus])

useEffect(() => {
if (pullRequestFilterModalActive) return
const selectedIndex = pullRequestStateFilters.indexOf(activeView.stateFilter)
if (selectedIndex < 0) return
setPullRequestFilterModal((current) => (current.selectedIndex === selectedIndex ? current : { selectedIndex }))
}, [activeView, pullRequestFilterModalActive])

useEffect(() => {
if (pullRequestStatus !== "ready" || !selectedPullRequest) return
hydratePullRequestDetails(selectedPullRequest, true)
Expand Down Expand Up @@ -1516,6 +1561,7 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
loadingIndicator,
visibleCount: visiblePullRequests.length,
filterText: visibleFilterText,
stateFilter: activeView.stateFilter,
})
const isSelectedPullRequestDetailLoading = selectedPullRequest !== null && !selectedPullRequest.detailLoaded
const halfPage = Math.max(1, Math.floor(wideBodyHeight / 2))
Expand Down Expand Up @@ -2723,14 +2769,27 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
const openRepositoryPicker = () => {
setOpenRepositoryModal({ query: selectedRepository ?? "", error: null })
}
const openPullRequestFilterModal = () => {
const selectedIndex = Math.max(0, pullRequestStateFilters.indexOf(activeView.stateFilter))
setPullRequestFilterModal({ selectedIndex })
}
const movePullRequestFilterSelection = (delta: -1 | 1) => {
setPullRequestFilterModal((current) => ({ ...current, selectedIndex: wrapIndex(current.selectedIndex + delta, pullRequestStateFilters.length) }))
}
const confirmPullRequestFilterSelection = () => {
const selected = pullRequestStateFilters[Math.max(0, Math.min(pullRequestFilterModal.selectedIndex, pullRequestStateFilters.length - 1))] ?? "open"
closeActiveModal()
setStateFiltersByView((current) => ({ ...current, [viewStateKey(activeView)]: selected }))
switchViewTo({ ...activeView, stateFilter: selected })
}
const openRepositoryFromInput = () => {
const repository = parseRepositoryInput(openRepositoryModal.query)
if (!repository) {
setOpenRepositoryModal((current) => ({ ...current, error: "Enter a repository as owner/name or a GitHub URL." }))
return
}
closeActiveModal()
switchViewTo({ _tag: "Repository", repository })
switchViewTo({ _tag: "Repository", repository, stateFilter: activeView.stateFilter })
flashNotice(`Opened ${repository}`)
}
const insertPastedText = (text: string) => {
Expand Down Expand Up @@ -2837,6 +2896,7 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
setFilterMode(false)
},
openThemeModal,
openPullRequestFilterModal,
openRepositoryPicker,
loadMorePullRequests,
switchViewTo,
Expand Down Expand Up @@ -2910,7 +2970,7 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
title: `Open ${repository}`,
scope: "View",
subtitle: "Switch to this repository",
run: () => switchViewTo({ _tag: "Repository", repository }),
run: () => switchViewTo({ _tag: "Repository", repository, stateFilter: activeView.stateFilter }),
}),
]
})()
Expand Down Expand Up @@ -3037,6 +3097,7 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
const appCtx: AppCtx = {
closeModalActive,
pullRequestStateModalActive,
pullRequestFilterModalActive,
mergeModalActive,
commentThreadModalActive,
changedFilesModalActive,
Expand Down Expand Up @@ -3069,6 +3130,11 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
confirmStateChange: confirmPullRequestStateChange,
moveSelection: movePullRequestStateSelection,
},
pullRequestFilterModal: {
closeModal: closeActiveModal,
confirmSelection: confirmPullRequestFilterSelection,
moveSelection: movePullRequestFilterSelection,
},
mergeModal: {
availableActionCount: visibleMergeKinds(mergeModal.info, mergeModal.allowedMethods, mergeModal.selectedMethod).length,
multipleMethodsAllowed: mergeModal.allowedMethods ? allowedMergeMethodList(mergeModal.allowedMethods).length > 1 : false,
Expand Down Expand Up @@ -3379,6 +3445,7 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
filterText: visibleFilterText,
showFilterBar: filterMode || filterQuery.length > 0,
isFilterEditing: filterMode,
stateFilter: activeView.stateFilter,
loadedCount: loadedPullRequestCount,
hasMore: hasMorePullRequests,
isLoadingMore: isLoadingMorePullRequests,
Expand Down Expand Up @@ -3426,6 +3493,11 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
const pullRequestStateModalHeight = pullRequestStateLayout.height
const pullRequestStateModalLeft = pullRequestStateLayout.left
const pullRequestStateModalTop = pullRequestStateLayout.top
const pullRequestFilterLayout = sizedModal(46, 68, 12, 10)
const pullRequestFilterModalWidth = pullRequestFilterLayout.width
const pullRequestFilterModalHeight = pullRequestFilterLayout.height
const pullRequestFilterModalLeft = pullRequestFilterLayout.left
const pullRequestFilterModalTop = pullRequestFilterLayout.top
const commentLayout = sizedModal(46, 76, 8, 16)
const commentModalWidth = commentLayout.width
const commentModalHeight = commentLayout.height
Expand Down Expand Up @@ -3721,7 +3793,8 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
closeModal.running ||
pullRequestStateModal.running ||
mergeModal.running ||
submitReviewModal.running
submitReviewModal.running ||
pullRequestFilterModalActive
}
loadingIndicator={loadingIndicator}
retryProgress={retryProgress}
Expand Down Expand Up @@ -3759,6 +3832,15 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
loadingIndicator={loadingIndicator}
/>
) : null}
{pullRequestFilterModalActive ? (
<PullRequestFilterModal
state={pullRequestFilterModal}
modalWidth={pullRequestFilterModalWidth}
modalHeight={pullRequestFilterModalHeight}
offsetLeft={pullRequestFilterModalLeft}
offsetTop={pullRequestFilterModalTop}
/>
) : null}
{commentModalActive ? (
<CommentModal
state={commentModal}
Expand Down
10 changes: 10 additions & 0 deletions src/appCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface AppCommandActions {
readonly openFilter: () => void
readonly clearFilter: () => void
readonly openThemeModal: () => void
readonly openPullRequestFilterModal: () => void
readonly openRepositoryPicker: () => void
readonly loadMorePullRequests: () => void
readonly switchViewTo: (view: PullRequestView) => void
Expand Down Expand Up @@ -167,6 +168,15 @@ export const buildAppCommands = ({
keywords: ["colors", "appearance"],
run: actions.openThemeModal,
}),
defineCommand({
id: "pull.filter-state",
title: "Filter pull requests by state",
scope: "View",
subtitle: "Show open, closed, or merged pull requests",
shortcut: "f",
keywords: ["state", "open", "closed", "merged"],
run: actions.openPullRequestFilterModal,
}),
defineCommand({
id: "repository.open",
title: "Open repository...",
Expand Down
4 changes: 4 additions & 0 deletions src/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export type LoadStatus = "loading" | "ready" | "error"
export const pullRequestStates = ["open", "closed", "merged"] as const
export type PullRequestState = (typeof pullRequestStates)[number]

export const pullRequestStateFilters = ["open", "closed", "merged"] as const
export type PullRequestStateFilter = (typeof pullRequestStateFilters)[number]

export const pullRequestQueueModes = ["authored", "review", "assigned", "mentioned"] as const
export type PullRequestUserQueueMode = (typeof pullRequestQueueModes)[number]
export type PullRequestQueueMode = "repository" | PullRequestUserQueueMode
Expand Down Expand Up @@ -164,6 +167,7 @@ export interface PullRequestPage {
export interface ListPullRequestPageInput {
readonly mode: PullRequestQueueMode
readonly repository: string | null
readonly stateFilter: PullRequestStateFilter
readonly cursor: string | null
readonly pageSize: number
}
Expand Down
Loading