mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-15 23:00:57 +00:00
Feat view name (#6685)
* feat: view name * feat: update code * feat: updat code * feat: update code * feat: optimzie code --------- Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
@@ -8,6 +8,7 @@ import { InternalLinkOperation } from '../operations';
|
||||
import DirOperationToolBar from '../../components/toolbar/dir-operation-toolbar';
|
||||
import ViewFileToolbar from '../../components/toolbar/view-file-toolbar';
|
||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||
import MetadataViewId2Name from '../../metadata/metadata-view-id-2-name';
|
||||
|
||||
const propTypes = {
|
||||
currentRepoInfo: PropTypes.object.isRequired,
|
||||
@@ -137,7 +138,7 @@ class DirPath extends React.Component {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<span className="path-split">/</span>
|
||||
<span className="path-item">{item}</span>
|
||||
<span className="path-item"><MetadataViewId2Name id={item} /></span>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||
// This hook provides content related to seahub interaction, such as whether to enable extended attributes, views data, etc.
|
||||
const MetadataContext = React.createContext(null);
|
||||
|
||||
export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView, renameMetadataView, children }) => {
|
||||
export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView, children }) => {
|
||||
const enableMetadataManagement = useMemo(() => {
|
||||
return window.app.pageOptions.enableMetadataManagement;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -17,6 +17,7 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
|
||||
const [enableMetadata, setEnableExtendedProperties] = useState(false);
|
||||
const [showFirstView, setShowFirstView] = useState(false);
|
||||
const [navigation, setNavigation] = useState([]);
|
||||
const [, setCount] = useState(0);
|
||||
const viewsMap = useRef({});
|
||||
|
||||
const cancelURLView = useCallback(() => {
|
||||
@@ -87,7 +88,7 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
|
||||
if (isSelected) return;
|
||||
const node = {
|
||||
children: [],
|
||||
path: '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view.name,
|
||||
path: '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view._id,
|
||||
isExpanded: false,
|
||||
isLoaded: true,
|
||||
isPreload: true,
|
||||
@@ -155,14 +156,12 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
|
||||
metadataAPI.modifyView(repoID, viewId, update).then(res => {
|
||||
const currentView = viewsMap.current[viewId];
|
||||
viewsMap.current[viewId] = { ...currentView, ...update };
|
||||
if (Object.prototype.hasOwnProperty.call(update, 'name')) {
|
||||
renameMetadataView(viewId, '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + update['name']);
|
||||
}
|
||||
setCount(n => n + 1);
|
||||
successCallback && successCallback();
|
||||
}).catch(error => {
|
||||
failCallback && failCallback(error);
|
||||
});
|
||||
}, [repoID, viewsMap, renameMetadataView]);
|
||||
}, [repoID, viewsMap]);
|
||||
|
||||
const moveView = useCallback((sourceViewId, targetViewId) => {
|
||||
metadataAPI.moveView(repoID, sourceViewId, targetViewId).then(res => {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
|
||||
import { Form, Input } from 'reactstrap';
|
||||
import { Input } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CustomizeAddTool } from '@seafile/sf-metadata-ui-component';
|
||||
import Icon from '../../components/icon';
|
||||
@@ -8,7 +8,9 @@ import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||
import ViewItem from './view-item';
|
||||
import { useMetadata } from '../hooks';
|
||||
import { AddView } from '../metadata-view/components/popover/view-popover';
|
||||
import { VIEW_TYPE_ICON } from '../metadata-view/_basic';
|
||||
import { isValidViewName, VIEW_TYPE_ICON } from '../metadata-view/_basic';
|
||||
import { isEnter } from '../metadata-view/_basic/utils/hotkey';
|
||||
import toaster from '../../components/toast';
|
||||
|
||||
import './index.css';
|
||||
|
||||
@@ -34,7 +36,7 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
|
||||
const [showAddViewPopover, setShowAddViewPopover] = useState(false);
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const inputRef = useRef(null);
|
||||
const [inputValue, setInputValue] = useState('Untitled');
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const { origin, pathname, search } = window.location;
|
||||
@@ -74,37 +76,38 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
|
||||
setInputValue(event.target.value);
|
||||
};
|
||||
|
||||
const handlePopoverOptionClick = (option) => {
|
||||
const handlePopoverOptionClick = useCallback((option) => {
|
||||
setNewView(option);
|
||||
let newViewName = gettext('Untitled');
|
||||
const otherViewsName = Object.values(viewsMap).map(v => v.name);
|
||||
let i = 1;
|
||||
while (otherViewsName.includes(newViewName)) {
|
||||
newViewName = gettext('Untitled') + ' (' + (i++) + ')';
|
||||
}
|
||||
setInputValue(newViewName);
|
||||
setShowInput(true);
|
||||
setShowAddViewPopover(false);
|
||||
};
|
||||
}, [viewsMap]);
|
||||
|
||||
const handleInputSubmit = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
addView(inputValue, newView.type);
|
||||
setShowInput(false);
|
||||
setInputValue('Untitled');
|
||||
}, [inputValue, addView, newView]);
|
||||
|
||||
const handleClickOutsideInput = useCallback((event) => {
|
||||
if (inputRef.current && !inputRef.current.contains(event.target)) {
|
||||
setShowInput(false);
|
||||
const viewNames = Object.values(viewsMap).map(v => v.name);
|
||||
const { isValid, message } = isValidViewName(inputValue, viewNames);
|
||||
if (!isValid) {
|
||||
toaster.danger(message);
|
||||
inputRef.current.focus();
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
addView(message, newView.type);
|
||||
setShowInput(false);
|
||||
}, [inputValue, viewsMap, addView, newView]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showInput) {
|
||||
inputRef.current.select();
|
||||
document.addEventListener('click', handleClickOutsideInput);
|
||||
const onKeyDown = useCallback((event) => {
|
||||
if (isEnter(event)) {
|
||||
handleInputSubmit(event);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClickOutsideInput);
|
||||
};
|
||||
}, [showInput, inputRef, handleClickOutsideInput]);
|
||||
|
||||
}, [handleInputSubmit]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -113,7 +116,7 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
|
||||
<div className="children">
|
||||
{navigation.map((item, index) => {
|
||||
const view = viewsMap[item._id];
|
||||
const viewPath = '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view.name;
|
||||
const viewPath = '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view._id;
|
||||
const isSelected = currentPath === viewPath;
|
||||
return (
|
||||
<ViewItem
|
||||
@@ -131,21 +134,20 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
|
||||
);
|
||||
})}
|
||||
{showInput && (
|
||||
<Form onSubmit={handleInputSubmit} className='tree-view-inner sf-metadata-view-form'>
|
||||
<div className="tree-view-inner sf-metadata-view-form">
|
||||
<div className="left-icon">
|
||||
<Icon symbol={VIEW_TYPE_ICON[newView.type] || 'table'} className="metadata-views-icon" />
|
||||
</div>
|
||||
<Input
|
||||
className='sf-metadata-view-input'
|
||||
className="sf-metadata-view-input"
|
||||
innerRef={inputRef}
|
||||
type='text'
|
||||
id='add-view-input'
|
||||
name='add-view'
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
autoFocus={true}
|
||||
onBlur={handleInputSubmit}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</Form>
|
||||
</div>
|
||||
)}
|
||||
{canAdd && (
|
||||
<div id="sf-metadata-view-popover">
|
||||
|
@@ -9,6 +9,7 @@ import { Utils, isMobile } from '../../../utils/utils';
|
||||
import { VIEW_TYPE_ICON } from '../../metadata-view/_basic';
|
||||
|
||||
import './index.css';
|
||||
import { useMetadata } from '../../hooks';
|
||||
|
||||
const ViewItem = ({
|
||||
canDelete,
|
||||
@@ -25,6 +26,9 @@ const ViewItem = ({
|
||||
const [freeze, setFreeze] = useState(false);
|
||||
const [isDropShow, setDropShow] = useState(false);
|
||||
const [isShowRenamePopover, setRenamePopoverShow] = useState(false);
|
||||
const { viewsMap } = useMetadata();
|
||||
|
||||
const otherViewsName = Object.values(viewsMap).filter(v => v._id !== view._id).map(v => v.name);
|
||||
|
||||
const canUpdate = useMemo(() => {
|
||||
if (userPerm !== 'rw' && userPerm !== 'admin') return false;
|
||||
@@ -89,8 +93,7 @@ const ViewItem = ({
|
||||
}
|
||||
}, [onDelete, onCopy]);
|
||||
|
||||
const closeRenamePopover = useCallback((event) => {
|
||||
event.stopPropagation();
|
||||
const closeRenamePopover = useCallback(() => {
|
||||
setRenamePopoverShow(false);
|
||||
}, []);
|
||||
|
||||
@@ -177,7 +180,7 @@ const ViewItem = ({
|
||||
</div>
|
||||
</div>
|
||||
{isShowRenamePopover && (
|
||||
<Rename value={view.name} target={`metadata-view-dropdown-item-${view._id}`} toggle={closeRenamePopover} onSubmit={renameView} />
|
||||
<Rename value={view.name} otherViewsName={otherViewsName} target={`metadata-view-dropdown-item-${view._id}`} toggle={closeRenamePopover} onSubmit={renameView} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
17
frontend/src/metadata/metadata-view-id-2-name.js
Normal file
17
frontend/src/metadata/metadata-view-id-2-name.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useMetadata } from './hooks';
|
||||
|
||||
const MetadataViewId2Name = ({ id }) => {
|
||||
const { viewsMap } = useMetadata();
|
||||
if (!id) return null;
|
||||
const view = viewsMap[id];
|
||||
if (!view) return null;
|
||||
return (<>{view.name}</>);
|
||||
};
|
||||
|
||||
MetadataViewId2Name.propTypes = {
|
||||
id: PropTypes.string,
|
||||
};
|
||||
|
||||
export default MetadataViewId2Name;
|
@@ -106,4 +106,5 @@ export {
|
||||
getColumnOptionIdsByNames,
|
||||
isLongTextValueExceedLimit,
|
||||
getValidLongTextValue,
|
||||
isValidViewName,
|
||||
} from './utils';
|
||||
|
@@ -96,6 +96,7 @@ export {
|
||||
isValidEmail,
|
||||
ValidateFilter,
|
||||
DATE_MODIFIERS_REQUIRE_TERM,
|
||||
isValidViewName,
|
||||
} from './validate';
|
||||
export {
|
||||
getViewById,
|
||||
|
@@ -6,3 +6,4 @@ export {
|
||||
export {
|
||||
isValidPosition,
|
||||
} from './geolocation';
|
||||
export { isValidViewName } from './view';
|
||||
|
@@ -0,0 +1,21 @@
|
||||
import { gettext } from '../../../utils';
|
||||
|
||||
export const isValidViewName = (name, names) => {
|
||||
if (typeof name !== 'string') {
|
||||
return { isValid: false, message: gettext('Name should be string') };
|
||||
}
|
||||
name = name.trim();
|
||||
if (name === '') {
|
||||
return { isValid: false, message: gettext('Name is required') };
|
||||
}
|
||||
if (name.includes('/')) {
|
||||
return { isValid: false, message: gettext('Name cannot contain slash') };
|
||||
}
|
||||
if (name.includes('\\')) {
|
||||
return { isValid: false, message: gettext('Name cannot contain backslash') };
|
||||
}
|
||||
if (names.includes(name)) {
|
||||
return { isValid: false, message: gettext('Name already exists') };
|
||||
}
|
||||
return { isValid: true, message: name };
|
||||
};
|
@@ -1,76 +1,84 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Form, Input, UncontrolledPopover } from 'reactstrap';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert, Input } from 'reactstrap';
|
||||
import { CustomizePopover } from '@seafile/sf-metadata-ui-component';
|
||||
import { gettext } from '../../../../utils';
|
||||
import { isValidViewName } from '../../../../_basic';
|
||||
import { isEnter } from '../../../../_basic/utils/hotkey';
|
||||
|
||||
import '../index.css';
|
||||
|
||||
const Rename = ({ value, target, toggle, onSubmit }) => {
|
||||
const Rename = ({ value, target, otherViewsName, toggle, onSubmit }) => {
|
||||
const [inputValue, setInputValue] = useState(value || '');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
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);
|
||||
};
|
||||
const onToggle = useCallback(() => {
|
||||
toggle();
|
||||
}, [toggle]);
|
||||
|
||||
const handleSubmit = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const { isValid, message } = isValidViewName(inputValue, otherViewsName);
|
||||
if (!isValid) {
|
||||
setErrorMessage(message);
|
||||
inputRef.current.focus();
|
||||
return;
|
||||
}
|
||||
if (message === value) {
|
||||
onToggle();
|
||||
return;
|
||||
}
|
||||
onSubmit(message);
|
||||
}, [value, inputValue, otherViewsName, onSubmit, onToggle]);
|
||||
|
||||
const onKeyDown = useCallback((event) => {
|
||||
if (isEnter(event)) {
|
||||
handleSubmit(event);
|
||||
}
|
||||
}, [handleSubmit]);
|
||||
|
||||
return (
|
||||
<UncontrolledPopover
|
||||
<CustomizePopover
|
||||
className='sf-metadata-rename-view-popover'
|
||||
isOpen={true}
|
||||
toggle={toggle}
|
||||
target={target}
|
||||
placement='right-start'
|
||||
hideArrow={true}
|
||||
fade={false}
|
||||
boundariesElement={document.body}
|
||||
modifiers={{ preventOverflow: { boundariesElement: document.body } }}
|
||||
canHide={!errorMessage}
|
||||
hide={onToggle}
|
||||
hideWithEsc={onToggle}
|
||||
>
|
||||
<div className='sf-metadata-rename-view-popover-header'>
|
||||
{gettext('Rename view')}
|
||||
</div>
|
||||
<div className="seafile-divider dropdown-divider"></div>
|
||||
<div className='sf-metadata-rename-view-popover-body'>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Input
|
||||
innerRef={inputRef}
|
||||
className='sf-metadata-view-input'
|
||||
type='text'
|
||||
id="rename-input"
|
||||
name='rename'
|
||||
className="sf-metadata-view-rename-input"
|
||||
value={inputValue}
|
||||
onChange={onChange}
|
||||
autoFocus={true}
|
||||
onBlur={handleSubmit}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</Form>
|
||||
{errorMessage && (<Alert color="danger" className="mt-2 mb-0">{errorMessage}</Alert>)}
|
||||
</div>
|
||||
</UncontrolledPopover>
|
||||
</CustomizePopover>
|
||||
);
|
||||
};
|
||||
|
||||
Rename.propTypes = {
|
||||
value: PropTypes.string,
|
||||
target: PropTypes.string.isRequired,
|
||||
otherViewsName: PropTypes.array,
|
||||
toggle: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
@@ -550,13 +550,6 @@ class LibContentView extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
renameMetadataView = (renamedViewId, newPath) => {
|
||||
const { viewId, content } = this.state;
|
||||
if (content !== '__sf-metadata') return;
|
||||
if (viewId !== renamedViewId) return;
|
||||
this.setState({ path: newPath });
|
||||
};
|
||||
|
||||
loadDirentList = (path) => {
|
||||
let repoID = this.props.repoID;
|
||||
seafileAPI.listDir(repoID, path, { 'with_thumbnail': true }).then(res => {
|
||||
@@ -1860,6 +1853,9 @@ class LibContentView extends React.Component {
|
||||
}
|
||||
|
||||
if (node.object.isDir()) { // isDir
|
||||
if (this.state.path.includes(PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES)) {
|
||||
this.isNeedUpdateHistoryState = true;
|
||||
}
|
||||
this.showDir(node.path);
|
||||
} else {
|
||||
if (Utils.isMarkdownFile(node.path)) {
|
||||
@@ -2207,7 +2203,6 @@ class LibContentView extends React.Component {
|
||||
<MetadataProvider
|
||||
repoID={this.props.repoID}
|
||||
selectMetadataView={this.onTreeNodeClick}
|
||||
renameMetadataView={this.renameMetadataView}
|
||||
hideMetadataView={this.hideFileMetadata}
|
||||
>
|
||||
<CollaboratorsProvider repoID={this.props.repoID}>
|
||||
|
Reference in New Issue
Block a user