diff --git a/src/client/web/src/client/files.ts b/src/client/web/src/client/files.ts index 8425819..e0cbbd2 100644 --- a/src/client/web/src/client/files.ts +++ b/src/client/web/src/client/files.ts @@ -239,13 +239,16 @@ export class FilesClient extends BaseClient { }); }; - search = (keyword: string): Promise => { + search = (keywords: string[]): Promise => { + const params = new URLSearchParams(); + keywords.forEach(keyword => { + params.append(keywordQuery, keyword); + }); + return this.do({ method: "get", url: `${this.url}/v1/fs/search`, - params: { - [keywordQuery]: keyword, - }, + params, }); }; diff --git a/src/client/web/src/client/files_mock.ts b/src/client/web/src/client/files_mock.ts index 672d515..2d66ed0 100644 --- a/src/client/web/src/client/files_mock.ts +++ b/src/client/web/src/client/files_mock.ts @@ -279,7 +279,7 @@ export class MockFilesClient { return this.wrapPromise(this.resps.downloadMockResp); }; - search = (keyword: string): Promise => { + search = (keywords: string[]): Promise => { return this.wrapPromise(this.resps.searchMockResp); }; diff --git a/src/client/web/src/client/index.ts b/src/client/web/src/client/index.ts index 7a00e61..f387b21 100644 --- a/src/client/web/src/client/index.ts +++ b/src/client/web/src/client/index.ts @@ -163,6 +163,7 @@ export interface IFilesClient { getSharingDir: (shareID: string) => Promise>; generateHash: (filePath: string) => Promise; download: (url: string) => Promise; + search: (keywords: string[]) => Promise>; } export interface ISettingsClient { diff --git a/src/client/web/src/components/core_state.ts b/src/client/web/src/components/core_state.ts index bc1dc24..2e77567 100644 --- a/src/client/web/src/components/core_state.ts +++ b/src/client/web/src/components/core_state.ts @@ -61,6 +61,7 @@ export function initState(): ICoreState { isSharing: false, orderBy: filesOrderBy, order: true, + searchResults: List([]), }, uploadingsInfo: { uploadings: List([]), diff --git a/src/client/web/src/components/panel_files.tsx b/src/client/web/src/components/panel_files.tsx index dac3741..f325ab0 100644 --- a/src/client/web/src/components/panel_files.tsx +++ b/src/client/web/src/components/panel_files.tsx @@ -51,6 +51,7 @@ export interface FilesProps { items: List; orderBy: string; order: boolean; + searchResults: List; } export interface Props { @@ -74,6 +75,7 @@ export interface State { selectedItems: Map; showDetail: Set; uploadFiles: string; + searchKeywords: string; } export class FilesPanel extends React.Component { @@ -90,6 +92,7 @@ export class FilesPanel extends React.Component { selectedItems: Map(), showDetail: Set(), uploadFiles: "", + searchKeywords: "", }; Up().setStatusCb(this.updateProgress); @@ -132,6 +135,10 @@ export class FilesPanel extends React.Component { this.setState({ newFolderName: ev.target.value }); }; + onSearchKeywordsChange = (ev: React.ChangeEvent) => { + this.setState({ searchKeywords: ev.target.value }); + }; + setLoading = (state: boolean) => { updater().setControlOption(loadingCtrl, state ? ctrlOn : ctrlOff); this.props.update(updater().updateUI); @@ -674,6 +681,54 @@ export class FilesPanel extends React.Component { this.props.update(updater().updateFilesInfo); }; + search = async () => { + if (this.state.searchKeywords.trim() === "") { + Env().alertMsg(this.props.msg.pkg.get("hint.keywords")); + return; + } + const keywords = this.state.searchKeywords.split(" "); + if (keywords.length === 0) { + Env().alertMsg(this.props.msg.pkg.get("hint.keywords")); + return; + } + + const status = await updater().search(keywords); + if (status !== "") { + Env().alertMsg(getErrMsg(this.props.msg.pkg, "op.fail", status)); + return; + } else if (!updater().hasResult()) { + Env().alertMsg(this.props.msg.pkg.get("term.noResult")); + return; + } + this.props.update(updater().updateFilesInfo); + }; + + searchKb = async ( + event: React.KeyboardEvent + ): Promise => { + if (event.key === "Enter") { + return await this.search(); + } + }; + + gotoSearchResult = async (pathname: string) => { + this.setLoading(true); + try { + const status = await updater().gotoSearchResult(pathname); + if (status !== "") { + Env().alertMsg(getErrMsg(this.props.msg.pkg, "op.fail", status)); + } + } finally { + this.setLoading(false); + } + }; + + truncateSearchResults = async () => { + updater().truncateSearchResults(); + this.setState({ searchKeywords: "" }); + this.props.update(updater().updateFilesInfo); + }; + render() { const showEndpoints = this.props.login.userRole === roleAdmin ? "" : "hidden"; @@ -686,14 +741,50 @@ export class FilesPanel extends React.Component { iconColor="normal" iconName="RiGridFill" /> -
- - +
+ + + + , + + <> + + + + , + ])} + childrenStyles={List([ + { flex: "0 0 50%" }, + { flex: "0 0 50%", justifyContent: "flex-end" }, + ])} + /> ); @@ -836,10 +927,49 @@ export class FilesPanel extends React.Component { ? "focus-font" : "major-font"; + const showSearchResults = + this.props.filesInfo.searchResults.size > 0 ? "" : "hidden"; + const searchResultPane = this.props.filesInfo.searchResults.map( + (searchResult: string) => { + return ( + <> + {searchResult}, + , + ])} + childrenStyles={List([{}, { justifyContent: "flex-end" }])} + /> +
+ + ); + } + ); + const itemListPane = (
{endPoints} +
+ + + <div className="hr"></div> + {searchResultPane} + </Container> + </div> + <div className={showOp}> <Container>{ops}</Container> </div> diff --git a/src/client/web/src/components/state_updater.ts b/src/client/web/src/components/state_updater.ts index 2fb5eaf..18007d5 100644 --- a/src/client/web/src/components/state_updater.ts +++ b/src/client/web/src/components/state_updater.ts @@ -287,6 +287,24 @@ export class Updater { return errServer; }; + gotoSearchResult = async (pathname: string): Promise<string> => { + const metadataResp = await this.filesClient.metadata(pathname); + if (metadataResp.status !== 200) { + return errServer; + } + + const parts = pathname.split("/"); + let targetDir = List(parts); + if (!metadataResp.data.isDir) { + targetDir = targetDir.slice(0, parts.length - 1); + } + return updater().setItems(List(targetDir)); + }; + + truncateSearchResults = () => { + this.props.filesInfo.searchResults = List([]); + }; + updateItems = (items: List<MetadataResp>) => { this.props.filesInfo.items = items; }; @@ -993,6 +1011,24 @@ export class Updater { return resp.status == 200 ? "" : errServer; }; + search = async (keywords: string[]): Promise<string> => { + const resp = await this.filesClient.search(keywords); + if (resp.status !== 200) { + return errServer; + } + + this.props.filesInfo.searchResults = List( + resp.data.results.sort((path1: string, path2: string) => { + return path1.length <= path2.length ? -1 : 1; + }) + ); + return ""; + }; + + hasResult = (): boolean => { + return this.props.filesInfo.searchResults.size > 0; + }; + updateAll = (prevState: ICoreState): ICoreState => { return { filesInfo: { ...prevState.filesInfo, ...this.props.filesInfo }, diff --git a/src/client/web/src/components/visual/icons.tsx b/src/client/web/src/components/visual/icons.tsx index 7123aae..ec385c7 100644 --- a/src/client/web/src/components/visual/icons.tsx +++ b/src/client/web/src/components/visual/icons.tsx @@ -24,6 +24,7 @@ import { RiListSettingsFill } from "@react-icons/all-files/ri/RiListSettingsFill import { RiHardDriveFill } from "@react-icons/all-files/ri/RiHardDriveFill"; import { RiGridFill } from "@react-icons/all-files/ri/RiGridFill"; import { RiFolderUploadFill } from "@react-icons/all-files/ri/RiFolderUploadFill"; +import { RiFileSearchFill } from "@react-icons/all-files/ri/RiFileSearchFill"; import { colorClass } from "./colors"; @@ -34,28 +35,29 @@ export interface IconProps { } const icons = Map<string, IconType>({ - RiFileList2Fill: RiFileList2Fill, - RiFolder2Fill: RiFolder2Fill, - RiShareBoxLine: RiShareBoxLine, - RiUploadCloudFill: RiUploadCloudFill, - RiSettings3Fill: RiSettings3Fill, - RiWindowFill: RiWindowFill, - RiCheckboxBlankFill: RiCheckboxBlankFill, - RiCheckboxFill: RiCheckboxFill, - RiMenuFill: RiMenuFill, - RiInformationFill: RiInformationFill, - RiDeleteBin2Fill: RiDeleteBin2Fill, - RiArchiveDrawerFill: RiArchiveDrawerFill, - RiArrowUpDownFill: RiArrowUpDownFill, - BiTable: BiTable, - BiListUl: BiListUl, - RiMore2Fill: RiMore2Fill, - RiCheckboxBlankLine: RiCheckboxBlankLine, - BiSortUp: BiSortUp, - RiListSettingsFill: RiListSettingsFill, - RiHardDriveFill: RiHardDriveFill, - RiGridFill: RiGridFill, - RiFolderUploadFill: RiFolderUploadFill, + RiFileList2Fill, + RiFolder2Fill, + RiShareBoxLine, + RiUploadCloudFill, + RiSettings3Fill, + RiWindowFill, + RiCheckboxBlankFill, + RiCheckboxFill, + RiMenuFill, + RiInformationFill, + RiDeleteBin2Fill, + RiArchiveDrawerFill, + RiArrowUpDownFill, + BiTable, + BiListUl, + RiMore2Fill, + RiCheckboxBlankLine, + BiSortUp, + RiListSettingsFill, + RiHardDriveFill, + RiGridFill, + RiFolderUploadFill, + RiFileSearchFill, }); export function getIconWithProps( diff --git a/src/client/web/src/i18n/en_US.ts b/src/client/web/src/i18n/en_US.ts index 3efab24..65e328a 100644 --- a/src/client/web/src/i18n/en_US.ts +++ b/src/client/web/src/i18n/en_US.ts @@ -153,5 +153,10 @@ export const msgs: Map<string, string> = Map({ "autoTheme": "Enable auto theme switching", "term.enabled": "Enabled", "term.disabled": "Disabled", - "term.dropAnywhere": "Drop files anywhere" + "term.dropAnywhere": "Drop files anywhere", + "term.search": "Search", + "term.results": "Results", + "term.noResult": "No result found", + "action.go": "Go", + "hint.keywords": "Please input keyword(s), separated by spaces" }); diff --git a/src/client/web/src/i18n/zh_CN.ts b/src/client/web/src/i18n/zh_CN.ts index d34810f..0f5c8ef 100644 --- a/src/client/web/src/i18n/zh_CN.ts +++ b/src/client/web/src/i18n/zh_CN.ts @@ -150,5 +150,10 @@ export const msgs: Map<string, string> = Map({ "autoTheme": "自动切换主题", "term.enabled": "启用", "term.disabled": "关闭", - "term.dropAnywhere": "把文件在任意处释放" + "term.dropAnywhere": "把文件在任意处释放", + "term.search": "搜索", + "term.results": "结果", + "term.noResult": "未找到结果", + "action.go": "前往", + "hint.keywords": "请输入关键字,以空格分隔" }); diff --git a/src/handlers/fileshdr/handlers.go b/src/handlers/fileshdr/handlers.go index b22b816..ad8f86d 100644 --- a/src/handlers/fileshdr/handlers.go +++ b/src/handlers/fileshdr/handlers.go @@ -1181,10 +1181,15 @@ func (h *FileHandlers) SearchItems(c *gin.Context) { } } + role := c.MustGet(q.RoleParam).(string) + userName := c.MustGet(q.UserParam).(string) results := []string{} for pathname, count := range resultsMap { if count >= len(keywords) { - results = append(results, pathname) + if role == db.AdminRole || + (role != db.AdminRole && strings.HasPrefix(pathname, userName)) { + results = append(results, pathname) + } } }