UI: Fix several issues with script editor tabs (#554) (#567)

This commit is contained in:
Aleksei Bezrodnov 2023-06-04 18:01:06 +02:00 committed by GitHub
parent bda1daf49f
commit 7050c90378
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 209 additions and 142 deletions

100
src/ScriptEditor/ui/Tab.tsx Normal file

@ -0,0 +1,100 @@
import React, { useEffect, useRef } from "react";
import { DraggableProvided } from "react-beautiful-dnd";
import Button from "@mui/material/Button";
import Tooltip from "@mui/material/Tooltip";
import SyncIcon from "@mui/icons-material/Sync";
import CloseIcon from "@mui/icons-material/Close";
import { Settings } from "../../Settings/Settings";
interface IProps {
provided: DraggableProvided;
title: string;
isActive: boolean;
isExternal: boolean;
onClick: () => void;
onClose: () => void;
onUpdate: () => void;
}
const tabMargin = 5;
const tabIconWidth = 25;
const tabIconHeight = 38.5;
export function Tab({ provided, title, isActive, isExternal, onClick, onClose, onUpdate }: IProps) {
const colorProps = isActive
? {
background: Settings.theme.button,
borderColor: Settings.theme.button,
color: Settings.theme.primary,
}
: {
background: Settings.theme.backgroundsecondary,
borderColor: Settings.theme.backgroundsecondary,
color: Settings.theme.secondary,
};
if (isExternal) {
colorProps.color = Settings.theme.info;
}
const iconButtonStyle = {
maxWidth: tabIconWidth,
minWidth: tabIconWidth,
minHeight: tabIconHeight,
maxHeight: tabIconHeight,
...colorProps,
};
const tabRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (tabRef.current && isActive) {
tabRef.current?.scrollIntoView();
}
}, [isActive]);
return (
<div
ref={(element) => {
tabRef.current = element;
provided.innerRef(element);
}}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
...provided.draggableProps.style,
marginRight: tabMargin,
flexShrink: 0,
border: "1px solid " + Settings.theme.well,
}}
>
<Tooltip title={title}>
<Button
onClick={onClick}
onMouseDown={(e) => {
e.preventDefault();
if (e.button === 1) onClose();
}}
style={{
minHeight: tabIconHeight,
overflow: "hidden",
...colorProps,
}}
>
<span style={{ overflow: "hidden", direction: "rtl", textOverflow: "ellipsis" }}>{title}</span>
</Button>
</Tooltip>
<Tooltip title="Overwrite editor content with saved file content">
<Button onClick={onUpdate} style={iconButtonStyle}>
<SyncIcon fontSize="small" />
</Button>
</Tooltip>
<Button onClick={onClose} style={iconButtonStyle}>
<CloseIcon fontSize="small" />
</Button>
</div>
);
}

@ -1,20 +1,24 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; import { DragDropContext, Droppable, Draggable, DropResult } from "react-beautiful-dnd";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip";
import { Box, Button, TextField, Tooltip } from "@mui/material";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import SearchIcon from "@mui/icons-material/Search"; import SearchIcon from "@mui/icons-material/Search";
import SyncIcon from "@mui/icons-material/Sync";
import { useRerender } from "../../ui/React/hooks"; import { useBoolean, useRerender } from "../../ui/React/hooks";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { dirty, reorder } from "./utils"; import { dirty, reorder } from "./utils";
import { OpenScript } from "./OpenScript"; import { OpenScript } from "./OpenScript";
import { Tab } from "./Tab";
const tabsMaxWidth = 1640; const tabsMaxWidth = 1640;
const tabMargin = 5; const searchWidth = 180;
const tabIconWidth = 25;
interface IProps { interface IProps {
scripts: OpenScript[]; scripts: OpenScript[];
@ -27,160 +31,123 @@ interface IProps {
export function Tabs({ scripts, currentScript, onTabClick, onTabClose, onTabUpdate }: IProps) { export function Tabs({ scripts, currentScript, onTabClick, onTabClose, onTabUpdate }: IProps) {
const [filter, setFilter] = useState(""); const [filter, setFilter] = useState("");
const [isSearchTooltipOpen, { on: openSearchTooltip, off: closeSearchTooltip }] = useBoolean(false);
const [searchExpanded, setSearchExpanded] = useState(false); const [searchExpanded, setSearchExpanded] = useState(false);
const rerender = useRerender(); const rerender = useRerender();
function onDragEnd(result: any): void { const filteredScripts = Object.values(scripts)
.map((script, originalIndex) => ({ script, originalIndex }))
.filter(({ script }) => script.hostname.includes(filter) || script.path.includes(filter));
function onDragEnd(result: DropResult): void {
// Dropped outside of the list // Dropped outside of the list
if (!result.destination) return; if (!result.destination) return;
reorder(scripts, result.source.index, result.destination.index); reorder(
scripts,
filteredScripts[result.source.index].originalIndex,
filteredScripts[result.destination.index].originalIndex,
);
rerender(); rerender();
} }
const filteredOpenScripts = Object.values(scripts).filter(
(script) => script.hostname.includes(filter) || script.path.includes(filter),
);
function handleFilterChange(event: React.ChangeEvent<HTMLInputElement>): void { function handleFilterChange(event: React.ChangeEvent<HTMLInputElement>): void {
setFilter(event.target.value); setFilter(event.target.value);
} }
function handleExpandSearch(): void { function toggleSearch(): void {
setFilter(""); setFilter("");
setSearchExpanded(!searchExpanded); setSearchExpanded(!searchExpanded);
closeSearchTooltip();
}
function handleScroll(e: React.WheelEvent<HTMLDivElement>): void {
e.currentTarget.scrollLeft += e.deltaY;
} }
const tabMaxWidth = filteredOpenScripts.length ? tabsMaxWidth / filteredOpenScripts.length - tabMargin : 0;
const tabTextWidth = tabMaxWidth - tabIconWidth * 2;
return ( return (
<DragDropContext onDragEnd={onDragEnd}> <Box display="flex" flexGrow="0" flexDirection="row" alignItems="center">
<Droppable droppableId="tabs" direction="horizontal"> <Tooltip
{(provided, snapshot) => ( title={"Search Open Scripts"}
<Box open={isSearchTooltipOpen}
maxWidth={`${tabsMaxWidth}px`} onOpen={openSearchTooltip}
display="flex" onClose={closeSearchTooltip}
flexGrow="0" >
flexDirection="row" <span style={{ marginRight: 5 }}>
alignItems="center" {searchExpanded ? (
whiteSpace="nowrap" <TextField
ref={provided.innerRef} value={filter}
{...provided.droppableProps} onChange={handleFilterChange}
style={{ autoFocus
backgroundColor: snapshot.isDraggingOver sx={{ minWidth: searchWidth, maxWidth: searchWidth }}
? Settings.theme.backgroundsecondary InputProps={{
: Settings.theme.backgroundprimary, startAdornment: <SearchIcon />,
overflowX: "scroll", spellCheck: false,
}} endAdornment: (
> <IconButton onClick={toggleSearch}>
<Tooltip title={"Search Open Scripts"}> <CloseIcon />
{searchExpanded ? ( </IconButton>
<TextField ),
value={filter} }}
onChange={handleFilterChange} />
autoFocus ) : (
InputProps={{ <Button onClick={toggleSearch}>
startAdornment: <SearchIcon />, <SearchIcon />
spellCheck: false, </Button>
endAdornment: <CloseIcon onClick={handleExpandSearch} />, )}
// TODO: reapply </span>
// sx: { minWidth: 200 }, </Tooltip>
}} <DragDropContext onDragEnd={onDragEnd}>
/> <Droppable droppableId="tabs" direction="horizontal">
) : ( {(provided, snapshot) => (
<Button onClick={handleExpandSearch}> <Box
<SearchIcon /> maxWidth={`${tabsMaxWidth}px`}
</Button> display="flex"
)} flexGrow="1"
</Tooltip> flexDirection="row"
{filteredOpenScripts.map(({ path: fileName, hostname }, index) => { alignItems="center"
const editingCurrentScript = whiteSpace="nowrap"
currentScript?.path === filteredOpenScripts[index].path && ref={provided.innerRef}
currentScript.hostname === filteredOpenScripts[index].hostname; {...provided.droppableProps}
const externalScript = hostname !== "home"; style={{
const colorProps = editingCurrentScript backgroundColor: snapshot.isDraggingOver
? { ? Settings.theme.backgroundsecondary
background: Settings.theme.button, : Settings.theme.backgroundprimary,
borderColor: Settings.theme.button, overflowX: "scroll",
color: Settings.theme.primary, }}
} onWheel={handleScroll}
: { >
background: Settings.theme.backgroundsecondary, {filteredScripts.map(({ script, originalIndex }, index) => {
borderColor: Settings.theme.backgroundsecondary, const { path: fileName, hostname } = script;
color: Settings.theme.secondary, const isActive = currentScript?.path === script.path && currentScript.hostname === script.hostname;
};
if (externalScript) { const title = `${hostname}:~${fileName.startsWith("/") ? "" : "/"}${fileName} ${dirty(scripts, index)}`;
colorProps.color = Settings.theme.info;
}
const iconButtonStyle = {
maxWidth: `${tabIconWidth}px`,
minWidth: `${tabIconWidth}px`,
minHeight: "38.5px",
maxHeight: "38.5px",
...colorProps,
};
const scriptTabText = `${hostname}:~${fileName.startsWith("/") ? "" : "/"}${fileName} ${dirty( return (
scripts, <Draggable
index, key={fileName + hostname}
)}`; draggableId={fileName + hostname}
index={index}
return ( disableInteractiveElementBlocking
<Draggable >
key={fileName + hostname} {(provided) => (
draggableId={fileName + hostname} <Tab
index={index} provided={provided}
disableInteractiveElementBlocking={true} title={title}
> isActive={isActive}
{(provided) => ( isExternal={hostname !== "home"}
<div onClick={() => onTabClick(originalIndex)}
ref={provided.innerRef} onClose={() => onTabClose(originalIndex)}
{...provided.draggableProps} onUpdate={() => onTabUpdate(originalIndex)}
{...provided.dragHandleProps} />
style={{ )}
...provided.draggableProps.style, </Draggable>
// maxWidth: `${tabMaxWidth}px`, );
marginRight: `${tabMargin}px`, })}
flexShrink: 0, {provided.placeholder}
border: "1px solid " + Settings.theme.well, </Box>
}} )}
> </Droppable>
<Tooltip title={scriptTabText}> </DragDropContext>
<Button </Box>
onClick={() => onTabClick(index)}
onMouseDown={(e) => {
e.preventDefault();
if (e.button === 1) onTabClose(index);
}}
style={{
maxWidth: `${tabTextWidth}px`,
minHeight: "38.5px",
overflow: "hidden",
...colorProps,
}}
>
<span style={{ overflow: "hidden", direction: "rtl", textOverflow: "ellipsis" }}>
{scriptTabText}
</span>
</Button>
</Tooltip>
<Tooltip title="Overwrite editor content with saved file content">
<Button onClick={() => onTabUpdate(index)} style={iconButtonStyle}>
<SyncIcon fontSize="small" />
</Button>
</Tooltip>
<Button onClick={() => onTabClose(index)} style={iconButtonStyle}>
<CloseIcon fontSize="small" />
</Button>
</div>
)}
</Draggable>
);
})}
{provided.placeholder}
</Box>
)}
</Droppable>
</DragDropContext>
); );
} }