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 2 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
15 changes: 4 additions & 11 deletions docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -99,23 +99,16 @@ type I18nText = {
fileTreeCreateFolderText?: string,

/**
* Text shown on dialog when file creation failed. Variables: ${filename}.
* Text shown on dialog when user attempts to edit files that don't match allowed patterns.
*
* @default 'Failed to create file "${filename}".'
* @default 'This action is not allowed'
*/
fileTreeFailedToCreateFileText?: string,

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

/**
* Text shown on dialog describing allowed patterns when file or folder createion failed.
*
* @default 'Allowed patterns are:'
* @default 'Created files and folders must match following patterns:'
*/
fileTreeAllowedPatternsText?: string,

Expand Down
7 changes: 3 additions & 4 deletions e2e/test/file-tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,15 +211,14 @@ test('user cannot create files or folders in disallowed directories', async ({ p
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();
const dialog = page.getByRole('dialog', { name: 'This action is not allowed' });

await expect(dialog.getByText('Allowed patterns are:')).toBeVisible();
await expect(dialog.getByText('Created files and folders must match following patterns:')).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 dialog.getByRole('button', { name: 'OK' }).click();
await expect(dialog).not.toBeVisible();
}
});
3 changes: 1 addition & 2 deletions packages/astro/src/default/utils/content.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import path from 'node:path';
import type { ChapterSchema, Lesson, LessonSchema, PartSchema, Tutorial, TutorialSchema } from '@tutorialkit/types';
import { interpolateString } from '@tutorialkit/types';
import { interpolateString, DEFAULT_LOCALIZATION } from '@tutorialkit/types';
import { getCollection } from 'astro:content';
import { DEFAULT_LOCALIZATION } from './content/default-localization';
import { getFilesRefList } from './content/files-ref';
import { squash } from './content/squash.js';
import { logger } from './logger';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,6 @@ exports[`create and eject a project 1`] = `
"src/utils/constants.ts",
"src/utils/content",
"src/utils/content.ts",
"src/utils/content/default-localization.ts",
"src/utils/content/files-ref.ts",
"src/utils/content/squash.ts",
"src/utils/logger.ts",
Expand Down
72 changes: 27 additions & 45 deletions packages/react/src/core/ContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useRef, useState, type ComponentProps, type ReactNode } from 'react';
import { Root, Portal, Content, Item, Trigger } from '@radix-ui/react-context-menu';
import * as RadixDialog from '@radix-ui/react-dialog';
import { DEFAULT_LOCALIZATION, type FileDescriptor, type I18n } from '@tutorialkit/types';
import picomatch from 'picomatch/posix';
import { interpolateString, type FileDescriptor, type I18n } from '@tutorialkit/types';
import { useRef, useState, type ComponentProps, type ReactNode } from 'react';

interface FileChangeEvent {
type: FileDescriptor['type'];
Expand Down Expand Up @@ -33,39 +33,27 @@ interface Props extends ComponentProps<'div'> {
I18n,
| 'fileTreeCreateFileText'
| 'fileTreeCreateFolderText'
| 'fileTreeFailedToCreateFileText'
| 'fileTreeFailedToCreateFolderText'
| 'fileTreeActionNotAllowedText'
| 'fileTreeAllowedPatternsText'
>;

/** Props for trigger wrapper. */
triggerProps?: ComponentProps<'div'> & { 'data-testid'?: string };
}

const i18nDefaults = {
fileTreeFailedToCreateFileText: 'Failed to create file "${filename}".',
fileTreeFailedToCreateFolderText: 'Failed to create folder "${filename}".',
fileTreeCreateFileText: 'Create file',
fileTreeCreateFolderText: 'Create folder',
fileTreeAllowedPatternsText: 'Allowed patterns are:',
} as const satisfies Props['i18n'];

export function ContextMenu({
onFileChange,
allowEditPatterns = ['**'],
directory,
i18n: i18nProps,
i18n,
position = 'before',
children,
triggerProps,
...props
}: Props) {
const [state, setState] = useState<'idle' | 'add_file' | 'add_folder' | { error: string }>('idle');
const [state, setState] = useState<'idle' | 'add_file' | 'add_folder' | 'add_failed'>('idle');
const inputRef = useRef<HTMLInputElement>(null);

const error = typeof state === 'string' ? false : state.error;
const i18n = { ...i18nProps, ...i18nDefaults };

if (!onFileChange) {
return children;
}
Expand All @@ -80,18 +68,15 @@ export function ContextMenu({
if (name) {
const value = `${directory}/${name}`;
const isAllowed = picomatch.isMatch(value, allowEditPatterns);
const isFile = state === 'add_file';

if (isAllowed) {
onFileChange?.({
value,
type: isFile ? 'file' : 'folder',
type: state === 'add_file' ? 'file' : 'folder',
method: 'add',
});
} else {
const text = isFile ? i18n.fileTreeFailedToCreateFileText : i18n.fileTreeFailedToCreateFolderText;

return setState({ error: interpolateString(text, { filename: value }) });
return setState('add_failed');
}
}

Expand Down Expand Up @@ -143,29 +128,28 @@ export function ContextMenu({
className="border border-tk-border-brighter b-rounded-md bg-tk-background-brighter py-2"
>
<MenuItem icon="i-ph-file-plus" onClick={() => setState('add_file')}>
{i18n.fileTreeCreateFileText}
{i18n?.fileTreeCreateFileText || DEFAULT_LOCALIZATION.fileTreeCreateFileText}
</MenuItem>

<MenuItem icon="i-ph-folder-plus" onClick={() => setState('add_folder')}>
{i18n.fileTreeCreateFolderText}
{i18n?.fileTreeCreateFolderText || DEFAULT_LOCALIZATION.fileTreeCreateFolderText}
</MenuItem>
</Content>
</Portal>

{error && (
<Dialog onClose={() => setState('idle')}>
<p className="mb-2">{error}</p>

<div>
{i18n.fileTreeAllowedPatternsText}
<ul className="list-disc ml-4 mt-2">
{allowEditPatterns.map((pattern) => (
<li key={pattern}>
<code>{pattern}</code>
</li>
))}
</ul>
</div>
{state === 'add_failed' && (
<Dialog
title={i18n?.fileTreeActionNotAllowedText || DEFAULT_LOCALIZATION.fileTreeActionNotAllowedText}
onClose={() => setState('idle')}
>
{i18n?.fileTreeAllowedPatternsText || DEFAULT_LOCALIZATION.fileTreeAllowedPatternsText}
<ul className="list-disc ml-4 mt-2">
{allowEditPatterns.map((pattern) => (
<li key={pattern} className="mb-1">
<code>{pattern}</code>
</li>
))}
</ul>
</Dialog>
)}
</Root>
Expand All @@ -184,21 +168,19 @@ function MenuItem({ icon, children, ...props }: { icon: string } & ComponentProp
);
}

function Dialog({ onClose, children }: { onClose: () => void; children: ReactNode }) {
function Dialog({ title, onClose, children }: { title: string; onClose: () => void; children: ReactNode }) {
return (
<RadixDialog.Root open={true} onOpenChange={(open) => !open && onClose()}>
<RadixDialog.Portal>
<RadixDialog.Overlay className="fixed inset-0 opacity-50 bg-black" />

<RadixDialog.Content className="fixed top-50% left-50% transform-translate--50% w-90vw max-w-450px max-h-85vh rounded-xl text-tk-text-primary bg-tk-background-negative">
<RadixDialog.Content className="fixed top-50% left-50% transform-translate--50% w-90vw max-w-450px max-h-85vh rounded-xl text-tk-text-primary bg-tk-background-primary">
<div className="relative py-4 px-10">
<RadixDialog.Title className="text-6 mb-2">Error</RadixDialog.Title>
<RadixDialog.Title className="text-6">{title}</RadixDialog.Title>

{children}
<div className="my-4">{children}</div>

<RadixDialog.Close title="Close" className="absolute top-4 right-4 w-6 h-6">
<span aria-hidden className="i-ph-x block w-full h-full"></span>
</RadixDialog.Close>
<RadixDialog.Close className="px-3 py-1 border border-tk-border-primary rounded">OK</RadixDialog.Close>
</div>
</RadixDialog.Content>
</RadixDialog.Portal>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Lesson } from '@tutorialkit/types';
import type { Lesson } from './entities/index.js';

export const DEFAULT_LOCALIZATION = {
partTemplate: 'Part ${index}: ${title}',
Expand All @@ -9,9 +9,8 @@ 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:',
fileTreeActionNotAllowedText: 'This action is not allowed',
fileTreeAllowedPatternsText: 'Created files and folders must match following patterns:',
prepareEnvironmentTitleText: 'Preparing Environment',
defaultPreviewTitleText: 'Preview',
reloadPreviewTitle: 'Reload Preview',
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export type * from './entities/index.js';
export * from './schemas/index.js';
export * from './files-ref.js';
export { interpolateString } from './utils/interpolation.js';
export { DEFAULT_LOCALIZATION } from './default-localization.js';
20 changes: 5 additions & 15 deletions packages/types/src/schemas/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,29 +73,19 @@ export const i18nSchema = z.object({
.describe("Text shown on file tree's context menu's folder creation button."),

/**
* Text shown on dialog when file creation failed. Variables: ${filename}.
* Text shown on dialog when user attempts to edit files that don't match allowed patterns.
*
* @default 'Failed to create file "${filename}".'
* @default 'This action is not allowed'
*/
fileTreeFailedToCreateFileText: z
fileTreeActionNotAllowedText: z
.string()
.optional()
.describe('Text shown on dialog when file creation failed. Variables: ${filename}.'),

/**
* Text shown on dialog when folder creation failed. Variables: ${filename}.
*
* @default 'Failed to create folder "${filename}".'
*/
fileTreeFailedToCreateFolderText: z
.string()
.optional()
.describe('Text shown on dialog when folder creation failed. Variables: ${filename}.'),
.describe("Text shown on dialog when user attempts to edit files that don't match allowed patterns."),

/**
* Text shown on dialog describing allowed patterns when file or folder createion failed.
*
* @default 'Allowed patterns are:'
* @default 'Created files and folders must match following patterns:'
*/
fileTreeAllowedPatternsText: z
.string()
Expand Down