feat(dbfs): set default share shortcut for new users

This commit is contained in:
Aaron Liu 2025-05-23 15:33:33 +08:00
parent 8e169aff08
commit b8b31fe6b8
7 changed files with 210 additions and 1 deletions

View File

@ -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.",

View File

@ -306,6 +306,9 @@
"captchaForResetDes": "パスワード再設定フォームでCAPTCHAを有効にするかどうか。",
"webauthnDes": "ユーザーが登録済みのハードウェア認証デバイス顔認証、指紋認証、USBキーなどを使用してログインすることを許可するかどうか。サイトでHTTPSを有効にする必要があります。",
"webauthn": "パスキーを使用してログイン",
"defaultSymbolics": "デフォルトの共有リンク",
"defaultSymbolicsDes": "新規ユーザーのルートディレクトリにデフォルトで存在する共有リンクのショートカットです。共有リンクをIDで検索できます。<0>共有リスト</0>の左側にIDが表示されます。",
"searchShare": "共有IDを検索...",
"defaultGroup": "デフォルトユーザーグループ",
"defaultGroupDes": "ユーザー登録後の初期ユーザーグループ",
"testMailSent": "テストメールを送信しました",

View File

@ -306,6 +306,9 @@
"captchaForResetDes": "是否启用找回密码表单验证码。",
"webauthnDes": "是否允许用户使用绑定的硬件认证设备登录,比如:人脸、指纹或 USB 密钥;站点必须启用 HTTPS 才能使用。",
"webauthn": "使用通行密钥登录",
"defaultSymbolics": "初始分享快捷方式",
"defaultSymbolicsDes": "新用户根目录下默认存在的分享链接快捷方式。请通过数字 ID 搜索分享链接,你可在 <0>分享列表</0> 最左侧看到数字 ID。",
"searchShare": "搜索分享 ID...",
"defaultGroup": "默认用户组",
"defaultGroupDes": "用户注册后的初始用户组。",
"testMailSent": "测试邮件已发送",

View File

@ -303,6 +303,9 @@
"captchaForResetDes": "是否啟用找回密碼表單驗證碼。",
"webauthnDes": "是否允許使用者使用繫結的硬體認證裝置登入,比如:人臉、指紋或 USB 金鑰;站點必須啟用 HTTPS 才能使用。",
"webauthn": "使用通行金鑰登入",
"defaultSymbolics": "預設分享快捷方式",
"defaultSymbolicsDes": "新使用者根目錄下預設存在的分享連結快捷方式。請通過數字 ID 搜尋分享連結,你可在 <0>分享列表</0> 最左側看到數字 ID。",
"searchShare": "搜尋分享 ID...",
"defaultGroup": "預設使用者組",
"defaultGroupDes": "使用者注冊後的初始使用者組。",
"testMailSent": "測試郵件已傳送",

View File

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

View File

@ -196,6 +196,7 @@ const Settings = () => {
"avatar_size",
"avatar_size_l",
"gravatar_server",
"default_symbolics",
]}
>
<UserSession />

View File

@ -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}>