first commit
This commit is contained in:
120
src/component/Dialogs/AggregatedErrorDetail.tsx
Executable file
120
src/component/Dialogs/AggregatedErrorDetail.tsx
Executable file
@@ -0,0 +1,120 @@
|
||||
import { useAppDispatch, useAppSelector } from "../../redux/hooks.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DraggableDialog, { StyledDialogContentText } from "./DraggableDialog.tsx";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { closeAggregatedErrorDialog } from "../../redux/globalStateSlice.ts";
|
||||
import {
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
Stack,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@mui/material";
|
||||
import { AppError } from "../../api/request.ts";
|
||||
import { FileResponse, FileType } from "../../api/explorer.ts";
|
||||
import FileBadge from "../FileManager/FileBadge.tsx";
|
||||
import { StyledTableContainerPaper } from "../Common/StyledComponents.tsx";
|
||||
import { CrUriPrefix } from "../../util/uri.ts";
|
||||
|
||||
interface ErrorTableProps {
|
||||
errors: {
|
||||
[key: string]: AppError;
|
||||
};
|
||||
files: {
|
||||
[key: string]: FileResponse;
|
||||
};
|
||||
}
|
||||
|
||||
const ErrorTable = (props: ErrorTableProps) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<TableContainer component={StyledTableContainerPaper}>
|
||||
<Table sx={{ width: "100%" }} size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>{t("common:object")}</TableCell>
|
||||
<TableCell>{t("common:error")}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{Object.keys(props.errors).map((id) => (
|
||||
<TableRow hover key={id} sx={{ "&:last-child td, &:last-child th": { border: 0 } }}>
|
||||
<TableCell component="th" scope="row">
|
||||
{props.files[id] && <FileBadge sx={{ maxWidth: "250px" }} file={props.files[id]} />}
|
||||
{!props.files[id] && !id.startsWith(CrUriPrefix) && id}
|
||||
{!props.files[id] && id.startsWith(CrUriPrefix) && (
|
||||
<FileBadge
|
||||
sx={{ maxWidth: "250px" }}
|
||||
simplifiedFile={{
|
||||
type: FileType.file,
|
||||
path: id,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{props.errors[id].message}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const AggregatedErrorDetail = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const open = useAppSelector((state) => state.globalState.aggregatedErrorDialogOpen);
|
||||
const files = useAppSelector((state) => state.globalState.aggregatedErrorFile);
|
||||
const error = useAppSelector((state) => state.globalState.aggregatedError);
|
||||
const onClose = useCallback(() => {
|
||||
dispatch(closeAggregatedErrorDialog());
|
||||
}, [dispatch]);
|
||||
|
||||
const [rootError, errors] = useMemo(() => {
|
||||
if (!error) {
|
||||
return [undefined, undefined];
|
||||
}
|
||||
|
||||
let rootError = new AppError(error);
|
||||
const errors: { [key: string]: AppError } = {};
|
||||
Object.keys(error.aggregated_error ?? {}).forEach((key) => {
|
||||
const inner = error.aggregated_error?.[key];
|
||||
if (inner) {
|
||||
errors[key] = new AppError(inner);
|
||||
}
|
||||
});
|
||||
return [rootError, errors] as const;
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<DraggableDialog
|
||||
title={t("application:modals.errorDetailsTitle")}
|
||||
dialogProps={{
|
||||
open: open ?? false,
|
||||
onClose: onClose,
|
||||
fullWidth: true,
|
||||
maxWidth: "sm",
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<Stack spacing={2}>
|
||||
<StyledDialogContentText>{rootError && rootError.message}</StyledDialogContentText>
|
||||
{files && errors && <ErrorTable errors={errors} files={files} />}
|
||||
{rootError && rootError.cid && (
|
||||
<DialogContentText variant={"caption"}>
|
||||
<code>{t("common:requestID", { id: rootError.cid })}</code>
|
||||
</DialogContentText>
|
||||
)}
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</DraggableDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AggregatedErrorDetail;
|
||||
66
src/component/Dialogs/BatchDownloadLog.tsx
Executable file
66
src/component/Dialogs/BatchDownloadLog.tsx
Executable file
@@ -0,0 +1,66 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, DialogContent } from "@mui/material";
|
||||
import { useAppDispatch, useAppSelector } from "../../redux/hooks.ts";
|
||||
import React, { useCallback, useRef } from "react";
|
||||
import DraggableDialog from "./DraggableDialog.tsx";
|
||||
import { closeBatchDownloadLogDialog } from "../../redux/globalStateSlice.ts";
|
||||
import { FilledTextField } from "../Common/StyledComponents.tsx";
|
||||
import { cancelSignals } from "../../redux/thunks/download.ts";
|
||||
|
||||
const BatchDownloadLog = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const logRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const open = useAppSelector((state) => state.globalState.batchDownloadLogDialogOpen);
|
||||
const downloadId = useAppSelector((state) => state.globalState.batchDownloadLogDialogId);
|
||||
const logs = useAppSelector((state) => state.globalState.batchDownloadLogDialogLogs?.[downloadId ?? ""]);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
dispatch(closeBatchDownloadLogDialog());
|
||||
}, [dispatch]);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
cancelSignals[downloadId ?? ""]?.abort();
|
||||
dispatch(closeBatchDownloadLogDialog());
|
||||
}, [downloadId, dispatch]);
|
||||
|
||||
return (
|
||||
<DraggableDialog
|
||||
title={t("modals.directoryDownloadTitle")}
|
||||
secondaryAction={
|
||||
<Button onClick={onCancel} color={"error"}>
|
||||
{t("modals.cancelDownload")}
|
||||
</Button>
|
||||
}
|
||||
showActions
|
||||
hideOk
|
||||
dialogProps={{
|
||||
open: open ?? false,
|
||||
onClose: onClose,
|
||||
fullWidth: true,
|
||||
}}
|
||||
>
|
||||
<DialogContent sx={{ pb: 0 }}>
|
||||
<FilledTextField
|
||||
inputProps={{
|
||||
ref: logRef,
|
||||
sx: {
|
||||
// @ts-ignore
|
||||
fontSize: (theme) => theme.typography.body2.fontSize,
|
||||
},
|
||||
}}
|
||||
sx={{ pt: 0.5 }}
|
||||
minRows={10}
|
||||
maxRows={10}
|
||||
variant="outlined"
|
||||
value={logs}
|
||||
multiline
|
||||
fullWidth
|
||||
id="standard-basic"
|
||||
/>
|
||||
</DialogContent>
|
||||
</DraggableDialog>
|
||||
);
|
||||
};
|
||||
export default BatchDownloadLog;
|
||||
50
src/component/Dialogs/Confirmation.tsx
Executable file
50
src/component/Dialogs/Confirmation.tsx
Executable file
@@ -0,0 +1,50 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DialogContent, Stack } from "@mui/material";
|
||||
import { useAppDispatch, useAppSelector } from "../../redux/hooks.ts";
|
||||
import { useCallback } from "react";
|
||||
import DraggableDialog, { StyledDialogContentText } from "./DraggableDialog.tsx";
|
||||
import { generalDialogPromisePool } from "../../redux/thunks/dialog.ts";
|
||||
import { closeConfirmDialog } from "../../redux/globalStateSlice.ts";
|
||||
|
||||
const Confirmation = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const open = useAppSelector((state) => state.globalState.confirmDialogOpen);
|
||||
const message = useAppSelector((state) => state.globalState.confirmDialogMessage);
|
||||
const promiseId = useAppSelector((state) => state.globalState.confirmPromiseId);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
dispatch(closeConfirmDialog());
|
||||
if (promiseId) {
|
||||
generalDialogPromisePool[promiseId]?.reject("cancel");
|
||||
}
|
||||
}, [dispatch, promiseId]);
|
||||
|
||||
const onAccept = useCallback(() => {
|
||||
dispatch(closeConfirmDialog());
|
||||
if (promiseId) {
|
||||
generalDialogPromisePool[promiseId]?.resolve();
|
||||
}
|
||||
}, [promiseId]);
|
||||
|
||||
return (
|
||||
<DraggableDialog
|
||||
title={t("common:areYouSure")}
|
||||
showActions
|
||||
showCancel
|
||||
onAccept={onAccept}
|
||||
dialogProps={{
|
||||
open: open ?? false,
|
||||
onClose: onClose,
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<Stack spacing={2}>
|
||||
<StyledDialogContentText>{message}</StyledDialogContentText>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</DraggableDialog>
|
||||
);
|
||||
};
|
||||
export default Confirmation;
|
||||
77
src/component/Dialogs/DialogAccordion.tsx
Executable file
77
src/component/Dialogs/DialogAccordion.tsx
Executable file
@@ -0,0 +1,77 @@
|
||||
import { AccordionDetailsProps, Box, styled } from "@mui/material";
|
||||
import MuiAccordion, { AccordionProps } from "@mui/material/Accordion";
|
||||
import MuiAccordionSummary, { AccordionSummaryProps } from "@mui/material/AccordionSummary";
|
||||
import MuiAccordionDetails from "@mui/material/AccordionDetails";
|
||||
import { useState } from "react";
|
||||
import { CaretDownIcon } from "../FileManager/TreeView/TreeFile.tsx";
|
||||
import { DefaultButton } from "../Common/StyledComponents.tsx";
|
||||
|
||||
const Accordion = styled((props: AccordionProps) => <MuiAccordion disableGutters elevation={0} square {...props} />)(
|
||||
({ theme, expanded }) => ({
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: expanded
|
||||
? theme.palette.mode == "light"
|
||||
? "rgba(0, 0, 0, 0.06)"
|
||||
: "rgba(255, 255, 255, 0.09)"
|
||||
: "initial",
|
||||
"&:not(:last-child)": {
|
||||
borderBottom: 0,
|
||||
},
|
||||
"&::before": {
|
||||
display: "none",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const AccordionSummary = styled((props: AccordionSummaryProps) => <MuiAccordionSummary {...props} />)(() => ({
|
||||
flexDirection: "row-reverse",
|
||||
minHeight: 0,
|
||||
padding: 0,
|
||||
"& .MuiAccordionSummary-content": {
|
||||
margin: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({
|
||||
padding: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const SummaryButton = styled(DefaultButton)<{ expanded: boolean }>(({ theme, expanded }) => ({
|
||||
justifyContent: "flex-start",
|
||||
backgroundColor: expanded
|
||||
? "initial"
|
||||
: theme.palette.mode == "light"
|
||||
? "rgba(0, 0, 0, 0.06)"
|
||||
: "rgba(255, 255, 255, 0.09)",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.mode == "light" ? "rgba(0, 0, 0, 0.09)" : "rgba(255, 255, 255, 0.13)",
|
||||
},
|
||||
}));
|
||||
|
||||
export interface DialogAccordionProps {
|
||||
children?: React.ReactNode;
|
||||
defaultExpanded?: boolean;
|
||||
title: string;
|
||||
accordionDetailProps?: AccordionDetailsProps;
|
||||
}
|
||||
|
||||
const DialogAccordion = (props: DialogAccordionProps) => {
|
||||
const [expanded, setExpanded] = useState(!!props.defaultExpanded);
|
||||
const handleChange = (_event: React.SyntheticEvent, newExpanded: boolean) => {
|
||||
setExpanded(newExpanded);
|
||||
};
|
||||
return (
|
||||
<Box>
|
||||
<Accordion expanded={expanded} onChange={handleChange}>
|
||||
<AccordionSummary aria-controls="panel1d-content" id="panel1d-header">
|
||||
<SummaryButton expanded={expanded} fullWidth startIcon={<CaretDownIcon expanded={expanded} />}>
|
||||
{props.title}
|
||||
</SummaryButton>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails {...props.accordionDetailProps}>{props.children}</AccordionDetails>
|
||||
</Accordion>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DialogAccordion;
|
||||
117
src/component/Dialogs/DraggableDialog.tsx
Executable file
117
src/component/Dialogs/DraggableDialog.tsx
Executable file
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContentText,
|
||||
DialogProps,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
Paper,
|
||||
PaperProps,
|
||||
Stack,
|
||||
styled,
|
||||
useMediaQuery,
|
||||
} from "@mui/material";
|
||||
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import { useCallback } from "react";
|
||||
import Draggable from "react-draggable";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Dismiss from "../Icons/Dismiss.tsx";
|
||||
|
||||
function PaperComponent(props: PaperProps) {
|
||||
return (
|
||||
<Draggable handle="#draggable-dialog-title" cancel={'[class*="MuiDialogContent-root"]'}>
|
||||
<Paper {...props} />
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
export const StyledDialogActions = styled(DialogActions)<{
|
||||
denseAction?: boolean;
|
||||
}>(({ theme, denseAction }) => ({
|
||||
padding: `${theme.spacing(denseAction ? 0.5 : 2)} ${theme.spacing(3)}`,
|
||||
justifyContent: "space-between",
|
||||
}));
|
||||
|
||||
export const StyledDialogContentText = styled(DialogContentText)(({ theme }) => ({
|
||||
fontSize: theme.typography.body2.fontSize,
|
||||
wordBreak: "break-all",
|
||||
}));
|
||||
|
||||
export const StyledDialogTitle = styled(DialogTitle)<{ moveable?: boolean }>(({ moveable }) => ({
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
cursor: moveable ? "move" : "initial",
|
||||
}));
|
||||
|
||||
export interface DraggableDialogProps {
|
||||
dialogProps: DialogProps;
|
||||
children?: React.ReactNode;
|
||||
secondaryAction?: React.ReactNode;
|
||||
showActions?: boolean;
|
||||
showCancel?: boolean;
|
||||
hideOk?: boolean;
|
||||
okText?: string;
|
||||
cancelText?: string;
|
||||
title?: string | React.ReactNode;
|
||||
onAccept?: () => void;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
denseAction?: boolean;
|
||||
secondaryFullWidth?: boolean;
|
||||
}
|
||||
|
||||
const DraggableDialog = (props: DraggableDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
const isTouch = useMediaQuery("(pointer: coarse)");
|
||||
const onClose = useCallback(() => {
|
||||
props.dialogProps.onClose && props.dialogProps.onClose({}, "backdropClick");
|
||||
}, [props.dialogProps.onClose]);
|
||||
return (
|
||||
<Dialog
|
||||
PaperComponent={isTouch ? undefined : PaperComponent}
|
||||
{...props.dialogProps}
|
||||
onClose={props.loading ? undefined : props.dialogProps.onClose}
|
||||
>
|
||||
{props.title != undefined && (
|
||||
<Box>
|
||||
<StyledDialogTitle moveable id="draggable-dialog-title">
|
||||
<Box>{props.title}</Box>
|
||||
<IconButton disabled={props.loading} onClick={onClose}>
|
||||
<Dismiss fontSize={"small"} />
|
||||
</IconButton>
|
||||
</StyledDialogTitle>
|
||||
</Box>
|
||||
)}
|
||||
{props.children}
|
||||
{props.showActions && (
|
||||
<StyledDialogActions denseAction={props.denseAction}>
|
||||
<Box sx={{ flexGrow: props.secondaryFullWidth ? 1 : "unset" }}>{props.secondaryAction}</Box>
|
||||
<Stack direction={"row"} spacing={1}>
|
||||
{props.showCancel && (
|
||||
<Button disabled={props.loading} onClick={onClose}>
|
||||
{props.cancelText ?? t("common:cancel")}
|
||||
</Button>
|
||||
)}
|
||||
{!props.hideOk && (
|
||||
<LoadingButton
|
||||
disabled={props.disabled}
|
||||
loading={props.loading}
|
||||
variant={"contained"}
|
||||
onClick={props.onAccept}
|
||||
color="primary"
|
||||
>
|
||||
<span>{props.okText ?? t("common:ok")}</span>
|
||||
</LoadingButton>
|
||||
)}
|
||||
</Stack>
|
||||
</StyledDialogActions>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DraggableDialog;
|
||||
20
src/component/Dialogs/GlobalDialogs.tsx
Executable file
20
src/component/Dialogs/GlobalDialogs.tsx
Executable file
@@ -0,0 +1,20 @@
|
||||
import { useAppSelector } from "../../redux/hooks.ts";
|
||||
import PinToSidebar from "../FileManager/Dialogs/PinToSidebar.tsx";
|
||||
import BatchDownloadLog from "./BatchDownloadLog.tsx";
|
||||
import Confirmation from "./Confirmation.tsx";
|
||||
import SelectOption from "./SelectOption.tsx";
|
||||
|
||||
const GlobalDialogs = () => {
|
||||
const selectOptionOpen = useAppSelector((state) => state.globalState.selectOptionDialogOpen);
|
||||
const batchDownloadLogOpen = useAppSelector((state) => state.globalState.batchDownloadLogDialogOpen);
|
||||
return (
|
||||
<>
|
||||
<Confirmation />
|
||||
<PinToSidebar />
|
||||
{batchDownloadLogOpen != undefined && <BatchDownloadLog />}
|
||||
{selectOptionOpen != undefined && <SelectOption />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalDialogs;
|
||||
67
src/component/Dialogs/SelectOption.tsx
Executable file
67
src/component/Dialogs/SelectOption.tsx
Executable file
@@ -0,0 +1,67 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DialogContent, List, ListItemButton, ListItemText } from "@mui/material";
|
||||
import { useAppDispatch, useAppSelector } from "../../redux/hooks.ts";
|
||||
import React, { useCallback } from "react";
|
||||
import DraggableDialog from "./DraggableDialog.tsx";
|
||||
import { selectOptionDialogPromisePool } from "../../redux/thunks/dialog.ts";
|
||||
import { closeSelectOptionDialog } from "../../redux/globalStateSlice.ts";
|
||||
|
||||
const SelectOption = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const open = useAppSelector((state) => state.globalState.selectOptionDialogOpen);
|
||||
const title = useAppSelector((state) => state.globalState.selectOptionTitle);
|
||||
const promiseId = useAppSelector((state) => state.globalState.selectOptionPromiseId);
|
||||
const options = useAppSelector((state) => state.globalState.selectOptionDialogOptions);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
dispatch(closeSelectOptionDialog());
|
||||
if (promiseId) {
|
||||
selectOptionDialogPromisePool[promiseId]?.reject("cancel");
|
||||
}
|
||||
}, [dispatch, promiseId]);
|
||||
|
||||
const onAccept = useCallback(
|
||||
(v: any) => {
|
||||
dispatch(closeSelectOptionDialog());
|
||||
if (promiseId) {
|
||||
selectOptionDialogPromisePool[promiseId]?.resolve(v);
|
||||
}
|
||||
},
|
||||
[promiseId],
|
||||
);
|
||||
|
||||
return (
|
||||
<DraggableDialog
|
||||
title={t(title ?? "")}
|
||||
dialogProps={{
|
||||
open: open ?? false,
|
||||
onClose: onClose,
|
||||
maxWidth: "sm",
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<List component="nav">
|
||||
{options?.map((o) => (
|
||||
<ListItemButton key={o.value} onClick={() => onAccept(o.value)}>
|
||||
<ListItemText
|
||||
primary={o.name}
|
||||
secondary={o.description}
|
||||
slotProps={{
|
||||
primary: {
|
||||
variant: "body2",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
|
||||
secondary: { variant: "body2" },
|
||||
}}
|
||||
/>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
</DialogContent>
|
||||
</DraggableDialog>
|
||||
);
|
||||
};
|
||||
export default SelectOption;
|
||||
Reference in New Issue
Block a user