diff --git a/frontend/src/metadata/metadata-tree-view/index.css b/frontend/src/metadata/metadata-tree-view/index.css index b6b985f187..a94f0befbe 100644 --- a/frontend/src/metadata/metadata-tree-view/index.css +++ b/frontend/src/metadata/metadata-tree-view/index.css @@ -22,9 +22,10 @@ } .metadata-tree-view .metadata-views-icon { - height: 14px; - width: 14px; + height: 1rem; + width: 1rem; line-height: 1.5; + color: #666; } .metadata-tree-view .sf-metadata-add-view { @@ -56,3 +57,11 @@ line-height: 24px; font-weight: 400; } + +.metadata-tree-view .sf-metadata-view-input { + width: 100%; + height: 28px; + padding: 0; + position: relative; + text-align: center; +} diff --git a/frontend/src/metadata/metadata-tree-view/index.js b/frontend/src/metadata/metadata-tree-view/index.js index 3d1a6cc953..8cd555931b 100644 --- a/frontend/src/metadata/metadata-tree-view/index.js +++ b/frontend/src/metadata/metadata-tree-view/index.js @@ -1,11 +1,13 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import PropTypes from 'prop-types'; import { CustomizeAddTool } from '@seafile/sf-metadata-ui-component'; import { gettext } from '../../utils/constants'; import { PRIVATE_FILE_TYPE } from '../../constants'; import ViewItem from './view-item'; -import NameDialog from './name-dialog'; import { useMetadata } from '../hooks'; +import { Form, Input } from 'reactstrap'; +import Icon from '../../components/icon'; +import { AddView } from '../metadata-view/components/popover/view-popover'; import './index.css'; @@ -14,7 +16,6 @@ const MetadataTreeView = ({ userPerm, currentPath }) => { if (userPerm !== 'rw' && userPerm !== 'admin') return false; return true; }, [userPerm]); - const [showAddViewDialog, setSowAddViewDialog] = useState(false); const [, setState] = useState(0); const { showFirstView, @@ -27,6 +28,11 @@ const MetadataTreeView = ({ userPerm, currentPath }) => { moveView } = useMetadata(); + const [showAddViewPopover, setShowAddViewPopover] = useState(false); + const [showInput, setShowInput] = useState(false); + const inputRef = useRef(null); + const [inputValue, setInputValue] = useState('Untitled'); + useEffect(() => { const { origin, pathname, search } = window.location; const urlParams = new URLSearchParams(search); @@ -49,14 +55,6 @@ const MetadataTreeView = ({ userPerm, currentPath }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const openAddView = useCallback(() => { - setSowAddViewDialog(true); - }, []); - - const closeAddView = useCallback(() => { - setSowAddViewDialog(false); - }, []); - const onUpdateView = useCallback((viewId, update, successCallback, failCallback) => { updateView(viewId, update, () => { setState(n => n + 1); @@ -64,6 +62,46 @@ const MetadataTreeView = ({ userPerm, currentPath }) => { }, failCallback); }, [updateView]); + const togglePopover = (event) => { + event.stopPropagation(); + setShowAddViewPopover(!showAddViewPopover); + }; + + const handleInputChange = (event) => { + setInputValue(event.target.value); + }; + + const handlePopoverOptionClick = () => { + setShowInput(true); + setShowAddViewPopover(false); + }; + + const handleInputSubmit = useCallback((event) => { + event.preventDefault(); + event.stopPropagation(); + addView(inputValue); + setShowInput(false); + setInputValue('Untitled'); + }, [inputValue, addView]); + + const handleClickOutsideInput = useCallback((event) => { + if (inputRef.current && !inputRef.current.contains(event.target)) { + setShowInput(false); + } + }, []); + + useEffect(() => { + if (showInput) { + inputRef.current.select(); + document.addEventListener('click', handleClickOutsideInput); + } + + return () => { + document.removeEventListener('click', handleClickOutsideInput); + }; + }, [showInput, inputRef, handleClickOutsideInput]); + + return ( <>
@@ -86,18 +124,41 @@ const MetadataTreeView = ({ userPerm, currentPath }) => { onMove={moveView} />); })} - {canAdd && - - } + {showInput && ( +
+
+
+ +
+
+ +
+ )} + {canAdd && ( +
+ +
+ )}
- {showAddViewDialog && ()} + {showAddViewPopover && ( + + )} ); }; diff --git a/frontend/src/metadata/metadata-tree-view/view-item/index.js b/frontend/src/metadata/metadata-tree-view/view-item/index.js index cd68c6b427..bcca778b52 100644 --- a/frontend/src/metadata/metadata-tree-view/view-item/index.js +++ b/frontend/src/metadata/metadata-tree-view/view-item/index.js @@ -4,12 +4,11 @@ import classnames from 'classnames'; import { gettext } from '../../../utils/constants'; import Icon from '../../../components/icon'; import ItemDropdownMenu from '../../../components/dropdown-menu/item-dropdown-menu'; -import NameDialog from '../name-dialog'; +import { Rename } from '../../metadata-view/components/popover/view-popover'; import { Utils, isMobile } from '../../../utils/utils'; import './index.css'; - const ViewItem = ({ canDelete, userPerm, @@ -22,8 +21,8 @@ const ViewItem = ({ }) => { const [highlight, setHighlight] = useState(false); const [freeze, setFreeze] = useState(false); - const [isShowRenameDialog, setRenameDialogShow] = useState(false); const [isDropShow, setDropShow] = useState(false); + const [isShowRenamePopover, setRenamePopoverShow] = useState(false); const canUpdate = useMemo(() => { if (userPerm !== 'rw' && userPerm !== 'admin') return false; @@ -72,7 +71,7 @@ const ViewItem = ({ const operationClick = useCallback((operationKey) => { if (operationKey === 'rename') { - setRenameDialogShow(true); + setRenamePopoverShow(true); return; } @@ -82,13 +81,14 @@ const ViewItem = ({ } }, [onDelete]); - const closeRenameDialog = useCallback(() => { - setRenameDialogShow(false); + const closeRenamePopover = useCallback((event) => { + event.stopPropagation(); + setRenamePopoverShow(false); }, []); const renameView = useCallback((name, failCallback) => { onUpdate({ name }, () => { - setRenameDialogShow(false); + setRenamePopoverShow(false); }, failCallback); }, [onUpdate]); @@ -154,7 +154,7 @@ const ViewItem = ({ -
+
{highlight && (
- {isShowRenameDialog && ( - + {isShowRenamePopover && ( + )} - ); }; diff --git a/frontend/src/metadata/metadata-view/components/popover/view-popover/add-view/index.js b/frontend/src/metadata/metadata-view/components/popover/view-popover/add-view/index.js new file mode 100644 index 0000000000..79405bfcd0 --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/popover/view-popover/add-view/index.js @@ -0,0 +1,62 @@ +import React, { useRef, useEffect, useCallback } from 'react'; +import { UncontrolledPopover } from 'reactstrap'; +import PropTypes from 'prop-types'; +import { gettext } from '../../../../utils'; +import Icon from '../../../../../../components/icon'; + +import '../index.css'; + +const AddView = ({ target, toggle, onOptionClick }) => { + const popoverRef = useRef(null); + + const handleClickOutside = useCallback((event) => { + if (popoverRef.current && !popoverRef.current.contains(event.target)) { + toggle(event); + } + }, [toggle]); + + useEffect(() => { + if (popoverRef.current) { + document.addEventListener('click', handleClickOutside, true); + } + + return () => { + document.removeEventListener('click', handleClickOutside, true); + }; + }, [handleClickOutside]); + + return ( + +
+
{gettext('New view')}
+
+ +
+
+
+ ); +}; + +AddView.propTypes = { + target: PropTypes.string.isRequired, + toggle: PropTypes.func.isRequired, + onOptionClick: PropTypes.func.isRequired, +}; + +export default AddView; diff --git a/frontend/src/metadata/metadata-view/components/popover/view-popover/index.css b/frontend/src/metadata/metadata-view/components/popover/view-popover/index.css new file mode 100644 index 0000000000..3ccf3925a5 --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/popover/view-popover/index.css @@ -0,0 +1,68 @@ +.sf-metadata-rename-view-popover .popover, +.sf-metadata-addview-popover .popover { + max-width: none; + min-width: 280px; + margin: 0; + padding: 0.5rem 0; + border: 0; +} + +.sf-metadata-view-form { + display: flex; + padding-left: 0.5rem; + gap: 0.5rem; +} + +.sf-metadata-addview-popover .sf-metadata-addview-popover-header { + width: 100%; + height: 2rem; + padding: 0 1rem; + display: flex; + align-items: center; + justify-content: left; + color: #666; + opacity: 1; + font-size: 0.875rem; +} + +.sf-metadata-rename-view-popover .sf-metadata-rename-view-popover-header { + width: 100%; + height: 1.5rem; + padding: 0 1rem; + display: flex; + align-items: center; + justify-content: left; + color: #666; + opacity: 1; + font-size: 0.875rem; +} + +.sf-metadata-addview-popover +.sf-metadata-addview-popover-body { + width: 100%; + display: flex; + flex-direction: column; +} + +.dropdown-item.sf-metadata-addview-popover-item { + width: 100%; + height: 2rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.dropdown-item.sf-metadata-addview-popover-item .metadata-view-icon { + display: flex; + align-items: center; + font-size: 1rem; + color: #666; +} + +.sf-metadata-rename-view-popover-body { + padding: 0 0.5rem; +} + +.dropdown-item:hover .metadata-view-icon { + color: #fff; +} diff --git a/frontend/src/metadata/metadata-view/components/popover/view-popover/index.js b/frontend/src/metadata/metadata-view/components/popover/view-popover/index.js new file mode 100644 index 0000000000..39ad3c2597 --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/popover/view-popover/index.js @@ -0,0 +1,2 @@ +export { default as Rename } from './rename'; +export { default as AddView } from './add-view'; diff --git a/frontend/src/metadata/metadata-view/components/popover/view-popover/rename/index.js b/frontend/src/metadata/metadata-view/components/popover/view-popover/rename/index.js new file mode 100644 index 0000000000..1b07b93827 --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/popover/view-popover/rename/index.js @@ -0,0 +1,78 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { Form, Input, UncontrolledPopover } from 'reactstrap'; +import PropTypes from 'prop-types'; +import { gettext } from '../../../../utils'; +import '../index.css'; + +const Rename = ({ value, target, toggle, onSubmit }) => { + const [inputValue, setInputValue] = useState(value || ''); + const inputRef = useRef(null); + + const onChange = useCallback((e) => { + setInputValue(e.target.value); + }, []); + + const handleSubmit = useCallback((e) => { + e.preventDefault(); + e.stopPropagation(); + onSubmit(inputValue); + }, [inputValue, onSubmit]); + + useEffect(() => { + const handleClickOutSide = (event) => { + if (inputRef.current && !inputRef.current.contains(event.target)) { + toggle(event); + } + }; + + if (inputRef.current) { + inputRef.current.select(); + document.addEventListener('mousedown', handleClickOutSide); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutSide); + }; + }, [toggle]); + + return ( + +
+ {gettext('Rename view')} +
+
+
+
+ +
+
+
+ ); +}; + +Rename.propTypes = { + value: PropTypes.string, + target: PropTypes.string.isRequired, + toggle: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, +}; + +export default Rename;