first commit

This commit is contained in:
2025-10-19 13:31:11 +00:00
commit 8bfc183b66
1248 changed files with 195992 additions and 0 deletions

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;