mirror of
https://github.com/cloudreve/frontend.git
synced 2025-12-25 19:52:48 +00:00
feat: list reordering with drag-and-drop and move buttons
This commit is contained in:
parent
0c9e7b8f7a
commit
bd712f506f
|
|
@ -1,3 +1,4 @@
|
|||
import React from "react";
|
||||
import { Box, IconButton, Stack, Table, TableBody, TableContainer, TableHead, TableRow } from "@mui/material";
|
||||
import { memo, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
|
@ -10,12 +11,145 @@ import {
|
|||
} from "../../../Common/StyledComponents.tsx";
|
||||
import Add from "../../../Icons/Add.tsx";
|
||||
import Dismiss from "../../../Icons/Dismiss.tsx";
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import { DndProvider, useDrag, useDrop } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
|
||||
export interface EmojiListProps {
|
||||
config: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const DND_TYPE = "emoji-row";
|
||||
|
||||
interface DraggableEmojiRowProps {
|
||||
r: string;
|
||||
i: number;
|
||||
moveRow: (from: number, to: number) => void;
|
||||
configParsed: { [key: string]: string[] };
|
||||
inputCache: { [key: number]: string | undefined };
|
||||
setInputCache: React.Dispatch<React.SetStateAction<{ [key: number]: string | undefined }>>;
|
||||
onChange: (value: string) => void;
|
||||
isFirst: boolean;
|
||||
isLast: boolean;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
function DraggableEmojiRow({
|
||||
r,
|
||||
i,
|
||||
moveRow,
|
||||
configParsed,
|
||||
inputCache,
|
||||
setInputCache,
|
||||
onChange,
|
||||
isFirst,
|
||||
isLast,
|
||||
}: DraggableEmojiRowProps) {
|
||||
const ref = React.useRef<HTMLTableRowElement>(null);
|
||||
const [, drop] = useDrop({
|
||||
accept: DND_TYPE,
|
||||
hover(item: any, monitor) {
|
||||
if (!ref.current) return;
|
||||
|
||||
const dragIndex = item.index;
|
||||
const hoverIndex = i;
|
||||
if (dragIndex === hoverIndex) return;
|
||||
|
||||
const hoverBoundingRect = ref.current.getBoundingClientRect();
|
||||
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
if (!clientOffset) return;
|
||||
|
||||
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
||||
|
||||
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return;
|
||||
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return;
|
||||
|
||||
moveRow(dragIndex, hoverIndex);
|
||||
item.index = hoverIndex;
|
||||
},
|
||||
});
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: DND_TYPE,
|
||||
item: { index: i },
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
});
|
||||
drag(drop(ref));
|
||||
return (
|
||||
<TableRow
|
||||
ref={ref}
|
||||
sx={{ "&:last-child td, &:last-child th": { border: 0 }, opacity: isDragging ? 0.5 : 1, cursor: "move" }}
|
||||
hover
|
||||
>
|
||||
<NoWrapCell>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
required
|
||||
value={r}
|
||||
onChange={(e) => {
|
||||
const newConfig = {
|
||||
...configParsed,
|
||||
[e.target.value]: configParsed[r],
|
||||
};
|
||||
delete newConfig[r];
|
||||
onChange(JSON.stringify(newConfig));
|
||||
}}
|
||||
/>
|
||||
</NoWrapCell>
|
||||
<NoWrapCell>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
multiline
|
||||
required
|
||||
value={inputCache[i] ?? configParsed[r].join()}
|
||||
onBlur={() => {
|
||||
onChange(
|
||||
JSON.stringify({
|
||||
...configParsed,
|
||||
[r]: inputCache[i]?.split(",") ?? configParsed[r],
|
||||
}),
|
||||
);
|
||||
setInputCache({
|
||||
...inputCache,
|
||||
[i]: undefined,
|
||||
});
|
||||
}}
|
||||
onChange={(e) =>
|
||||
setInputCache({
|
||||
...inputCache,
|
||||
[i]: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</NoWrapCell>
|
||||
<NoWrapCell>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
const newConfig = {
|
||||
...configParsed,
|
||||
};
|
||||
delete newConfig[r];
|
||||
onChange(JSON.stringify(newConfig));
|
||||
}}
|
||||
size={"small"}
|
||||
>
|
||||
<Dismiss fontSize={"small"} />
|
||||
</IconButton>
|
||||
<IconButton size="small" onClick={() => moveRow(i, i - 1)} disabled={isFirst}>
|
||||
<KeyboardArrowUpIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton size="small" onClick={() => moveRow(i, i + 1)} disabled={isLast}>
|
||||
<KeyboardArrowDownIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</NoWrapCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
const EmojiList = memo(({ config, onChange }: EmojiListProps) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const [render, setRender] = useState(false);
|
||||
|
|
@ -32,80 +166,49 @@ const EmojiList = memo(({ config, onChange }: EmojiListProps) => {
|
|||
</SecondaryButton>
|
||||
)}
|
||||
{render && Object.keys(configParsed ?? {}).length > 0 && (
|
||||
<TableContainer sx={{ mt: 1, maxHeight: 440 }} component={StyledTableContainerPaper}>
|
||||
<Table stickyHeader sx={{ width: "100%", tableLayout: "fixed" }} size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<NoWrapTableCell width={64}>{t("settings.category")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={200}>{t("settings.emojiOptions")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={64}></NoWrapTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{Object.keys(configParsed ?? {}).map((r, i) => (
|
||||
<TableRow key={i} sx={{ "&:last-child td, &:last-child th": { border: 0 } }} hover>
|
||||
<NoWrapCell>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
required
|
||||
value={r}
|
||||
onChange={(e) => {
|
||||
const newConfig = {
|
||||
...configParsed,
|
||||
[e.target.value]: configParsed[r],
|
||||
};
|
||||
delete newConfig[r];
|
||||
onChange(JSON.stringify(newConfig));
|
||||
}}
|
||||
/>
|
||||
</NoWrapCell>
|
||||
<NoWrapCell>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
multiline
|
||||
required
|
||||
value={inputCache[i] ?? configParsed[r].join()}
|
||||
onBlur={() => {
|
||||
onChange(
|
||||
JSON.stringify({
|
||||
...configParsed,
|
||||
[r]: inputCache[i]?.split(",") ?? configParsed[r],
|
||||
}),
|
||||
);
|
||||
setInputCache({
|
||||
...inputCache,
|
||||
[i]: undefined,
|
||||
});
|
||||
}}
|
||||
onChange={(e) =>
|
||||
setInputCache({
|
||||
...inputCache,
|
||||
[i]: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</NoWrapCell>
|
||||
|
||||
<NoWrapCell>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
const newConfig = {
|
||||
...configParsed,
|
||||
};
|
||||
|
||||
delete newConfig[r];
|
||||
onChange(JSON.stringify(newConfig));
|
||||
}}
|
||||
size={"small"}
|
||||
>
|
||||
<Dismiss fontSize={"small"} />
|
||||
</IconButton>
|
||||
</NoWrapCell>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<TableContainer sx={{ mt: 1, maxHeight: 440 }} component={StyledTableContainerPaper}>
|
||||
<Table stickyHeader sx={{ width: "100%", tableLayout: "fixed" }} size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<NoWrapTableCell width={64}>{t("settings.category")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={200}>{t("settings.emojiOptions")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={100}>{t("settings.actions")}</NoWrapTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{Object.keys(configParsed ?? {}).map((r, i, arr) => (
|
||||
<DraggableEmojiRow
|
||||
key={i}
|
||||
r={r}
|
||||
i={i}
|
||||
moveRow={(from, to) => {
|
||||
if (from === to || to < 0 || to >= arr.length) return;
|
||||
const keys = Object.keys(configParsed);
|
||||
const values = Object.values(configParsed);
|
||||
const [movedKey] = keys.splice(from, 1);
|
||||
const [movedValue] = values.splice(from, 1);
|
||||
keys.splice(to, 0, movedKey);
|
||||
values.splice(to, 0, movedValue);
|
||||
const newConfig: { [key: string]: string[] } = {};
|
||||
keys.forEach((k, idx) => {
|
||||
newConfig[k] = values[idx];
|
||||
});
|
||||
onChange(JSON.stringify(newConfig));
|
||||
}}
|
||||
configParsed={configParsed}
|
||||
inputCache={inputCache}
|
||||
setInputCache={setInputCache}
|
||||
onChange={onChange}
|
||||
isFirst={i === 0}
|
||||
isLast={i === arr.length - 1}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</DndProvider>
|
||||
)}
|
||||
</Box>
|
||||
{render && (
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@ import Dismiss from "../../../../Icons/Dismiss.tsx";
|
|||
import SettingForm from "../../../../Pages/Setting/SettingForm.tsx";
|
||||
import MagicVarDialog, { MagicVar } from "../../../Common/MagicVarDialog.tsx";
|
||||
import { NoMarginHelperText } from "../../Settings.tsx";
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import { DndProvider, useDrag, useDrop } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import { SelectChangeEvent } from "@mui/material";
|
||||
|
||||
const MonacoEditor = lazy(() => import("../../../../Viewers/CodeViewer/MonacoEditor.tsx"));
|
||||
|
||||
|
|
@ -86,6 +91,96 @@ const magicVars: MagicVar[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const DND_TYPE = "template-row";
|
||||
|
||||
interface DraggableTemplateRowProps {
|
||||
t: any;
|
||||
i: number;
|
||||
moveRow: (from: number, to: number) => void;
|
||||
onExtChange: (e: SelectChangeEvent<unknown>, child: React.ReactNode) => void;
|
||||
onNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onDelete: () => void;
|
||||
isFirst: boolean;
|
||||
isLast: boolean;
|
||||
extList: string[];
|
||||
template: any;
|
||||
}
|
||||
|
||||
function DraggableTemplateRow({ i, moveRow, onExtChange, onNameChange, onDelete, isFirst, isLast, extList, template }: DraggableTemplateRowProps) {
|
||||
const ref = React.useRef<HTMLTableRowElement>(null);
|
||||
const [, drop] = useDrop({
|
||||
accept: DND_TYPE,
|
||||
hover(item: any, monitor) {
|
||||
if (!ref.current) return;
|
||||
|
||||
const dragIndex = item.index;
|
||||
const hoverIndex = i;
|
||||
if (dragIndex === hoverIndex) return;
|
||||
|
||||
const hoverBoundingRect = ref.current.getBoundingClientRect();
|
||||
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
if (!clientOffset) return;
|
||||
|
||||
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
||||
|
||||
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return;
|
||||
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return;
|
||||
|
||||
moveRow(dragIndex, hoverIndex);
|
||||
item.index = hoverIndex;
|
||||
},
|
||||
});
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: DND_TYPE,
|
||||
item: { index: i },
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
});
|
||||
drag(drop(ref));
|
||||
return (
|
||||
<TableRow
|
||||
ref={ref}
|
||||
sx={{ "&:last-child td, &:last-child th": { border: 0 }, opacity: isDragging ? 0.5 : 1, cursor: "move" }}
|
||||
hover
|
||||
>
|
||||
<NoWrapTableCell>
|
||||
<DenseSelect
|
||||
value={template.ext}
|
||||
required
|
||||
onChange={onExtChange}
|
||||
>
|
||||
{extList.map((ext) => (
|
||||
<SquareMenuItem value={ext} key={ext}>
|
||||
<ListItemText slotProps={{ primary: { variant: "body2" } }}>{ext}</ListItemText>
|
||||
</SquareMenuItem>
|
||||
))}
|
||||
</DenseSelect>
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
required
|
||||
value={template.display_name}
|
||||
onChange={onNameChange}
|
||||
/>
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell>
|
||||
<IconButton size={"small"} onClick={onDelete}>
|
||||
<Dismiss fontSize={"small"} />
|
||||
</IconButton>
|
||||
<IconButton size="small" onClick={() => moveRow(i, i - 1)} disabled={isFirst}>
|
||||
<KeyboardArrowUpIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton size="small" onClick={() => moveRow(i, i + 1)} disabled={isLast}>
|
||||
<KeyboardArrowDownIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</NoWrapTableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
const FileViewerEditDialog = ({ viewer, onChange, open, onClose }: FileViewerEditDialogProps) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const theme = useTheme();
|
||||
|
|
@ -300,99 +395,72 @@ const FileViewerEditDialog = ({ viewer, onChange, open, onClose }: FileViewerEdi
|
|||
)}
|
||||
<SettingForm noContainer title={t("settings.newFileAction")} lgWidth={12}>
|
||||
{viewerShadowed?.templates && viewerShadowed.templates.length > 0 && (
|
||||
<TableContainer sx={{ mt: 1, maxHeight: 440 }} component={StyledTableContainerPaper}>
|
||||
<Table
|
||||
stickyHeader
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxHeight: 300,
|
||||
tableLayout: "fixed",
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<NoWrapTableCell width={100}>{t("settings.ext")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={200}>{t("settings.displayName")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={64}></NoWrapTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{viewerShadowed.templates?.map((t, i) => (
|
||||
<TableRow
|
||||
key={i}
|
||||
sx={{
|
||||
"&:last-child td, &:last-child th": { border: 0 },
|
||||
}}
|
||||
hover
|
||||
>
|
||||
<NoWrapTableCell>
|
||||
<DenseSelect
|
||||
value={t.ext}
|
||||
required
|
||||
onChange={(e) => {
|
||||
const newExt = e.target.value as string;
|
||||
setViewerShadowed((v) => ({
|
||||
...(v as Viewer),
|
||||
templates: (v?.templates ?? []).map((template, index) =>
|
||||
index == i ? { ...template, ext: newExt } : template,
|
||||
),
|
||||
}));
|
||||
}}
|
||||
>
|
||||
{viewerShadowed.exts.map((ext) => (
|
||||
<SquareMenuItem value={ext}>
|
||||
<ListItemText
|
||||
slotProps={{
|
||||
primary: {
|
||||
variant: "body2",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{ext}
|
||||
</ListItemText>
|
||||
</SquareMenuItem>
|
||||
))}
|
||||
</DenseSelect>
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
required
|
||||
value={t.display_name}
|
||||
onChange={(e) => {
|
||||
setViewerShadowed((v) => ({
|
||||
...(v as Viewer),
|
||||
templates: (v?.templates ?? []).map((template, index) =>
|
||||
index == i
|
||||
? {
|
||||
...template,
|
||||
display_name: e.target.value,
|
||||
}
|
||||
: template,
|
||||
),
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell>
|
||||
<IconButton
|
||||
size={"small"}
|
||||
onClick={() =>
|
||||
setViewerShadowed((v) => ({
|
||||
...(v as Viewer),
|
||||
templates: (v?.templates ?? []).filter((_, index) => index != i),
|
||||
}))
|
||||
}
|
||||
>
|
||||
<Dismiss fontSize={"small"} />
|
||||
</IconButton>
|
||||
</NoWrapTableCell>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<TableContainer sx={{ mt: 1, maxHeight: 440 }} component={StyledTableContainerPaper}>
|
||||
<Table
|
||||
stickyHeader
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxHeight: 300,
|
||||
tableLayout: "fixed",
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<NoWrapTableCell width={100}>{t("settings.ext")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={200}>{t("settings.displayName")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={100}>{t("settings.actions")}</NoWrapTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{viewerShadowed.templates?.map((template, i) => (
|
||||
<DraggableTemplateRow
|
||||
key={i}
|
||||
t={template}
|
||||
i={i}
|
||||
moveRow={(from, to) => {
|
||||
if (from === to || to < 0 || to >= (viewerShadowed.templates?.length ?? 0)) return;
|
||||
setViewerShadowed((v) => {
|
||||
const arr = [...(v?.templates ?? [])];
|
||||
const [moved] = arr.splice(from, 1);
|
||||
arr.splice(to, 0, moved);
|
||||
return { ...(v as Viewer), templates: arr };
|
||||
});
|
||||
}}
|
||||
onExtChange={(e) => {
|
||||
const newExt = e.target.value as string;
|
||||
setViewerShadowed((v) => ({
|
||||
...(v as Viewer),
|
||||
templates: (v?.templates ?? []).map((template, index) =>
|
||||
index == i ? { ...template, ext: newExt } : template,
|
||||
),
|
||||
}));
|
||||
}}
|
||||
onNameChange={(e) => {
|
||||
setViewerShadowed((v) => ({
|
||||
...(v as Viewer),
|
||||
templates: (v?.templates ?? []).map((template, index) =>
|
||||
index == i ? { ...template, display_name: e.target.value } : template,
|
||||
),
|
||||
}));
|
||||
}}
|
||||
onDelete={() => {
|
||||
setViewerShadowed((v) => ({
|
||||
...(v as Viewer),
|
||||
templates: (v?.templates ?? []).filter((_, index) => index != i),
|
||||
}));
|
||||
}}
|
||||
isFirst={i === 0}
|
||||
isLast={i === (viewerShadowed.templates?.length ?? 0) - 1}
|
||||
extList={viewerShadowed.exts}
|
||||
template={template}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</DndProvider>
|
||||
)}
|
||||
<SecondaryButton
|
||||
sx={{ mt: 1 }}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import { AccordionSummary, StyledAccordion } from "../../UserSession/SSOSettings
|
|||
import FileViewerEditDialog from "./FileViewerEditDialog.tsx";
|
||||
import FileViewerRow from "./FileViewerRow.tsx";
|
||||
import ImportWopiDialog from "./ImportWopiDialog.tsx";
|
||||
import { DndProvider, useDrag, useDrop } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
|
||||
interface ViewerGroupProps {
|
||||
group: ViewerGroup;
|
||||
|
|
@ -35,6 +37,66 @@ interface ViewerGroupProps {
|
|||
onGroupChange: (g: ViewerGroup) => void;
|
||||
}
|
||||
|
||||
const DND_TYPE = "viewer-row";
|
||||
|
||||
const DraggableViewerRow = memo(function DraggableViewerRow({
|
||||
viewer,
|
||||
index,
|
||||
moveRow,
|
||||
onChange,
|
||||
onDelete,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
isLast,
|
||||
isFirst,
|
||||
}: any) {
|
||||
const ref = React.useRef<HTMLTableRowElement>(null);
|
||||
const [, drop] = useDrop({
|
||||
accept: DND_TYPE,
|
||||
hover(item: any, monitor) {
|
||||
if (!ref.current) return;
|
||||
|
||||
const dragIndex = item.index;
|
||||
const hoverIndex = index;
|
||||
if (dragIndex === hoverIndex) return;
|
||||
|
||||
const hoverBoundingRect = ref.current.getBoundingClientRect();
|
||||
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
if (!clientOffset) return;
|
||||
|
||||
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
||||
|
||||
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return;
|
||||
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return;
|
||||
|
||||
moveRow(dragIndex, hoverIndex);
|
||||
item.index = hoverIndex;
|
||||
},
|
||||
});
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: DND_TYPE,
|
||||
item: { index },
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
});
|
||||
drag(drop(ref));
|
||||
return (
|
||||
<FileViewerRow
|
||||
ref={ref}
|
||||
viewer={viewer}
|
||||
onChange={onChange}
|
||||
onDelete={onDelete}
|
||||
onMoveUp={onMoveUp}
|
||||
onMoveDown={onMoveDown}
|
||||
isLast={isLast}
|
||||
isFirst={isFirst}
|
||||
style={{ opacity: isDragging ? 0.5 : 1, cursor: "move" }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const ViewerGroupRow = memo(({ group, index, onDelete, onGroupChange }: ViewerGroupProps) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
|
||||
|
|
@ -56,6 +118,27 @@ const ViewerGroupRow = memo(({ group, index, onDelete, onGroupChange }: ViewerGr
|
|||
});
|
||||
}, [group.viewers]);
|
||||
|
||||
const [viewers, setViewers] = useState(group.viewers);
|
||||
React.useEffect(() => {
|
||||
setViewers(group.viewers);
|
||||
}, [group.viewers]);
|
||||
const moveRow = useCallback((from: number, to: number) => {
|
||||
if (from === to) return;
|
||||
const updated = [...viewers];
|
||||
const [moved] = updated.splice(from, 1);
|
||||
updated.splice(to, 0, moved);
|
||||
setViewers(updated);
|
||||
onGroupChange({ viewers: updated });
|
||||
}, [viewers, onGroupChange]);
|
||||
const handleMoveUp = (idx: number) => {
|
||||
if (idx <= 0) return;
|
||||
moveRow(idx, idx - 1);
|
||||
};
|
||||
const handleMoveDown = (idx: number) => {
|
||||
if (idx >= viewers.length - 1) return;
|
||||
moveRow(idx, idx + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledAccordion
|
||||
defaultExpanded={index == 0}
|
||||
|
|
@ -84,31 +167,40 @@ const ViewerGroupRow = memo(({ group, index, onDelete, onGroupChange }: ViewerGr
|
|||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ display: "block" }}>
|
||||
<TableContainer sx={{ mt: 1, maxHeight: 440 }}>
|
||||
<Table stickyHeader sx={{ width: "100%", tableLayout: "fixed" }} size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<NoWrapTableCell width={64}>{t("settings.icon")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={100}>{t("settings.viewerType")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={200}>{t("settings.displayName")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={250}>{t("settings.exts")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={100}>{t("settings.newFileAction")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={64}>{t("settings.viewerEnabled")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={100}></NoWrapTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{group.viewers.map((viewer, index) => (
|
||||
<FileViewerRow
|
||||
onChange={onViewerChange[index]}
|
||||
onDelete={onViewerDeleted[index]}
|
||||
viewer={viewer}
|
||||
key={viewer.id}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<TableContainer sx={{ mt: 1, maxHeight: 440 }}>
|
||||
<Table stickyHeader sx={{ width: "100%", tableLayout: "fixed" }} size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<NoWrapTableCell width={64}>{t("settings.icon")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={100}>{t("settings.viewerType")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={200}>{t("settings.displayName")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={250}>{t("settings.exts")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={100}>{t("settings.newFileAction")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={64}>{t("settings.viewerEnabled")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={64}>{t("settings.actions")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={64}></NoWrapTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{viewers.map((viewer, idx) => (
|
||||
<DraggableViewerRow
|
||||
key={viewer.id}
|
||||
viewer={viewer}
|
||||
index={idx}
|
||||
moveRow={moveRow}
|
||||
onChange={onViewerChange[idx]}
|
||||
onDelete={onViewerDeleted[idx]}
|
||||
onMoveUp={() => handleMoveUp(idx)}
|
||||
onMoveDown={() => handleMoveDown(idx)}
|
||||
isFirst={idx === 0}
|
||||
isLast={idx === viewers.length - 1}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</DndProvider>
|
||||
</AccordionDetails>
|
||||
</StyledAccordion>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,75 +8,111 @@ import { ViewerIcon } from "../../../../FileManager/Dialogs/OpenWith.tsx";
|
|||
import Dismiss from "../../../../Icons/Dismiss.tsx";
|
||||
import Edit from "../../../../Icons/Edit.tsx";
|
||||
import FileViewerEditDialog from "./FileViewerEditDialog.tsx";
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
|
||||
export interface FileViewerRowProps {
|
||||
viewer: Viewer;
|
||||
onChange: (viewer: Viewer) => void;
|
||||
onDelete: (e: React.MouseEvent<HTMLElement>) => void;
|
||||
onMoveUp?: () => void;
|
||||
onMoveDown?: () => void;
|
||||
isFirst?: boolean;
|
||||
isLast?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const FileViewerRow = memo(({ viewer, onChange, onDelete }: FileViewerRowProps) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const [extCached, setExtCached] = useState("");
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const onClose = useCallback(() => {
|
||||
setEditOpen(false);
|
||||
}, [setEditOpen]);
|
||||
return (
|
||||
<TableRow sx={{ "&:last-child td, &:last-child th": { border: 0 } }} hover>
|
||||
<FileViewerEditDialog viewer={viewer} onChange={onChange} open={editOpen} onClose={onClose} />
|
||||
<NoWrapCell>
|
||||
<ViewerIcon viewer={viewer} />
|
||||
</NoWrapCell>
|
||||
<NoWrapCell>{t(`settings.${viewer.type}ViewerType`)}</NoWrapCell>
|
||||
<NoWrapCell>
|
||||
{t(viewer.display_name, {
|
||||
ns: "application",
|
||||
})}
|
||||
</NoWrapCell>
|
||||
<NoWrapCell>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
multiline
|
||||
required
|
||||
value={extCached == "" ? viewer.exts.join() : extCached}
|
||||
onBlur={() => {
|
||||
onChange({
|
||||
...viewer,
|
||||
exts: extCached == "" ? viewer.exts : extCached?.split(",")?.map((ext) => ext.trim()),
|
||||
});
|
||||
setExtCached("");
|
||||
}}
|
||||
onChange={(e) => setExtCached(e.target.value)}
|
||||
/>
|
||||
</NoWrapCell>
|
||||
<NoWrapCell>
|
||||
{viewer.templates?.length ? t("settings.nMapping", { num: viewer.templates?.length }) : t("share.none")}
|
||||
</NoWrapCell>
|
||||
<NoWrapCell>
|
||||
<StyledCheckbox
|
||||
size={"small"}
|
||||
checked={!viewer.disabled}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...viewer,
|
||||
disabled: !e.target.checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</NoWrapCell>
|
||||
<NoWrapCell>
|
||||
<IconButton size={"small"} onClick={() => setEditOpen(true)}>
|
||||
<Edit fontSize={"small"} />
|
||||
</IconButton>
|
||||
{viewer.type != ViewerType.builtin && (
|
||||
<IconButton size={"small"} onClick={onDelete}>
|
||||
<Dismiss fontSize={"small"} />
|
||||
</IconButton>
|
||||
)}
|
||||
</NoWrapCell>
|
||||
</TableRow>
|
||||
);
|
||||
});
|
||||
const FileViewerRow = React.memo(
|
||||
React.forwardRef<HTMLTableRowElement, FileViewerRowProps>(
|
||||
(
|
||||
{
|
||||
viewer,
|
||||
onChange,
|
||||
onDelete,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
isFirst,
|
||||
isLast,
|
||||
style,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const [extCached, setExtCached] = useState("");
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const onClose = useCallback(() => {
|
||||
setEditOpen(false);
|
||||
}, [setEditOpen]);
|
||||
return (
|
||||
<TableRow
|
||||
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
||||
hover
|
||||
ref={ref}
|
||||
style={style}
|
||||
>
|
||||
<FileViewerEditDialog viewer={viewer} onChange={onChange} open={editOpen} onClose={onClose} />
|
||||
<NoWrapCell>
|
||||
<ViewerIcon viewer={viewer} />
|
||||
</NoWrapCell>
|
||||
<NoWrapCell>{t(`settings.${viewer.type}ViewerType`)}</NoWrapCell>
|
||||
<NoWrapCell>
|
||||
{t(viewer.display_name, {
|
||||
ns: "application",
|
||||
})}
|
||||
</NoWrapCell>
|
||||
<NoWrapCell>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
multiline
|
||||
required
|
||||
value={extCached == "" ? viewer.exts.join() : extCached}
|
||||
onBlur={() => {
|
||||
onChange({
|
||||
...viewer,
|
||||
exts: extCached == "" ? viewer.exts : extCached?.split(",")?.map((ext) => ext.trim()),
|
||||
});
|
||||
setExtCached("");
|
||||
}}
|
||||
onChange={(e) => setExtCached(e.target.value)}
|
||||
/>
|
||||
</NoWrapCell>
|
||||
<NoWrapCell>
|
||||
{viewer.templates?.length ? t("settings.nMapping", { num: viewer.templates?.length }) : t("share.none")}
|
||||
</NoWrapCell>
|
||||
<NoWrapCell>
|
||||
<StyledCheckbox
|
||||
size={"small"}
|
||||
checked={!viewer.disabled}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...viewer,
|
||||
disabled: !e.target.checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</NoWrapCell>
|
||||
<NoWrapCell>
|
||||
<IconButton size={"small"} onClick={() => setEditOpen(true)}>
|
||||
<Edit fontSize={"small"} />
|
||||
</IconButton>
|
||||
{viewer.type != ViewerType.builtin && (
|
||||
<IconButton size={"small"} onClick={onDelete}>
|
||||
<Dismiss fontSize={"small"} />
|
||||
</IconButton>
|
||||
)}
|
||||
</NoWrapCell>
|
||||
<NoWrapCell>
|
||||
<IconButton size="small" onClick={onMoveUp} disabled={isFirst}>
|
||||
<KeyboardArrowUpIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton size="small" onClick={onMoveDown} disabled={isLast}>
|
||||
<KeyboardArrowDownIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</NoWrapCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default FileViewerRow;
|
||||
|
|
|
|||
|
|
@ -19,11 +19,90 @@ import AutoHeight from "../../../Common/AutoHeight.tsx";
|
|||
import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar.tsx";
|
||||
import { StyledTableContainerPaper } from "../../../Common/StyledComponents.tsx";
|
||||
import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx";
|
||||
import ArrowDown from "../../../Icons/ArrowDown.tsx";
|
||||
import Dismiss from "../../../Icons/Dismiss.tsx";
|
||||
import { FileManagerIndex } from "../../FileManager.tsx";
|
||||
import AddColumn from "./AddColumn.tsx";
|
||||
import { getColumnTypeDefaults, ListViewColumnSetting } from "./Column.tsx";
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import { DndProvider, useDrag, useDrop } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import React from "react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
const DND_TYPE = "column-row";
|
||||
|
||||
interface DraggableColumnRowProps {
|
||||
column: ListViewColumnSetting;
|
||||
index: number;
|
||||
moveRow: (from: number, to: number) => void;
|
||||
columns: ListViewColumnSetting[];
|
||||
setColumns: Dispatch<SetStateAction<ListViewColumnSetting[]>>;
|
||||
t: (key: string) => string;
|
||||
onDelete: (idx: number) => void;
|
||||
isFirst: boolean;
|
||||
isLast: boolean;
|
||||
}
|
||||
|
||||
const DraggableColumnRow: React.FC<DraggableColumnRowProps> = ({ column, index, moveRow, columns, t, onDelete, isFirst, isLast }) => {
|
||||
const ref = React.useRef<HTMLTableRowElement>(null);
|
||||
const [, drop] = useDrop({
|
||||
accept: DND_TYPE,
|
||||
hover(item: any, monitor) {
|
||||
if (!ref.current) return;
|
||||
|
||||
const dragIndex = item.index;
|
||||
const hoverIndex = index;
|
||||
if (dragIndex === hoverIndex) return;
|
||||
|
||||
const hoverBoundingRect = ref.current.getBoundingClientRect();
|
||||
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
if (!clientOffset) return;
|
||||
|
||||
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
||||
|
||||
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return;
|
||||
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return;
|
||||
|
||||
moveRow(dragIndex, hoverIndex);
|
||||
item.index = hoverIndex;
|
||||
},
|
||||
});
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: DND_TYPE,
|
||||
item: { index },
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
});
|
||||
drag(drop(ref));
|
||||
return (
|
||||
<TableRow
|
||||
ref={ref}
|
||||
hover
|
||||
key={index}
|
||||
sx={{ "&:last-child td, &:last-child th": { border: 0 }, opacity: isDragging ? 0.5 : 1, cursor: "move" }}
|
||||
>
|
||||
<TableCell component="th" scope="row">
|
||||
{t(getColumnTypeDefaults(column).title)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<IconButton size="small" onClick={() => moveRow(index, index - 1)} disabled={isFirst}>
|
||||
<KeyboardArrowUpIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton size="small" onClick={() => moveRow(index, index + 1)} disabled={isLast}>
|
||||
<KeyboardArrowDownIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton size="small" onClick={() => onDelete(index)} disabled={columns.length <= 1}>
|
||||
<Dismiss sx={{ width: "18px", height: "18px" }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
const ColumnSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -82,99 +161,42 @@ const ColumnSetting = () => {
|
|||
>
|
||||
<DialogContent sx={{ pb: 0 }}>
|
||||
<AutoHeight>
|
||||
<TableContainer component={StyledTableContainerPaper}>
|
||||
<Table sx={{ width: "100%" }} size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell width={"50%"}>{t("fileManager.column")}</TableCell>
|
||||
<TableCell>{t("fileManager.actions")}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{columns.map((column, index) => (
|
||||
<TableRow hover key={index} sx={{ "&:last-child td, &:last-child th": { border: 0 } }}>
|
||||
<TableCell component="th" scope="row">
|
||||
{t(getColumnTypeDefaults(column).title)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
{index > 0 && columns.length > 1 ? (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setColumns((prev) => {
|
||||
const newColumns = [...prev];
|
||||
const temp = newColumns[index];
|
||||
newColumns[index] = newColumns[index - 1];
|
||||
newColumns[index - 1] = temp;
|
||||
return newColumns;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ArrowDown
|
||||
sx={{
|
||||
width: "18px",
|
||||
height: "18px",
|
||||
transform: "rotate(180deg)",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
) : (
|
||||
<Box sx={{ width: 28, height: 28 }} />
|
||||
)}
|
||||
{index < columns.length - 1 && columns.length > 1 ? (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setColumns((prev) => {
|
||||
const newColumns = [...prev];
|
||||
const temp = newColumns[index];
|
||||
newColumns[index] = newColumns[index + 1];
|
||||
newColumns[index + 1] = temp;
|
||||
return newColumns;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ArrowDown
|
||||
sx={{
|
||||
width: "18px",
|
||||
height: "18px",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
) : (
|
||||
<Box sx={{ width: 28, height: 28 }} />
|
||||
)}
|
||||
{columns.length > 1 ? (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setColumns((prev) => {
|
||||
return prev.filter((_, i) => i !== index);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Dismiss
|
||||
sx={{
|
||||
width: "18px",
|
||||
height: "18px",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
) : (
|
||||
<Box sx={{ width: 28, height: 28 }} />
|
||||
)}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<TableContainer component={StyledTableContainerPaper}>
|
||||
<Table sx={{ width: "100%" }} size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell width={"50%"}>{t("fileManager.column")}</TableCell>
|
||||
<TableCell>{t("fileManager.actions")}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{columns.map((column, index) => (
|
||||
<DraggableColumnRow
|
||||
key={index}
|
||||
column={column}
|
||||
index={index}
|
||||
moveRow={(from, to) => {
|
||||
if (from === to || to < 0 || to >= columns.length) return;
|
||||
setColumns((prev) => {
|
||||
const arr = [...prev];
|
||||
const [moved] = arr.splice(from, 1);
|
||||
arr.splice(to, 0, moved);
|
||||
return arr;
|
||||
});
|
||||
}}
|
||||
columns={columns}
|
||||
setColumns={setColumns}
|
||||
t={t}
|
||||
onDelete={(idx) => setColumns((prev) => prev.filter((_, i) => i !== idx))}
|
||||
isFirst={index === 0}
|
||||
isLast={index === columns.length - 1}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</DndProvider>
|
||||
</AutoHeight>
|
||||
</DialogContent>
|
||||
</DraggableDialog>
|
||||
|
|
|
|||
Loading…
Reference in New Issue