feat(ui/files_pane): enable file/folder searching
This commit is contained in:
parent
302d3a6af8
commit
dbb1ce65ef
10 changed files with 225 additions and 37 deletions
|
@ -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({
|
||||
method: "get",
|
||||
url: `${this.url}/v1/fs/search`,
|
||||
params: {
|
||||
[keywordQuery]: keyword,
|
||||
},
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -279,7 +279,7 @@ export class MockFilesClient {
|
|||
return this.wrapPromise(this.resps.downloadMockResp);
|
||||
};
|
||||
|
||||
search = (keyword: string): Promise<Response> => {
|
||||
search = (keywords: string[]): Promise<Response> => {
|
||||
return this.wrapPromise(this.resps.searchMockResp);
|
||||
};
|
||||
|
||||
|
|
|
@ -163,6 +163,7 @@ export interface IFilesClient {
|
|||
getSharingDir: (shareID: string) => Promise<Response<GetSharingDirResp>>;
|
||||
generateHash: (filePath: string) => Promise<Response>;
|
||||
download: (url: string) => Promise<Response>;
|
||||
search: (keywords: string[]) => Promise<Response<SearchItemsResp>>;
|
||||
}
|
||||
|
||||
export interface ISettingsClient {
|
||||
|
|
|
@ -61,6 +61,7 @@ export function initState(): ICoreState {
|
|||
isSharing: false,
|
||||
orderBy: filesOrderBy,
|
||||
order: true,
|
||||
searchResults: List<string>([]),
|
||||
},
|
||||
uploadingsInfo: {
|
||||
uploadings: List<UploadEntry>([]),
|
||||
|
|
|
@ -51,6 +51,7 @@ export interface FilesProps {
|
|||
items: List<MetadataResp>;
|
||||
orderBy: string;
|
||||
order: boolean;
|
||||
searchResults: List<string>;
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
|
@ -74,6 +75,7 @@ export interface State {
|
|||
selectedItems: Map<string, boolean>;
|
||||
showDetail: Set<string>;
|
||||
uploadFiles: string;
|
||||
searchKeywords: string;
|
||||
}
|
||||
|
||||
export class FilesPanel extends React.Component<Props, State, {}> {
|
||||
|
@ -90,6 +92,7 @@ export class FilesPanel extends React.Component<Props, State, {}> {
|
|||
selectedItems: Map<string, boolean>(),
|
||||
showDetail: Set<string>(),
|
||||
uploadFiles: "",
|
||||
searchKeywords: "",
|
||||
};
|
||||
|
||||
Up().setStatusCb(this.updateProgress);
|
||||
|
@ -132,6 +135,10 @@ export class FilesPanel extends React.Component<Props, State, {}> {
|
|||
this.setState({ newFolderName: ev.target.value });
|
||||
};
|
||||
|
||||
onSearchKeywordsChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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<Props, State, {}> {
|
|||
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() {
|
||||
const showEndpoints =
|
||||
this.props.login.userRole === roleAdmin ? "" : "hidden";
|
||||
|
@ -686,14 +741,50 @@ export class FilesPanel extends React.Component<Props, State, {}> {
|
|||
iconColor="normal"
|
||||
iconName="RiGridFill"
|
||||
/>
|
||||
<div className="hr"></div>
|
||||
|
||||
<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>
|
||||
<div className="hr"></div>
|
||||
<Flexbox
|
||||
children={List([
|
||||
<>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
|
@ -836,10 +927,49 @@ export class FilesPanel extends React.Component<Props, State, {}> {
|
|||
? "focus-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 = (
|
||||
<div>
|
||||
{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}>
|
||||
<Container>{ops}</Container>
|
||||
</div>
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
});
|
||||
|
|
|
@ -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": "请输入关键字,以空格分隔"
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue