mirror of
https://github.com/cloudreve/frontend.git
synced 2025-12-25 19:52:48 +00:00
feat(dbfs): set default share shortcut for new users
This commit is contained in:
parent
8e169aff08
commit
b8b31fe6b8
|
|
@ -306,6 +306,9 @@
|
|||
"captchaForResetDes": "Whether to enable the captcha for resetting password.",
|
||||
"webauthnDes": "Whether to allow users to sign-in with hardware authentication devices, such as: face, fingerprint or USB key; the site must enable HTTPS.",
|
||||
"webauthn": "Sign-in with Passkeys",
|
||||
"defaultSymbolics": "Default share shortcuts",
|
||||
"defaultSymbolicsDes": "Default share shortcuts in the root directory of new users. Please search for share links by ID, you can see the ID on the left side of the <0>share list</0>.",
|
||||
"searchShare": "Search share ID...",
|
||||
"defaultGroup": "Default group",
|
||||
"defaultGroupDes": "The initial user group after user registration.",
|
||||
"testMailSent": "Test email is sent.",
|
||||
|
|
|
|||
|
|
@ -306,6 +306,9 @@
|
|||
"captchaForResetDes": "パスワード再設定フォームでCAPTCHAを有効にするかどうか。",
|
||||
"webauthnDes": "ユーザーが登録済みのハードウェア認証デバイス(顔認証、指紋認証、USBキーなど)を使用してログインすることを許可するかどうか。サイトでHTTPSを有効にする必要があります。",
|
||||
"webauthn": "パスキーを使用してログイン",
|
||||
"defaultSymbolics": "デフォルトの共有リンク",
|
||||
"defaultSymbolicsDes": "新規ユーザーのルートディレクトリにデフォルトで存在する共有リンクのショートカットです。共有リンクをIDで検索できます。<0>共有リスト</0>の左側にIDが表示されます。",
|
||||
"searchShare": "共有IDを検索...",
|
||||
"defaultGroup": "デフォルトユーザーグループ",
|
||||
"defaultGroupDes": "ユーザー登録後の初期ユーザーグループ",
|
||||
"testMailSent": "テストメールを送信しました",
|
||||
|
|
|
|||
|
|
@ -306,6 +306,9 @@
|
|||
"captchaForResetDes": "是否启用找回密码表单验证码。",
|
||||
"webauthnDes": "是否允许用户使用绑定的硬件认证设备登录,比如:人脸、指纹或 USB 密钥;站点必须启用 HTTPS 才能使用。",
|
||||
"webauthn": "使用通行密钥登录",
|
||||
"defaultSymbolics": "初始分享快捷方式",
|
||||
"defaultSymbolicsDes": "新用户根目录下默认存在的分享链接快捷方式。请通过数字 ID 搜索分享链接,你可在 <0>分享列表</0> 最左侧看到数字 ID。",
|
||||
"searchShare": "搜索分享 ID...",
|
||||
"defaultGroup": "默认用户组",
|
||||
"defaultGroupDes": "用户注册后的初始用户组。",
|
||||
"testMailSent": "测试邮件已发送",
|
||||
|
|
|
|||
|
|
@ -303,6 +303,9 @@
|
|||
"captchaForResetDes": "是否啟用找回密碼表單驗證碼。",
|
||||
"webauthnDes": "是否允許使用者使用繫結的硬體認證裝置登入,比如:人臉、指紋或 USB 金鑰;站點必須啟用 HTTPS 才能使用。",
|
||||
"webauthn": "使用通行金鑰登入",
|
||||
"defaultSymbolics": "預設分享快捷方式",
|
||||
"defaultSymbolicsDes": "新使用者根目錄下預設存在的分享連結快捷方式。請通過數字 ID 搜尋分享連結,你可在 <0>分享列表</0> 最左側看到數字 ID。",
|
||||
"searchShare": "搜尋分享 ID...",
|
||||
"defaultGroup": "預設使用者組",
|
||||
"defaultGroupDes": "使用者注冊後的初始使用者組。",
|
||||
"testMailSent": "測試郵件已傳送",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,169 @@
|
|||
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 {
|
||||
value: number[];
|
||||
onChange: (value: number[]) => void;
|
||||
}
|
||||
|
||||
const SharesInput = (props: SharesInputProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [options, setOptions] = useState<number[]>([]);
|
||||
const [idShareMap, setIdShareMap] = useState<Record<number, Share>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (props.value.length > 0) {
|
||||
fetch({ input: props.value.join(",") });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetch = useMemo(
|
||||
() =>
|
||||
debounce((request: { input: string }) => {
|
||||
setLoading(true);
|
||||
dispatch(
|
||||
getShareList({
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
order_by: "",
|
||||
order_direction: "desc",
|
||||
conditions: {
|
||||
share_id: request.input,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.then((results) => {
|
||||
setOptions(results?.shares?.map((share) => share.id) ?? []);
|
||||
setIdShareMap((origin) => ({
|
||||
...origin,
|
||||
...results?.shares?.reduce(
|
||||
(acc, share) => {
|
||||
acc[share.id] = share;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<number, Share>,
|
||||
),
|
||||
}));
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, 400),
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
|
||||
if (inputValue === "" || inputValue.length < 2) {
|
||||
setOptions([]);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
fetch({ input: inputValue });
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [inputValue, fetch]);
|
||||
|
||||
const handleChange = (_event: React.SyntheticEvent, value: number[]) => {
|
||||
props.onChange(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<DenseAutocomplete
|
||||
multiple
|
||||
value={props.value}
|
||||
options={options}
|
||||
loading={loading}
|
||||
blurOnSelect
|
||||
onChange={(_event: any, newValue: unknown) => {
|
||||
if (newValue) {
|
||||
console.log(newValue);
|
||||
props.onChange(newValue as number[]);
|
||||
}
|
||||
}}
|
||||
onInputChange={(_event, newInputValue) => {
|
||||
setInputValue(newInputValue);
|
||||
}}
|
||||
noOptionsText={t("application:modals.noResults")}
|
||||
renderTags={(value: readonly unknown[], getTagProps) =>
|
||||
value.map((option: unknown, index: number) => {
|
||||
const { key, ...tagProps } = getTagProps({ index });
|
||||
const share = idShareMap[option as number];
|
||||
return (
|
||||
<SquareChip
|
||||
icon={
|
||||
share ? (
|
||||
<FileTypeIcon name={share?.edges?.file?.name ?? ""} fileType={share?.edges?.file?.type ?? 0} />
|
||||
) : (
|
||||
<LinkDismiss />
|
||||
)
|
||||
}
|
||||
size="small"
|
||||
label={share?.edges?.file?.name ?? t("application:share.expiredLink")}
|
||||
key={key}
|
||||
{...tagProps}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
renderOption={(props, option) => {
|
||||
const share = idShareMap[option as number];
|
||||
return (
|
||||
<li {...props}>
|
||||
<Box sx={{ display: "flex", width: "100%", alignItems: "center" }}>
|
||||
{share ? (
|
||||
<FileTypeIcon name={share?.edges?.file?.name ?? ""} fileType={share?.edges?.file?.type ?? 0} />
|
||||
) : (
|
||||
<LinkDismiss />
|
||||
)}
|
||||
<NoWrapBox
|
||||
sx={{
|
||||
fontSize: (theme) => theme.typography.body2.fontSize,
|
||||
width: "100%",
|
||||
ml: 2,
|
||||
}}
|
||||
>
|
||||
{share?.edges?.file?.name ?? t("application:share.expiredLink")}
|
||||
</NoWrapBox>
|
||||
</Box>
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
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;
|
||||
|
|
@ -196,6 +196,7 @@ const Settings = () => {
|
|||
"avatar_size",
|
||||
"avatar_size_l",
|
||||
"gravatar_server",
|
||||
"default_symbolics",
|
||||
]}
|
||||
>
|
||||
<UserSession />
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { Box, FormControl, FormControlLabel, Link, ListItemText, Stack, Switch, Typography } from "@mui/material";
|
||||
import { useContext } from "react";
|
||||
import { useContext, useMemo } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { isTrueVal } from "../../../../session/utils.ts";
|
||||
import SizeInput from "../../../Common/SizeInput.tsx";
|
||||
import { DenseFilledTextField, DenseSelect } from "../../../Common/StyledComponents.tsx";
|
||||
import { SquareMenuItem } from "../../../FileManager/ContextMenu/ContextMenu.tsx";
|
||||
import SettingForm, { ProChip } from "../../../Pages/Setting/SettingForm.tsx";
|
||||
import GroupSelectionInput from "../../Common/GroupSelectionInput.tsx";
|
||||
import SharesInput from "../../Common/SharesInput.tsx";
|
||||
import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../Settings.tsx";
|
||||
import { SettingContext } from "../SettingWrapper.tsx";
|
||||
import SSOSettings from "./SSOSettings.tsx";
|
||||
|
|
@ -15,6 +17,16 @@ const UserSession = () => {
|
|||
const { t } = useTranslation("dashboard");
|
||||
const { formRef, setSettings, values } = useContext(SettingContext);
|
||||
|
||||
const defaultSymbolics = useMemo(() => {
|
||||
let result: number[] = [];
|
||||
try {
|
||||
result = JSON.parse(values?.default_symbolics ?? "[]");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return result;
|
||||
}, [values?.default_symbolics]);
|
||||
|
||||
return (
|
||||
<Box component={"form"} ref={formRef} onSubmit={(e) => e.preventDefault()}>
|
||||
<Stack spacing={5}>
|
||||
|
|
@ -96,6 +108,21 @@ const UserSession = () => {
|
|||
<NoMarginHelperText>{t("settings.defaultGroupDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("settings.defaultSymbolics")} lgWidth={5}>
|
||||
<FormControl>
|
||||
<SharesInput
|
||||
value={defaultSymbolics}
|
||||
onChange={(shares) => setSettings({ default_symbolics: JSON.stringify(shares) })}
|
||||
/>
|
||||
<NoMarginHelperText>
|
||||
<Trans
|
||||
i18nKey="settings.defaultSymbolicsDes"
|
||||
ns={"dashboard"}
|
||||
components={[<Link component={RouterLink} to={"/admin/share"} />]}
|
||||
/>
|
||||
</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("vas.filterEmailProvider")} lgWidth={5} pro>
|
||||
<FormControl>
|
||||
<DenseSelect value={0}>
|
||||
|
|
|
|||
Loading…
Reference in New Issue