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({
|
return this.do({
|
||||||
method: "get",
|
method: "get",
|
||||||
url: `${this.url}/v1/fs/search`,
|
url: `${this.url}/v1/fs/search`,
|
||||||
params: {
|
params,
|
||||||
[keywordQuery]: keyword,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>([]),
|
||||||
|
|
|
@ -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>
|
||||||
|
<Flexbox
|
||||||
|
children={List([
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={gotoRoot}
|
||||||
|
className="button-default margin-r-m"
|
||||||
|
>
|
||||||
{this.props.msg.pkg.get("endpoints.root")}
|
{this.props.msg.pkg.get("endpoints.root")}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={this.goHome} className="button-default">
|
<button onClick={this.goHome} className="button-default">
|
||||||
{this.props.msg.pkg.get("endpoints.home")}
|
{this.props.msg.pkg.get("endpoints.home")}
|
||||||
</button>
|
</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>
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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"
|
||||||
});
|
});
|
||||||
|
|
|
@ -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": "请输入关键字,以空格分隔"
|
||||||
});
|
});
|
||||||
|
|
|
@ -1181,12 +1181,17 @@ 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) {
|
||||||
|
if role == db.AdminRole ||
|
||||||
|
(role != db.AdminRole && strings.HasPrefix(pathname, userName)) {
|
||||||
results = append(results, pathname)
|
results = append(results, pathname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(200, &SearchItemsResp{Results: results})
|
c.JSON(200, &SearchItemsResp{Results: results})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue