Skip to content

Commit f17d9a0

Browse files
authored
add ability to collapse directories in code browser (#2407)
1 parent 175c28b commit f17d9a0

4 files changed

Lines changed: 165 additions & 45 deletions

File tree

components/Icons/index.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1717,3 +1717,26 @@ export function MinimizeIcon({ width = 24, height = 24, style }: IconProps) {
17171717
</Svg>
17181718
);
17191719
}
1720+
1721+
export function FolderOpenIcon({ width = 24, height = 24, style }: IconProps) {
1722+
return (
1723+
<Svg width={width} height={height} viewBox="0 0 256 256" style={style}>
1724+
<path
1725+
d="M32,208V64a8,8,0,0,1,8-8H93.33a8,8,0,0,1,4.8,1.6L128,80h72a8,8,0,0,1,8,8v24"
1726+
fill="none"
1727+
stroke="currentColor"
1728+
strokeLinecap="round"
1729+
strokeLinejoin="round"
1730+
strokeWidth="16"
1731+
/>
1732+
<path
1733+
d="M32,208l30.18-90.53A8,8,0,0,1,69.77,112H232a8,8,0,0,1,7.59,10.53L211.09,208Z"
1734+
fill="none"
1735+
stroke="currentColor"
1736+
strokeLinecap="round"
1737+
strokeLinejoin="round"
1738+
strokeWidth="16"
1739+
/>
1740+
</Svg>
1741+
);
1742+
}

components/Package/CodeBrowser/CodeBrowserFileRow.tsx

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import { Pressable, View } from 'react-native';
33

44
import { P } from '~/common/styleguide';
55
import {
6+
Arrow,
67
FileIcon,
78
FileMetadataIcon,
89
FolderIcon,
10+
FolderOpenIcon,
911
ImageFileIcon,
1012
WarningBlockquote,
1113
} from '~/components/Icons';
@@ -18,6 +20,7 @@ type Props = {
1820
depth?: number;
1921
isActive?: boolean;
2022
isDirectory?: boolean;
23+
isCollapsed?: boolean;
2124
isNested?: boolean;
2225
onPress?: () => void;
2326
};
@@ -27,6 +30,7 @@ export default function CodeBrowserFileRow({
2730
depth = 0,
2831
isActive = false,
2932
isDirectory = false,
33+
isCollapsed = false,
3034
isNested = false,
3135
onPress,
3236
}: Props) {
@@ -37,22 +41,37 @@ export default function CodeBrowserFileRow({
3741

3842
const Icon = useMemo(() => {
3943
if (isDirectory) {
40-
return FolderIcon;
44+
if (isCollapsed) {
45+
return FolderIcon;
46+
}
47+
return FolderOpenIcon;
4148
} else if (isNested) {
4249
return FileMetadataIcon;
4350
} else if (isImageFile) {
4451
return ImageFileIcon;
4552
}
4653
return FileIcon;
47-
}, [isDirectory, isNested, isImageFile]);
54+
}, [isDirectory, isNested, isImageFile, isCollapsed]);
4855

4956
const rowStyle = [
5057
tw`flex flex-row items-center gap-1.5 px-3 py-[3px] last:mb-20`,
58+
isDirectory && tw`pl-1.5`,
5159
{ paddingLeft: (isNested ? 6 : 10) + depth * 8 },
5260
];
61+
const hasTrailingContent = warning != null || isDirectory;
5362

5463
const content = (
5564
<>
65+
{isDirectory ? (
66+
<Arrow
67+
style={[
68+
tw`size-2.5 shrink-0 text-palette-gray4 dark:text-palette-gray5`,
69+
isCollapsed ? tw`rotate-90` : tw`rotate-270`,
70+
]}
71+
/>
72+
) : (
73+
<View style={tw`size-2.5`} />
74+
)}
5675
<Icon
5776
style={[
5877
tw`size-4 shrink-0 text-icon`,
@@ -72,15 +91,19 @@ export default function CodeBrowserFileRow({
7291
]}>
7392
{label}
7493
</P>
75-
{warning && (
76-
<Tooltip
77-
trigger={
78-
<View style={tw`ml-auto`}>
79-
<WarningBlockquote style={tw`size-3.5 text-warning-dark dark:text-warning`} />
80-
</View>
81-
}>
82-
<P style={tw`text-[12px] font-light`}>{warning.message}</P>
83-
</Tooltip>
94+
{hasTrailingContent && (
95+
<View style={tw`ml-auto flex-row items-center gap-1.5`}>
96+
{warning && (
97+
<Tooltip
98+
trigger={
99+
<View>
100+
<WarningBlockquote style={tw`size-3.5 text-warning-dark dark:text-warning`} />
101+
</View>
102+
}>
103+
<P style={tw`text-[12px] font-light`}>{warning.message}</P>
104+
</Tooltip>
105+
)}
106+
</View>
84107
)}
85108
</>
86109
);
Lines changed: 104 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import { useEffect, useMemo, useState } from 'react';
12
import { View } from 'react-native';
23

3-
import { type CodeBrowserTreeDirectory } from '~/types';
4+
import { type CodeBrowserTreeDirectory, type CodeBrowserTreeFile } from '~/types';
45

56
import CodeBrowserFileRow from './CodeBrowserFileRow';
67

@@ -10,6 +11,7 @@ type Props = {
1011
onSelectFile: (filePath: string) => void;
1112
depth?: number;
1213
isNested?: boolean;
14+
isSearchActive?: boolean;
1315
};
1416

1517
export default function CodeBrowserFileTree({
@@ -18,27 +20,26 @@ export default function CodeBrowserFileTree({
1820
onSelectFile,
1921
depth = 0,
2022
isNested = false,
23+
isSearchActive = false,
2124
}: Props) {
22-
const directories = Object.values(tree.directories).sort((a, b) => a.name.localeCompare(b.name));
25+
const directories = useMemo(
26+
() => Object.values(tree.directories).sort((a, b) => a.name.localeCompare(b.name)),
27+
[tree.directories]
28+
);
2329
const files = [...tree.files].sort((a, b) => a.name.localeCompare(b.name));
2430

2531
return (
2632
<>
27-
{directories.map(directory => {
28-
const collapsedDirectory = collapseDirectoryPath(directory);
29-
30-
return (
31-
<View key={directory.path}>
32-
<CodeBrowserFileRow label={collapsedDirectory.label} depth={depth} isDirectory />
33-
<CodeBrowserFileTree
34-
tree={collapsedDirectory.directory}
35-
activeFile={activeFile}
36-
onSelectFile={onSelectFile}
37-
depth={depth + 1}
38-
/>
39-
</View>
40-
);
41-
})}
33+
{directories.map(directory => (
34+
<CodeBrowserDirectoryRow
35+
key={directory.path}
36+
directory={directory}
37+
activeFile={activeFile}
38+
onSelectFile={onSelectFile}
39+
depth={depth}
40+
isSearchActive={isSearchActive}
41+
/>
42+
))}
4243
{files.map(file => (
4344
<View key={file.path}>
4445
<CodeBrowserFileRow
@@ -60,6 +61,7 @@ export default function CodeBrowserFileTree({
6061
onSelectFile={onSelectFile}
6162
depth={depth + 1}
6263
isNested
64+
isSearchActive={isSearchActive}
6365
/>
6466
)}
6567
</View>
@@ -68,22 +70,93 @@ export default function CodeBrowserFileTree({
6870
);
6971
}
7072

71-
function collapseDirectoryPath(directory: CodeBrowserTreeDirectory) {
72-
const pathSegments = [directory.name];
73-
let collapsedDirectory = directory;
73+
type CodeBrowserDirectoryRowProps = {
74+
directory: CodeBrowserTreeDirectory;
75+
activeFile: string | null;
76+
onSelectFile: (filePath: string) => void;
77+
depth: number;
78+
isSearchActive: boolean;
79+
};
80+
81+
function CodeBrowserDirectoryRow({
82+
directory,
83+
activeFile,
84+
onSelectFile,
85+
depth,
86+
isSearchActive,
87+
}: CodeBrowserDirectoryRowProps) {
88+
const [collapsed, setCollapsed] = useState(false);
89+
90+
const collapsedDirectory = useMemo(() => {
91+
const pathSegments = [directory.name];
92+
let collapsedDirectory = directory;
7493

75-
while (
76-
collapsedDirectory.files.length === 0 &&
77-
Object.keys(collapsedDirectory.directories).length === 1
78-
) {
79-
const [nextDirectory] = Object.values(collapsedDirectory.directories);
94+
while (
95+
collapsedDirectory.files.length === 0 &&
96+
Object.keys(collapsedDirectory.directories).length === 1
97+
) {
98+
const [nextDirectory] = Object.values(collapsedDirectory.directories);
8099

81-
pathSegments.push(nextDirectory.name);
82-
collapsedDirectory = nextDirectory;
100+
pathSegments.push(nextDirectory.name);
101+
collapsedDirectory = nextDirectory;
102+
}
103+
104+
return {
105+
directory: collapsedDirectory,
106+
label: pathSegments.join('/'),
107+
};
108+
}, [directory]);
109+
110+
const shouldForceExpand =
111+
isSearchActive || directoryContainsFile(collapsedDirectory.directory, activeFile);
112+
113+
useEffect(() => {
114+
if (shouldForceExpand) {
115+
setCollapsed(false);
116+
}
117+
}, [shouldForceExpand]);
118+
119+
return (
120+
<View>
121+
<CodeBrowserFileRow
122+
label={collapsedDirectory.label}
123+
depth={depth}
124+
isDirectory
125+
isCollapsed={collapsed}
126+
onPress={() => setCollapsed(currentCollapsed => !currentCollapsed)}
127+
/>
128+
{!collapsed && (
129+
<CodeBrowserFileTree
130+
tree={collapsedDirectory.directory}
131+
activeFile={activeFile}
132+
onSelectFile={onSelectFile}
133+
depth={depth + 1}
134+
isSearchActive={isSearchActive}
135+
/>
136+
)}
137+
</View>
138+
);
139+
}
140+
141+
function directoryContainsFile(
142+
directory: CodeBrowserTreeDirectory,
143+
activeFile: string | null
144+
): boolean {
145+
if (!activeFile) {
146+
return false;
83147
}
84148

85-
return {
86-
directory: collapsedDirectory,
87-
label: pathSegments.join('/'),
88-
};
149+
return (
150+
directory.files.some(file => fileContainsPath(file, activeFile)) ||
151+
Object.values(directory.directories).some(childDirectory =>
152+
directoryContainsFile(childDirectory, activeFile)
153+
)
154+
);
155+
}
156+
157+
function fileContainsPath(file: CodeBrowserTreeFile, activeFile: string): boolean {
158+
return (
159+
file.path === activeFile ||
160+
file.nestedFiles?.some(nestedFile => fileContainsPath(nestedFile, activeFile)) === true
161+
);
89162
}

components/Package/CodeBrowser/index.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,9 +200,9 @@ export default function CodeBrowser({
200200
id="codeBrowserList"
201201
style={[
202202
tw`border-palette-gray2 dark:border-default`,
203-
!isSmallScreen && tw`w-[320px] flex-grow border-r`,
204-
!isSmallScreen && isBrowserMaximized && tw`w-[16vw] min-w-[320px]`,
205-
isSmallScreen && tw`h-[320px] flex-grow-0 border-b`,
203+
!isSmallScreen && tw`w-[340px] flex-grow border-r`,
204+
!isSmallScreen && isBrowserMaximized && tw`w-[16vw] min-w-[340px]`,
205+
isSmallScreen && tw`h-[300px] flex-grow-0 border-b`,
206206
]}
207207
contentContainerStyle={tw`pt-2`}>
208208
{filteredFiles.length > 0 ? (
@@ -211,6 +211,7 @@ export default function CodeBrowser({
211211
tree={fileTree}
212212
activeFile={activeFile}
213213
onSelectFile={onSelectFile}
214+
isSearchActive={Boolean(normalizedSearch)}
214215
/>
215216
<View style={tw`h-2`} />
216217
</>

0 commit comments

Comments
 (0)