1
0
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:
杨国璇
2024-11-24 20:25:52 +08:00
committed by GitHub
parent 4db4711421
commit e7a4e29239
23 changed files with 286 additions and 366 deletions

View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import DirentNoneView from '../../components/dirent-list-view/dirent-none-view';
import RepoInfoBar from '../../components/repo-info-bar';
import DirentListView from '../../components/dirent-list-view/dirent-list-view';
import Loading from '../loading';
const propTypes = {
path: PropTypes.string.isRequired,
@@ -71,6 +72,9 @@ class DirListView extends React.Component {
onFileTagChanged={this.props.onFileTagChanged}
/>
)}
{this.props.isDirentListLoading ? (
<Loading />
) : (
<DirentListView
path={this.props.path}
currentRepoInfo={this.props.currentRepoInfo}
@@ -90,7 +94,6 @@ class DirListView extends React.Component {
onItemMove={this.props.onItemMove}
onItemCopy={this.props.onItemCopy}
onDirentClick={this.props.onDirentClick}
isDirentListLoading={this.props.isDirentListLoading}
updateDirent={this.props.updateDirent}
isAllItemSelected={this.props.isAllItemSelected}
onAllItemSelected={this.props.onAllItemSelected}
@@ -108,6 +111,7 @@ class DirListView extends React.Component {
getMenuContainerSize={this.props.getMenuContainerSize}
eventBus={this.props.eventBus}
/>
)}
</Fragment>
);
}

View File

@@ -819,17 +819,18 @@ class DirentListItem extends React.Component {
onMouseDown={this.onItemMouseDown}
onContextMenu={this.onItemContextMenu}
>
<td className={`pl10 ${this.state.isDragTipShow ? 'tr-drag-effect' : ''}`}>
<td className={`pl10 pr-2 ${this.state.isDragTipShow ? 'tr-drag-effect' : ''}`}>
<input
type="checkbox"
className="vam"
onClick={this.onItemSelected}
style={{ position: 'relative', top: -1 }}
onChange={() => {}}
checked={isSelected}
aria-label={isSelected ? gettext('Unselect this item') : gettext('Select this item')}
/>
</td>
<td className="pl10">
<td className="pl-2 pr-2">
{dirent.starred !== undefined &&
<i
role="button"
@@ -840,7 +841,7 @@ class DirentListItem extends React.Component {
</i>
}
</td>
<td className="pl10">
<td className="pl-2 pr-2">
<div className="dir-icon">
{(this.canPreview && dirent.encoded_thumbnail_src) ?
<img ref='drag_icon' src={`${siteRoot}${dirent.encoded_thumbnail_src}`} className="thumbnail cursor-pointer" onClick={this.onItemClick} alt="" /> :

View File

@@ -4,7 +4,6 @@ import { siteRoot, gettext, username, enableSeadoc, thumbnailSizeForOriginal, th
import { Utils } from '../../utils/utils';
import TextTranslation from '../../utils/text-translation';
import URLDecorator from '../../utils/url-decorator';
import Loading from '../loading';
import toaster from '../toast';
import ModalPortal from '../modal-portal';
import CreateFile from '../dialog/create-file-dialog';
@@ -27,7 +26,6 @@ const propTypes = {
repoID: PropTypes.string.isRequired,
currentRepoInfo: PropTypes.object,
isAllItemSelected: PropTypes.bool.isRequired,
isDirentListLoading: PropTypes.bool.isRequired,
direntList: PropTypes.array.isRequired,
sortBy: PropTypes.string.isRequired,
sortOrder: PropTypes.string.isRequired,
@@ -82,6 +80,7 @@ class DirentListView extends React.Component {
activeDirent: null,
isListDropTipShow: false,
isShowDirentsDraggablePreview: false,
containerWidth: 0,
};
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;
this.canDrop = modify;
}
this.containerRef = null;
}
componentDidMount() {
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 = () => {
if (!this.state.isImagePopupOpen) return;
let imageItems = this.props.direntList
@@ -122,6 +129,7 @@ class DirentListView extends React.Component {
componentWillUnmount() {
this.unsubscribeEvent();
this.containerRef && this.resizeObserver.unobserve(this.containerRef);
}
freezeItem = () => {
@@ -680,10 +688,7 @@ class DirentListView extends React.Component {
render() {
const { direntList, sortBy, sortOrder } = this.props;
if (this.props.isDirentListLoading) {
return (<Loading />);
}
const { containerWidth } = this.state;
// sort
const sortByName = sortBy == 'name';
@@ -704,13 +709,14 @@ class DirentListView extends React.Component {
onDragOver={this.onTableDragOver}
onDragLeave={this.onTableDragLeave}
onDrop={this.tableDrop}
ref={ref => this.containerRef = ref}
>
{direntList.length > 0 &&
<table className={`table-hover ${isDesktop ? '' : 'table-thead-hidden'}`}>
{isDesktop ? (
<thead onMouseDown={this.onThreadMouseDown} onContextMenu={this.onThreadContextMenu}>
<tr>
<th width="3%" className="pl10">
<th style={{ width: 31 }} className="pl10 pr-2">
<input
type="checkbox"
className="vam"
@@ -721,13 +727,13 @@ class DirentListView extends React.Component {
disabled={direntList.length === 0}
/>
</th>
<th width="3%" className="pl10">{/* icon */}</th>
<th width="5%" className="pl10">{/* star */}</th>
<th width="39%"><a className="d-block table-sort-op" href="#" onClick={this.sortByName}>{gettext('Name')} {sortByName && sortIcon}</a></th>
<th width="6%">{/* tag */}</th>
<th width="18%">{/* operation */}</th>
<th width="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: 32 }} className="pl-2 pr-2">{/* star */}</th>
<th style={{ width: 40 }} className="pl-2 pr-2">{/* icon */}</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 style={{ width: (containerWidth - 103) * 0.06 }}>{/* tag */}</th>
<th style={{ width: (containerWidth - 103) * 0.18 }}>{/* operation */}</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 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>
</thead>
) : (

View File

@@ -37,9 +37,12 @@ const FileNameEditor = React.forwardRef((props, ref) => {
if (mode === EDITOR_TYPE.PREVIEWER) {
const fileType = getFileType();
const repoID = window.sfMetadataContext.getSetting('repoID');
const repoInfo = window.sfMetadataContext.getSetting('repoInfo');
if (fileType === 'image') {
return (
<ImagePreviewer {...props} closeImagePopup={props.onCommitCancel} />
<ImagePreviewer {...props} repoID={repoID} repoInfo={repoInfo} closeImagePopup={props.onCommitCancel} />
);
}

View File

@@ -9,14 +9,11 @@ import { Utils } from '../../../utils/utils';
import { siteRoot, thumbnailSizeForOriginal, fileServerRoot, thumbnailDefaultSize } from '../../../utils/constants';
import { getFileNameFromRecord, getParentDirFromRecord, getRecordIdFromRecord } from '../../utils/cell';
const ImagePreviewer = (props) => {
const { record, table, closeImagePopup } = props;
const ImagePreviewer = ({ record, table, repoID, repoInfo, closeImagePopup }) => {
const [imageIndex, setImageIndex] = useState(0);
const [imageItems, setImageItems] = useState([]);
useEffect(() => {
const repoID = window.sfMetadataContext.getSetting('repoID');
const repoInfo = window.sfMetadataContext.getSetting('repoInfo');
const newImageItems = table.rows
.filter((row) => Utils.imageCheck(getFileNameFromRecord(row)))
.map((row) => {
@@ -40,7 +37,7 @@ const ImagePreviewer = (props) => {
};
});
setImageItems(newImageItems);
}, [table]);
}, [table, repoID, repoInfo]);
useEffect(() => {
if (imageItems.length > 0) {
@@ -98,9 +95,10 @@ const ImagePreviewer = (props) => {
};
ImagePreviewer.propTypes = {
table: PropTypes.object,
column: PropTypes.object,
record: PropTypes.object,
table: PropTypes.object,
repoID: PropTypes.string,
repoInfo: PropTypes.object,
closeImagePopup: PropTypes.func,
};

View File

@@ -39,6 +39,7 @@ const COLUMNS_ICON_NAME = {
[CellType.GEOLOCATION]: 'Geolocation',
[CellType.RATE]: 'Rate',
[CellType.LINK]: 'Link',
[CellType.TAGS]: 'Tag',
};
export {

View File

@@ -1,4 +1,6 @@
import { PRIVATE_COLUMN_KEY, PRIVATE_COLUMN_KEYS } from '../../constants';
import { siteRoot } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
/**
* @param {any} value
@@ -59,3 +61,18 @@ export const getFileMTimeFromRecord = record => {
export const getTagsFromRecord = record => {
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;
};

View File

@@ -2,7 +2,6 @@ import { getFileNameFromRecord, getParentDirFromRecord } from './cell';
import { checkIsDir } from './row';
import { Utils } from '../../utils/utils';
import { siteRoot } from '../../utils/constants';
import { EVENT_BUS_TYPE } from '../../components/common/event-bus-type';
const FILE_TYPE = {
FOLDER: 'folder',
@@ -32,23 +31,28 @@ const _getParentDir = (record) => {
return parentDir;
};
const _generateUrl = (fileName, parentDir) => {
const repoID = window.sfMetadataContext.getSetting('repoID');
const _generateUrl = (repoID, fileName, parentDir) => {
const path = Utils.encodePath(Utils.joinPath(parentDir, fileName));
return `${siteRoot}lib/${repoID}/file${path}`;
};
const _openUrl = (url) => {
const isWeChat = Utils.isWeChat();
if (isWeChat) {
location.href = url;
return;
}
window.open(url);
};
const _openMarkdown = (fileName, parentDir, eventBus) => {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.OPEN_MARKDOWN, parentDir, fileName);
const _openMarkdown = (repoID, fileName, parentDir) => {
const url = _generateUrl(repoID, fileName, parentDir);
_openUrl(url);
};
const _openByNewWindow = (fileName, parentDir, fileType) => {
const _openByNewWindow = (repoID, fileName, parentDir, fileType) => {
if (!fileType) {
const url = _generateUrl(fileName, parentDir);
const url = _generateUrl(repoID, fileName, parentDir);
_openUrl(url);
return;
}
@@ -59,16 +63,16 @@ const _openByNewWindow = (fileName, parentDir, fileType) => {
_openUrl(window.location.origin + pathname + Utils.encodePath(Utils.joinPath(parentDir, fileName)));
};
const _openSdoc = (fileName, parentDir) => {
const url = _generateUrl(fileName, parentDir);
const _openSdoc = (repoID, fileName, parentDir) => {
const url = _generateUrl(repoID, fileName, parentDir);
_openUrl(url);
};
const _openOthers = (fileName, parentDir, fileType) => {
_openByNewWindow(fileName, parentDir, fileType);
const _openOthers = (repoID, fileName, parentDir, fileType) => {
_openByNewWindow(repoID, fileName, parentDir, fileType);
};
export const openFile = (record, eventBus, _openImage = () => {}) => {
export const openFile = (repoID, record, _openImage = () => {}) => {
if (!record) return;
const fileName = getFileNameFromRecord(record);
const isDir = checkIsDir(record);
@@ -77,11 +81,11 @@ export const openFile = (record, eventBus, _openImage = () => {}) => {
switch (fileType) {
case FILE_TYPE.MARKDOWN: {
_openMarkdown(fileName, parentDir, eventBus);
_openMarkdown(repoID, fileName, parentDir);
break;
}
case FILE_TYPE.SDOC: {
_openSdoc(fileName, parentDir);
_openSdoc(repoID, fileName, parentDir);
break;
}
case FILE_TYPE.IMAGE: {
@@ -89,7 +93,7 @@ export const openFile = (record, eventBus, _openImage = () => {}) => {
break;
}
default: {
_openOthers(fileName, parentDir, fileType);
_openOthers(repoID, fileName, parentDir, fileType);
break;
}
}

View File

@@ -12,7 +12,7 @@
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;
}

View File

@@ -176,7 +176,8 @@ const Boards = ({ modifyRecord, modifyColumnData, onCloseSettings }) => {
}, []);
const onOpenFile = useCallback((record) => {
openFile(record, window.sfMetadataContext.eventBus, () => {
const repoID = window.sfMetadataContext.getSetting('repoID');
openFile(repoID, record, () => {
currentImageRef.current = record;
setImagePreviewerVisible(true);
});
@@ -222,6 +223,8 @@ const Boards = ({ modifyRecord, modifyColumnData, onCloseSettings }) => {
}, [isDirentDetailShow]);
const isEmpty = boards.length === 0;
const repoID = window.sfMetadataContext.getSetting('repoID');
const repoInfo = window.sfMetadataContext.getSetting('repoInfo');
return (
<div
@@ -264,7 +267,15 @@ const Boards = ({ modifyRecord, modifyColumnData, onCloseSettings }) => {
)}
{!readonly && (<AddBoard groupByColumn={groupByColumn}/>)}
</div>
{isImagePreviewerVisible && (<ImagePreviewer record={currentImageRef.current} table={metadata} closeImagePopup={closeImagePreviewer} />)}
{isImagePreviewerVisible && (
<ImagePreviewer
repoID={repoID}
repoInfo={repoInfo}
record={currentImageRef.current}
table={metadata}
closeImagePopup={closeImagePreviewer}
/>
)}
</div>
);
};

View File

@@ -21,7 +21,8 @@ const FileNameOperationBtn = ({ column, record, ...props }) => {
const handelClick = (event) => {
event.stopPropagation();
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);
});
};

View File

@@ -447,14 +447,6 @@ class LibContentView extends React.Component {
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) => {
let repoID = this.props.repoID;
@@ -517,10 +509,6 @@ class LibContentView extends React.Component {
path: filePath,
viewId: viewId,
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)}`;
window.history.pushState({ url: url, path: '' }, '', url);

View File

@@ -45,6 +45,7 @@ export const TagViewProvider = ({ repoID, tagID, children, ...params }) => {
tagFiles,
repoID,
tagID,
repoInfo: params.repoInfo,
deleteFilesCallback: params.deleteFilesCallback,
renameFileCallback: params.renameFileCallback,
updateCurrentDirent: params.updateCurrentDirent,

View File

@@ -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 { useTags } from '../hooks';
import Tag from './tag';
@@ -19,8 +19,7 @@ const updateFavicon = () => {
const TagsTreeView = ({ userPerm, currentPath }) => {
const originalTitle = useRef('');
// const {} = { }
const { tagsData, selectTag, deleteTags, duplicateTag, updateTag } = useTags();
const { tagsData, selectTag } = useTags();
const tags = useMemo(() => {
if (!tagsData) return [];
@@ -32,16 +31,6 @@ const TagsTreeView = ({ userPerm, currentPath }) => {
return true;
}, [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(() => {
originalTitle.current = document.title;
}, []);
@@ -105,13 +94,8 @@ const TagsTreeView = ({ userPerm, currentPath }) => {
<Tag
key={id}
tag={tag}
tags={tags}
userPerm={userPerm}
isSelected={isSelected}
onClick={(tag) => selectTag(tag, isSelected)}
onDelete={() => deleteTag(id, isSelected)}
onCopy={() => duplicateTag(id)}
onUpdateTag={updateTag}
/>
);
})}

View File

@@ -1,8 +1,8 @@
.tag-tree-node .tag-tree-node-color {
height: 12px;
width: 12px;
height: 10px;
width: 10px;
border-radius: 50%;
transform: translateY(2px);
transform: translateY(3px);
}
.tag-tree-node .tag-tree-node-text {
@@ -14,11 +14,22 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.tag-tree-node .tag-tree-node-text .tag-tree-node-count {
color: #666;
font-size: 14px;
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;
}

View File

@@ -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 classnames from 'classnames';
import { Input } from 'reactstrap';
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 { getTagColor, getTagName, getTagFilesCount } from '../../utils';
import './index.css';
const Tag = ({
userPerm,
isSelected,
tag,
tags,
onClick,
onDelete,
onCopy,
onUpdateTag,
}) => {
const Tag = ({ isSelected, tag, onClick }) => {
const tagName = useMemo(() => getTagName(tag), [tag]);
const tagColor = useMemo(() => getTagColor(tag), [tag]);
const tagId = useMemo(() => getTagId(tag), [tag]);
const tagCount = useMemo(() => getTagFilesCount(tag), [tag]);
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(() => {
if (freeze) return;
setHighlight(true);
}, [freeze]);
}, []);
const onMouseOver = useCallback(() => {
if (freeze) return;
setHighlight(true);
}, [freeze]);
}, []);
const onMouseLeave = useCallback(() => {
if (freeze) return;
setHighlight(false);
}, [freeze]);
const freezeItem = useCallback(() => {
setFreeze(true);
}, []);
const unfreezeItem = useCallback(() => {
setFreeze(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 (
<>
<div
className={classnames('tree-node-inner text-nowrap tag-tree-node', { 'tree-node-inner-hover': highlight, 'tree-node-hight-light': isSelected })}
title={tagName}
title={`${tagName} (${tagCount})`}
onMouseEnter={onMouseEnter}
onMouseOver={onMouseOver}
onMouseLeave={onMouseLeave}
onClick={() => onClick(tag)}
>
<div className="tree-node-text tag-tree-node-text">
<div className="tag-tree-node-name">
{isRenaming ? (
<Input
innerRef={inputRef}
className="sf-metadata-view-input mt-0"
value={inputValue}
onChange={onChange}
autoFocus={true}
onBlur={() => setRenaming(false)}
onClick={onInputClick}
onKeyDown={onKeyDown}
/>
) : (
<>{tagName}</>
)}
</div>
<div className="tag-tree-node-count">{` (${tagCount})`}</div>
<div className="tag-tree-node-name">{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>
</>
);
};
Tag.propTypes = {
canDelete: PropTypes.bool,
isSelected: PropTypes.bool,
tag: PropTypes.object,
onClick: PropTypes.func,

View File

@@ -0,0 +1,3 @@
.sf-metadata-tag-files-wrapper .sf-metadata-tags-main {
overflow: auto !important;
}

View File

@@ -1,15 +1,21 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback, useState, useRef, useEffect } from 'react';
import { useTagView } from '../../hooks';
import { gettext } from '../../../utils/constants';
import TagFile from './tag-file';
import { getRecordIdFromRecord } from '../../../metadata/utils/cell';
import EmptyTip from '../../../components/empty-tip';
import ImagePreviewer from '../../../metadata/components/cell-formatter/image-previewer';
import './index.css';
const TagFiles = () => {
const { tagFiles, repoID } = useTagView();
const { tagFiles, repoID, repoInfo } = useTagView();
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) => {
if (event.button === 2) {
@@ -45,6 +51,31 @@ const TagFiles = () => {
}
}, [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) {
return (<EmptyTip text={gettext('No files')} />);
}
@@ -52,11 +83,12 @@ const TagFiles = () => {
const isSelectedAll = selectedFiles && selectedFiles.length === tagFiles.rows.length;
return (
<div className="table-container">
<>
<div className="table-container" ref={containerRef}>
<table className="table-hover">
<thead onMouseDown={onThreadMouseDown} onContextMenu={onThreadContextMenu}>
<tr>
<th width="3%" className="pl10">
<th style={{ width: 31 }} className="pl10 pr-2">
<input
type="checkbox"
className="vam"
@@ -66,12 +98,12 @@ const TagFiles = () => {
disabled={tagFiles.rows.length === 0}
/>
</th>
<th width="3%" className="pl10">{/* icon */}</th>
<th width="45%"><a className="d-block table-sort-op" href="#">{gettext('Name')}</a></th>
<th width="18%">{/* operation */}</th>
<th width="6%">{/* tag */}</th>
<th width="11%"><a className="d-block table-sort-op" href="#">{gettext('Size')}</a></th>
<th width="15%"><a className="d-block table-sort-op" href="#">{gettext('Last Update')}</a></th>
<th style={{ width: 40 }} className="pl-2 pr-2">{/* icon */}</th>
<th style={{ width: (containerWidth - 71) * 0.5 }}><a className="d-block table-sort-op" href="#">{gettext('Name')}</a></th>
<th style={{ width: (containerWidth - 71) * 0.06 }}>{/* tag */}</th>
<th style={{ width: (containerWidth - 71) * 0.18 }}>{/* operation */}</th>
<th style={{ width: (containerWidth - 71) * 0.11 }}><a className="d-block table-sort-op" href="#">{gettext('Size')}</a></th>
<th style={{ width: (containerWidth - 71) * 0.15 }}><a className="d-block table-sort-op" href="#">{gettext('Last Update')}</a></th>
</tr>
</thead>
<tbody>
@@ -84,11 +116,22 @@ const TagFiles = () => {
isSelected={selectedFiles && selectedFiles.includes(fileId)}
file={file}
onSelectFile={onSelectFile}
openImagePreview={openImagePreview}
/>);
})}
</tbody>
</table>
</div>
{isImagePreviewerVisible && (
<ImagePreviewer
repoID={repoID}
repoInfo={repoInfo}
record={currentImageRef.current}
table={tagFiles}
closeImagePopup={closeImagePreviewer}
/>
)}
</>
);
};

View File

@@ -6,3 +6,7 @@
.tag-list-title .sf-metadata-tags-formatter .sf-metadata-tag-formatter:last-child {
margin-right: 0;
}
.sf-metadata-tags-main .table-container td.name a {
word-break: break-word;
}

View File

@@ -1,20 +1,21 @@
import React, { useCallback, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { gettext, siteRoot, thumbnailDefaultSize } from '../../../../utils/constants';
import { getParentDirFromRecord, getRecordIdFromRecord, getFileNameFromRecord, getFileSizedFromRecord,
getFileMTimeFromRecord, getTagsFromRecord,
getFileMTimeFromRecord, getTagsFromRecord, getFilePathByRecord,
} from '../../../../metadata/utils/cell';
import { Utils } from '../../../../utils/utils';
import FileTagsFormatter from '../../../../metadata/components/cell-formatter/file-tags-formatter';
import { openFile } from '../../../../metadata/utils/open-file';
import './index.css';
dayjs.extend(relativeTime);
const TagFile = ({ isSelected, repoID, file, onSelectFile }) => {
const TagFile = ({ isSelected, repoID, file, onSelectFile, openImagePreview }) => {
const [highlight, setHighlight] = 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 mtimeRelative = useMemo(() => mtime ? dayjs(mtime).fromNow() : '', [mtime]);
const path = useMemo(() => getFilePathByRecord(repoID, file), [repoID, file]);
const displayIcons = useMemo(() => {
const defaultIconUrl = Utils.getFileIconUrl(name);
@@ -67,6 +69,13 @@ const TagFile = ({ isSelected, repoID, file, onSelectFile }) => {
setIconLoadError(true);
}, []);
const handelClickFileName = useCallback((event) => {
event.preventDefault();
openFile(repoID, file, () => {
openImagePreview(file);
});
}, [repoID, file, openImagePreview]);
return (
<tr
className={classnames({
@@ -76,28 +85,29 @@ const TagFile = ({ isSelected, repoID, file, onSelectFile }) => {
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<td className="pl10">
<td className="pl10 pr-2">
<input
type="checkbox"
className="vam"
style={{ position: 'relative', top: -1 }}
onClick={handleSelected}
onChange={() => {}}
checked={isSelected}
aria-label={isSelected ? gettext('Unselect this item') : gettext('Select this item')}
/>
</td>
<td className="pl10">
<td className="pl-2 pr-2">
<div className="dir-icon">
<img src={displayIcon} onError={onIconLoadError} className="thumbnail cursor-pointer" alt="" />
</div>
</td>
<td className="name">
{name}
<td className="name" onClick={handelClickFileName}>
<a href={path}>{name}</a>
</td>
<td className="operation"></td>
<td className="tag-list-title">
<FileTagsFormatter value={tags} />
</td>
<td className="operation"></td>
<td className="file-size">{size || ''}</td>
<td className="last-update" title={mtimeTip}>{mtimeRelative}</td>
</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;

View File

@@ -40,7 +40,7 @@ const TagsManagement = () => {
if (isLoading) return (<CenteredLoading />);
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-management-container">
<div className="sf-metadata-container-header">
@@ -48,7 +48,7 @@ const TagsManagement = () => {
<div className="sf-metadata-container-header-actions">
{context.canAddTag() && (
<Button color="primary" className="sf-metadata-container-header-add-tag" onClick={openAddTag}>
{gettext('Add Tag')}
{gettext('New Tag')}
</Button>
)}
</div>

View File

@@ -18,12 +18,10 @@
.sf-metadata-tags-table-cell-tag .sf-metadata-tag-color {
display: inline-block;
height: 12px;
width: 12px;
height: 10px;
width: 10px;
border-radius: 50%;
margin-right: 8px;
position: relative;
top: 1px;
}
.sf-metadata-tags-table-row:hover .sf-metadata-tags-table-cell .sf-metadata-tags-table-cell-action {

View File

@@ -13,7 +13,7 @@ const View = () => {
if (isLoading) return (<CenteredLoading />);
return (
<div className="sf-metadata-tags-wrapper">
<div className="sf-metadata-tags-wrapper sf-metadata-tag-files-wrapper">
<div className="sf-metadata-tags-main">
{errorMessage ? <div className="d-center-middle error">{errorMessage}</div> : renderTagView()}
</div>