1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-12 13:24:52 +00:00

Feature/tags view file operations (#7464)

* add basic file operations

* add tag view toolbar operations

* add share and rename

* optimize

* optimize

* update codes

---------

Co-authored-by: zhouwenxuan <aries@Mac.local>
Co-authored-by: renjie-run <rj.aiyayao@gmail.com>
This commit is contained in:
Aries
2025-02-25 16:48:59 +08:00
committed by GitHub
parent 49896f53ef
commit 30d0a34afa
14 changed files with 890 additions and 98 deletions

View File

@@ -84,6 +84,7 @@ const propTypes = {
eventBus: PropTypes.object,
updateCurrentDirent: PropTypes.func.isRequired,
updateCurrentPath: PropTypes.func,
toggleShowDirentToolbar: PropTypes.func,
};
class DirColumnView extends React.Component {
@@ -225,6 +226,11 @@ class DirColumnView extends React.Component {
renameFileCallback={this.props.renameFileCallback}
updateCurrentDirent={this.props.updateCurrentDirent}
updateCurrentPath={this.props.updateCurrentPath}
moveFileCallback={this.props.moveFileCallback}
copyFileCallback={this.props.copyFileCallback}
convertFileCallback={this.props.convertFileCallback}
addFolderCallback={this.props.onAddFolder}
toggleShowDirentToolbar={this.props.toggleShowDirentToolbar}
/>
)}
{currentMode === LIST_MODE && (

View File

@@ -0,0 +1,143 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import ItemDropdownMenu from '../dropdown-menu/item-dropdown-menu';
import { gettext } from '../../utils/constants';
import { EVENT_BUS_TYPE } from '../../metadata/constants';
import TextTranslation from '../../utils/text-translation';
import { getFileById, getFileName, getTagFileOperationList } from '../../tag/utils/file';
const TagFilesToolbar = ({ currentRepoInfo }) => {
const [selectedFileIds, setSelectedFileIds] = useState([]);
const tagFilesRef = useRef([]);
const canModify = window.sfTagsDataContext && window.sfTagsDataContext.canModify();
const eventBus = window.sfTagsDataContext && window.sfTagsDataContext.eventBus;
const selectedFilesLen = useMemo(() => {
return selectedFileIds.length;
}, [selectedFileIds]);
const unSelect = useCallback(() => {
setSelectedFileIds([]);
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.UNSELECT_TAG_FILES);
}, [eventBus]);
const moveTagFile = useCallback(() => {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.MOVE_TAG_FILE);
}, [eventBus]);
const copyTagFile = useCallback(() => {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.COPY_TAG_FILE);
}, [eventBus]);
const deleteTagFiles = useCallback(() => {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.DELETE_TAG_FILES);
}, [eventBus]);
const downloadTagFiles = useCallback(() => {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.DOWNLOAD_TAG_FILES);
}, [eventBus]);
const getMenuList = useCallback(() => {
if (selectedFilesLen > 1) return [];
const fileId = selectedFileIds[0];
const file = getFileById(tagFilesRef.current, fileId);
const fileName = getFileName(file);
const allOperations = getTagFileOperationList(fileName, currentRepoInfo, canModify);
const excludesOperations = ['Move', 'Copy', 'Delete', 'Download'];
const validOperations = allOperations.filter((item) => {
return excludesOperations.indexOf(item.key) == -1;
});
return validOperations;
}, [canModify, currentRepoInfo, selectedFileIds, selectedFilesLen]);
const onMenuItemClick = useCallback((operation) => {
switch (operation) {
case TextTranslation.SHARE.key:
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.SHARE_TAG_FILE);
break;
case TextTranslation.RENAME.key:
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_RENAME_DIALOG);
break;
case TextTranslation.HISTORY.key:
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.FILE_HISTORY);
break;
case TextTranslation.ACCESS_LOG.key:
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.FILE_ACCESS_LOG);
break;
case TextTranslation.OPEN_VIA_CLIENT.key:
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.OPEN_VIA_CLIENT);
break;
case TextTranslation.CONVERT_TO_SDOC.key:
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.CONVERT_FILE, 'sdoc');
break;
case TextTranslation.CONVERT_TO_MARKDOWN.key: {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.CONVERT_FILE, 'markdown');
break;
}
case TextTranslation.CONVERT_TO_DOCX.key: {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.CONVERT_FILE, 'docx');
break;
}
case TextTranslation.EXPORT_DOCX.key: {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.EXPORT_DOCX);
break;
}
case TextTranslation.EXPORT_SDOC.key: {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.EXPORT_SDOC);
break;
}
default:
break;
}
}, [eventBus]);
useEffect(() => {
const unsubscribeSelectedFileIds = eventBus && eventBus.subscribe(EVENT_BUS_TYPE.SELECT_TAG_FILES, (ids, tagFiles) => {
tagFilesRef.current = tagFiles || [];
setSelectedFileIds(ids);
});
return () => {
unsubscribeSelectedFileIds && unsubscribeSelectedFileIds();
};
}, []);
return (
<div className="selected-dirents-toolbar">
<span className="cur-view-path-btn px-2" onClick={unSelect}>
<span className="sf3-font-x-01 sf3-font mr-2" aria-label={gettext('Unselect')} title={gettext('Unselect')}></span>
<span>{selectedFilesLen}{' '}{gettext('selected')}</span>
</span>
{(selectedFilesLen === 1 && canModify) &&
<>
<span className="cur-view-path-btn" onClick={moveTagFile}>
<span className="sf3-font-move1 sf3-font" aria-label={gettext('Move')} title={gettext('Move')}></span>
</span>
<span className="cur-view-path-btn" onClick={copyTagFile}>
<span className="sf3-font-copy1 sf3-font" aria-label={gettext('Copy')} title={gettext('Copy')}></span>
</span>
</>
}
{canModify &&
<>
<span className="cur-view-path-btn" onClick={deleteTagFiles}>
<span className="sf3-font-delete1 sf3-font" aria-label={gettext('Delete')} title={gettext('Delete')}></span>
</span>
<span className="cur-view-path-btn" onClick={downloadTagFiles}>
<span className="sf3-font-download1 sf3-font" aria-label={gettext('Download')} title={gettext('Download')}></span>
</span>
</>
}
{selectedFilesLen === 1 &&
<ItemDropdownMenu
item={{}}
toggleClass={'cur-view-path-btn sf3-font-more-vertical sf3-font'}
onMenuItemClick={onMenuItemClick}
getMenuList={getMenuList}
/>
}
</div>
);
};
export default TagFilesToolbar;

View File

@@ -39,10 +39,11 @@ const FileNameEditor = React.forwardRef((props, ref) => {
const fileType = getFileType();
const repoID = window.sfMetadataContext.getSetting('repoID');
const repoInfo = window.sfMetadataContext.getSetting('repoInfo');
const canDelete = window.sfMetadataContext.checkCanDeleteRow();
if (fileType === 'image') {
return (
<ImagePreviewer {...props} repoID={repoID} repoInfo={repoInfo} closeImagePopup={props.onCommitCancel} />
<ImagePreviewer {...props} repoID={repoID} repoInfo={repoInfo} closeImagePopup={props.onCommitCancel} canDelete={canDelete} />
);
}

View File

@@ -9,7 +9,7 @@ import { Utils } from '../../../utils/utils';
import { siteRoot, thumbnailSizeForOriginal, fileServerRoot, thumbnailDefaultSize } from '../../../utils/constants';
import { getFileNameFromRecord, getParentDirFromRecord, getRecordIdFromRecord } from '../../utils/cell';
const ImagePreviewer = ({ record, table, repoID, repoInfo, closeImagePopup, deleteRecords }) => {
const ImagePreviewer = ({ record, table, repoID, repoInfo, closeImagePopup, deleteRecords, canDelete }) => {
const [imageIndex, setImageIndex] = useState(0);
const [imageItems, setImageItems] = useState([]);
@@ -95,8 +95,6 @@ const ImagePreviewer = ({ record, table, repoID, repoInfo, closeImagePopup, dele
}
};
const canDelete = window.sfMetadataContext.checkCanDeleteRow();
return (
<ModalPortal>
<ImageDialog
@@ -119,6 +117,7 @@ ImagePreviewer.propTypes = {
repoInfo: PropTypes.object,
closeImagePopup: PropTypes.func,
deleteRecords: PropTypes.func,
canDelete: PropTypes.bool,
};
export default ImagePreviewer;

View File

@@ -78,4 +78,24 @@ export const EVENT_BUS_TYPE = {
// map
MODIFY_MAP_TYPE: 'modify_map_type',
MAP_VIEW: 'map_view',
// tag file
MOVE_TAG_FILE: 'move_tag_file',
COPY_TAG_FILE: 'copy_tag_file',
RENAME_TAG_FILE: 'rename_tag_file',
TOGGLE_RENAME_DIALOG: 'toggle_rename_dialog',
SHARE_TAG_FILE: 'share_tag_file',
TOGGLE_ZIP_DIALOG: 'toggle_zip_dialog',
DOWNLOAD_TAG_FILES: 'download_tag_files',
DELETE_TAG_FILES: 'delete_tag_files',
SELECT_TAG_FILES: 'select_tag_files',
UNSELECT_TAG_FILES: 'unselect_tag_files',
// file
FILE_HISTORY: 'file_history',
FILE_ACCESS_LOG: 'file_access_log',
OPEN_VIA_CLIENT: 'open_via_client',
CONVERT_FILE: 'convert_file',
EXPORT_DOCX: 'export_docx',
EXPORT_SDOC: 'export_sdoc',
};

View File

@@ -30,6 +30,7 @@ import DirTool from '../../components/cur-dir-path/dir-tool';
import Detail from '../../components/dirent-detail';
import DirColumnView from '../../components/dir-view-mode/dir-column-view';
import SelectedDirentsToolbar from '../../components/toolbar/selected-dirents-toolbar';
import TagFilesToolbar from '../../components/toolbar/tag-files-toolbar';
import '../../css/lib-content-view.css';
@@ -1337,7 +1338,7 @@ class LibContentView extends React.Component {
this.updateMoveCopyTreeNode(copyToDirentPath);
}
if (copyToDirentPath === nodeParentPath && this.state.currentMode !== METADATA_MODE) {
if (copyToDirentPath === nodeParentPath && this.state.currentMode !== METADATA_MODE && this.state.currentMode !== TAGS_MODE) {
this.loadDirentList(this.state.path);
}
@@ -1382,31 +1383,36 @@ class LibContentView extends React.Component {
});
};
convertFileAjaxCallback = ({ newName, parentDir, size, path, error }) => {
if (error) {
let errMessage = Utils.getErrorMsg(error);
if (errMessage === gettext('Error')) {
const name = Utils.getFileName(path);
errMessage = gettext('Failed to convert {name}.').replace('{name}', name);
}
toaster.danger(errMessage, { 'id': 'conversion' });
return;
}
const new_path = parentDir + '/' + newName;
const parentPath = Utils.getDirName(new_path);
if (this.state.isTreePanelShown) {
this.addNodeToTree(newName, parentPath, 'file');
}
this.addDirent(newName, 'file', size);
const message = gettext('Successfully converted the file.');
toaster.success(message, { 'id': 'conversion' });
};
onConvertItem = (dirent, dstType) => {
let path = Utils.joinPath(this.state.path, dirent.name);
let repoID = this.props.repoID;
toaster.notifyInProgress(gettext('Converting, please wait...'), { 'id': 'conversion' });
seafileAPI.convertFile(repoID, path, dstType).then((res) => {
let newFileName = res.data.obj_name;
let parentDir = res.data.parent_dir;
let new_path = parentDir + '/' + newFileName;
let parentPath = Utils.getDirName(new_path);
if (this.state.isTreePanelShown) {
this.addNodeToTree(newFileName, parentPath, 'file');
}
this.addDirent(newFileName, 'file', res.data.size);
let message = gettext('Successfully converted the file.');
toaster.success(message, { 'id': 'conversion' });
const newFileName = res.data.obj_name;
const parentDir = res.data.parent_dir;
this.convertFileAjaxCallback({ newName: newFileName, parentDir, size: res.data.size });
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
if (errMessage === gettext('Error')) {
let name = Utils.getFileName(path);
errMessage = gettext('Failed to convert {name}.').replace('{name}', name);
}
toaster.danger(errMessage, { 'id': 'conversion' });
this.convertFileAjaxCallback({ path, error });
});
};
@@ -2164,6 +2170,10 @@ class LibContentView extends React.Component {
this.setState({ path });
};
toggleShowDirentToolbar = (isDirentSelected) => {
this.setState({ isDirentSelected });
};
render() {
const { repoID } = this.props;
let { currentRepoInfo, userPerm, isCopyMoveProgressDialogShow, isDeleteFolderDialogOpen, errorMsg,
@@ -2251,7 +2261,10 @@ class LibContentView extends React.Component {
'w-100': !isDesktop,
'animation-children': isDirentSelected
})}>
{isDirentSelected ?
{isDirentSelected ? (
this.state.currentMode === TAGS_MODE ?
<TagFilesToolbar currentRepoInfo={this.state.currentRepoInfo} />
:
<SelectedDirentsToolbar
repoID={this.props.repoID}
path={this.state.path}
@@ -2277,7 +2290,7 @@ class LibContentView extends React.Component {
onItemConvert={this.onConvertItem}
onAddFolder={this.onAddFolder}
/>
:
) : (
<CurDirPath
currentRepoInfo={this.state.currentRepoInfo}
repoID={this.props.repoID}
@@ -2310,7 +2323,7 @@ class LibContentView extends React.Component {
loadDirentList={this.loadDirentList}
onAddFolderNode={this.onAddFolder}
/>
}
)}
</div>
{isDesktop &&
<div className="cur-view-path-right py-1">
@@ -2389,6 +2402,7 @@ class LibContentView extends React.Component {
moveFileCallback={this.moveItemsAjaxCallback}
onItemCopy={this.onCopyItem}
copyFileCallback={this.copyItemsAjaxCallback}
convertFileCallback={this.convertFileAjaxCallback}
onItemConvert={this.onConvertItem}
onDirentClick={this.onDirentClick}
updateDirent={this.updateDirent}
@@ -2405,6 +2419,7 @@ class LibContentView extends React.Component {
eventBus={this.props.eventBus}
updateCurrentDirent={this.updateCurrentDirent}
updateCurrentPath={this.updatePath}
toggleShowDirentToolbar={this.toggleShowDirentToolbar}
/>
:
<div className="message err-tip">{gettext('Folder does not exist.')}</div>

View File

@@ -0,0 +1,8 @@
export const TAG_FILE_KEY = {
ID: '_id',
NAME: '_name',
SIZE: '_size',
PARENT_DIR: '_parent_dir',
FILE_MTIME: '_file_mtime',
TAGS: '_tags',
};

View File

@@ -1,19 +1,34 @@
import React, { useCallback, useContext, useEffect, useState } from 'react';
import toaster from '../../components/toast';
import { Utils } from '../../utils/utils';
import { metadataAPI } from '../../metadata';
import tagsAPI from '../api';
import { useTags } from './tags';
import { getTreeNodeById, getTreeNodeByKey } from '../../components/sf-table/utils/tree';
import { getAllChildTagsIdsFromNode } from '../utils/tree';
import { seafileAPI } from '../../utils/seafile-api';
import { TAG_FILE_KEY } from '../constants/file';
import { EVENT_BUS_TYPE } from '../../metadata/constants';
import { getFileById } from '../utils/file';
import { getRowById } from '../../metadata/utils/table';
import { getTagFilesLinks } from '../utils/cell';
import { PRIVATE_COLUMN_KEY } from '../constants';
import URLDecorator from '../../utils/url-decorator';
import { fileServerRoot, useGoFileserver } from '../../utils/constants';
// This hook provides content related to seahub interaction, such as whether to enable extended attributes, views data, etc.
const TagViewContext = React.createContext(null);
export const TagViewProvider = ({ repoID, tagID, nodeKey, children, ...params }) => {
export const TagViewProvider = ({
repoID, tagID, nodeKey, children, moveFileCallback, copyFileCallback, addFolderCallback, deleteFilesCallback, renameFileCallback, convertFileCallback,
toggleShowDirentToolbar, ...params
}) => {
const [isLoading, setLoading] = useState(true);
const [tagFiles, setTagFiles] = useState(null);
const [errorMessage, setErrorMessage] = useState(null);
const [selectedFileIds, setSelectedFileIds] = useState([]);
const { tagsData } = useTags();
const { tagsData, updateLocalTags } = useTags();
const getChildTagsIds = useCallback((tagID, nodeKey) => {
let displayNode = null;
@@ -26,6 +41,121 @@ export const TagViewProvider = ({ repoID, tagID, nodeKey, children, ...params })
return getAllChildTagsIdsFromNode(displayNode);
}, [tagsData]);
const updateSelectedFileIds = useCallback((ids) => {
toggleShowDirentToolbar(ids.length > 0);
setSelectedFileIds(ids);
setTimeout(() => {
window.sfTagsDataContext && window.sfTagsDataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_TAG_FILES, ids, tagFiles);
}, 0);
}, [setSelectedFileIds, tagFiles, toggleShowDirentToolbar]);
const moveTagFile = useCallback((targetRepo, dirent, targetParentPath, sourceParentPath, isByDialog) => {
seafileAPI.moveDir(repoID, targetRepo.repo_id, targetParentPath, sourceParentPath, dirent.name).then(res => {
moveFileCallback && moveFileCallback(repoID, targetRepo, dirent, targetParentPath, sourceParentPath, res.data.task_id || null, isByDialog);
updateSelectedFileIds([]);
});
}, [repoID, moveFileCallback, updateSelectedFileIds]);
const copyTagFile = useCallback((targetRepo, dirent, targetParentPath, sourceParentPath, isByDialog) => {
seafileAPI.copyDir(repoID, targetRepo.repo_id, targetParentPath, sourceParentPath, dirent.name).then(res => {
copyFileCallback && copyFileCallback(repoID, targetRepo, dirent, targetParentPath, sourceParentPath, res.data.task_id || null, isByDialog);
updateSelectedFileIds([]);
});
}, [repoID, copyFileCallback, updateSelectedFileIds]);
const deleteTagFiles = useCallback(() => {
const files = selectedFileIds.map(id => getFileById(tagFiles, id));
const paths = files.map(f => Utils.joinPath(f[TAG_FILE_KEY.PARENT_DIR], f[TAG_FILE_KEY.NAME]));
const fileNames = files.map(f => f[TAG_FILE_KEY.NAME]);
metadataAPI.batchDeleteFiles(repoID, paths).then(() => {
const updatedTags = new Set();
files.forEach(file => {
file._tags.forEach(tag => updatedTags.add(tag.row_id));
});
let idTagUpdates = {};
updatedTags.forEach(tagID => {
const row = getRowById(tagsData, tagID);
const oldTagFileLinks = getTagFilesLinks(row);
if (Array.isArray(oldTagFileLinks) && oldTagFileLinks.length > 0) {
const newTagFileLinks = oldTagFileLinks.filter(link => !selectedFileIds.includes(link.row_id));
const update = { [PRIVATE_COLUMN_KEY.TAG_FILE_LINKS]: newTagFileLinks };
idTagUpdates[tagID] = update;
}
});
updateLocalTags([...updatedTags], idTagUpdates);
setTagFiles(prevTagFiles => ({
...prevTagFiles,
rows: prevTagFiles.rows.filter(row => !selectedFileIds.includes(row[TAG_FILE_KEY.ID])),
}));
deleteFilesCallback && deleteFilesCallback(paths, fileNames);
updateSelectedFileIds([]);
});
}, [repoID, tagsData, tagFiles, selectedFileIds, updateLocalTags, deleteFilesCallback, updateSelectedFileIds]);
const getDownloadTarget = useCallback(() => {
if (!selectedFileIds.length) return [];
return selectedFileIds.map(id => {
const file = getFileById(tagFiles, id);
const path = file[TAG_FILE_KEY.PARENT_DIR] === '/' ? file[TAG_FILE_KEY.NAME] : `${file[TAG_FILE_KEY.PARENT_DIR]}/${file[TAG_FILE_KEY.NAME]}`;
return path;
});
}, [tagFiles, selectedFileIds]);
const downloadTagFiles = useCallback(() => {
if (!selectedFileIds.length) return;
if (selectedFileIds.length === 1) {
const file = getFileById(tagFiles, selectedFileIds[0]);
const filePath = Utils.joinPath(file[TAG_FILE_KEY.PARENT_DIR], file[TAG_FILE_KEY.NAME]);
const url = URLDecorator.getUrl({ type: 'download_file_url', repoID, filePath });
location.href = url;
return;
}
if (!useGoFileserver) {
window.sfTagsDataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_ZIP_DIALOG);
return;
}
const target = getDownloadTarget();
metadataAPI.zipDownload(repoID, '/', target).then(res => {
const zipToken = res.data['zip_token'];
location.href = `${fileServerRoot}zip/${zipToken}`;
}).catch(error => {
const errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}, [repoID, tagFiles, selectedFileIds, getDownloadTarget]);
const renameTagFile = useCallback((id, path, newName) => {
seafileAPI.renameFile(repoID, path, newName).then(res => {
renameFileCallback && renameFileCallback(path, newName);
setTagFiles(prevTagFiles => ({
...prevTagFiles,
rows: prevTagFiles.rows.map(row => {
if (row[TAG_FILE_KEY.ID] === id) {
return { ...row, [TAG_FILE_KEY.NAME]: newName };
}
return row;
})
}));
}).catch(error => {
const errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}, [repoID, renameFileCallback]);
const convertFile = useCallback((path, dstType) => {
seafileAPI.convertFile(repoID, path, dstType).then((res) => {
const newFileName = res.data.obj_name;
const parentDir = res.data.parent_dir;
convertFileCallback({ newName: newFileName, parentDir, size: res.data.size });
}).catch((error) => {
convertFileCallback({ path, error });
});
}, [repoID, convertFileCallback]);
useEffect(() => {
setLoading(true);
const childTagsIds = getChildTagsIds(tagID, nodeKey);
@@ -53,9 +183,17 @@ export const TagViewProvider = ({ repoID, tagID, nodeKey, children, ...params })
repoID,
tagID,
repoInfo: params.repoInfo,
deleteFilesCallback: params.deleteFilesCallback,
renameFileCallback: params.renameFileCallback,
updateCurrentDirent: params.updateCurrentDirent,
selectedFileIds,
updateSelectedFileIds,
moveTagFile,
copyTagFile,
addFolder: addFolderCallback,
deleteTagFiles,
getDownloadTarget,
downloadTagFiles,
renameTagFile,
convertFile,
}}>
{children}
</TagViewContext.Provider>

View File

@@ -192,6 +192,29 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
modifyLocalTags(tagIds, idTagUpdates, { [tagId]: originalRowUpdates }, { [tagId]: oldRowData }, { [tagId]: originalOldRowData }, { success_callback, fail_callback });
}, [tagsData, modifyLocalTags]);
const updateLocalTags = useCallback((tagIds, idTagUpdates, { success_callback, fail_callback } = { }) => {
if (!Array.isArray(tagIds) || tagIds.length === 0) {
return;
}
let idOriginalRowUpdates = {};
let idOldRowData = {};
let idOriginalOldRowData = {};
tagIds.forEach((tagId) => {
const tag = getRowById(tagsData, tagId);
const tagUpdates = idTagUpdates[tagId];
if (tagUpdates) {
Object.keys(tagUpdates).forEach((key) => {
const column = tagsData.key_column_map[key];
const columnName = getColumnOriginName(column);
idOriginalRowUpdates[tagId] = Object.assign({}, idOriginalRowUpdates[tagId], { [key]: tagUpdates[key] });
idOldRowData[tagId] = Object.assign({}, idOldRowData[tagId], { [key]: getCellValueByColumn(tag, column) });
idOriginalOldRowData[tagId] = Object.assign({}, idOriginalOldRowData[tagId], { [columnName]: getCellValueByColumn(tag, column) });
});
}
});
modifyLocalTags(tagIds, idTagUpdates, idOriginalRowUpdates, idOldRowData, idOriginalOldRowData, { success_callback, fail_callback });
}, [tagsData, modifyLocalTags]);
const addTagLinks = useCallback((columnKey, tagId, otherTagsIds, { success_callback, fail_callback } = {}) => {
storeRef.current.addTagLinks(columnKey, tagId, otherTagsIds, success_callback, fail_callback);
}, [storeRef]);
@@ -291,9 +314,10 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
deleteTagsLinks,
mergeTags,
updateLocalTag,
updateLocalTags,
selectTag: handleSelectTag,
modifyColumnWidth,
modifyLocalFileTags,
modifyLocalFileTags
}}>
{children}
</TagsContext.Provider>

View File

@@ -8,6 +8,7 @@ import { PRIVATE_COLUMN_KEY, ALL_TAGS_ID } from '../constants';
import { checkTreeNodeHasChildNodes, getTreeChildNodes, getTreeNodeDepth, getTreeNodeId, getTreeNodeKey } from '../../components/sf-table/utils/tree';
import { getRowById } from '../../metadata/utils/table';
import { SIDEBAR_INIT_LEFT_INDENT } from '../constants/sidebar-tree';
import { EVENT_BUS_TYPE } from '../../metadata/constants';
import './index.css';
@@ -77,6 +78,7 @@ const TagsTreeView = ({ currentPath }) => {
const nodeKey = getTreeNodeKey(node);
selectTag(tag, nodeKey);
setCurrSelectedNodeKey(nodeKey);
window.sfTagsDataContext && window.sfTagsDataContext.eventBus.dispatch(EVENT_BUS_TYPE.UNSELECT_TAG_FILES);
}, [tagsData, selectTag]);
const selectAllTags = useCallback((isSelected) => {

View File

@@ -0,0 +1,62 @@
import { enableSeadoc, fileAuditEnabled, isPro } from '../../utils/constants';
import TextTranslation from '../../utils/text-translation';
import { Utils } from '../../utils/utils';
import { TAG_FILE_KEY } from '../constants/file';
export const getFileById = (tagFiles, fileId) => {
return fileId ? tagFiles.rows.find(file => file._id === fileId) : '';
};
export const getFileName = (file) => {
return file ? file[TAG_FILE_KEY.NAME] : '';
};
export const getFileParentDir = (file) => {
return file ? file[TAG_FILE_KEY.PARENT_DIR] : '';
};
export const getTagFileOperationList = (fileName, repo, canModify) => {
const {
SHARE, DOWNLOAD, DELETE, RENAME, MOVE, COPY, HISTORY, ACCESS_LOG, OPEN_VIA_CLIENT, CONVERT_AND_EXPORT, CONVERT_TO_MARKDOWN, CONVERT_TO_DOCX, EXPORT_DOCX, CONVERT_TO_SDOC, EXPORT_SDOC
} = TextTranslation;
let menuList = [DOWNLOAD, SHARE];
if (canModify) {
menuList.push(DELETE, 'Divider', RENAME, MOVE, COPY);
if (enableSeadoc && !repo.encrypted) {
menuList.push('Divider');
if (fileName.endsWith('.md') || fileName.endsWith('.docx')) {
menuList.push(CONVERT_TO_SDOC);
}
if (fileName.endsWith('.sdoc')) {
if (Utils.isDesktop()) {
let subOpList = [CONVERT_TO_MARKDOWN, CONVERT_TO_DOCX, EXPORT_DOCX, EXPORT_SDOC];
menuList.push({ ...CONVERT_AND_EXPORT, subOpList });
} else {
menuList.push(CONVERT_TO_MARKDOWN);
menuList.push(CONVERT_TO_DOCX);
menuList.push(EXPORT_DOCX);
menuList.push(EXPORT_SDOC);
}
}
}
menuList.push('Divider', HISTORY);
if (isPro && fileAuditEnabled) {
menuList.push(ACCESS_LOG);
}
menuList.push('Divider', OPEN_VIA_CLIENT);
}
// if the last item of menuList is Divider, delete the last item
if (menuList[menuList.length - 1] === 'Divider') {
menuList.pop();
}
// Remove adjacent excess 'Divider'
for (let i = 0; i < menuList.length; i++) {
if (menuList[i] === 'Divider' && menuList[i + 1] === 'Divider') {
menuList.splice(i, 1);
i--;
}
}
return menuList;
};

View File

@@ -1,3 +1,5 @@
.sf-metadata-tag-files-wrapper .sf-metadata-tags-main {
display: flex;
flex-direction: column;
overflow: auto !important;
}

View File

@@ -1,25 +1,67 @@
import React, { useCallback, useState, useRef, useMemo } from 'react';
import React, { useCallback, useState, useRef, useMemo, useEffect } from 'react';
import { useTagView, useTags } from '../../hooks';
import { gettext } from '../../../utils/constants';
import TagFile from './tag-file';
import { getRecordIdFromRecord } from '../../../metadata/utils/cell';
import { gettext, username } from '../../../utils/constants';
import EmptyTip from '../../../components/empty-tip';
import ImagePreviewer from '../../../metadata/components/cell-formatter/image-previewer';
import toaster from '../../../components/toast';
import ContextMenu from '../../../components/context-menu/context-menu';
import MoveDirent from '../../../components/dialog/move-dirent-dialog';
import CopyDirent from '../../../components/dialog/copy-dirent-dialog';
import ZipDownloadDialog from '../../../components/dialog/zip-download-dialog';
import ShareDialog from '../../../components/dialog/share-dialog';
import FileAccessLog from '../../../components/dialog/file-access-log';
import Rename from '../../../components/dialog/rename-dirent';
import FixedWidthTable from '../../../components/common/fixed-width-table';
import ImagePreviewer from '../../../metadata/components/cell-formatter/image-previewer';
import TagFile from './tag-file';
import TextTranslation from '../../../utils/text-translation';
import { getRecordIdFromRecord } from '../../../metadata/utils/cell';
import { seafileAPI } from '../../../utils/seafile-api';
import { TAG_FILE_KEY } from '../../constants/file';
import { Utils } from '../../../utils/utils';
import { getFileById, getFileName, getFileParentDir, getTagFileOperationList } from '../../utils/file';
import { EVENT_BUS_TYPE } from '../../../metadata/constants';
import { hideMenu, showMenu } from '../../../components/context-menu/actions';
import URLDecorator from '../../../utils/url-decorator';
import './index.css';
const TAG_FILE_CONTEXT_MENU_ID = 'tag-files-context-menu';
const TagFiles = () => {
const { tagFiles, repoID, repoInfo } = useTagView();
const { tagsData } = useTags();
const [selectedFiles, setSelectedFiles] = useState(null);
const {
tagFiles, repoID, repoInfo, selectedFileIds, updateSelectedFileIds,
moveTagFile, copyTagFile, addFolder, deleteTagFiles, renameTagFile, getDownloadTarget, downloadTagFiles, convertFile,
} = useTagView();
const [isMoveDialogOpen, setIsMoveDialogOpen] = useState(false);
const [isCopyDialogOpen, setIsCopyDialogOpen] = useState(false);
const [isZipDialogOpen, setIsZipDialogOpen] = useState(false);
const [isShareDialogOpen, setIsShareDialogOpen] = useState(false);
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
const [isImagePreviewerVisible, setImagePreviewerVisible] = useState(false);
const [isFileAccessLogDialogOpen, setIsFileAccessLogDialogOpen] = useState(false);
const currentImageRef = useRef(null);
const isSelectedAll = useMemo(() => {
return selectedFiles ? selectedFiles.length === tagFiles.rows.length : false;
}, [selectedFiles, tagFiles]);
return selectedFileIds ? selectedFileIds.length === tagFiles.rows.length : false;
}, [selectedFileIds, tagFiles]);
// selectedFile
const selectedFile = useMemo(() => {
if (!selectedFileIds || selectedFileIds.length === 0) return null;
return getFileById(tagFiles, selectedFileIds[0]);
}, [selectedFileIds, tagFiles]);
const selectedFileParentDir = useMemo(() => {
return getFileParentDir(selectedFile);
}, [selectedFile]);
const selectedFileName = useMemo(() => {
return getFileName(selectedFile);
}, [selectedFile]);
const selectedFilePath = useMemo(() => {
return selectedFileParentDir && selectedFileName ? Utils.joinPath(selectedFileParentDir, selectedFileName) : '';
}, [selectedFileParentDir, selectedFileName]);
const onMouseDown = useCallback((event) => {
if (event.button === 2) {
@@ -38,30 +80,48 @@ const TagFiles = () => {
const onSelectedAll = useCallback(() => {
if (isSelectedAll) {
setSelectedFiles([]);
updateSelectedFileIds([]);
} else {
const allIds = tagFiles.rows.map(record => getRecordIdFromRecord(record));
setSelectedFiles(allIds);
updateSelectedFileIds(allIds);
}
}, [tagFiles, isSelectedAll]);
}, [tagFiles, isSelectedAll, updateSelectedFileIds]);
const onSelectFile = useCallback((fileId) => {
let newSelectedFiles = selectedFiles ? selectedFiles.slice(0) : [];
if (newSelectedFiles.includes(fileId)) {
newSelectedFiles = newSelectedFiles.filter(item => item !== fileId);
} else {
newSelectedFiles.push(fileId);
}
if (newSelectedFiles.length > 0) {
setSelectedFiles(newSelectedFiles);
} else {
setSelectedFiles(null);
}
}, [selectedFiles]);
const onContainerClick = (event) => {
hideMenu();
if (selectedFileIds.length > 0) updateSelectedFileIds([]);
};
const reSelectFiles = useCallback((fileId) => {
setSelectedFiles([fileId]);
}, []);
const onSelectFile = useCallback((event, fileId) => {
if (event.target.tagName === 'TD') {
updateSelectedFileIds([fileId]);
return;
}
const newSelectedFileIds = selectedFileIds.includes(fileId)
? selectedFileIds.filter(id => id !== fileId)
: [...selectedFileIds, fileId];
updateSelectedFileIds(newSelectedFileIds);
}, [selectedFileIds, updateSelectedFileIds]);
const toggleMoveDialog = useCallback(() => {
setIsMoveDialogOpen(!isMoveDialogOpen);
}, [isMoveDialogOpen]);
const toggleCopyDialog = useCallback(() => {
setIsCopyDialogOpen(!isCopyDialogOpen);
}, [isCopyDialogOpen]);
const toggleZipDialog = useCallback(() => {
setIsZipDialogOpen(!isZipDialogOpen);
}, [isZipDialogOpen]);
const toggleShareDialog = useCallback(() => {
setIsShareDialogOpen(!isShareDialogOpen);
}, [isShareDialogOpen]);
const toggleRenameDialog = useCallback(() => {
setIsRenameDialogOpen(!isRenameDialogOpen);
}, [isRenameDialogOpen]);
const openImagePreview = useCallback((record) => {
currentImageRef.current = record;
@@ -73,6 +133,197 @@ const TagFiles = () => {
setImagePreviewerVisible(false);
}, []);
const handleDeleteTagFiles = useCallback(() => {
deleteTagFiles();
updateSelectedFileIds([]);
}, [deleteTagFiles, updateSelectedFileIds]);
const handleRenameTagFile = useCallback((newName) => {
const path = selectedFile[TAG_FILE_KEY.PARENT_DIR];
const newPath = Utils.joinPath(path, newName);
seafileAPI.getFileInfo(repoID, newPath).then(() => {
let errMessage = gettext('The name "{name}" is already taken. Please choose a different name.');
errMessage = errMessage.replace('{name}', Utils.HTMLescape(newName));
toaster.danger(errMessage);
}).catch(error => {
if (error && error.response && error.response.status === 404) {
const fullPath = Utils.joinPath(path, selectedFile[TAG_FILE_KEY.NAME]);
renameTagFile(selectedFile[TAG_FILE_KEY.ID], fullPath, newName);
} else {
const errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
}
});
}, [repoID, selectedFile, renameTagFile]);
const openViaClient = useCallback(() => {
let url = URLDecorator.getUrl({ type: 'open_via_client', repoID, selectedFilePath });
location.href = url;
}, [repoID, selectedFilePath]);
const onHistory = useCallback(() => {
let url = URLDecorator.getUrl({
type: 'file_revisions',
repoID: repoID,
filePath: selectedFilePath,
});
location.href = url;
}, [repoID, selectedFilePath]);
const onConvertFile = useCallback((dstType) => {
toaster.notifyInProgress(gettext('Converting, please wait...'), { 'id': 'conversion' });
convertFile(selectedFilePath, dstType);
}, [selectedFilePath, convertFile]);
const exportDocx = useCallback((dirent) => {
const serviceUrl = window.app.config.serviceURL;
const exportToDocxUrl = serviceUrl + '/repo/sdoc_export_to_docx/' + repoID + '/?file_path=' + selectedFilePath;
window.location.href = exportToDocxUrl;
}, [repoID, selectedFilePath]);
const exportSdoc = useCallback((dirent) => {
const serviceUrl = window.app.config.serviceURL;
const exportToSdocUrl = serviceUrl + '/lib/' + repoID + '/file/' + selectedFilePath + '?dl=1';
window.location.href = exportToSdocUrl;
}, [repoID, selectedFilePath]);
const getMenuContainerSize = useCallback(() => {
return {
width: window.innerWidth,
height: window.innerHeight,
};
}, []);
const getMenuList = useCallback((file) => {
const { DOWNLOAD, DELETE } = TextTranslation;
if (selectedFileIds.length > 1) {
return [DOWNLOAD, DELETE];
}
const canModify = window.sfTagsDataContext && window.sfTagsDataContext.canModify();
const fileName = file ? getFileName(file) : selectedFileName;
return getTagFileOperationList(fileName, repoInfo, canModify);
}, [selectedFileIds, selectedFileName, repoInfo]);
const onMenuItemClick = useCallback((option) => {
if (!option) return;
switch (option) {
case TextTranslation.MOVE.key:
toggleMoveDialog();
break;
case TextTranslation.COPY.key:
toggleCopyDialog();
break;
case TextTranslation.DELETE.key:
handleDeleteTagFiles();
break;
case TextTranslation.SHARE.key:
toggleShareDialog();
break;
case TextTranslation.DOWNLOAD.key:
downloadTagFiles();
break;
case TextTranslation.RENAME.key:
window.sfTagsDataContext && window.sfTagsDataContext.eventBus.dispatch(EVENT_BUS_TYPE.RENAME_TAG_FILE, selectedFileIds[0]);
break;
case TextTranslation.CONVERT_TO_SDOC.key:
onConvertFile('sdoc');
break;
case TextTranslation.CONVERT_TO_MARKDOWN.key: {
onConvertFile('markdown');
break;
}
case TextTranslation.CONVERT_TO_DOCX.key: {
onConvertFile('docx');
break;
}
case TextTranslation.EXPORT_DOCX.key: {
exportDocx();
break;
}
case TextTranslation.EXPORT_SDOC.key: {
exportSdoc();
break;
}
case TextTranslation.HISTORY.key:
onHistory();
break;
case TextTranslation.ACCESS_LOG.key:
setIsFileAccessLogDialogOpen(true);
break;
case TextTranslation.OPEN_VIA_CLIENT.key:
openViaClient();
break;
default:
break;
}
hideMenu();
}, [toggleMoveDialog, toggleCopyDialog, handleDeleteTagFiles, downloadTagFiles, selectedFileIds, onConvertFile, exportDocx, exportSdoc, toggleShareDialog, openViaClient, onHistory]);
const onTagFileContextMenu = useCallback((event, file) => {
let menuList = [];
if (selectedFileIds.length <= 1) {
const fileId = getRecordIdFromRecord(file);
updateSelectedFileIds([fileId]);
menuList = getMenuList(file);
} else {
menuList = getMenuList();
}
const id = TAG_FILE_CONTEXT_MENU_ID;
let x = event.clientX || (event.touches && event.touches[0].pageX);
let y = event.clientY || (event.touches && event.touches[0].pageY);
hideMenu();
let showMenuConfig = {
id: id,
position: { x, y },
target: event.target,
currentObject: file,
menuList: menuList,
};
if (menuList.length === 0) {
return;
}
showMenu(showMenuConfig);
}, [selectedFileIds, updateSelectedFileIds, getMenuList]);
useEffect(() => {
if (!window.sfTagsDataContext) return;
const unsubscribeUnselectFiles = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.UNSELECT_TAG_FILES, () => updateSelectedFileIds([]));
const unsubscribeDeleteTagFiles = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.DELETE_TAG_FILES, deleteTagFiles);
const unsubScribeMoveTagFile = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.MOVE_TAG_FILE, toggleMoveDialog);
const unsubScribeCopyTagFile = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.COPY_TAG_FILE, toggleCopyDialog);
const unsubscribeShareTagFile = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.SHARE_TAG_FILE, toggleShareDialog);
const unsubscribeRenameTagFile = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.TOGGLE_RENAME_DIALOG, toggleRenameDialog);
const unsubscribeDownloadTagFiles = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.DOWNLOAD_TAG_FILES, downloadTagFiles);
const unsubscribeZipDownload = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.TOGGLE_ZIP_DIALOG, toggleZipDialog);
const unsubscribeFileHistory = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.FILE_HISTORY, onHistory);
const unsubscribeFileAccessLog = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.FILE_ACCESS_LOG, () => setIsFileAccessLogDialogOpen(true));
const unsubscribeOpenViaClient = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.OPEN_VIA_CLIENT, openViaClient);
const unsubscribeConvertFile = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.CONVERT_FILE, onConvertFile);
const unsubscribeExportDocx = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.EXPORT_DOCX, exportDocx);
const unsubscribeExportSDoc = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.EXPORT_SDOC, exportSdoc);
return () => {
unsubscribeUnselectFiles();
unsubscribeDeleteTagFiles();
unsubScribeMoveTagFile();
unsubScribeCopyTagFile();
unsubscribeShareTagFile();
unsubscribeRenameTagFile();
unsubscribeDownloadTagFiles();
unsubscribeZipDownload();
unsubscribeFileHistory();
unsubscribeFileAccessLog();
unsubscribeOpenViaClient();
unsubscribeConvertFile();
unsubscribeExportDocx();
unsubscribeExportSDoc();
};
});
if (tagFiles.rows.length === 0) {
return (<EmptyTip text={gettext('No files')} />);
}
@@ -117,9 +368,18 @@ const TagFiles = () => {
}
];
let enableDirPrivateShare = false;
let isRepoOwner = repoInfo.owner_email === username;
let isVirtual = repoInfo.is_virtual;
let isAdmin = repoInfo.is_admin;
if (!isVirtual && (isRepoOwner || isAdmin)) {
enableDirPrivateShare = true;
}
const isGroupOwnedRepo = repoInfo.owner_email.includes('@seafile_group');
const canDelete = window.sfTagsDataContext && window.sfTagsDataContext.canModifyTag();
return (
<>
<div className="table-container">
<div className="table-container" onClick={onContainerClick}>
<FixedWidthTable
headers={headers}
className="table-hover"
@@ -134,12 +394,13 @@ const TagFiles = () => {
<TagFile
key={fileId}
repoID={repoID}
isSelected={selectedFiles ? selectedFiles.includes(fileId) : false}
isSelected={selectedFileIds ? selectedFileIds.includes(fileId) : false}
file={file}
tagsData={tagsData}
onSelectFile={onSelectFile}
reSelectFiles={reSelectFiles}
openImagePreview={openImagePreview}
onRenameFile={handleRenameTagFile}
onContextMenu={onTagFileContextMenu}
/>);
})}
</FixedWidthTable>
@@ -151,8 +412,75 @@ const TagFiles = () => {
record={currentImageRef.current}
table={tagFiles}
closeImagePopup={closeImagePreviewer}
canDelete={canDelete}
/>
)}
<ContextMenu
id={TAG_FILE_CONTEXT_MENU_ID}
onMenuItemClick={onMenuItemClick}
getMenuContainerSize={getMenuContainerSize}
/>
{isMoveDialogOpen && (
<MoveDirent
path={selectedFile[TAG_FILE_KEY.PARENT_DIR]}
repoID={repoID}
repoEncrypted={repoInfo.encrypted}
isMultipleOperation={false}
dirent={{ name: selectedFile[TAG_FILE_KEY.NAME] }}
onItemMove={moveTagFile}
onCancelMove={toggleMoveDialog}
onAddFolder={addFolder}
/>
)}
{isCopyDialogOpen && (
<CopyDirent
path={selectedFile[TAG_FILE_KEY.PARENT_DIR]}
repoID={repoID}
repoEncrypted={repoInfo.encrypted}
isMultipleOperation={false}
dirent={{ name: selectedFile[TAG_FILE_KEY.NAME] }}
onItemCopy={copyTagFile}
onCancelCopy={toggleCopyDialog}
onAddFolder={addFolder}
/>
)}
{isZipDialogOpen && (
<ZipDownloadDialog
repoID={repoID}
path="/"
target={getDownloadTarget()}
toggleDialog={toggleZipDialog}
/>
)}
{isShareDialogOpen &&
<ShareDialog
itemType='file'
itemName={selectedFile[TAG_FILE_KEY.NAME]}
itemPath={Utils.joinPath(selectedFile[TAG_FILE_KEY.PARENT_DIR], selectedFile[TAG_FILE_KEY.NAME])}
userPerm={repoInfo.permission}
repoID={repoID}
repoEncrypted={repoInfo.repoEncrypted}
enableDirPrivateShare={enableDirPrivateShare}
isGroupOwnedRepo={isGroupOwnedRepo}
toggleDialog={toggleShareDialog}
/>
}
{isRenameDialogOpen &&
<Rename
dirent={{ name: selectedFile[TAG_FILE_KEY.NAME], type: 'file' }}
onRename={handleRenameTagFile}
checkDuplicatedName={() => {}}
toggleCancel={toggleRenameDialog}
/>
}
{isFileAccessLogDialogOpen &&
<FileAccessLog
repoID={repoID}
filePath={Utils.joinPath(selectedFile[TAG_FILE_KEY.PARENT_DIR], selectedFile[TAG_FILE_KEY.NAME])}
fileName={selectedFile[TAG_FILE_KEY.NAME]}
toggleDialog={() => setIsFileAccessLogDialogOpen(false)}
/>
}
</>
);
};

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import dayjs from 'dayjs';
@@ -10,14 +10,18 @@ import { getParentDirFromRecord, getRecordIdFromRecord, getFileNameFromRecord, g
} from '../../../../metadata/utils/cell';
import { Utils } from '../../../../utils/utils';
import { openFile } from '../../../../metadata/utils/file';
import Rename from '../../../../components/rename';
import { TAG_FILE_KEY } from '../../../constants/file';
import { EVENT_BUS_TYPE } from '../../../../metadata/constants';
import './index.css';
dayjs.extend(relativeTime);
const TagFile = ({ isSelected, repoID, file, tagsData, onSelectFile, reSelectFiles, openImagePreview }) => {
const TagFile = ({ isSelected, repoID, file, tagsData, onSelectFile, openImagePreview, onRenameFile, onContextMenu }) => {
const [highlight, setHighlight] = useState(false);
const [isIconLoadError, setIconLoadError] = useState(false);
const [isRenameing, setIsRenaming] = useState(false);
const fileId = useMemo(() => getRecordIdFromRecord(file), [file]);
const parentDir = useMemo(() => getParentDirFromRecord(file), [file]);
@@ -62,7 +66,7 @@ const TagFile = ({ isSelected, repoID, file, tagsData, onSelectFile, reSelectFil
const handleSelected = useCallback((event) => {
event.stopPropagation();
onSelectFile(fileId);
onSelectFile(event, fileId);
}, [fileId, onSelectFile]);
const onIconLoadError = useCallback(() => {
@@ -71,16 +75,45 @@ const TagFile = ({ isSelected, repoID, file, tagsData, onSelectFile, reSelectFil
const handelClickFileName = useCallback((event) => {
event.preventDefault();
if (isRenameing) return;
openFile(repoID, file, () => {
openImagePreview(file);
});
}, [repoID, file, openImagePreview]);
}, [repoID, file, openImagePreview, isRenameing]);
const handelClick = useCallback((event) => {
if (event.target.tagName == 'TD') {
reSelectFiles(fileId);
}
}, [fileId, reSelectFiles]);
event.stopPropagation();
if (isRenameing) return;
onSelectFile(event, fileId);
}, [fileId, onSelectFile, isRenameing]);
const onRenameCancel = useCallback(() => {
setIsRenaming(false);
}, []);
const onRenameConfirm = useCallback((newName) => {
onRenameFile(newName);
onRenameCancel();
}, [onRenameFile, onRenameCancel]);
const toggleRename = useCallback((id) => {
if (id === file[TAG_FILE_KEY.ID]) setIsRenaming(true);
}, [file]);
const handleContextMenu = useCallback((event) => {
event.preventDefault();
event.stopPropagation();
onContextMenu(event, file);
}, [file, onContextMenu]);
useEffect(() => {
if (!window.sfTagsDataContext) return;
const unsubscribeRenameTagFile = window.sfTagsDataContext.eventBus.subscribe(EVENT_BUS_TYPE.RENAME_TAG_FILE, (id) => toggleRename(id));
return () => {
unsubscribeRenameTagFile && unsubscribeRenameTagFile();
};
}, [toggleRename]);
return (
<tr
@@ -91,6 +124,7 @@ const TagFile = ({ isSelected, repoID, file, tagsData, onSelectFile, reSelectFil
onClick={handelClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onContextMenu={handleContextMenu}
>
<td className="pl10 pr-2">
<input
@@ -109,7 +143,16 @@ const TagFile = ({ isSelected, repoID, file, tagsData, onSelectFile, reSelectFil
</div>
</td>
<td className="name">
{isRenameing ? (
<Rename
hasSuffix={true}
name={file[TAG_FILE_KEY.NAME]}
onRenameConfirm={onRenameConfirm}
onRenameCancel={onRenameCancel}
/>
) : (
<a href={path} onClick={handelClickFileName}>{name}</a>
)}
</td>
<td className="tag-list-title">
<FileTagsFormatter value={tags} tagsData={tagsData} className="sf-metadata-tags-formatter" />
@@ -129,6 +172,7 @@ TagFile.propTypes = {
onSelectFile: PropTypes.func,
openImagePreview: PropTypes.func,
reSelectFiles: PropTypes.func,
onRenameFile: PropTypes.func,
};
export default TagFile;