feat(ui/files_pane): enable file/folder searching

This commit is contained in:
hexxa 2022-07-30 15:49:38 +08:00 committed by Hexxa
parent 302d3a6af8
commit dbb1ce65ef
10 changed files with 225 additions and 37 deletions

View file

@ -239,13 +239,16 @@ export class FilesClient extends BaseClient {
}); });
}; };
search = (keyword: string): Promise<Response> => { search = (keywords: string[]): Promise<Response> => {
const params = new URLSearchParams();
keywords.forEach(keyword => {
params.append(keywordQuery, keyword);
});
return this.do({ return this.do({
method: "get", method: "get",
url: `${this.url}/v1/fs/search`, url: `${this.url}/v1/fs/search`,
params: { params,
[keywordQuery]: keyword,
},
}); });
}; };

View file

@ -279,7 +279,7 @@ export class MockFilesClient {
return this.wrapPromise(this.resps.downloadMockResp); return this.wrapPromise(this.resps.downloadMockResp);
}; };
search = (keyword: string): Promise<Response> => { search = (keywords: string[]): Promise<Response> => {
return this.wrapPromise(this.resps.searchMockResp); return this.wrapPromise(this.resps.searchMockResp);
}; };

View file

@ -163,6 +163,7 @@ export interface IFilesClient {
getSharingDir: (shareID: string) => Promise<Response<GetSharingDirResp>>; getSharingDir: (shareID: string) => Promise<Response<GetSharingDirResp>>;
generateHash: (filePath: string) => Promise<Response>; generateHash: (filePath: string) => Promise<Response>;
download: (url: string) => Promise<Response>; download: (url: string) => Promise<Response>;
search: (keywords: string[]) => Promise<Response<SearchItemsResp>>;
} }
export interface ISettingsClient { export interface ISettingsClient {

View file

@ -61,6 +61,7 @@ export function initState(): ICoreState {
isSharing: false, isSharing: false,
orderBy: filesOrderBy, orderBy: filesOrderBy,
order: true, order: true,
searchResults: List<string>([]),
}, },
uploadingsInfo: { uploadingsInfo: {
uploadings: List<UploadEntry>([]), uploadings: List<UploadEntry>([]),

View file

@ -51,6 +51,7 @@ export interface FilesProps {
items: List<MetadataResp>; items: List<MetadataResp>;
orderBy: string; orderBy: string;
order: boolean; order: boolean;
searchResults: List<string>;
} }
export interface Props { export interface Props {
@ -74,6 +75,7 @@ export interface State {
selectedItems: Map<string, boolean>; selectedItems: Map<string, boolean>;
showDetail: Set<string>; showDetail: Set<string>;
uploadFiles: string; uploadFiles: string;
searchKeywords: string;
} }
export class FilesPanel extends React.Component<Props, State, {}> { export class FilesPanel extends React.Component<Props, State, {}> {
@ -90,6 +92,7 @@ export class FilesPanel extends React.Component<Props, State, {}> {
selectedItems: Map<string, boolean>(), selectedItems: Map<string, boolean>(),
showDetail: Set<string>(), showDetail: Set<string>(),
uploadFiles: "", uploadFiles: "",
searchKeywords: "",
}; };
Up().setStatusCb(this.updateProgress); Up().setStatusCb(this.updateProgress);
@ -132,6 +135,10 @@ export class FilesPanel extends React.Component<Props, State, {}> {
this.setState({ newFolderName: ev.target.value }); this.setState({ newFolderName: ev.target.value });
}; };
onSearchKeywordsChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ searchKeywords: ev.target.value });
};
setLoading = (state: boolean) => { setLoading = (state: boolean) => {
updater().setControlOption(loadingCtrl, state ? ctrlOn : ctrlOff); updater().setControlOption(loadingCtrl, state ? ctrlOn : ctrlOff);
this.props.update(updater().updateUI); this.props.update(updater().updateUI);
@ -674,6 +681,54 @@ export class FilesPanel extends React.Component<Props, State, {}> {
this.props.update(updater().updateFilesInfo); 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<HTMLInputElement>
): Promise<void> => {
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() { render() {
const showEndpoints = const showEndpoints =
this.props.login.userRole === roleAdmin ? "" : "hidden"; this.props.login.userRole === roleAdmin ? "" : "hidden";
@ -686,14 +741,50 @@ export class FilesPanel extends React.Component<Props, State, {}> {
iconColor="normal" iconColor="normal"
iconName="RiGridFill" iconName="RiGridFill"
/> />
<div className="hr"></div>
<button onClick={gotoRoot} className="button-default margin-r-m"> <div className="hr"></div>
{this.props.msg.pkg.get("endpoints.root")} <Flexbox
</button> children={List([
<button onClick={this.goHome} className="button-default"> <>
{this.props.msg.pkg.get("endpoints.home")} <button
</button> onClick={gotoRoot}
className="button-default margin-r-m"
>
{this.props.msg.pkg.get("endpoints.root")}
</button>
<button onClick={this.goHome} className="button-default">
{this.props.msg.pkg.get("endpoints.home")}
</button>
</>,
<>
<input
type="text"
onChange={this.onSearchKeywordsChange}
onKeyUp={this.searchKb}
value={this.state.searchKeywords}
placeholder={this.props.msg.pkg.get("hint.keywords")}
className="inline-block margin-r-m"
/>
<button
onClick={this.search}
className="button-default margin-r-m"
>
{this.props.msg.pkg.get("term.search")}
</button>
<button
onClick={this.truncateSearchResults}
className="button-default"
>
{this.props.msg.pkg.get("reset")}
</button>
</>,
])}
childrenStyles={List([
{ flex: "0 0 50%" },
{ flex: "0 0 50%", justifyContent: "flex-end" },
])}
/>
</Container> </Container>
</div> </div>
); );
@ -836,10 +927,49 @@ export class FilesPanel extends React.Component<Props, State, {}> {
? "focus-font" ? "focus-font"
: "major-font"; : "major-font";
const showSearchResults =
this.props.filesInfo.searchResults.size > 0 ? "" : "hidden";
const searchResultPane = this.props.filesInfo.searchResults.map(
(searchResult: string) => {
return (
<>
<Flexbox
children={List([
<span className="font-s">{searchResult}</span>,
<button
type="button"
onClick={() => {
this.gotoSearchResult(searchResult);
}}
className="button-default"
>
{this.props.msg.pkg.get("action.go")}
</button>,
])}
childrenStyles={List([{}, { justifyContent: "flex-end" }])}
/>
<div className="hr"></div>
</>
);
}
);
const itemListPane = ( const itemListPane = (
<div> <div>
{endPoints} {endPoints}
<div className={showSearchResults}>
<Container>
<Title
title={this.props.msg.pkg.get("term.results")}
iconColor="normal"
iconName="RiFileSearchFill"
/>
<div className="hr"></div>
{searchResultPane}
</Container>
</div>
<div className={showOp}> <div className={showOp}>
<Container>{ops}</Container> <Container>{ops}</Container>
</div> </div>

View file

@ -287,6 +287,24 @@ export class Updater {
return errServer; 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>) => { updateItems = (items: List<MetadataResp>) => {
this.props.filesInfo.items = items; this.props.filesInfo.items = items;
}; };
@ -993,6 +1011,24 @@ export class Updater {
return resp.status == 200 ? "" : errServer; 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 => { updateAll = (prevState: ICoreState): ICoreState => {
return { return {
filesInfo: { ...prevState.filesInfo, ...this.props.filesInfo }, filesInfo: { ...prevState.filesInfo, ...this.props.filesInfo },

View file

@ -24,6 +24,7 @@ import { RiListSettingsFill } from "@react-icons/all-files/ri/RiListSettingsFill
import { RiHardDriveFill } from "@react-icons/all-files/ri/RiHardDriveFill"; import { RiHardDriveFill } from "@react-icons/all-files/ri/RiHardDriveFill";
import { RiGridFill } from "@react-icons/all-files/ri/RiGridFill"; import { RiGridFill } from "@react-icons/all-files/ri/RiGridFill";
import { RiFolderUploadFill } from "@react-icons/all-files/ri/RiFolderUploadFill"; import { RiFolderUploadFill } from "@react-icons/all-files/ri/RiFolderUploadFill";
import { RiFileSearchFill } from "@react-icons/all-files/ri/RiFileSearchFill";
import { colorClass } from "./colors"; import { colorClass } from "./colors";
@ -34,28 +35,29 @@ export interface IconProps {
} }
const icons = Map<string, IconType>({ const icons = Map<string, IconType>({
RiFileList2Fill: RiFileList2Fill, RiFileList2Fill,
RiFolder2Fill: RiFolder2Fill, RiFolder2Fill,
RiShareBoxLine: RiShareBoxLine, RiShareBoxLine,
RiUploadCloudFill: RiUploadCloudFill, RiUploadCloudFill,
RiSettings3Fill: RiSettings3Fill, RiSettings3Fill,
RiWindowFill: RiWindowFill, RiWindowFill,
RiCheckboxBlankFill: RiCheckboxBlankFill, RiCheckboxBlankFill,
RiCheckboxFill: RiCheckboxFill, RiCheckboxFill,
RiMenuFill: RiMenuFill, RiMenuFill,
RiInformationFill: RiInformationFill, RiInformationFill,
RiDeleteBin2Fill: RiDeleteBin2Fill, RiDeleteBin2Fill,
RiArchiveDrawerFill: RiArchiveDrawerFill, RiArchiveDrawerFill,
RiArrowUpDownFill: RiArrowUpDownFill, RiArrowUpDownFill,
BiTable: BiTable, BiTable,
BiListUl: BiListUl, BiListUl,
RiMore2Fill: RiMore2Fill, RiMore2Fill,
RiCheckboxBlankLine: RiCheckboxBlankLine, RiCheckboxBlankLine,
BiSortUp: BiSortUp, BiSortUp,
RiListSettingsFill: RiListSettingsFill, RiListSettingsFill,
RiHardDriveFill: RiHardDriveFill, RiHardDriveFill,
RiGridFill: RiGridFill, RiGridFill,
RiFolderUploadFill: RiFolderUploadFill, RiFolderUploadFill,
RiFileSearchFill,
}); });
export function getIconWithProps( export function getIconWithProps(

View file

@ -153,5 +153,10 @@ export const msgs: Map<string, string> = Map({
"autoTheme": "Enable auto theme switching", "autoTheme": "Enable auto theme switching",
"term.enabled": "Enabled", "term.enabled": "Enabled",
"term.disabled": "Disabled", "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"
}); });

View file

@ -150,5 +150,10 @@ export const msgs: Map<string, string> = Map({
"autoTheme": "自动切换主题", "autoTheme": "自动切换主题",
"term.enabled": "启用", "term.enabled": "启用",
"term.disabled": "关闭", "term.disabled": "关闭",
"term.dropAnywhere": "把文件在任意处释放" "term.dropAnywhere": "把文件在任意处释放",
"term.search": "搜索",
"term.results": "结果",
"term.noResult": "未找到结果",
"action.go": "前往",
"hint.keywords": "请输入关键字,以空格分隔"
}); });

View file

@ -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{} results := []string{}
for pathname, count := range resultsMap { for pathname, count := range resultsMap {
if count >= len(keywords) { if count >= len(keywords) {
results = append(results, pathname) if role == db.AdminRole ||
(role != db.AdminRole && strings.HasPrefix(pathname, userName)) {
results = append(results, pathname)
}
} }
} }