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 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>
); );
} }

View File

@@ -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="" /> :

View File

@@ -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>
) : ( ) : (

View File

@@ -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} />
); );
} }

View File

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

View File

@@ -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 {

View File

@@ -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;
};

View File

@@ -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;
} }
} }

View File

@@ -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;
} }

View File

@@ -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>
); );
}; };

View File

@@ -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);
}); });
}; };

View File

@@ -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);

View File

@@ -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,

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 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}
/> />
); );
})} })}

View File

@@ -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;
} }

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 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,

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 { 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}
/>
)}
</>
); );
}; };

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>