diff --git a/public/locales/en-US/dashboard.json b/public/locales/en-US/dashboard.json index 4594d46..e90c270 100644 --- a/public/locales/en-US/dashboard.json +++ b/public/locales/en-US/dashboard.json @@ -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.", + "searchShare": "Search share ID...", "defaultGroup": "Default group", "defaultGroupDes": "The initial user group after user registration.", "testMailSent": "Test email is sent.", diff --git a/public/locales/ja-JP/dashboard.json b/public/locales/ja-JP/dashboard.json index 76cdbf6..ab51e35 100644 --- a/public/locales/ja-JP/dashboard.json +++ b/public/locales/ja-JP/dashboard.json @@ -306,6 +306,9 @@ "captchaForResetDes": "パスワード再設定フォームでCAPTCHAを有効にするかどうか。", "webauthnDes": "ユーザーが登録済みのハードウェア認証デバイス(顔認証、指紋認証、USBキーなど)を使用してログインすることを許可するかどうか。サイトでHTTPSを有効にする必要があります。", "webauthn": "パスキーを使用してログイン", + "defaultSymbolics": "デフォルトの共有リンク", + "defaultSymbolicsDes": "新規ユーザーのルートディレクトリにデフォルトで存在する共有リンクのショートカットです。共有リンクをIDで検索できます。<0>共有リストの左側にIDが表示されます。", + "searchShare": "共有IDを検索...", "defaultGroup": "デフォルトユーザーグループ", "defaultGroupDes": "ユーザー登録後の初期ユーザーグループ", "testMailSent": "テストメールを送信しました", diff --git a/public/locales/zh-CN/dashboard.json b/public/locales/zh-CN/dashboard.json index 6d47163..b024747 100644 --- a/public/locales/zh-CN/dashboard.json +++ b/public/locales/zh-CN/dashboard.json @@ -306,6 +306,9 @@ "captchaForResetDes": "是否启用找回密码表单验证码。", "webauthnDes": "是否允许用户使用绑定的硬件认证设备登录,比如:人脸、指纹或 USB 密钥;站点必须启用 HTTPS 才能使用。", "webauthn": "使用通行密钥登录", + "defaultSymbolics": "初始分享快捷方式", + "defaultSymbolicsDes": "新用户根目录下默认存在的分享链接快捷方式。请通过数字 ID 搜索分享链接,你可在 <0>分享列表 最左侧看到数字 ID。", + "searchShare": "搜索分享 ID...", "defaultGroup": "默认用户组", "defaultGroupDes": "用户注册后的初始用户组。", "testMailSent": "测试邮件已发送", diff --git a/public/locales/zh-TW/dashboard.json b/public/locales/zh-TW/dashboard.json index e63c746..21bc148 100644 --- a/public/locales/zh-TW/dashboard.json +++ b/public/locales/zh-TW/dashboard.json @@ -303,6 +303,9 @@ "captchaForResetDes": "是否啟用找回密碼表單驗證碼。", "webauthnDes": "是否允許使用者使用繫結的硬體認證裝置登入,比如:人臉、指紋或 USB 金鑰;站點必須啟用 HTTPS 才能使用。", "webauthn": "使用通行金鑰登入", + "defaultSymbolics": "預設分享快捷方式", + "defaultSymbolicsDes": "新使用者根目錄下預設存在的分享連結快捷方式。請通過數字 ID 搜尋分享連結,你可在 <0>分享列表 最左側看到數字 ID。", + "searchShare": "搜尋分享 ID...", "defaultGroup": "預設使用者組", "defaultGroupDes": "使用者注冊後的初始使用者組。", "testMailSent": "測試郵件已傳送", diff --git a/src/component/Admin/Common/SharesInput.tsx b/src/component/Admin/Common/SharesInput.tsx new file mode 100644 index 0000000..1aa070a --- /dev/null +++ b/src/component/Admin/Common/SharesInput.tsx @@ -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([]); + const [idShareMap, setIdShareMap] = useState>({}); + 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, + ), + })); + }) + .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 ( + { + 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 ( + + ) : ( + + ) + } + size="small" + label={share?.edges?.file?.name ?? t("application:share.expiredLink")} + key={key} + {...tagProps} + /> + ); + }) + } + renderOption={(props, option) => { + const share = idShareMap[option as number]; + return ( +
  • + + {share ? ( + + ) : ( + + )} + theme.typography.body2.fontSize, + width: "100%", + ml: 2, + }} + > + {share?.edges?.file?.name ?? t("application:share.expiredLink")} + + +
  • + ); + }} + renderInput={(params) => ( + + )} + /> + ); +}; + +export default SharesInput; diff --git a/src/component/Admin/Settings/Settings.tsx b/src/component/Admin/Settings/Settings.tsx index 2c63551..d029395 100644 --- a/src/component/Admin/Settings/Settings.tsx +++ b/src/component/Admin/Settings/Settings.tsx @@ -196,6 +196,7 @@ const Settings = () => { "avatar_size", "avatar_size_l", "gravatar_server", + "default_symbolics", ]} > diff --git a/src/component/Admin/Settings/UserSession/UserSession.tsx b/src/component/Admin/Settings/UserSession/UserSession.tsx index 94e8c85..3a780bf 100644 --- a/src/component/Admin/Settings/UserSession/UserSession.tsx +++ b/src/component/Admin/Settings/UserSession/UserSession.tsx @@ -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 ( e.preventDefault()}> @@ -96,6 +108,21 @@ const UserSession = () => { {t("settings.defaultGroupDes")} + + + setSettings({ default_symbolics: JSON.stringify(shares) })} + /> + + ]} + /> + + +