mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-23 12:27:48 +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:
@@ -9,8 +9,9 @@ import { useMetadataStatus } from '../../../hooks';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import { SYSTEM_FOLDERS } from '../../../constants';
|
||||
|
||||
const DirDetails = ({ direntDetail }) => {
|
||||
const DirDetails = ({ readOnly = false, direntDetail, tagsData }) => {
|
||||
const { enableMetadata, enableMetadataManagement } = useMetadataStatus();
|
||||
|
||||
const lastModifiedTimeField = useMemo(() => {
|
||||
return { type: CellType.MTIME, name: gettext('Last modified time') };
|
||||
}, []);
|
||||
@@ -39,7 +40,7 @@ const DirDetails = ({ direntDetail }) => {
|
||||
<Formatter field={CellType.TEXT} value={'--'} /> :
|
||||
<Formatter field={sizeField} value={size} />}
|
||||
</DetailItem>
|
||||
<MetadataDetails />
|
||||
<MetadataDetails readOnly={readOnly} tagsData={tagsData} />
|
||||
</>
|
||||
)}
|
||||
<DetailItem field={lastModifiedTimeField} className="sf-metadata-property-detail-formatter">
|
||||
@@ -50,7 +51,9 @@ const DirDetails = ({ direntDetail }) => {
|
||||
};
|
||||
|
||||
DirDetails.propTypes = {
|
||||
readOnly: PropTypes.bool,
|
||||
direntDetail: PropTypes.object,
|
||||
tagsData: PropTypes.object,
|
||||
};
|
||||
|
||||
export default DirDetails;
|
||||
|
@@ -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 { enableFaceRecognition, enableMetadata } = useMetadataStatus();
|
||||
const { record } = useMetadataDetails();
|
||||
@@ -102,7 +102,7 @@ const FileDetails = React.memo(({ repoID, dirent, path, direntDetail, isShowRepo
|
||||
/>
|
||||
</DetailItem>
|
||||
)}
|
||||
{enableMetadata && <MetadataDetails />}
|
||||
{enableMetadata && <MetadataDetails readOnly={readOnly} tagsData={tagsData} />}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -162,6 +162,8 @@ FileDetails.propTypes = {
|
||||
direntDetail: PropTypes.object,
|
||||
repoTags: PropTypes.array,
|
||||
fileTagList: PropTypes.array,
|
||||
readOnly: PropTypes.bool,
|
||||
tagsData: PropTypes.object,
|
||||
onFileTagChanged: PropTypes.func,
|
||||
};
|
||||
|
||||
|
@@ -100,6 +100,7 @@ class DirentDetails extends React.Component {
|
||||
dirent={dirent}
|
||||
direntDetail={direntDetail}
|
||||
direntType={dirent?.type !== 'file' ? 'dir' : 'file'}
|
||||
modifyLocalFileTags={this.props.modifyLocalFileTags}
|
||||
>
|
||||
<Detail>
|
||||
<Header title={dirent?.name || ''} icon={Utils.getDirentIcon(dirent, true)} onClose={this.props.onClose} >
|
||||
@@ -111,7 +112,11 @@ class DirentDetails extends React.Component {
|
||||
{dirent && direntDetail && (
|
||||
<div className="detail-content">
|
||||
{dirent.type !== 'file' ? (
|
||||
<DirDetails direntDetail={direntDetail} />
|
||||
<DirDetails
|
||||
direntDetail={direntDetail}
|
||||
tagsData={this.props.tagsData}
|
||||
addTag={this.props.addTag}
|
||||
/>
|
||||
) : (
|
||||
<FileDetails
|
||||
repoID={repoID}
|
||||
@@ -120,6 +125,8 @@ class DirentDetails extends React.Component {
|
||||
direntDetail={direntDetail}
|
||||
repoTags={this.props.repoTags}
|
||||
fileTagList={dirent ? dirent.file_tags : fileTags}
|
||||
tagsData={this.props.tagsData}
|
||||
addTag={this.props.addTag}
|
||||
onFileTagChanged={this.props.onFileTagChanged}
|
||||
/>
|
||||
)}
|
||||
@@ -141,6 +148,13 @@ DirentDetails.propTypes = {
|
||||
onFileTagChanged: PropTypes.func.isRequired,
|
||||
repoTags: 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;
|
||||
|
@@ -11,6 +11,7 @@ import { MetadataDetailsProvider } from '../../../metadata/hooks';
|
||||
import AIIcon from '../../../metadata/components/metadata-details/ai-icon';
|
||||
import SettingsIcon from '../../../metadata/components/metadata-details/settings-icon';
|
||||
import Loading from '../../loading';
|
||||
import { useTags } from '../../../tag/hooks';
|
||||
|
||||
import './index.css';
|
||||
|
||||
@@ -21,6 +22,8 @@ const EmbeddedFileDetails = ({ repoID, repoInfo, dirent, path, onClose, width =
|
||||
const [direntDetail, setDirentDetail] = useState('');
|
||||
const [isFetching, setIsFetching] = useState(true);
|
||||
|
||||
const { tagsData, addTag } = useTags();
|
||||
|
||||
const isView = useMemo(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.has('view');
|
||||
@@ -87,7 +90,15 @@ const EmbeddedFileDetails = ({ repoID, repoInfo, dirent, path, onClose, width =
|
||||
:
|
||||
dirent && direntDetail && (
|
||||
<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>
|
||||
)}
|
||||
</Body>
|
||||
|
@@ -8,8 +8,13 @@ import { MetadataContext } from '../../metadata';
|
||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||
import { METADATA_MODE, TAGS_MODE } from '../dir-view-mode/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 { 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 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}
|
||||
repoTags={repoTags}
|
||||
fileTags={fileTags}
|
||||
enableMetadata={enableMetadata}
|
||||
enableFaceRecognition={enableFaceRecognition}
|
||||
detailsSettings={detailsSettings}
|
||||
modifyDetailsSettings={modifyDetailsSettings}
|
||||
tagsData={tagsData}
|
||||
addTag={addTag}
|
||||
modifyLocalFileTags={modifyLocalFileTags}
|
||||
onFileTagChanged={onFileTagChanged}
|
||||
onClose={onClose}
|
||||
/>
|
||||
|
@@ -73,7 +73,7 @@ const LibDetail = React.memo(({ currentRepoInfo, onClose }) => {
|
||||
|
||||
LibDetail.propTypes = {
|
||||
currentRepoInfo: PropTypes.object.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
export default LibDetail;
|
||||
|
90
frontend/src/components/search/details/details.js
Normal file
90
frontend/src/components/search/details/details.js
Normal 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;
|
4
frontend/src/components/search/details/index.css
Normal file
4
frontend/src/components/search/details/index.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.searched-item-details .detail-header {
|
||||
border: none;
|
||||
padding: 8px;
|
||||
}
|
103
frontend/src/components/search/details/index.js
Normal file
103
frontend/src/components/search/details/index.js
Normal 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;
|
||||
|
@@ -5,17 +5,38 @@ import { Utils } from '../../utils/utils';
|
||||
|
||||
const propTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
idx: PropTypes.number.isRequired,
|
||||
onItemClickHandler: PropTypes.func.isRequired,
|
||||
isHighlight: PropTypes.bool,
|
||||
setRef: PropTypes.func,
|
||||
onHighlightIndex: PropTypes.func,
|
||||
timer: PropTypes.number,
|
||||
onSetTimer: PropTypes.func,
|
||||
};
|
||||
|
||||
class SearchResultItem extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.controller = null;
|
||||
}
|
||||
|
||||
onClickHandler = () => {
|
||||
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() {
|
||||
const { item, setRef = (() => {}) } = this.props;
|
||||
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 })}
|
||||
onClick={this.onClickHandler}
|
||||
ref={ref => setRef(ref)}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
>
|
||||
<img className={item.link_content ? 'item-img' : 'lib-item-img'} src={fileIconUrl} alt="" />
|
||||
<div className="item-content">
|
||||
|
@@ -8,7 +8,7 @@ import searchAPI from '../../utils/search-api';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import SearchResultItem from './search-result-item';
|
||||
import SearchResultLibrary from './search-result-library';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { debounce, Utils } from '../../utils/utils';
|
||||
import toaster from '../toast';
|
||||
import Loading from '../loading';
|
||||
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 SearchTags from './search-tags';
|
||||
import IconBtn from '../icon-btn';
|
||||
import SearchedItemDetails from './details';
|
||||
import { CollaboratorsProvider } from '../../metadata';
|
||||
|
||||
const propTypes = {
|
||||
repoID: PropTypes.string,
|
||||
@@ -75,6 +77,7 @@ class Search extends Component {
|
||||
this.isChineseInput = false;
|
||||
this.searchResultListContainerRef = React.createRef();
|
||||
this.calculateStoreKey(props);
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -138,7 +141,7 @@ class Search extends Component {
|
||||
};
|
||||
|
||||
onFocusHandler = () => {
|
||||
this.setState({ width: '570px', isMaskShow: true });
|
||||
this.setState({ width: '100%', isMaskShow: true });
|
||||
this.calculateHighlightType();
|
||||
};
|
||||
|
||||
@@ -700,22 +703,49 @@ class Search extends Component {
|
||||
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) => {
|
||||
const { highlightIndex } = this.state;
|
||||
|
||||
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}>
|
||||
{resultItems.map((item, index) => {
|
||||
const isHighlight = index === highlightIndex;
|
||||
return (
|
||||
<SearchResultItem
|
||||
key={index}
|
||||
idx={index}
|
||||
item={item}
|
||||
onItemClickHandler={this.onItemClickHandler}
|
||||
isHighlight={isHighlight}
|
||||
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 (
|
||||
<>
|
||||
<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-container-sidepanel d-flex flex-column flex-grow-1">
|
||||
{this.renderDetails(resultItems)}
|
||||
</div>
|
||||
</div>
|
||||
</MediaQuery>
|
||||
<MediaQuery query="(max-width: 767.8px)">
|
||||
{results}
|
||||
|
@@ -22,7 +22,7 @@
|
||||
box-shadow: 0 3px 8px 0 rgba(116, 129, 141, 0.1);
|
||||
background-color: #fff;
|
||||
cursor: default;
|
||||
width: 600px;
|
||||
width: 700px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
@@ -112,8 +112,6 @@
|
||||
}
|
||||
|
||||
.dropdown-search-result-container {
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
top: 0;
|
||||
box-shadow: none;
|
||||
@@ -134,6 +132,9 @@
|
||||
|
||||
|
||||
.search-result-container .search-result-list-container {
|
||||
max-height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
scrollbar-color: #C1C1C1 rgba(0, 0, 0, 0);
|
||||
flex: 1;
|
||||
@@ -149,11 +150,28 @@
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.search-result-container .search-result-item:hover,
|
||||
.search-result-container .search-result-item.search-result-item-highlight {
|
||||
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 {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
|
@@ -126,11 +126,10 @@ export const MetadataStatusProvider = ({ repoID, repoInfo, hideMetadataView, sta
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div style={{ width: '300px' }}>
|
||||
<div className="metadata-status-loading-container">
|
||||
<Loading/>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
|
@@ -4,9 +4,8 @@ import Formatter from '../formatter';
|
||||
import FileName from './file-name';
|
||||
import { useCollaborators } from '../../hooks';
|
||||
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 props = useMemo(() => {
|
||||
return {
|
||||
@@ -20,7 +19,6 @@ const CellFormatter = ({ readonly, value, field, record, ...params }) => {
|
||||
record,
|
||||
};
|
||||
}, [readonly, value, field, collaborators, collaboratorsCache, updateCollaboratorsCache, queryUser, record]);
|
||||
const { tagsData } = useTags();
|
||||
|
||||
if (field.type === CellType.FILE_NAME) {
|
||||
return (<FileName { ...props } { ...params } record={record} />);
|
||||
@@ -36,6 +34,7 @@ CellFormatter.propTypes = {
|
||||
value: PropTypes.any,
|
||||
field: PropTypes.object.isRequired,
|
||||
record: PropTypes.object,
|
||||
tagsData: PropTypes.object,
|
||||
};
|
||||
|
||||
export default CellFormatter;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import CellFormatter from '../cell-formatter';
|
||||
import DetailEditor from '../detail-editor';
|
||||
import DetailItem from '../../../components/dirent-detail/detail-item';
|
||||
@@ -13,7 +14,7 @@ import Location from './location';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const MetadataDetails = () => {
|
||||
const MetadataDetails = ({ readOnly, tagsData }) => {
|
||||
const { canModifyRecord, record, columns, onChange, modifyColumnData, updateFileTags } = useMetadataDetails();
|
||||
|
||||
const displayColumns = useMemo(() => columns.filter(c => c.shown), [columns]);
|
||||
@@ -34,7 +35,7 @@ const MetadataDetails = () => {
|
||||
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)) {
|
||||
canEdit = false;
|
||||
} else if (field.key === PRIVATE_COLUMN_KEY.TAGS && isDir) {
|
||||
@@ -59,6 +60,7 @@ const MetadataDetails = () => {
|
||||
value={value}
|
||||
emptyTip={gettext('Empty')}
|
||||
className="sf-metadata-property-detail-formatter"
|
||||
tagsData={tagsData}
|
||||
/>
|
||||
}
|
||||
</DetailItem>
|
||||
@@ -68,4 +70,9 @@ const MetadataDetails = () => {
|
||||
);
|
||||
};
|
||||
|
||||
MetadataDetails.propTypes = {
|
||||
readOnly: PropTypes.bool,
|
||||
tagsData: PropTypes.object,
|
||||
};
|
||||
|
||||
export default MetadataDetails;
|
||||
|
@@ -41,8 +41,10 @@ class Location extends React.Component {
|
||||
this.initMap();
|
||||
|
||||
this.unsubscribeClearMapInstance = eventBus.subscribe(EVENT_BUS_TYPE.CLEAR_MAP_INSTANCE, () => {
|
||||
if (window.mapInstance) {
|
||||
window.mapInstance = null;
|
||||
delete window.mapInstance;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -9,16 +9,14 @@ import { normalizeFields } from '../components/metadata-details/utils';
|
||||
import { CellType, EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY } from '../constants';
|
||||
import { getCellValueByColumn, getColumnOptionNamesByIds, getColumnOptionNameById, getRecordIdFromRecord, getServerOptions } from '../utils/cell';
|
||||
import tagsAPI from '../../tag/api';
|
||||
import { useTags } from '../../tag/hooks';
|
||||
import { getColumnByKey, getColumnOptions, getColumnOriginName } from '../utils/column';
|
||||
import ObjectUtils from '../../utils/object';
|
||||
import { NOT_DISPLAY_COLUMN_KEYS } from '../components/metadata-details/constants';
|
||||
|
||||
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 { modifyLocalFileTags } = useTags();
|
||||
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
const [record, setRecord] = useState(null);
|
||||
@@ -137,7 +135,7 @@ export const MetadataDetailsProvider = ({ repoID, repoInfo, path, dirent, dirent
|
||||
}, [repoID, record, modifyLocalFileTags]);
|
||||
|
||||
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]);
|
||||
|
||||
const modifyHiddenColumns = useCallback((hiddenColumns) => {
|
||||
|
@@ -14,6 +14,7 @@ const Card = ({
|
||||
record,
|
||||
titleColumn,
|
||||
displayColumns,
|
||||
tagsData,
|
||||
onOpenFile,
|
||||
onSelectCard,
|
||||
onContextMenu,
|
||||
@@ -45,7 +46,7 @@ const Card = ({
|
||||
>
|
||||
{titleColumn && (
|
||||
<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 className="sf-metadata-kanban-card-body">
|
||||
@@ -65,7 +66,7 @@ const Card = ({
|
||||
return (
|
||||
<div className="sf-metadata-kanban-card-record" key={column.key}>
|
||||
{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>
|
||||
);
|
||||
})}
|
||||
|
@@ -14,7 +14,7 @@ const SPECIAL_FILE_ICON = [
|
||||
'word.png',
|
||||
];
|
||||
|
||||
const Formatter = ({ value, column, record, ...params }) => {
|
||||
const Formatter = ({ value, column, record, tagsData, ...params }) => {
|
||||
let className = '';
|
||||
|
||||
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 = {
|
||||
|
@@ -8,7 +8,7 @@ import { CellType } from '../../../../../constants';
|
||||
|
||||
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
|
||||
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-title" ref={headerRef}>
|
||||
{value ? (
|
||||
<CellFormatter value={titleValue} field={groupByColumn} readonly={true} />
|
||||
<CellFormatter value={titleValue} field={groupByColumn} readonly={true} tagsData={tagsData} />
|
||||
) : (
|
||||
<span>{gettext('Uncategorized')}</span>
|
||||
)}
|
||||
|
@@ -8,6 +8,7 @@ import { useMetadataView } from '../../../../hooks/metadata-view';
|
||||
import { getRowById } from '../../../../../components/sf-table/utils/table';
|
||||
import { getRecordIdFromRecord } from '../../../../utils/cell';
|
||||
import { gettext } from '@/utils/constants';
|
||||
import { useTags } from '../../../../../tag/hooks';
|
||||
|
||||
import './index.css';
|
||||
|
||||
@@ -37,6 +38,7 @@ const Board = ({
|
||||
const cardsQuantity = useMemo(() => board.children.length, [board.children]);
|
||||
|
||||
const { metadata } = useMetadataView();
|
||||
const { tagsData } = useTags();
|
||||
|
||||
const onDragStart = useCallback(({ payload }) => {
|
||||
updateDragging(true);
|
||||
@@ -68,6 +70,7 @@ const Board = ({
|
||||
groupByColumn={groupByColumn}
|
||||
haveFreezed={haveFreezed}
|
||||
cardsQuantity={cardsQuantity}
|
||||
tagsData={tagsData}
|
||||
onDelete={() => deleteOption(board.key)}
|
||||
onFreezed={onFreezed}
|
||||
onUnFreezed={onUnFreezed}
|
||||
@@ -114,6 +117,7 @@ const Board = ({
|
||||
record={record}
|
||||
titleColumn={titleColumn}
|
||||
displayColumns={displayColumns}
|
||||
tagsData={tagsData}
|
||||
onOpenFile={onOpenFile}
|
||||
onSelectCard={onSelectCard}
|
||||
onContextMenu={(e) => onContextMenu(e, recordId)}
|
||||
|
@@ -6,7 +6,7 @@ import RateEditor from '../../../../../../components/cell-editors/rate-editor';
|
||||
import { canEditCell } from '../../../../../../utils/column';
|
||||
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 cellEditAble = canEditCell(field, record, true);
|
||||
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 (<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 = {
|
||||
|
@@ -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 { checkIsDir } from '../../../../../../utils/row';
|
||||
import { openFile } from '../../../../../../utils/file';
|
||||
import { useTags } from '../../../../../../../tag/hooks';
|
||||
|
||||
import './index.css';
|
||||
|
||||
@@ -27,6 +28,7 @@ const Cell = React.memo(({
|
||||
frozen,
|
||||
height,
|
||||
}) => {
|
||||
const { tagsData } = useTags();
|
||||
const canEditable = useMemo(() => {
|
||||
const { type } = column;
|
||||
if (!window.sfMetadataContext.canModifyColumn(column)) return false;
|
||||
@@ -171,7 +173,7 @@ const Cell = React.memo(({
|
||||
|
||||
return (
|
||||
<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}/>)}
|
||||
</div>
|
||||
);
|
||||
|
@@ -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) => {
|
||||
const { key } = column;
|
||||
let width = keyColumnWidth[column.key];
|
||||
|
Reference in New Issue
Block a user