Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add files via file tree #314

Merged
merged 16 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useState } from 'react';
import { useState, type ComponentProps } from 'react';
import FileTree from '@tutorialkit/react/core/FileTree';

export default function ExampleFileTree() {
const [files, setFiles] = useState(INITIAL_FILES);
const [selectedFile, setSelectedFile] = useState(INITIAL_FILES[0]);
const [selectedFile, setSelectedFile] = useState(INITIAL_FILES[0].path);

return (
<FileTree
Expand All @@ -15,18 +15,18 @@ export default function ExampleFileTree() {
onFileSelect={setSelectedFile}
onFileChange={(event) => {
if (event.method === 'ADD') {
setFiles([...files, event.value].sort());
setFiles([...files, { path: event.value, type: event.type }].sort());
Nemikolh marked this conversation as resolved.
Show resolved Hide resolved
}
}}
/>
);
}

const INITIAL_FILES = [
'/package-lock.json',
'/package.json',
'/src/assets/logo.svg',
'/src/index.html',
'/src/index.js',
'/vite.config.js',
const INITIAL_FILES: ComponentProps<typeof FileTree>['files'] = [
{ path: '/package-lock.json', type: 'FILE' },
{ path: '/package.json', type: 'FILE' },
{ path: '/src/assets/logo.svg', type: 'FILE' },
{ path: '/src/index.html', type: 'FILE' },
{ path: '/src/index.js', type: 'FILE' },
{ path: '/vite.config.js', type: 'FILE' },
];
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ const FILES: Record<string, EditorDocument> = {
},
};

const FILE_PATHS = Object.keys(FILES);
const FILE_PATHS = Object.keys(FILES).map((path) => ({ path, type: 'FILE' }) as const);

function stripIndent(string: string) {
const indent = minIndent(string.slice(1));
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/Panels/EditorPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const DEFAULT_FILE_TREE_SIZE = 25;
interface Props {
theme: Theme;
id: unknown;
files: string[];
files: ComponentProps<typeof FileTree>['files'];
i18n: I18n;
hideRoot?: boolean;
fileTreeScope?: string;
Expand Down
6 changes: 2 additions & 4 deletions packages/react/src/core/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,11 @@ export function ContextMenu({

function onFileNameEnd(event: React.KeyboardEvent<HTMLInputElement> | React.FocusEvent<HTMLInputElement>) {
const name = event.currentTarget.value;
const isFile = state === 'ADD_FILE';

// files must contain extension
if (name && (!isFile || name.includes('.'))) {
if (name) {
onFileChange?.({
value: `${directory}/${name}`,
type: isFile ? 'FILE' : 'FOLDER',
type: state === 'ADD_FILE' ? 'FILE' : 'FOLDER',
method: 'ADD',
});
}
Expand Down
32 changes: 17 additions & 15 deletions packages/react/src/core/FileTree.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { useEffect, useMemo, useState, type ComponentProps, type ReactNode } from 'react';
import type { File } from '@tutorialkit/types';
import { ContextMenu } from './ContextMenu.js';
import { classNames } from '../utils/classnames.js';

const NODE_PADDING_LEFT = 12;
const DEFAULT_HIDDEN_FILES = [/\/node_modules\//];

interface Props {
files: string[];
files: File[];
selectedFile?: string;
onFileSelect?: (filePath: string) => void;
onFileChange?: ComponentProps<typeof ContextMenu>['onFileChange'];
Expand Down Expand Up @@ -86,10 +87,10 @@ export function FileTree({
}

return (
<div className={classNames(className, 'h-100% transition-theme bg-tk-elements-fileTree-backgroundColor')}>
<div className={classNames(className, 'h-full transition-theme bg-tk-elements-fileTree-backgroundColor')}>
{filteredFileList.map((fileOrFolder) => {
switch (fileOrFolder.kind) {
case 'file': {
case 'FILE': {
return (
<File
key={fileOrFolder.id}
Expand All @@ -99,7 +100,7 @@ export function FileTree({
/>
);
}
case 'folder': {
case 'FOLDER': {
return (
<Folder
key={fileOrFolder.id}
Expand All @@ -120,7 +121,7 @@ export function FileTree({
style={getDepthStyle(0)}
directory=""
onFileChange={onFileChange}
triggerProps={{ className: 'h-100%', 'data-testid': 'file-tree-root-context-menu' }}
triggerProps={{ className: 'h-full', 'data-testid': 'file-tree-root-context-menu' }}
/>
</div>
);
Expand Down Expand Up @@ -215,18 +216,19 @@ interface BaseNode {
depth: number;
name: string;
fullPath: string;
kind: File['type'];
}

interface FileNode extends BaseNode {
kind: 'file';
kind: 'FILE';
}

interface FolderNode extends BaseNode {
kind: 'folder';
kind: 'FOLDER';
AriPerkkio marked this conversation as resolved.
Show resolved Hide resolved
}

function buildFileList(
files: string[],
files: File[],
hideRoot: boolean,
scope: string | undefined,
hiddenFiles: Array<string | RegExp>,
Expand All @@ -236,18 +238,18 @@ function buildFileList(
const defaultDepth = hideRoot ? 0 : 1;

if (!hideRoot) {
fileList.push({ kind: 'folder', name: '/', fullPath: '/', depth: 0, id: 0 });
fileList.push({ kind: 'FOLDER', name: '/', fullPath: '/', depth: 0, id: 0 });
}

for (const filePath of files) {
if (scope && !filePath.startsWith(scope)) {
for (const file of files) {
if (scope && !file.path.startsWith(scope)) {
continue;
}

const segments = filePath.split('/').filter((s) => s);
const segments = file.path.split('/').filter((s) => s);
const fileName = segments.at(-1);

if (!fileName || isHiddenFile(filePath, fileName, hiddenFiles)) {
if (!fileName || isHiddenFile(file.path, fileName, hiddenFiles)) {
continue;
}

Expand All @@ -259,7 +261,7 @@ function buildFileList(

if (depth === segments.length - 1) {
fileList.push({
kind: name.includes('.') ? 'file' : 'folder',
kind: file.type,
id: fileList.length,
name,
fullPath,
Expand All @@ -269,7 +271,7 @@ function buildFileList(
folderPaths.add(fullPath);

fileList.push({
kind: 'folder',
kind: 'FOLDER',
id: fileList.length,
name,
fullPath,
Expand Down
102 changes: 102 additions & 0 deletions packages/runtime/src/store/editor.spec.ts
AriPerkkio marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { expect, test } from 'vitest';

import { EditorStore } from './editor.js';
import type { File } from '@tutorialkit/types';

test('initial files are sorted', () => {
const store = new EditorStore();
store.setDocuments({
'test/math.test.ts': '',
'.gitignore': '',
'src/math.ts': '',
'src/geometry.ts': '',
});

expect(store.files.get().map(toFilename)).toMatchInlineSnapshot(`
[
"src/geometry.ts",
"src/math.ts",
"test/math.test.ts",
".gitignore",
]
`);
});

test('added files are sorted', () => {
const store = new EditorStore();
store.setDocuments({
'test/math.test.ts': '',
'.gitignore': '',
'src/math.ts': '',
});

store.addFileOrFolder({ path: 'something.ts', type: 'FILE' });
store.addFileOrFolder({ path: 'another.css', type: 'FILE' });
store.addFileOrFolder({ path: 'no-extension', type: 'FILE' });

expect(store.files.get().map(toFilename)).toMatchInlineSnapshot(`
[
"src/math.ts",
"test/math.test.ts",
".gitignore",
"another.css",
"no-extension",
"something.ts",
]
`);
});

test('added folders are sorted', () => {
const store = new EditorStore();
store.setDocuments({
'test/math.test.ts': '',
'.gitignore': '',
'src/math.ts': '',
});

store.addFileOrFolder({ path: 'src/components', type: 'FOLDER' });
store.addFileOrFolder({ path: 'src/utils', type: 'FOLDER' });
store.addFileOrFolder({ path: 'test/unit', type: 'FOLDER' });
store.addFileOrFolder({ path: 'e2e', type: 'FOLDER' });

expect(store.files.get().map(toFilename)).toMatchInlineSnapshot(`
[
"e2e",
"src/components",
"src/utils",
"src/math.ts",
"test/unit",
"test/math.test.ts",
".gitignore",
]
`);
});

test('empty directories are removed when new content is added', () => {
const store = new EditorStore();
store.setDocuments({
'src/index.ts': '',
});

store.addFileOrFolder({ path: 'src/components', type: 'FOLDER' });

expect(store.files.get().map(toFilename)).toMatchInlineSnapshot(`
[
"src/components",
"src/index.ts",
]
`);

store.addFileOrFolder({ path: 'src/components/FileTree', type: 'FOLDER' });

expect(store.files.get().map(toFilename)).toMatchInlineSnapshot(`
[
"src/components/FileTree",
"src/index.ts",
]
`);
});

function toFilename(file: File) {
return file.path;
}
Loading