From 964a9bf9c78b2f51e73c93ee42c93a6bc0cd2948 Mon Sep 17 00:00:00 2001 From: Ashfaq Naseem Date: Fri, 12 Jun 2026 21:07:40 +0600 Subject: [PATCH 1/7] Feat: set history tab button ui same as changes tab --- .../ui/history/commit-graph-filter-button.tsx | 27 +++++++++++++ app/src/ui/history/commit-graph-sidebar.tsx | 22 ++++++----- app/styles/ui/history/_commit-graph.scss | 38 +++++++++++++++++++ 3 files changed, 78 insertions(+), 9 deletions(-) create mode 100644 app/src/ui/history/commit-graph-filter-button.tsx diff --git a/app/src/ui/history/commit-graph-filter-button.tsx b/app/src/ui/history/commit-graph-filter-button.tsx new file mode 100644 index 00000000000..66417c8afa3 --- /dev/null +++ b/app/src/ui/history/commit-graph-filter-button.tsx @@ -0,0 +1,27 @@ +import * as React from 'react' +import { Button } from '../lib/button' +import { Octicon } from '../octicons' +import * as octicons from '../octicons/octicons.generated' + +/** + * A visual-only filter button for the commit graph sidebar. + * This is a placeholder for future filter functionality. + */ +export class CommitGraphFilterButton extends React.Component { + public render() { + const buttonTextLabel = 'Filter Options' + + return ( + + ) + } +} diff --git a/app/src/ui/history/commit-graph-sidebar.tsx b/app/src/ui/history/commit-graph-sidebar.tsx index 2a916343f0a..ad42cea2a7f 100644 --- a/app/src/ui/history/commit-graph-sidebar.tsx +++ b/app/src/ui/history/commit-graph-sidebar.tsx @@ -37,6 +37,7 @@ import { ICommitGraphRow, } from './commit-graph-model' import { CommitGraphCommitListItem } from './commit-graph-commit-list-item' +import { CommitGraphFilterButton } from './commit-graph-filter-button' type CommitGraphBranchGroup = | 'local' @@ -545,15 +546,18 @@ export class CommitGraphSidebar extends React.Component<
- +
+ + +
{this.commitGraph_renderViewModeSwitch()}
diff --git a/app/styles/ui/history/_commit-graph.scss b/app/styles/ui/history/_commit-graph.scss index 4b25bd80bb9..17d09d7652a 100644 --- a/app/styles/ui/history/_commit-graph.scss +++ b/app/styles/ui/history/_commit-graph.scss @@ -11,6 +11,44 @@ flex: 1 1 auto; min-width: 0; border-bottom: 0; + + .filter-box-container { + display: flex; + align-items: center; + + .filter-button { + border-radius: var(--border-radius) 0 0 var(--border-radius); + border-right: none; + font-weight: var(--font-weight-semibold); + color: var(--text-color); + justify-content: space-between; + display: inline-flex; + align-items: center; + padding-right: var(--spacing-half); + position: relative; + + &:hover { + color: var(--text-secondary-color); + background-color: inherit; + } + } + + .fancy-text-box-component { + border: var(--contrast-border); + border-radius: 0 var(--border-radius) var(--border-radius) 0; + width: 100%; + height: var(--text-field-height); + + &.focused { + border-color: var(--focus-color); + } + + & .fancy-octicon, + & .text-box-component input { + height: 100%; + } + } + } } .commitGraph-view-mode-switch { From a2fd4a75b2755a5c3595d1212f39505820a0356a Mon Sep 17 00:00:00 2001 From: Ashfaq Naseem Date: Sat, 13 Jun 2026 14:11:25 +0600 Subject: [PATCH 2/7] Feat: add filter button to search by author email --- app/src/lib/stores/app-store.ts | 57 ++++- app/src/ui/dispatcher/dispatcher.ts | 6 +- .../ui/history/commit-graph-filter-button.tsx | 224 ++++++++++++++++-- app/src/ui/history/commit-graph-sidebar.tsx | 69 ++++-- app/src/ui/lib/popover.tsx | 18 +- app/styles/ui/history/_commit-graph.scss | 36 +++ 6 files changed, 362 insertions(+), 48 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index aeefcde6c22..e4219928d38 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -450,6 +450,7 @@ import { gatherCommitContext, } from '../copilot-conflict-context' import { resolveWithin } from '../path' +import { TFilters } from '../../ui/history/commit-graph-filter-button' const LastSelectedRepositoryIDKey = 'last-selected-repository-id' @@ -2019,7 +2020,8 @@ export class AppStore extends TypedBaseStore { public async _loadNextCommitBatch( repository: Repository, alreadyFiltered: number, - queryTextLowercase?: string + queryTextLowercase?: string, + authorFiltersLowercase?: string[] ): Promise { const gitStore = this.gitStoreCache.get(repository) @@ -2055,7 +2057,17 @@ export class AppStore extends TypedBaseStore { return } - const newFilteredCommits = newCommits.filter(sha => + let newFilteredCommits = + authorFiltersLowercase && authorFiltersLowercase.length > 0 + ? newCommits.filter(sha => { + const commit = gitStore.commitLookup.get(sha) + return authorFiltersLowercase.some(filter => + this.commitIsIncludedByAuthorFilter(commit, filter) + ) + }) + : newCommits + + newFilteredCommits = newFilteredCommits.filter(sha => this.commitIsIncluded(gitStore.commitLookup.get(sha), queryTextLowercase) ) @@ -2073,7 +2085,8 @@ export class AppStore extends TypedBaseStore { return this._loadNextCommitBatch( repository, numFilteredCommits, - queryTextLowercase + queryTextLowercase, + authorFiltersLowercase ) } return @@ -2301,14 +2314,30 @@ export class AppStore extends TypedBaseStore { ) } + private commitIsIncludedByAuthorFilter( + commit: Commit | undefined, + filterTextLowerCase: string + ): boolean { + if (!commit) { + return false + } + return ( + !filterTextLowerCase || + commit.author.email.toLowerCase() === filterTextLowerCase + ) + } + public async _updateCommitSearchQuery( repository: Repository, - query: string + query: string, + filters?: TFilters ): Promise { const state = this.repositoryStateCache.get(repository) const compareState = state.compareState - const isIncrementalSearch = query.startsWith(compareState.commitSearchQuery) - + const authorFilters = filters && filters.get('author') + const isIncrementalSearch = query + ? query.startsWith(compareState.commitSearchQuery) + : false this.repositoryStateCache.updateCompareState(repository, () => ({ commitSearchQuery: query, })) @@ -2320,12 +2349,28 @@ export class AppStore extends TypedBaseStore { const candidateCommitSHAs = isIncrementalSearch ? compareState.filteredHistoryCommitSHAs : compareState.allHistoryCommitSHAs + const queryTextLowercase = query.toLowerCase() const filteredCommitSHAs = queryTextLowercase ? candidateCommitSHAs.filter(sha => this.commitIsIncluded(state.commitLookup.get(sha), queryTextLowercase) ) : candidateCommitSHAs + + console.log(filteredCommitSHAs, ' query ') + + const filteredCommitSHAsByAuthor = + authorFilters && authorFilters.size > 0 + ? filteredCommitSHAs.filter(sha => { + const commit = state.commitLookup.get(sha) + return Array.from(authorFilters).some(filter => + this.commitIsIncludedByAuthorFilter(commit, filter.toLowerCase()) + ) + }) + : filteredCommitSHAs + + console.log(filteredCommitSHAsByAuthor, ' author ') + this.repositoryStateCache.updateCompareState(repository, () => ({ filteredHistoryCommitSHAs: filteredCommitSHAs, })) diff --git a/app/src/ui/dispatcher/dispatcher.ts b/app/src/ui/dispatcher/dispatcher.ts index 8a4e53bfee9..4e1a555434a 100644 --- a/app/src/ui/dispatcher/dispatcher.ts +++ b/app/src/ui/dispatcher/dispatcher.ts @@ -146,6 +146,7 @@ import { ICopilotConflictResolutionResponse, IConflictResolutionProgress, } from '../../lib/copilot-conflict-resolution' +import { TFilters } from '../history/commit-graph-filter-button' /** * An error handler function. @@ -296,9 +297,10 @@ export class Dispatcher { /** Update the commit search filter text. */ public setCommitSearchQuery( repository: Repository, - text: string + text: string, + filters?: TFilters ): Promise { - return this.appStore._updateCommitSearchQuery(repository, text) + return this.appStore._updateCommitSearchQuery(repository, text, filters) } /** Load the changed files for the current history selection. */ diff --git a/app/src/ui/history/commit-graph-filter-button.tsx b/app/src/ui/history/commit-graph-filter-button.tsx index 66417c8afa3..fefe601b2c3 100644 --- a/app/src/ui/history/commit-graph-filter-button.tsx +++ b/app/src/ui/history/commit-graph-filter-button.tsx @@ -1,27 +1,221 @@ import * as React from 'react' import { Button } from '../lib/button' +import { Checkbox, CheckboxValue } from '../lib/checkbox' +import { + Popover, + PopoverAnchorPosition, + PopoverDecoration, +} from '../lib/popover' import { Octicon } from '../octicons' import * as octicons from '../octicons/octicons.generated' -/** - * A visual-only filter button for the commit graph sidebar. - * This is a placeholder for future filter functionality. - */ -export class CommitGraphFilterButton extends React.Component { +export type TFilters = Map> +interface ICommitGraphFilterButtonProps { + readonly filters: TFilters + readonly onFilterUpdate: (filters: TFilters) => void +} +interface ICommitGraphFilterButtonState { + readonly isFilterOptionsOpen: boolean + readonly isAuthorFilterChecked: boolean + readonly hasFilterOptionsMounted: boolean +} +export class CommitGraphFilterButton extends React.Component< + ICommitGraphFilterButtonProps, + ICommitGraphFilterButtonState +> { + private AUTHOR_FILTER_KEY = 'author' + private filterOptionsButtonRef: HTMLButtonElement | null = null + private filterContainerRef: HTMLDivElement | null = null + + public constructor(props: ICommitGraphFilterButtonProps) { + super(props) + + this.state = { + isFilterOptionsOpen: false, + isAuthorFilterChecked: false, + hasFilterOptionsMounted: false, + } + } + + private onFilterOptionsButtonRef = (buttonRef: HTMLButtonElement | null) => { + this.filterOptionsButtonRef = buttonRef + } + + private toggleFilterOptionsOpen = () => { + this.setState(prevState => ({ + isFilterOptionsOpen: !prevState.isFilterOptionsOpen, + })) + } + private closeFilterOptions = () => { + this.setState({ isFilterOptionsOpen: false }) + } + private onAuthorFilterToIncludedInCommit = ( + evt: React.FormEvent + ) => { + const newFilters = new Map(this.props.filters) + if (evt.currentTarget.checked) { + newFilters.set(this.AUTHOR_FILTER_KEY, new Set()) + this.setState({ + isAuthorFilterChecked: true, + }) + } else { + newFilters.delete(this.AUTHOR_FILTER_KEY) + this.setState({ + isAuthorFilterChecked: false, + }) + } + this.props.onFilterUpdate(newFilters) + // this.closeFilterOptions() + } + + private onAuthorSubFilterSelect = ( + event: React.FormEvent, + key: string + ) => { + if (!key) return + const checked = event.currentTarget.checked + + const newFilters = new Map(this.props.filters) + const newSet = new Set(newFilters.get(this.AUTHOR_FILTER_KEY)) + newFilters.set(this.AUTHOR_FILTER_KEY, newSet) + if (checked) { + newSet.add(key) + } else { + newSet.delete(key) + } + this.props.onFilterUpdate(newFilters) + } + + private onFilterContainerRef = (divRef: HTMLDivElement | null) => { + this.filterContainerRef = divRef + } + + private onMountFilterOptions = () => { + this.setState({ + hasFilterOptionsMounted: true, + }) + } + + private onUnmountFilterOptions = () => { + this.setState({ + hasFilterOptionsMounted: false, + }) + } + + private renderFilterOptions() { + return ( + <> + +
+

Filter Options

+ +
+
+ +
+ {/* {filtersActive && ( +
+ +
+ )} */} +
+ {this.state.hasFilterOptionsMounted && + this.props.filters.size > 0 && + this.renderSubFilterOptions()} + + ) + } + + private renderSubFilterOptions() { + return ( + +
+

Authors

+ {/* */} +
+
+ + this.onAuthorSubFilterSelect(event, 'ashfaqnaseem1@gmail.com') + } + label="Ashfaq Naseem" + /> +
+ {/* {filtersActive && ( +
+ +
+ )} */} +
+ ) + } + public render() { const buttonTextLabel = 'Filter Options' return ( - + <> + + {this.state.isFilterOptionsOpen && this.renderFilterOptions()} + ) } } diff --git a/app/src/ui/history/commit-graph-sidebar.tsx b/app/src/ui/history/commit-graph-sidebar.tsx index ad42cea2a7f..f792dc38087 100644 --- a/app/src/ui/history/commit-graph-sidebar.tsx +++ b/app/src/ui/history/commit-graph-sidebar.tsx @@ -1,43 +1,43 @@ import * as React from 'react' -import { Commit, CommitOneLine, ICommitContext } from '../../models/commit' +import classNames from 'classnames' +import memoizeOne from 'memoize-one' import { ICompareState, IConstrainedValue } from '../../lib/app-state' +import { Emoji } from '../../lib/emoji' +import { doMergeCommitsExistAfterCommit } from '../../lib/git' +import { getSquashedCommitDescription } from '../../lib/squash/squashed-commit-description' import { commitGraph_getStoredViewMode, commitGraph_setStoredViewMode, CommitHistoryViewMode, } from '../../lib/stores/commit-graph-state' -import { Repository } from '../../models/repository' +import { getUniqueCoauthorsAsAuthors } from '../../lib/unique-coauthors-as-authors' +import { Account } from '../../models/account' import { Branch, BranchType } from '../../models/branch' -import { Dispatcher, defaultErrorHandler } from '../dispatcher' -import { CommitList } from './commit-list' -import type { ICommitListItemRenderProps } from './commit-list' -import { FancyTextBox } from '../lib/fancy-text-box' +import { Commit, CommitOneLine, ICommitContext } from '../../models/commit' +import { DragType } from '../../models/drag-drop' +import { PopupType } from '../../models/popup' +import { Repository } from '../../models/repository' +import { defaultErrorHandler, Dispatcher } from '../dispatcher' import { Button } from '../lib/button' import { Checkbox, CheckboxValue } from '../lib/checkbox' -import { Resizable } from '../resizable' -import { Account } from '../../models/account' -import { Emoji } from '../../lib/emoji' +import { FancyTextBox } from '../lib/fancy-text-box' import { KeyboardInsertionData } from '../lib/list' -import { DragType } from '../../models/drag-drop' -import { PopupType } from '../../models/popup' -import { getUniqueCoauthorsAsAuthors } from '../../lib/unique-coauthors-as-authors' -import { getSquashedCommitDescription } from '../../lib/squash/squashed-commit-description' -import { doMergeCommitsExistAfterCommit } from '../../lib/git' -import { Octicon, syncClockwise } from '../octicons' -import * as octicons from '../octicons/octicons.generated' -import classNames from 'classnames' -import memoizeOne from 'memoize-one' import { ThrottledScheduler } from '../lib/throttled-scheduler' import { startTimer } from '../lib/timing' +import { Octicon, syncClockwise } from '../octicons' +import * as octicons from '../octicons/octicons.generated' +import { Resizable } from '../resizable' +import { CommitGraphCommitListItem } from './commit-graph-commit-list-item' +import { CommitGraphFilterButton, TFilters } from './commit-graph-filter-button' import { commitGraph_buildRows, commitGraph_getColor, commitGraph_RowHeight, ICommitGraphRow, } from './commit-graph-model' -import { CommitGraphCommitListItem } from './commit-graph-commit-list-item' -import { CommitGraphFilterButton } from './commit-graph-filter-button' +import type { ICommitListItemRenderProps } from './commit-list' +import { CommitList } from './commit-list' type CommitGraphBranchGroup = | 'local' @@ -83,6 +83,7 @@ interface ICommitGraphSidebarState { readonly isSearching: boolean readonly commitGraphViewMode: CommitHistoryViewMode readonly commitGraphSelectedBranchRef: string | null + readonly filters: TFilters } interface ICommitGraphBranches { @@ -520,6 +521,7 @@ export class CommitGraphSidebar extends React.Component< isSearching: false, commitGraphViewMode: commitGraph_getStoredViewMode(), commitGraphSelectedBranchRef: null, + filters: new Map(), } } @@ -539,6 +541,12 @@ export class CommitGraphSidebar extends React.Component< this.commitListRef.current?.focus() } + private onFilterUpdate = (filters: TFilters) => { + console.log(filters, ' filters ') + this.setState({ filters }) + this.onCommitSearchFiltersChanged(filters) + } + public render() { const { commitSearchQuery } = this.props.compareState @@ -547,11 +555,17 @@ export class CommitGraphSidebar extends React.Component<
- + + { + private onCommitQuery = async (text: string, filters: TFilters) => { if (this.state.commitGraphViewMode === CommitHistoryViewMode.Graph) { this.props.dispatcher.updateCompareForm(this.props.repository, { commitSearchQuery: text, @@ -1335,10 +1349,17 @@ export class CommitGraphSidebar extends React.Component< this.setState({ isSearching: true }) await this.props.dispatcher.setCommitSearchQuery( this.props.repository, - text + text, + filters ) this.setState({ isSearching: false }) } + private onCommitSearchQueryChanged = async (text: string) => { + await this.onCommitQuery(text, this.state.filters) + } + private onCommitSearchFiltersChanged = async (filters: TFilters) => { + await this.onCommitQuery(this.props.compareState.commitSearchQuery, filters) + } private onCreateTag = (targetCommitSha: string) => { this.props.dispatcher.showCreateTagDialog( diff --git a/app/src/ui/lib/popover.tsx b/app/src/ui/lib/popover.tsx index 8268acb0ef1..415d4a7c4b6 100644 --- a/app/src/ui/lib/popover.tsx +++ b/app/src/ui/lib/popover.tsx @@ -18,6 +18,7 @@ import { } from '@floating-ui/core' import { assertNever } from '../../lib/fatal-error' import { isMacOSSequoia, isMacOSSonoma, isMacOSVentura } from '../../lib/get-os' +import { createObservableRef } from './observable-ref' /** * Position of the popover relative to its anchor element. It's composed by 2 @@ -84,6 +85,18 @@ interface IPopoverProps { readonly maxHeight?: number /** Minimum height decided by clients of Popover */ readonly minHeight?: number + + /** + * The `ref` for the underlying container
element. + * + * Ideally this would be named `ref`, but TypeScript seems to special-case its + * handling of the `ref` type into some ungodly monstrosity. Hopefully someday + * this will be unnecessary. + */ + readonly onContainerRef?: (instance: HTMLDivElement | null) => void + + readonly onComponentDidMount?: () => void + readonly onComponentWillUnmount?: () => void } interface IPopoverState { @@ -92,13 +105,14 @@ interface IPopoverState { export class Popover extends React.Component { private focusTrapOptions: FocusTrapOptions - private containerDivRef = React.createRef() + private containerDivRef = createObservableRef() private contentDivRef = React.createRef() private tipDivRef = React.createRef() private floatingCleanUp: (() => void) | null = null public constructor(props: IPopoverProps) { super(props) + this.containerDivRef.subscribe(div => this.props.onContainerRef?.(div)) this.focusTrapOptions = { allowOutsideClick: true, @@ -195,6 +209,7 @@ export class Popover extends React.Component { document.addEventListener('click', this.onDocumentClick) document.addEventListener('mousedown', this.onDocumentMouseDown) this.setupPosition() + this.props.onComponentDidMount?.() } public componentDidUpdate(prevProps: IPopoverProps) { @@ -206,6 +221,7 @@ export class Popover extends React.Component { public componentWillUnmount() { document.removeEventListener('click', this.onDocumentClick) document.removeEventListener('mousedown', this.onDocumentMouseDown) + this.props.onComponentWillUnmount?.() } private onDocumentClick = (event: MouseEvent) => { diff --git a/app/styles/ui/history/_commit-graph.scss b/app/styles/ui/history/_commit-graph.scss index 17d09d7652a..a909e266154 100644 --- a/app/styles/ui/history/_commit-graph.scss +++ b/app/styles/ui/history/_commit-graph.scss @@ -15,6 +15,42 @@ .filter-box-container { display: flex; align-items: center; + margin-bottom: var(--spacing-half); + transition: all 0.3s ease-out; + + .filter-popover { + text-align: right; + min-width: 200px; + + .filter-popover-header { + display: flex; + justify-content: space-between; + align-items: center; + + h3 { + margin: 0; + } + } + + .filter-options { + margin: var(--spacing) 0; + } + + .filter-options-footer { + padding: var(--spacing-half) 0 var(--spacing) 0; + margin-top: var(--spacing-quarter); + text-align: left; + } + .popover-content { + padding: var(--spacing) var(--spacing) 0 var(--spacing); + } + + @include close-button; + + .close { + margin: 0; + } + } .filter-button { border-radius: var(--border-radius) 0 0 var(--border-radius); From 9b088bc1456ab65ae88e5c9de463df26f4ab89ff Mon Sep 17 00:00:00 2001 From: Ashfaq Naseem Date: Sat, 13 Jun 2026 14:53:39 +0600 Subject: [PATCH 3/7] Feat: use existing incremental search for filters --- app/src/lib/app-state.ts | 3 ++ app/src/lib/stores/app-store.ts | 53 ++++++++++++-------- app/src/lib/stores/repository-state-cache.ts | 1 + 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/app/src/lib/app-state.ts b/app/src/lib/app-state.ts index 3d75f4b3080..e3b05f1e089 100644 --- a/app/src/lib/app-state.ts +++ b/app/src/lib/app-state.ts @@ -1006,6 +1006,9 @@ export interface ICompareState { /** The SHA associated with the most recent history state */ readonly tip: string | null + /** The SHAs of commits to render in the compare list */ + readonly prevFilteredHistoryCommitSHAs: ReadonlyArray + /** The SHAs of commits to render in the compare list */ readonly filteredHistoryCommitSHAs: ReadonlyArray diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index e4219928d38..35773de0f09 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -2057,24 +2057,28 @@ export class AppStore extends TypedBaseStore { return } - let newFilteredCommits = + const baseFilteredCommits = newCommits.filter(sha => + this.commitIsIncluded(gitStore.commitLookup.get(sha), queryTextLowercase) + ) + + const newFilteredCommits = authorFiltersLowercase && authorFiltersLowercase.length > 0 - ? newCommits.filter(sha => { + ? baseFilteredCommits.filter(sha => { const commit = gitStore.commitLookup.get(sha) return authorFiltersLowercase.some(filter => this.commitIsIncludedByAuthorFilter(commit, filter) ) }) - : newCommits - - newFilteredCommits = newFilteredCommits.filter(sha => - this.commitIsIncluded(gitStore.commitLookup.get(sha), queryTextLowercase) - ) + : baseFilteredCommits this.repositoryStateCache.updateCompareState(repository, () => ({ allHistoryCommitSHAs: commits.concat(newCommits), filteredHistoryCommitSHAs: state.compareState.filteredHistoryCommitSHAs.concat(newFilteredCommits), + prevFilteredHistoryCommitSHAs: + state.compareState.filteredHistoryCommitSHAs.concat( + baseFilteredCommits + ), })) const numFilteredCommits = alreadyFiltered + newFilteredCommits.length @@ -2334,7 +2338,10 @@ export class AppStore extends TypedBaseStore { ): Promise { const state = this.repositoryStateCache.get(repository) const compareState = state.compareState - const authorFilters = filters && filters.get('author') + const authorFiltersSet = filters && filters.get('author') + const authorFiltersLowercase = authorFiltersSet + ? Array.from(authorFiltersSet).map(item => item.toLowerCase()) + : [] const isIncrementalSearch = query ? query.startsWith(compareState.commitSearchQuery) : false @@ -2347,39 +2354,41 @@ export class AppStore extends TypedBaseStore { } const candidateCommitSHAs = isIncrementalSearch - ? compareState.filteredHistoryCommitSHAs + ? compareState.prevFilteredHistoryCommitSHAs : compareState.allHistoryCommitSHAs const queryTextLowercase = query.toLowerCase() - const filteredCommitSHAs = queryTextLowercase + const baseFilteredCommitSHAs = queryTextLowercase ? candidateCommitSHAs.filter(sha => this.commitIsIncluded(state.commitLookup.get(sha), queryTextLowercase) ) : candidateCommitSHAs - console.log(filteredCommitSHAs, ' query ') + console.log(baseFilteredCommitSHAs, ' query ') - const filteredCommitSHAsByAuthor = - authorFilters && authorFilters.size > 0 - ? filteredCommitSHAs.filter(sha => { + const newfilteredCommitSHAs = + authorFiltersLowercase.length > 0 + ? baseFilteredCommitSHAs.filter(sha => { const commit = state.commitLookup.get(sha) - return Array.from(authorFilters).some(filter => - this.commitIsIncludedByAuthorFilter(commit, filter.toLowerCase()) + return authorFiltersLowercase.some(filter => + this.commitIsIncludedByAuthorFilter(commit, filter) ) }) - : filteredCommitSHAs + : baseFilteredCommitSHAs - console.log(filteredCommitSHAsByAuthor, ' author ') + console.log(newfilteredCommitSHAs, ' author ') this.repositoryStateCache.updateCompareState(repository, () => ({ - filteredHistoryCommitSHAs: filteredCommitSHAs, + filteredHistoryCommitSHAs: newfilteredCommitSHAs, + prevFilteredHistoryCommitSHAs: baseFilteredCommitSHAs, })) this.emitUpdate() - if (filteredCommitSHAs.length < MinimumFilteredCommitsToLoad) { + if (newfilteredCommitSHAs.length < MinimumFilteredCommitsToLoad) { this.currentCommitFilterPromise = this._loadNextCommitBatch( repository, - filteredCommitSHAs.length, - queryTextLowercase + newfilteredCommitSHAs.length, + queryTextLowercase, + authorFiltersLowercase ) await this.currentCommitFilterPromise this.currentCommitFilterPromise = null diff --git a/app/src/lib/stores/repository-state-cache.ts b/app/src/lib/stores/repository-state-cache.ts index ff61f8de302..2d5e6be2b53 100644 --- a/app/src/lib/stores/repository-state-cache.ts +++ b/app/src/lib/stores/repository-state-cache.ts @@ -366,6 +366,7 @@ function getInitialRepositoryState(): IRepositoryState { filterText: '', commitSearchQuery: '', allHistoryCommitSHAs: [], + prevFilteredHistoryCommitSHAs: [], filteredHistoryCommitSHAs: [], commitGraphRefs: [], commitGraphHiddenBranchRefs: null, From 54f56b3f9dc95d5121213d43c9de4d391b3aaff6 Mon Sep 17 00:00:00 2001 From: Ashfaq Naseem Date: Sat, 13 Jun 2026 21:18:53 +0600 Subject: [PATCH 4/7] Feat: Populate author filters dynamically Replace hardcoded author filter options with dynamically extracted authors from commit history. Adds visual badge indicator and count display to the filter button when filters are active. --- app/src/lib/stores/app-store.ts | 10 +-- .../ui/history/commit-graph-filter-button.tsx | 73 +++++++++++++------ app/src/ui/history/commit-graph-sidebar.tsx | 30 +++++++- app/styles/ui/history/_commit-graph.scss | 34 ++++++++- 4 files changed, 115 insertions(+), 32 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 35773de0f09..10a5da4a063 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -2366,7 +2366,7 @@ export class AppStore extends TypedBaseStore { console.log(baseFilteredCommitSHAs, ' query ') - const newfilteredCommitSHAs = + const newFilteredCommitSHAs = authorFiltersLowercase.length > 0 ? baseFilteredCommitSHAs.filter(sha => { const commit = state.commitLookup.get(sha) @@ -2376,17 +2376,17 @@ export class AppStore extends TypedBaseStore { }) : baseFilteredCommitSHAs - console.log(newfilteredCommitSHAs, ' author ') + console.log(newFilteredCommitSHAs, ' author ') this.repositoryStateCache.updateCompareState(repository, () => ({ - filteredHistoryCommitSHAs: newfilteredCommitSHAs, + filteredHistoryCommitSHAs: newFilteredCommitSHAs, prevFilteredHistoryCommitSHAs: baseFilteredCommitSHAs, })) this.emitUpdate() - if (newfilteredCommitSHAs.length < MinimumFilteredCommitsToLoad) { + if (newFilteredCommitSHAs.length < MinimumFilteredCommitsToLoad) { this.currentCommitFilterPromise = this._loadNextCommitBatch( repository, - newfilteredCommitSHAs.length, + newFilteredCommitSHAs.length, queryTextLowercase, authorFiltersLowercase ) diff --git a/app/src/ui/history/commit-graph-filter-button.tsx b/app/src/ui/history/commit-graph-filter-button.tsx index fefe601b2c3..9e19a2a004f 100644 --- a/app/src/ui/history/commit-graph-filter-button.tsx +++ b/app/src/ui/history/commit-graph-filter-button.tsx @@ -8,22 +8,26 @@ import { } from '../lib/popover' import { Octicon } from '../octicons' import * as octicons from '../octicons/octicons.generated' +import classNames from 'classnames' +export const AUTHOR_FILTER_KEY = 'author' export type TFilters = Map> +export type TFilterFillData = { name: string; email: string } interface ICommitGraphFilterButtonProps { readonly filters: TFilters + readonly filtersFillData: Record> readonly onFilterUpdate: (filters: TFilters) => void } interface ICommitGraphFilterButtonState { readonly isFilterOptionsOpen: boolean readonly isAuthorFilterChecked: boolean readonly hasFilterOptionsMounted: boolean + readonly activeFilterData: TFilterFillData[] } export class CommitGraphFilterButton extends React.Component< ICommitGraphFilterButtonProps, ICommitGraphFilterButtonState > { - private AUTHOR_FILTER_KEY = 'author' private filterOptionsButtonRef: HTMLButtonElement | null = null private filterContainerRef: HTMLDivElement | null = null @@ -34,6 +38,7 @@ export class CommitGraphFilterButton extends React.Component< isFilterOptionsOpen: false, isAuthorFilterChecked: false, hasFilterOptionsMounted: false, + activeFilterData: [], } } @@ -54,12 +59,13 @@ export class CommitGraphFilterButton extends React.Component< ) => { const newFilters = new Map(this.props.filters) if (evt.currentTarget.checked) { - newFilters.set(this.AUTHOR_FILTER_KEY, new Set()) + newFilters.set(AUTHOR_FILTER_KEY, new Set()) this.setState({ isAuthorFilterChecked: true, + activeFilterData: this.props.filtersFillData[AUTHOR_FILTER_KEY], }) } else { - newFilters.delete(this.AUTHOR_FILTER_KEY) + newFilters.delete(AUTHOR_FILTER_KEY) this.setState({ isAuthorFilterChecked: false, }) @@ -76,8 +82,8 @@ export class CommitGraphFilterButton extends React.Component< const checked = event.currentTarget.checked const newFilters = new Map(this.props.filters) - const newSet = new Set(newFilters.get(this.AUTHOR_FILTER_KEY)) - newFilters.set(this.AUTHOR_FILTER_KEY, newSet) + const newSet = new Set(newFilters.get(AUTHOR_FILTER_KEY)) + newFilters.set(AUTHOR_FILTER_KEY, newSet) if (checked) { newSet.add(key) } else { @@ -151,14 +157,18 @@ export class CommitGraphFilterButton extends React.Component< ) } + public componentDidMount(): void { + console.log(this.props.filtersFillData) + } + private renderSubFilterOptions() { return ( @@ -173,19 +183,20 @@ export class CommitGraphFilterButton extends React.Component< */}
- - this.onAuthorSubFilterSelect(event, 'ashfaqnaseem1@gmail.com') - } - label="Ashfaq Naseem" - /> + {this.state.activeFilterData.map(({ name, email }) => { + return ( + this.onAuthorSubFilterSelect(event, email)} + label={name} + /> + ) + })}
{/* {filtersActive && (
@@ -197,12 +208,19 @@ export class CommitGraphFilterButton extends React.Component< } public render() { - const buttonTextLabel = 'Filter Options' - + const authorFilterSet = this.props.filters.get(AUTHOR_FILTER_KEY) + const activeFiltersCount = authorFilterSet ? authorFilterSet.size : 0 + const hasActiveFilters = authorFilterSet ? activeFiltersCount > 0 : false + console.log({ hasActiveFilters }) + const buttonTextLabel = `Filter Options ${ + hasActiveFilters ? `(${activeFiltersCount} applied)` : '' + }` return ( <> {this.state.isFilterOptionsOpen && this.renderFilterOptions()} diff --git a/app/src/ui/history/commit-graph-sidebar.tsx b/app/src/ui/history/commit-graph-sidebar.tsx index f792dc38087..d7afd1944d5 100644 --- a/app/src/ui/history/commit-graph-sidebar.tsx +++ b/app/src/ui/history/commit-graph-sidebar.tsx @@ -29,7 +29,12 @@ import { Octicon, syncClockwise } from '../octicons' import * as octicons from '../octicons/octicons.generated' import { Resizable } from '../resizable' import { CommitGraphCommitListItem } from './commit-graph-commit-list-item' -import { CommitGraphFilterButton, TFilters } from './commit-graph-filter-button' +import { + AUTHOR_FILTER_KEY, + CommitGraphFilterButton, + TFilterFillData, + TFilters, +} from './commit-graph-filter-button' import { commitGraph_buildRows, commitGraph_getColor, @@ -547,6 +552,26 @@ export class CommitGraphSidebar extends React.Component< this.onCommitSearchFiltersChanged(filters) } + private getAuthorFilterData = () => { + const seenEmails = new Set() + const uniqueAuthors: TFilterFillData[] = [] + + for (const sha of this.props.compareState.allHistoryCommitSHAs) { + const commit = this.props.commitLookup.get(sha) + + if (commit?.author?.email && !seenEmails.has(commit.author.email)) { + seenEmails.add(commit.author.email) + + uniqueAuthors.push({ + name: commit.author.name, + email: commit.author.email, + }) + } + } + + return uniqueAuthors + } + public render() { const { commitSearchQuery } = this.props.compareState @@ -557,6 +582,9 @@ export class CommitGraphSidebar extends React.Component<
diff --git a/app/styles/ui/history/_commit-graph.scss b/app/styles/ui/history/_commit-graph.scss index a909e266154..07a1bc220fb 100644 --- a/app/styles/ui/history/_commit-graph.scss +++ b/app/styles/ui/history/_commit-graph.scss @@ -6,6 +6,7 @@ align-items: stretch; background: var(--box-alt-background-color); border-bottom: var(--base-border); + padding-inline: var(--spacing-half); .commit-search-form { flex: 1 1 auto; @@ -15,7 +16,6 @@ .filter-box-container { display: flex; align-items: center; - margin-bottom: var(--spacing-half); transition: all 0.3s ease-out; .filter-popover { @@ -63,16 +63,46 @@ padding-right: var(--spacing-half); position: relative; + &.active { + span:first-child { + color: var(--box-selected-active-background-color); + } + } + + .active-badge { + position: absolute; + right: 18px; + top: 4px; + + .badge-bg { + padding: 1px; + border-radius: 50%; + background-color: var(--secondary-button-background); + + .badge { + width: 5px; + height: 5px; + background-color: var(--box-selected-active-background-color); + border-radius: 50%; + } + } + } + &:hover { color: var(--text-secondary-color); background-color: inherit; + + .badge-bg { + background-color: var(--secondary-button-background); + } } } .fancy-text-box-component { border: var(--contrast-border); border-radius: 0 var(--border-radius) var(--border-radius) 0; - width: 100%; + // width: 100%; + flex-grow: 1; height: var(--text-field-height); &.focused { From 0689c06abe1083b8efdce9275596d8c7788b040c Mon Sep 17 00:00:00 2001 From: Ashfaq Naseem Date: Sat, 13 Jun 2026 21:47:26 +0600 Subject: [PATCH 5/7] Bug: Fix sidebar history input resize and reduce CSS specificity Fix UI bug where sidebar history input width wouldn't change on resize. Reduce CSS specificity in commit graph styles. --- app/src/ui/history/commit-graph-sidebar.tsx | 17 +- app/styles/ui/history/_commit-graph.scss | 210 ++++++++++---------- 2 files changed, 116 insertions(+), 111 deletions(-) diff --git a/app/src/ui/history/commit-graph-sidebar.tsx b/app/src/ui/history/commit-graph-sidebar.tsx index d7afd1944d5..da2c8c8281e 100644 --- a/app/src/ui/history/commit-graph-sidebar.tsx +++ b/app/src/ui/history/commit-graph-sidebar.tsx @@ -580,14 +580,15 @@ export class CommitGraphSidebar extends React.Component<
- - + + + Date: Sat, 13 Jun 2026 21:54:48 +0600 Subject: [PATCH 6/7] Remove debug console logs --- app/src/lib/stores/app-store.ts | 4 ---- app/src/ui/history/commit-graph-filter-button.tsx | 5 ----- app/src/ui/history/commit-graph-sidebar.tsx | 1 - 3 files changed, 10 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 10a5da4a063..cfe17308e86 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -2364,8 +2364,6 @@ export class AppStore extends TypedBaseStore { ) : candidateCommitSHAs - console.log(baseFilteredCommitSHAs, ' query ') - const newFilteredCommitSHAs = authorFiltersLowercase.length > 0 ? baseFilteredCommitSHAs.filter(sha => { @@ -2376,8 +2374,6 @@ export class AppStore extends TypedBaseStore { }) : baseFilteredCommitSHAs - console.log(newFilteredCommitSHAs, ' author ') - this.repositoryStateCache.updateCompareState(repository, () => ({ filteredHistoryCommitSHAs: newFilteredCommitSHAs, prevFilteredHistoryCommitSHAs: baseFilteredCommitSHAs, diff --git a/app/src/ui/history/commit-graph-filter-button.tsx b/app/src/ui/history/commit-graph-filter-button.tsx index 9e19a2a004f..76b1268d0aa 100644 --- a/app/src/ui/history/commit-graph-filter-button.tsx +++ b/app/src/ui/history/commit-graph-filter-button.tsx @@ -157,10 +157,6 @@ export class CommitGraphFilterButton extends React.Component< ) } - public componentDidMount(): void { - console.log(this.props.filtersFillData) - } - private renderSubFilterOptions() { return ( 0 : false - console.log({ hasActiveFilters }) const buttonTextLabel = `Filter Options ${ hasActiveFilters ? `(${activeFiltersCount} applied)` : '' }` diff --git a/app/src/ui/history/commit-graph-sidebar.tsx b/app/src/ui/history/commit-graph-sidebar.tsx index da2c8c8281e..64db17ea010 100644 --- a/app/src/ui/history/commit-graph-sidebar.tsx +++ b/app/src/ui/history/commit-graph-sidebar.tsx @@ -547,7 +547,6 @@ export class CommitGraphSidebar extends React.Component< } private onFilterUpdate = (filters: TFilters) => { - console.log(filters, ' filters ') this.setState({ filters }) this.onCommitSearchFiltersChanged(filters) } From d1ff7f18bfa07e5968bf558cc5788fe0018caab7 Mon Sep 17 00:00:00 2001 From: Ashfaq Naseem Date: Sat, 13 Jun 2026 22:26:01 +0600 Subject: [PATCH 7/7] Fix lint errors --- .../ui/history/commit-graph-filter-button.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/ui/history/commit-graph-filter-button.tsx b/app/src/ui/history/commit-graph-filter-button.tsx index 76b1268d0aa..40bae15152c 100644 --- a/app/src/ui/history/commit-graph-filter-button.tsx +++ b/app/src/ui/history/commit-graph-filter-button.tsx @@ -15,14 +15,14 @@ export type TFilters = Map> export type TFilterFillData = { name: string; email: string } interface ICommitGraphFilterButtonProps { readonly filters: TFilters - readonly filtersFillData: Record> + readonly filtersFillData: Record> readonly onFilterUpdate: (filters: TFilters) => void } interface ICommitGraphFilterButtonState { readonly isFilterOptionsOpen: boolean readonly isAuthorFilterChecked: boolean readonly hasFilterOptionsMounted: boolean - readonly activeFilterData: TFilterFillData[] + readonly activeFilterData: ReadonlyArray } export class CommitGraphFilterButton extends React.Component< ICommitGraphFilterButtonProps, @@ -78,7 +78,9 @@ export class CommitGraphFilterButton extends React.Component< event: React.FormEvent, key: string ) => { - if (!key) return + if (!key) { + return + } const checked = event.currentTarget.checked const newFilters = new Map(this.props.filters) @@ -108,6 +110,12 @@ export class CommitGraphFilterButton extends React.Component< }) } + private onAuthorSubFilterChange = (email: string) => { + return (event: React.FormEvent) => { + this.onAuthorSubFilterSelect(event, email) + } + } + private renderFilterOptions() { return ( <> @@ -188,7 +196,7 @@ export class CommitGraphFilterButton extends React.Component< ? CheckboxValue.On : CheckboxValue.Off } - onChange={event => this.onAuthorSubFilterSelect(event, email)} + onChange={this.onAuthorSubFilterChange(email)} label={name} /> )