Skip to content

feat: support glob patterns in editor.fileTree.allowEdits #332

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

Merged
merged 9 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
Expand Up @@ -84,6 +84,41 @@ type I18nText = {
*/
filesTitleText?: string,

/**
* Text shown on file tree's context menu's file creation button.
*
* @default 'Create file'
*/
fileTreeCreateFileText?: string,

/**
* Text shown on file tree's context menu's folder creation button.
*
* @default 'Create folder'
*/
fileTreeCreateFolderText?: string,

/**
* Text shown on dialog when file creation failed. Variables: ${filename}.
*
* @default 'Failed to create file "${filename}".'
*/
fileTreeFailedToCreateFileText?: string,

/**
* Text shown on dialog when folder creation failed. Variables: ${filename}.
*
* @default 'Failed to create folder "${filename}".'
*/
fileTreeFailedToCreateFolderText?: string,

/**
* Text shown on dialog describing allowed patterns when file or folder createion failed.
*
* @default 'Allowed patterns are:'
*/
fileTreeAllowedPatternsText?: string,

/**
* Text shown on top of the steps section.
*
Expand Down Expand Up @@ -144,9 +179,11 @@ File tree can be set to allow file editing from right clicks by setting `fileTre
The `Editor` type has the following shape:

```ts
type GlobPattern = string

type Editor =
| false
| { editor: { allowEdits: boolean } }
| { editor: { allowEdits: boolean | GlobPattern | GlobPattern[] } }

```

Expand All @@ -161,6 +198,18 @@ editor: # Editor is visible
editor: # Editor is visible
fileTree: # File tree is visible
allowEdits: true # User can add new files and folders from the file tree


editor: # Editor is visible
fileTree: # File tree is visible
allowEdits: "/src/**" # User can add files and folders anywhere inside "/src/"

editor: # Editor is visible
fileTree: # File tree is visible
allowEdits:
- "/*" # User can add files and folders directly in the root
- "/first-level/allowed-filename-only.js" # Only "allowed-filename-only.js" inside "/first-level" folder
- "**/second-level/**" # Anything inside "second-level" folders anywhere
```

##### `previews`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ A component to list files in a tree view.
}
```

* `allowEditPatterns?: string[]` - Glob patterns for paths that allow editing files and folders. Defaults to `['**']`.

* `hideRoot: boolean` - Whether or not to hide the root directory in the tree. Defaults to `false`.

* `hiddenFiles: (string | RegExp)[]` - A list of file paths that should be hidden from the tree.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'File in first level';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'File in second level';
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
type: lesson
title: Allow Edits Glob
previews: false
editor:
fileTree:
allowEdits:
# Items in root
- "/*"
# Only "allowed-filename-only.js" inside "/first-level" folder
- "/first-level/allowed-filename-only.js"
# Anything inside "second-level" folders anywhere
- "**/second-level/**"
terminal:
panels: terminal
---

# File Tree test - Allow Edits Glob
68 changes: 68 additions & 0 deletions e2e/test/file-tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,71 @@ test('user can create folders', async ({ page }) => {
await expect(terminalOutput).toContainText(folder, { useInnerText: true });
}
});

test('user can create files and folders in allowed directories', async ({ page }) => {
await page.goto(`${BASE_URL}/allow-edits-glob`);
await expect(page.getByRole('heading', { level: 1, name: 'File Tree test - Allow Edits Glob' })).toBeVisible();

// wait for terminal to start
const terminal = page.getByRole('textbox', { name: 'Terminal input' });
const terminalOutput = page.getByRole('tabpanel', { name: 'Terminal' });
await expect(terminalOutput).toContainText('~/tutorial', { useInnerText: true });

// can create files in root and inside "second-level"
for (const [locator, name, type] of [
[page.getByTestId('file-tree-root-context-menu'), 'new-file.js', 'file'],
[page.getByRole('button', { name: 'second-level' }), 'new-folder.js', 'folder'],
] as const) {
await locator.click({ button: 'right' });
await page.getByRole('menuitem', { name: `Create ${type}` }).click();

await page.locator('*:focus').fill(name);
await page.locator('*:focus').press('Enter');
await expect(page.getByRole('button', { name })).toBeVisible();
}

await expect(page.getByRole('button', { name: 'new-file.js' })).toBeVisible();
await expect(page.getByRole('button', { name: 'new-folder' })).toBeVisible();

// verify that files are present on file system via terminal
for (const [directory, folder] of [
['./', 'new-file.js'],
['./first-level/second-level/', 'new-folder'],
]) {
await terminal.fill(`clear; ls ${directory}`);
await terminal.press('Enter');

await expect(terminalOutput).toContainText(folder, { useInnerText: true });
}
});

test('user cannot create files or folders in disallowed directories', async ({ page }) => {
await page.goto(`${BASE_URL}/allow-edits-glob`);
await expect(page.getByRole('heading', { level: 1, name: 'File Tree test - Allow Edits Glob' })).toBeVisible();

// wait for terminal to start
const terminalOutput = page.getByRole('tabpanel', { name: 'Terminal' });
await expect(terminalOutput).toContainText('~/tutorial', { useInnerText: true });

for (const [name, type] of [
['new-file.js', 'file'],
['new-folder', 'folder'],
] as const) {
await page.getByRole('button', { name: 'first-level' }).click({ button: 'right' });
await page.getByRole('menuitem', { name: `Create ${type}` }).click();

await page.locator('*:focus').fill(name);
await page.locator('*:focus').press('Enter');

const dialog = page.getByRole('dialog', { name: 'Error' });
await expect(dialog.getByText(`Failed to create ${type} "/first-level/${name}".`)).toBeVisible();

await expect(dialog.getByText('Allowed patterns are:')).toBeVisible();
await expect(dialog.getByRole('listitem').nth(0)).toHaveText('/*');
await expect(dialog.getByRole('listitem').nth(1)).toHaveText('/first-level/allowed-filename-only.js');
await expect(dialog.getByRole('listitem').nth(2)).toHaveText('**/second-level/**');

await dialog.getByRole('button', { name: 'Close' }).click();
await expect(dialog).not.toBeVisible();
}
});
7 changes: 6 additions & 1 deletion e2e/uno.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { defineConfig } from '@tutorialkit/theme';

export default defineConfig({
// add your UnoCSS config here: https://unocss.dev/guide/config-file
// required for TutorialKit monorepo development mode
content: {
pipeline: {
include: '**',
},
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export const DEFAULT_LOCALIZATION = {
filesTitleText: 'Files',
fileTreeCreateFileText: 'Create file',
fileTreeCreateFolderText: 'Create folder',
fileTreeFailedToCreateFileText: 'Failed to create file "${filename}".',
fileTreeFailedToCreateFolderText: 'Failed to create folder "${filename}".',
fileTreeAllowedPatternsText: 'Allowed patterns are:',
prepareEnvironmentTitleText: 'Preparing Environment',
defaultPreviewTitleText: 'Preview',
reloadPreviewTitle: 'Reload Preview',
Expand Down
3 changes: 3 additions & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"@nanostores/react": "0.7.2",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-context-menu": "^2.2.1",
"@radix-ui/react-dialog": "^1.1.1",
"@replit/codemirror-lang-svelte": "^6.0.0",
"@tutorialkit/runtime": "workspace:*",
"@tutorialkit/theme": "workspace:*",
Expand All @@ -85,12 +86,14 @@
"codemirror": "^6.0.1",
"framer-motion": "^11.2.11",
"nanostores": "^0.10.3",
"picomatch": "^4.0.2",
"react": "^18.3.1",
"react-resizable-panels": "^2.0.19"
},
"devDependencies": {
"@codemirror/search": "^6.5.6",
"@tutorialkit/types": "workspace:*",
"@types/picomatch": "^3.0.1",
"@types/react": "^18.3.3",
"chokidar": "3.6.0",
"execa": "^9.2.0",
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/Panels/EditorPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface Props {
helpAction?: 'solve' | 'reset';
editorDocument?: EditorDocument;
selectedFile?: string | undefined;
allowEditPatterns?: ComponentProps<typeof FileTree>['allowEditPatterns'];
onEditorChange?: OnEditorChange;
onEditorScroll?: OnEditorScroll;
onHelpClick?: () => void;
Expand All @@ -43,6 +44,7 @@ export function EditorPanel({
helpAction,
editorDocument,
selectedFile,
allowEditPatterns,
onEditorChange,
onEditorScroll,
onHelpClick,
Expand Down Expand Up @@ -83,6 +85,7 @@ export function EditorPanel({
hideRoot={hideRoot ?? true}
files={files}
scope={fileTreeScope}
allowEditPatterns={allowEditPatterns}
onFileSelect={onFileSelect}
onFileChange={onFileTreeChange}
/>
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/Panels/WorkspacePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ function EditorSection({ theme, tutorialStore, hasEditor }: PanelProps) {
onHelpClick={lessonFullyLoaded ? onHelpClick : undefined}
onFileSelect={(filePath) => tutorialStore.setSelectedFile(filePath)}
onFileTreeChange={editorConfig.fileTree.allowEdits ? onFileTreeChange : undefined}
allowEditPatterns={editorConfig.fileTree.allowEdits ? editorConfig.fileTree.allowEdits : undefined}
selectedFile={selectedFile}
onEditorScroll={(position) => tutorialStore.setCurrentDocumentScrollPosition(position)}
onEditorChange={(update) => tutorialStore.setCurrentDocumentContent(update.content)}
Expand Down
Loading