diff --git a/sandpack-client/src/types.ts b/sandpack-client/src/types.ts index 43e819397..e8f3a4697 100644 --- a/sandpack-client/src/types.ts +++ b/sandpack-client/src/types.ts @@ -2,6 +2,8 @@ import type { ITemplate } from "codesandbox-import-util-types"; export interface SandpackBundlerFile { code: string; + hidden?: boolean; + active?: boolean; readOnly?: boolean; } diff --git a/sandpack-react/src/Issues.stories.tsx b/sandpack-react/src/Issues.stories.tsx index 29cc4e7f4..0003e2021 100644 --- a/sandpack-react/src/Issues.stories.tsx +++ b/sandpack-react/src/Issues.stories.tsx @@ -1,12 +1,64 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { useState } from "react"; -import { Sandpack } from "./index"; +import { + Sandpack, + SandpackCodeEditor, + SandpackFileExplorer, + SandpackLayout, + SandpackProvider, +} from "./index"; export default { title: "Bug reports/Issues", }; +export const Issue482 = (): JSX.Element => { + const [hidden, setHidden] = useState(false); + + const toggleHidden = (): void => { + setHidden((prevHidden) => !prevHidden); + }; + + return ( + <> + + + + + + + + + ); +}; + export const Issue454 = (): JSX.Element => { const [readOnly, setReadOnly] = useState(false); diff --git a/sandpack-react/src/components/FileExplorer/Directory.tsx b/sandpack-react/src/components/FileExplorer/Directory.tsx index dae34f04b..98d961ec2 100644 --- a/sandpack-react/src/components/FileExplorer/Directory.tsx +++ b/sandpack-react/src/components/FileExplorer/Directory.tsx @@ -1,52 +1,55 @@ import type { SandpackBundlerFiles } from "@codesandbox/sandpack-client"; import * as React from "react"; +import type { SandpackOptions } from "../.."; + import { File } from "./File"; import { ModuleList } from "./ModuleList"; -export interface Props { +import type { SandpackFileExplorerProp } from "."; + +export interface Props extends SandpackFileExplorerProp { prefixedPath: string; files: SandpackBundlerFiles; selectFile: (path: string) => void; - activeFile: string; + activeFile: NonNullable; depth: number; + visibleFiles: NonNullable; } -interface State { - open: boolean; -} - -export class Directory extends React.Component { - state = { - open: true, - }; - - toggleOpen = (): void => { - this.setState((state) => ({ open: !state.open })); - }; - - render(): React.ReactElement { - const { prefixedPath, files, selectFile, activeFile, depth } = this.props; - - return ( -
- = ({ + prefixedPath, + files, + selectFile, + activeFile, + depth, + autoHiddenFiles, + visibleFiles, +}) => { + const [open, setOpen] = React.useState(true); + + const toggle = (): void => setOpen((prev) => !prev); + + return ( +
+ + + {open && ( + - - {this.state.open && ( - - )} -
- ); - } -} + )} +
+ ); +}; diff --git a/sandpack-react/src/components/FileExplorer/FileExplorer.stories.tsx b/sandpack-react/src/components/FileExplorer/FileExplorer.stories.tsx index 91bd906dc..867fc7302 100644 --- a/sandpack-react/src/components/FileExplorer/FileExplorer.stories.tsx +++ b/sandpack-react/src/components/FileExplorer/FileExplorer.stories.tsx @@ -83,8 +83,10 @@ export const DirectoryIconStory: React.FC = () => ( depth={1} files={{ App: { code: "" } }} prefixedPath="/src" - // eslint-disable-next-line @typescript-eslint/no-explicit-any - selectFile={(): any => null} + selectFile={(): void => { + // + }} + visibleFiles={[]} /> diff --git a/sandpack-react/src/components/FileExplorer/ModuleList.tsx b/sandpack-react/src/components/FileExplorer/ModuleList.tsx index 3fe590341..1aa96232e 100644 --- a/sandpack-react/src/components/FileExplorer/ModuleList.tsx +++ b/sandpack-react/src/components/FileExplorer/ModuleList.tsx @@ -1,64 +1,63 @@ import type { SandpackBundlerFiles } from "@codesandbox/sandpack-client"; import * as React from "react"; +import type { SandpackOptions } from "../../types"; + import { Directory } from "./Directory"; import { File } from "./File"; +import { fromPropsToModules } from "./utils"; + +import type { SandpackFileExplorerProp } from "."; -export interface Props { +export interface ModuleListProps extends SandpackFileExplorerProp { prefixedPath: string; files: SandpackBundlerFiles; selectFile: (path: string) => void; - activeFile: string; + activeFile: NonNullable; depth?: number; + visibleFiles: NonNullable; } -export class ModuleList extends React.PureComponent { - render(): JSX.Element { - const { - depth = 0, - activeFile, - selectFile, - prefixedPath, - files, - } = this.props; - - const fileListWithoutPrefix = Object.keys(files) - .filter((file) => file.startsWith(prefixedPath)) - .map((file) => file.substring(prefixedPath.length)); - - const directoriesToShow = new Set( - fileListWithoutPrefix - .filter((file) => file.includes("/")) - .map((file) => `${prefixedPath}${file.split("/")[0]}/`) - ); - - const filesToShow = fileListWithoutPrefix - .filter((file) => !file.includes("/")) - .map((file) => ({ path: `${prefixedPath}${file}` })); - - return ( -
- {Array.from(directoriesToShow).map((dir) => ( - - ))} - - {filesToShow.map((file) => ( - - ))} -
- ); - } -} +export const ModuleList: React.FC = ({ + depth = 0, + activeFile, + selectFile, + prefixedPath, + files, + autoHiddenFiles, + visibleFiles, +}) => { + const { directories, modules } = fromPropsToModules({ + visibleFiles, + autoHiddenFiles, + prefixedPath, + files, + }); + + return ( +
+ {directories.map((dir) => ( + + ))} + + {modules.map((file) => ( + + ))} +
+ ); +}; diff --git a/sandpack-react/src/components/FileExplorer/index.tsx b/sandpack-react/src/components/FileExplorer/index.tsx index b49b383fe..5e2a94d4e 100644 --- a/sandpack-react/src/components/FileExplorer/index.tsx +++ b/sandpack-react/src/components/FileExplorer/index.tsx @@ -12,22 +12,36 @@ const fileExplorerClassName = css({ height: "100%", }); +export interface SandpackFileExplorerProp { + /** + * enable auto hidden file in file explorer + * + * @description set with hidden property in files property + * @default false + */ + autoHiddenFiles?: boolean; +} + /** * @category Components */ export const SandpackFileExplorer = ({ className, + autoHiddenFiles = false, ...props -}: React.HTMLAttributes): JSX.Element | null => { +}: SandpackFileExplorerProp & + React.HTMLAttributes): JSX.Element | null => { const { sandpack } = useSandpack(); return (
); diff --git a/sandpack-react/src/components/FileExplorer/util.test.ts b/sandpack-react/src/components/FileExplorer/util.test.ts new file mode 100644 index 000000000..3708639d2 --- /dev/null +++ b/sandpack-react/src/components/FileExplorer/util.test.ts @@ -0,0 +1,78 @@ +import type { ModuleListProps } from "./ModuleList"; +import { fromPropsToModules } from "./utils"; + +const defaultProps: ModuleListProps = { + files: { + "/src/component/index.js": { code: "", hidden: true }, + "/src/folder/index.js": { code: "", hidden: true }, + "/component/index.js": { code: "", hidden: true }, + "/component/src/index.js": { code: "", hidden: true }, + "/hidden-folder/index.js": { code: "", hidden: true }, + "/non-hidden-folder/index.js": { code: "", hidden: false }, + "/index.js": { code: "", hidden: true }, + "/App.js": { code: "", hidden: false }, + }, + autoHiddenFiles: false, + visibleFiles: [], + prefixedPath: "/", + activeFile: "", + selectFile: () => { + // + }, +}; + +describe(fromPropsToModules, () => { + it("returns a list of unique folder", () => { + expect(fromPropsToModules(defaultProps).directories).toEqual([ + "/src/", + "/component/", + "/hidden-folder/", + "/non-hidden-folder/", + ]); + }); + + it("returns only the root files", () => { + expect(fromPropsToModules(defaultProps).modules).toEqual([ + "/index.js", + "/App.js", + ]); + }); + + it("returns the folder from a subfolder", () => { + const input: ModuleListProps = { + ...defaultProps, + prefixedPath: "/src/", + }; + + expect(fromPropsToModules(input).directories).toEqual([ + "/src/component/", + "/src/folder/", + ]); + }); + + it("returns only the files from the visibleFiles prop (autoHiddenFiles)", () => { + const input: ModuleListProps = { + ...defaultProps, + autoHiddenFiles: true, + visibleFiles: ["/index.js", "/src/component/index.js"], + }; + + expect(fromPropsToModules(input)).toEqual({ + directories: ["/src/"], + modules: ["/index.js"], + }); + }); + + it("returns only the non-hidden files (autoHiddenFiles)", () => { + const input: ModuleListProps = { + ...defaultProps, + autoHiddenFiles: true, + visibleFiles: [], + }; + + expect(fromPropsToModules(input)).toEqual({ + directories: ["/non-hidden-folder/"], + modules: ["/App.js"], + }); + }); +}); diff --git a/sandpack-react/src/components/FileExplorer/utils.ts b/sandpack-react/src/components/FileExplorer/utils.ts new file mode 100644 index 000000000..d73ee539d --- /dev/null +++ b/sandpack-react/src/components/FileExplorer/utils.ts @@ -0,0 +1,49 @@ +import type { SandpackBundlerFiles } from "@codesandbox/sandpack-client"; + +export const fromPropsToModules = ({ + autoHiddenFiles, + visibleFiles, + files, + prefixedPath, +}: { + prefixedPath: string; + files: SandpackBundlerFiles; + autoHiddenFiles?: boolean; + visibleFiles: string[]; +}): { directories: string[]; modules: string[] } => { + const hasVisibleFilesOption = visibleFiles.length > 0; + + /** + * When visibleFiles or activeFile are set, the hidden and active flags on the files prop are ignored. + * @see: https://sandpack.codesandbox.io/docs/getting-started/custom-content#visiblefiles-and-activefile + */ + const filterByHiddenProperty = autoHiddenFiles && !hasVisibleFilesOption; + const filterByVisibleFilesOption = autoHiddenFiles && !!hasVisibleFilesOption; + + const fileListWithoutPrefix = Object.keys(files) + .filter((filePath) => { + const isValidatedPath = filePath.startsWith(prefixedPath); + if (filterByVisibleFilesOption) { + return isValidatedPath && visibleFiles.includes(filePath); + } + + if (filterByHiddenProperty) { + return isValidatedPath && !files[filePath]?.hidden; + } + + return isValidatedPath; + }) + .map((file) => file.substring(prefixedPath.length)); + + const directories = new Set( + fileListWithoutPrefix + .filter((file) => file.includes("/")) + .map((file) => `${prefixedPath}${file.split("/")[0]}/`) + ); + + const modules = fileListWithoutPrefix + .filter((file) => !file.includes("/")) + .map((file) => `${prefixedPath}${file}`); + + return { directories: Array.from(directories), modules }; +};