xref: /expo/docs/ui/components/FileTree/index.tsx (revision 920d7a63)
1a9f9618cSEvan Baconimport { FileCode01Icon, LayoutAlt01Icon, FolderIcon } from '@expo/styleguide-icons';
2c7504584SBartosz Kaszubowskiimport { HTMLAttributes, ReactNode } from 'react';
3c7504584SBartosz Kaszubowski
4c7504584SBartosz Kaszubowskitype FileTreeProps = HTMLAttributes<HTMLDivElement> & {
5215a1323SEvan Bacon  files?: (string | [string, string])[];
6c7504584SBartosz Kaszubowski};
7c7504584SBartosz Kaszubowski
8c7504584SBartosz Kaszubowskitype FileObject = {
9215a1323SEvan Bacon  name: string;
10215a1323SEvan Bacon  note?: string;
11215a1323SEvan Bacon  files: FileObject[];
12c7504584SBartosz Kaszubowski};
13c7504584SBartosz Kaszubowski
14c7504584SBartosz Kaszubowskiexport function FileTree({ files = [], ...rest }: FileTreeProps) {
15c7504584SBartosz Kaszubowski  return (
16215a1323SEvan Bacon    <div
17215a1323SEvan Bacon      className="text-xs border border-default rounded-md bg-default mb-4 p-2 pr-4 pb-4 whitespace-nowrap overflow-x-auto"
18215a1323SEvan Bacon      {...rest}>
19c7504584SBartosz Kaszubowski      {renderStructure(generateStructure(files))}
20c7504584SBartosz Kaszubowski    </div>
21c7504584SBartosz Kaszubowski  );
22c7504584SBartosz Kaszubowski}
23c7504584SBartosz Kaszubowski
24215a1323SEvan Bacon/**
25215a1323SEvan Bacon * Given an array of file paths, generate a tree structure.
26215a1323SEvan Bacon * @param files
27215a1323SEvan Bacon * @returns
28215a1323SEvan Bacon */
29215a1323SEvan Baconfunction generateStructure(files: (string | [string, string])[]): FileObject[] {
30215a1323SEvan Bacon  const structure: FileObject[] = [];
31215a1323SEvan Bacon
32215a1323SEvan Bacon  function modifyPath(path: string, note?: string) {
33215a1323SEvan Bacon    const parts = path.split('/');
34215a1323SEvan Bacon    let currentLevel = structure;
35215a1323SEvan Bacon    parts.forEach((part, index) => {
36215a1323SEvan Bacon      const existingPath = currentLevel.find(item => item.name === part);
37215a1323SEvan Bacon      if (existingPath) {
38215a1323SEvan Bacon        currentLevel = existingPath.files;
39215a1323SEvan Bacon      } else {
40215a1323SEvan Bacon        const newPart: FileObject = {
41215a1323SEvan Bacon          name: part,
42215a1323SEvan Bacon          files: [],
43215a1323SEvan Bacon        };
44215a1323SEvan Bacon        if (note && index === parts.length - 1) {
45215a1323SEvan Bacon          newPart.note = note;
46215a1323SEvan Bacon        }
47215a1323SEvan Bacon        currentLevel.push(newPart);
48215a1323SEvan Bacon        currentLevel = newPart.files;
49215a1323SEvan Bacon      }
50215a1323SEvan Bacon    });
51215a1323SEvan Bacon  }
52215a1323SEvan Bacon
53215a1323SEvan Bacon  files.forEach(path => {
54215a1323SEvan Bacon    if (Array.isArray(path)) {
55215a1323SEvan Bacon      return modifyPath(path[0], path[1]);
56215a1323SEvan Bacon    } else {
57215a1323SEvan Bacon      return modifyPath(path);
58215a1323SEvan Bacon    }
59215a1323SEvan Bacon  });
60215a1323SEvan Bacon
61c7504584SBartosz Kaszubowski  return structure;
62c7504584SBartosz Kaszubowski}
63c7504584SBartosz Kaszubowski
64215a1323SEvan Baconfunction renderStructure(structure: FileObject[], level = 0): ReactNode {
65215a1323SEvan Bacon  return structure.map(({ name, note, files }, index) => {
66215a1323SEvan Bacon    const FileIcon = getIconForFile(name);
67215a1323SEvan Bacon    return files.length ? (
68215a1323SEvan Bacon      <div key={name + '_' + index} className="mt-1 pt-1 pl-2 rounded-sm flex flex-col">
69c7504584SBartosz Kaszubowski        <div className="flex items-center">
70c7504584SBartosz Kaszubowski          {' '.repeat(level)}
71215a1323SEvan Bacon          <FolderIcon className="text-icon-tertiary mr-2 opacity-60 min-w-[20px]" />
72215a1323SEvan Bacon          <TextWithNote name={name} note={note} className="text-secondary" />
73c7504584SBartosz Kaszubowski        </div>
74215a1323SEvan Bacon        {renderStructure(files, level + 1)}
75c7504584SBartosz Kaszubowski      </div>
76c7504584SBartosz Kaszubowski    ) : (
77*920d7a63SBartosz Kaszubowski      <div key={name + '_' + index} className="mt-1 pt-1 pl-2 rounded-sm flex items-center">
78215a1323SEvan Bacon        {' '.repeat(Math.max(level, 0))}
79215a1323SEvan Bacon        <FileIcon className="text-icon-tertiary mr-2 min-w-[20px]" />
80215a1323SEvan Bacon        <TextWithNote name={name} note={note} className="text-default" />
81c7504584SBartosz Kaszubowski      </div>
82c7504584SBartosz Kaszubowski    );
83c7504584SBartosz Kaszubowski  });
84c7504584SBartosz Kaszubowski}
85a9f9618cSEvan Bacon
86215a1323SEvan Baconfunction TextWithNote({
87215a1323SEvan Bacon  name,
88215a1323SEvan Bacon  note,
89215a1323SEvan Bacon  className,
90215a1323SEvan Bacon}: {
91215a1323SEvan Bacon  name: string;
92215a1323SEvan Bacon  note?: string;
93215a1323SEvan Bacon  className: string;
94215a1323SEvan Bacon}) {
95215a1323SEvan Bacon  return (
96215a1323SEvan Bacon    <span className="flex items-center flex-1">
97215a1323SEvan Bacon      {/* File/folder name  */}
98215a1323SEvan Bacon      <code className={className}>{name}</code>
99215a1323SEvan Bacon
100215a1323SEvan Bacon      {note && (
101215a1323SEvan Bacon        <>
102215a1323SEvan Bacon          {/* divider pushing  */}
103215a1323SEvan Bacon          <span className="flex-1 border-b border-default opacity-60 mx-2 md:mx-3 min-w-[2rem]" />
104215a1323SEvan Bacon          {/* Optional note */}
105215a1323SEvan Bacon          <code className="text-default">{note}</code>
106215a1323SEvan Bacon        </>
107215a1323SEvan Bacon      )}
108215a1323SEvan Bacon    </span>
109215a1323SEvan Bacon  );
110215a1323SEvan Bacon}
111215a1323SEvan Bacon
112a9f9618cSEvan Baconfunction getIconForFile(filename: string) {
113a9f9618cSEvan Bacon  if (/_layout\.[jt]sx?/.test(filename)) {
114a9f9618cSEvan Bacon    return LayoutAlt01Icon;
115a9f9618cSEvan Bacon  }
116a9f9618cSEvan Bacon  return FileCode01Icon;
117a9f9618cSEvan Bacon}
118