addons/isl/src/FileTree.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {Comparison} from 'shared/Comparison';
b69ab319import type {ChangedFilesDisplayType} from './ChangedFileDisplayTypePicker';
b69ab3110import type {Place, UIChangedFile} from './UncommittedChanges';
b69ab3111import type {UseUncommittedSelection} from './partialSelection';
b69ab3112import type {PathTree} from './pathTree';
b69ab3113
b69ab3114import {Button} from 'isl-components/Button';
b69ab3115import {Checkbox} from 'isl-components/Checkbox';
b69ab3116import {Icon} from 'isl-components/Icon';
b69ab3117import {useMemo, useState} from 'react';
b69ab3118import {mapIterable} from 'shared/utils';
b69ab3119import {File} from './ChangedFile';
b69ab3120import {buildPathTree, calculateTreeSelectionStates} from './pathTree';
b69ab3121
b69ab3122export function FileTreeFolderHeader({
b69ab3123 isCollapsed,
b69ab3124 toggleCollapsed,
b69ab3125 checkedState,
b69ab3126 toggleChecked,
b69ab3127 folder,
b69ab3128}: {
b69ab3129 isCollapsed: boolean;
b69ab3130 toggleCollapsed: () => void;
b69ab3131 checkedState?: true | false | 'indeterminate';
b69ab3132 toggleChecked?: (checked: boolean) => void;
b69ab3133 folder: string;
b69ab3134}) {
b69ab3135 return (
b69ab3136 <span className="file-tree-folder-path">
b69ab3137 {checkedState != null && toggleChecked != null && (
b69ab3138 <Checkbox
b69ab3139 checked={checkedState === true}
b69ab3140 indeterminate={checkedState === 'indeterminate'}
b69ab3141 onChange={() => toggleChecked(checkedState !== true)}
b69ab3142 />
b69ab3143 )}
b69ab3144 <Button icon onClick={toggleCollapsed}>
b69ab3145 <Icon icon={isCollapsed ? 'chevron-right' : 'chevron-down'} slot="start" />
b69ab3146 {folder}
b69ab3147 </Button>
b69ab3148 </span>
b69ab3149 );
b69ab3150}
b69ab3151
b69ab3152function* iteratePathTree(tree: PathTree<UIChangedFile>): Generator<UIChangedFile> {
b69ab3153 for (const node of tree.values()) {
b69ab3154 if (node instanceof Map) {
b69ab3155 yield* iteratePathTree(node);
b69ab3156 } else {
b69ab3157 yield node;
b69ab3158 }
b69ab3159 }
b69ab3160}
b69ab3161
b69ab3162export function FileTree(props: {
b69ab3163 files: Array<UIChangedFile>;
b69ab3164 displayType: ChangedFilesDisplayType;
b69ab3165 comparison: Comparison;
b69ab3166 selection?: UseUncommittedSelection;
b69ab3167 place?: Place;
b69ab3168}) {
b69ab3169 const {files, ...rest} = props;
b69ab3170
b69ab3171 const tree = useMemo(
b69ab3172 () => buildPathTree(Object.fromEntries(files.map(file => [file.path, file]))),
b69ab3173 [files],
b69ab3174 );
b69ab3175
b69ab3176 const directoryCheckedStates = useMemo(
b69ab3177 () => (props.selection == null ? null : calculateTreeSelectionStates(tree, props.selection)),
b69ab3178 [tree, props.selection],
b69ab3179 );
b69ab3180
b69ab3181 const [collapsed, setCollapsed] = useState(new Set());
b69ab3182
b69ab3183 function renderTree(tree: PathTree<UIChangedFile>, accumulatedPath = '') {
b69ab3184 return (
b69ab3185 <div className="file-tree">
b69ab3186 {[...tree.entries()].map(([folder, inner]) => {
b69ab3187 const folderKey = `${accumulatedPath}/${folder}`;
b69ab3188 const isCollapsed = collapsed.has(folderKey);
b69ab3189
b69ab3190 let content;
b69ab3191 if (inner instanceof Map) {
b69ab3192 const checkedState = directoryCheckedStates?.get(folderKey);
b69ab3193 content = (
b69ab3194 <>
b69ab3195 <FileTreeFolderHeader
b69ab3196 isCollapsed={isCollapsed}
b69ab3197 checkedState={checkedState}
b69ab3198 toggleChecked={
b69ab3199 rest.selection == null
b69ab31100 ? undefined
b69ab31101 : checked => {
b69ab31102 const paths = mapIterable(iteratePathTree(inner), file => file.path);
b69ab31103 if (checked) {
b69ab31104 rest.selection?.select(...paths);
b69ab31105 } else {
b69ab31106 rest.selection?.deselect(...paths);
b69ab31107 }
b69ab31108 }
b69ab31109 }
b69ab31110 toggleCollapsed={() => {
b69ab31111 setCollapsed(last =>
b69ab31112 isCollapsed
b69ab31113 ? new Set([...last].filter(v => v !== folderKey))
b69ab31114 : new Set([...last, folderKey]),
b69ab31115 );
b69ab31116 }}
b69ab31117 folder={folder}
b69ab31118 />
b69ab31119 {isCollapsed ? null : renderTree(inner, folderKey)}
b69ab31120 </>
b69ab31121 );
b69ab31122 } else {
b69ab31123 content = <File key={inner.path} {...rest} file={inner} />;
b69ab31124 }
b69ab31125
b69ab31126 return (
b69ab31127 <div className="file-tree-level" key={folderKey}>
b69ab31128 {content}
b69ab31129 </div>
b69ab31130 );
b69ab31131 })}
b69ab31132 </div>
b69ab31133 );
b69ab31134 }
b69ab31135
b69ab31136 return renderTree(tree);
b69ab31137}