diff --git a/app/src/lib/feature-flag.ts b/app/src/lib/feature-flag.ts index 585ad70590..4a5625d8ea 100644 --- a/app/src/lib/feature-flag.ts +++ b/app/src/lib/feature-flag.ts @@ -96,3 +96,7 @@ export function enablePullRequestQuickView(): boolean { export function enableMoveStash(): boolean { return enableBetaFeatures() } + +export function enableSectionList(): boolean { + return enableDevelopmentFeatures() +} diff --git a/app/src/models/drag-drop.ts b/app/src/models/drag-drop.ts index 95003e6241..18b75d75f4 100644 --- a/app/src/models/drag-drop.ts +++ b/app/src/models/drag-drop.ts @@ -1,3 +1,4 @@ +import { RowIndexPath } from '../ui/lib/list/list-row-index-path' import { Commit } from './commit' import { GitHubRepository } from './github-repository' @@ -51,7 +52,7 @@ export type CommitTarget = { export type ListInsertionPointTarget = { type: DropTargetType.ListInsertionPoint data: DragData - index: number + index: RowIndexPath } /** diff --git a/app/src/ui/branches/branch-list.tsx b/app/src/ui/branches/branch-list.tsx index dfd54f732b..3c71ed1c79 100644 --- a/app/src/ui/branches/branch-list.tsx +++ b/app/src/ui/branches/branch-list.tsx @@ -22,6 +22,8 @@ import { NoBranches } from './no-branches' import { SelectionDirection, ClickSource } from '../lib/list' import { generateBranchContextMenuItems } from './branch-list-item-context-menu' import { showContextualMenu } from '../../lib/menu-item' +import { enableSectionList } from '../../lib/feature-flag' +import { SectionFilterList } from '../lib/section-filter-list' const RowHeight = 30 @@ -168,7 +170,10 @@ export class BranchList extends React.Component< IBranchListProps, IBranchListState > { - private branchFilterList: FilterList | null = null + private branchFilterList: + | FilterList + | SectionFilterList + | null = null public constructor(props: IBranchListProps) { super(props) @@ -186,7 +191,32 @@ export class BranchList extends React.Component< } public render() { - return ( + return enableSectionList() ? ( + + ref={this.onBranchesFilterListRef} + className="branches-list" + rowHeight={RowHeight} + filterText={this.props.filterText} + onFilterTextChanged={this.props.onFilterTextChanged} + onFilterKeyDown={this.props.onFilterKeyDown} + selectedItem={this.state.selectedItem} + renderItem={this.renderItem} + renderGroupHeader={this.renderGroupHeader} + onItemClick={this.onItemClick} + onSelectionChanged={this.onSelectionChanged} + onEnterPressedWithoutFilteredItems={this.onCreateNewBranch} + groups={this.state.groups} + invalidationProps={this.props.allBranches} + renderPostFilter={this.onRenderNewButton} + renderNoItems={this.onRenderNoItems} + filterTextBox={this.props.textbox} + hideFilterRow={this.props.hideFilterRow} + onFilterListResultsChanged={this.props.onFilterListResultsChanged} + renderPreList={this.props.renderPreList} + onItemContextMenu={this.onBranchContextMenu} + getGroupAriaLabel={this.getGroupAriaLabel} + /> + ) : ( ref={this.onBranchesFilterListRef} className="branches-list" @@ -237,7 +267,10 @@ export class BranchList extends React.Component< } private onBranchesFilterListRef = ( - filterList: FilterList | null + filterList: + | FilterList + | SectionFilterList + | null ) => { this.branchFilterList = filterList } @@ -257,6 +290,15 @@ export class BranchList extends React.Component< } } + private getGroupAriaLabel = (group: number) => { + const GroupIdentifiers: ReadonlyArray = [ + 'default', + 'recent', + 'other', + ] + return this.getGroupLabel(GroupIdentifiers[group]) + } + private renderGroupHeader = (label: string) => { const identifier = this.parseHeader(label) diff --git a/app/src/ui/clone-repository/cloneable-repository-filter-list.tsx b/app/src/ui/clone-repository/cloneable-repository-filter-list.tsx index 50a0293c5a..d4c055c484 100644 --- a/app/src/ui/clone-repository/cloneable-repository-filter-list.tsx +++ b/app/src/ui/clone-repository/cloneable-repository-filter-list.tsx @@ -15,6 +15,8 @@ import { HighlightText } from '../lib/highlight-text' import { ClickSource } from '../lib/list' import { LinkButton } from '../lib/link-button' import { Ref } from '../lib/ref' +import { enableSectionList } from '../../lib/feature-flag' +import { SectionFilterList } from '../lib/section-filter-list' interface ICloneableRepositoryFilterListProps { /** The account to clone from. */ @@ -157,26 +159,34 @@ export class CloneableRepositoryFilterList extends React.PureComponent { + const groupIdentifier = groups[group].identifier + return groupIdentifier === YourRepositoriesIdentifier + ? this.getYourRepositoriesLabel() + : groupIdentifier + } - return ( - - className="clone-github-repo" - rowHeight={RowHeight} - selectedItem={selectedListItem} - renderItem={this.renderItem} - renderGroupHeader={this.renderGroupHeader} - onSelectionChanged={this.onSelectionChanged} - invalidationProps={groups} - groups={groups} - filterText={this.props.filterText} - onFilterTextChanged={this.props.onFilterTextChanged} - renderNoItems={this.renderNoItems} - renderPostFilter={this.renderPostFilter} - onItemClick={this.props.onItemClicked ? this.onItemClick : undefined} - placeholderText="Filter your repositories" - /> - ) + const selectedListItem = this.getSelectedListItem(groups, selectedItem) + const ListComponent = enableSectionList() ? SectionFilterList : FilterList + const filterListProps: typeof ListComponent['prototype']['props'] = { + className: 'clone-github-repo', + rowHeight: RowHeight, + selectedItem: selectedListItem, + renderItem: this.renderItem, + renderGroupHeader: this.renderGroupHeader, + onSelectionChanged: this.onSelectionChanged, + invalidationProps: groups, + groups: groups, + filterText: this.props.filterText, + onFilterTextChanged: this.props.onFilterTextChanged, + renderNoItems: this.renderNoItems, + renderPostFilter: this.renderPostFilter, + onItemClick: this.props.onItemClicked ? this.onItemClick : undefined, + placeholderText: 'Filter your repositories', + getGroupAriaLabel, + } + + return } private onItemClick = ( @@ -206,10 +216,14 @@ export class CloneableRepositoryFilterList extends React.PureComponent { + return __DARWIN__ ? 'Your Repositories' : 'Your repositories' + } + private renderGroupHeader = (identifier: string) => { let header = identifier if (identifier === YourRepositoriesIdentifier) { - header = __DARWIN__ ? 'Your Repositories' : 'Your repositories' + header = this.getYourRepositoriesLabel() } return (
diff --git a/app/src/ui/lib/list/list-item-insertion-overlay.tsx b/app/src/ui/lib/list/list-item-insertion-overlay.tsx index ac06b30de6..b85878d3e6 100644 --- a/app/src/ui/lib/list/list-item-insertion-overlay.tsx +++ b/app/src/ui/lib/list/list-item-insertion-overlay.tsx @@ -4,6 +4,7 @@ import { Disposable } from 'event-kit' import * as React from 'react' import { dragAndDropManager } from '../../../lib/drag-and-drop-manager' import { DragData, DragType, DropTargetType } from '../../../models/drag-drop' +import { RowIndexPath } from './list-row-index-path' enum InsertionFeedbackType { None, @@ -13,11 +14,11 @@ enum InsertionFeedbackType { interface IListItemInsertionOverlayProps { readonly onDropDataInsertion?: ( - insertionIndex: number, + insertionIndex: RowIndexPath, data: DragData ) => void - readonly itemIndex: number + readonly itemIndex: RowIndexPath readonly dragType: DragType } @@ -188,7 +189,10 @@ export class ListItemInsertionOverlay extends React.PureComponent< let index = this.props.itemIndex if (this.state.feedbackType === InsertionFeedbackType.Bottom) { - index++ + index = { + ...index, + row: index.row + 1, + } } this.props.onDropDataInsertion(index, dragAndDropManager.dragData) } diff --git a/app/src/ui/lib/list/list-row-index-path.ts b/app/src/ui/lib/list/list-row-index-path.ts new file mode 100644 index 0000000000..20b40044ae --- /dev/null +++ b/app/src/ui/lib/list/list-row-index-path.ts @@ -0,0 +1,92 @@ +export type RowIndexPath = { + readonly row: number + readonly section: number +} + +export const InvalidRowIndexPath: RowIndexPath = { section: -1, row: -1 } + +export function rowIndexPathEquals(a: RowIndexPath, b: RowIndexPath): boolean { + return a.section === b.section && a.row === b.row +} + +export function getTotalRowCount(rowCount: ReadonlyArray) { + return rowCount.reduce((sum, count) => sum + count, 0) +} + +export function rowIndexPathToGlobalIndex( + indexPath: RowIndexPath, + rowCount: ReadonlyArray +): number | null { + if (!isValidRow(indexPath, rowCount)) { + return null + } + + let index = 0 + + for (let section = 0; section < indexPath.section; section++) { + index += rowCount[section] + } + + index += indexPath.row + + return index +} + +export function globalIndexToRowIndexPath( + index: number, + rowCount: ReadonlyArray +): RowIndexPath | null { + if (index < 0 || index >= getTotalRowCount(rowCount)) { + return null + } + + let section = 0 + let row = index + + while (row >= rowCount[section]) { + row -= rowCount[section] + section++ + } + + return { section, row } +} + +export function isValidRow( + indexPath: RowIndexPath, + rowCount: ReadonlyArray +) { + return ( + indexPath.section >= 0 && + indexPath.section < rowCount.length && + indexPath.row >= 0 && + indexPath.row < rowCount[indexPath.section] + ) +} + +export function getFirstRowIndexPath( + rowCount: ReadonlyArray +): RowIndexPath | null { + if (rowCount.length > 0) { + for (let section = 0; section < rowCount.length; section++) { + if (rowCount[section] > 0) { + return { section, row: 0 } + } + } + } + + return null +} + +export function getLastRowIndexPath( + rowCount: ReadonlyArray +): RowIndexPath | null { + if (rowCount.length > 0) { + for (let section = rowCount.length - 1; section >= 0; section--) { + if (rowCount[section] > 0) { + return { section, row: rowCount[section] - 1 } + } + } + } + + return null +} diff --git a/app/src/ui/lib/list/list-row.tsx b/app/src/ui/lib/list/list-row.tsx index a001aeb5cd..27b04557e5 100644 --- a/app/src/ui/lib/list/list-row.tsx +++ b/app/src/ui/lib/list/list-row.tsx @@ -1,12 +1,16 @@ import * as React from 'react' import classNames from 'classnames' +import { RowIndexPath } from './list-row-index-path' interface IListRowProps { + /** whether or not the section to which this row belongs has a header */ + readonly sectionHasHeader: boolean + /** the total number of row in this list */ readonly rowCount: number /** the index of the row in the list */ - readonly rowIndex: number + readonly rowIndex: RowIndexPath /** custom styles to provide to the row */ readonly style?: React.CSSProperties @@ -21,39 +25,51 @@ interface IListRowProps { readonly selected?: boolean /** callback to fire when the DOM element is created */ - readonly onRowRef?: (index: number, element: HTMLDivElement | null) => void + readonly onRowRef?: ( + index: RowIndexPath, + element: HTMLDivElement | null + ) => void /** callback to fire when the row receives a mousedown event */ - readonly onRowMouseDown: (index: number, e: React.MouseEvent) => void + readonly onRowMouseDown: ( + index: RowIndexPath, + e: React.MouseEvent + ) => void /** callback to fire when the row receives a mouseup event */ - readonly onRowMouseUp: (index: number, e: React.MouseEvent) => void + readonly onRowMouseUp: (index: RowIndexPath, e: React.MouseEvent) => void /** callback to fire when the row is clicked */ - readonly onRowClick: (index: number, e: React.MouseEvent) => void + readonly onRowClick: (index: RowIndexPath, e: React.MouseEvent) => void /** callback to fire when the row is double clicked */ - readonly onRowDoubleClick: (index: number, e: React.MouseEvent) => void + readonly onRowDoubleClick: ( + index: RowIndexPath, + e: React.MouseEvent + ) => void /** callback to fire when the row receives a keyboard event */ - readonly onRowKeyDown: (index: number, e: React.KeyboardEvent) => void + readonly onRowKeyDown: ( + index: RowIndexPath, + e: React.KeyboardEvent + ) => void /** called when the row (or any of its descendants) receives focus */ readonly onRowFocus?: ( - index: number, + index: RowIndexPath, e: React.FocusEvent ) => void /** called when the row (and all of its descendants) loses focus */ readonly onRowBlur?: ( - index: number, + index: RowIndexPath, e: React.FocusEvent ) => void /** Called back for when the context menu is invoked (user right clicks of * uses keyboard shortcuts) */ readonly onContextMenu?: ( - index: number, + index: RowIndexPath, e: React.MouseEvent ) => void @@ -106,12 +122,23 @@ export class ListRow extends React.Component { } public render() { - const selected = this.props.selected - const className = classNames( + const { + selected, + selectable, + className, + style, + rowCount, + id, + tabIndex, + rowIndex, + children, + sectionHasHeader, + } = this.props + const rowClassName = classNames( 'list-item', { selected }, - { 'not-selectable': this.props.selectable === false }, - this.props.className + { 'not-selectable': selectable === false }, + className ) // react-virtualized gives us an explicit pixel width for rows, but that // width doesn't take into account whether or not the scroll bar needs @@ -120,29 +147,43 @@ export class ListRow extends React.Component { // *But* the parent Grid uses `autoContainerWidth` which means its width // *does* reflect any width needed by the scroll bar. So we should just use // that width. - const style = { ...this.props.style, width: '100%' } + const fullWidthStyle = { ...style, width: '100%' } + + let ariaSetSize: number | undefined = rowCount + let ariaPosInSet: number | undefined = rowIndex.row + 1 + if (sectionHasHeader) { + if (rowIndex.row === 0) { + ariaSetSize = undefined + ariaPosInSet = undefined + } else { + ariaSetSize -= 1 + ariaPosInSet -= 1 + } + } return (
- {this.props.children} + {children}
) } diff --git a/app/src/ui/lib/list/list.tsx b/app/src/ui/lib/list/list.tsx index 48fec4d29c..6305ffa0da 100644 --- a/app/src/ui/lib/list/list.tsx +++ b/app/src/ui/lib/list/list.tsx @@ -18,6 +18,7 @@ import { range } from '../../../lib/range' import { ListItemInsertionOverlay } from './list-item-insertion-overlay' import { DragData, DragType } from '../../../models/drag-drop' import memoizeOne from 'memoize-one' +import { RowIndexPath } from './list-row-index-path' import { sendNonFatalException } from '../../../lib/helpers/non-fatal-exception' /** @@ -580,11 +581,11 @@ export class List extends React.Component { } private onRowKeyDown = ( - rowIndex: number, + indexPath: RowIndexPath, event: React.KeyboardEvent ) => { if (this.props.onRowKeyDown) { - this.props.onRowKeyDown(rowIndex, event) + this.props.onRowKeyDown(indexPath.row, event) } const hasModifier = @@ -645,21 +646,27 @@ export class List extends React.Component { }) } - private onRowFocus = (index: number, e: React.FocusEvent) => { - this.focusRow = index + private onRowFocus = ( + indexPath: RowIndexPath, + e: React.FocusEvent + ) => { + this.focusRow = indexPath.row } - private onRowBlur = (index: number, e: React.FocusEvent) => { - if (this.focusRow === index) { + private onRowBlur = ( + indexPath: RowIndexPath, + e: React.FocusEvent + ) => { + if (this.focusRow === indexPath.row) { this.focusRow = -1 } } private onRowContextMenu = ( - row: number, + indexPath: RowIndexPath, e: React.MouseEvent ) => { - this.props.onRowContextMenu?.(row, e) + this.props.onRowContextMenu?.(indexPath.row, e) } /** Convenience method for invoking canSelectRow callback when it exists */ @@ -867,14 +874,17 @@ export class List extends React.Component { } } - private onRowRef = (rowIndex: number, element: HTMLDivElement | null) => { + private onRowRef = ( + indexPath: RowIndexPath, + element: HTMLDivElement | null + ) => { if (element === null) { - this.rowRefs.delete(rowIndex) + this.rowRefs.delete(indexPath.row) } else { - this.rowRefs.set(rowIndex, element) + this.rowRefs.set(indexPath.row, element) } - if (rowIndex === this.focusRow) { + if (indexPath.row === this.focusRow) { // The currently focused row is going being unmounted so we'll move focus // programmatically to the grid so that keyboard navigation still works if (element === null) { @@ -925,8 +935,8 @@ export class List extends React.Component { const element = this.props.insertionDragType !== undefined ? ( {row} @@ -943,7 +953,8 @@ export class List extends React.Component { id={id} onRowRef={this.onRowRef} rowCount={this.props.rowCount} - rowIndex={rowIndex} + rowIndex={{ section: 0, row: rowIndex }} + sectionHasHeader={false} selected={selected} onRowClick={this.onRowClick} onRowDoubleClick={this.onRowDoubleClick} @@ -1144,7 +1155,12 @@ export class List extends React.Component { } } - private onRowMouseDown = (row: number, event: React.MouseEvent) => { + private onRowMouseDown = ( + indexPath: RowIndexPath, + event: React.MouseEvent + ) => { + const { row } = indexPath + if (this.canSelectRow(row)) { if (this.props.onRowMouseDown) { this.props.onRowMouseDown(row, event) @@ -1237,7 +1253,12 @@ export class List extends React.Component { } } - private onRowMouseUp = (row: number, event: React.MouseEvent) => { + private onRowMouseUp = ( + indexPath: RowIndexPath, + event: React.MouseEvent + ) => { + const { row } = indexPath + if (!this.canSelectRow(row)) { return } @@ -1299,27 +1320,37 @@ export class List extends React.Component { } } - private onRowClick = (row: number, event: React.MouseEvent) => { - if (this.canSelectRow(row) && this.props.onRowClick) { + private onDropDataInsertion = (indexPath: RowIndexPath, data: DragData) => { + this.props.onDropDataInsertion?.(indexPath.row, data) + } + + private onRowClick = ( + indexPath: RowIndexPath, + event: React.MouseEvent + ) => { + if (this.canSelectRow(indexPath.row) && this.props.onRowClick) { const rowCount = this.props.rowCount - if (row < 0 || row >= rowCount) { + if (indexPath.row < 0 || indexPath.row >= rowCount) { log.debug( - `[List.onRowClick] unable to onRowClick for row ${row} as it is outside the bounds of the array [0, ${rowCount}]` + `[List.onRowClick] unable to onRowClick for row ${indexPath.row} as it is outside the bounds of the array [0, ${rowCount}]` ) return } - this.props.onRowClick(row, { kind: 'mouseclick', event }) + this.props.onRowClick(indexPath.row, { kind: 'mouseclick', event }) } } - private onRowDoubleClick = (row: number, event: React.MouseEvent) => { + private onRowDoubleClick = ( + indexPath: RowIndexPath, + event: React.MouseEvent + ) => { if (!this.props.onRowDoubleClick) { return } - this.props.onRowDoubleClick(row, { kind: 'mouseclick', event }) + this.props.onRowDoubleClick(indexPath.row, { kind: 'mouseclick', event }) } private onScroll = ({ diff --git a/app/src/ui/lib/list/section-list-selection.ts b/app/src/ui/lib/list/section-list-selection.ts new file mode 100644 index 0000000000..87e1473bdd --- /dev/null +++ b/app/src/ui/lib/list/section-list-selection.ts @@ -0,0 +1,191 @@ +import * as React from 'react' +import { + getTotalRowCount, + globalIndexToRowIndexPath, + InvalidRowIndexPath, + isValidRow, + RowIndexPath, + rowIndexPathEquals, + rowIndexPathToGlobalIndex, +} from './list-row-index-path' + +export type SelectionDirection = 'up' | 'down' + +interface ISelectRowAction { + /** + * The vertical direction use when searching for a selectable row. + */ + readonly direction: SelectionDirection + + /** + * The starting row index to search from. + */ + readonly row: RowIndexPath + + /** + * A flag to indicate or not to look beyond the last or first + * row (depending on direction) such that given the last row and + * a downward direction will consider the first row as a + * candidate or given the first row and an upward direction + * will consider the last row as a candidate. + * + * Defaults to true if not set. + */ + readonly wrap?: boolean +} + +/** + * Interface describing a user initiated selection change event + * originating from a pointer device clicking or pressing on an item. + */ +export interface IMouseClickSource { + readonly kind: 'mouseclick' + readonly event: React.MouseEvent +} + +/** + * Interface describing a user initiated selection change event + * originating from a pointer device hovering over an item. + * Only applicable when selectedOnHover is set. + */ +export interface IHoverSource { + readonly kind: 'hover' + readonly event: React.MouseEvent +} + +/** + * Interface describing a user initiated selection change event + * originating from a keyboard + */ +export interface IKeyboardSource { + readonly kind: 'keyboard' + readonly event: React.KeyboardEvent +} + +/** + * Interface describing a user initiated selection of all list + * items (usually by clicking the Edit > Select all menu item in + * the application window). This is highly specific to GitHub Desktop + */ +export interface ISelectAllSource { + readonly kind: 'select-all' +} + +/** A type union of possible sources of a selection changed event */ +export type SelectionSource = + | IMouseClickSource + | IHoverSource + | IKeyboardSource + | ISelectAllSource + +/** + * Determine the next selectable row, given the direction and a starting + * row index. Whether a row is selectable or not is determined using + * the `canSelectRow` function, which defaults to true if not provided. + * + * Returns null if no row can be selected or if the only selectable row is + * identical to the given row parameter. + */ +export function findNextSelectableRow( + rowCount: ReadonlyArray, + action: ISelectRowAction, + canSelectRow: (indexPath: RowIndexPath) => boolean = row => true +): RowIndexPath | null { + const totalRowCount = getTotalRowCount(rowCount) + if (totalRowCount === 0) { + return null + } + + const { direction, row } = action + const wrap = action.wrap === undefined ? true : action.wrap + const rowIndex = rowIndexPathEquals(InvalidRowIndexPath, row) + ? -1 + : rowIndexPathToGlobalIndex(row, rowCount) + + if (rowIndex === null) { + return null + } + + // Ensure the row value is in the range between 0 and rowCount - 1 + // + // If the row falls outside this range, use the direction + // given to choose a suitable value: + // + // - move in an upward direction -> select last row + // - move in a downward direction -> select first row + // + let currentRow = isValidRow(row, rowCount) + ? rowIndex + : direction === 'up' + ? totalRowCount - 1 + : 0 + + // handle specific case from switching from filter text to list + // + // locking currentRow to [0,rowCount) above means that the below loops + // will skip over the first entry + if (direction === 'down' && rowIndexPathEquals(row, InvalidRowIndexPath)) { + currentRow = -1 + } + + const delta = direction === 'up' ? -1 : 1 + + // Iterate through all rows (starting offset from the + // given row and ending on and including the given row) + for (let i = 0; i < totalRowCount; i++) { + currentRow += delta + + if (currentRow >= totalRowCount) { + // We've hit rock bottom, wrap around to the top + // if we're allowed to or give up. + if (wrap) { + currentRow = 0 + } else { + break + } + } else if (currentRow < 0) { + // We've reached the top, wrap around to the bottom + // if we're allowed to or give up + if (wrap) { + currentRow = totalRowCount - 1 + } else { + break + } + } + + const currentRowIndexPath = globalIndexToRowIndexPath(currentRow, rowCount) + if ( + currentRowIndexPath !== null && + !rowIndexPathEquals(row, currentRowIndexPath) && + canSelectRow(currentRowIndexPath) + ) { + return currentRowIndexPath + } + } + + return null +} + +/** + * Find the last selectable row in either direction, used + * for moving to the first or last selectable row in a list, + * i.e. Home/End key navigation. + */ +export function findLastSelectableRow( + direction: SelectionDirection, + rowCount: ReadonlyArray, + canSelectRow: (indexPath: RowIndexPath) => boolean +): RowIndexPath | null { + const totalRowCount = getTotalRowCount(rowCount) + let i = direction === 'up' ? 0 : totalRowCount - 1 + const delta = direction === 'up' ? 1 : -1 + + for (; i >= 0 && i < totalRowCount; i += delta) { + const indexPath = globalIndexToRowIndexPath(i, rowCount) + if (indexPath !== null && canSelectRow(indexPath)) { + return indexPath + } + } + + return null +} diff --git a/app/src/ui/lib/list/section-list.tsx b/app/src/ui/lib/list/section-list.tsx new file mode 100644 index 0000000000..a695eb526f --- /dev/null +++ b/app/src/ui/lib/list/section-list.tsx @@ -0,0 +1,1681 @@ +import * as React from 'react' +import * as ReactDOM from 'react-dom' +import { Grid, AutoSizer, Index } from 'react-virtualized' +import { shallowEquals, structuralEquals } from '../../../lib/equality' +import { FocusContainer } from '../../lib/focus-container' +import { ListRow } from './list-row' +import { + findNextSelectableRow, + SelectionSource, + SelectionDirection, + IMouseClickSource, + IKeyboardSource, + ISelectAllSource, + findLastSelectableRow, +} from './section-list-selection' +import { createUniqueId, releaseUniqueId } from '../../lib/id-pool' +import { ListItemInsertionOverlay } from './list-item-insertion-overlay' +import { DragData, DragType } from '../../../models/drag-drop' +import memoizeOne from 'memoize-one' +import { + getTotalRowCount, + globalIndexToRowIndexPath, + InvalidRowIndexPath, + isValidRow, + RowIndexPath, + rowIndexPathEquals, + rowIndexPathToGlobalIndex, +} from './list-row-index-path' +import { range } from '../../../lib/range' +import { sendNonFatalException } from '../../../lib/helpers/non-fatal-exception' + +/** + * Describe the first argument given to the cellRenderer, + * See + * https://github.com/bvaughn/react-virtualized/issues/386 + * https://github.com/bvaughn/react-virtualized/blob/8.0.11/source/Grid/defaultCellRangeRenderer.js#L38-L44 + */ +export interface IRowRendererParams { + /** Horizontal (column) index of cell */ + readonly columnIndex: number + + /** The Grid is currently being scrolled */ + readonly isScrolling: boolean + + /** Unique key within array of cells */ + readonly key: React.Key + + /** Vertical (row) index of cell */ + readonly rowIndex: number + + /** Style object to be applied to cell */ + readonly style: React.CSSProperties +} + +export type ClickSource = IMouseClickSource | IKeyboardSource + +interface ISectionListProps { + /** + * Mandatory callback for rendering the contents of a particular + * row. The callback is not responsible for the outer wrapper + * of the row, only its contents and may return null although + * that will result in an empty list item. + */ + readonly rowRenderer: (indexPath: RowIndexPath) => JSX.Element | null + + /** + * Whether or not a given section has a header row at the beginning. When + * ommitted, it's assumed the section does NOT have a header row. + */ + readonly sectionHasHeader?: (section: number) => boolean + + /** Aria label for a section in the list. */ + readonly getSectionAriaLabel?: (section: number) => string | undefined + + /** + * The total number of rows in the list. This is used for + * scroll virtualization purposes when calculating the theoretical + * height of the list. + */ + readonly rowCount: ReadonlyArray + + /** + * The height of each individual row in the list. This height + * is enforced for each row container and attempting to render a row + * which does not fit inside that height is forbidden. + * + * Can either be a number (most efficient) in which case all rows + * are of equal height, or, a function that, given a row index returns + * the height of that particular row. + */ + readonly rowHeight: number | ((info: { index: RowIndexPath }) => number) + + /** + * Function that generates an ID for a given row. This will allow the + * container component of the list to have control over the ID of the + * row and allow it to be used for things like keyboard navigation. + */ + readonly rowId?: (indexPath: RowIndexPath) => string + + /** + * The currently selected rows indexes. Used to attach a special + * selection class on those row's containers as well as being used + * for keyboard selection. + * + * It is expected that the use case for this is setting of the initially + * selected rows or clearing a list selection. + * + * N.B. Since it is used for keyboard selection, changing the ordering of + * elements in this array in a parent component may result in unexpected + * behaviors when a user modifies their selection via key commands. + * See #15536 lessons learned. + */ + readonly selectedRows: ReadonlyArray + + /** + * Used to attach special classes to specific rows + */ + readonly rowCustomClassNameMap?: Map> + + /** + * This function will be called when a pointer device is pressed and then + * released on a selectable row. Note that this follows the conventions + * of button elements such that pressing Enter or Space on a keyboard + * while focused on a particular row will also trigger this event. Consumers + * can differentiate between the two using the source parameter. + * + * Note that this event handler will not be called for keyboard events + * if `event.preventDefault()` was called in the onRowKeyDown event handler. + * + * Consumers of this event do _not_ have to call event.preventDefault, + * when this event is subscribed to the list will automatically call it. + */ + readonly onRowClick?: (row: RowIndexPath, source: ClickSource) => void + + readonly onRowDoubleClick?: ( + indexPath: RowIndexPath, + source: IMouseClickSource + ) => void + + /** + * This prop defines the behaviour of the selection of items within this list. + * - 'single' : (default) single list-item selection. [shift] and [ctrl] have + * no effect. Use in combination with one of: + * onSelectedRowChanged(row: number) + * onSelectionChanged(rows: number[]) + * - 'range' : allows for selecting continuous ranges. [shift] can be used. + * [ctrl] has no effect. Use in combination with one of: + * onSelectedRangeChanged(start: number, end: number) + * onSelectionChanged(rows: number[]) + * - 'multi' : allows range and/or arbitrary selection. [shift] and [ctrl] + * can be used. Use in combination with: + * onSelectionChanged(rows: number[]) + */ + readonly selectionMode?: 'single' | 'range' | 'multi' + + /** + * This function will be called when the selection changes as a result of a + * user keyboard or mouse action (i.e. not when props change). This function + * will not be invoked when an already selected row is clicked on. + * Use this function when the selectionMode is 'single' + * + * @param row - The index of the row that was just selected + * @param source - The kind of user action that provoked the change, either + * a pointer device press or a keyboard event (arrow up/down) + */ + readonly onSelectedRowChanged?: ( + indexPath: RowIndexPath, + source: SelectionSource + ) => void + + /** + * This function will be called when the selection changes as a result of a + * user keyboard or mouse action (i.e. not when props change). This function + * will not be invoked when an already selected row is clicked on. + * Index parameters are inclusive + * Use this function when the selectionMode is 'range' + * + * @param start - The index of the first selected row + * @param end - The index of the last selected row + * @param source - The kind of user action that provoked the change, either + * a pointer device press or a keyboard event (arrow up/down) + */ + readonly onSelectedRangeChanged?: ( + start: RowIndexPath, + end: RowIndexPath, + source: SelectionSource + ) => void + + /** + * This function will be called when the selection changes as a result of a + * user keyboard or mouse action (i.e. not when props change). This function + * will not be invoked when an already selected row is clicked on. + * Use this function for any selectionMode + * + * @param rows - The indexes of the row(s) that are part of the selection + * @param source - The kind of user action that provoked the change, either + * a pointer device press or a keyboard event (arrow up/down) + */ + readonly onSelectionChanged?: ( + rows: ReadonlyArray, + source: SelectionSource + ) => void + + /** + * A handler called whenever a key down event is received on the + * row container element. Due to the way the container is currently + * implemented the element produced by the rowRendered will never + * see keyboard events without stealing focus away from the container. + * + * Primary use case for this is to allow items to react to the space + * bar in order to toggle selection. This function is responsible + * for calling event.preventDefault() when acting on a key press. + */ + readonly onRowKeyDown?: ( + indexPath: RowIndexPath, + event: React.KeyboardEvent + ) => void + + /** + * A handler called whenever a mouse down event is received on the + * row container element. Unlike onSelectionChanged, this is raised + * for every mouse down event, whether the row is selected or not. + */ + readonly onRowMouseDown?: ( + indexPath: RowIndexPath, + event: React.MouseEvent + ) => void + + /** + * A handler called whenever a context menu event is received on the + * row container element. + * + * The context menu is invoked when a user right clicks the row or + * uses keyboard shortcut. + */ + readonly onRowContextMenu?: ( + row: RowIndexPath, + event: React.MouseEvent + ) => void + + /** + * A handler called whenever the user drops items on the list to be inserted. + * + * @param row - The index of the row where the user intends to insert the new + * items. + * @param data - The data dropped by the user. + */ + readonly onDropDataInsertion?: ( + indexPath: RowIndexPath, + data: DragData + ) => void + + /** + * An optional handler called to determine whether a given row is + * selectable or not. Reasons for why a row might not be selectable + * includes it being a group header or the item being disabled. + */ + readonly canSelectRow?: (row: RowIndexPath) => boolean + readonly onScroll?: (scrollTop: number, clientHeight: number) => void + + /** + * List's underlying implementation acts as a pure component based on the + * above props. So if there are any other properties that also determine + * whether the list should re-render, List must know about them. + */ + readonly invalidationProps?: any + + /** The unique identifier for the outer element of the component (optional, defaults to null) */ + readonly id?: string + + /** The unique identifier of the accessible list component (optional) */ + readonly accessibleListId?: string + + /** The row that should be scrolled to when the list is rendered. */ + readonly scrollToRow?: RowIndexPath + + /** Type of elements that can be inserted in the list via drag & drop. Optional. */ + readonly insertionDragType?: DragType + + /** + * The number of pixels from the top of the list indicating + * where to scroll do on rendering of the list. + */ + readonly setScrollTop?: number + + /** The aria-labelledby attribute for the list component. */ + readonly ariaLabelledBy?: string + + /** The aria-label attribute for the list component. */ + readonly ariaLabel?: string +} + +interface ISectionListState { + /** The available height for the list as determined by ResizeObserver */ + readonly height?: number + + /** The available width for the list as determined by ResizeObserver */ + readonly width?: number + + readonly rowIdPrefix?: string + + readonly scrollTop: number +} + +/** + * Create an array with row indices between firstRow and lastRow (inclusive). + * + * This is essentially a range function with the explicit behavior of + * inclusive upper and lower bound. + */ +function createSelectionBetween( + firstRow: RowIndexPath, + lastRow: RowIndexPath, + rowCount: ReadonlyArray +): ReadonlyArray { + const firstIndex = rowIndexPathToGlobalIndex(firstRow, rowCount) + const lastIndex = rowIndexPathToGlobalIndex(lastRow, rowCount) + if (firstIndex === null || lastIndex === null) { + return [] + } + + const end = lastIndex > firstIndex ? lastIndex + 1 : lastIndex - 1 + // range is upper bound exclusive + const rowRange = range(firstIndex, end) + const selection = new Array(rowRange.length) + for (let i = 0; i < rowRange.length; i++) { + const indexPath = globalIndexToRowIndexPath(rowRange[i], rowCount) + if (indexPath !== null) { + selection[i] = indexPath + } + } + return selection +} + +// Since objects cannot be keys in a Map, this class encapsulates the logic for +// creating a string key from a RowIndexPath. +class RowRefsMap { + private readonly map = new Map() + + private getIndexPathKey(indexPath: RowIndexPath): string { + return `${indexPath.section}-${indexPath.row}` + } + + public get(indexPath: RowIndexPath): HTMLDivElement | undefined { + return this.map.get(this.getIndexPathKey(indexPath)) + } + + public set(indexPath: RowIndexPath, element: HTMLDivElement) { + this.map.set(this.getIndexPathKey(indexPath), element) + } + + public delete(indexPath: RowIndexPath) { + this.map.delete(this.getIndexPathKey(indexPath)) + } +} + +export class SectionList extends React.Component< + ISectionListProps, + ISectionListState +> { + private fakeScroll: HTMLDivElement | null = null + private focusRow: RowIndexPath = InvalidRowIndexPath + + private readonly rowRefs = new RowRefsMap() + + /** + * The style prop for our child Grid. We keep this here in order + * to not create a new object on each render and thus forcing + * the Grid to re-render even though nothing has changed. + */ + private gridStyle: React.CSSProperties = { overflowX: 'hidden' } + + /** + * On Win32 we use a fake scroll bar. This variable keeps track of + * which of the actual scroll container and the fake scroll container + * received the scroll event first to avoid bouncing back and forth + * causing jerky scroll bars and more importantly making the mouse + * wheel scroll speed appear different when scrolling over the + * fake scroll bar and the actual one. + */ + private lastScroll: 'grid' | 'fake' | null = null + + private list: HTMLDivElement | null = null + private rootGrid: Grid | null = null + private grids = new Map() + private readonly resizeObserver: ResizeObserver | null = null + private updateSizeTimeoutId: NodeJS.Immediate | null = null + + /** + * Get the props for the inner scroll container (called containerProps on the + * Grid component). This is memoized to avoid causing the Grid component to + * rerender every time the list component rerenders (the Grid component is a + * pure component so a complex object like containerProps being instantiated + * on each render would cause it to rerender constantly). + */ + private getContainerProps = memoizeOne( + ( + activeDescendant: string | undefined + ): React.HTMLProps => ({ + onKeyDown: this.onKeyDown, + 'aria-activedescendant': activeDescendant, + 'aria-multiselectable': + this.props.selectionMode === 'multi' || + this.props.selectionMode === 'range' + ? 'true' + : undefined, + }) + ) + + public constructor(props: ISectionListProps) { + super(props) + + this.state = { + scrollTop: 0, + } + + const ResizeObserverClass: typeof ResizeObserver = (window as any) + .ResizeObserver + + if (ResizeObserver || false) { + this.resizeObserver = new ResizeObserverClass(entries => { + for (const { target, contentRect } of entries) { + if (target === this.list && this.list !== null) { + // We might end up causing a recursive update by updating the state + // when we're reacting to a resize so we'll defer it until after + // react is done with this frame. + if (this.updateSizeTimeoutId !== null) { + clearImmediate(this.updateSizeTimeoutId) + } + + this.updateSizeTimeoutId = setImmediate( + this.onResized, + this.list, + contentRect + ) + } + } + }) + } + } + + private get totalRowCount() { + return getTotalRowCount(this.props.rowCount) + } + + private getRowId(indexPath: RowIndexPath): string | undefined { + if (this.props.rowId) { + return this.props.rowId(indexPath) + } + + return this.state.rowIdPrefix === undefined + ? undefined + : `${this.state.rowIdPrefix}-${indexPath.section}-${indexPath.row}` + } + + private onResized = (target: HTMLElement, contentRect: ClientRect) => { + this.updateSizeTimeoutId = null + + const [width, height] = [target.offsetWidth, target.offsetHeight] + + if (this.state.width !== width || this.state.height !== height) { + this.setState({ width, height }) + } + } + + private onSelectAll = (event: Event | React.SyntheticEvent) => { + const selectionMode = this.props.selectionMode + + if (selectionMode !== 'range' && selectionMode !== 'multi') { + return + } + + event.preventDefault() + + if (this.totalRowCount <= 0) { + return + } + + const source: ISelectAllSource = { kind: 'select-all' } + const firstRow: RowIndexPath = { section: 0, row: 0 } + const lastRow: RowIndexPath = { + section: this.props.rowCount.length - 1, + row: this.props.rowCount[this.props.rowCount.length - 1] - 1, + } + + if (this.props.onSelectionChanged) { + const newSelection = createSelectionBetween( + firstRow, + lastRow, + this.props.rowCount + ) + this.props.onSelectionChanged(newSelection, source) + } + + if (selectionMode === 'range' && this.props.onSelectedRangeChanged) { + this.props.onSelectedRangeChanged(firstRow, lastRow, source) + } + } + + private onRef = (element: HTMLDivElement | null) => { + if (element === null && this.list !== null) { + this.list.removeEventListener('select-all', this.onSelectAll) + } + + this.list = element + + if (element !== null) { + // This is a custom event that desktop emits through + // when the user selects the Edit > Select all menu item. We + // hijack it and select all list items rather than let it bubble + // to electron's default behavior which is to select all selectable + // text in the renderer. + element.addEventListener('select-all', this.onSelectAll) + } + + if (this.resizeObserver) { + this.resizeObserver.disconnect() + + if (element !== null) { + this.resizeObserver.observe(element) + } else { + this.setState({ width: undefined, height: undefined }) + } + } + } + + private onKeyDown = (event: React.KeyboardEvent) => { + if (this.props.onRowKeyDown) { + for (const row of this.props.selectedRows) { + this.props.onRowKeyDown(row, event) + } + } + + // The consumer is given a change to prevent the default behavior for + // keyboard navigation so that they can customize its behavior as needed. + if (event.defaultPrevented) { + return + } + + const source: SelectionSource = { kind: 'keyboard', event } + + // Home is Cmd+ArrowUp on macOS, end is Cmd+ArrowDown, see + // https://github.com/desktop/desktop/pull/8644#issuecomment-645965884 + const isHomeKey = __DARWIN__ + ? event.metaKey && event.key === 'ArrowUp' + : event.key === 'Home' + const isEndKey = __DARWIN__ + ? event.metaKey && event.key === 'ArrowDown' + : event.key === 'End' + + const isRangeSelection = + event.shiftKey && + this.props.selectionMode !== undefined && + this.props.selectionMode !== 'single' + + if (isHomeKey || isEndKey) { + const direction = isHomeKey ? 'up' : 'down' + if (isRangeSelection) { + this.addSelectionToLastSelectableRow(direction, source) + } else { + this.moveSelectionToLastSelectableRow(direction, source) + } + event.preventDefault() + } else if (event.key === 'ArrowUp' || event.key === 'ArrowDown') { + const direction = event.key === 'ArrowUp' ? 'up' : 'down' + if (isRangeSelection) { + this.addSelection(direction, source) + } else { + this.moveSelection(direction, source) + } + event.preventDefault() + } else if (!__DARWIN__ && event.key === 'a' && event.ctrlKey) { + // On Windows Chromium will steal the Ctrl+A shortcut before + // Electron gets its hands on it meaning that the Select all + // menu item can't be invoked by means of keyboard shortcuts + // on Windows. Clicking on the menu item still emits the + // 'select-all' custom DOM event. + this.onSelectAll(event) + } else if (event.key === 'PageUp' || event.key === 'PageDown') { + const direction = event.key === 'PageUp' ? 'up' : 'down' + if (isRangeSelection) { + this.addSelectionByPage(direction, source) + } else { + this.moveSelectionByPage(direction, source) + } + event.preventDefault() + } + } + + private moveSelectionByPage( + direction: SelectionDirection, + source: SelectionSource + ) { + const newSelection = this.getNextPageRowIndexPath(direction) + this.moveSelectionTo(newSelection, source) + } + + private addSelectionByPage( + direction: SelectionDirection, + source: SelectionSource + ) { + const { selectedRows } = this.props + const newSelection = this.getNextPageRowIndexPath(direction) + const firstSelection = selectedRows[0] ?? 0 + const range = createSelectionBetween( + firstSelection, + newSelection, + this.props.rowCount + ) + + if (this.props.onSelectionChanged) { + this.props.onSelectionChanged(range, source) + } + + if (this.props.onSelectedRangeChanged) { + this.props.onSelectedRangeChanged( + range[0], + range[range.length - 1], + source + ) + } + + this.scrollRowToVisible(newSelection) + } + + private getNextPageRowIndexPath(direction: SelectionDirection) { + const { selectedRows } = this.props + const lastSelection: RowIndexPath = selectedRows.at(-1) ?? { + row: 0, + section: 0, + } + + return this.findNextPageSelectableRow(lastSelection, direction) + } + + private getHeightForRowAtIndexPath(index: RowIndexPath) { + const { rowHeight } = this.props + return typeof rowHeight === 'number' ? rowHeight : rowHeight({ index }) + } + + private findNextPageSelectableRow( + fromRow: RowIndexPath, + direction: SelectionDirection + ) { + const { height: listHeight } = this.state + const { rowCount } = this.props + + if (listHeight === undefined) { + return fromRow + } + + let offset = 0 + let newSelection = fromRow + const delta = direction === 'up' ? -1 : 1 + + // Starting from the last selected row, move up or down depending + // on the direction, keeping a sum of the height of all the rows + // we've seen until the accumulated height is about to exceed that + // of the list height. Once we've found the index of the item that + // just about exceeds the height we'll pick that one as the next + // selection. + for (let i = fromRow.section; i < rowCount.length && i >= 0; i += delta) { + const initialRow = i === fromRow.section ? fromRow.row : 0 + + for (let j = initialRow; j < rowCount[i] && j >= 0; j += delta) { + const indexPath = { section: i, row: j } + const h = this.getHeightForRowAtIndexPath(indexPath) + + if (offset + h > listHeight) { + break + } + offset += h + + if (this.canSelectRow(indexPath)) { + newSelection = indexPath + } + } + } + + return newSelection + } + + private onRowKeyDown = ( + rowIndex: RowIndexPath, + event: React.KeyboardEvent + ) => { + if (this.props.onRowKeyDown) { + this.props.onRowKeyDown(rowIndex, event) + } + + const hasModifier = + event.altKey || event.ctrlKey || event.metaKey || event.shiftKey + + // We give consumers the power to prevent the onRowClick event by subscribing + // to the onRowKeyDown event and calling event.preventDefault. This lets + // consumers add their own semantics for keyboard presses. + if ( + !event.defaultPrevented && + !hasModifier && + (event.key === 'Enter' || event.key === ' ') + ) { + this.toggleSelection(event) + event.preventDefault() + } + } + + private onFocusContainerKeyDown = (event: React.KeyboardEvent) => { + const hasModifier = + event.altKey || event.ctrlKey || event.metaKey || event.shiftKey + + if ( + !event.defaultPrevented && + !hasModifier && + (event.key === 'Enter' || event.key === ' ') + ) { + this.toggleSelection(event) + event.preventDefault() + } + } + + private onFocusWithinChanged = (focusWithin: boolean) => { + // So the grid lost focus (we manually focus the grid if the focused list + // item is unmounted) so we mustn't attempt to refocus the previously + // focused list item if it scrolls back into view. + if (!focusWithin) { + this.focusRow = InvalidRowIndexPath + } + } + + private toggleSelection = (event: React.KeyboardEvent) => { + this.props.selectedRows.forEach(row => { + if (!this.props.onRowClick) { + return + } + + if (!isValidRow(row, this.props.rowCount)) { + log.debug( + `[List.toggleSelection] unable to onRowClick for row ${row} as it is outside the bounds` + ) + return + } + + this.props.onRowClick(row, { kind: 'keyboard', event }) + }) + } + + private onRowFocus = ( + index: RowIndexPath, + e: React.FocusEvent + ) => { + this.focusRow = index + } + + private onRowBlur = ( + index: RowIndexPath, + e: React.FocusEvent + ) => { + if (rowIndexPathEquals(this.focusRow, index)) { + this.focusRow = InvalidRowIndexPath + } + } + + private onRowContextMenu = ( + row: RowIndexPath, + e: React.MouseEvent + ) => { + this.props.onRowContextMenu?.(row, e) + } + + private get firstRowIndexPath(): RowIndexPath { + for (let section = 0; section < this.props.rowCount.length; section++) { + const rowCount = this.props.rowCount[section] + if (rowCount > 0) { + return { section, row: 0 } + } + } + + return InvalidRowIndexPath + } + + /** Convenience method for invoking canSelectRow callback when it exists */ + private canSelectRow = (rowIndex: RowIndexPath) => { + return this.props.canSelectRow ? this.props.canSelectRow(rowIndex) : true + } + + private addSelection(direction: SelectionDirection, source: SelectionSource) { + if (this.props.selectedRows.length === 0) { + return this.moveSelection(direction, source) + } + + const lastSelection = + this.props.selectedRows[this.props.selectedRows.length - 1] + + const selectionOrigin = this.props.selectedRows[0] + + const newRow = findNextSelectableRow( + this.props.rowCount, + { direction, row: lastSelection, wrap: false }, + this.canSelectRow + ) + + if (newRow != null) { + if (this.props.onSelectionChanged) { + const newSelection = createSelectionBetween( + selectionOrigin, + newRow, + this.props.rowCount + ) + this.props.onSelectionChanged(newSelection, source) + } + + if ( + this.props.selectionMode === 'range' && + this.props.onSelectedRangeChanged + ) { + this.props.onSelectedRangeChanged(selectionOrigin, newRow, source) + } + + this.scrollRowToVisible(newRow) + } + } + + private moveSelection( + direction: SelectionDirection, + source: SelectionSource + ) { + const lastSelection = + this.props.selectedRows.length > 0 + ? this.props.selectedRows[this.props.selectedRows.length - 1] + : InvalidRowIndexPath + + const newRow = findNextSelectableRow( + this.props.rowCount, + { direction, row: lastSelection }, + this.canSelectRow + ) + + if (newRow != null) { + this.moveSelectionTo(newRow, source) + } + } + + private moveSelectionToLastSelectableRow( + direction: SelectionDirection, + source: SelectionSource + ) { + const { canSelectRow, props } = this + const { rowCount } = props + const row = findLastSelectableRow(direction, rowCount, canSelectRow) + + if (row !== null) { + this.moveSelectionTo(row, source) + } + } + + private addSelectionToLastSelectableRow( + direction: SelectionDirection, + source: SelectionSource + ) { + const { canSelectRow, props } = this + const { rowCount, selectedRows } = props + const row = findLastSelectableRow(direction, rowCount, canSelectRow) + + if (row === null) { + return + } + + const firstRow = this.firstRowIndexPath + const firstSelection = selectedRows[0] ?? firstRow + const range = createSelectionBetween(firstSelection, row, rowCount) + + this.props.onSelectionChanged?.(range, source) + + const from = range.at(0) ?? firstRow + const to = range.at(-1) ?? firstRow + + this.props.onSelectedRangeChanged?.(from, to, source) + + this.scrollRowToVisible(row) + } + + private moveSelectionTo(indexPath: RowIndexPath, source: SelectionSource) { + if (this.props.onSelectionChanged) { + this.props.onSelectionChanged([indexPath], source) + } + + if (this.props.onSelectedRowChanged) { + if (!isValidRow(indexPath, this.props.rowCount)) { + log.debug( + `[List.moveSelection] unable to onSelectedRowChanged for row '${indexPath}' as it is outside the bounds` + ) + return + } + + this.props.onSelectedRowChanged(indexPath, source) + } + + this.scrollRowToVisible(indexPath) + } + + private scrollRowToVisible(indexPath: RowIndexPath, moveFocus = true) { + if (this.rootGrid === null) { + return + } + + const { scrollTop } = this.state + + const rowHeight = this.getHeightForRowAtIndexPath(indexPath) + const sectionOffset = this.getSectionScrollOffset(indexPath.section) + const rowOffsetInSection = this.getRowOffsetInSection(indexPath) + + const grid = ReactDOM.findDOMNode(this.rootGrid) + if (!(grid instanceof HTMLElement)) { + return + } + const gridHeight = grid.getBoundingClientRect().height + + const minCellOffset = + sectionOffset + rowOffsetInSection + rowHeight - gridHeight + const maxCellOffset = sectionOffset + rowOffsetInSection + + const newScrollTop = Math.max( + minCellOffset, + Math.min(maxCellOffset, scrollTop) + ) + + this.rootGrid?.scrollToPosition({ + scrollLeft: 0, + scrollTop: newScrollTop, + }) + + if (moveFocus) { + this.focusRow = indexPath + this.rowRefs.get(indexPath)?.focus({ preventScroll: true }) + } + } + + public componentDidMount() { + const { props } = this + const { selectedRows, scrollToRow, setScrollTop } = props + + // If we have a selected row when we're about to mount + // we'll scroll to it immediately. + const row = scrollToRow ?? selectedRows.at(0) + if (row === undefined) { + return + } + + const grid = this.grids.get(row.section) + + // Prefer scrollTop position over scrollToRow + if (grid !== undefined && setScrollTop === undefined) { + grid.scrollToCell({ rowIndex: row.row, columnIndex: 0 }) + } + } + + public componentDidUpdate( + prevProps: ISectionListProps, + prevState: ISectionListState + ) { + const { scrollToRow, setScrollTop } = this.props + if ( + scrollToRow !== undefined && + (prevProps.scrollToRow === undefined || + !rowIndexPathEquals(prevProps.scrollToRow, scrollToRow)) + ) { + // Prefer scrollTop position over scrollToRow + if (setScrollTop === undefined) { + this.scrollRowToVisible(scrollToRow, false) + } + } + + if (this.grids.size > 0) { + const hasEqualRowCount = structuralEquals( + this.props.rowCount, + prevProps.rowCount + ) + + // A non-exhaustive set of checks to see if our current update has already + // triggered a re-render of the Grid. In order to do this perfectly we'd + // have to do a shallow compare on all the props we pass to Grid but + // this should cover the majority of cases. + const gridHasUpdatedAlready = + !hasEqualRowCount || + this.state.width !== prevState.width || + this.state.height !== prevState.height + + // If the number of groups doesn't change, but the size of them does, we + // need to recompute the grid size to ensure that the rows are laid out + // correctly. + if (!hasEqualRowCount) { + this.rootGrid?.recomputeGridSize() + } + + if (!gridHasUpdatedAlready) { + const selectedRowChanged = !structuralEquals( + prevProps.selectedRows, + this.props.selectedRows + ) + + const invalidationPropsChanged = !shallowEquals( + prevProps.invalidationProps, + this.props.invalidationProps + ) + + // Now we need to figure out whether anything changed in such a way that + // the Grid has to update regardless of its props. Previously we passed + // our selectedRow and invalidationProps down to Grid and figured that + // it, being a pure component, would do the right thing but that's not + // quite the case since invalidationProps is a complex object. + if (selectedRowChanged || invalidationPropsChanged) { + for (const grid of this.grids.values()) { + grid.forceUpdate() + } + } + } + } + } + + public componentWillMount() { + this.setState({ rowIdPrefix: createUniqueId('ListRow') }) + } + + public componentWillUnmount() { + if (this.updateSizeTimeoutId !== null) { + clearImmediate(this.updateSizeTimeoutId) + this.updateSizeTimeoutId = null + } + + if (this.resizeObserver) { + this.resizeObserver.disconnect() + } + + if (this.state.rowIdPrefix) { + releaseUniqueId(this.state.rowIdPrefix) + } + } + + private onRowRef = ( + rowIndex: RowIndexPath, + element: HTMLDivElement | null + ) => { + if (element === null) { + this.rowRefs.delete(rowIndex) + } else { + this.rowRefs.set(rowIndex, element) + } + + if (rowIndexPathEquals(rowIndex, this.focusRow)) { + // The currently focused row is going being unmounted so we'll move focus + // programmatically to the grid so that keyboard navigation still works + if (element === null) { + const rowGrid = this.grids.get(rowIndex.section) + if (rowGrid === undefined) { + const grid = ReactDOM.findDOMNode(rowGrid) + if (grid instanceof HTMLElement) { + grid.focus({ preventScroll: true }) + } + } + } else { + // A previously focused row is being mounted again, we'll move focus + // back to it + element.focus({ preventScroll: true }) + } + } + } + + private getCustomRowClassNames = (rowIndex: RowIndexPath) => { + const { rowCustomClassNameMap } = this.props + if (rowCustomClassNameMap === undefined) { + return undefined + } + + const customClasses = new Array() + rowCustomClassNameMap.forEach( + (rows: ReadonlyArray, className: string) => { + if (rows.includes(rowIndex)) { + customClasses.push(className) + } + } + ) + + return customClasses.length === 0 ? undefined : customClasses.join(' ') + } + + private getRowRenderer = (section: number) => { + return (params: IRowRendererParams) => { + const indexPath: RowIndexPath = { + section: section, + row: params.rowIndex, + } + + const selectable = this.canSelectRow(indexPath) + const selected = + this.props.selectedRows.findIndex(r => + rowIndexPathEquals(r, indexPath) + ) !== -1 + const customClasses = this.getCustomRowClassNames(indexPath) + + // An unselectable row shouldn't be focusable + let tabIndex: number | undefined = undefined + if (selectable) { + tabIndex = + selected && rowIndexPathEquals(this.props.selectedRows[0], indexPath) + ? 0 + : -1 + } + + const row = this.props.rowRenderer(indexPath) + const sectionHasHeader = + this.props.sectionHasHeader?.(indexPath.section) ?? false + + const element = + this.props.insertionDragType !== undefined ? ( + + {row} + + ) : ( + row + ) + + const id = this.getRowId(indexPath) + + return ( + + ) + } + } + + public render() { + let content: JSX.Element[] | JSX.Element | null + if (this.resizeObserver) { + content = this.renderContents( + this.state.width ?? 0, + this.state.height ?? 0 + ) + } else { + // Legacy in the event that we don't have ResizeObserver + content = ( + + {({ width, height }: { width: number; height: number }) => + this.renderContents(width, height) + } + + ) + } + + return ( +
+ {content} +
+ ) + } + + /** + * Renders the react-virtualized Grid component and optionally + * a fake scroll bar component if running on Windows. + * + * @param width - The width of the Grid as given by AutoSizer + * @param height - The height of the Grid as given by AutoSizer + */ + private renderContents(width: number, height: number) { + if (__WIN32__) { + return ( + <> + {this.renderGrid(width, height)} + {this.renderFakeScroll(height)} + + ) + } + + return this.renderGrid(width, height) + } + + private getRowHeight = (section: number) => { + const rowHeight = this.props.rowHeight + + if (typeof rowHeight === 'number') { + return rowHeight + } + + return (params: Index) => { + const index: RowIndexPath = { + section: section, + row: params.index, + } + + return rowHeight({ index }) + } + } + + private onRootGridRef = (ref: Grid | null) => { + this.rootGrid = ref + } + + private getOnGridRef = (section: number) => { + return (ref: Grid | null) => { + if (ref === null) { + this.grids.delete(section) + } else { + this.grids.set(section, ref) + } + } + } + + private onFakeScrollRef = (ref: HTMLDivElement | null) => { + this.fakeScroll = ref + } + + private getSectionScrollOffset = (section: number) => + this.props.rowCount + .slice(0, section) + .reduce((height, _x, idx) => height + this.getSectionHeight(idx), 0) + + private getSectionGridRenderer = + (width: number, height: number) => (params: IRowRendererParams) => { + const section = params.rowIndex + + // we select the last item from the selection array for this prop + const sectionHeight = this.getSectionHeight(section) + const offset = this.getSectionScrollOffset(section) + + const relativeScrollTop = Math.max( + 0, + Math.min(sectionHeight, this.state.scrollTop - offset) + ) + + return ( + + ) + } + + private getRowOffsetInSection(indexPath: RowIndexPath) { + if (typeof this.props.rowHeight === 'number') { + return indexPath.row * this.props.rowHeight + } + + let offset = 0 + for (let i = 0; i < indexPath.row; i++) { + offset += this.props.rowHeight({ index: indexPath }) + } + return offset + } + + private getSectionHeight(section: number) { + if (typeof this.props.rowHeight === 'number') { + return this.props.rowCount[section] * this.props.rowHeight + } + + let height = 0 + for (let i = 0; i < this.props.rowCount[section]; i++) { + height += this.props.rowHeight({ index: { section, row: i } }) + } + return height + } + + private get totalHeight() { + return this.props.rowCount.reduce((total, _count, section) => { + return total + this.getSectionHeight(section) + }) + } + + private sectionHeight = ({ index }: Index) => { + return this.getSectionHeight(index) + } + + /** + * Renders the react-virtualized Grid component + * + * @param width - The width of the Grid as given by AutoSizer + * @param height - The height of the Grid as given by AutoSizer + */ + private renderGrid(width: number, height: number) { + // It is possible to send an invalid array such as [-1] to this component, + // if you do, you get weird focus problems. We shouldn't be doing this.. but + // if we do, send a non fatal exception to tell us about it. + const firstSelectedRow = this.props.selectedRows.at(0) + if ( + firstSelectedRow && + rowIndexPathEquals(firstSelectedRow, InvalidRowIndexPath) + ) { + sendNonFatalException( + 'The selected rows of the List.tsx contained a negative number.', + new Error( + `Invalid selected rows that contained a negative number passed to List component. This will cause keyboard navigation and focus problems.` + ) + ) + } + + const { selectedRows } = this.props + const activeDescendant = + selectedRows.length && this.state.rowIdPrefix + ? this.getRowId(selectedRows[selectedRows.length - 1]) + : undefined + const containerProps = this.getContainerProps(activeDescendant) + // The currently selected list item is focusable but if there's no focused + // item the list itself needs to be focusable so that you can reach it with + // keyboard navigation and select an item. + const tabIndex = selectedRows.length > 0 ? -1 : 0 + return ( + + + + ) + } + + /** + * Renders a fake scroll container which sits on top of the + * react-virtualized Grid component in order for us to be + * able to have nice looking scrollbars on Windows. + * + * The fake scroll bar synchronizes its position + * + * NB: Should only be used on win32 platforms and needs to + * be coupled with styling that hides scroll bars on Grid + * and accurately positions the fake scroll bar. + * + * @param height The height of the Grid as given by AutoSizer + */ + private renderFakeScroll(height: number) { + return ( +
+
+
+ ) + } + + // Set the scroll position of the actual Grid to that + // of the fake scroll bar. This is for mousewheel/touchpad + // scrolling on top of the fake Grid or actual dragging of + // the scroll thumb. + private onFakeScroll = (e: React.UIEvent) => { + // We're getting this event in reaction to the Grid + // having been scrolled and subsequently updating the + // fake scrollTop, ignore it + if (this.lastScroll === 'grid') { + this.lastScroll = null + return + } + + this.lastScroll = 'fake' + + // TODO: calculate scrollTop of the right grid(s)? + + if (this.rootGrid) { + const element = ReactDOM.findDOMNode(this.rootGrid) + if (element instanceof Element) { + element.scrollTop = e.currentTarget.scrollTop + } + } + } + + private onRowMouseDown = ( + row: RowIndexPath, + event: React.MouseEvent + ) => { + if (this.canSelectRow(row)) { + if (this.props.onRowMouseDown) { + this.props.onRowMouseDown(row, event) + } + + // macOS allow emulating a right click by holding down the ctrl key while + // performing a "normal" click. + const isRightClick = + event.button === 2 || + (__DARWIN__ && event.button === 0 && event.ctrlKey) + + // prevent the right-click event from changing the selection if not necessary + if (isRightClick && this.props.selectedRows.includes(row)) { + return + } + + const multiSelectKey = __DARWIN__ ? event.metaKey : event.ctrlKey + + if ( + event.shiftKey && + this.props.selectedRows.length && + this.props.selectionMode && + this.props.selectionMode !== 'single' + ) { + /* + * if [shift] is pressed and selectionMode is different than 'single', + * select all in-between first selection and current row + */ + const selectionOrigin = this.props.selectedRows[0] + + if (this.props.onSelectionChanged) { + const newSelection = createSelectionBetween( + selectionOrigin, + row, + this.props.rowCount + ) + this.props.onSelectionChanged(newSelection, { + kind: 'mouseclick', + event, + }) + } + if ( + this.props.selectionMode === 'range' && + this.props.onSelectedRangeChanged + ) { + this.props.onSelectedRangeChanged(selectionOrigin, row, { + kind: 'mouseclick', + event, + }) + } + } else if (multiSelectKey && this.props.selectionMode === 'multi') { + /* + * if [ctrl] is pressed and selectionMode is 'multi', + * toggle selection of the targeted row + */ + if (this.props.onSelectionChanged) { + let newSelection: ReadonlyArray + if (this.props.selectedRows.includes(row)) { + // remove the ability to deselect the last item + if (this.props.selectedRows.length === 1) { + return + } + newSelection = this.props.selectedRows.filter( + ix => !rowIndexPathEquals(ix, row) + ) + } else { + newSelection = [...this.props.selectedRows, row] + } + + this.props.onSelectionChanged(newSelection, { + kind: 'mouseclick', + event, + }) + } + } else if ( + (this.props.selectionMode === 'range' || + this.props.selectionMode === 'multi') && + this.props.selectedRows.length > 1 && + this.props.selectedRows.includes(row) + ) { + // Do nothing. Multiple rows are already selected. We assume the user is + // pressing down on multiple and may desire to start dragging. We will + // invoke the single selection `onRowMouseUp` if they let go here and no + // special keys are being pressed. + } else if ( + this.props.selectedRows.length !== 1 || + (this.props.selectedRows.length === 1 && + !rowIndexPathEquals(row, this.props.selectedRows[0])) + ) { + /* + * if no special key is pressed, and that the selection is different, + * single selection occurs + */ + this.selectSingleRowAfterMouseEvent(row, event) + } + } + } + + private onRowMouseUp = (row: RowIndexPath, event: React.MouseEvent) => { + if (!this.canSelectRow(row)) { + return + } + + // macOS allow emulating a right click by holding down the ctrl key while + // performing a "normal" click. + const isRightClick = + event.button === 2 || (__DARWIN__ && event.button === 0 && event.ctrlKey) + + // prevent the right-click event from changing the selection if not necessary + if (isRightClick && this.props.selectedRows.includes(row)) { + return + } + + const multiSelectKey = __DARWIN__ ? event.metaKey : event.ctrlKey + + if ( + !event.shiftKey && + !multiSelectKey && + this.props.selectedRows.length > 1 && + this.props.selectedRows.includes(row) && + (this.props.selectionMode === 'range' || + this.props.selectionMode === 'multi') + ) { + // No special keys are depressed and multiple rows were selected. The + // onRowMouseDown event was ignored for this scenario because the user may + // desire to started dragging multiple. However, if they let go, we want a + // new single selection to occur. + this.selectSingleRowAfterMouseEvent(row, event) + } + } + + private selectSingleRowAfterMouseEvent( + row: RowIndexPath, + event: React.MouseEvent + ): void { + if (this.props.onSelectionChanged) { + this.props.onSelectionChanged([row], { kind: 'mouseclick', event }) + } + + if (this.props.onSelectedRangeChanged) { + this.props.onSelectedRangeChanged(row, row, { + kind: 'mouseclick', + event, + }) + } + + if (this.props.onSelectedRowChanged) { + if (!isValidRow(row, this.props.rowCount)) { + log.debug( + `[List.selectSingleRowAfterMouseEvent] unable to onSelectedRowChanged for row '${row}' as it is outside the bounds` + ) + return + } + + this.props.onSelectedRowChanged(row, { kind: 'mouseclick', event }) + } + } + + private onRowClick = (row: RowIndexPath, event: React.MouseEvent) => { + if (this.canSelectRow(row) && this.props.onRowClick) { + if (!isValidRow(row, this.props.rowCount)) { + log.debug( + `[List.onRowClick] unable to onRowClick for row ${row} as it is outside the bounds` + ) + return + } + + this.props.onRowClick(row, { kind: 'mouseclick', event }) + } + } + + private onRowDoubleClick = ( + row: RowIndexPath, + event: React.MouseEvent + ) => { + if (!this.props.onRowDoubleClick) { + return + } + + this.props.onRowDoubleClick(row, { kind: 'mouseclick', event }) + } + + private onScroll = ({ + scrollTop, + clientHeight, + }: { + scrollTop: number + clientHeight: number + }) => { + if (this.props.onScroll) { + this.props.onScroll(scrollTop, clientHeight) + } + + // Set the scroll position of the fake scroll bar to that + // of the actual Grid. This is for mousewheel/touchpad scrolling + // on top of the Grid. + if (__WIN32__ && this.fakeScroll) { + // We're getting this event in reaction to the fake scroll + // having been scrolled and subsequently updating the + // Grid scrollTop, ignore it. + if (this.lastScroll === 'fake') { + this.lastScroll = null + return + } + + this.lastScroll = 'grid' + + this.fakeScroll.scrollTop = scrollTop + } + + this.setState({ scrollTop }) + + // Make sure the root grid re-renders its children + this.rootGrid?.recomputeGridSize() + } + + /** + * Explicitly put keyboard focus on the list or the selected item in the list. + * + * If the list a selected item it will be scrolled (if it's not already + * visible) and it will receive keyboard focus. If the list has no selected + * item the list itself will receive focus. From there keyboard navigation + * can be used to select the first or last items in the list. + * + * This method is a noop if the list has not yet been mounted. + */ + public focus() { + const { selectedRows, rowCount } = this.props + const lastSelectedRow = selectedRows.at(-1) + + if ( + lastSelectedRow !== undefined && + isValidRow(lastSelectedRow, rowCount) + ) { + this.scrollRowToVisible(lastSelectedRow) + } else { + // TODO: decide which grid to focus + // if (this.grid) { + // const element = ReactDOM.findDOMNode(this.grid) as HTMLDivElement + // if (element) { + // element.focus() + // } + // } + } + } +} diff --git a/app/src/ui/lib/list/selection.ts b/app/src/ui/lib/list/selection.ts index 8369463eef..0c288d7390 100644 --- a/app/src/ui/lib/list/selection.ts +++ b/app/src/ui/lib/list/selection.ts @@ -87,7 +87,7 @@ export function findNextSelectableRow( } const { direction, row } = action - const wrap = action.wrap === undefined ? true : action.wrap + const wrap = action.wrap ?? true // Ensure the row value is in the range between 0 and rowCount - 1 // diff --git a/app/src/ui/lib/section-filter-list.tsx b/app/src/ui/lib/section-filter-list.tsx new file mode 100644 index 0000000000..0fb251446e --- /dev/null +++ b/app/src/ui/lib/section-filter-list.tsx @@ -0,0 +1,696 @@ +import * as React from 'react' +import classnames from 'classnames' + +import { SectionList, ClickSource } from '../lib/list/section-list' +import { + findNextSelectableRow, + SelectionDirection, +} 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' +import { + InvalidRowIndexPath, + RowIndexPath, + rowIndexPathEquals, +} from './list/list-row-index-path' +import { + IFilterListGroup, + IFilterListItem, + SelectionSource, +} from './filter-list' + +interface IFlattenedGroup { + readonly kind: 'group' + readonly identifier: string +} + +interface IFlattenedItem { + readonly kind: 'item' + readonly item: T + /** Array of indexes in `item.text` that should be highlighted */ + readonly matches: IMatches +} + +/** + * A row in the list. This is used internally after the user-provided groups are + * flattened. + */ +type IFilterListRow = + | IFlattenedGroup + | IFlattenedItem + +interface ISectionFilterListProps { + /** A class name for the wrapping element. */ + readonly className?: string + + /** The height of the rows. */ + readonly rowHeight: number + + /** The ordered groups to display in the list. */ + readonly groups: ReadonlyArray> + + /** The selected item. */ + readonly selectedItem: T | null + + /** Called to render each visible item. */ + readonly renderItem: (item: T, matches: IMatches) => JSX.Element | null + + /** Called to render header for the group with the given identifier. */ + readonly renderGroupHeader?: (identifier: string) => JSX.Element | null + + /** Called to render content before/above the filter and list. */ + readonly renderPreList?: () => JSX.Element | null + + /** + * This function will be called when a pointer device is pressed and then + * released on a selectable row. Note that this follows the conventions + * of button elements such that pressing Enter or Space on a keyboard + * while focused on a particular row will also trigger this event. Consumers + * can differentiate between the two using the source parameter. + * + * Note that this event handler will not be called for keyboard events + * if `event.preventDefault()` was called in the onRowKeyDown event handler. + * + * Consumers of this event do _not_ have to call event.preventDefault, + * when this event is subscribed to the list will automatically call it. + */ + readonly onItemClick?: (item: T, source: ClickSource) => void + + /** + * This function will be called when the selection changes as a result of a + * user keyboard or mouse action (i.e. not when props change). This function + * will not be invoked when an already selected row is clicked on. + * + * @param selectedItem - The item that was just selected + * @param source - The kind of user action that provoked the change, + * either a pointer device press, or a keyboard event + * (arrow up/down) + */ + readonly onSelectionChanged?: ( + selectedItem: T | null, + source: SelectionSource + ) => void + + /** + * Called when a key down happens in the filter text input. Users have a + * chance to respond or cancel the default behavior by calling + * `preventDefault()`. + */ + readonly onFilterKeyDown?: ( + event: React.KeyboardEvent + ) => void + + /** Called when the Enter key is pressed in field of type search */ + readonly onEnterPressedWithoutFilteredItems?: (text: string) => void + + /** Aria label for a specific group */ + readonly getGroupAriaLabel?: (group: number) => string | undefined + + /** The current filter text to use in the form */ + readonly filterText?: string + + /** Called when the filter text is changed by the user */ + readonly onFilterTextChanged?: (text: string) => void + + /** + * Whether or not the filter list should allow selection + * and filtering. Defaults to false. + */ + readonly disabled?: boolean + + /** Any props which should cause a re-render if they change. */ + readonly invalidationProps: any + + /** Called to render content after the filter. */ + readonly renderPostFilter?: () => JSX.Element | null + + /** Called when there are no items to render. */ + readonly renderNoItems?: () => JSX.Element | null + + /** + * A reference to a TextBox that will be used to control this component. + * + * See https://github.com/desktop/desktop/issues/4317 for refactoring work to + * make this more composable which should make this unnecessary. + */ + readonly filterTextBox?: TextBox + + /** + * Callback to fire when the items in the filter list are updated + */ + readonly onFilterListResultsChanged?: (resultCount: number) => void + + /** Placeholder text for text box. Default is "Filter". */ + readonly placeholderText?: string + + /** If true, we do not render the filter. */ + readonly hideFilterRow?: boolean + + /** + * A handler called whenever a context menu event is received on the + * row container element. + * + * The context menu is invoked when a user right clicks the row or + * uses keyboard shortcut.s + */ + readonly onItemContextMenu?: ( + item: T, + event: React.MouseEvent + ) => void +} + +interface IFilterListState { + readonly rows: ReadonlyArray>> + readonly selectedRow: RowIndexPath + readonly filterValue: string + // Indices of groups in the filtered list + readonly groups: ReadonlyArray +} + +/** A List which includes the ability to filter based on its contents. */ +export class SectionFilterList< + T extends IFilterListItem +> extends React.Component, IFilterListState> { + private list: SectionList | null = null + private filterTextBox: TextBox | null = null + + public constructor(props: ISectionFilterListProps) { + super(props) + + this.state = createStateUpdate(props) + } + + public componentWillMount() { + if (this.props.filterTextBox !== undefined) { + this.filterTextBox = this.props.filterTextBox + } + } + + public componentWillReceiveProps(nextProps: ISectionFilterListProps) { + this.setState(createStateUpdate(nextProps)) + } + + public componentDidUpdate( + prevProps: ISectionFilterListProps, + prevState: IFilterListState + ) { + if (this.props.onSelectionChanged) { + const oldSelectedItemId = getItemIdFromRowIndex( + prevState.rows, + prevState.selectedRow + ) + const newSelectedItemId = getItemIdFromRowIndex( + this.state.rows, + this.state.selectedRow + ) + + if (oldSelectedItemId !== newSelectedItemId) { + const propSelectionId = this.props.selectedItem + ? this.props.selectedItem.id + : null + + if (propSelectionId !== newSelectedItemId) { + const newSelectedItem = getItemFromRowIndex( + this.state.rows, + this.state.selectedRow + ) + this.props.onSelectionChanged(newSelectedItem, { + kind: 'filter', + filterText: this.props.filterText || '', + }) + } + } + } + + if (this.props.onFilterListResultsChanged !== undefined) { + const itemCount = this.state.rows + .flat() + .filter(row => row.kind === 'item').length + + this.props.onFilterListResultsChanged(itemCount) + } + } + + public componentDidMount() { + if (this.filterTextBox !== null) { + this.filterTextBox.selectAll() + } + } + + public renderTextBox() { + return ( + + ) + } + + public renderFilterRow() { + if (this.props.hideFilterRow === true) { + return null + } + + return ( + + {this.props.filterTextBox === undefined ? this.renderTextBox() : null} + {this.props.renderPostFilter ? this.props.renderPostFilter() : null} + + ) + } + + public render() { + const itemRows = this.state.rows.flat().filter(row => row.kind === 'item') + const resultsPluralized = itemRows.length === 1 ? 'result' : 'results' + + return ( +
+ + {itemRows.length} {resultsPluralized} + + {this.props.renderPreList ? this.props.renderPreList() : null} + + {this.renderFilterRow()} + +
{this.renderContent()}
+
+ ) + } + + public selectNextItem( + focus: boolean = false, + inDirection: SelectionDirection = 'down' + ) { + if (this.list === null) { + return + } + let next: RowIndexPath | null = null + + const rowCount = this.state.rows.map(r => r.length) + if ( + this.state.selectedRow.row === -1 || + this.state.selectedRow.row === this.state.rows.length + ) { + next = findNextSelectableRow( + rowCount, + { + direction: inDirection, + row: InvalidRowIndexPath, + }, + this.canSelectRow + ) + } else { + next = findNextSelectableRow( + rowCount, + { + direction: inDirection, + row: this.state.selectedRow, + }, + this.canSelectRow + ) + } + + if (next !== null) { + this.setState({ selectedRow: next }, () => { + if (focus && this.list !== null) { + this.list.focus() + } + }) + } + } + + private renderContent() { + if (this.state.rows.length === 0 && this.props.renderNoItems) { + return this.props.renderNoItems() + } else { + return ( + r.length)} + rowRenderer={this.renderRow} + sectionHasHeader={this.sectionHasHeader} + getSectionAriaLabel={this.getGroupAriaLabel} + rowHeight={this.props.rowHeight} + selectedRows={ + rowIndexPathEquals(this.state.selectedRow, InvalidRowIndexPath) + ? [] + : [this.state.selectedRow] + } + onSelectedRowChanged={this.onSelectedRowChanged} + onRowClick={this.onRowClick} + onRowKeyDown={this.onRowKeyDown} + onRowContextMenu={this.onRowContextMenu} + canSelectRow={this.canSelectRow} + invalidationProps={{ + ...this.props, + ...this.props.invalidationProps, + }} + /> + ) + } + } + + private sectionHasHeader = (section: number) => { + const rows = this.state.rows[section] + return rows.length > 0 && rows[0].kind === 'group' + } + + private getGroupAriaLabel = (group: number) => { + return this.props.getGroupAriaLabel?.(this.state.groups[group]) + } + + 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) { + return this.props.renderGroupHeader(row.identifier) + } else { + return null + } + } + + private onTextBoxRef = (component: TextBox | null) => { + this.filterTextBox = component + } + + private onListRef = (instance: SectionList | null) => { + this.list = instance + } + + private onFilterValueChanged = (text: string) => { + if (this.props.onFilterTextChanged) { + this.props.onFilterTextChanged(text) + } + } + + private onEnterPressed = (text: string) => { + const rows = this.state.rows.length + if ( + rows === 0 && + text.trim().length > 0 && + this.props.onEnterPressedWithoutFilteredItems !== undefined + ) { + this.props.onEnterPressedWithoutFilteredItems(text) + } + } + + private onSelectedRowChanged = ( + index: RowIndexPath, + source: SelectionSource + ) => { + this.setState({ selectedRow: index }) + + if (this.props.onSelectionChanged) { + const row = this.state.rows[index.section][index.row] + if (row.kind === 'item') { + this.props.onSelectionChanged(row.item, source) + } + } + } + + private canSelectRow = (index: RowIndexPath) => { + if (this.props.disabled) { + return false + } + + const row = this.state.rows[index.section][index.row] + return row.kind === 'item' + } + + private onRowClick = (index: RowIndexPath, source: ClickSource) => { + if (this.props.onItemClick) { + const row = this.state.rows[index.section][index.row] + + if (row.kind === 'item') { + this.props.onItemClick(row.item, source) + } + } + } + + private onRowContextMenu = ( + index: RowIndexPath, + source: React.MouseEvent + ) => { + if (!this.props.onItemContextMenu) { + return + } + + const row = this.state.rows[index.section][index.row] + + if (row.kind !== 'item') { + return + } + + this.props.onItemContextMenu(row.item, source) + } + + private onRowKeyDown = ( + indexPath: RowIndexPath, + event: React.KeyboardEvent + ) => { + const list = this.list + if (!list) { + return + } + + const rowCount = this.state.rows.map(r => r.length) + + const firstSelectableRow = findNextSelectableRow( + rowCount, + { direction: 'down', row: InvalidRowIndexPath }, + this.canSelectRow + ) + const lastSelectableRow = findNextSelectableRow( + rowCount, + { + direction: 'up', + row: { + section: 0, + row: 0, + }, + }, + this.canSelectRow + ) + + let shouldFocus = false + + if ( + event.key === 'ArrowUp' && + firstSelectableRow && + rowIndexPathEquals(indexPath, firstSelectableRow) + ) { + shouldFocus = true + } else if ( + event.key === 'ArrowDown' && + lastSelectableRow && + rowIndexPathEquals(indexPath, lastSelectableRow) + ) { + shouldFocus = true + } + + if (shouldFocus) { + const textBox = this.filterTextBox + + if (textBox) { + event.preventDefault() + textBox.focus() + } + } + } + + private onKeyDown = (event: React.KeyboardEvent) => { + const list = this.list + const key = event.key + + if (!list) { + return + } + + if (this.props.onFilterKeyDown) { + this.props.onFilterKeyDown(event) + } + + if (event.defaultPrevented) { + return + } + + const rowCount = this.state.rows.map(r => r.length) + + if (key === 'ArrowDown') { + if (rowCount.length > 0) { + const selectedRow = findNextSelectableRow( + rowCount, + { direction: 'down', row: InvalidRowIndexPath }, + this.canSelectRow + ) + if (selectedRow != null) { + this.setState({ selectedRow }, () => { + list.focus() + }) + } + } + + event.preventDefault() + } else if (key === 'ArrowUp') { + if (rowCount.length > 0) { + const selectedRow = findNextSelectableRow( + rowCount, + { + direction: 'up', + row: { + section: 0, + row: 0, + }, + }, + this.canSelectRow + ) + if (selectedRow != null) { + this.setState({ selectedRow }, () => { + list.focus() + }) + } + } + + event.preventDefault() + } else if (key === 'Enter') { + // no repositories currently displayed, bail out + if (rowCount.length === 0) { + return event.preventDefault() + } + + const filterText = this.props.filterText + + if (filterText !== undefined && !/\S/.test(filterText)) { + return event.preventDefault() + } + + const row = findNextSelectableRow( + rowCount, + { direction: 'down', row: InvalidRowIndexPath }, + this.canSelectRow + ) + + if (row != null) { + this.onRowClick(row, { kind: 'keyboard', event }) + } + } + } +} + +export function getText( + item: T +): ReadonlyArray { + 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: ISectionFilterListProps +) { + const rows = new Array>>() + const filter = (props.filterText || '').toLowerCase() + let selectedRow = InvalidRowIndexPath + let section = 0 + const selectedItem = props.selectedItem + const groupIndices = [] + + 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 => ({ + score: 1, + matches: { title: [], subtitle: [] }, + item, + })) + + if (!items.length) { + continue + } + + groupIndices.push(idx) + + if (props.renderGroupHeader) { + groupRows.push({ kind: 'group', identifier: group.identifier }) + } + + for (const { item, matches } of items) { + if (selectedItem && item.id === selectedItem.id) { + selectedRow = { + section, + row: groupRows.length, + } + } + + groupRows.push({ kind: 'item', item, matches }) + } + + rows.push(groupRows) + section++ + } + + 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 = getFirstVisibleRow(rows) + } + + return { rows: rows, selectedRow, filterValue: filter, groups: groupIndices } +} + +function getItemFromRowIndex( + items: ReadonlyArray>>, + index: RowIndexPath +): T | null { + if (index.section < 0 || index.section >= items.length) { + return null + } + + 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: RowIndexPath +): string | null { + const item = getItemFromRowIndex(items, index) + return item ? item.id : null +} diff --git a/app/src/ui/repositories-list/repositories-list.tsx b/app/src/ui/repositories-list/repositories-list.tsx index bdf39b8502..a1ec771569 100644 --- a/app/src/ui/repositories-list/repositories-list.tsx +++ b/app/src/ui/repositories-list/repositories-list.tsx @@ -24,6 +24,8 @@ import { TooltippedContent } from '../lib/tooltipped-content' import memoizeOne from 'memoize-one' import { KeyboardShortcut } from '../keyboard-shortcut/keyboard-shortcut' import { generateRepositoryListContextMenu } from '../repositories-list/repository-list-item-context-menu' +import { SectionFilterList } from '../lib/section-filter-list' +import { enableSectionList } from '../../lib/feature-flag' const BlankSlateImage = encodePathAsUrl(__dirname, 'static/empty-no-repo.svg') @@ -241,25 +243,31 @@ export class RepositoriesList extends React.Component< ] : baseGroups + const getGroupAriaLabel = (group: number) => groups[group].identifier + + const ListComponent = enableSectionList() ? SectionFilterList : FilterList + const filterListProps: typeof ListComponent['prototype']['props'] = { + rowHeight: RowHeight, + selectedItem: selectedItem, + filterText: this.props.filterText, + onFilterTextChanged: this.props.onFilterTextChanged, + renderItem: this.renderItem, + renderGroupHeader: this.renderGroupHeader, + onItemClick: this.onItemClick, + renderPostFilter: this.renderPostFilter, + renderNoItems: this.renderNoItems, + groups: groups, + invalidationProps: { + repositories: this.props.repositories, + filterText: this.props.filterText, + }, + onItemContextMenu: this.onItemContextMenu, + getGroupAriaLabel, + } + return (
- - rowHeight={RowHeight} - selectedItem={selectedItem} - filterText={this.props.filterText} - onFilterTextChanged={this.props.onFilterTextChanged} - renderItem={this.renderItem} - renderGroupHeader={this.renderGroupHeader} - onItemClick={this.onItemClick} - renderPostFilter={this.renderPostFilter} - renderNoItems={this.renderNoItems} - groups={groups} - invalidationProps={{ - repositories: this.props.repositories, - filterText: this.props.filterText, - }} - onItemContextMenu={this.onItemContextMenu} - /> +
) } diff --git a/app/src/ui/test-notifications/test-notifications.tsx b/app/src/ui/test-notifications/test-notifications.tsx index ff7c197580..e4e3172986 100644 --- a/app/src/ui/test-notifications/test-notifications.tsx +++ b/app/src/ui/test-notifications/test-notifications.tsx @@ -18,7 +18,8 @@ import { import { DialogHeader } from '../dialog/header' import { Dispatcher } from '../dispatcher' import { Button } from '../lib/button' -import { List } from '../lib/list' +import { RowIndexPath } from '../lib/list/list-row-index-path' +import { SectionList } from '../lib/list/section-list' import { Loading } from '../lib/loading' import { getPullRequestReviewStateIcon } from '../notifications/pull-request-review-helpers' import { Octicon } from '../octicons' @@ -397,9 +398,9 @@ export class TestNotifications extends React.Component< return (
Pull requests: - { - const pullRequest = this.state.pullRequests[row] + private onPullRequestRowClick = (indexPath: RowIndexPath) => { + const pullRequest = this.state.pullRequests[indexPath.row] const stepResults = this.state.stepResults stepResults.set(TestNotificationStepKind.SelectPullRequest, { kind: TestNotificationStepKind.SelectPullRequest, @@ -440,9 +441,9 @@ export class TestNotifications extends React.Component< return (
Reviews: - { - const review = this.state.reviews[row] + private onPullRequestReviewRowClick = (indexPath: RowIndexPath) => { + const review = this.state.reviews[indexPath.row] const stepResults = this.state.stepResults stepResults.set(TestNotificationStepKind.SelectPullRequestReview, { kind: TestNotificationStepKind.SelectPullRequestReview, @@ -483,9 +484,9 @@ export class TestNotifications extends React.Component< return (
Comments: - { - const comment = this.state.comments[row] + private onPullRequestCommentRowClick = (indexPath: RowIndexPath) => { + const comment = this.state.comments[indexPath.row] const stepResults = this.state.stepResults stepResults.set(TestNotificationStepKind.SelectPullRequestComment, { kind: TestNotificationStepKind.SelectPullRequestComment, @@ -513,8 +514,8 @@ export class TestNotifications extends React.Component< ) } - private renderPullRequestCommentRow = (row: number) => { - const comment = this.state.comments[row] + private renderPullRequestCommentRow = (indexPath: RowIndexPath) => { + const comment = this.state.comments[indexPath.row] return ( { - const review = this.state.reviews[row] + private renderPullRequestReviewRow = (indexPath: RowIndexPath) => { + const review = this.state.reviews[indexPath.row] return ( { - const pullRequest = this.state.pullRequests[row] + private renderPullRequestRow = (indexPath: RowIndexPath) => { + const pullRequest = this.state.pullRequests[indexPath.row] const repository = this.props.repository.gitHubRepository const endpointHtmlUrl = getHTMLURL(repository.endpoint) const htmlURL = `${endpointHtmlUrl}/${repository.owner.login}/${repository.name}/pull/${pullRequest.pullRequestNumber}` diff --git a/app/test/unit/section-list-selection-test.ts b/app/test/unit/section-list-selection-test.ts new file mode 100644 index 0000000000..5f88d42dcf --- /dev/null +++ b/app/test/unit/section-list-selection-test.ts @@ -0,0 +1,88 @@ +import { + InvalidRowIndexPath, + rowIndexPathEquals, +} from '../../src/ui/lib/list/list-row-index-path' +import { findNextSelectableRow } from '../../src/ui/lib/list/section-list-selection' + +describe('section-list-selection', () => { + describe('findNextSelectableRow', () => { + const rowCount = [5, 3, 8] + + it('returns first row when selecting down outside list (filter text)', () => { + const selectedRow = findNextSelectableRow(rowCount, { + direction: 'down', + row: InvalidRowIndexPath, + }) + expect(selectedRow?.row).toBe(0) + }) + + it('returns first selectable row when header is first', () => { + const selectedRow = findNextSelectableRow( + rowCount, + { + direction: 'down', + row: InvalidRowIndexPath, + }, + row => { + if (row.section === 0 && row.row === 0) { + return false + } else { + return true + } + } + ) + expect(selectedRow?.row).toBe(1) + }) + + it('returns first row when selecting down from last row', () => { + const lastRow = rowCount[0] - 1 + const selectedRow = findNextSelectableRow(rowCount, { + direction: 'down', + row: { + section: 0, + row: lastRow, + }, + }) + expect(selectedRow?.row).toBe(0) + }) + + it('returns last row when selecting up from top row', () => { + const selectedRow = findNextSelectableRow(rowCount, { + direction: 'up', + row: { + section: 0, + row: 0, + }, + }) + expect( + rowIndexPathEquals(selectedRow!, { section: 2, row: 7 }) + ).toBeTrue() + }) + + it('returns first row of next section when selecting down from last row of a section', () => { + const selectedRow = findNextSelectableRow(rowCount, { + direction: 'down', + row: { + section: 0, + row: 4, + }, + }) + expect( + rowIndexPathEquals(selectedRow!, { section: 1, row: 0 }) + ).toBeTrue() + }) + + it('returns last row of previous section when selecting up from first row of a section', () => { + const selectedRow = findNextSelectableRow(rowCount, { + direction: 'up', + row: { + section: 2, + row: 0, + }, + }) + expect( + rowIndexPathEquals(selectedRow!, { section: 1, row: 2 }) + ).toBeTrue() + }) + }) +}) diff --git a/docs/contributing/linux-testing-and-you.md b/docs/contributing/linux-testing-and-you.md index 7a5f493e4d..ccf71b3ab2 100644 --- a/docs/contributing/linux-testing-and-you.md +++ b/docs/contributing/linux-testing-and-you.md @@ -43,23 +43,3 @@ $ yarn run package If you think you've found a solution, please submit a pull request to [`shiftkey/desktop`](https://github.com/shiftkey/desktop) explaining the change and what it fixes. If you're not quite sure, open an issue on the [`shiftkey/desktop`](https://github.com/shiftkey/desktop) fork explaining what you've found and where you think the problem lies. Maybe someone else has insight into the issue. [**@shiftkey**](https://github.com/shiftkey) will co-ordinate upstreaming merged pull requests to the main repository. - -## Technical Details - -We use `electron-packager` to generate the artifacts and `electron-builder` to generate the installer. - -`electron-packager` details: - -* [API options](https://github.com/electron-userland/electron-packager/blob/development/docs/api.md#options) -* [`dist-info.js` config file](https://github.com/desktop/desktop/blob/development/script/dist-info.js) -* [Usage in Desktop](https://github.com/desktop/desktop/blob/development/script/build.ts#L98-L151) - -`dist-info.js` contains the various metadata we provide to Desktop as part of packaging. This seems fairly stable, but we might need to tweak some things in here for Linux-specific changes. - -`electron-builder` details: - -* [API options](https://www.electron.build/configuration/linux) -* [`electron-builder-linux.yml` config file](https://github.com/desktop/desktop/blob/development/script/electron-builder-linux.yml) -* [Usage in Desktop](https://github.com/desktop/desktop/blob/development/script/package.ts#L124-L145) - -We use `electron-builder-linux.yml` to configure the installers, so please investigate the documentation if you find a problem with an installer to see if something has been overlooked and can be fixed fairly easily. diff --git a/docs/contributing/upgrading-dependencies.md b/docs/contributing/upgrading-dependencies.md index fca0c46f7a..654df34f25 100644 --- a/docs/contributing/upgrading-dependencies.md +++ b/docs/contributing/upgrading-dependencies.md @@ -10,7 +10,6 @@ In the interest of stability and caution we tend to stay a version (or more) beh | Dependency | Versions Behind Latest | | --- | --- | | electron | >= 1 major | -| electron-builder | >= 1 minor | | electron-packager | >= 1 major | | electron-winstaller | >= 1 minor | | typescript | >= 1 minor | @@ -76,7 +75,6 @@ These are the most important dependencies to the app, and include: - `package.json` - `@types/node` - `electron` - - `electron-builder` - `electron-packager` - `electron-winstaller` - `typescript` diff --git a/docs/technical/packaging.md b/docs/technical/packaging.md index d2cd6ce777..17a0599890 100644 --- a/docs/technical/packaging.md +++ b/docs/technical/packaging.md @@ -107,16 +107,8 @@ Other things to note about the Windows packaging process: ### Linux -Desktop uses `electron-builder` to generate these three packages: - - - `.deb` package for Debian-based distributions - - `.rpm` package for RPM-based various distributions - - `.AppImage` package for various distributions (no elevated permissions - required) - - `.snap` package for various distributions - -The `script/electron-builder-linux.yml` configuration file contains the details -applied to each package (if applicable). +Refer to the [`shiftkey/desktop`](https://github.com/shiftkey/desktop) fork +for packaging details about Linux. ## `script/publish.ts` diff --git a/package.json b/package.json index e6224f8253..ab78274af5 100644 --- a/package.json +++ b/package.json @@ -155,7 +155,6 @@ "@types/webpack-merge": "^5.0.0", "@types/xml2js": "^0.4.11", "electron": "24.4.0", - "electron-builder": "^23.6.0", "electron-packager": "^17.1.1", "electron-winstaller": "^5.0.0", "eslint-plugin-github": "^4.3.7", diff --git a/script/electron-builder-linux.yml b/script/electron-builder-linux.yml deleted file mode 100644 index de59d242e2..0000000000 --- a/script/electron-builder-linux.yml +++ /dev/null @@ -1,36 +0,0 @@ -productName: 'GitHubDesktop' -artifactName: '${productName}-${os}-${arch}-${version}.${ext}' -linux: - category: 'GNOME;GTK;Development' - packageCategory: 'GNOME;GTK;Development' - icon: 'app/static/logos' - target: - - deb - - rpm - - AppImage - maintainer: 'GitHub, Inc ' -deb: - afterInstall: './script/linux-after-install.sh' - afterRemove: './script/linux-after-remove.sh' - depends: - # default Electron dependencies - - gconf2 - - gconf-service - - libnotify4 - - libappindicator1 - - libxtst6 - - libnss3 - # dugite-native dependencies - - libcurl3 | libcurl4 - # keytar dependencies - - libsecret-1-0 -rpm: - depends: - # default Electron dependencies - - libXScrnSaver - - libappindicator - - libnotify - # dugite-native dependencies - - libcurl - # keytar dependencies - - libsecret diff --git a/script/linux-after-install.sh b/script/linux-after-install.sh deleted file mode 100644 index b6f0a7d1f0..0000000000 --- a/script/linux-after-install.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -set -e - -PROFILE_D_FILE="/etc/profile.d/github-desktop.sh" -INSTALL_DIR="/opt/${productFilename}" -SCRIPT=$"#!/bin/sh -export PATH=\"$INSTALL_DIR:\$PATH\"" - -case "$1" in - configure) - echo "$SCRIPT" > "${PROFILE_D_FILE}"; - . "${PROFILE_D_FILE}"; - ;; - - abort-upgrade|abort-remove|abort-deconfigure) - ;; - - *) - echo "postinst called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/script/linux-after-remove.sh b/script/linux-after-remove.sh deleted file mode 100644 index 9a73886e2a..0000000000 --- a/script/linux-after-remove.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -set -e - -PROFILE_D_FILE="/etc/profile.d/github-desktop.sh" - -case "$1" in - purge|remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) - echo "#!/bin/sh" > "${PROFILE_D_FILE}"; - . "${PROFILE_D_FILE}"; - rm "${PROFILE_D_FILE}"; - ;; - - *) - echo "postrm called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/script/package.ts b/script/package.ts index a2b05d8d18..a1e2eb8bce 100644 --- a/script/package.ts +++ b/script/package.ts @@ -32,10 +32,8 @@ if (process.platform === 'darwin') { packageOSX() } else if (process.platform === 'win32') { packageWindows() -} else if (process.platform === 'linux') { - packageLinux() } else { - console.error(`I dunno how to package for ${process.platform} :(`) + console.error(`I don't know how to package for ${process.platform} :(`) process.exit(1) } @@ -138,27 +136,3 @@ function packageWindows() { process.exit(1) }) } - -function packageLinux() { - const electronBuilder = path.resolve( - __dirname, - '..', - 'node_modules', - '.bin', - 'electron-builder' - ) - - const configPath = path.resolve(__dirname, 'electron-builder-linux.yml') - - const args = [ - 'build', - '--prepackaged', - distPath, - '--x64', - '--config', - configPath, - ] - - console.log('Packaging for Linux…') - cp.spawnSync(electronBuilder, args, { stdio: 'inherit' }) -} diff --git a/yarn.lock b/yarn.lock index 97783f8b82..2b61047136 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,11 +2,6 @@ # yarn lockfile v1 -"7zip-bin@~5.1.1": - version "5.1.1" - resolved "https://registry.yarnpkg.com/7zip-bin/-/7zip-bin-5.1.1.tgz#9274ec7460652f9c632c59addf24efb1684ef876" - integrity sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ== - "@azure/abort-controller@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-1.1.0.tgz#788ee78457a55af8a1ad342acb182383d2119249" @@ -602,14 +597,6 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@develar/schema-utils@~2.6.5": - version "2.6.5" - resolved "https://registry.yarnpkg.com/@develar/schema-utils/-/schema-utils-2.6.5.tgz#3ece22c5838402419a6e0425f85742b961d9b6c6" - integrity sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig== - dependencies: - ajv "^6.12.0" - ajv-keywords "^3.4.1" - "@electron/asar@^3.2.1": version "3.2.4" resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.2.4.tgz#7e8635a3c4f6d8b3f8ae6efaf5ecb9fbf3bd9864" @@ -655,19 +642,6 @@ minimist "^1.2.6" plist "^3.0.5" -"@electron/universal@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@electron/universal/-/universal-1.2.1.tgz#3c2c4ff37063a4e9ab1e6ff57db0bc619bc82339" - integrity sha512-7323HyMh7KBAl/nPDppdLsC87G6RwRU02dy5FPeGB1eS7rUePh55+WNWiDPLhFQqqVPHzh77M69uhmoT8XnwMQ== - dependencies: - "@malept/cross-spawn-promise" "^1.1.0" - asar "^3.1.0" - debug "^4.3.1" - dir-compare "^2.4.0" - fs-extra "^9.0.1" - minimatch "^3.0.4" - plist "^3.0.4" - "@electron/universal@^1.3.2": version "1.3.4" resolved "https://registry.yarnpkg.com/@electron/universal/-/universal-1.3.4.tgz#bccd94b635d7c85eeed5eabba457eb4ed2be2777" @@ -977,16 +951,6 @@ dependencies: cross-spawn "^7.0.1" -"@malept/flatpak-bundler@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz#e8a32c30a95d20c2b1bb635cc580981a06389858" - integrity sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q== - dependencies: - debug "^4.1.1" - fs-extra "^9.0.0" - lodash "^4.17.15" - tmp-promise "^3.0.2" - "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1056,11 +1020,6 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== -"@tootallnate/once@2": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" - integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== - "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.12" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.12.tgz#4d8e9e51eb265552a7e4f1ff2219ab6133bdfb2d" @@ -1143,13 +1102,6 @@ dependencies: "@types/node" "*" -"@types/debug@^4.1.6": - version "4.1.7" - resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" - integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg== - dependencies: - "@types/ms" "*" - "@types/deep-equal@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/deep-equal/-/deep-equal-1.0.1.tgz#71cfabb247c22bcc16d536111f50c0ed12476b03" @@ -1235,13 +1187,6 @@ dependencies: "@types/node" "*" -"@types/fs-extra@^9.0.11": - version "9.0.13" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45" - integrity sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA== - dependencies: - "@types/node" "*" - "@types/fuzzaldrin-plus@^0.0.1": version "0.0.1" resolved "https://registry.yarnpkg.com/@types/fuzzaldrin-plus/-/fuzzaldrin-plus-0.0.1.tgz#bfe5e25bc5b4066848171baf7a8aaf30e267d30a" @@ -1256,14 +1201,6 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/glob@^7.1.1": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" - integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== - dependencies: - "@types/minimatch" "*" - "@types/node" "*" - "@types/graceful-fs@^4.1.2": version "4.1.4" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.4.tgz#4ff9f641a7c6d1a3508ff88bc3141b152772e753" @@ -1390,11 +1327,6 @@ resolved "https://registry.yarnpkg.com/@types/mri/-/mri-1.1.0.tgz#66555e4d797713789ea0fefdae0898d8170bf5af" integrity sha512-fMl88ZoZXOB7VKazJ6wUMpZc9QIn+jcigSFRf2K/rrw4DcXn+/uGxlWX8DDlcE7JkwgIZ7BDH+JgxZPlc/Ap3g== -"@types/ms@*": - version "0.7.31" - resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" - integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== - "@types/node-fetch@^2.5.0": version "2.6.3" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.3.tgz#175d977f5e24d93ad0f57602693c435c57ad7e80" @@ -1423,7 +1355,7 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== -"@types/plist@^3.0.1", "@types/plist@^3.0.2": +"@types/plist@^3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/plist/-/plist-3.0.2.tgz#61b3727bba0f5c462fe333542534a0c3e19ccb01" integrity sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw== @@ -1638,11 +1570,6 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.9.tgz#fcf01997bbc9f7c09ae5f91383af076d466594e1" integrity sha512-XDwyIlt/47l2kWLTzw/mtrpLdB+GPSskR2n/PIcPn+VYhVO77rGhRncIR5GPU0KRzXuqkDO+J5qqrG0Y8P6jzQ== -"@types/verror@^1.10.3": - version "1.10.6" - resolved "https://registry.yarnpkg.com/@types/verror/-/verror-1.10.6.tgz#3e600c62d210c5826460858f84bcbb65805460bb" - integrity sha512-NNm+gdePAX1VGvPcGZCDKQZKYSiAWigKhKaz5KF94hG6f2s8de9Ow5+7AbXoeKxL8gavZfk4UquSAygOF2duEQ== - "@types/webpack-bundle-analyzer@^4.4.1": version "4.4.1" resolved "https://registry.yarnpkg.com/@types/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.1.tgz#bcc2501be10c8cdd1d170bc6b7847b3321f20440" @@ -1701,13 +1628,6 @@ dependencies: "@types/yargs-parser" "*" -"@types/yargs@^17.0.1": - version "17.0.22" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.22.tgz#7dd37697691b5f17d020f3c63e7a45971ff71e9a" - integrity sha512-pet5WJ9U8yPVRhkwuEIp5ktAeAqRZOq4UdAyWLWzxbtpyXnzbtLdKiXAjJzi/KLmPGS9wk86lUFWZFN6sISo4g== - dependencies: - "@types/yargs-parser" "*" - "@types/yauzl@^2.9.1": version "2.9.1" resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.1.tgz#d10f69f9f522eef3cf98e30afb684a1e1ec923af" @@ -2116,11 +2036,6 @@ ajv-formats@^2.1.1: dependencies: ajv "^8.0.0" -ajv-keywords@^3.4.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.1.tgz#b83ca89c5d42d69031f424cad49aada0236c6957" - integrity sha512-KWcq3xN8fDjSB+IMoh2VaXVhRI0BBGxoYp3rx7Pkb6z0cFjYR9Q9l4yZqqals0/zsioCmocC5H6UvsGD4MoIBA== - ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" @@ -2141,7 +2056,7 @@ ajv@^4.9.2: co "^4.6.0" json-stable-stringify "^1.0.1" -ajv@^6.10.0, ajv@^6.12.0, ajv@^6.12.5, ajv@^6.9.1: +ajv@^6.10.0, ajv@^6.12.5, ajv@^6.9.1: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -2256,43 +2171,6 @@ anymatch@^3.0.3, anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" -app-builder-bin@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-4.0.0.tgz#1df8e654bd1395e4a319d82545c98667d7eed2f0" - integrity sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA== - -app-builder-lib@23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-23.6.0.tgz#03cade02838c077db99d86212d61c5fc1d6da1a8" - integrity sha512-dQYDuqm/rmy8GSCE6Xl/3ShJg6Ab4bZJMT8KaTKGzT436gl1DN4REP3FCWfXoh75qGTJ+u+WsdnnpO9Jl8nyMA== - dependencies: - "7zip-bin" "~5.1.1" - "@develar/schema-utils" "~2.6.5" - "@electron/universal" "1.2.1" - "@malept/flatpak-bundler" "^0.4.0" - async-exit-hook "^2.0.1" - bluebird-lst "^1.0.9" - builder-util "23.6.0" - builder-util-runtime "9.1.1" - chromium-pickle-js "^0.2.0" - debug "^4.3.4" - ejs "^3.1.7" - electron-osx-sign "^0.6.0" - electron-publish "23.6.0" - form-data "^4.0.0" - fs-extra "^10.1.0" - hosted-git-info "^4.1.0" - is-ci "^3.0.0" - isbinaryfile "^4.0.10" - js-yaml "^4.1.0" - lazy-val "^1.0.5" - minimatch "^3.1.2" - read-config-file "6.2.0" - sanitize-filename "^1.6.3" - semver "^7.3.7" - tar "^6.1.11" - temp-file "^3.4.0" - are-docs-informative@^0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/are-docs-informative/-/are-docs-informative-0.0.2.tgz#387f0e93f5d45280373d387a59d34c96db321963" @@ -2438,23 +2316,6 @@ asar@^2.0.1: mkdirp "^0.5.1" tmp-promise "^1.0.5" -asar@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/asar/-/asar-3.2.0.tgz#e6edb5edd6f627ebef04db62f771c61bea9c1221" - integrity sha512-COdw2ZQvKdFGFxXwX3oYh2/sOsJWJegrdJCGxnN4MZ7IULgRBp9P6665aqj9z1v9VwP4oP1hRBojRDQ//IGgAg== - dependencies: - chromium-pickle-js "^0.2.0" - commander "^5.0.0" - glob "^7.1.6" - minimatch "^3.0.4" - optionalDependencies: - "@types/glob" "^7.1.1" - -assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= - assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" @@ -2470,21 +2331,6 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== - -async-exit-hook@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/async-exit-hook/-/async-exit-hook-2.0.1.tgz#8bd8b024b0ec9b1c01cccb9af9db29bd717dfaf3" - integrity sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw== - -async@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" - integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -2600,7 +2446,7 @@ base64-arraybuffer-es6@0.3.1: resolved "https://registry.yarnpkg.com/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.3.1.tgz#fdf0e382f4e2f56caf881f48ee0ce01ae79afe48" integrity sha512-TrhBheudYaff9adiTAqjSScjvtmClQ4vF9l4cqkPNkVsA11m4/NRdH4LkZ/tAMmpzzwfI20BXnJ/PTtafECCNA== -base64-js@^1.3.1, base64-js@^1.5.1: +base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -2628,23 +2474,11 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== -bluebird-lst@^1.0.9: - version "1.0.9" - resolved "https://registry.yarnpkg.com/bluebird-lst/-/bluebird-lst-1.0.9.tgz#a64a0e4365658b9ab5fe875eb9dfb694189bb41c" - integrity sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw== - dependencies: - bluebird "^3.5.5" - bluebird@^3.0.6, bluebird@^3.1.1, bluebird@^3.5.0: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA== -bluebird@^3.5.5: - version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - body-parser@1.19.2: version "1.19.2" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.2.tgz#4714ccd9c157d44797b8b5607d72c0b89952f26e" @@ -2743,11 +2577,6 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= -buffer-equal@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" - integrity sha512-tcBWO2Dl4e7Asr9hTGcpVrCe+F7DubpmqWCTbj4FHLmjqO2hIaC383acQubWtRJhdceqs5uBHs6Es+Sk//RKiQ== - buffer-equal@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.1.tgz#2f7651be5b1b3f057fcd6e7ee16cf34767077d90" @@ -2763,45 +2592,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer@^5.1.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -builder-util-runtime@9.1.1: - version "9.1.1" - resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.1.1.tgz#2da7b34e78a64ad14ccd070d6eed4662d893bd60" - integrity sha512-azRhYLEoDvRDR8Dhis4JatELC/jUvYjm4cVSj7n9dauGTOM2eeNn9KS0z6YA6oDsjI1xphjNbY6PZZeHPzzqaw== - dependencies: - debug "^4.3.4" - sax "^1.2.4" - -builder-util@23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-23.6.0.tgz#1880ec6da7da3fd6fa19b8bd71df7f39e8d17dd9" - integrity sha512-QiQHweYsh8o+U/KNCZFSvISRnvRctb8m/2rB2I1JdByzvNKxPeFLlHFRPQRXab6aYeXc18j9LpsDLJ3sGQmWTQ== - dependencies: - "7zip-bin" "~5.1.1" - "@types/debug" "^4.1.6" - "@types/fs-extra" "^9.0.11" - app-builder-bin "4.0.0" - bluebird-lst "^1.0.9" - builder-util-runtime "9.1.1" - chalk "^4.1.1" - cross-spawn "^7.0.3" - debug "^4.3.4" - fs-extra "^10.0.0" - http-proxy-agent "^5.0.0" - https-proxy-agent "^5.0.0" - is-ci "^3.0.0" - js-yaml "^4.1.0" - source-map-support "^0.5.19" - stat-mode "^1.0.0" - temp-file "^3.4.0" - builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -2929,7 +2719,7 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1: +chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -2957,11 +2747,6 @@ char-regex@^1.0.2: optionalDependencies: fsevents "~2.1.2" -chownr@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" - integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== - chrome-trace-event@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" @@ -2979,11 +2764,6 @@ ci-info@^2.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== -ci-info@^3.2.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" - integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== - cjs-module-lexer@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz#4186fcca0eae175970aee870b9fe2d6cf8d5655f" @@ -3012,14 +2792,6 @@ clean-css@^5.2.2: dependencies: source-map "~0.6.0" -cli-truncate@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" - integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== - dependencies: - slice-ansi "^3.0.0" - string-width "^4.2.0" - cliui@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" @@ -3029,15 +2801,6 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" -cliui@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" - integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.1" - wrap-ansi "^7.0.0" - clone-deep@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" @@ -3101,11 +2864,6 @@ colorette@^2.0.10: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g== -colors@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" - integrity sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw== - combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -3113,13 +2871,6 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" - integrity sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A== - dependencies: - graceful-readlink ">= 1.0.0" - commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -3219,18 +2970,11 @@ core-js@^2.4.1, core-js@^2.5.3: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" integrity sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw== -core-util-is@1.0.2, core-util-is@~1.0.0: +core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= -crc@^3.8.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6" - integrity sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ== - dependencies: - buffer "^5.1.0" - cross-env@^5.0.5: version "5.1.1" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.1.1.tgz#b6d8ab97f304c0f71dae7277b75fe424c08dfa74" @@ -3268,7 +3012,7 @@ cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -3353,7 +3097,7 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" -debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -3516,16 +3260,6 @@ diff@^3.1.0: resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== -dir-compare@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/dir-compare/-/dir-compare-2.4.0.tgz#785c41dc5f645b34343a4eafc50b79bac7f11631" - integrity sha512-l9hmu8x/rjVC9Z2zmGzkhOEowZvW7pmYws5CWHutg8u1JgvsKWMx7Q/UODeu4djLZ4FgW5besw5yvMQnBHzuCA== - dependencies: - buffer-equal "1.0.0" - colors "1.0.3" - commander "2.9.0" - minimatch "3.0.4" - dir-compare@^3.0.0: version "3.3.0" resolved "https://registry.yarnpkg.com/dir-compare/-/dir-compare-3.3.0.tgz#2c749f973b5c4b5d087f11edaae730db31788416" @@ -3541,34 +3275,6 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -dmg-builder@23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-23.6.0.tgz#d39d3871bce996f16c07d2cafe922d6ecbb2a948" - integrity sha512-jFZvY1JohyHarIAlTbfQOk+HnceGjjAdFjVn3n8xlDWKsYNqbO4muca6qXEZTfGXeQMG7TYim6CeS5XKSfSsGA== - dependencies: - app-builder-lib "23.6.0" - builder-util "23.6.0" - builder-util-runtime "9.1.1" - fs-extra "^10.0.0" - iconv-lite "^0.6.2" - js-yaml "^4.1.0" - optionalDependencies: - dmg-license "^1.0.11" - -dmg-license@^1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/dmg-license/-/dmg-license-1.0.11.tgz#7b3bc3745d1b52be7506b4ee80cb61df6e4cd79a" - integrity sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q== - dependencies: - "@types/plist" "^3.0.1" - "@types/verror" "^1.10.3" - ajv "^6.10.0" - crc "^3.8.0" - iconv-corefoundation "^1.1.7" - plist "^3.0.4" - smart-buffer "^4.0.2" - verror "^1.10.0" - doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -3642,16 +3348,6 @@ dot-case@^3.0.4: no-case "^3.0.4" tslib "^2.0.3" -dotenv-expand@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" - integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== - -dotenv@^9.0.2: - version "9.0.2" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-9.0.2.tgz#dacc20160935a37dea6364aa1bef819fb9b6ab05" - integrity sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg== - duplexer@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" @@ -3667,43 +3363,6 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -ejs@^3.1.7: - version "3.1.8" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.8.tgz#758d32910c78047585c7ef1f92f9ee041c1c190b" - integrity sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ== - dependencies: - jake "^10.8.5" - -electron-builder@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-23.6.0.tgz#c79050cbdce90ed96c5feb67c34e9e0a21b5331b" - integrity sha512-y8D4zO+HXGCNxFBV/JlyhFnoQ0Y0K7/sFH+XwIbj47pqaW8S6PGYQbjoObolKBR1ddQFPt4rwp4CnwMJrW3HAw== - dependencies: - "@types/yargs" "^17.0.1" - app-builder-lib "23.6.0" - builder-util "23.6.0" - builder-util-runtime "9.1.1" - chalk "^4.1.1" - dmg-builder "23.6.0" - fs-extra "^10.0.0" - is-ci "^3.0.0" - lazy-val "^1.0.5" - read-config-file "6.2.0" - simple-update-notifier "^1.0.7" - yargs "^17.5.1" - -electron-osx-sign@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/electron-osx-sign/-/electron-osx-sign-0.6.0.tgz#9b69c191d471d9458ef5b1e4fdd52baa059f1bb8" - integrity sha512-+hiIEb2Xxk6eDKJ2FFlpofCnemCbjbT5jz+BKGpVBrRNT3kWTGs4DfNX6IzGwgi33hUcXF+kFs9JW+r6Wc1LRg== - dependencies: - bluebird "^3.5.0" - compare-version "^0.1.2" - debug "^2.6.8" - isbinaryfile "^3.0.2" - minimist "^1.2.0" - plist "^3.0.1" - electron-packager@^17.1.1: version "17.1.1" resolved "https://registry.yarnpkg.com/electron-packager/-/electron-packager-17.1.1.tgz#f156fc63d3a66f4e902e4b42992550a172982d59" @@ -3729,19 +3388,6 @@ electron-packager@^17.1.1: semver "^7.1.3" yargs-parser "^21.1.1" -electron-publish@23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-23.6.0.tgz#ac9b469e0b07752eb89357dd660e5fb10b3d1ce9" - integrity sha512-jPj3y+eIZQJF/+t5SLvsI5eS4mazCbNYqatv5JihbqOstIM13k0d1Z3vAWntvtt13Itl61SO6seicWdioOU5dg== - dependencies: - "@types/fs-extra" "^9.0.11" - builder-util "23.6.0" - builder-util-runtime "9.1.1" - chalk "^4.1.1" - fs-extra "^10.0.0" - lazy-val "^1.0.5" - mime "^2.5.2" - electron-to-chromium@^1.4.202: version "1.4.233" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.233.tgz#aa142e45468bda111b88abc9cc59d573b75d6a60" @@ -4511,11 +4157,6 @@ extract-zip@^2.0.0, extract-zip@^2.0.1: optionalDependencies: "@types/yauzl" "^2.9.1" -extsprintf@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= - fake-indexeddb@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-2.0.4.tgz#401715deb7fc9501866c9f329bde7742599e2de8" @@ -4584,13 +4225,6 @@ file-entry-cache@^5.0.1: dependencies: flat-cache "^2.0.1" -filelist@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.3.tgz#448607750376484932f67ef1b9ff07386b036c83" - integrity sha512-LwjCsruLWQULGYKy7TX0OPtrL9kLpojOFKc5VCTxdFTV7w5zbsgqVKfnkKG7Qgjtq50gKfO56hJv88OfcGb70Q== - dependencies: - minimatch "^5.0.1" - filename-reserved-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz#abf73dfab735d045440abfea2d91f389ebbfa229" @@ -4755,7 +4389,7 @@ fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.0.0, fs-extra@^9.0.1: +fs-extra@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== @@ -4765,13 +4399,6 @@ fs-extra@^9.0.0, fs-extra@^9.0.1: jsonfile "^6.0.1" universalify "^1.0.0" -fs-minipass@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" - integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== - dependencies: - minipass "^3.0.0" - fs-monkey@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3" @@ -4840,7 +4467,7 @@ gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -get-caller-file@^2.0.1, get-caller-file@^2.0.5: +get-caller-file@^2.0.1: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== @@ -5030,11 +4657,6 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== -"graceful-readlink@>= 1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" - integrity sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w== - growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" @@ -5168,13 +4790,6 @@ hosted-git-info@^2.1.4: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== -hosted-git-info@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" - integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== - dependencies: - lru-cache "^6.0.0" - html-encoding-sniffer@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" @@ -5251,15 +4866,6 @@ http-proxy-agent@^4.0.1: agent-base "6" debug "4" -http-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" - integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== - dependencies: - "@tootallnate/once" "2" - agent-base "6" - debug "4" - http2-wrapper@^1.0.0-beta.5.2: version "1.0.3" resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" @@ -5281,14 +4887,6 @@ human-signals@^1.1.1: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== -iconv-corefoundation@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz#31065e6ab2c9272154c8b0821151e2c88f1b002a" - integrity sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ== - dependencies: - cli-truncate "^2.1.0" - node-addon-api "^1.6.3" - iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -5296,23 +4894,11 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.6.2: - version "0.6.3" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - icss-utils@^5.0.0, icss-utils@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== -ieee754@^1.1.13: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" @@ -5466,13 +5052,6 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" -is-ci@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" - integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== - dependencies: - ci-info "^3.2.0" - is-core-module@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" @@ -5734,12 +5313,7 @@ isarray@1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= -isbinaryfile@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621" - integrity sha1-Sj6XTsDLqQBNP8bN5yCeppNopiE= - -isbinaryfile@^4.0.10, isbinaryfile@^4.0.8: +isbinaryfile@^4.0.8: version "4.0.10" resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3" integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw== @@ -5802,16 +5376,6 @@ istanbul-reports@^3.0.2: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jake@^10.8.5: - version "10.8.5" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" - integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw== - dependencies: - async "^3.2.3" - chalk "^4.0.2" - filelist "^1.0.1" - minimatch "^3.0.4" - jest-changed-files@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.6.2.tgz#f6198479e1cc66f22f9ae1e22acaa0b429c042d0" @@ -6420,11 +5984,6 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -json5@^2.2.0: - version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - jsonc-parser@^2.2.1: version "2.3.0" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.0.tgz#7c7fc988ee1486d35734faaaa866fadb00fa91ee" @@ -6554,16 +6113,6 @@ lazy-cache@^2.0.2: dependencies: set-getter "^0.1.0" -lazy-val@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.4.tgz#882636a7245c2cfe6e0a4e3ba6c5d68a137e5c65" - integrity sha512-u93kb2fPbIrfzBuLjZE+w+fJbUUMhNDXxNmMfaqNgpfQf1CO5ZSe2LfsnBqVAk7i/2NF48OSoRj+Xe2VT+lE8Q== - -lazy-val@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.5.tgz#6cf3b9f5bc31cee7ee3e369c0832b7583dcd923d" - integrity sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q== - legal-eagle@0.16.0: version "0.16.0" resolved "https://registry.yarnpkg.com/legal-eagle/-/legal-eagle-0.16.0.tgz#bd3d136dd1b761a540bad898ace68f4009412575" @@ -6945,11 +6494,6 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.5.2: - version "2.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" - integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== - mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -6972,13 +6516,6 @@ mini-css-extract-plugin@^2.5.3: dependencies: schema-utils "^4.0.0" -minimatch@3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - minimatch@^3.0.4, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -7010,26 +6547,6 @@ minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== -minipass@^3.0.0: - version "3.3.6" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" - integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== - dependencies: - yallist "^4.0.0" - -minipass@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.0.3.tgz#00bfbaf1e16e35e804f4aa31a7c1f6b8d9f0ee72" - integrity sha512-OW2r4sQ0sI+z5ckEt5c1Tri4xTgZwYDxpE54eqWlQloQRoWtXjqt9udJ5Z4dSv7wK+nfFI7FRXyCpBSft+gpFw== - -minizlib@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" - integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== - dependencies: - minipass "^3.0.0" - yallist "^4.0.0" - mixin-deep@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.2.0.tgz#d02b8c6f8b6d4b8f5982d3fd009c4919851c3fe2" @@ -7038,7 +6555,7 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^0.1.1" -mkdirp@1.x, mkdirp@^1.0.3: +mkdirp@1.x: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -7121,11 +6638,6 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -node-addon-api@^1.6.3: - version "1.7.2" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d" - integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg== - node-fetch@^2.6.7, node-fetch@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" @@ -7655,7 +7167,7 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -plist@^3.0.0, plist@^3.0.1, plist@^3.0.4, plist@^3.0.5: +plist@^3.0.0, plist@^3.0.4, plist@^3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.6.tgz#7cfb68a856a7834bca6dbfe3218eb9c7740145d3" integrity sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA== @@ -7954,17 +7466,6 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== -read-config-file@6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/read-config-file/-/read-config-file-6.2.0.tgz#71536072330bcd62ba814f91458b12add9fc7ade" - integrity sha512-gx7Pgr5I56JtYz+WuqEbQHj/xWo+5Vwua2jhb1VwM4Wid5PqYmZ4i00ZB0YEGIfkVBsCv9UrjgyqCiQfS/Oosg== - dependencies: - dotenv "^9.0.2" - dotenv-expand "^5.1.0" - js-yaml "^4.1.0" - json5 "^2.2.0" - lazy-val "^1.0.4" - read-installed@4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/read-installed/-/read-installed-4.0.3.tgz#ff9b8b67f187d1e4c29b9feb31f6b223acd19067" @@ -8341,7 +7842,7 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": +"safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -8361,13 +7862,6 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -sanitize-filename@^1.6.3: - version "1.6.3" - resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378" - integrity sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg== - dependencies: - truncate-utf8-bytes "^1.0.0" - sass-loader@^10.0.3: version "10.0.3" resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.0.3.tgz#9e2f1bfdd6355f2adde4e4835d838b020bf800b0" @@ -8386,7 +7880,7 @@ sass@^1.27.0: dependencies: chokidar ">=2.0.0 <4.0.0" -sax@>=0.6.0, sax@^1.2.4: +sax@>=0.6.0: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -8460,11 +7954,6 @@ semver@^7.5.0: dependencies: lru-cache "^6.0.0" -semver@~7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" - integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== - send@0.17.2: version "0.17.2" resolved "https://registry.yarnpkg.com/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820" @@ -8605,13 +8094,6 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= -simple-update-notifier@^1.0.7: - version "1.1.0" - resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz#67694c121de354af592b347cdba798463ed49c82" - integrity sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg== - dependencies: - semver "~7.0.0" - sirv@^1.0.7: version "1.0.19" resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.19.tgz#1d73979b38c7fe91fcba49c85280daa9c2363b49" @@ -8650,25 +8132,11 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" -slice-ansi@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" - integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - slide@~1.1.3: version "1.1.6" resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc= -smart-buffer@^4.0.2: - version "4.2.0" - resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" - integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== - smoothscroll-polyfill@^0.3.6: version "0.3.6" resolved "https://registry.yarnpkg.com/smoothscroll-polyfill/-/smoothscroll-polyfill-0.3.6.tgz#492be845195157cdc2fc529a95d89e7a71509172" @@ -8720,7 +8188,7 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@^0.5.19, source-map-support@^0.5.6, source-map-support@~0.5.20: +source-map-support@^0.5.6, source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== @@ -8819,11 +8287,6 @@ stack-utils@^2.0.2: dependencies: escape-string-regexp "^2.0.0" -stat-mode@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-1.0.0.tgz#68b55cb61ea639ff57136f36b216a291800d1465" - integrity sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg== - static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -8863,15 +8326,6 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string.prototype.matchall@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-3.0.0.tgz#66f4d8dd5c6c6cea4dffb55ec5f3184a8dd0dd59" @@ -9113,26 +8567,6 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -tar@^6.1.11: - version "6.1.13" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b" - integrity sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^4.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - -temp-file@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/temp-file/-/temp-file-3.4.0.tgz#766ea28911c683996c248ef1a20eea04d51652c7" - integrity sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg== - dependencies: - async-exit-hook "^2.0.1" - fs-extra "^10.0.0" - temp@^0.9.0: version "0.9.1" resolved "https://registry.yarnpkg.com/temp/-/temp-0.9.1.tgz#2d666114fafa26966cd4065996d7ceedd4dd4697" @@ -9196,13 +8630,6 @@ tmp-promise@^1.0.5: bluebird "^3.5.0" tmp "0.0.33" -tmp-promise@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-3.0.3.tgz#60a1a1cc98c988674fcbfd23b6e3367bdeac4ce7" - integrity sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ== - dependencies: - tmp "^0.2.0" - tmp@0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -9210,13 +8637,6 @@ tmp@0.0.33: dependencies: os-tmpdir "~1.0.2" -tmp@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" - integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== - dependencies: - rimraf "^3.0.0" - tmpl@1.0.x: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -9332,13 +8752,6 @@ trim-repeated@^1.0.0: dependencies: escape-string-regexp "^1.0.2" -truncate-utf8-bytes@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b" - integrity sha1-QFkjkJWS1W94pYGENLC3hInKXys= - dependencies: - utf8-byte-length "^1.0.1" - ts-jest@^26.4.4: version "26.4.4" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.4.4.tgz#61f13fb21ab400853c532270e52cc0ed7e502c49" @@ -9603,11 +9016,6 @@ use@^2.0.0: isobject "^3.0.0" lazy-cache "^2.0.2" -utf8-byte-length@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61" - integrity sha1-9F8VDExm7uloGGUFq5P8u4rWv2E= - util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -9665,15 +9073,6 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= -verror@^1.10.0: - version "1.10.1" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.1.tgz#4bf09eeccf4563b109ed4b3d458380c972b0cdeb" - integrity sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg== - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - vscode-json-languageservice@^3.7.0: version "3.8.0" resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-3.8.0.tgz#c7e7283f993e3db39fa5501407b023ada6fd3ae3" @@ -9938,15 +9337,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -10012,11 +9402,6 @@ y18n@^4.0.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" @@ -10062,19 +9447,6 @@ yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^17.5.1: - version "17.7.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.0.tgz#b21e9af1e0a619a2a9c67b1133219b2975a07985" - integrity sha512-dwqOPg5trmrre9+v8SUo2q/hAwyKoVfu8OC1xPHKJGNdxAvPl4sKxL4vBnh3bQz/ZvvGAFeA5H3ou2kcOY8sQQ== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.1.1" - yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"