mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-04 16:31:13 +00:00
fix: tag files ui (#7095)
* fix: tag files ui * feat: optimize code * feat: optimize code * feat: optimize code * feat: optimize code * feat: optimize ui * feat: optimize ui * feat: optimize ui --------- Co-authored-by: 杨国璇 <ygx@Hello-word.local> Co-authored-by: 杨国璇 <ygx@192.168.1.2>
This commit is contained in:
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import DirentNoneView from '../../components/dirent-list-view/dirent-none-view';
|
import DirentNoneView from '../../components/dirent-list-view/dirent-none-view';
|
||||||
import RepoInfoBar from '../../components/repo-info-bar';
|
import RepoInfoBar from '../../components/repo-info-bar';
|
||||||
import DirentListView from '../../components/dirent-list-view/dirent-list-view';
|
import DirentListView from '../../components/dirent-list-view/dirent-list-view';
|
||||||
|
import Loading from '../loading';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
path: PropTypes.string.isRequired,
|
path: PropTypes.string.isRequired,
|
||||||
@@ -71,43 +72,46 @@ class DirListView extends React.Component {
|
|||||||
onFileTagChanged={this.props.onFileTagChanged}
|
onFileTagChanged={this.props.onFileTagChanged}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<DirentListView
|
{this.props.isDirentListLoading ? (
|
||||||
path={this.props.path}
|
<Loading />
|
||||||
currentRepoInfo={this.props.currentRepoInfo}
|
) : (
|
||||||
repoID={this.props.repoID}
|
<DirentListView
|
||||||
isGroupOwnedRepo={this.props.isGroupOwnedRepo}
|
path={this.props.path}
|
||||||
userPerm={this.props.userPerm}
|
currentRepoInfo={this.props.currentRepoInfo}
|
||||||
enableDirPrivateShare={this.props.enableDirPrivateShare}
|
repoID={this.props.repoID}
|
||||||
direntList={this.props.direntList}
|
isGroupOwnedRepo={this.props.isGroupOwnedRepo}
|
||||||
fullDirentList={this.props.fullDirentList}
|
userPerm={this.props.userPerm}
|
||||||
sortBy={this.props.sortBy}
|
enableDirPrivateShare={this.props.enableDirPrivateShare}
|
||||||
sortOrder={this.props.sortOrder}
|
direntList={this.props.direntList}
|
||||||
sortItems={this.props.sortItems}
|
fullDirentList={this.props.fullDirentList}
|
||||||
onItemClick={this.props.onItemClick}
|
sortBy={this.props.sortBy}
|
||||||
onItemSelected={this.props.onItemSelected}
|
sortOrder={this.props.sortOrder}
|
||||||
onItemDelete={this.props.onItemDelete}
|
sortItems={this.props.sortItems}
|
||||||
onItemRename={this.props.onItemRename}
|
onItemClick={this.props.onItemClick}
|
||||||
onItemMove={this.props.onItemMove}
|
onItemSelected={this.props.onItemSelected}
|
||||||
onItemCopy={this.props.onItemCopy}
|
onItemDelete={this.props.onItemDelete}
|
||||||
onDirentClick={this.props.onDirentClick}
|
onItemRename={this.props.onItemRename}
|
||||||
isDirentListLoading={this.props.isDirentListLoading}
|
onItemMove={this.props.onItemMove}
|
||||||
updateDirent={this.props.updateDirent}
|
onItemCopy={this.props.onItemCopy}
|
||||||
isAllItemSelected={this.props.isAllItemSelected}
|
onDirentClick={this.props.onDirentClick}
|
||||||
onAllItemSelected={this.props.onAllItemSelected}
|
updateDirent={this.props.updateDirent}
|
||||||
selectedDirentList={this.props.selectedDirentList}
|
isAllItemSelected={this.props.isAllItemSelected}
|
||||||
onItemsMove={this.props.onItemsMove}
|
onAllItemSelected={this.props.onAllItemSelected}
|
||||||
onItemsCopy={this.props.onItemsCopy}
|
selectedDirentList={this.props.selectedDirentList}
|
||||||
onItemConvert={this.props.onItemConvert}
|
onItemsMove={this.props.onItemsMove}
|
||||||
onItemsDelete={this.props.onItemsDelete}
|
onItemsCopy={this.props.onItemsCopy}
|
||||||
onAddFile={this.props.onAddFile}
|
onItemConvert={this.props.onItemConvert}
|
||||||
onAddFolder={this.props.onAddFolder}
|
onItemsDelete={this.props.onItemsDelete}
|
||||||
repoTags={this.props.repoTags}
|
onAddFile={this.props.onAddFile}
|
||||||
onFileTagChanged={this.props.onFileTagChanged}
|
onAddFolder={this.props.onAddFolder}
|
||||||
showDirentDetail={this.props.showDirentDetail}
|
repoTags={this.props.repoTags}
|
||||||
loadDirentList={this.props.loadDirentList}
|
onFileTagChanged={this.props.onFileTagChanged}
|
||||||
getMenuContainerSize={this.props.getMenuContainerSize}
|
showDirentDetail={this.props.showDirentDetail}
|
||||||
eventBus={this.props.eventBus}
|
loadDirentList={this.props.loadDirentList}
|
||||||
/>
|
getMenuContainerSize={this.props.getMenuContainerSize}
|
||||||
|
eventBus={this.props.eventBus}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -819,17 +819,18 @@ class DirentListItem extends React.Component {
|
|||||||
onMouseDown={this.onItemMouseDown}
|
onMouseDown={this.onItemMouseDown}
|
||||||
onContextMenu={this.onItemContextMenu}
|
onContextMenu={this.onItemContextMenu}
|
||||||
>
|
>
|
||||||
<td className={`pl10 ${this.state.isDragTipShow ? 'tr-drag-effect' : ''}`}>
|
<td className={`pl10 pr-2 ${this.state.isDragTipShow ? 'tr-drag-effect' : ''}`}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="vam"
|
className="vam"
|
||||||
onClick={this.onItemSelected}
|
onClick={this.onItemSelected}
|
||||||
|
style={{ position: 'relative', top: -1 }}
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
aria-label={isSelected ? gettext('Unselect this item') : gettext('Select this item')}
|
aria-label={isSelected ? gettext('Unselect this item') : gettext('Select this item')}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="pl10">
|
<td className="pl-2 pr-2">
|
||||||
{dirent.starred !== undefined &&
|
{dirent.starred !== undefined &&
|
||||||
<i
|
<i
|
||||||
role="button"
|
role="button"
|
||||||
@@ -840,7 +841,7 @@ class DirentListItem extends React.Component {
|
|||||||
</i>
|
</i>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td className="pl10">
|
<td className="pl-2 pr-2">
|
||||||
<div className="dir-icon">
|
<div className="dir-icon">
|
||||||
{(this.canPreview && dirent.encoded_thumbnail_src) ?
|
{(this.canPreview && dirent.encoded_thumbnail_src) ?
|
||||||
<img ref='drag_icon' src={`${siteRoot}${dirent.encoded_thumbnail_src}`} className="thumbnail cursor-pointer" onClick={this.onItemClick} alt="" /> :
|
<img ref='drag_icon' src={`${siteRoot}${dirent.encoded_thumbnail_src}`} className="thumbnail cursor-pointer" onClick={this.onItemClick} alt="" /> :
|
||||||
|
@@ -4,7 +4,6 @@ import { siteRoot, gettext, username, enableSeadoc, thumbnailSizeForOriginal, th
|
|||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
import TextTranslation from '../../utils/text-translation';
|
import TextTranslation from '../../utils/text-translation';
|
||||||
import URLDecorator from '../../utils/url-decorator';
|
import URLDecorator from '../../utils/url-decorator';
|
||||||
import Loading from '../loading';
|
|
||||||
import toaster from '../toast';
|
import toaster from '../toast';
|
||||||
import ModalPortal from '../modal-portal';
|
import ModalPortal from '../modal-portal';
|
||||||
import CreateFile from '../dialog/create-file-dialog';
|
import CreateFile from '../dialog/create-file-dialog';
|
||||||
@@ -27,7 +26,6 @@ const propTypes = {
|
|||||||
repoID: PropTypes.string.isRequired,
|
repoID: PropTypes.string.isRequired,
|
||||||
currentRepoInfo: PropTypes.object,
|
currentRepoInfo: PropTypes.object,
|
||||||
isAllItemSelected: PropTypes.bool.isRequired,
|
isAllItemSelected: PropTypes.bool.isRequired,
|
||||||
isDirentListLoading: PropTypes.bool.isRequired,
|
|
||||||
direntList: PropTypes.array.isRequired,
|
direntList: PropTypes.array.isRequired,
|
||||||
sortBy: PropTypes.string.isRequired,
|
sortBy: PropTypes.string.isRequired,
|
||||||
sortOrder: PropTypes.string.isRequired,
|
sortOrder: PropTypes.string.isRequired,
|
||||||
@@ -82,6 +80,7 @@ class DirentListView extends React.Component {
|
|||||||
activeDirent: null,
|
activeDirent: null,
|
||||||
isListDropTipShow: false,
|
isListDropTipShow: false,
|
||||||
isShowDirentsDraggablePreview: false,
|
isShowDirentsDraggablePreview: false,
|
||||||
|
containerWidth: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.enteredCounter = 0; // Determine whether to enter the child element to avoid dragging bubbling bugs。
|
this.enteredCounter = 0; // Determine whether to enter the child element to avoid dragging bubbling bugs。
|
||||||
@@ -102,12 +101,20 @@ class DirentListView extends React.Component {
|
|||||||
const { modify } = customPermission.permission;
|
const { modify } = customPermission.permission;
|
||||||
this.canDrop = modify;
|
this.canDrop = modify;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.containerRef = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.unsubscribeEvent = this.props.eventBus.subscribe(EVENT_BUS_TYPE.RESTORE_IMAGE, this.recalculateImageItems);
|
this.unsubscribeEvent = this.props.eventBus.subscribe(EVENT_BUS_TYPE.RESTORE_IMAGE, this.recalculateImageItems);
|
||||||
|
this.resizeObserver = new ResizeObserver(this.handleResize);
|
||||||
|
this.containerRef && this.resizeObserver.observe(this.containerRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleResize = () => {
|
||||||
|
this.setState({ containerWidth: this.containerRef.offsetWidth - 32 });
|
||||||
|
};
|
||||||
|
|
||||||
recalculateImageItems = () => {
|
recalculateImageItems = () => {
|
||||||
if (!this.state.isImagePopupOpen) return;
|
if (!this.state.isImagePopupOpen) return;
|
||||||
let imageItems = this.props.direntList
|
let imageItems = this.props.direntList
|
||||||
@@ -122,6 +129,7 @@ class DirentListView extends React.Component {
|
|||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.unsubscribeEvent();
|
this.unsubscribeEvent();
|
||||||
|
this.containerRef && this.resizeObserver.unobserve(this.containerRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
freezeItem = () => {
|
freezeItem = () => {
|
||||||
@@ -680,10 +688,7 @@ class DirentListView extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { direntList, sortBy, sortOrder } = this.props;
|
const { direntList, sortBy, sortOrder } = this.props;
|
||||||
|
const { containerWidth } = this.state;
|
||||||
if (this.props.isDirentListLoading) {
|
|
||||||
return (<Loading />);
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort
|
// sort
|
||||||
const sortByName = sortBy == 'name';
|
const sortByName = sortBy == 'name';
|
||||||
@@ -704,13 +709,14 @@ class DirentListView extends React.Component {
|
|||||||
onDragOver={this.onTableDragOver}
|
onDragOver={this.onTableDragOver}
|
||||||
onDragLeave={this.onTableDragLeave}
|
onDragLeave={this.onTableDragLeave}
|
||||||
onDrop={this.tableDrop}
|
onDrop={this.tableDrop}
|
||||||
|
ref={ref => this.containerRef = ref}
|
||||||
>
|
>
|
||||||
{direntList.length > 0 &&
|
{direntList.length > 0 &&
|
||||||
<table className={`table-hover ${isDesktop ? '' : 'table-thead-hidden'}`}>
|
<table className={`table-hover ${isDesktop ? '' : 'table-thead-hidden'}`}>
|
||||||
{isDesktop ? (
|
{isDesktop ? (
|
||||||
<thead onMouseDown={this.onThreadMouseDown} onContextMenu={this.onThreadContextMenu}>
|
<thead onMouseDown={this.onThreadMouseDown} onContextMenu={this.onThreadContextMenu}>
|
||||||
<tr>
|
<tr>
|
||||||
<th width="3%" className="pl10">
|
<th style={{ width: 31 }} className="pl10 pr-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="vam"
|
className="vam"
|
||||||
@@ -721,13 +727,13 @@ class DirentListView extends React.Component {
|
|||||||
disabled={direntList.length === 0}
|
disabled={direntList.length === 0}
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th width="3%" className="pl10">{/* icon */}</th>
|
<th style={{ width: 32 }} className="pl-2 pr-2">{/* star */}</th>
|
||||||
<th width="5%" className="pl10">{/* star */}</th>
|
<th style={{ width: 40 }} className="pl-2 pr-2">{/* icon */}</th>
|
||||||
<th width="39%"><a className="d-block table-sort-op" href="#" onClick={this.sortByName}>{gettext('Name')} {sortByName && sortIcon}</a></th>
|
<th style={{ width: (containerWidth - 103) * 0.5 }}><a className="d-block table-sort-op" href="#" onClick={this.sortByName}>{gettext('Name')} {sortByName && sortIcon}</a></th>
|
||||||
<th width="6%">{/* tag */}</th>
|
<th style={{ width: (containerWidth - 103) * 0.06 }}>{/* tag */}</th>
|
||||||
<th width="18%">{/* operation */}</th>
|
<th style={{ width: (containerWidth - 103) * 0.18 }}>{/* operation */}</th>
|
||||||
<th width="11%"><a className="d-block table-sort-op" href="#" onClick={this.sortBySize}>{gettext('Size')} {sortBySize && sortIcon}</a></th>
|
<th style={{ width: (containerWidth - 103) * 0.11 }}><a className="d-block table-sort-op" href="#" onClick={this.sortBySize}>{gettext('Size')} {sortBySize && sortIcon}</a></th>
|
||||||
<th width="15%"><a className="d-block table-sort-op" href="#" onClick={this.sortByTime}>{gettext('Last Update')} {sortByTime && sortIcon}</a></th>
|
<th style={{ width: (containerWidth - 103) * 0.15 }}><a className="d-block table-sort-op" href="#" onClick={this.sortByTime}>{gettext('Last Update')} {sortByTime && sortIcon}</a></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
) : (
|
) : (
|
||||||
|
@@ -37,9 +37,12 @@ const FileNameEditor = React.forwardRef((props, ref) => {
|
|||||||
|
|
||||||
if (mode === EDITOR_TYPE.PREVIEWER) {
|
if (mode === EDITOR_TYPE.PREVIEWER) {
|
||||||
const fileType = getFileType();
|
const fileType = getFileType();
|
||||||
|
const repoID = window.sfMetadataContext.getSetting('repoID');
|
||||||
|
const repoInfo = window.sfMetadataContext.getSetting('repoInfo');
|
||||||
|
|
||||||
if (fileType === 'image') {
|
if (fileType === 'image') {
|
||||||
return (
|
return (
|
||||||
<ImagePreviewer {...props} closeImagePopup={props.onCommitCancel} />
|
<ImagePreviewer {...props} repoID={repoID} repoInfo={repoInfo} closeImagePopup={props.onCommitCancel} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -9,14 +9,11 @@ import { Utils } from '../../../utils/utils';
|
|||||||
import { siteRoot, thumbnailSizeForOriginal, fileServerRoot, thumbnailDefaultSize } from '../../../utils/constants';
|
import { siteRoot, thumbnailSizeForOriginal, fileServerRoot, thumbnailDefaultSize } from '../../../utils/constants';
|
||||||
import { getFileNameFromRecord, getParentDirFromRecord, getRecordIdFromRecord } from '../../utils/cell';
|
import { getFileNameFromRecord, getParentDirFromRecord, getRecordIdFromRecord } from '../../utils/cell';
|
||||||
|
|
||||||
const ImagePreviewer = (props) => {
|
const ImagePreviewer = ({ record, table, repoID, repoInfo, closeImagePopup }) => {
|
||||||
const { record, table, closeImagePopup } = props;
|
|
||||||
const [imageIndex, setImageIndex] = useState(0);
|
const [imageIndex, setImageIndex] = useState(0);
|
||||||
const [imageItems, setImageItems] = useState([]);
|
const [imageItems, setImageItems] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const repoID = window.sfMetadataContext.getSetting('repoID');
|
|
||||||
const repoInfo = window.sfMetadataContext.getSetting('repoInfo');
|
|
||||||
const newImageItems = table.rows
|
const newImageItems = table.rows
|
||||||
.filter((row) => Utils.imageCheck(getFileNameFromRecord(row)))
|
.filter((row) => Utils.imageCheck(getFileNameFromRecord(row)))
|
||||||
.map((row) => {
|
.map((row) => {
|
||||||
@@ -40,7 +37,7 @@ const ImagePreviewer = (props) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
setImageItems(newImageItems);
|
setImageItems(newImageItems);
|
||||||
}, [table]);
|
}, [table, repoID, repoInfo]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (imageItems.length > 0) {
|
if (imageItems.length > 0) {
|
||||||
@@ -98,9 +95,10 @@ const ImagePreviewer = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ImagePreviewer.propTypes = {
|
ImagePreviewer.propTypes = {
|
||||||
table: PropTypes.object,
|
|
||||||
column: PropTypes.object,
|
|
||||||
record: PropTypes.object,
|
record: PropTypes.object,
|
||||||
|
table: PropTypes.object,
|
||||||
|
repoID: PropTypes.string,
|
||||||
|
repoInfo: PropTypes.object,
|
||||||
closeImagePopup: PropTypes.func,
|
closeImagePopup: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -39,6 +39,7 @@ const COLUMNS_ICON_NAME = {
|
|||||||
[CellType.GEOLOCATION]: 'Geolocation',
|
[CellType.GEOLOCATION]: 'Geolocation',
|
||||||
[CellType.RATE]: 'Rate',
|
[CellType.RATE]: 'Rate',
|
||||||
[CellType.LINK]: 'Link',
|
[CellType.LINK]: 'Link',
|
||||||
|
[CellType.TAGS]: 'Tag',
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
import { PRIVATE_COLUMN_KEY, PRIVATE_COLUMN_KEYS } from '../../constants';
|
import { PRIVATE_COLUMN_KEY, PRIVATE_COLUMN_KEYS } from '../../constants';
|
||||||
|
import { siteRoot } from '../../../utils/constants';
|
||||||
|
import { Utils } from '../../../utils/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
@@ -59,3 +61,18 @@ export const getFileMTimeFromRecord = record => {
|
|||||||
export const getTagsFromRecord = record => {
|
export const getTagsFromRecord = record => {
|
||||||
return record ? record[PRIVATE_COLUMN_KEY.TAGS] : '';
|
return record ? record[PRIVATE_COLUMN_KEY.TAGS] : '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _getParentDir = (record) => {
|
||||||
|
const parentDir = getParentDirFromRecord(record);
|
||||||
|
if (parentDir === '/') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return parentDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFilePathByRecord = (repoID, record) => {
|
||||||
|
const parentDir = _getParentDir(record);
|
||||||
|
const fileName = getFileNameFromRecord(record);
|
||||||
|
const path = Utils.encodePath(Utils.joinPath(parentDir, fileName));
|
||||||
|
return siteRoot + 'lib/' + repoID + '/file' + path;
|
||||||
|
};
|
||||||
|
@@ -2,7 +2,6 @@ import { getFileNameFromRecord, getParentDirFromRecord } from './cell';
|
|||||||
import { checkIsDir } from './row';
|
import { checkIsDir } from './row';
|
||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
import { siteRoot } from '../../utils/constants';
|
import { siteRoot } from '../../utils/constants';
|
||||||
import { EVENT_BUS_TYPE } from '../../components/common/event-bus-type';
|
|
||||||
|
|
||||||
const FILE_TYPE = {
|
const FILE_TYPE = {
|
||||||
FOLDER: 'folder',
|
FOLDER: 'folder',
|
||||||
@@ -32,23 +31,28 @@ const _getParentDir = (record) => {
|
|||||||
return parentDir;
|
return parentDir;
|
||||||
};
|
};
|
||||||
|
|
||||||
const _generateUrl = (fileName, parentDir) => {
|
const _generateUrl = (repoID, fileName, parentDir) => {
|
||||||
const repoID = window.sfMetadataContext.getSetting('repoID');
|
|
||||||
const path = Utils.encodePath(Utils.joinPath(parentDir, fileName));
|
const path = Utils.encodePath(Utils.joinPath(parentDir, fileName));
|
||||||
return `${siteRoot}lib/${repoID}/file${path}`;
|
return `${siteRoot}lib/${repoID}/file${path}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const _openUrl = (url) => {
|
const _openUrl = (url) => {
|
||||||
|
const isWeChat = Utils.isWeChat();
|
||||||
|
if (isWeChat) {
|
||||||
|
location.href = url;
|
||||||
|
return;
|
||||||
|
}
|
||||||
window.open(url);
|
window.open(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
const _openMarkdown = (fileName, parentDir, eventBus) => {
|
const _openMarkdown = (repoID, fileName, parentDir) => {
|
||||||
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.OPEN_MARKDOWN, parentDir, fileName);
|
const url = _generateUrl(repoID, fileName, parentDir);
|
||||||
|
_openUrl(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
const _openByNewWindow = (fileName, parentDir, fileType) => {
|
const _openByNewWindow = (repoID, fileName, parentDir, fileType) => {
|
||||||
if (!fileType) {
|
if (!fileType) {
|
||||||
const url = _generateUrl(fileName, parentDir);
|
const url = _generateUrl(repoID, fileName, parentDir);
|
||||||
_openUrl(url);
|
_openUrl(url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -59,16 +63,16 @@ const _openByNewWindow = (fileName, parentDir, fileType) => {
|
|||||||
_openUrl(window.location.origin + pathname + Utils.encodePath(Utils.joinPath(parentDir, fileName)));
|
_openUrl(window.location.origin + pathname + Utils.encodePath(Utils.joinPath(parentDir, fileName)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const _openSdoc = (fileName, parentDir) => {
|
const _openSdoc = (repoID, fileName, parentDir) => {
|
||||||
const url = _generateUrl(fileName, parentDir);
|
const url = _generateUrl(repoID, fileName, parentDir);
|
||||||
_openUrl(url);
|
_openUrl(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
const _openOthers = (fileName, parentDir, fileType) => {
|
const _openOthers = (repoID, fileName, parentDir, fileType) => {
|
||||||
_openByNewWindow(fileName, parentDir, fileType);
|
_openByNewWindow(repoID, fileName, parentDir, fileType);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const openFile = (record, eventBus, _openImage = () => {}) => {
|
export const openFile = (repoID, record, _openImage = () => {}) => {
|
||||||
if (!record) return;
|
if (!record) return;
|
||||||
const fileName = getFileNameFromRecord(record);
|
const fileName = getFileNameFromRecord(record);
|
||||||
const isDir = checkIsDir(record);
|
const isDir = checkIsDir(record);
|
||||||
@@ -77,11 +81,11 @@ export const openFile = (record, eventBus, _openImage = () => {}) => {
|
|||||||
|
|
||||||
switch (fileType) {
|
switch (fileType) {
|
||||||
case FILE_TYPE.MARKDOWN: {
|
case FILE_TYPE.MARKDOWN: {
|
||||||
_openMarkdown(fileName, parentDir, eventBus);
|
_openMarkdown(repoID, fileName, parentDir);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case FILE_TYPE.SDOC: {
|
case FILE_TYPE.SDOC: {
|
||||||
_openSdoc(fileName, parentDir);
|
_openSdoc(repoID, fileName, parentDir);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case FILE_TYPE.IMAGE: {
|
case FILE_TYPE.IMAGE: {
|
||||||
@@ -89,7 +93,7 @@ export const openFile = (record, eventBus, _openImage = () => {}) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
_openOthers(fileName, parentDir, fileType);
|
_openOthers(repoID, fileName, parentDir, fileType);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,7 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.smooth-dnd-container.vertical .sf-metadata-kanban-card:last-child {
|
.smooth-dnd-container.vertical .smooth-dnd-draggable-wrapper:last-child .sf-metadata-kanban-card {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -176,7 +176,8 @@ const Boards = ({ modifyRecord, modifyColumnData, onCloseSettings }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onOpenFile = useCallback((record) => {
|
const onOpenFile = useCallback((record) => {
|
||||||
openFile(record, window.sfMetadataContext.eventBus, () => {
|
const repoID = window.sfMetadataContext.getSetting('repoID');
|
||||||
|
openFile(repoID, record, () => {
|
||||||
currentImageRef.current = record;
|
currentImageRef.current = record;
|
||||||
setImagePreviewerVisible(true);
|
setImagePreviewerVisible(true);
|
||||||
});
|
});
|
||||||
@@ -222,6 +223,8 @@ const Boards = ({ modifyRecord, modifyColumnData, onCloseSettings }) => {
|
|||||||
}, [isDirentDetailShow]);
|
}, [isDirentDetailShow]);
|
||||||
|
|
||||||
const isEmpty = boards.length === 0;
|
const isEmpty = boards.length === 0;
|
||||||
|
const repoID = window.sfMetadataContext.getSetting('repoID');
|
||||||
|
const repoInfo = window.sfMetadataContext.getSetting('repoInfo');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -264,7 +267,15 @@ const Boards = ({ modifyRecord, modifyColumnData, onCloseSettings }) => {
|
|||||||
)}
|
)}
|
||||||
{!readonly && (<AddBoard groupByColumn={groupByColumn}/>)}
|
{!readonly && (<AddBoard groupByColumn={groupByColumn}/>)}
|
||||||
</div>
|
</div>
|
||||||
{isImagePreviewerVisible && (<ImagePreviewer record={currentImageRef.current} table={metadata} closeImagePopup={closeImagePreviewer} />)}
|
{isImagePreviewerVisible && (
|
||||||
|
<ImagePreviewer
|
||||||
|
repoID={repoID}
|
||||||
|
repoInfo={repoInfo}
|
||||||
|
record={currentImageRef.current}
|
||||||
|
table={metadata}
|
||||||
|
closeImagePopup={closeImagePreviewer}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -21,7 +21,8 @@ const FileNameOperationBtn = ({ column, record, ...props }) => {
|
|||||||
const handelClick = (event) => {
|
const handelClick = (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.nativeEvent.stopImmediatePropagation();
|
event.nativeEvent.stopImmediatePropagation();
|
||||||
openFile(record, window.sfMetadataContext.eventBus, () => {
|
const repoID = window.sfMetadataContext.getSetting('repoID');
|
||||||
|
openFile(repoID, record, () => {
|
||||||
window.sfMetadataContext.eventBus.dispatch(METADATA_EVENT_BUS_TYPE.OPEN_EDITOR, EDITOR_TYPE.PREVIEWER);
|
window.sfMetadataContext.eventBus.dispatch(METADATA_EVENT_BUS_TYPE.OPEN_EDITOR, EDITOR_TYPE.PREVIEWER);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -447,14 +447,6 @@ class LibContentView extends React.Component {
|
|||||||
window.history.pushState({ url: url, path: path }, path, url);
|
window.history.pushState({ url: url, path: path }, path, url);
|
||||||
};
|
};
|
||||||
|
|
||||||
openMarkDown = (parentDir, fileName) => {
|
|
||||||
let filePath = Utils.joinPath(parentDir, fileName);
|
|
||||||
let repoID = this.props.repoID;
|
|
||||||
const w = window.open('about:blank');
|
|
||||||
const url = siteRoot + 'lib/' + repoID + '/file' + Utils.encodePath(filePath);
|
|
||||||
w.location.href = url;
|
|
||||||
};
|
|
||||||
|
|
||||||
showFile = (filePath, noRedirection) => {
|
showFile = (filePath, noRedirection) => {
|
||||||
let repoID = this.props.repoID;
|
let repoID = this.props.repoID;
|
||||||
|
|
||||||
@@ -517,10 +509,6 @@ class LibContentView extends React.Component {
|
|||||||
path: filePath,
|
path: filePath,
|
||||||
viewId: viewId,
|
viewId: viewId,
|
||||||
isDirentDetailShow: viewType === VIEW_TYPE.GALLERY ? this.state.isDirentDetailShow : false,
|
isDirentDetailShow: viewType === VIEW_TYPE.GALLERY ? this.state.isDirentDetailShow : false,
|
||||||
}, () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.unsubscribeEventBus = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.OPEN_MARKDOWN, this.openMarkDown);
|
|
||||||
}, 1);
|
|
||||||
});
|
});
|
||||||
const url = `${siteRoot}library/${repoID}/${encodeURIComponent(repoInfo.repo_name)}/?view=${encodeURIComponent(viewId)}`;
|
const url = `${siteRoot}library/${repoID}/${encodeURIComponent(repoInfo.repo_name)}/?view=${encodeURIComponent(viewId)}`;
|
||||||
window.history.pushState({ url: url, path: '' }, '', url);
|
window.history.pushState({ url: url, path: '' }, '', url);
|
||||||
|
@@ -45,6 +45,7 @@ export const TagViewProvider = ({ repoID, tagID, children, ...params }) => {
|
|||||||
tagFiles,
|
tagFiles,
|
||||||
repoID,
|
repoID,
|
||||||
tagID,
|
tagID,
|
||||||
|
repoInfo: params.repoInfo,
|
||||||
deleteFilesCallback: params.deleteFilesCallback,
|
deleteFilesCallback: params.deleteFilesCallback,
|
||||||
renameFileCallback: params.renameFileCallback,
|
renameFileCallback: params.renameFileCallback,
|
||||||
updateCurrentDirent: params.updateCurrentDirent,
|
updateCurrentDirent: params.updateCurrentDirent,
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
import React, { useEffect, useMemo, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTags } from '../hooks';
|
import { useTags } from '../hooks';
|
||||||
import Tag from './tag';
|
import Tag from './tag';
|
||||||
@@ -19,8 +19,7 @@ const updateFavicon = () => {
|
|||||||
const TagsTreeView = ({ userPerm, currentPath }) => {
|
const TagsTreeView = ({ userPerm, currentPath }) => {
|
||||||
const originalTitle = useRef('');
|
const originalTitle = useRef('');
|
||||||
|
|
||||||
// const {} = { }
|
const { tagsData, selectTag } = useTags();
|
||||||
const { tagsData, selectTag, deleteTags, duplicateTag, updateTag } = useTags();
|
|
||||||
|
|
||||||
const tags = useMemo(() => {
|
const tags = useMemo(() => {
|
||||||
if (!tagsData) return [];
|
if (!tagsData) return [];
|
||||||
@@ -32,16 +31,6 @@ const TagsTreeView = ({ userPerm, currentPath }) => {
|
|||||||
return true;
|
return true;
|
||||||
}, [userPerm]);
|
}, [userPerm]);
|
||||||
|
|
||||||
const deleteTag = useCallback((tagId, isSelected) => {
|
|
||||||
if (isSelected) {
|
|
||||||
const currentTagIndex = tagsData.row_ids.indexOf(tagId);
|
|
||||||
const lastTagId = tagsData.row_ids[currentTagIndex - 1];
|
|
||||||
const lastTag = getRowById(tagsData, lastTagId);
|
|
||||||
selectTag(lastTag);
|
|
||||||
}
|
|
||||||
deleteTags([tagId]);
|
|
||||||
}, [tagsData, deleteTags, selectTag]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
originalTitle.current = document.title;
|
originalTitle.current = document.title;
|
||||||
}, []);
|
}, []);
|
||||||
@@ -105,13 +94,8 @@ const TagsTreeView = ({ userPerm, currentPath }) => {
|
|||||||
<Tag
|
<Tag
|
||||||
key={id}
|
key={id}
|
||||||
tag={tag}
|
tag={tag}
|
||||||
tags={tags}
|
|
||||||
userPerm={userPerm}
|
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
onClick={(tag) => selectTag(tag, isSelected)}
|
onClick={(tag) => selectTag(tag, isSelected)}
|
||||||
onDelete={() => deleteTag(id, isSelected)}
|
|
||||||
onCopy={() => duplicateTag(id)}
|
|
||||||
onUpdateTag={updateTag}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
.tag-tree-node .tag-tree-node-color {
|
.tag-tree-node .tag-tree-node-color {
|
||||||
height: 12px;
|
height: 10px;
|
||||||
width: 12px;
|
width: 10px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transform: translateY(2px);
|
transform: translateY(3px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-tree-node .tag-tree-node-text {
|
.tag-tree-node .tag-tree-node-text {
|
||||||
@@ -14,11 +14,22 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-tree-node .tag-tree-node-text .tag-tree-node-count {
|
.tag-tree-node .tag-tree-node-text .tag-tree-node-count {
|
||||||
color: #666;
|
color: #666;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
margin-right: 8px;
|
padding-right: 14px;
|
||||||
|
min-width: 36px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-tree-view .tag-tree-node .tag-tree-node-text {
|
||||||
|
width: calc(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-tree-view .tag-tree-node .right-icon:hover {
|
||||||
|
background-color: unset;
|
||||||
}
|
}
|
||||||
|
@@ -1,227 +1,51 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { Input } from 'reactstrap';
|
import { getTagColor, getTagName, getTagFilesCount } from '../../utils';
|
||||||
import { gettext } from '../../../utils/constants';
|
|
||||||
import ItemDropdownMenu from '../../../components/dropdown-menu/item-dropdown-menu';
|
|
||||||
import { isMobile } from '../../../utils/utils';
|
|
||||||
import { getTagColor, getTagName, getTagId, getTagFilesCount, isValidTagName } from '../../utils';
|
|
||||||
import { isEnter } from '../../../metadata/utils/hotkey';
|
|
||||||
import toaster from '../../../components/toast';
|
|
||||||
import { PRIVATE_COLUMN_KEY } from '../../constants';
|
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const Tag = ({
|
const Tag = ({ isSelected, tag, onClick }) => {
|
||||||
userPerm,
|
|
||||||
isSelected,
|
|
||||||
tag,
|
|
||||||
tags,
|
|
||||||
onClick,
|
|
||||||
onDelete,
|
|
||||||
onCopy,
|
|
||||||
onUpdateTag,
|
|
||||||
}) => {
|
|
||||||
const tagName = useMemo(() => getTagName(tag), [tag]);
|
const tagName = useMemo(() => getTagName(tag), [tag]);
|
||||||
const tagColor = useMemo(() => getTagColor(tag), [tag]);
|
const tagColor = useMemo(() => getTagColor(tag), [tag]);
|
||||||
const tagId = useMemo(() => getTagId(tag), [tag]);
|
|
||||||
const tagCount = useMemo(() => getTagFilesCount(tag), [tag]);
|
const tagCount = useMemo(() => getTagFilesCount(tag), [tag]);
|
||||||
const [highlight, setHighlight] = useState(false);
|
const [highlight, setHighlight] = useState(false);
|
||||||
const [freeze, setFreeze] = useState(false);
|
|
||||||
const [isRenaming, setRenaming] = useState(false);
|
|
||||||
const [inputValue, setInputValue] = useState('');
|
|
||||||
|
|
||||||
const inputRef = useRef(null);
|
|
||||||
|
|
||||||
const otherTagsName = useMemo(() => {
|
|
||||||
return tags.filter(tagItem => getTagId(tagItem) !== tagId).map(tagItem => getTagName(tagItem));
|
|
||||||
}, [tags, tagId]);
|
|
||||||
|
|
||||||
const canUpdate = useMemo(() => {
|
|
||||||
if (userPerm !== 'rw' && userPerm !== 'admin') return false;
|
|
||||||
return true;
|
|
||||||
}, [userPerm]);
|
|
||||||
|
|
||||||
const operations = useMemo(() => {
|
|
||||||
if (!canUpdate) return [];
|
|
||||||
const value = [
|
|
||||||
{ key: 'rename', value: gettext('Rename') },
|
|
||||||
{ key: 'duplicate', value: gettext('Duplicate') },
|
|
||||||
{ key: 'delete', value: gettext('Delete') }
|
|
||||||
];
|
|
||||||
return value;
|
|
||||||
}, [canUpdate]);
|
|
||||||
|
|
||||||
const onMouseEnter = useCallback(() => {
|
const onMouseEnter = useCallback(() => {
|
||||||
if (freeze) return;
|
|
||||||
setHighlight(true);
|
setHighlight(true);
|
||||||
}, [freeze]);
|
}, []);
|
||||||
|
|
||||||
const onMouseOver = useCallback(() => {
|
const onMouseOver = useCallback(() => {
|
||||||
if (freeze) return;
|
|
||||||
setHighlight(true);
|
setHighlight(true);
|
||||||
}, [freeze]);
|
}, []);
|
||||||
|
|
||||||
const onMouseLeave = useCallback(() => {
|
const onMouseLeave = useCallback(() => {
|
||||||
if (freeze) return;
|
|
||||||
setHighlight(false);
|
|
||||||
}, [freeze]);
|
|
||||||
|
|
||||||
const freezeItem = useCallback(() => {
|
|
||||||
setFreeze(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const unfreezeItem = useCallback(() => {
|
|
||||||
setFreeze(false);
|
|
||||||
setHighlight(false);
|
setHighlight(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const operationClick = useCallback((operationKey) => {
|
|
||||||
switch (operationKey) {
|
|
||||||
case 'rename': {
|
|
||||||
setInputValue(tagName);
|
|
||||||
setRenaming(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case 'duplicate': {
|
|
||||||
onCopy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case 'delete': {
|
|
||||||
onDelete();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [tagName, onDelete, onCopy]);
|
|
||||||
|
|
||||||
const renameTag = useCallback((name, failCallback) => {
|
|
||||||
onUpdateTag(tagId, { [PRIVATE_COLUMN_KEY.TAG_NAME]: name }, {
|
|
||||||
success_callback: () => {
|
|
||||||
setRenaming(false);
|
|
||||||
if (!isSelected) return;
|
|
||||||
document.title = `${name} - Seafile`;
|
|
||||||
},
|
|
||||||
fail_callback: (error) => {
|
|
||||||
failCallback(error);
|
|
||||||
if (!isSelected) return;
|
|
||||||
document.title = `${tagName} - Seafile`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [onUpdateTag, isSelected, tagId, tagName]);
|
|
||||||
|
|
||||||
const handleSubmit = useCallback((event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
const { isValid, message } = isValidTagName(inputValue, otherTagsName);
|
|
||||||
if (!isValid) {
|
|
||||||
toaster.danger(message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (message === tagName) {
|
|
||||||
setRenaming(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
renameTag(message);
|
|
||||||
}, [tagName, inputValue, otherTagsName, renameTag]);
|
|
||||||
|
|
||||||
const onChange = useCallback((e) => {
|
|
||||||
setInputValue(e.target.value);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onKeyDown = useCallback((event) => {
|
|
||||||
if (isEnter(event)) {
|
|
||||||
handleSubmit(event);
|
|
||||||
unfreezeItem();
|
|
||||||
}
|
|
||||||
}, [handleSubmit, unfreezeItem]);
|
|
||||||
|
|
||||||
const onInputClick = useCallback((event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.nativeEvent.stopImmediatePropagation();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isRenaming && inputRef.current) {
|
|
||||||
inputRef.current.focus();
|
|
||||||
inputRef.current.select();
|
|
||||||
}
|
|
||||||
}, [isRenaming]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event) => {
|
|
||||||
if (inputRef.current && !inputRef.current.contains(event.target)) {
|
|
||||||
handleSubmit(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isRenaming) {
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
} else {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
};
|
|
||||||
}, [isRenaming, handleSubmit]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<div
|
className={classnames('tree-node-inner text-nowrap tag-tree-node', { 'tree-node-inner-hover': highlight, 'tree-node-hight-light': isSelected })}
|
||||||
className={classnames('tree-node-inner text-nowrap tag-tree-node', { 'tree-node-inner-hover': highlight, 'tree-node-hight-light': isSelected })}
|
title={`${tagName} (${tagCount})`}
|
||||||
title={tagName}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseOver={onMouseOver}
|
||||||
onMouseOver={onMouseOver}
|
onMouseLeave={onMouseLeave}
|
||||||
onMouseLeave={onMouseLeave}
|
onClick={() => onClick(tag)}
|
||||||
onClick={() => onClick(tag)}
|
>
|
||||||
>
|
<div className="tree-node-text tag-tree-node-text">
|
||||||
<div className="tree-node-text tag-tree-node-text">
|
<div className="tag-tree-node-name">{tagName}</div>
|
||||||
<div className="tag-tree-node-name">
|
<div className="tag-tree-node-count">{tagCount}</div>
|
||||||
{isRenaming ? (
|
</div>
|
||||||
<Input
|
<div className="left-icon">
|
||||||
innerRef={inputRef}
|
<div className="tree-node-icon">
|
||||||
className="sf-metadata-view-input mt-0"
|
<div className="tag-tree-node-color" style={{ backgroundColor: tagColor }}></div>
|
||||||
value={inputValue}
|
|
||||||
onChange={onChange}
|
|
||||||
autoFocus={true}
|
|
||||||
onBlur={() => setRenaming(false)}
|
|
||||||
onClick={onInputClick}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>{tagName}</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="tag-tree-node-count">{` (${tagCount})`}</div>
|
|
||||||
</div>
|
|
||||||
<div className="left-icon">
|
|
||||||
<div className="tree-node-icon">
|
|
||||||
<div className="tag-tree-node-color" style={{ backgroundColor: tagColor }}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="right-icon" id={`metadata-tag-dropdown-item-${tagId}`} >
|
|
||||||
{highlight && operations.length > 0 && (
|
|
||||||
<ItemDropdownMenu
|
|
||||||
item={{ name: 'tags' }}
|
|
||||||
toggleClass="sf3-font sf3-font-more"
|
|
||||||
freezeItem={freezeItem}
|
|
||||||
unfreezeItem={unfreezeItem}
|
|
||||||
getMenuList={() => operations}
|
|
||||||
onMenuItemClick={operationClick}
|
|
||||||
menuStyle={isMobile ? { zIndex: 1050 } : {}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Tag.propTypes = {
|
Tag.propTypes = {
|
||||||
canDelete: PropTypes.bool,
|
|
||||||
isSelected: PropTypes.bool,
|
isSelected: PropTypes.bool,
|
||||||
tag: PropTypes.object,
|
tag: PropTypes.object,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
.sf-metadata-tag-files-wrapper .sf-metadata-tags-main {
|
||||||
|
overflow: auto !important;
|
||||||
|
}
|
||||||
|
@@ -1,15 +1,21 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState, useRef, useEffect } from 'react';
|
||||||
import { useTagView } from '../../hooks';
|
import { useTagView } from '../../hooks';
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
import TagFile from './tag-file';
|
import TagFile from './tag-file';
|
||||||
import { getRecordIdFromRecord } from '../../../metadata/utils/cell';
|
import { getRecordIdFromRecord } from '../../../metadata/utils/cell';
|
||||||
import EmptyTip from '../../../components/empty-tip';
|
import EmptyTip from '../../../components/empty-tip';
|
||||||
|
import ImagePreviewer from '../../../metadata/components/cell-formatter/image-previewer';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const TagFiles = () => {
|
const TagFiles = () => {
|
||||||
const { tagFiles, repoID } = useTagView();
|
const { tagFiles, repoID, repoInfo } = useTagView();
|
||||||
const [selectedFiles, setSelectedFiles] = useState(null);
|
const [selectedFiles, setSelectedFiles] = useState(null);
|
||||||
|
const [isImagePreviewerVisible, setImagePreviewerVisible] = useState(false);
|
||||||
|
const [containerWidth, setContainerWidth] = useState(0);
|
||||||
|
|
||||||
|
const currentImageRef = useRef(null);
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
const onMouseDown = useCallback((event) => {
|
const onMouseDown = useCallback((event) => {
|
||||||
if (event.button === 2) {
|
if (event.button === 2) {
|
||||||
@@ -45,6 +51,31 @@ const TagFiles = () => {
|
|||||||
}
|
}
|
||||||
}, [selectedFiles]);
|
}, [selectedFiles]);
|
||||||
|
|
||||||
|
const openImagePreview = useCallback((record) => {
|
||||||
|
currentImageRef.current = record;
|
||||||
|
setImagePreviewerVisible(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeImagePreviewer = useCallback(() => {
|
||||||
|
currentImageRef.current = null;
|
||||||
|
setImagePreviewerVisible(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
const handleResize = () => {
|
||||||
|
if (!container) return;
|
||||||
|
// 32: container padding left + container padding right
|
||||||
|
setContainerWidth(container.offsetWidth - 32);
|
||||||
|
};
|
||||||
|
const resizeObserver = new ResizeObserver(handleResize);
|
||||||
|
container && resizeObserver.observe(container);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
container && resizeObserver.unobserve(container);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (tagFiles.rows.length === 0) {
|
if (tagFiles.rows.length === 0) {
|
||||||
return (<EmptyTip text={gettext('No files')} />);
|
return (<EmptyTip text={gettext('No files')} />);
|
||||||
}
|
}
|
||||||
@@ -52,43 +83,55 @@ const TagFiles = () => {
|
|||||||
const isSelectedAll = selectedFiles && selectedFiles.length === tagFiles.rows.length;
|
const isSelectedAll = selectedFiles && selectedFiles.length === tagFiles.rows.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="table-container">
|
<>
|
||||||
<table className="table-hover">
|
<div className="table-container" ref={containerRef}>
|
||||||
<thead onMouseDown={onThreadMouseDown} onContextMenu={onThreadContextMenu}>
|
<table className="table-hover">
|
||||||
<tr>
|
<thead onMouseDown={onThreadMouseDown} onContextMenu={onThreadContextMenu}>
|
||||||
<th width="3%" className="pl10">
|
<tr>
|
||||||
<input
|
<th style={{ width: 31 }} className="pl10 pr-2">
|
||||||
type="checkbox"
|
<input
|
||||||
className="vam"
|
type="checkbox"
|
||||||
onChange={onSelectedAll}
|
className="vam"
|
||||||
checked={isSelectedAll}
|
onChange={onSelectedAll}
|
||||||
title={isSelectedAll ? gettext('Unselect all') : gettext('Select all')}
|
checked={isSelectedAll}
|
||||||
disabled={tagFiles.rows.length === 0}
|
title={isSelectedAll ? gettext('Unselect all') : gettext('Select all')}
|
||||||
/>
|
disabled={tagFiles.rows.length === 0}
|
||||||
</th>
|
/>
|
||||||
<th width="3%" className="pl10">{/* icon */}</th>
|
</th>
|
||||||
<th width="45%"><a className="d-block table-sort-op" href="#">{gettext('Name')}</a></th>
|
<th style={{ width: 40 }} className="pl-2 pr-2">{/* icon */}</th>
|
||||||
<th width="18%">{/* operation */}</th>
|
<th style={{ width: (containerWidth - 71) * 0.5 }}><a className="d-block table-sort-op" href="#">{gettext('Name')}</a></th>
|
||||||
<th width="6%">{/* tag */}</th>
|
<th style={{ width: (containerWidth - 71) * 0.06 }}>{/* tag */}</th>
|
||||||
<th width="11%"><a className="d-block table-sort-op" href="#">{gettext('Size')}</a></th>
|
<th style={{ width: (containerWidth - 71) * 0.18 }}>{/* operation */}</th>
|
||||||
<th width="15%"><a className="d-block table-sort-op" href="#">{gettext('Last Update')}</a></th>
|
<th style={{ width: (containerWidth - 71) * 0.11 }}><a className="d-block table-sort-op" href="#">{gettext('Size')}</a></th>
|
||||||
</tr>
|
<th style={{ width: (containerWidth - 71) * 0.15 }}><a className="d-block table-sort-op" href="#">{gettext('Last Update')}</a></th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
{tagFiles.rows.map(file => {
|
<tbody>
|
||||||
const fileId = getRecordIdFromRecord(file);
|
{tagFiles.rows.map(file => {
|
||||||
return (
|
const fileId = getRecordIdFromRecord(file);
|
||||||
<TagFile
|
return (
|
||||||
key={fileId}
|
<TagFile
|
||||||
repoID={repoID}
|
key={fileId}
|
||||||
isSelected={selectedFiles && selectedFiles.includes(fileId)}
|
repoID={repoID}
|
||||||
file={file}
|
isSelected={selectedFiles && selectedFiles.includes(fileId)}
|
||||||
onSelectFile={onSelectFile}
|
file={file}
|
||||||
/>);
|
onSelectFile={onSelectFile}
|
||||||
})}
|
openImagePreview={openImagePreview}
|
||||||
</tbody>
|
/>);
|
||||||
</table>
|
})}
|
||||||
</div>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{isImagePreviewerVisible && (
|
||||||
|
<ImagePreviewer
|
||||||
|
repoID={repoID}
|
||||||
|
repoInfo={repoInfo}
|
||||||
|
record={currentImageRef.current}
|
||||||
|
table={tagFiles}
|
||||||
|
closeImagePopup={closeImagePreviewer}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -6,3 +6,7 @@
|
|||||||
.tag-list-title .sf-metadata-tags-formatter .sf-metadata-tag-formatter:last-child {
|
.tag-list-title .sf-metadata-tags-formatter .sf-metadata-tag-formatter:last-child {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sf-metadata-tags-main .table-container td.name a {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
@@ -1,20 +1,21 @@
|
|||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import { gettext, siteRoot, thumbnailDefaultSize } from '../../../../utils/constants';
|
import { gettext, siteRoot, thumbnailDefaultSize } from '../../../../utils/constants';
|
||||||
import { getParentDirFromRecord, getRecordIdFromRecord, getFileNameFromRecord, getFileSizedFromRecord,
|
import { getParentDirFromRecord, getRecordIdFromRecord, getFileNameFromRecord, getFileSizedFromRecord,
|
||||||
getFileMTimeFromRecord, getTagsFromRecord,
|
getFileMTimeFromRecord, getTagsFromRecord, getFilePathByRecord,
|
||||||
} from '../../../../metadata/utils/cell';
|
} from '../../../../metadata/utils/cell';
|
||||||
import { Utils } from '../../../../utils/utils';
|
import { Utils } from '../../../../utils/utils';
|
||||||
import FileTagsFormatter from '../../../../metadata/components/cell-formatter/file-tags-formatter';
|
import FileTagsFormatter from '../../../../metadata/components/cell-formatter/file-tags-formatter';
|
||||||
|
import { openFile } from '../../../../metadata/utils/open-file';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
const TagFile = ({ isSelected, repoID, file, onSelectFile }) => {
|
const TagFile = ({ isSelected, repoID, file, onSelectFile, openImagePreview }) => {
|
||||||
const [highlight, setHighlight] = useState(false);
|
const [highlight, setHighlight] = useState(false);
|
||||||
const [isIconLoadError, setIconLoadError] = useState(false);
|
const [isIconLoadError, setIconLoadError] = useState(false);
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ const TagFile = ({ isSelected, repoID, file, onSelectFile }) => {
|
|||||||
|
|
||||||
const mtimeTip = useMemo(() => mtime ? dayjs(mtime).format('dddd, MMMM D, YYYY h:mm:ss A') : '', [mtime]);
|
const mtimeTip = useMemo(() => mtime ? dayjs(mtime).format('dddd, MMMM D, YYYY h:mm:ss A') : '', [mtime]);
|
||||||
const mtimeRelative = useMemo(() => mtime ? dayjs(mtime).fromNow() : '', [mtime]);
|
const mtimeRelative = useMemo(() => mtime ? dayjs(mtime).fromNow() : '', [mtime]);
|
||||||
|
const path = useMemo(() => getFilePathByRecord(repoID, file), [repoID, file]);
|
||||||
|
|
||||||
const displayIcons = useMemo(() => {
|
const displayIcons = useMemo(() => {
|
||||||
const defaultIconUrl = Utils.getFileIconUrl(name);
|
const defaultIconUrl = Utils.getFileIconUrl(name);
|
||||||
@@ -67,6 +69,13 @@ const TagFile = ({ isSelected, repoID, file, onSelectFile }) => {
|
|||||||
setIconLoadError(true);
|
setIconLoadError(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handelClickFileName = useCallback((event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
openFile(repoID, file, () => {
|
||||||
|
openImagePreview(file);
|
||||||
|
});
|
||||||
|
}, [repoID, file, openImagePreview]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
className={classnames({
|
className={classnames({
|
||||||
@@ -76,28 +85,29 @@ const TagFile = ({ isSelected, repoID, file, onSelectFile }) => {
|
|||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
>
|
>
|
||||||
<td className="pl10">
|
<td className="pl10 pr-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="vam"
|
className="vam"
|
||||||
|
style={{ position: 'relative', top: -1 }}
|
||||||
onClick={handleSelected}
|
onClick={handleSelected}
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
aria-label={isSelected ? gettext('Unselect this item') : gettext('Select this item')}
|
aria-label={isSelected ? gettext('Unselect this item') : gettext('Select this item')}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="pl10">
|
<td className="pl-2 pr-2">
|
||||||
<div className="dir-icon">
|
<div className="dir-icon">
|
||||||
<img src={displayIcon} onError={onIconLoadError} className="thumbnail cursor-pointer" alt="" />
|
<img src={displayIcon} onError={onIconLoadError} className="thumbnail cursor-pointer" alt="" />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="name">
|
<td className="name" onClick={handelClickFileName}>
|
||||||
{name}
|
<a href={path}>{name}</a>
|
||||||
</td>
|
</td>
|
||||||
<td className="operation"></td>
|
|
||||||
<td className="tag-list-title">
|
<td className="tag-list-title">
|
||||||
<FileTagsFormatter value={tags} />
|
<FileTagsFormatter value={tags} />
|
||||||
</td>
|
</td>
|
||||||
|
<td className="operation"></td>
|
||||||
<td className="file-size">{size || ''}</td>
|
<td className="file-size">{size || ''}</td>
|
||||||
<td className="last-update" title={mtimeTip}>{mtimeRelative}</td>
|
<td className="last-update" title={mtimeTip}>{mtimeRelative}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -105,4 +115,12 @@ const TagFile = ({ isSelected, repoID, file, onSelectFile }) => {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
TagFile.propTypes = {
|
||||||
|
isSelected: PropTypes.bool,
|
||||||
|
repoID: PropTypes.string,
|
||||||
|
file: PropTypes.object,
|
||||||
|
onSelectFile: PropTypes.func,
|
||||||
|
openImagePreview: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
export default TagFile;
|
export default TagFile;
|
||||||
|
@@ -40,7 +40,7 @@ const TagsManagement = () => {
|
|||||||
if (isLoading) return (<CenteredLoading />);
|
if (isLoading) return (<CenteredLoading />);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="ssf-metadata-tags-wrapper">
|
<div className="sf-metadata-tags-wrapper sf-metadata-tags-management-wrapper">
|
||||||
<div className="sf-metadata-tags-main">
|
<div className="sf-metadata-tags-main">
|
||||||
<div className="sf-metadata-tags-management-container">
|
<div className="sf-metadata-tags-management-container">
|
||||||
<div className="sf-metadata-container-header">
|
<div className="sf-metadata-container-header">
|
||||||
@@ -48,7 +48,7 @@ const TagsManagement = () => {
|
|||||||
<div className="sf-metadata-container-header-actions">
|
<div className="sf-metadata-container-header-actions">
|
||||||
{context.canAddTag() && (
|
{context.canAddTag() && (
|
||||||
<Button color="primary" className="sf-metadata-container-header-add-tag" onClick={openAddTag}>
|
<Button color="primary" className="sf-metadata-container-header-add-tag" onClick={openAddTag}>
|
||||||
{gettext('Add Tag')}
|
{gettext('New Tag')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -18,12 +18,10 @@
|
|||||||
|
|
||||||
.sf-metadata-tags-table-cell-tag .sf-metadata-tag-color {
|
.sf-metadata-tags-table-cell-tag .sf-metadata-tag-color {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 12px;
|
height: 10px;
|
||||||
width: 12px;
|
width: 10px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
position: relative;
|
|
||||||
top: 1px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-metadata-tags-table-row:hover .sf-metadata-tags-table-cell .sf-metadata-tags-table-cell-action {
|
.sf-metadata-tags-table-row:hover .sf-metadata-tags-table-cell .sf-metadata-tags-table-cell-action {
|
||||||
|
@@ -13,7 +13,7 @@ const View = () => {
|
|||||||
|
|
||||||
if (isLoading) return (<CenteredLoading />);
|
if (isLoading) return (<CenteredLoading />);
|
||||||
return (
|
return (
|
||||||
<div className="sf-metadata-tags-wrapper">
|
<div className="sf-metadata-tags-wrapper sf-metadata-tag-files-wrapper">
|
||||||
<div className="sf-metadata-tags-main">
|
<div className="sf-metadata-tags-main">
|
||||||
{errorMessage ? <div className="d-center-middle error">{errorMessage}</div> : renderTagView()}
|
{errorMessage ? <div className="d-center-middle error">{errorMessage}</div> : renderTagView()}
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user