From 2c60d69f0d7b286baaaf6e9367894dc513170b64 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:02:53 -0500 Subject: [PATCH 01/12] Should be a sectioned filtered list --- app/src/ui/lib/augmented-filter-list.tsx | 299 ++++++++++++++--------- 1 file changed, 189 insertions(+), 110 deletions(-) diff --git a/app/src/ui/lib/augmented-filter-list.tsx b/app/src/ui/lib/augmented-filter-list.tsx index 96042f1c0f3..aa4eb17f65f 100644 --- a/app/src/ui/lib/augmented-filter-list.tsx +++ b/app/src/ui/lib/augmented-filter-list.tsx @@ -1,36 +1,27 @@ import * as React from 'react' import classnames from 'classnames' +import { SectionList, ClickSource } from '../lib/list/section-list' import { - List, - SelectionSource as ListSelectionSource, findNextSelectableRow, - ClickSource, SelectionDirection, -} from '../lib/list' +} from '../lib/list/section-list-selection' import { TextBox } from '../lib/text-box' import { Row } from '../lib/row' import { match, IMatch, IMatches } from '../../lib/fuzzy-find' import { AriaLiveContainer } from '../accessibility/aria-live-container' - -/** An item in the filter list. */ -export interface IFilterListItem { - /** The text which represents the item. This is used for filtering. */ - readonly text: ReadonlyArray - - /** A unique identifier for the item. */ - readonly id: string -} - -/** A group of items in the list. */ -export interface IFilterListGroup { - /** The identifier for this group. */ - readonly identifier: string - - /** The items in the group. */ - readonly items: ReadonlyArray -} +import { + InvalidRowIndexPath, + RowIndexPath, + rowIndexPathEquals, +} from './list/list-row-index-path' +import { + IFilterListGroup, + IFilterListItem, + SelectionSource, +} from './filter-list' +import * as octicons from '../octicons/octicons.generated' interface IFlattenedGroup { readonly kind: 'group' @@ -52,7 +43,7 @@ type IFilterListRow = | IFlattenedGroup | IFlattenedItem -interface IAugmentedFilterListProps { +interface IAugmentedSectionFilterListProps { /** A class name for the wrapping element. */ readonly className?: string @@ -117,6 +108,12 @@ interface IAugmentedFilterListProps { /** Called when the Enter key is pressed in field of type search */ readonly onEnterPressedWithoutFilteredItems?: (text: string) => void + /** Aria label for a specific item */ + readonly getItemAriaLabel?: (item: T) => string | undefined + + /** Aria label for a specific group */ + readonly getGroupAriaLabel?: (group: number) => string | undefined + /** The current filter text to use in the form */ readonly filterText?: string @@ -170,37 +167,26 @@ interface IAugmentedFilterListProps { ) => void } -interface IAugmentedFilterListState { - readonly rows: ReadonlyArray> - readonly selectedRow: number +interface IAugmentedSectionFilterListState { + readonly rows: ReadonlyArray>> + readonly selectedRow: RowIndexPath readonly filterValue: string readonly filterValueChanged: boolean + // Indices of groups in the filtered list + readonly groups: ReadonlyArray } -/** - * Interface describing a user initiated selection change event - * originating from changing the filter text. - */ -export interface IFilterSelectionSource { - kind: 'filter' - - /** The filter text at the time the selection event was raised. */ - filterText: string -} - -export type SelectionSource = ListSelectionSource | IFilterSelectionSource - /** A List which includes the ability to filter based on its contents. */ -export class AugmentedFilterList< +export class AugmentedSectionFilterList< T extends IFilterListItem > extends React.Component< - IAugmentedFilterListProps, - IAugmentedFilterListState + IAugmentedSectionFilterListProps, + IAugmentedSectionFilterListState > { - private list: List | null = null + private list: SectionList | null = null private filterTextBox: TextBox | null = null - public constructor(props: IAugmentedFilterListProps) { + public constructor(props: IAugmentedSectionFilterListProps) { super(props) if (props.filterTextBox !== undefined) { @@ -210,13 +196,15 @@ export class AugmentedFilterList< this.state = createStateUpdate(props, null) } - public componentWillReceiveProps(nextProps: IAugmentedFilterListProps) { + public componentWillReceiveProps( + nextProps: IAugmentedSectionFilterListProps + ) { this.setState(createStateUpdate(nextProps, this.state)) } public componentDidUpdate( - prevProps: IAugmentedFilterListProps, - prevState: IAugmentedFilterListState + prevProps: IAugmentedSectionFilterListProps, + prevState: IAugmentedSectionFilterListState ) { if (this.props.onSelectionChanged) { const oldSelectedItemId = getItemIdFromRowIndex( @@ -247,9 +235,9 @@ export class AugmentedFilterList< } if (this.props.onFilterListResultsChanged !== undefined) { - const itemCount = this.state.rows.filter( - row => row.kind === 'item' - ).length + const itemCount = this.state.rows + .flat() + .filter(row => row.kind === 'item').length this.props.onFilterListResultsChanged(itemCount) } @@ -266,6 +254,7 @@ export class AugmentedFilterList< row.kind === 'item') + const itemRows = this.state.rows.flat().filter(row => row.kind === 'item') const resultsPluralized = itemRows.length === 1 ? 'result' : 'results' const screenReaderMessage = `${itemRows.length} ${resultsPluralized}` return ( ) } @@ -329,23 +318,24 @@ export class AugmentedFilterList< if (this.list === null) { return } - let next: number | null = null + let next: RowIndexPath | null = null + const rowCount = this.state.rows.map(r => r.length) if ( - this.state.selectedRow === -1 || - this.state.selectedRow === this.state.rows.length + this.state.selectedRow.row === -1 || + this.state.selectedRow.row === this.state.rows.length ) { next = findNextSelectableRow( - this.state.rows.length, + rowCount, { direction: inDirection, - row: -1, + row: InvalidRowIndexPath, }, this.canSelectRow ) } else { next = findNextSelectableRow( - this.state.rows.length, + rowCount, { direction: inDirection, row: this.state.selectedRow, @@ -368,13 +358,17 @@ export class AugmentedFilterList< return this.props.renderNoItems() } else { return ( - r.length)} rowRenderer={this.renderRow} + sectionHasHeader={this.sectionHasHeader} + getRowAriaLabel={this.getRowAriaLabel} rowHeight={this.props.rowHeight} selectedRows={ - this.state.selectedRow === -1 ? [] : [this.state.selectedRow] + rowIndexPathEquals(this.state.selectedRow, InvalidRowIndexPath) + ? [] + : [this.state.selectedRow] } onSelectedRowChanged={this.onSelectedRowChanged} onRowClick={this.onRowClick} @@ -390,8 +384,34 @@ export class AugmentedFilterList< } } - private renderRow = (index: number) => { - const row = this.state.rows[index] + private sectionHasHeader = (section: number) => { + const rows = this.state.rows[section] + return rows.length > 0 && rows[0].kind === 'group' + } + + private getRowAriaLabel = (index: RowIndexPath) => { + const row = this.state.rows[index.section][index.row] + if (row.kind !== 'item') { + return undefined + } + + const itemAriaLabel = this.props.getItemAriaLabel?.(row.item) + + if (itemAriaLabel === undefined) { + return undefined + } + + const groupAriaLabel = this.props.getGroupAriaLabel?.( + this.state.groups[index.section] + ) + + return groupAriaLabel !== undefined + ? `${itemAriaLabel}, ${groupAriaLabel}` + : itemAriaLabel + } + + private renderRow = (index: RowIndexPath) => { + const row = this.state.rows[index.section][index.row] if (row.kind === 'item') { return this.props.renderItem(row.item, row.matches) } else if (this.props.renderGroupHeader) { @@ -405,7 +425,7 @@ export class AugmentedFilterList< this.filterTextBox = component } - private onListRef = (instance: List | null) => { + private onListRef = (instance: SectionList | null) => { this.list = instance } @@ -426,29 +446,32 @@ export class AugmentedFilterList< } } - private onSelectedRowChanged = (index: number, source: SelectionSource) => { + private onSelectedRowChanged = ( + index: RowIndexPath, + source: SelectionSource + ) => { this.setState({ selectedRow: index }) if (this.props.onSelectionChanged) { - const row = this.state.rows[index] + const row = this.state.rows[index.section][index.row] if (row.kind === 'item') { this.props.onSelectionChanged(row.item, source) } } } - private canSelectRow = (index: number) => { + private canSelectRow = (index: RowIndexPath) => { if (this.props.disabled) { return false } - const row = this.state.rows[index] + const row = this.state.rows[index.section][index.row] return row.kind === 'item' } - private onRowClick = (index: number, source: ClickSource) => { + private onRowClick = (index: RowIndexPath, source: ClickSource) => { if (this.props.onItemClick) { - const row = this.state.rows[index] + const row = this.state.rows[index.section][index.row] if (row.kind === 'item') { this.props.onItemClick(row.item, source) @@ -457,14 +480,14 @@ export class AugmentedFilterList< } private onRowContextMenu = ( - index: number, + index: RowIndexPath, source: React.MouseEvent ) => { if (!this.props.onItemContextMenu) { return } - const row = this.state.rows[index] + const row = this.state.rows[index.section][index.row] if (row.kind !== 'item') { return @@ -473,30 +496,47 @@ export class AugmentedFilterList< this.props.onItemContextMenu(row.item, source) } - private onRowKeyDown = (row: number, event: React.KeyboardEvent) => { + private onRowKeyDown = ( + indexPath: RowIndexPath, + event: React.KeyboardEvent + ) => { const list = this.list if (!list) { return } - const rowCount = this.state.rows.length + const rowCount = this.state.rows.map(r => r.length) const firstSelectableRow = findNextSelectableRow( rowCount, - { direction: 'down', row: -1 }, + { direction: 'down', row: InvalidRowIndexPath }, this.canSelectRow ) const lastSelectableRow = findNextSelectableRow( rowCount, - { direction: 'up', row: 0 }, + { + direction: 'up', + row: { + section: 0, + row: 0, + }, + }, this.canSelectRow ) let shouldFocus = false - if (event.key === 'ArrowUp' && row === firstSelectableRow) { + if ( + event.key === 'ArrowUp' && + firstSelectableRow && + rowIndexPathEquals(indexPath, firstSelectableRow) + ) { shouldFocus = true - } else if (event.key === 'ArrowDown' && row === lastSelectableRow) { + } else if ( + event.key === 'ArrowDown' && + lastSelectableRow && + rowIndexPathEquals(indexPath, lastSelectableRow) + ) { shouldFocus = true } @@ -526,13 +566,13 @@ export class AugmentedFilterList< return } - const rowCount = this.state.rows.length + const rowCount = this.state.rows.map(r => r.length) if (key === 'ArrowDown') { - if (rowCount > 0) { + if (rowCount.length > 0) { const selectedRow = findNextSelectableRow( rowCount, - { direction: 'down', row: -1 }, + { direction: 'down', row: InvalidRowIndexPath }, this.canSelectRow ) if (selectedRow != null) { @@ -544,10 +584,16 @@ export class AugmentedFilterList< event.preventDefault() } else if (key === 'ArrowUp') { - if (rowCount > 0) { + if (rowCount.length > 0) { const selectedRow = findNextSelectableRow( rowCount, - { direction: 'up', row: 0 }, + { + direction: 'up', + row: { + section: 0, + row: 0, + }, + }, this.canSelectRow ) if (selectedRow != null) { @@ -560,7 +606,7 @@ export class AugmentedFilterList< event.preventDefault() } else if (key === 'Enter') { // no repositories currently displayed, bail out - if (rowCount === 0) { + if (rowCount.length === 0) { return event.preventDefault() } @@ -572,7 +618,7 @@ export class AugmentedFilterList< const row = findNextSelectableRow( rowCount, - { direction: 'down', row: -1 }, + { direction: 'down', row: InvalidRowIndexPath }, this.canSelectRow ) @@ -589,14 +635,35 @@ export function getText( return item['text'] } +function getFirstVisibleRow( + rows: ReadonlyArray>> +): RowIndexPath { + for (let i = 0; i < rows.length; i++) { + const groupRows = rows[i] + for (let j = 0; j < groupRows.length; j++) { + const row = groupRows[j] + if (row.kind === 'item') { + return { section: i, row: j } + } + } + } + + return InvalidRowIndexPath +} + function createStateUpdate( - props: IAugmentedFilterListProps, - state: IAugmentedFilterListState | null + props: IAugmentedSectionFilterListProps, + state: IAugmentedSectionFilterListState | null ) { - const flattenedRows = new Array>() + const rows = new Array>>() const filter = (props.filterText || '').toLowerCase() + let selectedRow = InvalidRowIndexPath + let section = 0 + const selectedItem = props.selectedItem + const groupIndices = [] - for (const group of props.groups) { + for (const [idx, group] of props.groups.entries()) { + const groupRows = new Array>() const items: ReadonlyArray> = filter ? match(filter, group.items, getText) : group.items.map(item => ({ @@ -609,27 +676,31 @@ function createStateUpdate( continue } + groupIndices.push(idx) + if (props.renderGroupHeader) { - flattenedRows.push({ kind: 'group', identifier: group.identifier }) + groupRows.push({ kind: 'group', identifier: group.identifier }) } for (const { item, matches } of items) { - flattenedRows.push({ kind: 'item', item, matches }) + if (selectedItem && item.id === selectedItem.id) { + selectedRow = { + section, + row: groupRows.length, + } + } + + groupRows.push({ kind: 'item', item, matches }) } - } - let selectedRow = -1 - const selectedItem = props.selectedItem - if (selectedItem) { - selectedRow = flattenedRows.findIndex( - i => i.kind === 'item' && i.item.id === selectedItem.id - ) + rows.push(groupRows) + section++ } - if (selectedRow < 0 && filter.length) { + if (selectedRow.row < 0 && filter.length) { // If the selected item isn't in the list (e.g., filtered out), then // select the first visible item. - selectedRow = flattenedRows.findIndex(i => i.kind === 'item') + selectedRow = getFirstVisibleRow(rows) } // Stay true if already set, otherwise become true if the filter has content @@ -638,31 +709,39 @@ function createStateUpdate( : filter.length > 0 return { - rows: flattenedRows, + rows: rows, selectedRow, filterValue: filter, filterValueChanged, + groups: groupIndices, } } function getItemFromRowIndex( - items: ReadonlyArray>, - index: number + items: ReadonlyArray>>, + index: RowIndexPath ): T | null { - if (index >= 0 && index < items.length) { - const row = items[index] + if (index.section < 0 || index.section >= items.length) { + return null + } - if (row.kind === 'item') { - return row.item - } + const group = items[index.section] + if (index.row < 0 || index.row >= group.length) { + return null + } + + const row = group[index.row] + + if (row.kind === 'item') { + return row.item } return null } function getItemIdFromRowIndex( - items: ReadonlyArray>, - index: number + items: ReadonlyArray>>, + index: RowIndexPath ): string | null { const item = getItemFromRowIndex(items, index) return item ? item.id : null From bd51ca98c0c6785c3ff5583a01792fd090d8a1b4 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:52:26 -0500 Subject: [PATCH 02/12] Swap to using filtered list component (List broken till switching data model to grouped items) --- app/src/ui/changes/filter-changes-list.tsx | 73 ++++++++++++++-------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/app/src/ui/changes/filter-changes-list.tsx b/app/src/ui/changes/filter-changes-list.tsx index 8dfcab28462..0122be631db 100644 --- a/app/src/ui/changes/filter-changes-list.tsx +++ b/app/src/ui/changes/filter-changes-list.tsx @@ -18,7 +18,6 @@ import { } from '../../models/repository' import { Account } from '../../models/account' import { Author, UnknownAuthor } from '../../models/author' -import { List, ClickSource } from '../lib/list' import { Checkbox, CheckboxValue } from '../lib/checkbox' import { isSafeFileExtension, @@ -58,6 +57,7 @@ import { TooltippedContent } from '../lib/tooltipped-content' import { RepoRulesInfo } from '../../models/repo-rules' import { IAheadBehind } from '../../models/branch' import { StashDiffViewerId } from '../stashing' +import { AugmentedSectionFilterList } from '../lib/augmented-filter-list' const RowHeight = 29 const StashIcon: OcticonSymbolVariant = { @@ -128,7 +128,7 @@ interface IFilterChangesListProps { readonly conflictState: ConflictState | null readonly rebaseConflictState: RebaseConflictState | null readonly selectedFileIDs: ReadonlyArray - readonly onFileSelectionChanged: (rows: ReadonlyArray) => void + // TBD: readonly onFileSelectionChanged: (rows: ReadonlyArray) => void readonly onIncludeChanged: (path: string, include: boolean) => void readonly onSelectAll: (selectAll: boolean) => void readonly onCreateCommit: (context: ICommitContext) => Promise @@ -146,7 +146,7 @@ interface IFilterChangesListProps { readonly onChangesListScrolled: (scrollTop: number) => void /* The scrollTop of the compareList. It is stored to allow for scroll position persistence */ - readonly changesListScrollTop?: number + // TBD: readonly changesListScrollTop?: number /** * Called to open a file in its default application @@ -179,7 +179,7 @@ interface IFilterChangesListProps { * Click event handler passed directly to the onRowClick prop of List, see * List Props for documentation. */ - readonly onRowClick?: (row: number, source: ClickSource) => void + // TBD: readonly onRowClick?: (row: number, source: ClickSource) => void readonly commitMessage: ICommitMessage /** The autocompletion providers available to the repository. */ @@ -260,6 +260,9 @@ export class FilterChangesList extends React.Component< selectedRows: getSelectedRowsFromProps(props), focusedRow: null, } + + // TBD: remove with selected rows figured out + console.log(this.state.selectedRows) } public componentWillReceiveProps(nextProps: IFilterChangesListProps) { @@ -281,7 +284,8 @@ export class FilterChangesList extends React.Component< this.props.onSelectAll(include) } - private renderRow = (row: number): JSX.Element => { + // TBD: private when rendered + public renderRow = (row: number): JSX.Element => { const { workingDirectory, rebaseConflictState, @@ -341,6 +345,10 @@ export class FilterChangesList extends React.Component< ) } + private renderChangedFile = (): JSX.Element | null => { + return null + } + private onDiscardAllChanges = () => { this.props.onDiscardChangesFromFiles( this.props.workingDirectory.files, @@ -708,9 +716,10 @@ export class FilterChangesList extends React.Component< } private onItemContextMenu = ( - row: number, + item: any, event: React.MouseEvent ) => { + const row = 0 /// TBD; const { workingDirectory } = this.props const file = workingDirectory.files[row] @@ -754,7 +763,8 @@ export class FilterChangesList extends React.Component< } } - private onScroll = (scrollTop: number, clientHeight: number) => { + // TBD: make private + public onScroll = (scrollTop: number, clientHeight: number) => { this.props.onChangesListScrolled(scrollTop) } @@ -946,13 +956,15 @@ export class FilterChangesList extends React.Component< ) } - private onRowDoubleClick = (row: number) => { + // TBD: make private + public onRowDoubleClick = (row: number) => { const file = this.props.workingDirectory.files[row] this.props.onOpenItemInExternalEditor(file.path) } - private onRowKeyDown = ( + // TBD: make private + public onRowKeyDown = ( _row: number, event: React.KeyboardEvent ) => { @@ -972,6 +984,10 @@ export class FilterChangesList extends React.Component< this.includeAllCheckBoxRef.current?.focus() } + private onChangedFileClick = (item: any) => { + console.log('ChangedFileClick', item) + } + public render() { const { workingDirectory, rebaseConflictState, isCommitting } = this.props const { files } = workingDirectory @@ -1019,28 +1035,29 @@ export class FilterChangesList extends React.Component< {selectedChangesDescription} - {this.renderStashedChanges()} @@ -1049,11 +1066,13 @@ export class FilterChangesList extends React.Component< ) } - private onRowFocus = (row: number) => { + // TBD: Needs private once hooked into list + public onRowFocus = (row: number) => { this.setState({ focusedRow: row }) } - private onRowBlur = (row: number) => { + // TBD: Needs private once hooked into list + public onRowBlur = (row: number) => { if (this.state.focusedRow === row) { this.setState({ focusedRow: null }) } From 77dc7f1ed49e736c707a77c6e70178128583f55c Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:54:55 -0500 Subject: [PATCH 03/12] Add id back onto list --- app/src/ui/changes/filter-changes-list.tsx | 2 +- app/src/ui/lib/augmented-filter-list.tsx | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/ui/changes/filter-changes-list.tsx b/app/src/ui/changes/filter-changes-list.tsx index 0122be631db..3fa6e920a3f 100644 --- a/app/src/ui/changes/filter-changes-list.tsx +++ b/app/src/ui/changes/filter-changes-list.tsx @@ -1036,7 +1036,7 @@ export class FilterChangesList extends React.Component< = | IFlattenedItem interface IAugmentedSectionFilterListProps { + /** The unique identifier for the outer element of the component (optional, defaults to null) */ + readonly id?: string + /** A class name for the wrapping element. */ readonly className?: string @@ -359,6 +362,7 @@ export class AugmentedSectionFilterList< } else { return ( r.length)} rowRenderer={this.renderRow} From 3e905905f76eadbc33eb1ace21e9ef24099d9aa8 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 10 Dec 2024 14:03:30 -0500 Subject: [PATCH 04/12] Create IChangesListItem --- app/src/ui/changes/filter-changes-list.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/ui/changes/filter-changes-list.tsx b/app/src/ui/changes/filter-changes-list.tsx index 3fa6e920a3f..331e1debe21 100644 --- a/app/src/ui/changes/filter-changes-list.tsx +++ b/app/src/ui/changes/filter-changes-list.tsx @@ -58,6 +58,13 @@ import { RepoRulesInfo } from '../../models/repo-rules' import { IAheadBehind } from '../../models/branch' import { StashDiffViewerId } from '../stashing' import { AugmentedSectionFilterList } from '../lib/augmented-filter-list' +import { IFilterListItem } from '../lib/filter-list' + +interface IChangesListItem extends IFilterListItem { + readonly id: string + readonly text: ReadonlyArray + readonly change: WorkingDirectoryFileChange +} const RowHeight = 29 const StashIcon: OcticonSymbolVariant = { @@ -1035,7 +1042,7 @@ export class FilterChangesList extends React.Component< {selectedChangesDescription} - id="changes-list" rowHeight={RowHeight} filterText={undefined} // TBD: likely a prop so it can be remembered... From 9727d835733ab837c4af47c814bd557c5f3cd3c9 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 10 Dec 2024 14:04:14 -0500 Subject: [PATCH 05/12] Update renderRow => renderChangedFile to use ChangesListItem instead of row number --- app/src/ui/changes/filter-changes-list.tsx | 24 +++++++++------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/app/src/ui/changes/filter-changes-list.tsx b/app/src/ui/changes/filter-changes-list.tsx index 331e1debe21..e2462d498e8 100644 --- a/app/src/ui/changes/filter-changes-list.tsx +++ b/app/src/ui/changes/filter-changes-list.tsx @@ -235,7 +235,7 @@ interface IFilterChangesListProps { interface IFilterChangesListState { readonly selectedRows: ReadonlyArray - readonly focusedRow: number | null + readonly focusedRow: string | null } function getSelectedRowsFromProps( @@ -291,17 +291,17 @@ export class FilterChangesList extends React.Component< this.props.onSelectAll(include) } - // TBD: private when rendered - public renderRow = (row: number): JSX.Element => { + private renderChangedFile = ( + changeListItem: IChangesListItem + ): JSX.Element | null => { const { - workingDirectory, rebaseConflictState, isCommitting, onIncludeChanged, availableWidth, } = this.props - const file = workingDirectory.files[row] + const file = changeListItem.change const selection = file.selection.getSelectionType() const { submoduleStatus } = file.status @@ -347,15 +347,11 @@ export class FilterChangesList extends React.Component< availableWidth={availableWidth} disableSelection={disableSelection} checkboxTooltip={checkboxTooltip} - focused={this.state.focusedRow === row} + focused={this.state.focusedRow === changeListItem.id} /> ) } - private renderChangedFile = (): JSX.Element | null => { - return null - } - private onDiscardAllChanges = () => { this.props.onDiscardChangesFromFiles( this.props.workingDirectory.files, @@ -1074,13 +1070,13 @@ export class FilterChangesList extends React.Component< } // TBD: Needs private once hooked into list - public onRowFocus = (row: number) => { - this.setState({ focusedRow: row }) + public onRowFocus = (changeListItem: IChangesListItem) => { + this.setState({ focusedRow: changeListItem.id }) } // TBD: Needs private once hooked into list - public onRowBlur = (row: number) => { - if (this.state.focusedRow === row) { + public onRowBlur = (changeListItem: IChangesListItem) => { + if (this.state.focusedRow === changeListItem.id) { this.setState({ focusedRow: null }) } } From 3a2bea2a1ac62256bee28e9e75fa67b8ec108044 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 10 Dec 2024 14:06:23 -0500 Subject: [PATCH 06/12] Create groups on state --- app/src/ui/changes/filter-changes-list.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/ui/changes/filter-changes-list.tsx b/app/src/ui/changes/filter-changes-list.tsx index e2462d498e8..2211a513f7f 100644 --- a/app/src/ui/changes/filter-changes-list.tsx +++ b/app/src/ui/changes/filter-changes-list.tsx @@ -58,7 +58,7 @@ import { RepoRulesInfo } from '../../models/repo-rules' import { IAheadBehind } from '../../models/branch' import { StashDiffViewerId } from '../stashing' import { AugmentedSectionFilterList } from '../lib/augmented-filter-list' -import { IFilterListItem } from '../lib/filter-list' +import { IFilterListGroup, IFilterListItem } from '../lib/filter-list' interface IChangesListItem extends IFilterListItem { readonly id: string @@ -236,6 +236,7 @@ interface IFilterChangesListProps { interface IFilterChangesListState { readonly selectedRows: ReadonlyArray readonly focusedRow: string | null + readonly groups: ReadonlyArray> } function getSelectedRowsFromProps( @@ -266,6 +267,7 @@ export class FilterChangesList extends React.Component< this.state = { selectedRows: getSelectedRowsFromProps(props), focusedRow: null, + groups: [], } // TBD: remove with selected rows figured out @@ -1053,7 +1055,7 @@ export class FilterChangesList extends React.Component< // setScrollTop={this.props.changesListScrollTop} // onRowKeyDown={this.onRowKeyDown} onSelectionChanged={undefined} // this.props.onFileSelectionChanged - groups={[]} // + groups={this.state.groups} // invalidationProps={{ workingDirectory: workingDirectory, isCommitting: isCommitting, From 9aec3adf7796bef7845944385e0847b2b6f66dda Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 10 Dec 2024 14:27:24 -0500 Subject: [PATCH 07/12] Create grouped items (Can see list items again!) --- app/src/ui/changes/filter-changes-list.tsx | 25 ++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/app/src/ui/changes/filter-changes-list.tsx b/app/src/ui/changes/filter-changes-list.tsx index 2211a513f7f..d9a725d7adf 100644 --- a/app/src/ui/changes/filter-changes-list.tsx +++ b/app/src/ui/changes/filter-changes-list.tsx @@ -264,10 +264,13 @@ export class FilterChangesList extends React.Component< public constructor(props: IFilterChangesListProps) { super(props) + + const groups = [this.createListItems(props.workingDirectory.files)] + this.state = { selectedRows: getSelectedRowsFromProps(props), focusedRow: null, - groups: [], + groups, } // TBD: remove with selected rows figured out @@ -284,7 +287,25 @@ export class FilterChangesList extends React.Component< this.props.workingDirectory.files ) ) { - this.setState({ selectedRows: getSelectedRowsFromProps(nextProps) }) + this.setState({ + selectedRows: getSelectedRowsFromProps(nextProps), + groups: [this.createListItems(nextProps.workingDirectory.files)], + }) + } + } + + private createListItems( + files: ReadonlyArray + ): IFilterListGroup { + const items = files.map(file => ({ + text: [file.path, file.status.kind.toString()], + id: file.id, + change: file, + })) + + return { + identifier: 'changed-files', + items, } } From e2b77a7ab2f37c59f369223b3b2a93d6ea5017f0 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 10 Dec 2024 14:31:38 -0500 Subject: [PATCH 08/12] Add filter text to state (filter works!) --- app/src/ui/changes/filter-changes-list.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/ui/changes/filter-changes-list.tsx b/app/src/ui/changes/filter-changes-list.tsx index d9a725d7adf..907b4c79f6c 100644 --- a/app/src/ui/changes/filter-changes-list.tsx +++ b/app/src/ui/changes/filter-changes-list.tsx @@ -234,6 +234,7 @@ interface IFilterChangesListProps { } interface IFilterChangesListState { + readonly filterText: string readonly selectedRows: ReadonlyArray readonly focusedRow: string | null readonly groups: ReadonlyArray> @@ -268,6 +269,7 @@ export class FilterChangesList extends React.Component< const groups = [this.createListItems(props.workingDirectory.files)] this.state = { + filterText: '', selectedRows: getSelectedRowsFromProps(props), focusedRow: null, groups, @@ -1014,6 +1016,10 @@ export class FilterChangesList extends React.Component< console.log('ChangedFileClick', item) } + private onFilterTextChanged = (text: string) => { + this.setState({ filterText: text }) + } + public render() { const { workingDirectory, rebaseConflictState, isCommitting } = this.props const { files } = workingDirectory @@ -1064,8 +1070,8 @@ export class FilterChangesList extends React.Component< id="changes-list" rowHeight={RowHeight} - filterText={undefined} // TBD: likely a prop so it can be remembered... - onFilterTextChanged={undefined} // TBD: likely update store state + filterText={this.state.filterText} + onFilterTextChanged={this.onFilterTextChanged} selectedItem={null} // selectedRows={this.state.selectedRows} need multi selection // selectionMode="multi"... renderItem={this.renderChangedFile} //rowRenderer={this.renderRow} onItemClick={this.onChangedFileClick} // onRowClick={this.props.onRowClick} From 4707212b73c86c8441980f17574da42d2acee772 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 10 Dec 2024 15:02:59 -0500 Subject: [PATCH 09/12] Wire up checking/unchecking with spacebar --- app/src/ui/changes/filter-changes-list.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/app/src/ui/changes/filter-changes-list.tsx b/app/src/ui/changes/filter-changes-list.tsx index 907b4c79f6c..5a0da8a801c 100644 --- a/app/src/ui/changes/filter-changes-list.tsx +++ b/app/src/ui/changes/filter-changes-list.tsx @@ -59,6 +59,7 @@ import { IAheadBehind } from '../../models/branch' import { StashDiffViewerId } from '../stashing' import { AugmentedSectionFilterList } from '../lib/augmented-filter-list' import { IFilterListGroup, IFilterListItem } from '../lib/filter-list' +import { ClickSource } from '../lib/list' interface IChangesListItem extends IFilterListItem { readonly id: string @@ -186,7 +187,7 @@ interface IFilterChangesListProps { * Click event handler passed directly to the onRowClick prop of List, see * List Props for documentation. */ - // TBD: readonly onRowClick?: (row: number, source: ClickSource) => void + readonly onRowClick?: (row: number, source: ClickSource) => void readonly commitMessage: ICommitMessage /** The autocompletion providers available to the repository. */ @@ -1012,8 +1013,14 @@ export class FilterChangesList extends React.Component< this.includeAllCheckBoxRef.current?.focus() } - private onChangedFileClick = (item: any) => { - console.log('ChangedFileClick', item) + private onChangedFileClick = ( + item: IChangesListItem, + source: ClickSource + ) => { + const fileIndex = this.props.workingDirectory.files.findIndex( + f => f.id === item.change.id + ) + this.props.onRowClick?.(fileIndex, source) } private onFilterTextChanged = (text: string) => { @@ -1073,8 +1080,8 @@ export class FilterChangesList extends React.Component< filterText={this.state.filterText} onFilterTextChanged={this.onFilterTextChanged} selectedItem={null} // selectedRows={this.state.selectedRows} need multi selection // selectionMode="multi"... - renderItem={this.renderChangedFile} //rowRenderer={this.renderRow} - onItemClick={this.onChangedFileClick} // onRowClick={this.props.onRowClick} + renderItem={this.renderChangedFile} + onItemClick={this.onChangedFileClick} // onRowDoubleClick={this.onRowDoubleClick} // onRowKeyboardFocus={this.onRowFocus} // onRowBlur={this.onRowBlur} From 85a09d7e1a749d24fee80d59f34889f706fb302e Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 10 Dec 2024 15:55:33 -0500 Subject: [PATCH 10/12] Set selectedItem from selected Ids --- app/src/ui/changes/filter-changes-list.tsx | 26 +++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/app/src/ui/changes/filter-changes-list.tsx b/app/src/ui/changes/filter-changes-list.tsx index 5a0da8a801c..7c6fa7e7f22 100644 --- a/app/src/ui/changes/filter-changes-list.tsx +++ b/app/src/ui/changes/filter-changes-list.tsx @@ -237,6 +237,7 @@ interface IFilterChangesListProps { interface IFilterChangesListState { readonly filterText: string readonly selectedRows: ReadonlyArray + readonly selectedItem: IChangesListItem | null readonly focusedRow: string | null readonly groups: ReadonlyArray> } @@ -257,6 +258,26 @@ function getSelectedRowsFromProps( return selectedRows } +function getSelectedItemFromProps( + props: IFilterChangesListProps +): IChangesListItem | null { + if (props.selectedFileIDs.length === 0) { + return null + } + + const file = props.workingDirectory.findFileWithID(props.selectedFileIDs[0]) + + if (!file) { + return null + } + + return { + text: [file.path, file.status.kind.toString()], + id: file.id, + change: file, + } +} + export class FilterChangesList extends React.Component< IFilterChangesListProps, IFilterChangesListState @@ -272,6 +293,8 @@ export class FilterChangesList extends React.Component< this.state = { filterText: '', selectedRows: getSelectedRowsFromProps(props), + // TBD: should be selectedItem(s) but section list doesn't support that yet. + selectedItem: getSelectedItemFromProps(props), focusedRow: null, groups, } @@ -292,6 +315,7 @@ export class FilterChangesList extends React.Component< ) { this.setState({ selectedRows: getSelectedRowsFromProps(nextProps), + selectedItem: getSelectedItemFromProps(nextProps), groups: [this.createListItems(nextProps.workingDirectory.files)], }) } @@ -1079,7 +1103,7 @@ export class FilterChangesList extends React.Component< rowHeight={RowHeight} filterText={this.state.filterText} onFilterTextChanged={this.onFilterTextChanged} - selectedItem={null} // selectedRows={this.state.selectedRows} need multi selection // selectionMode="multi"... + selectedItem={this.state.selectedItem} // selectedRows={this.state.selectedRows} need multi selection // selectionMode="multi"... renderItem={this.renderChangedFile} onItemClick={this.onChangedFileClick} // onRowDoubleClick={this.onRowDoubleClick} From b4405b664cab8c4279c1922d6ca538729340c564 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:06:16 -0500 Subject: [PATCH 11/12] Wire up selection change again! (single for now..) --- app/src/ui/changes/filter-changes-list.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/app/src/ui/changes/filter-changes-list.tsx b/app/src/ui/changes/filter-changes-list.tsx index 7c6fa7e7f22..11bf498ca48 100644 --- a/app/src/ui/changes/filter-changes-list.tsx +++ b/app/src/ui/changes/filter-changes-list.tsx @@ -136,7 +136,7 @@ interface IFilterChangesListProps { readonly conflictState: ConflictState | null readonly rebaseConflictState: RebaseConflictState | null readonly selectedFileIDs: ReadonlyArray - // TBD: readonly onFileSelectionChanged: (rows: ReadonlyArray) => void + readonly onFileSelectionChanged: (rows: ReadonlyArray) => void readonly onIncludeChanged: (path: string, include: boolean) => void readonly onSelectAll: (selectAll: boolean) => void readonly onCreateCommit: (context: ICommitContext) => Promise @@ -1051,6 +1051,17 @@ export class FilterChangesList extends React.Component< this.setState({ filterText: text }) } + private onFileSelectionChanged = (item: IChangesListItem | null) => { + const rows = item + ? [ + this.props.workingDirectory.files.findIndex( + f => f.id === item.change.id + ), + ] + : [] + this.props.onFileSelectionChanged(rows) + } + public render() { const { workingDirectory, rebaseConflictState, isCommitting } = this.props const { files } = workingDirectory @@ -1103,16 +1114,17 @@ export class FilterChangesList extends React.Component< rowHeight={RowHeight} filterText={this.state.filterText} onFilterTextChanged={this.onFilterTextChanged} - selectedItem={this.state.selectedItem} // selectedRows={this.state.selectedRows} need multi selection // selectionMode="multi"... + selectedItem={this.state.selectedItem} renderItem={this.renderChangedFile} onItemClick={this.onChangedFileClick} + // selectionMode="multi"... // onRowDoubleClick={this.onRowDoubleClick} // onRowKeyboardFocus={this.onRowFocus} // onRowBlur={this.onRowBlur} // onScroll={this.onScroll} // setScrollTop={this.props.changesListScrollTop} // onRowKeyDown={this.onRowKeyDown} - onSelectionChanged={undefined} // this.props.onFileSelectionChanged + onSelectionChanged={this.onFileSelectionChanged} groups={this.state.groups} // invalidationProps={{ workingDirectory: workingDirectory, From 78171435d20fec0751ae97711b9de94aa62fb90c Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 19 Dec 2024 07:53:34 -0500 Subject: [PATCH 12/12] Use findFileIndexByID --- app/src/ui/changes/filter-changes-list.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/app/src/ui/changes/filter-changes-list.tsx b/app/src/ui/changes/filter-changes-list.tsx index 11bf498ca48..011b90026d3 100644 --- a/app/src/ui/changes/filter-changes-list.tsx +++ b/app/src/ui/changes/filter-changes-list.tsx @@ -1041,9 +1041,10 @@ export class FilterChangesList extends React.Component< item: IChangesListItem, source: ClickSource ) => { - const fileIndex = this.props.workingDirectory.files.findIndex( - f => f.id === item.change.id + const fileIndex = this.props.workingDirectory.findFileIndexByID( + item.change.id ) + this.props.onRowClick?.(fileIndex, source) } @@ -1053,11 +1054,7 @@ export class FilterChangesList extends React.Component< private onFileSelectionChanged = (item: IChangesListItem | null) => { const rows = item - ? [ - this.props.workingDirectory.files.findIndex( - f => f.id === item.change.id - ), - ] + ? [this.props.workingDirectory.findFileIndexByID(item.change.id)] : [] this.props.onFileSelectionChanged(rows) }