1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-23 04:18:21 +00:00

Feature/sidepanel in search result (#7844)

* show sidepanel in search result

* show metadata details

* show library details

* optimize

* optimize

* optimize

---------

Co-authored-by: zhouwenxuan <aries@Mac.local>
This commit is contained in:
Aries
2025-05-28 21:29:46 +08:00
committed by GitHub
parent 1ef68b2a9c
commit 9f64c3ab8f
24 changed files with 364 additions and 39 deletions

View File

@@ -9,8 +9,9 @@ import { useMetadataStatus } from '../../../hooks';
import { Utils } from '../../../utils/utils'; import { Utils } from '../../../utils/utils';
import { SYSTEM_FOLDERS } from '../../../constants'; import { SYSTEM_FOLDERS } from '../../../constants';
const DirDetails = ({ direntDetail }) => { const DirDetails = ({ readOnly = false, direntDetail, tagsData }) => {
const { enableMetadata, enableMetadataManagement } = useMetadataStatus(); const { enableMetadata, enableMetadataManagement } = useMetadataStatus();
const lastModifiedTimeField = useMemo(() => { const lastModifiedTimeField = useMemo(() => {
return { type: CellType.MTIME, name: gettext('Last modified time') }; return { type: CellType.MTIME, name: gettext('Last modified time') };
}, []); }, []);
@@ -39,7 +40,7 @@ const DirDetails = ({ direntDetail }) => {
<Formatter field={CellType.TEXT} value={'--'} /> : <Formatter field={CellType.TEXT} value={'--'} /> :
<Formatter field={sizeField} value={size} />} <Formatter field={sizeField} value={size} />}
</DetailItem> </DetailItem>
<MetadataDetails /> <MetadataDetails readOnly={readOnly} tagsData={tagsData} />
</> </>
)} )}
<DetailItem field={lastModifiedTimeField} className="sf-metadata-property-detail-formatter"> <DetailItem field={lastModifiedTimeField} className="sf-metadata-property-detail-formatter">
@@ -50,7 +51,9 @@ const DirDetails = ({ direntDetail }) => {
}; };
DirDetails.propTypes = { DirDetails.propTypes = {
readOnly: PropTypes.bool,
direntDetail: PropTypes.object, direntDetail: PropTypes.object,
tagsData: PropTypes.object,
}; };
export default DirDetails; export default DirDetails;

View File

@@ -55,7 +55,7 @@ const getImageInfoValue = (key, value) => {
} }
}; };
const FileDetails = React.memo(({ repoID, dirent, path, direntDetail, isShowRepoTags = true, repoTags, fileTagList, onFileTagChanged }) => { const FileDetails = React.memo(({ repoID, dirent, path, direntDetail, isShowRepoTags = true, repoTags, fileTagList, readOnly = false, tagsData, onFileTagChanged }) => {
const [isCaptureInfoShow, setCaptureInfoShow] = useState(false); const [isCaptureInfoShow, setCaptureInfoShow] = useState(false);
const { enableFaceRecognition, enableMetadata } = useMetadataStatus(); const { enableFaceRecognition, enableMetadata } = useMetadataStatus();
const { record } = useMetadataDetails(); const { record } = useMetadataDetails();
@@ -102,7 +102,7 @@ const FileDetails = React.memo(({ repoID, dirent, path, direntDetail, isShowRepo
/> />
</DetailItem> </DetailItem>
)} )}
{enableMetadata && <MetadataDetails />} {enableMetadata && <MetadataDetails readOnly={readOnly} tagsData={tagsData} />}
</> </>
); );
@@ -162,6 +162,8 @@ FileDetails.propTypes = {
direntDetail: PropTypes.object, direntDetail: PropTypes.object,
repoTags: PropTypes.array, repoTags: PropTypes.array,
fileTagList: PropTypes.array, fileTagList: PropTypes.array,
readOnly: PropTypes.bool,
tagsData: PropTypes.object,
onFileTagChanged: PropTypes.func, onFileTagChanged: PropTypes.func,
}; };

View File

@@ -100,6 +100,7 @@ class DirentDetails extends React.Component {
dirent={dirent} dirent={dirent}
direntDetail={direntDetail} direntDetail={direntDetail}
direntType={dirent?.type !== 'file' ? 'dir' : 'file'} direntType={dirent?.type !== 'file' ? 'dir' : 'file'}
modifyLocalFileTags={this.props.modifyLocalFileTags}
> >
<Detail> <Detail>
<Header title={dirent?.name || ''} icon={Utils.getDirentIcon(dirent, true)} onClose={this.props.onClose} > <Header title={dirent?.name || ''} icon={Utils.getDirentIcon(dirent, true)} onClose={this.props.onClose} >
@@ -111,7 +112,11 @@ class DirentDetails extends React.Component {
{dirent && direntDetail && ( {dirent && direntDetail && (
<div className="detail-content"> <div className="detail-content">
{dirent.type !== 'file' ? ( {dirent.type !== 'file' ? (
<DirDetails direntDetail={direntDetail} /> <DirDetails
direntDetail={direntDetail}
tagsData={this.props.tagsData}
addTag={this.props.addTag}
/>
) : ( ) : (
<FileDetails <FileDetails
repoID={repoID} repoID={repoID}
@@ -120,6 +125,8 @@ class DirentDetails extends React.Component {
direntDetail={direntDetail} direntDetail={direntDetail}
repoTags={this.props.repoTags} repoTags={this.props.repoTags}
fileTagList={dirent ? dirent.file_tags : fileTags} fileTagList={dirent ? dirent.file_tags : fileTags}
tagsData={this.props.tagsData}
addTag={this.props.addTag}
onFileTagChanged={this.props.onFileTagChanged} onFileTagChanged={this.props.onFileTagChanged}
/> />
)} )}
@@ -141,6 +148,13 @@ DirentDetails.propTypes = {
onFileTagChanged: PropTypes.func.isRequired, onFileTagChanged: PropTypes.func.isRequired,
repoTags: PropTypes.array, repoTags: PropTypes.array,
fileTags: PropTypes.array, fileTags: PropTypes.array,
enableMetadata: PropTypes.bool,
enableFaceRecognition: PropTypes.bool,
detailsSettings: PropTypes.object,
tagsData: PropTypes.object,
addTag: PropTypes.func,
modifyDetailsSettings: PropTypes.func,
modifyLocalFileTags: PropTypes.func,
}; };
export default DirentDetails; export default DirentDetails;

View File

@@ -11,6 +11,7 @@ import { MetadataDetailsProvider } from '../../../metadata/hooks';
import AIIcon from '../../../metadata/components/metadata-details/ai-icon'; import AIIcon from '../../../metadata/components/metadata-details/ai-icon';
import SettingsIcon from '../../../metadata/components/metadata-details/settings-icon'; import SettingsIcon from '../../../metadata/components/metadata-details/settings-icon';
import Loading from '../../loading'; import Loading from '../../loading';
import { useTags } from '../../../tag/hooks';
import './index.css'; import './index.css';
@@ -21,6 +22,8 @@ const EmbeddedFileDetails = ({ repoID, repoInfo, dirent, path, onClose, width =
const [direntDetail, setDirentDetail] = useState(''); const [direntDetail, setDirentDetail] = useState('');
const [isFetching, setIsFetching] = useState(true); const [isFetching, setIsFetching] = useState(true);
const { tagsData, addTag } = useTags();
const isView = useMemo(() => { const isView = useMemo(() => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
return urlParams.has('view'); return urlParams.has('view');
@@ -87,7 +90,15 @@ const EmbeddedFileDetails = ({ repoID, repoInfo, dirent, path, onClose, width =
: :
dirent && direntDetail && ( dirent && direntDetail && (
<div className="detail-content"> <div className="detail-content">
<FileDetails repoID={repoID} isShowRepoTags={false} dirent={dirent} direntDetail={direntDetail} /> <FileDetails
repoID={repoID}
isShowRepoTags={false}
dirent={dirent}
direntDetail={direntDetail}
tagsData={tagsData}
addTag={addTag}
onFileTagChanged={() => {}}
/>
</div> </div>
)} )}
</Body> </Body>

View File

@@ -8,8 +8,13 @@ import { MetadataContext } from '../../metadata';
import { PRIVATE_FILE_TYPE } from '../../constants'; import { PRIVATE_FILE_TYPE } from '../../constants';
import { METADATA_MODE, TAGS_MODE } from '../dir-view-mode/constants'; import { METADATA_MODE, TAGS_MODE } from '../dir-view-mode/constants';
import { FACE_RECOGNITION_VIEW_ID } from '../../metadata/constants'; import { FACE_RECOGNITION_VIEW_ID } from '../../metadata/constants';
import { useTags } from '../../tag/hooks';
import { useMetadataStatus } from '../../hooks';
const Detail = React.memo(({ repoID, path, currentMode, dirent, currentRepoInfo, repoTags, fileTags, onClose, onFileTagChanged }) => { const Detail = React.memo(({ repoID, path, currentMode, dirent, currentRepoInfo, repoTags, fileTags, onClose, onFileTagChanged }) => {
const { enableMetadata, enableFaceRecognition, detailsSettings, modifyDetailsSettings } = useMetadataStatus();
const { tagsData, addTag, modifyLocalFileTags } = useTags();
const isView = useMemo(() => currentMode === METADATA_MODE || path.startsWith('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES), [currentMode, path]); const isView = useMemo(() => currentMode === METADATA_MODE || path.startsWith('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES), [currentMode, path]);
const isTag = useMemo(() => currentMode === TAGS_MODE || path.startsWith('/' + PRIVATE_FILE_TYPE.TAGS_PROPERTIES), [currentMode, path]); const isTag = useMemo(() => currentMode === TAGS_MODE || path.startsWith('/' + PRIVATE_FILE_TYPE.TAGS_PROPERTIES), [currentMode, path]);
@@ -53,6 +58,13 @@ const Detail = React.memo(({ repoID, path, currentMode, dirent, currentRepoInfo,
currentRepoInfo={currentRepoInfo} currentRepoInfo={currentRepoInfo}
repoTags={repoTags} repoTags={repoTags}
fileTags={fileTags} fileTags={fileTags}
enableMetadata={enableMetadata}
enableFaceRecognition={enableFaceRecognition}
detailsSettings={detailsSettings}
modifyDetailsSettings={modifyDetailsSettings}
tagsData={tagsData}
addTag={addTag}
modifyLocalFileTags={modifyLocalFileTags}
onFileTagChanged={onFileTagChanged} onFileTagChanged={onFileTagChanged}
onClose={onClose} onClose={onClose}
/> />

View File

@@ -73,7 +73,7 @@ const LibDetail = React.memo(({ currentRepoInfo, onClose }) => {
LibDetail.propTypes = { LibDetail.propTypes = {
currentRepoInfo: PropTypes.object.isRequired, currentRepoInfo: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func,
}; };
export default LibDetail; export default LibDetail;

View File

@@ -0,0 +1,90 @@
import PropTypes from 'prop-types';
import { Body, Header } from '../../dirent-detail/detail';
import { siteRoot, thumbnailSizeForGrid } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
import FileDetails from '../../dirent-detail/dirent-details/file-details';
import DirDetails from '../../dirent-detail/dirent-details/dir-details';
import { useEffect, useState } from 'react';
import { useMetadataStatus } from '../../../hooks';
import tagsAPI from '../../../tag/api';
import { PER_LOAD_NUMBER } from '../../../metadata/constants';
import { normalizeColumns } from '../../../tag/utils/column';
import { TAGS_DEFAULT_SORT } from '../../../tag/constants/sort';
import TagsData from '../../../tag/model/tagsData';
import toaster from '../../toast';
const Details = ({ repoID, repoInfo, path, dirent, direntDetail }) => {
const [tagsData, setTagsData] = useState(null);
const { enableMetadata, enableTags } = useMetadataStatus();
useEffect(() => {
if (enableMetadata && enableTags) {
tagsAPI.getTags(repoID, 0, PER_LOAD_NUMBER).then(res => {
const rows = res?.data?.results || [];
const columns = normalizeColumns(res?.data?.metadata);
const tagsData = new TagsData({ rows, columns, TAGS_DEFAULT_SORT });
setTagsData(tagsData);
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(errorMsg);
});
} else {
setTagsData(null);
}
}, [repoID, enableMetadata, enableTags]);
let src = '';
if (repoInfo.encrypted) {
src = `${siteRoot}repo/${repoID}/raw` + Utils.encodePath(`${path === '/' ? '' : path}/${dirent.name}`);
} else {
src = `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForGrid}` + Utils.encodePath(`${path === '/' ? '' : path}/${dirent.name}`) + '?mtime=' + direntDetail.mtime;
}
return (
<div className="searched-item-details">
<div
className="cur-view-detail"
style={{ width: 300 }}
>
<Header title={dirent?.name || ''} icon={Utils.getDirentIcon(dirent, true)}></Header>
<Body>
{Utils.imageCheck(dirent.name) && (
<div className="detail-image">
<img src={src} alt="" />
</div>
)}
<div className="detail-content">
{dirent.type !== 'file' ? (
<DirDetails
direntDetail={direntDetail}
readOnly={true}
tagsData={tagsData}
/>
) : (
<FileDetails
repoID={repoID}
dirent={dirent}
path={path}
direntDetail={direntDetail}
repoTags={[]}
fileTagList={dirent ? dirent.file_tags : []}
readOnly={true}
tagsData={tagsData}
onFileTagChanged={() => {}}
/>
)}
</div>
</Body>
</div>
</div>
);
};
Details.propTypes = {
repoID: PropTypes.string.isRequired,
repoInfo: PropTypes.object.isRequired,
path: PropTypes.string.isRequired,
dirent: PropTypes.object.isRequired,
direntDetail: PropTypes.object.isRequired,
};
export default Details;

View File

@@ -0,0 +1,4 @@
.searched-item-details .detail-header {
border: none;
padding: 8px;
}

View File

@@ -0,0 +1,103 @@
import { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Utils } from '../../../utils/utils';
import { seafileAPI } from '../../../utils/seafile-api';
import toaster from '../../toast';
import { MetadataDetailsProvider } from '../../../metadata';
import { Repo } from '../../../models';
import { MetadataStatusProvider } from '../../../hooks';
import Details from './details';
import LibDetail from '../../dirent-detail/lib-details';
import './index.css';
const SearchedItemDetails = ({ repoID, path, dirent }) => {
const [repoInfo, setRepoInfo] = useState(null);
const [direntDetail, setDirentDetail] = useState(null);
useEffect(() => {
seafileAPI.getRepoInfo(repoID).then(res => {
const repo = new Repo(res.data);
setRepoInfo(repo);
}).catch(error => {
const errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}, [repoID]);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
if (!repoID || !path || !dirent || dirent.isLib) {
setDirentDetail(null);
return;
}
try {
const res = await seafileAPI[dirent.type === 'file' ? 'getFileInfo' : 'getDirInfo'](
repoID,
path,
{ signal: controller.signal }
);
setDirentDetail(res.data);
} catch (error) {
if (error.name !== 'AbortError') {
const errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
}
}
};
const timer = setTimeout(fetchData, 200);
return () => {
controller.abort();
clearTimeout(timer);
};
}, [repoID, repoInfo, path, dirent]);
if (!repoInfo) return;
if (dirent.isLib) {
return (
<div className="searched-item-details">
<LibDetail currentRepoInfo={repoInfo} />
</div>
);
}
if (!direntDetail) return null;
let parentDir = path !== '/' && path.endsWith('/') ? path.slice(0, -1) : path; // deal with folder path comes from search results, eg: /folder/
parentDir = Utils.getDirName(parentDir);
return (
<MetadataStatusProvider key={repoID} repoID={repoID} repoInfo={repoInfo}>
<MetadataDetailsProvider
repoID={repoID}
repoInfo={repoInfo}
path={path !== '/' && path.endsWith('/') ? path.slice(0, -1) : path}
dirent={dirent}
direntDetail={direntDetail}
direntType={dirent?.type !== 'file' ? 'dir' : 'file'}
>
<Details
repoID={repoID}
repoInfo={repoInfo}
path={parentDir}
dirent={dirent}
direntDetail={direntDetail}
/>
</MetadataDetailsProvider>
</MetadataStatusProvider>
);
};
SearchedItemDetails.propTypes = {
repoID: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
dirent: PropTypes.object.isRequired,
};
export default SearchedItemDetails;

View File

@@ -5,17 +5,38 @@ import { Utils } from '../../utils/utils';
const propTypes = { const propTypes = {
item: PropTypes.object.isRequired, item: PropTypes.object.isRequired,
idx: PropTypes.number.isRequired,
onItemClickHandler: PropTypes.func.isRequired, onItemClickHandler: PropTypes.func.isRequired,
isHighlight: PropTypes.bool, isHighlight: PropTypes.bool,
setRef: PropTypes.func, setRef: PropTypes.func,
onHighlightIndex: PropTypes.func,
timer: PropTypes.number,
onSetTimer: PropTypes.func,
}; };
class SearchResultItem extends React.Component { class SearchResultItem extends React.Component {
constructor(props) {
super(props);
this.controller = null;
}
onClickHandler = () => { onClickHandler = () => {
this.props.onItemClickHandler(this.props.item); this.props.onItemClickHandler(this.props.item);
}; };
onMouseEnter = () => {
if (this.props.isHighlight) return;
if (this.controller) {
this.controller.abort();
}
this.controller = new AbortController();
if (this.props.onHighlightIndex) {
this.props.onHighlightIndex(this.props.idx);
}
};
render() { render() {
const { item, setRef = (() => {}) } = this.props; const { item, setRef = (() => {}) } = this.props;
let folderIconUrl = item.link_content ? Utils.getFolderIconUrl(false, 192) : Utils.getDefaultLibIconUrl(); let folderIconUrl = item.link_content ? Utils.getFolderIconUrl(false, 192) : Utils.getDefaultLibIconUrl();
@@ -32,6 +53,7 @@ class SearchResultItem extends React.Component {
className={classnames('search-result-item', { 'search-result-item-highlight': this.props.isHighlight })} className={classnames('search-result-item', { 'search-result-item-highlight': this.props.isHighlight })}
onClick={this.onClickHandler} onClick={this.onClickHandler}
ref={ref => setRef(ref)} ref={ref => setRef(ref)}
onMouseEnter={this.onMouseEnter}
> >
<img className={item.link_content ? 'item-img' : 'lib-item-img'} src={fileIconUrl} alt="" /> <img className={item.link_content ? 'item-img' : 'lib-item-img'} src={fileIconUrl} alt="" />
<div className="item-content"> <div className="item-content">

View File

@@ -8,7 +8,7 @@ import searchAPI from '../../utils/search-api';
import { gettext } from '../../utils/constants'; import { gettext } from '../../utils/constants';
import SearchResultItem from './search-result-item'; import SearchResultItem from './search-result-item';
import SearchResultLibrary from './search-result-library'; import SearchResultLibrary from './search-result-library';
import { Utils } from '../../utils/utils'; import { debounce, Utils } from '../../utils/utils';
import toaster from '../toast'; import toaster from '../toast';
import Loading from '../loading'; import Loading from '../loading';
import { SEARCH_MASK, SEARCH_CONTAINER } from '../../constants/zIndexes'; import { SEARCH_MASK, SEARCH_CONTAINER } from '../../constants/zIndexes';
@@ -16,6 +16,8 @@ import { PRIVATE_FILE_TYPE, SEARCH_FILTER_BY_DATE_OPTION_KEY, SEARCH_FILTER_BY_D
import SearchFilters from './search-filters'; import SearchFilters from './search-filters';
import SearchTags from './search-tags'; import SearchTags from './search-tags';
import IconBtn from '../icon-btn'; import IconBtn from '../icon-btn';
import SearchedItemDetails from './details';
import { CollaboratorsProvider } from '../../metadata';
const propTypes = { const propTypes = {
repoID: PropTypes.string, repoID: PropTypes.string,
@@ -75,6 +77,7 @@ class Search extends Component {
this.isChineseInput = false; this.isChineseInput = false;
this.searchResultListContainerRef = React.createRef(); this.searchResultListContainerRef = React.createRef();
this.calculateStoreKey(props); this.calculateStoreKey(props);
this.timer = null;
} }
componentDidMount() { componentDidMount() {
@@ -138,7 +141,7 @@ class Search extends Component {
}; };
onFocusHandler = () => { onFocusHandler = () => {
this.setState({ width: '570px', isMaskShow: true }); this.setState({ width: '100%', isMaskShow: true });
this.calculateHighlightType(); this.calculateHighlightType();
}; };
@@ -700,22 +703,49 @@ class Search extends Component {
this.getSearchResult(this.buildSearchParams(queryData)); this.getSearchResult(this.buildSearchParams(queryData));
}; };
renderDetails = (results) => {
const { repoID: currentRepoID } = this.props;
const { highlightIndex } = this.state;
const item = results[highlightIndex];
if (!item) return null;
const repoID = item.repo_id;
const isLib = !currentRepoID && item.path === '/';
const dirent = { name: item.name, type: item.is_dir ? 'dir' : 'file', isLib, file_tags: [], path: item.path };
return (
<CollaboratorsProvider repoID={repoID}>
<SearchedItemDetails repoID={repoID} path={item.path} dirent={dirent} />
</CollaboratorsProvider>
);
};
debounceHighlight = debounce((index) => {
this.setState({ highlightIndex: index });
}, 200);
renderResults = (resultItems, isVisited) => { renderResults = (resultItems, isVisited) => {
const { highlightIndex } = this.state; const { highlightIndex } = this.state;
const results = ( const results = (
<> <>
{isVisited && <h4 className="visited-search-results-title">{gettext('Search results visited recently')}</h4>} {isVisited ? (
<h4 className="visited-search-results-title">{gettext('Search results visited recently')}</h4>
) : (
<h4 className="search-results-title">{gettext('Files')}</h4>
)}
<ul className="search-result-list" ref={this.searchResultListRef}> <ul className="search-result-list" ref={this.searchResultListRef}>
{resultItems.map((item, index) => { {resultItems.map((item, index) => {
const isHighlight = index === highlightIndex; const isHighlight = index === highlightIndex;
return ( return (
<SearchResultItem <SearchResultItem
key={index} key={index}
idx={index}
item={item} item={item}
onItemClickHandler={this.onItemClickHandler} onItemClickHandler={this.onItemClickHandler}
isHighlight={isHighlight} isHighlight={isHighlight}
setRef={isHighlight ? (ref) => {this.highlightRef = ref;} : () => {}} setRef={isHighlight ? (ref) => {this.highlightRef = ref;} : () => {}}
onHighlightIndex={this.debounceHighlight}
timer={this.timer}
onSetTimer={(timer) => {this.timer = timer;}}
/> />
); );
})} })}
@@ -726,8 +756,12 @@ class Search extends Component {
return ( return (
<> <>
<MediaQuery query="(min-width: 768px)"> <MediaQuery query="(min-width: 768px)">
{!isVisited && <h4 className="search-results-title">{gettext('Files')}</h4>} <div className="search-result-sidepanel-wrapper d-flex">
<div className="search-result-list-container" ref={this.searchResultListContainerRef}>{results}</div> <div className="search-result-list-container" ref={this.searchResultListContainerRef}>{results}</div>
<div className="search-result-container-sidepanel d-flex flex-column flex-grow-1">
{this.renderDetails(resultItems)}
</div>
</div>
</MediaQuery> </MediaQuery>
<MediaQuery query="(max-width: 767.8px)"> <MediaQuery query="(max-width: 767.8px)">
{results} {results}

View File

@@ -22,7 +22,7 @@
box-shadow: 0 3px 8px 0 rgba(116, 129, 141, 0.1); box-shadow: 0 3px 8px 0 rgba(116, 129, 141, 0.1);
background-color: #fff; background-color: #fff;
cursor: default; cursor: default;
width: 600px; width: 700px;
padding: 16px 0; padding: 16px 0;
} }
@@ -112,8 +112,6 @@
} }
.dropdown-search-result-container { .dropdown-search-result-container {
max-height: 300px;
overflow: auto;
position: relative; position: relative;
top: 0; top: 0;
box-shadow: none; box-shadow: none;
@@ -134,6 +132,9 @@
.search-result-container .search-result-list-container { .search-result-container .search-result-list-container {
max-height: 400px;
display: flex;
flex-direction: column;
overflow: auto; overflow: auto;
scrollbar-color: #C1C1C1 rgba(0, 0, 0, 0); scrollbar-color: #C1C1C1 rgba(0, 0, 0, 0);
flex: 1; flex: 1;
@@ -149,11 +150,28 @@
border-radius: 4px; border-radius: 4px;
} }
.search-result-container .search-result-item:hover,
.search-result-container .search-result-item.search-result-item-highlight { .search-result-container .search-result-item.search-result-item-highlight {
background-color: #f0f0f0; background-color: #f0f0f0;
} }
.search-result-container .search-result-container-sidepanel {
max-width: 300px;
max-height: 400px;
}
.search-result-container .search-result-container-sidepanel .searched-item-details {
height: 100%;
overflow: auto;
}
.search-result-container .sf-metadata-status-loading-container {
width: 300px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.search-result-item .item-img { .search-result-item .item-img {
width: 36px; width: 36px;
height: 36px; height: 36px;

View File

@@ -126,11 +126,10 @@ export const MetadataStatusProvider = ({ repoID, repoInfo, hideMetadataView, sta
if (isLoading) { if (isLoading) {
return ( return (
<div style={{ width: '300px' }}> <div className="metadata-status-loading-container">
<Loading/> <Loading/>
</div> </div>
); );
} }
return ( return (

View File

@@ -4,9 +4,8 @@ import Formatter from '../formatter';
import FileName from './file-name'; import FileName from './file-name';
import { useCollaborators } from '../../hooks'; import { useCollaborators } from '../../hooks';
import { CellType } from '../../constants'; import { CellType } from '../../constants';
import { useTags } from '../../../tag/hooks';
const CellFormatter = ({ readonly, value, field, record, ...params }) => { const CellFormatter = ({ readonly, value, field, record, tagsData, ...params }) => {
const { collaborators, collaboratorsCache, updateCollaboratorsCache, queryUser } = useCollaborators(); const { collaborators, collaboratorsCache, updateCollaboratorsCache, queryUser } = useCollaborators();
const props = useMemo(() => { const props = useMemo(() => {
return { return {
@@ -20,7 +19,6 @@ const CellFormatter = ({ readonly, value, field, record, ...params }) => {
record, record,
}; };
}, [readonly, value, field, collaborators, collaboratorsCache, updateCollaboratorsCache, queryUser, record]); }, [readonly, value, field, collaborators, collaboratorsCache, updateCollaboratorsCache, queryUser, record]);
const { tagsData } = useTags();
if (field.type === CellType.FILE_NAME) { if (field.type === CellType.FILE_NAME) {
return (<FileName { ...props } { ...params } record={record} />); return (<FileName { ...props } { ...params } record={record} />);
@@ -36,6 +34,7 @@ CellFormatter.propTypes = {
value: PropTypes.any, value: PropTypes.any,
field: PropTypes.object.isRequired, field: PropTypes.object.isRequired,
record: PropTypes.object, record: PropTypes.object,
tagsData: PropTypes.object,
}; };
export default CellFormatter; export default CellFormatter;

View File

@@ -1,4 +1,5 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import CellFormatter from '../cell-formatter'; import CellFormatter from '../cell-formatter';
import DetailEditor from '../detail-editor'; import DetailEditor from '../detail-editor';
import DetailItem from '../../../components/dirent-detail/detail-item'; import DetailItem from '../../../components/dirent-detail/detail-item';
@@ -13,7 +14,7 @@ import Location from './location';
import './index.css'; import './index.css';
const MetadataDetails = () => { const MetadataDetails = ({ readOnly, tagsData }) => {
const { canModifyRecord, record, columns, onChange, modifyColumnData, updateFileTags } = useMetadataDetails(); const { canModifyRecord, record, columns, onChange, modifyColumnData, updateFileTags } = useMetadataDetails();
const displayColumns = useMemo(() => columns.filter(c => c.shown), [columns]); const displayColumns = useMemo(() => columns.filter(c => c.shown), [columns]);
@@ -34,7 +35,7 @@ const MetadataDetails = () => {
return <Location key={field.key} position={value} record={record} />; return <Location key={field.key} position={value} record={record} />;
} }
let canEdit = canModifyRecord && field.editable; let canEdit = canModifyRecord && field.editable && !readOnly;
if (!isImageOrVideo && IMAGE_PRIVATE_COLUMN_KEYS.includes(field.key)) { if (!isImageOrVideo && IMAGE_PRIVATE_COLUMN_KEYS.includes(field.key)) {
canEdit = false; canEdit = false;
} else if (field.key === PRIVATE_COLUMN_KEY.TAGS && isDir) { } else if (field.key === PRIVATE_COLUMN_KEY.TAGS && isDir) {
@@ -59,6 +60,7 @@ const MetadataDetails = () => {
value={value} value={value}
emptyTip={gettext('Empty')} emptyTip={gettext('Empty')}
className="sf-metadata-property-detail-formatter" className="sf-metadata-property-detail-formatter"
tagsData={tagsData}
/> />
} }
</DetailItem> </DetailItem>
@@ -68,4 +70,9 @@ const MetadataDetails = () => {
); );
}; };
MetadataDetails.propTypes = {
readOnly: PropTypes.bool,
tagsData: PropTypes.object,
};
export default MetadataDetails; export default MetadataDetails;

View File

@@ -41,8 +41,10 @@ class Location extends React.Component {
this.initMap(); this.initMap();
this.unsubscribeClearMapInstance = eventBus.subscribe(EVENT_BUS_TYPE.CLEAR_MAP_INSTANCE, () => { this.unsubscribeClearMapInstance = eventBus.subscribe(EVENT_BUS_TYPE.CLEAR_MAP_INSTANCE, () => {
if (window.mapInstance) {
window.mapInstance = null; window.mapInstance = null;
delete window.mapInstance; delete window.mapInstance;
}
}); });
} }

View File

@@ -9,16 +9,14 @@ import { normalizeFields } from '../components/metadata-details/utils';
import { CellType, EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY } from '../constants'; import { CellType, EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY } from '../constants';
import { getCellValueByColumn, getColumnOptionNamesByIds, getColumnOptionNameById, getRecordIdFromRecord, getServerOptions } from '../utils/cell'; import { getCellValueByColumn, getColumnOptionNamesByIds, getColumnOptionNameById, getRecordIdFromRecord, getServerOptions } from '../utils/cell';
import tagsAPI from '../../tag/api'; import tagsAPI from '../../tag/api';
import { useTags } from '../../tag/hooks';
import { getColumnByKey, getColumnOptions, getColumnOriginName } from '../utils/column'; import { getColumnByKey, getColumnOptions, getColumnOriginName } from '../utils/column';
import ObjectUtils from '../../utils/object'; import ObjectUtils from '../../utils/object';
import { NOT_DISPLAY_COLUMN_KEYS } from '../components/metadata-details/constants'; import { NOT_DISPLAY_COLUMN_KEYS } from '../components/metadata-details/constants';
const MetadataDetailsContext = React.createContext(null); const MetadataDetailsContext = React.createContext(null);
export const MetadataDetailsProvider = ({ repoID, repoInfo, path, dirent, direntDetail, direntType, children }) => { export const MetadataDetailsProvider = ({ repoID, repoInfo, path, dirent, direntDetail, direntType, modifyLocalFileTags, children }) => {
const { enableMetadata, detailsSettings, modifyDetailsSettings } = useMetadataStatus(); const { enableMetadata, detailsSettings, modifyDetailsSettings } = useMetadataStatus();
const { modifyLocalFileTags } = useTags();
const [isLoading, setLoading] = useState(true); const [isLoading, setLoading] = useState(true);
const [record, setRecord] = useState(null); const [record, setRecord] = useState(null);
@@ -137,7 +135,7 @@ export const MetadataDetailsProvider = ({ repoID, repoInfo, path, dirent, dirent
}, [repoID, record, modifyLocalFileTags]); }, [repoID, record, modifyLocalFileTags]);
const saveColumns = useCallback((columns) => { const saveColumns = useCallback((columns) => {
modifyDetailsSettings({ columns: columns.map(c => ({ key: c.key, shown: c.shown })) }); modifyDetailsSettings && modifyDetailsSettings({ columns: columns.map(c => ({ key: c.key, shown: c.shown })) });
}, [modifyDetailsSettings]); }, [modifyDetailsSettings]);
const modifyHiddenColumns = useCallback((hiddenColumns) => { const modifyHiddenColumns = useCallback((hiddenColumns) => {

View File

@@ -14,6 +14,7 @@ const Card = ({
record, record,
titleColumn, titleColumn,
displayColumns, displayColumns,
tagsData,
onOpenFile, onOpenFile,
onSelectCard, onSelectCard,
onContextMenu, onContextMenu,
@@ -45,7 +46,7 @@ const Card = ({
> >
{titleColumn && ( {titleColumn && (
<div className="sf-metadata-kanban-card-header"> <div className="sf-metadata-kanban-card-header">
<Formatter value={titleValue} column={titleColumn} record={record} onFileNameClick={handleFilenameClick} /> <Formatter value={titleValue} column={titleColumn} record={record} onFileNameClick={handleFilenameClick} tagsData={tagsData} />
</div> </div>
)} )}
<div className="sf-metadata-kanban-card-body"> <div className="sf-metadata-kanban-card-body">
@@ -65,7 +66,7 @@ const Card = ({
return ( return (
<div className="sf-metadata-kanban-card-record" key={column.key}> <div className="sf-metadata-kanban-card-record" key={column.key}>
{displayColumnName && (<div className="sf-metadata-kanban-card-record-name">{column.name}</div>)} {displayColumnName && (<div className="sf-metadata-kanban-card-record-name">{column.name}</div>)}
<Formatter value={value} column={column} record={record}/> <Formatter value={value} column={column} record={record} tagsData={tagsData} />
</div> </div>
); );
})} })}

View File

@@ -14,7 +14,7 @@ const SPECIAL_FILE_ICON = [
'word.png', 'word.png',
]; ];
const Formatter = ({ value, column, record, ...params }) => { const Formatter = ({ value, column, record, tagsData, ...params }) => {
let className = ''; let className = '';
if (column.type === CellType.FILE_NAME && value) { if (column.type === CellType.FILE_NAME && value) {
@@ -24,7 +24,7 @@ const Formatter = ({ value, column, record, ...params }) => {
} }
} }
return (<CellFormatter { ...params } readonly={true} className={className} value={value} field={column} record={record} />); return (<CellFormatter { ...params } readonly={true} className={className} value={value} field={column} record={record} tagsData={tagsData} />);
}; };
Formatter.propTypes = { Formatter.propTypes = {

View File

@@ -8,7 +8,7 @@ import { CellType } from '../../../../../constants';
import './index.css'; import './index.css';
const Header = ({ readonly, haveFreezed, value, groupByColumn, cardsQuantity, onDelete, onFreezed, onUnFreezed, isCollapsed, onCollapse }) => { const Header = ({ readonly, haveFreezed, value, groupByColumn, cardsQuantity, tagsData, onDelete, onFreezed, onUnFreezed, isCollapsed, onCollapse }) => {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
@@ -48,7 +48,7 @@ const Header = ({ readonly, haveFreezed, value, groupByColumn, cardsQuantity, on
<div className="sf-metadata-view-kanban-board-header" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> <div className="sf-metadata-view-kanban-board-header" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<div className="sf-metadata-view-kanban-board-header-title" ref={headerRef}> <div className="sf-metadata-view-kanban-board-header-title" ref={headerRef}>
{value ? ( {value ? (
<CellFormatter value={titleValue} field={groupByColumn} readonly={true} /> <CellFormatter value={titleValue} field={groupByColumn} readonly={true} tagsData={tagsData} />
) : ( ) : (
<span>{gettext('Uncategorized')}</span> <span>{gettext('Uncategorized')}</span>
)} )}

View File

@@ -8,6 +8,7 @@ import { useMetadataView } from '../../../../hooks/metadata-view';
import { getRowById } from '../../../../../components/sf-table/utils/table'; import { getRowById } from '../../../../../components/sf-table/utils/table';
import { getRecordIdFromRecord } from '../../../../utils/cell'; import { getRecordIdFromRecord } from '../../../../utils/cell';
import { gettext } from '@/utils/constants'; import { gettext } from '@/utils/constants';
import { useTags } from '../../../../../tag/hooks';
import './index.css'; import './index.css';
@@ -37,6 +38,7 @@ const Board = ({
const cardsQuantity = useMemo(() => board.children.length, [board.children]); const cardsQuantity = useMemo(() => board.children.length, [board.children]);
const { metadata } = useMetadataView(); const { metadata } = useMetadataView();
const { tagsData } = useTags();
const onDragStart = useCallback(({ payload }) => { const onDragStart = useCallback(({ payload }) => {
updateDragging(true); updateDragging(true);
@@ -68,6 +70,7 @@ const Board = ({
groupByColumn={groupByColumn} groupByColumn={groupByColumn}
haveFreezed={haveFreezed} haveFreezed={haveFreezed}
cardsQuantity={cardsQuantity} cardsQuantity={cardsQuantity}
tagsData={tagsData}
onDelete={() => deleteOption(board.key)} onDelete={() => deleteOption(board.key)}
onFreezed={onFreezed} onFreezed={onFreezed}
onUnFreezed={onUnFreezed} onUnFreezed={onUnFreezed}
@@ -114,6 +117,7 @@ const Board = ({
record={record} record={record}
titleColumn={titleColumn} titleColumn={titleColumn}
displayColumns={displayColumns} displayColumns={displayColumns}
tagsData={tagsData}
onOpenFile={onOpenFile} onOpenFile={onOpenFile}
onSelectCard={onSelectCard} onSelectCard={onSelectCard}
onContextMenu={(e) => onContextMenu(e, recordId)} onContextMenu={(e) => onContextMenu(e, recordId)}

View File

@@ -6,7 +6,7 @@ import RateEditor from '../../../../../../components/cell-editors/rate-editor';
import { canEditCell } from '../../../../../../utils/column'; import { canEditCell } from '../../../../../../utils/column';
import { CellType } from '../../../../../../constants'; import { CellType } from '../../../../../../constants';
const Formatter = ({ isCellSelected, field, value, onChange, record, ...params }) => { const Formatter = ({ isCellSelected, field, value, onChange, record, tagsData, ...params }) => {
const { type } = field; const { type } = field;
const cellEditAble = canEditCell(field, record, true); const cellEditAble = canEditCell(field, record, true);
if (type === CellType.CHECKBOX && cellEditAble) { if (type === CellType.CHECKBOX && cellEditAble) {
@@ -16,7 +16,7 @@ const Formatter = ({ isCellSelected, field, value, onChange, record, ...params }
return (<RateEditor isCellSelected={isCellSelected} value={value} field={field} onChange={onChange} />); return (<RateEditor isCellSelected={isCellSelected} value={value} field={field} onChange={onChange} />);
} }
return (<CellFormatter { ...params } readonly={true} value={value} field={field} record={record} />); return (<CellFormatter { ...params } readonly={true} value={value} field={field} record={record} tagsData={tagsData} />);
}; };
Formatter.propTypes = { Formatter.propTypes = {

View File

@@ -9,6 +9,7 @@ import { isCellValueChanged, getCellValueByColumn } from '../../../../../../util
import { CellType, PRIVATE_COLUMN_KEYS, TABLE_SUPPORT_EDIT_TYPE_MAP, EDITOR_TYPE, EVENT_BUS_TYPE } from '../../../../../../constants'; import { CellType, PRIVATE_COLUMN_KEYS, TABLE_SUPPORT_EDIT_TYPE_MAP, EDITOR_TYPE, EVENT_BUS_TYPE } from '../../../../../../constants';
import { checkIsDir } from '../../../../../../utils/row'; import { checkIsDir } from '../../../../../../utils/row';
import { openFile } from '../../../../../../utils/file'; import { openFile } from '../../../../../../utils/file';
import { useTags } from '../../../../../../../tag/hooks';
import './index.css'; import './index.css';
@@ -27,6 +28,7 @@ const Cell = React.memo(({
frozen, frozen,
height, height,
}) => { }) => {
const { tagsData } = useTags();
const canEditable = useMemo(() => { const canEditable = useMemo(() => {
const { type } = column; const { type } = column;
if (!window.sfMetadataContext.canModifyColumn(column)) return false; if (!window.sfMetadataContext.canModifyColumn(column)) return false;
@@ -171,7 +173,7 @@ const Cell = React.memo(({
return ( return (
<div key={`${record._id}-${column.key}`} {...containerProps}> <div key={`${record._id}-${column.key}`} {...containerProps}>
<Formatter isCellSelected={isCellSelected} value={cellValue} field={column} onChange={modifyRecord} record={record} onFileNameClick={onFileNameClick} /> <Formatter isCellSelected={isCellSelected} value={cellValue} field={column} onChange={modifyRecord} record={record} tagsData={tagsData} onFileNameClick={onFileNameClick} />
{isCellSelected && (<CellOperationBtn record={record} column={column}/>)} {isCellSelected && (<CellOperationBtn record={record} column={column}/>)}
</div> </div>
); );

View File

@@ -31,7 +31,7 @@ export const normalizeColumns = (columns) => {
); );
} }
const keyColumnWidth = window.sfTagsDataContext.localStorage.getItem('columns_width') || {}; const keyColumnWidth = window.sfTagsDataContext?.localStorage?.getItem('columns_width') || {};
return normalizedColumns.map((column) => { return normalizedColumns.map((column) => {
const { key } = column; const { key } = column;
let width = keyColumnWidth[column.key]; let width = keyColumnWidth[column.key];