Skip to content

Commit a3944c3

Browse files
committed
feat(runtime): store to support file and folder creation
1 parent 7a5faac commit a3944c3

File tree

6 files changed

+79
-3
lines changed

6 files changed

+79
-3
lines changed

packages/react/src/Panels/EditorPanel.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { I18n } from '@tutorialkit/types';
2-
import { useEffect, useRef } from 'react';
2+
import { useEffect, useRef, type ComponentProps } from 'react';
33
import { Panel, PanelGroup, PanelResizeHandle, type ImperativePanelHandle } from 'react-resizable-panels';
44
import {
55
CodeMirrorEditor,
@@ -29,6 +29,7 @@ interface Props {
2929
onEditorScroll?: OnEditorScroll;
3030
onHelpClick?: () => void;
3131
onFileSelect?: (value?: string) => void;
32+
onFileTreeChange?: ComponentProps<typeof FileTree>['onFileChange'];
3233
}
3334

3435
export function EditorPanel({
@@ -46,6 +47,7 @@ export function EditorPanel({
4647
onEditorScroll,
4748
onHelpClick,
4849
onFileSelect,
50+
onFileTreeChange,
4951
}: Props) {
5052
const fileTreePanelRef = useRef<ImperativePanelHandle>(null);
5153

@@ -81,6 +83,7 @@ export function EditorPanel({
8183
files={files}
8284
scope={fileTreeScope}
8385
onFileSelect={onFileSelect}
86+
onFileChange={onFileTreeChange}
8487
/>
8588
</Panel>
8689
<PanelResizeHandle

packages/react/src/Panels/WorkspacePanel.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useStore } from '@nanostores/react';
22
import { TutorialStore } from '@tutorialkit/runtime';
33
import type { I18n } from '@tutorialkit/types';
4-
import { useCallback, useEffect, useRef, useState } from 'react';
4+
import { useCallback, useEffect, useRef, useState, type ComponentProps } from 'react';
55
import { Panel, PanelGroup, PanelResizeHandle, type ImperativePanelHandle } from 'react-resizable-panels';
66
import type { Theme } from '../core/types.js';
77
import resizePanelStyles from '../styles/resize-panel.module.css';
@@ -12,6 +12,8 @@ import { TerminalPanel } from './TerminalPanel.js';
1212

1313
const DEFAULT_TERMINAL_SIZE = 25;
1414

15+
type FileTreeChangeEvent = Parameters<NonNullable<ComponentProps<typeof EditorPanel>['onFileTreeChange']>>[0];
16+
1517
interface Props {
1618
tutorialStore: TutorialStore;
1719
theme: Theme;
@@ -111,6 +113,16 @@ function EditorSection({ theme, tutorialStore, hasEditor }: PanelProps) {
111113
}
112114
}
113115

116+
function onFileTreeChange({ method, type, value }: FileTreeChangeEvent) {
117+
if (method == 'ADD' && type === 'FILE') {
118+
return tutorialStore.addFile(value);
119+
}
120+
121+
if (method == 'ADD' && type === 'DIRECTORY') {
122+
return tutorialStore.addFolder(value);
123+
}
124+
}
125+
114126
useEffect(() => {
115127
if (tutorialStore.hasSolution()) {
116128
setHelpAction('solve');
@@ -139,6 +151,7 @@ function EditorSection({ theme, tutorialStore, hasEditor }: PanelProps) {
139151
helpAction={helpAction}
140152
onHelpClick={lessonFullyLoaded ? onHelpClick : undefined}
141153
onFileSelect={(filePath) => tutorialStore.setSelectedFile(filePath)}
154+
onFileTreeChange={onFileTreeChange}
142155
selectedFile={selectedFile}
143156
onEditorScroll={(position) => tutorialStore.setCurrentDocumentScrollPosition(position)}
144157
onEditorChange={(update) => tutorialStore.setCurrentDocumentContent(update.content)}

packages/react/src/core/FileTree.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,17 @@ import { classNames } from '../utils/classnames.js';
44
const NODE_PADDING_LEFT = 12;
55
const DEFAULT_HIDDEN_FILES = [/\/node_modules\//];
66

7+
interface FileChangeEvent {
8+
type: 'FILE' | 'DIRECTORY';
9+
method: 'ADD' | 'REMOVE' | 'RENAME';
10+
value: string;
11+
}
12+
713
interface Props {
814
files: string[];
915
selectedFile?: string;
1016
onFileSelect?: (filePath: string) => void;
17+
onFileChange?: (event: FileChangeEvent) => void;
1118
hideRoot: boolean;
1219
scope?: string;
1320
hiddenFiles?: Array<string | RegExp>;

packages/runtime/src/store/editor.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export interface ScrollPosition {
1313
left: number;
1414
}
1515

16-
export type EditorDocuments = Record<string, EditorDocument>;
16+
export type EditorDocuments = Record<string, EditorDocument | undefined>;
1717

1818
export class EditorStore {
1919
selectedFile = atom<string | undefined>();
@@ -83,6 +83,21 @@ export class EditorStore {
8383
});
8484
}
8585

86+
addFileOrFolder(filePath: string) {
87+
// when adding file to empty folder, remove the empty folder from documents
88+
const emptyDirectory = this.files.value?.find((path) => filePath.startsWith(path));
89+
90+
if (emptyDirectory) {
91+
this.documents.setKey(emptyDirectory, undefined);
92+
}
93+
94+
this.documents.setKey(filePath, {
95+
filePath,
96+
value: '',
97+
loading: false,
98+
});
99+
}
100+
86101
updateFile(filePath: string, content: string): boolean {
87102
const documentState = this.documents.get()[filePath];
88103

packages/runtime/src/store/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,27 @@ export class TutorialStore {
308308
this._editorStore.setSelectedFile(filePath);
309309
}
310310

311+
addFile(filePath: string) {
312+
// prevent creating duplicates
313+
if (this._editorStore.files.get().includes(filePath)) {
314+
return this.setSelectedFile(filePath);
315+
}
316+
317+
this._editorStore.addFileOrFolder(filePath);
318+
this.setSelectedFile(filePath);
319+
this._runner.updateFile(filePath, '');
320+
}
321+
322+
addFolder(folderPath: string) {
323+
// prevent creating duplicates
324+
if (this._editorStore.files.get().includes(folderPath)) {
325+
return this.setSelectedFile(folderPath);
326+
}
327+
328+
this._editorStore.addFileOrFolder(folderPath);
329+
this._runner.createFolder(folderPath);
330+
}
331+
311332
updateFile(filePath: string, content: string) {
312333
const hasChanged = this._editorStore.updateFile(filePath, content);
313334

packages/runtime/src/tutorial-runner.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,23 @@ export class TutorialRunner {
9898
this._currentCommandProcess?.resize({ cols, rows });
9999
}
100100

101+
createFolder(folderPAth: string): void {
102+
const previousLoadPromise = this._currentLoadTask?.promise;
103+
104+
this._currentLoadTask = newTask(
105+
async (signal) => {
106+
await previousLoadPromise;
107+
108+
const webcontainer = await this._webcontainer;
109+
110+
signal.throwIfAborted();
111+
112+
await webcontainer.fs.mkdir(folderPAth);
113+
},
114+
{ ignoreCancel: true },
115+
);
116+
}
117+
101118
/**
102119
* Update the content of a single file in WebContainer.
103120
*

0 commit comments

Comments
 (0)