1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-16 15:19:06 +00:00
Files
seahub/frontend/src/metadata/metadata-tree-view/folder.js

339 lines
11 KiB
JavaScript

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/metadata-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 [isSortShow, setSortShow] = 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 isValid = useCallback((event) => {
return event.dataTransfer.types.includes(METADATA_VIEWS_DRAG_DATA_KEY);
}, []);
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) => {
event.stopPropagation();
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 || !isValid(event)) return false;
const dragMode = getDragMode();
if (!canDrop || folderId && dragMode === VIEWS_TYPE_FOLDER) {
// not allowed drag folder into folder
setSortShow(true);
return false;
}
const targetRect = event.target.getBoundingClientRect();
const pointerPosition = event.clientY - targetRect.top;
if (pointerPosition <= 4) {
setSortShow(true);
} else {
setDropShow(true);
}
}, [canDrop, folderId, getDragMode, isValid]);
const onDragLeave = useCallback(() => {
if (!canDrop) return false;
setDropShow(false);
setSortShow(false);
}, [canDrop]);
const onDragMove = useCallback((event) => {
if (!canDrop || !isValid(event)) return false;
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
const targetRect = event.target.getBoundingClientRect();
const pointerPosition = event.clientY - targetRect.top;
if (pointerPosition <= 4) {
setSortShow(true);
setDropShow(false);
} else {
setDropShow(true);
setSortShow(false);
}
}, [canDrop, isValid]);
const onDrop = useCallback((event) => {
if (!canDrop) return false;
event.stopPropagation();
setDropShow(false);
setSortShow(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, isAboveFolder: isSortShow });
}, [canDrop, folderId, getDragMode, moveView, isSortShow]);
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, 'tree-node-sort': isSortShow })}
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;