first commit
This commit is contained in:
41
src/component/Admin/Common/AdminCard.tsx
Executable file
41
src/component/Admin/Common/AdminCard.tsx
Executable file
@@ -0,0 +1,41 @@
|
||||
import { Box, styled } from "@mui/material";
|
||||
|
||||
export const BorderedCard = styled(Box)(({ theme }) => ({
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
padding: theme.spacing(2),
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
}));
|
||||
|
||||
export const BorderedCardClickable = styled(BorderedCard)(({ theme }) => ({
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
transition: "background-color 0.3s ease",
|
||||
}));
|
||||
|
||||
export const BorderedCardClickableBaImg = styled(BorderedCardClickable)<{ img?: string }>(({ theme, img }) => ({
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
"&::before": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: "-20px",
|
||||
right: "-20px",
|
||||
width: "150px",
|
||||
height: "150px",
|
||||
backgroundImage: `url(${img})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
transform: "rotate(-10deg)",
|
||||
opacity: 0.1,
|
||||
maskImage: "radial-gradient(circle at center, black 30%, transparent 80%)",
|
||||
pointerEvents: "none",
|
||||
zIndex: 0,
|
||||
},
|
||||
"& > *": {
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
},
|
||||
}));
|
||||
44
src/component/Admin/Common/EndpointInput.tsx
Executable file
44
src/component/Admin/Common/EndpointInput.tsx
Executable file
@@ -0,0 +1,44 @@
|
||||
import { TextFieldProps } from "@mui/material";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DenseFilledTextField } from "../../Common/StyledComponents";
|
||||
|
||||
export interface EndpointInputProps extends TextFieldProps<"outlined"> {
|
||||
enforceProtocol?: boolean;
|
||||
enforcePrefix?: boolean;
|
||||
}
|
||||
|
||||
export const EndpointInput = (props: EndpointInputProps) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const { enforceProtocol, enforcePrefix = true, onChange, ...rest } = props;
|
||||
const [value, setValue] = useState<string>((props.value as string) ?? "");
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(e.target.value);
|
||||
onChange?.(e);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const showError = useMemo(() => {
|
||||
if (!enforceProtocol) return false;
|
||||
return value.startsWith("http://") && window.location.protocol == "https:";
|
||||
}, [enforceProtocol, value]);
|
||||
|
||||
return (
|
||||
<DenseFilledTextField
|
||||
onChange={handleChange}
|
||||
value={props.value}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
// start with http:// or https://
|
||||
pattern: enforcePrefix ? `^https?://.*$` : undefined,
|
||||
title: enforcePrefix ? t("settings.startWithProtocol") : undefined,
|
||||
},
|
||||
}}
|
||||
error={showError}
|
||||
helperText={showError ? t("settings.tlsWarning") : undefined}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
94
src/component/Admin/Common/GroupSelectionInput.tsx
Executable file
94
src/component/Admin/Common/GroupSelectionInput.tsx
Executable file
@@ -0,0 +1,94 @@
|
||||
import { ListItemText } from "@mui/material";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getGroupList } from "../../../api/api";
|
||||
import { GroupEnt } from "../../../api/dashboard";
|
||||
import { useAppDispatch } from "../../../redux/hooks";
|
||||
import { DenseSelect } from "../../Common/StyledComponents";
|
||||
import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu";
|
||||
|
||||
export interface GroupSelectionInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onChangeGroup?: (group?: GroupEnt) => void;
|
||||
emptyValue?: string;
|
||||
emptyText?: string;
|
||||
fullWidth?: boolean;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
const AnonymousGroupId = 3;
|
||||
|
||||
const GroupSelectionInput = ({
|
||||
value,
|
||||
onChange,
|
||||
onChangeGroup,
|
||||
emptyValue,
|
||||
emptyText,
|
||||
fullWidth,
|
||||
required,
|
||||
}: GroupSelectionInputProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [groups, setGroups] = useState<GroupEnt[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
dispatch(
|
||||
getGroupList({
|
||||
page_size: 1000,
|
||||
page: 1,
|
||||
order_by: "id",
|
||||
order_direction: "asc",
|
||||
}),
|
||||
)
|
||||
.then((res) => {
|
||||
setGroups(res.groups);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
onChange(value);
|
||||
onChangeGroup?.(groups.find((g) => g.id === parseInt(value)));
|
||||
};
|
||||
|
||||
return (
|
||||
<FormControl fullWidth={fullWidth}>
|
||||
<DenseSelect
|
||||
disabled={loading}
|
||||
value={value}
|
||||
onChange={(e) => handleChange(e.target.value as string)}
|
||||
required={required}
|
||||
>
|
||||
{groups
|
||||
.filter((g) => g.id != AnonymousGroupId)
|
||||
.map((g) => (
|
||||
<SquareMenuItem value={g.id.toString()}>
|
||||
<ListItemText
|
||||
slotProps={{
|
||||
primary: { variant: "body2" },
|
||||
}}
|
||||
>
|
||||
{g.name}
|
||||
</ListItemText>
|
||||
</SquareMenuItem>
|
||||
))}
|
||||
{emptyValue !== undefined && emptyText && (
|
||||
<SquareMenuItem value={emptyValue}>
|
||||
<ListItemText
|
||||
primary={<em>{emptyText}</em>}
|
||||
slotProps={{
|
||||
primary: { variant: "body2" },
|
||||
}}
|
||||
/>
|
||||
</SquareMenuItem>
|
||||
)}
|
||||
</DenseSelect>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupSelectionInput;
|
||||
60
src/component/Admin/Common/MagicVarDialog.tsx
Executable file
60
src/component/Admin/Common/MagicVarDialog.tsx
Executable file
@@ -0,0 +1,60 @@
|
||||
import { DialogContent, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { NoWrapTableCell } from "../../Common/StyledComponents.tsx";
|
||||
import DraggableDialog from "../../Dialogs/DraggableDialog";
|
||||
|
||||
export interface MagicVar {
|
||||
name: string;
|
||||
value: string;
|
||||
example?: string;
|
||||
}
|
||||
|
||||
export interface MagicVarDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
vars: MagicVar[];
|
||||
}
|
||||
|
||||
const MagicVarDialog = ({ open, onClose, vars }: MagicVarDialogProps) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
|
||||
return (
|
||||
<DraggableDialog
|
||||
title={t("policy.magicVar.variable")}
|
||||
onAccept={onClose}
|
||||
dialogProps={{
|
||||
fullWidth: true,
|
||||
maxWidth: "md",
|
||||
open,
|
||||
onClose,
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<TableContainer sx={{ mt: 1, maxHeight: 440 }}>
|
||||
<Table stickyHeader sx={{ width: "100%", tableLayout: "fixed" }} size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<NoWrapTableCell width={350}>{t("policy.magicVar.variable")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={200}>{t("policy.magicVar.description")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={250}>{t("policy.magicVar.example")}</NoWrapTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{vars.map((v, i) => (
|
||||
<TableRow key={i} sx={{ "&:last-child td, &:last-child th": { border: 0 } }} hover>
|
||||
<TableCell>
|
||||
<code>{v.name}</code>
|
||||
</TableCell>
|
||||
<TableCell>{t(v.value)}</TableCell>
|
||||
<TableCell sx={{ wordBreak: "break-all" }}>{v.example}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</DialogContent>
|
||||
</DraggableDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default MagicVarDialog;
|
||||
89
src/component/Admin/Common/NodeSelectionInput.tsx
Executable file
89
src/component/Admin/Common/NodeSelectionInput.tsx
Executable file
@@ -0,0 +1,89 @@
|
||||
import { Alert, Box, OutlinedSelectProps, Typography } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getNodeList } from "../../../api/api";
|
||||
import { Node, NodeStatus, NodeType } from "../../../api/dashboard";
|
||||
import { useAppDispatch } from "../../../redux/hooks";
|
||||
import { DenseSelect } from "../../Common/StyledComponents";
|
||||
import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu";
|
||||
|
||||
export interface NodeSelectionInputProps extends OutlinedSelectProps {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
}
|
||||
|
||||
export const NodeStatusCondition = "node_status";
|
||||
|
||||
const NodeSelectionInput = ({ value, onChange, ...rest }: NodeSelectionInputProps) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const dispatch = useAppDispatch();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [nodes, setNodes] = useState<Node[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
dispatch(
|
||||
getNodeList({
|
||||
page_size: 1000,
|
||||
page: 1,
|
||||
order_by: "id",
|
||||
order_direction: "desc",
|
||||
conditions: {
|
||||
[NodeStatusCondition]: NodeStatus.active,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.then((res) => {
|
||||
const filteredNodes = res.nodes.filter((n) => n.type != NodeType.master);
|
||||
setNodes(filteredNodes);
|
||||
if (!value && filteredNodes.length > 0) {
|
||||
onChange(filteredNodes[0].id);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!loading && nodes.length == 0) {
|
||||
return <Alert severity="warning">{t("settings.noNodes")}</Alert>;
|
||||
}
|
||||
return (
|
||||
<DenseSelect
|
||||
disabled={loading}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value as number)}
|
||||
MenuProps={{
|
||||
PaperProps: { sx: { maxWidth: 230 } },
|
||||
MenuListProps: {
|
||||
sx: {
|
||||
"& .MuiMenuItem-root": {
|
||||
whiteSpace: "normal",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{nodes.map((g) => (
|
||||
<SquareMenuItem value={g.id}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Typography variant={"body2"} fontWeight={600}>
|
||||
{g.name}
|
||||
</Typography>
|
||||
<Typography variant={"caption"} color={"textSecondary"}>
|
||||
{g.server}
|
||||
</Typography>
|
||||
</Box>
|
||||
</SquareMenuItem>
|
||||
))}
|
||||
</DenseSelect>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeSelectionInput;
|
||||
141
src/component/Admin/Common/ProDialog.tsx
Executable file
141
src/component/Admin/Common/ProDialog.tsx
Executable file
@@ -0,0 +1,141 @@
|
||||
import {
|
||||
Alert,
|
||||
AlertTitle,
|
||||
Button,
|
||||
DialogContent,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Typography,
|
||||
styled,
|
||||
} from "@mui/material";
|
||||
import dayjs from "dayjs";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import DraggableDialog, { StyledDialogActions } from "../../Dialogs/DraggableDialog";
|
||||
import CheckmarkCircleFilled from "../../Icons/CheckmarkCircleFilled";
|
||||
import Gift from "../../Icons/Gift";
|
||||
import { Code } from "../../Common/Code.tsx";
|
||||
|
||||
export interface ProDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const features = [
|
||||
"shareLinkCollabration",
|
||||
"filePermission",
|
||||
"multipleStoragePolicy",
|
||||
"auditAndActivity",
|
||||
"vasService",
|
||||
"sso",
|
||||
"more",
|
||||
];
|
||||
|
||||
const StyledButton = styled(Button)(({ theme }) => ({
|
||||
background: `linear-gradient(45deg, ${theme.palette.primary.main} 30%, ${theme.palette.primary.light} 90%)`,
|
||||
color: theme.palette.primary.contrastText,
|
||||
"&:hover": {
|
||||
background: `linear-gradient(45deg, ${theme.palette.primary.dark} 30%, ${theme.palette.primary.main} 90%)`,
|
||||
},
|
||||
transition: "all 300ms cubic-bezier(0.4, 0, 0.2, 1) !important",
|
||||
}));
|
||||
|
||||
// TODO: fetch from cloudreve.org
|
||||
const currentPromotion = {
|
||||
code: "FI5Q9668YV",
|
||||
discount: 15,
|
||||
start: "2025-08-12T00:00:00Z",
|
||||
end: "2025-10-12T23:59:59Z",
|
||||
};
|
||||
|
||||
const ProDialog = ({ open, onClose }: ProDialogProps) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const openMore = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
window.open("https://cloudreve.org/pro", "_blank");
|
||||
}, []);
|
||||
const showPromotion = useMemo(() => {
|
||||
const now = dayjs();
|
||||
return now >= dayjs(currentPromotion.start) && now <= dayjs(currentPromotion.end);
|
||||
}, []);
|
||||
return (
|
||||
<DraggableDialog
|
||||
title={t("pro.title")}
|
||||
dialogProps={{
|
||||
open,
|
||||
onClose,
|
||||
maxWidth: "sm",
|
||||
fullWidth: true,
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
{t("pro.description")}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" fontWeight={600} sx={{ mt: 2 }}>
|
||||
{t("pro.proInclude")}
|
||||
</Typography>
|
||||
|
||||
<List dense>
|
||||
{features.map((feature) => (
|
||||
<ListItem key={feature}>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: "36px",
|
||||
}}
|
||||
>
|
||||
<CheckmarkCircleFilled color="primary" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
slotProps={{
|
||||
primary: {
|
||||
sx: {},
|
||||
variant: "body1",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t(`pro.${feature}`)}
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
{showPromotion && (
|
||||
<Alert
|
||||
iconMapping={{
|
||||
info: <Gift fontSize="inherit" />,
|
||||
}}
|
||||
severity="info"
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
<AlertTitle>{t("pro.promotionTitle")}</AlertTitle>
|
||||
<Trans
|
||||
i18nKey="dashboard:pro.promotion"
|
||||
values={{
|
||||
code: currentPromotion.code,
|
||||
discount: currentPromotion.discount,
|
||||
}}
|
||||
components={[<Code />, <Typography component={"span"} fontWeight={600} />]}
|
||||
/>
|
||||
</Alert>
|
||||
)}
|
||||
</DialogContent>
|
||||
<StyledDialogActions
|
||||
sx={{
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Button variant="outlined" color="primary" onClick={onClose}>
|
||||
{t("pro.later")}
|
||||
</Button>
|
||||
<StyledButton onClick={openMore} variant="contained" color="primary">
|
||||
{t("pro.learnMore")}
|
||||
</StyledButton>
|
||||
</StyledDialogActions>
|
||||
</DraggableDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProDialog;
|
||||
45
src/component/Admin/Common/SharesInput.tsx
Executable file
45
src/component/Admin/Common/SharesInput.tsx
Executable file
@@ -0,0 +1,45 @@
|
||||
import { Box, debounce, useTheme } from "@mui/material";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getShareList } from "../../../api/api.ts";
|
||||
import { Share } from "../../../api/dashboard.ts";
|
||||
import { useAppDispatch } from "../../../redux/hooks.ts";
|
||||
import { DenseAutocomplete, DenseFilledTextField, NoWrapBox, SquareChip } from "../../Common/StyledComponents.tsx";
|
||||
import FileTypeIcon from "../../FileManager/Explorer/FileTypeIcon.tsx";
|
||||
import LinkDismiss from "../../Icons/LinkDismiss.tsx";
|
||||
|
||||
export interface SharesInputProps {}
|
||||
|
||||
const SharesInput = (props: SharesInputProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [options, setOptions] = useState<number[]>([]);
|
||||
|
||||
return (
|
||||
<DenseAutocomplete
|
||||
multiple
|
||||
options={options}
|
||||
blurOnSelect
|
||||
renderInput={(params) => (
|
||||
<DenseFilledTextField
|
||||
{...params}
|
||||
sx={{
|
||||
"& .MuiInputBase-root": {},
|
||||
"& .MuiInputBase-root.MuiOutlinedInput-root": {
|
||||
paddingTop: theme.spacing(0.6),
|
||||
paddingBottom: theme.spacing(0.6),
|
||||
},
|
||||
mt: 0,
|
||||
}}
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
placeholder={t("dashboard:settings.searchShare")}
|
||||
type="text"
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SharesInput;
|
||||
122
src/component/Admin/Common/SinglePolicySelectionInput.tsx
Executable file
122
src/component/Admin/Common/SinglePolicySelectionInput.tsx
Executable file
@@ -0,0 +1,122 @@
|
||||
import { Box, FormControl, ListItemText, SelectChangeEvent, Typography } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getStoragePolicyList } from "../../../api/api";
|
||||
import { StoragePolicy } from "../../../api/dashboard";
|
||||
import { useAppDispatch } from "../../../redux/hooks";
|
||||
import { DenseSelect } from "../../Common/StyledComponents";
|
||||
import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu";
|
||||
|
||||
export interface SinglePolicySelectionInputProps {
|
||||
value: number | undefined;
|
||||
onChange: (value: number) => void;
|
||||
emptyValue?: number;
|
||||
emptyText?: string;
|
||||
simplified?: boolean;
|
||||
}
|
||||
|
||||
const SinglePolicySelectionInput = ({
|
||||
value,
|
||||
onChange,
|
||||
emptyValue,
|
||||
emptyText,
|
||||
simplified,
|
||||
}: SinglePolicySelectionInputProps) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const dispatch = useAppDispatch();
|
||||
const [policies, setPolicies] = useState<StoragePolicy[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [policyMap, setPolicyMap] = useState<Record<number, StoragePolicy>>({});
|
||||
|
||||
const handleChange = (event: SelectChangeEvent<unknown>) => {
|
||||
const {
|
||||
target: { value },
|
||||
} = event;
|
||||
onChange(value as number);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
dispatch(getStoragePolicyList({ page: 1, page_size: 1000, order_by: "id", order_direction: "asc" }))
|
||||
.then((res) => {
|
||||
setPolicies(res.policies);
|
||||
setPolicyMap(
|
||||
res.policies.reduce(
|
||||
(acc, policy) => {
|
||||
acc[policy.id] = policy;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<number, StoragePolicy>,
|
||||
),
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FormControl fullWidth>
|
||||
<DenseSelect
|
||||
value={value}
|
||||
required
|
||||
onChange={handleChange}
|
||||
sx={{
|
||||
minHeight: 39,
|
||||
}}
|
||||
renderValue={
|
||||
simplified
|
||||
? (value) => (
|
||||
<ListItemText
|
||||
primary={policyMap[value as number]?.name}
|
||||
slotProps={{ primary: { variant: "body2" } }}
|
||||
/>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
disabled={loading}
|
||||
MenuProps={{
|
||||
PaperProps: { sx: { maxWidth: 230 } },
|
||||
MenuListProps: {
|
||||
sx: {
|
||||
"& .MuiMenuItem-root": {
|
||||
whiteSpace: "normal",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{policies.length > 0 &&
|
||||
policies.map((policy) => (
|
||||
<SquareMenuItem key={policy.id} value={policy.id}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Typography variant={"body2"} fontWeight={600}>
|
||||
{policy.name}
|
||||
</Typography>
|
||||
<Typography variant={"caption"} color={"textSecondary"}>
|
||||
{t(`policy.${policy.type}`)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</SquareMenuItem>
|
||||
))}
|
||||
{emptyValue !== undefined && emptyText && (
|
||||
<SquareMenuItem value={emptyValue}>
|
||||
<ListItemText
|
||||
primary={<em>{t(emptyText)}</em>}
|
||||
slotProps={{
|
||||
primary: { variant: "body2" },
|
||||
}}
|
||||
/>
|
||||
</SquareMenuItem>
|
||||
)}
|
||||
</DenseSelect>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default SinglePolicySelectionInput;
|
||||
86
src/component/Admin/Common/TablePagination.tsx
Executable file
86
src/component/Admin/Common/TablePagination.tsx
Executable file
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
Box,
|
||||
ListItemText,
|
||||
Pagination,
|
||||
PaginationProps,
|
||||
SelectChangeEvent,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DenseSelect } from "../../Common/StyledComponents";
|
||||
import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu";
|
||||
|
||||
export interface TablePaginationProps extends PaginationProps {
|
||||
rowsPerPageOptions?: number[];
|
||||
rowsPerPage?: number;
|
||||
onRowsPerPageChange?: (pageSize: number) => void;
|
||||
page: number;
|
||||
totalItems: number;
|
||||
onChange: (event: React.ChangeEvent<unknown>, value: number) => void;
|
||||
}
|
||||
|
||||
export const TablePagination = ({
|
||||
rowsPerPageOptions = [5, 10, 25, 50],
|
||||
rowsPerPage = 5,
|
||||
onRowsPerPageChange,
|
||||
page,
|
||||
onChange,
|
||||
totalItems,
|
||||
...props
|
||||
}: TablePaginationProps) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
const onDenseSelectChange = (e: SelectChangeEvent<unknown>) => {
|
||||
onRowsPerPageChange?.(e.target.value as number);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if ((page - 1) * rowsPerPage >= totalItems) {
|
||||
onChange({} as React.ChangeEvent<unknown>, Math.ceil(totalItems / rowsPerPage));
|
||||
}
|
||||
}, [rowsPerPage, totalItems]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
py: 1,
|
||||
display: "flex",
|
||||
flexDirection: isMobile ? "column" : "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: isMobile ? "start" : "center",
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Pagination
|
||||
size={isMobile ? "small" : "medium"}
|
||||
count={Math.ceil(totalItems / rowsPerPage)}
|
||||
page={page}
|
||||
onChange={onChange}
|
||||
{...props}
|
||||
/>
|
||||
<DenseSelect
|
||||
variant="filled"
|
||||
value={rowsPerPage}
|
||||
onChange={onDenseSelectChange}
|
||||
renderValue={(value) => (
|
||||
<ListItemText primary={t("settings.perPage", { num: value })} slotProps={{ primary: { variant: "body2" } }} />
|
||||
)}
|
||||
>
|
||||
{rowsPerPageOptions.map((option) => (
|
||||
<SquareMenuItem key={option} value={option}>
|
||||
<ListItemText
|
||||
primary={t("settings.perPage", { num: option })}
|
||||
slotProps={{ primary: { variant: "body2" } }}
|
||||
/>
|
||||
</SquareMenuItem>
|
||||
))}
|
||||
</DenseSelect>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TablePagination;
|
||||
Reference in New Issue
Block a user