ERC x LFS: workspace supports d/d

This commit is contained in:
Enrico Ros
2024-08-14 00:44:46 -07:00
parent c745aae281
commit 0bb4fd4517
4 changed files with 117 additions and 72 deletions
@@ -5,7 +5,7 @@ import { Box, Button, ColorPaletteProp, SvgIcon } from '@mui/joy';
import UploadFileRoundedIcon from '@mui/icons-material/UploadFileRounded';
import { TooltipOutlined } from '~/common/components/TooltipOutlined';
import { getDataTransferFilesOrPromises } from '~/common/util/fileSystemUtils';
import { getFirstFileSystemFileHandle } from '~/common/util/fileSystemUtils';
import { useDragDropDataTransfer } from '~/common/components/useDragDropDataTransfer';
import { LiveFileChooseIcon, LiveFileIcon } from '~/common/livefile/liveFile.icons';
@@ -41,19 +41,9 @@ export function LiveFileControlButton(props: {
// state
const handleDataTransfer = React.useCallback(async (dataTransfer: DataTransfer) => {
// get FileSystemFileHandle objects from the DataTransfer
const fileOrFSHandlePromises = getDataTransferFilesOrPromises(dataTransfer.items, false);
if (!fileOrFSHandlePromises.length)
return;
// resolve the promises to get the actual files/handles
const filesOrHandles = await Promise.all(fileOrFSHandlePromises);
for (let filesOrHandle of filesOrHandles) {
if (!(filesOrHandle instanceof File) && filesOrHandle?.kind === 'file' && filesOrHandle) {
await onPairWithFSFHandle(filesOrHandle);
break;
}
}
const fsfHandle = await getFirstFileSystemFileHandle(dataTransfer);
if (fsfHandle)
await onPairWithFSFHandle(fsfHandle);
}, [onPairWithFSFHandle]);
const { dragContainerSx, dropComponent, handleContainerDragEnter, handleContainerDragStart } =
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Box, Button, IconButton, ListDivider, ListItem, ListItemDecorator, MenuItem, Typography } from '@mui/joy';
import { Box, Button, IconButton, ListDivider, ListItem, ListItemDecorator, MenuItem, SvgIcon, Typography } from '@mui/joy';
import ClearIcon from '@mui/icons-material/Clear';
import CodeIcon from '@mui/icons-material/Code';
@@ -8,10 +8,13 @@ import type { LiveFileId, LiveFileMetadata } from '~/common/livefile/liveFile.ty
import { CloseableMenu } from '~/common/components/CloseableMenu';
import { LiveFileChooseIcon } from '~/common/livefile/liveFile.icons';
import { LiveFilePatchIcon } from '~/common/components/icons/LiveFilePatchIcon';
import { TooltipOutlined } from '~/common/components/TooltipOutlined';
import { getFirstFileSystemFileHandle } from '~/common/util/fileSystemUtils';
import { useDragDropDataTransfer } from '~/common/components/useDragDropDataTransfer';
import type { DWorkspaceId } from './workspace.types';
import { useContextWorkspaceId } from './WorkspaceIdProvider';
import { useWorkspaceContentsMetadata } from './useWorkspaceContentsMetadata';
import { useContextWorkspaceId } from '~/common/stores/workspace/WorkspaceIdProvider';
// configuration
@@ -22,13 +25,14 @@ const ENABLE_AUTO_WORKSPACE_PICK = false;
* Allows selection of LiveFiles in the current Workspace
*/
export function WorkspaceLiveFilePicker(props: {
autoSelectName: string | null;
buttonLabel: string;
liveFileId: LiveFileId | null;
allowRemove?: boolean;
autoSelectName: string | null;
labelButton: string;
labelTooltip?: string;
liveFileId: LiveFileId | null;
onSelectLiveFile: (id: LiveFileId | null) => void;
onSelectNewFile?: (workspaceId: DWorkspaceId | null) => void;
// tooltipLabel?: string;
onSelectFileOpen?: (workspaceId: DWorkspaceId | null) => Promise<void>;
onSelectFileSystemFileHandle?: (workspaceId: DWorkspaceId | null, fsHandle: FileSystemFileHandle) => Promise<void>;
}) {
// state for anchor
@@ -40,7 +44,7 @@ export function WorkspaceLiveFilePicker(props: {
// set as disabled when empty
const haveLiveFiles = wLiveFiles.length > 0;
const { autoSelectName, liveFileId, onSelectLiveFile, onSelectNewFile } = props;
const { autoSelectName, liveFileId, onSelectLiveFile, onSelectFileOpen, onSelectFileSystemFileHandle } = props;
// [effect] auto-select a LiveFileId
@@ -72,16 +76,29 @@ export function WorkspaceLiveFilePicker(props: {
}, []);
const handleSelectLiveFile = React.useCallback((id: LiveFileId | null) => {
onSelectLiveFile(id);
setMenuAnchor(null);
onSelectLiveFile(id);
}, [onSelectLiveFile]);
const handleSelectNewFile = React.useCallback(() => {
if (onSelectNewFile) {
onSelectNewFile(workspaceId);
const handleSelectNewFile = React.useCallback(async () => {
if (onSelectFileOpen) {
setMenuAnchor(null);
await onSelectFileOpen(workspaceId);
}
}, [onSelectNewFile, workspaceId]);
}, [onSelectFileOpen, workspaceId]);
const handleDataTransferDrop = React.useCallback(async (dataTransfer: DataTransfer) => {
if (onSelectFileSystemFileHandle) {
const fsfHandle = await getFirstFileSystemFileHandle(dataTransfer);
if (fsfHandle) {
setMenuAnchor(null);
await onSelectFileSystemFileHandle(workspaceId, fsfHandle);
}
}
}, [onSelectFileSystemFileHandle, workspaceId]);
const { dragContainerSx, dropComponent, handleContainerDragEnter, handleContainerDragStart } =
useDragDropDataTransfer(true, 'Select', LiveFileChooseIcon as typeof SvgIcon, 'startDecorator', true, handleDataTransferDrop);
// Note: in the future let this be, we can show a file picker that adds LiveFiles to the workspace
@@ -92,35 +109,42 @@ export function WorkspaceLiveFilePicker(props: {
return <>
{/*<TooltipOutlined*/}
{/* title={tooltipLabel} */}
{/* color='success'*/}
{/* placement='top-end'*/}
{/*>*/}
{liveFileId ? (
<IconButton
color='success'
size='sm'
onClick={handleToggleMenu}
>
<LiveFilePatchIcon color='success' />
</IconButton>
) : (
<Button
variant='plain'
color='neutral'
size='sm'
onClick={handleToggleMenu}
endDecorator={<LiveFileChooseIcon color='success' />}
// endDecorator={<LiveFilePatchIcon color='success' />}
>
{props.buttonLabel}
</Button>
)}
{/*</TooltipOutlined>*/}
{/* Main Button, also a drop target */}
<Box
onDragEnter={handleContainerDragEnter}
onDragStart={handleContainerDragStart}
sx={dragContainerSx}
>
{liveFileId && (
<IconButton
color='success'
size='sm'
onClick={handleToggleMenu}
>
<LiveFilePatchIcon color='success' />
</IconButton>
)}
{!liveFileId && (
<TooltipOutlined title={props.labelTooltip} color='success' placement='top-end'>
<Button
variant='plain'
color='neutral'
size='sm'
onClick={handleToggleMenu}
endDecorator={<LiveFileChooseIcon />}
// endDecorator={<LiveFilePatchIcon color='success' />}
>
{props.labelButton}
</Button>
</TooltipOutlined>
)}
{dropComponent}
</Box>
{/* Menu: list of workspace files */}
{/* Select/Upload file menu */}
{!!menuAnchor && (
<CloseableMenu
open
@@ -155,7 +179,7 @@ export function WorkspaceLiveFilePicker(props: {
))}
{/* Pair a new file */}
{!!props.onSelectNewFile && (
{!!props.onSelectFileOpen && (
<MenuItem onClick={handleSelectNewFile} sx={haveLiveFiles ? { minHeight: '3rem' } : undefined}>
<ListItemDecorator>
<LiveFileChooseIcon />
+22
View File
@@ -96,3 +96,25 @@ export function getDataTransferFilesOrPromises(items: DataTransferItemList, fall
return results;
}
/**
* Utility function to get the first file system handle from a DataTransfer object.
* Note that a DataTransfer object can contain multiple files, but we assume the first is the one.
*/
export async function getFirstFileSystemFileHandle(dataTransfer: DataTransfer): Promise<FileSystemFileHandle | null> {
// get FileSystemFileHandle objects from the DataTransfer
const fileOrFSHandlePromises = getDataTransferFilesOrPromises(dataTransfer.items, false);
if (!fileOrFSHandlePromises.length)
return null;
// resolve the promises to get the actual files/handles
const filesOrHandles = await Promise.all(fileOrFSHandlePromises);
for (let filesOrHandle of filesOrHandles)
if (!(filesOrHandle instanceof File) && filesOrHandle?.kind === 'file' && filesOrHandle)
return filesOrHandle;
// no file system handle found
return null;
}
@@ -28,23 +28,15 @@ export function useLiveFilePatch(title: string, code: string, isPartial: boolean
// handlers
const handleLiveFileSelected = React.useCallback((id: LiveFileId | null) => {
const handleSelectLiveFile = React.useCallback((id: LiveFileId | null) => {
setLiveFileId(id);
}, []);
const handleSelectNewFile = React.useCallback(async (workspaceId: DWorkspaceId | null) => {
// pick a file
const fileWithHandle = await fileOpen({ description: 'Insert into file...' }).catch(() => null /* The User closed the files picker */);
if (!fileWithHandle)
return;
const fileSystemFileHandle = fileWithHandle.handle;
if (!fileSystemFileHandle) {
// setStatus({ message: `Browser does not support LiveFile operations. ${isLiveFileSupported() ? 'No filesystem handles.' : ''}`, mtype: 'error' });
return;
}
const handleSelectFileSystemFileHandle = React.useCallback(async (workspaceId: DWorkspaceId | null, fsfHandle: FileSystemFileHandle) => {
// Create a new LiveFile and attach it to the workspace
try {
const newLiveFileId = await liveFileCreateOrThrow(fileSystemFileHandle);
const newLiveFileId = await liveFileCreateOrThrow(fsfHandle);
setLiveFileId(newLiveFileId);
// Pair the file with the workspace
@@ -59,9 +51,24 @@ export function useLiveFilePatch(title: string, code: string, isPartial: boolean
} catch (error) {
console.error('Error creating new file:', error);
// setStatus({ message: `Error pairing the file: ${error?.message || typeof error === 'string' ? error : 'Unknown error'}`, mtype: 'error' });
}
}, []);
const handleSelectFilePicker = React.useCallback(async (workspaceId: DWorkspaceId | null) => {
// pick a file
const fileWithHandle = await fileOpen({ description: 'Insert into file...' }).catch(() => null /* The User closed the files picker */);
if (!fileWithHandle)
return;
const fileSystemFileHandle = fileWithHandle.handle;
if (!fileSystemFileHandle) {
// setStatus({ message: `Browser does not support LiveFile operations. ${isLiveFileSupported() ? 'No filesystem handles.' : ''}`, mtype: 'error' });
return;
}
// proceed
await handleSelectFileSystemFileHandle(workspaceId, fileSystemFileHandle);
}, [handleSelectFileSystemFileHandle]);
// components
@@ -88,14 +95,16 @@ export function useLiveFilePatch(title: string, code: string, isPartial: boolean
{/* Pick LiveFile */}
<WorkspaceLiveFilePicker
autoSelectName={title}
buttonLabel='Insert...'
labelButton='Insert...'
labelTooltip='Insert this code into a file'
liveFileId={liveFileId}
onSelectLiveFile={handleLiveFileSelected}
onSelectNewFile={handleSelectNewFile}
onSelectFileOpen={handleSelectFilePicker}
onSelectFileSystemFileHandle={handleSelectFileSystemFileHandle}
onSelectLiveFile={handleSelectLiveFile}
/>
</Box>
), [handleLiveFileSelected, handleSelectNewFile, isEnabled, liveFileId, title]);
), [handleSelectLiveFile, handleSelectFilePicker, handleSelectFileSystemFileHandle, isEnabled, liveFileId, title]);
const actionBar = React.useMemo(() => (!isEnabled || !liveFileId || true) ? null : (