| b69ab31 | | | 1 | /** |
| b69ab31 | | | 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| b69ab31 | | | 3 | * |
| b69ab31 | | | 4 | * This source code is licensed under the MIT license found in the |
| b69ab31 | | | 5 | * LICENSE file in the root directory of this source tree. |
| b69ab31 | | | 6 | */ |
| b69ab31 | | | 7 | |
| b69ab31 | | | 8 | import type {Comparison} from 'shared/Comparison'; |
| b69ab31 | | | 9 | import type {ChangedFilesDisplayType} from './ChangedFileDisplayTypePicker'; |
| b69ab31 | | | 10 | import type {Place, UIChangedFile} from './UncommittedChanges'; |
| b69ab31 | | | 11 | import type {UseUncommittedSelection} from './partialSelection'; |
| b69ab31 | | | 12 | import type {PathTree} from './pathTree'; |
| b69ab31 | | | 13 | |
| b69ab31 | | | 14 | import {Button} from 'isl-components/Button'; |
| b69ab31 | | | 15 | import {Checkbox} from 'isl-components/Checkbox'; |
| b69ab31 | | | 16 | import {Icon} from 'isl-components/Icon'; |
| b69ab31 | | | 17 | import {useMemo, useState} from 'react'; |
| b69ab31 | | | 18 | import {mapIterable} from 'shared/utils'; |
| b69ab31 | | | 19 | import {File} from './ChangedFile'; |
| b69ab31 | | | 20 | import {buildPathTree, calculateTreeSelectionStates} from './pathTree'; |
| b69ab31 | | | 21 | |
| b69ab31 | | | 22 | export function FileTreeFolderHeader({ |
| b69ab31 | | | 23 | isCollapsed, |
| b69ab31 | | | 24 | toggleCollapsed, |
| b69ab31 | | | 25 | checkedState, |
| b69ab31 | | | 26 | toggleChecked, |
| b69ab31 | | | 27 | folder, |
| b69ab31 | | | 28 | }: { |
| b69ab31 | | | 29 | isCollapsed: boolean; |
| b69ab31 | | | 30 | toggleCollapsed: () => void; |
| b69ab31 | | | 31 | checkedState?: true | false | 'indeterminate'; |
| b69ab31 | | | 32 | toggleChecked?: (checked: boolean) => void; |
| b69ab31 | | | 33 | folder: string; |
| b69ab31 | | | 34 | }) { |
| b69ab31 | | | 35 | return ( |
| b69ab31 | | | 36 | <span className="file-tree-folder-path"> |
| b69ab31 | | | 37 | {checkedState != null && toggleChecked != null && ( |
| b69ab31 | | | 38 | <Checkbox |
| b69ab31 | | | 39 | checked={checkedState === true} |
| b69ab31 | | | 40 | indeterminate={checkedState === 'indeterminate'} |
| b69ab31 | | | 41 | onChange={() => toggleChecked(checkedState !== true)} |
| b69ab31 | | | 42 | /> |
| b69ab31 | | | 43 | )} |
| b69ab31 | | | 44 | <Button icon onClick={toggleCollapsed}> |
| b69ab31 | | | 45 | <Icon icon={isCollapsed ? 'chevron-right' : 'chevron-down'} slot="start" /> |
| b69ab31 | | | 46 | {folder} |
| b69ab31 | | | 47 | </Button> |
| b69ab31 | | | 48 | </span> |
| b69ab31 | | | 49 | ); |
| b69ab31 | | | 50 | } |
| b69ab31 | | | 51 | |
| b69ab31 | | | 52 | function* iteratePathTree(tree: PathTree<UIChangedFile>): Generator<UIChangedFile> { |
| b69ab31 | | | 53 | for (const node of tree.values()) { |
| b69ab31 | | | 54 | if (node instanceof Map) { |
| b69ab31 | | | 55 | yield* iteratePathTree(node); |
| b69ab31 | | | 56 | } else { |
| b69ab31 | | | 57 | yield node; |
| b69ab31 | | | 58 | } |
| b69ab31 | | | 59 | } |
| b69ab31 | | | 60 | } |
| b69ab31 | | | 61 | |
| b69ab31 | | | 62 | export function FileTree(props: { |
| b69ab31 | | | 63 | files: Array<UIChangedFile>; |
| b69ab31 | | | 64 | displayType: ChangedFilesDisplayType; |
| b69ab31 | | | 65 | comparison: Comparison; |
| b69ab31 | | | 66 | selection?: UseUncommittedSelection; |
| b69ab31 | | | 67 | place?: Place; |
| b69ab31 | | | 68 | }) { |
| b69ab31 | | | 69 | const {files, ...rest} = props; |
| b69ab31 | | | 70 | |
| b69ab31 | | | 71 | const tree = useMemo( |
| b69ab31 | | | 72 | () => buildPathTree(Object.fromEntries(files.map(file => [file.path, file]))), |
| b69ab31 | | | 73 | [files], |
| b69ab31 | | | 74 | ); |
| b69ab31 | | | 75 | |
| b69ab31 | | | 76 | const directoryCheckedStates = useMemo( |
| b69ab31 | | | 77 | () => (props.selection == null ? null : calculateTreeSelectionStates(tree, props.selection)), |
| b69ab31 | | | 78 | [tree, props.selection], |
| b69ab31 | | | 79 | ); |
| b69ab31 | | | 80 | |
| b69ab31 | | | 81 | const [collapsed, setCollapsed] = useState(new Set()); |
| b69ab31 | | | 82 | |
| b69ab31 | | | 83 | function renderTree(tree: PathTree<UIChangedFile>, accumulatedPath = '') { |
| b69ab31 | | | 84 | return ( |
| b69ab31 | | | 85 | <div className="file-tree"> |
| b69ab31 | | | 86 | {[...tree.entries()].map(([folder, inner]) => { |
| b69ab31 | | | 87 | const folderKey = `${accumulatedPath}/${folder}`; |
| b69ab31 | | | 88 | const isCollapsed = collapsed.has(folderKey); |
| b69ab31 | | | 89 | |
| b69ab31 | | | 90 | let content; |
| b69ab31 | | | 91 | if (inner instanceof Map) { |
| b69ab31 | | | 92 | const checkedState = directoryCheckedStates?.get(folderKey); |
| b69ab31 | | | 93 | content = ( |
| b69ab31 | | | 94 | <> |
| b69ab31 | | | 95 | <FileTreeFolderHeader |
| b69ab31 | | | 96 | isCollapsed={isCollapsed} |
| b69ab31 | | | 97 | checkedState={checkedState} |
| b69ab31 | | | 98 | toggleChecked={ |
| b69ab31 | | | 99 | rest.selection == null |
| b69ab31 | | | 100 | ? undefined |
| b69ab31 | | | 101 | : checked => { |
| b69ab31 | | | 102 | const paths = mapIterable(iteratePathTree(inner), file => file.path); |
| b69ab31 | | | 103 | if (checked) { |
| b69ab31 | | | 104 | rest.selection?.select(...paths); |
| b69ab31 | | | 105 | } else { |
| b69ab31 | | | 106 | rest.selection?.deselect(...paths); |
| b69ab31 | | | 107 | } |
| b69ab31 | | | 108 | } |
| b69ab31 | | | 109 | } |
| b69ab31 | | | 110 | toggleCollapsed={() => { |
| b69ab31 | | | 111 | setCollapsed(last => |
| b69ab31 | | | 112 | isCollapsed |
| b69ab31 | | | 113 | ? new Set([...last].filter(v => v !== folderKey)) |
| b69ab31 | | | 114 | : new Set([...last, folderKey]), |
| b69ab31 | | | 115 | ); |
| b69ab31 | | | 116 | }} |
| b69ab31 | | | 117 | folder={folder} |
| b69ab31 | | | 118 | /> |
| b69ab31 | | | 119 | {isCollapsed ? null : renderTree(inner, folderKey)} |
| b69ab31 | | | 120 | </> |
| b69ab31 | | | 121 | ); |
| b69ab31 | | | 122 | } else { |
| b69ab31 | | | 123 | content = <File key={inner.path} {...rest} file={inner} />; |
| b69ab31 | | | 124 | } |
| b69ab31 | | | 125 | |
| b69ab31 | | | 126 | return ( |
| b69ab31 | | | 127 | <div className="file-tree-level" key={folderKey}> |
| b69ab31 | | | 128 | {content} |
| b69ab31 | | | 129 | </div> |
| b69ab31 | | | 130 | ); |
| b69ab31 | | | 131 | })} |
| b69ab31 | | | 132 | </div> |
| b69ab31 | | | 133 | ); |
| b69ab31 | | | 134 | } |
| b69ab31 | | | 135 | |
| b69ab31 | | | 136 | return renderTree(tree); |
| b69ab31 | | | 137 | } |