Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions src/common/CollapsibleButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export interface CollapsibleButtonProps
buttonWidth?: number | string;
}

export type CollapsableButtonComposibleProps = Omit<
CollapsibleButtonProps,
"onClick" | "text" | "icon"
>;

/**
* Button that can be a regular or icon button.
*
Expand Down
1 change: 0 additions & 1 deletion src/common/ConfirmDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export const ConfirmDialog = ({
isOpen={isOpen}
leastDestructiveRef={leastDestructiveRef}
onClose={onCancel}
isCentered
>
<AlertDialogOverlay>
<AlertDialogContent>
Expand Down
100 changes: 52 additions & 48 deletions src/common/FileInputButton.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Button, ButtonProps, Input } from "@chakra-ui/react";
import React, { useCallback, useRef } from "react";
import { RiFolderOpenLine } from "react-icons/ri";
import { Input } from "@chakra-ui/react";
import React, { ForwardedRef, useCallback, useRef } from "react";
import CollapsableButton, { CollapsibleButtonProps } from "./CollapsibleButton";

interface OpenButtonProps extends ButtonProps {
interface FileInputButtonProps extends CollapsibleButtonProps {
onOpen: (file: File) => void;
/**
* File input tag accept attribute.
Expand All @@ -13,53 +13,57 @@ interface OpenButtonProps extends ButtonProps {
/**
* File open button, with an associated input field.
*/
const FileInputButton = ({
accept,
onOpen,
leftIcon = <RiFolderOpenLine />,
children,
...props
}: OpenButtonProps) => {
const ref = useRef<HTMLInputElement>(null);
const FileInputButton = React.forwardRef(
(
{ accept, onOpen, icon, children, ...props }: FileInputButtonProps,
ref: ForwardedRef<HTMLButtonElement>
) => {
const inputRef = useRef<HTMLInputElement>(null);

const handleChooseFile = useCallback(() => {
ref.current && ref.current.click();
}, []);
const handleChooseFile = useCallback(() => {
inputRef.current && inputRef.current.click();
}, []);

const handleOpenFile = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files) {
const file = files.item(0);
// Clear the input so we're triggered if the user opens the same file again.
ref.current!.value = "";
if (file) {
onOpen(file);
const handleOpenFile = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files) {
const file = files.item(0);
// Clear the input so we're triggered if the user opens the same file again.
inputRef.current!.value = "";
if (file) {
onOpen(file);
}
}
}
},
[onOpen]
);
},
[onOpen]
);

return (
<>
<Input
data-testid={
(props as any)["data-testid"]
? (props as any)["data-testid"] + "-input"
: undefined
}
type="file"
accept={accept}
display="none"
onChange={handleOpenFile}
ref={ref}
/>
<Button leftIcon={leftIcon} onClick={handleChooseFile} {...props}>
{children}
</Button>
</>
);
};
return (
<>
<Input
data-testid={
(props as any)["data-testid"]
? (props as any)["data-testid"] + "-input"
: undefined
}
type="file"
accept={accept}
display="none"
onChange={handleOpenFile}
ref={inputRef}
/>
<CollapsableButton
ref={ref}
icon={icon}
onClick={handleChooseFile}
{...props}
>
{children}
</CollapsableButton>
</>
);
}
);

export default FileInputButton;
14 changes: 14 additions & 0 deletions src/common/FilesAreaNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ButtonGroup } from "@chakra-ui/button";
import NewButton from "../project/NewButton";
import UploadButton from "../project/UploadButton";

const FilesAreaNav = () => {
return (
<ButtonGroup pl={1} pr={1} spacing={0}>
<UploadButton variant="ghost" mode="icon" />
<NewButton variant="ghost" mode="icon" />
</ButtonGroup>
);
};

export default FilesAreaNav;
31 changes: 19 additions & 12 deletions src/e2e/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,9 @@ export class App {
* @param filename The name of the file in the file list.
*/
async switchToEditing(filename: string): Promise<void> {
await this.selectSideBar("Files");
await this.openFileActionsMenu(filename);
const document = await this.document();
const editButton = await document.findByRole("button", {
const editButton = await document.findByRole("menuitem", {
name: "Edit " + filename,
});
await editButton.click();
Expand All @@ -137,13 +137,12 @@ export class App {
* @param filename The name of the file in the file list.
*/
async canSwitchToEditing(filename: string): Promise<boolean> {
await this.selectSideBar("Files");
await this.openFileActionsMenu(filename);
const document = await this.document();
await document.findByText(filename);
const editButton = await document.getByRole("button", {
const editButton = await document.findByRole("menuitem", {
name: "Edit " + filename,
});
return editButton && !(await isDisabled(editButton));
return !(await isDisabled(editButton));
}

/**
Expand All @@ -155,24 +154,23 @@ export class App {
filename: string,
dialogChoice: string = "Delete"
): Promise<void> {
await this.selectSideBar("Files");
await this.openFileActionsMenu(filename);
const document = await this.document();
const button = await document.findByRole("button", {
const button = await document.findByRole("menuitem", {
name: "Delete " + filename,
});
await button.click();
await document.findByRole("alert");
const dialogButton = await document.findByRole("button", {
name: dialogChoice,
});
await dialogButton.click();
}

async canDeleteFile(filename: string): Promise<boolean> {
await this.selectSideBar("Files");
await this.openFileActionsMenu(filename);
const document = await this.document();
const button = await document.getByRole("button", {
name: "Delete " + filename,
const button = await document.findByRole("menuitem", {
name: `Delete ${filename}`,
});

return !(await isDisabled(button));
Expand Down Expand Up @@ -333,6 +331,15 @@ export class App {
await new Promise((resolve) => setTimeout(resolve, 20));
}
}

private async openFileActionsMenu(filename: string): Promise<void> {
await this.selectSideBar("Files");
const document = await this.document();
const actions = await document.findByRole("button", {
name: `${filename} file actions`,
});
await actions.click();
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/e2e/multiple-files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe("Browser - multiple and missing file cases", () => {
await app.findVisibleEditorContents(/Hello, World/);
});

it.only("Doesn't offer editor for non-Python file", async () => {
it("Doesn't offer editor for non-Python file", async () => {
await app.uploadFile("testData/null.dat");

expect(await app.canSwitchToEditing("null.dat")).toEqual(false);
Expand Down
88 changes: 60 additions & 28 deletions src/files/FileRow.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,91 @@
import { Button, HStack, IconButton } from "@chakra-ui/react";
import { RiDeleteBinLine, RiDownload2Line } from "react-icons/ri";
import {
BoxProps,
HStack,
IconButton,
Menu,
MenuButton,
MenuItem,
MenuList,
Portal,
Text,
} from "@chakra-ui/react";
import { MdMoreVert } from "react-icons/md";
import { RiDeleteBin2Line, RiDownload2Line, RiEdit2Line } from "react-icons/ri";
import { MAIN_FILE } from "../fs/fs";
import { FileVersion } from "../fs/storage";
import { useProjectActions } from "../project/project-hooks";
import { isEditableFile } from "../project/project-utils";

interface FileRowProps {
interface FileRowProps extends BoxProps {
projectName: string;
value: FileVersion;
onClick: () => void;
onEdit: () => void;
}

/**
* A row in the files area.
*/
const FileRow = ({ projectName, value, onClick }: FileRowProps) => {
const FileRow = ({ projectName, value, onEdit, ...props }: FileRowProps) => {
const { name } = value;
const isMainFile = name === MAIN_FILE;
const prettyName = isMainFile ? `${projectName} (${name})` : name;
const actions = useProjectActions();

return (
<HStack justify="space-between" lineHeight={2}>
<Button
onClick={onClick}
<HStack {...props} justify="space-between" lineHeight={2}>
{/* Accessibility for edit is via the row actions */}
<Text
component="span"
cursor="pointer"
onClick={onEdit}
variant="unstyled"
aria-label={`Edit ${name}`}
disabled={!isEditableFile(name)}
fontSize="md"
fontWeight="normal"
flexGrow={1}
textAlign="left"
overflowX="hidden"
textOverflow="ellipsis"
>
{prettyName}
</Button>
<HStack spacing={1}>
<IconButton
size="sm"
icon={<RiDeleteBinLine />}
aria-label={`Delete ${name}`}
</Text>
<Menu>
<MenuButton
as={IconButton}
aria-label={`${name} file actions`}
size="md"
variant="ghost"
disabled={isMainFile}
onClick={() => actions.deleteFile(name)}
icon={<MdMoreVert />}
/>
<IconButton
size="sm"
icon={<RiDownload2Line />}
aria-label={`Download ${name}`}
variant="ghost"
onClick={() => actions.downloadFile(name)}
/>
</HStack>
<Portal>
<MenuList>
<MenuItem
icon={<RiEdit2Line />}
isDisabled={!isEditableFile(name)}
onClick={onEdit}
aria-label={`Edit ${name}`}
>
Edit {name}
</MenuItem>
<MenuItem
icon={<RiDownload2Line />}
onClick={() => actions.downloadFile(name)}
aria-label={`Download ${name}`}
>
Download {name}
</MenuItem>
<MenuItem
icon={<RiDeleteBin2Line />}
onClick={() => actions.deleteFile(name)}
isDisabled={isMainFile}
aria-label={`Delete ${name}`}
>
Delete {name}
</MenuItem>
</MenuList>
</Portal>
</Menu>
</HStack>
);
};

const isEditableFile = (filename: string) => filename.match(/\.[Pp][Yy]$/);

export default FileRow;
Loading