mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-25 14:50:29 +00:00
feat(metadata-views): support add folder (#7175)
This commit is contained in:
313
frontend/src/metadata/metadata-tree-view/folder.js
Normal file
313
frontend/src/metadata/metadata-tree-view/folder.js
Normal file
@@ -0,0 +1,313 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import ViewItem from './view';
|
||||
import ItemDropdownMenu from '../../components/dropdown-menu/item-dropdown-menu';
|
||||
import toaster from '../../components/toast';
|
||||
import NewView from './new-view';
|
||||
import InlineNameEditor from './inline-name-editor';
|
||||
import { useMetadata } from '../hooks';
|
||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||
import { Utils, isMobile } from '../../utils/utils';
|
||||
import TextTranslation from '../../utils/text-translation';
|
||||
import { validateName } from '../utils/validate';
|
||||
import { METADATA_VIEWS_DRAG_DATA_KEY, METADATA_VIEWS_KEY, TREE_NODE_LEFT_INDENT, VIEW_TYPE, VIEWS_TYPE_FOLDER, VIEWS_TYPE_VIEW } from '../constants';
|
||||
import { getNewViewMenuItem, KEY_ADD_VIEW_MAP } from '../../components/dir-view-mode/dir-views/new-view-menu';
|
||||
|
||||
const ViewsFolder = ({
|
||||
leftIndent, folder, currentPath, userPerm, canDeleteView, getFoldersNames, getMoveableFolders, generateNewViewDefaultName,
|
||||
setDragMode, getDragMode, selectView, modifyView,
|
||||
}) => {
|
||||
const {
|
||||
idViewMap, collapsedFoldersIds, collapseFolder, expandFolder, modifyFolder, deleteFolder,
|
||||
deleteView, duplicateView, addView, moveView,
|
||||
} = useMetadata();
|
||||
const { _id: folderId, name: folderName, children } = folder || {};
|
||||
const [expanded, setExpanded] = useState(!collapsedFoldersIds.includes(folderId));
|
||||
const [highlight, setHighlight] = useState(false);
|
||||
const [freeze, setFreeze] = useState(false);
|
||||
const [isRenaming, setRenaming] = useState(false);
|
||||
const [newView, setNewView] = useState(null);
|
||||
const [isDropShow, setDropShow] = useState(false);
|
||||
|
||||
const canUpdate = useMemo(() => {
|
||||
if (userPerm !== 'rw' && userPerm !== 'admin') return false;
|
||||
return true;
|
||||
}, [userPerm]);
|
||||
|
||||
const canDrop = useMemo(() => {
|
||||
if (Utils.isIEBrowser() || !canUpdate) return false;
|
||||
return true;
|
||||
}, [canUpdate]);
|
||||
|
||||
const folderMoreOperationMenus = useMemo(() => {
|
||||
let menus = [];
|
||||
if (canUpdate) {
|
||||
menus.push(
|
||||
getNewViewMenuItem(),
|
||||
TextTranslation.RENAME,
|
||||
TextTranslation.DELETE,
|
||||
);
|
||||
}
|
||||
return menus;
|
||||
}, [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);
|
||||
setHighlight(true);
|
||||
}, []);
|
||||
|
||||
const unfreezeItem = useCallback(() => {
|
||||
setFreeze(false);
|
||||
setHighlight(false);
|
||||
}, []);
|
||||
|
||||
const clickFolder = useCallback(() => {
|
||||
if (expanded) {
|
||||
collapseFolder(folderId);
|
||||
} else {
|
||||
expandFolder(folderId);
|
||||
}
|
||||
setExpanded(!expanded);
|
||||
}, [expanded, folderId, collapseFolder, expandFolder]);
|
||||
|
||||
const prepareAddView = useCallback((viewType) => {
|
||||
setNewView({ key: viewType, type: viewType, default_name: generateNewViewDefaultName() });
|
||||
}, [generateNewViewDefaultName]);
|
||||
|
||||
const clickMenu = useCallback((operationKey) => {
|
||||
switch (operationKey) {
|
||||
case KEY_ADD_VIEW_MAP.ADD_TABLE: {
|
||||
prepareAddView(VIEW_TYPE.TABLE);
|
||||
return;
|
||||
}
|
||||
case KEY_ADD_VIEW_MAP.ADD_GALLERY: {
|
||||
prepareAddView(VIEW_TYPE.GALLERY);
|
||||
return;
|
||||
}
|
||||
case KEY_ADD_VIEW_MAP.ADD_KANBAN: {
|
||||
prepareAddView(VIEW_TYPE.KANBAN);
|
||||
return;
|
||||
}
|
||||
case KEY_ADD_VIEW_MAP.ADD_MAP: {
|
||||
prepareAddView(VIEW_TYPE.MAP);
|
||||
return;
|
||||
}
|
||||
case TextTranslation.RENAME.key: {
|
||||
setRenaming(true);
|
||||
return;
|
||||
}
|
||||
case TextTranslation.DELETE.key: {
|
||||
deleteFolder(folderId);
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [prepareAddView, folderId, deleteFolder]);
|
||||
|
||||
const onDragStart = useCallback((event) => {
|
||||
if (!canDrop) return false;
|
||||
const dragData = JSON.stringify({ type: METADATA_VIEWS_KEY, folder_id: folderId, mode: VIEWS_TYPE_FOLDER });
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData(METADATA_VIEWS_DRAG_DATA_KEY, dragData);
|
||||
setDragMode(VIEWS_TYPE_FOLDER);
|
||||
}, [canDrop, folderId, setDragMode]);
|
||||
|
||||
const onDragEnter = useCallback((event) => {
|
||||
if (!canDrop) {
|
||||
// not allowed drag folder into folder
|
||||
return false;
|
||||
}
|
||||
if (!canDrop) {
|
||||
return false;
|
||||
}
|
||||
setDropShow(true);
|
||||
}, [canDrop]);
|
||||
|
||||
const onDragLeave = useCallback(() => {
|
||||
if (!canDrop) return false;
|
||||
setDropShow(false);
|
||||
}, [canDrop]);
|
||||
|
||||
const onDragMove = useCallback((event) => {
|
||||
if (!canDrop) return false;
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}, [canDrop]);
|
||||
|
||||
const onDrop = useCallback((event) => {
|
||||
if (!canDrop) return false;
|
||||
event.stopPropagation();
|
||||
setDropShow(false);
|
||||
|
||||
let dragData = event.dataTransfer.getData(METADATA_VIEWS_DRAG_DATA_KEY);
|
||||
if (!dragData) return;
|
||||
dragData = JSON.parse(dragData);
|
||||
if (dragData.type !== METADATA_VIEWS_KEY) return false;
|
||||
const dragMode = getDragMode();
|
||||
const { view_id: sourceViewId, folder_id: sourceFolderId } = dragData;
|
||||
if ((dragMode === VIEWS_TYPE_VIEW && !sourceViewId)) {
|
||||
return;
|
||||
}
|
||||
moveView({ sourceViewId, sourceFolderId, targetFolderId: folderId });
|
||||
}, [canDrop, folderId, getDragMode, moveView]);
|
||||
|
||||
const onConfirmRename = useCallback((name) => {
|
||||
const foldersNames = getFoldersNames();
|
||||
const otherFoldersNames = foldersNames.filter((currFolderName) => currFolderName !== folderName);
|
||||
const { isValid, message } = validateName(name, otherFoldersNames);
|
||||
if (!isValid) {
|
||||
toaster.danger(message);
|
||||
return;
|
||||
}
|
||||
if (message === folderName) {
|
||||
setRenaming(false);
|
||||
return;
|
||||
}
|
||||
modifyFolder(folderId, { name: message });
|
||||
setRenaming(false);
|
||||
}, [folderId, folderName, getFoldersNames, modifyFolder]);
|
||||
|
||||
const closeNewView = useCallback(() => {
|
||||
setNewView(null);
|
||||
}, []);
|
||||
|
||||
const addViewIntoFolder = useCallback((viewName, viewType) => {
|
||||
addView({ folderId, name: viewName, type: viewType });
|
||||
}, [folderId, addView]);
|
||||
|
||||
const deleteViewFromFolder = useCallback((viewId, isSelected) => {
|
||||
deleteView({ folderId, viewId, isSelected });
|
||||
}, [folderId, deleteView]);
|
||||
|
||||
const duplicateViewFromFolder = useCallback((viewId) => {
|
||||
duplicateView({ folderId, viewId });
|
||||
}, [folderId, duplicateView]);
|
||||
|
||||
const renderViews = () => {
|
||||
if (!Array.isArray(children) || children.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return children.map((children) => {
|
||||
const { _id: viewId, type: childType } = children || {};
|
||||
if (!viewId || childType !== VIEWS_TYPE_VIEW) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const view = idViewMap[viewId];
|
||||
if (!view) return null;
|
||||
|
||||
const viewPath = '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + viewId;
|
||||
const isSelected = currentPath === viewPath;
|
||||
return (
|
||||
<ViewItem
|
||||
key={viewId}
|
||||
folderId={folderId}
|
||||
leftIndent={leftIndent + TREE_NODE_LEFT_INDENT}
|
||||
canDelete={canDeleteView}
|
||||
isSelected={isSelected}
|
||||
userPerm={userPerm}
|
||||
view={view}
|
||||
getMoveableFolders={getMoveableFolders}
|
||||
setDragMode={setDragMode}
|
||||
getDragMode={getDragMode}
|
||||
onClick={selectView}
|
||||
onDelete={deleteViewFromFolder}
|
||||
onCopy={duplicateViewFromFolder}
|
||||
onUpdate={modifyView}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tree-node views-folder-wrapper">
|
||||
<div
|
||||
className={classnames('tree-node-inner views-folder-main text-nowrap', { 'tree-node-inner-hover': highlight, 'tree-node-drop': isDropShow })}
|
||||
type="dir"
|
||||
title={folderName}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onClick={clickFolder}
|
||||
>
|
||||
<div
|
||||
className="tree-node-text views-folder-name"
|
||||
style={{ paddingLeft: leftIndent + 5 }}
|
||||
draggable={canUpdate}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnter={onDragEnter}
|
||||
onDragLeave={onDragLeave}
|
||||
onDragOver={onDragMove}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
{isRenaming ? (
|
||||
<InlineNameEditor
|
||||
name={folderName}
|
||||
className="rename mt-0"
|
||||
onSubmit={onConfirmRename}
|
||||
/>
|
||||
) : folderName}
|
||||
</div>
|
||||
<div className="left-icon" style={{ left: leftIndent - 40 }}>
|
||||
<i className={classnames('folder-toggle-icon sf3-font sf3-font-down', { 'rotate-270': !expanded })}></i>
|
||||
<span className="tree-node-icon">
|
||||
<i className="sf3-font sf3-font-folder"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div className="right-icon">
|
||||
{(highlight && folderMoreOperationMenus.length > 0) && (
|
||||
<ItemDropdownMenu
|
||||
item={{ name: 'metadata-folder' }}
|
||||
menuClassname="metadata-views-dropdown-menu"
|
||||
toggleClass="sf3-font sf3-font-more"
|
||||
freezeItem={freezeItem}
|
||||
unfreezeItem={unfreezeItem}
|
||||
getMenuList={() => folderMoreOperationMenus}
|
||||
onMenuItemClick={clickMenu}
|
||||
menuStyle={isMobile ? { zIndex: 1050 } : {}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="children views-folder-children">
|
||||
{expanded && renderViews()}
|
||||
{newView && <NewView newView={newView} leftIndent={TREE_NODE_LEFT_INDENT * 3} closeNewView={closeNewView} addView={addViewIntoFolder} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ViewsFolder.propTypes = {
|
||||
leftIndent: PropTypes.number,
|
||||
folder: PropTypes.object,
|
||||
currentPath: PropTypes.string,
|
||||
userPerm: PropTypes.string,
|
||||
canDeleteView: PropTypes.bool,
|
||||
getFoldersNames: PropTypes.func,
|
||||
getMoveableFolders: PropTypes.func,
|
||||
generateNewViewDefaultName: PropTypes.func,
|
||||
setDragMode: PropTypes.func,
|
||||
getDragMode: PropTypes.func,
|
||||
selectView: PropTypes.func,
|
||||
modifyView: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ViewsFolder;
|
Reference in New Issue
Block a user