1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-17 07:41:26 +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:
杨国璇
2024-09-05 13:42:07 +08:00
committed by GitHub
parent dd907e14ed
commit eaf2114643
11 changed files with 139 additions and 90 deletions

View File

@@ -8,6 +8,7 @@ import { InternalLinkOperation } from '../operations';
import DirOperationToolBar from '../../components/toolbar/dir-operation-toolbar'; import DirOperationToolBar from '../../components/toolbar/dir-operation-toolbar';
import ViewFileToolbar from '../../components/toolbar/view-file-toolbar'; import ViewFileToolbar from '../../components/toolbar/view-file-toolbar';
import { PRIVATE_FILE_TYPE } from '../../constants'; import { PRIVATE_FILE_TYPE } from '../../constants';
import MetadataViewId2Name from '../../metadata/metadata-view-id-2-name';
const propTypes = { const propTypes = {
currentRepoInfo: PropTypes.object.isRequired, currentRepoInfo: PropTypes.object.isRequired,
@@ -137,7 +138,7 @@ class DirPath extends React.Component {
return ( return (
<Fragment key={index}> <Fragment key={index}>
<span className="path-split">/</span> <span className="path-split">/</span>
<span className="path-item">{item}</span> <span className="path-item"><MetadataViewId2Name id={item} /></span>
</Fragment> </Fragment>
); );
} }

View File

@@ -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. // This hook provides content related to seahub interaction, such as whether to enable extended attributes, views data, etc.
const MetadataContext = React.createContext(null); const MetadataContext = React.createContext(null);
export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView, renameMetadataView, children }) => { export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView, children }) => {
const enableMetadataManagement = useMemo(() => { const enableMetadataManagement = useMemo(() => {
return window.app.pageOptions.enableMetadataManagement; return window.app.pageOptions.enableMetadataManagement;
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -17,6 +17,7 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
const [enableMetadata, setEnableExtendedProperties] = useState(false); const [enableMetadata, setEnableExtendedProperties] = useState(false);
const [showFirstView, setShowFirstView] = useState(false); const [showFirstView, setShowFirstView] = useState(false);
const [navigation, setNavigation] = useState([]); const [navigation, setNavigation] = useState([]);
const [, setCount] = useState(0);
const viewsMap = useRef({}); const viewsMap = useRef({});
const cancelURLView = useCallback(() => { const cancelURLView = useCallback(() => {
@@ -87,7 +88,7 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
if (isSelected) return; if (isSelected) return;
const node = { const node = {
children: [], children: [],
path: '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view.name, path: '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view._id,
isExpanded: false, isExpanded: false,
isLoaded: true, isLoaded: true,
isPreload: true, isPreload: true,
@@ -155,14 +156,12 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
metadataAPI.modifyView(repoID, viewId, update).then(res => { metadataAPI.modifyView(repoID, viewId, update).then(res => {
const currentView = viewsMap.current[viewId]; const currentView = viewsMap.current[viewId];
viewsMap.current[viewId] = { ...currentView, ...update }; viewsMap.current[viewId] = { ...currentView, ...update };
if (Object.prototype.hasOwnProperty.call(update, 'name')) { setCount(n => n + 1);
renameMetadataView(viewId, '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + update['name']);
}
successCallback && successCallback(); successCallback && successCallback();
}).catch(error => { }).catch(error => {
failCallback && failCallback(error); failCallback && failCallback(error);
}); });
}, [repoID, viewsMap, renameMetadataView]); }, [repoID, viewsMap]);
const moveView = useCallback((sourceViewId, targetViewId) => { const moveView = useCallback((sourceViewId, targetViewId) => {
metadataAPI.moveView(repoID, sourceViewId, targetViewId).then(res => { metadataAPI.moveView(repoID, sourceViewId, targetViewId).then(res => {

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import { Form, Input } from 'reactstrap'; import { Input } from 'reactstrap';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { CustomizeAddTool } from '@seafile/sf-metadata-ui-component'; import { CustomizeAddTool } from '@seafile/sf-metadata-ui-component';
import Icon from '../../components/icon'; import Icon from '../../components/icon';
@@ -8,7 +8,9 @@ import { PRIVATE_FILE_TYPE } from '../../constants';
import ViewItem from './view-item'; import ViewItem from './view-item';
import { useMetadata } from '../hooks'; import { useMetadata } from '../hooks';
import { AddView } from '../metadata-view/components/popover/view-popover'; 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'; import './index.css';
@@ -34,7 +36,7 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
const [showAddViewPopover, setShowAddViewPopover] = useState(false); const [showAddViewPopover, setShowAddViewPopover] = useState(false);
const [showInput, setShowInput] = useState(false); const [showInput, setShowInput] = useState(false);
const inputRef = useRef(null); const inputRef = useRef(null);
const [inputValue, setInputValue] = useState('Untitled'); const [inputValue, setInputValue] = useState('');
useEffect(() => { useEffect(() => {
const { origin, pathname, search } = window.location; const { origin, pathname, search } = window.location;
@@ -74,37 +76,38 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
setInputValue(event.target.value); setInputValue(event.target.value);
}; };
const handlePopoverOptionClick = (option) => { const handlePopoverOptionClick = useCallback((option) => {
setNewView(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); setShowInput(true);
setShowAddViewPopover(false); setShowAddViewPopover(false);
}; }, [viewsMap]);
const handleInputSubmit = useCallback((event) => { const handleInputSubmit = useCallback((event) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
addView(inputValue, newView.type); const viewNames = Object.values(viewsMap).map(v => v.name);
setShowInput(false); const { isValid, message } = isValidViewName(inputValue, viewNames);
setInputValue('Untitled'); if (!isValid) {
}, [inputValue, addView, newView]); toaster.danger(message);
inputRef.current.focus();
const handleClickOutsideInput = useCallback((event) => { return;
if (inputRef.current && !inputRef.current.contains(event.target)) {
setShowInput(false);
} }
}, []); addView(message, newView.type);
setShowInput(false);
}, [inputValue, viewsMap, addView, newView]);
useEffect(() => { const onKeyDown = useCallback((event) => {
if (showInput) { if (isEnter(event)) {
inputRef.current.select(); handleInputSubmit(event);
document.addEventListener('click', handleClickOutsideInput);
} }
}, [handleInputSubmit]);
return () => {
document.removeEventListener('click', handleClickOutsideInput);
};
}, [showInput, inputRef, handleClickOutsideInput]);
return ( return (
<> <>
@@ -113,7 +116,7 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
<div className="children"> <div className="children">
{navigation.map((item, index) => { {navigation.map((item, index) => {
const view = viewsMap[item._id]; 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; const isSelected = currentPath === viewPath;
return ( return (
<ViewItem <ViewItem
@@ -131,21 +134,20 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
); );
})} })}
{showInput && ( {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"> <div className="left-icon">
<Icon symbol={VIEW_TYPE_ICON[newView.type] || 'table'} className="metadata-views-icon" /> <Icon symbol={VIEW_TYPE_ICON[newView.type] || 'table'} className="metadata-views-icon" />
</div> </div>
<Input <Input
className='sf-metadata-view-input' className="sf-metadata-view-input"
innerRef={inputRef} innerRef={inputRef}
type='text'
id='add-view-input'
name='add-view'
value={inputValue} value={inputValue}
onChange={handleInputChange} onChange={handleInputChange}
autoFocus={true} autoFocus={true}
onBlur={handleInputSubmit}
onKeyDown={onKeyDown}
/> />
</Form> </div>
)} )}
{canAdd && ( {canAdd && (
<div id="sf-metadata-view-popover"> <div id="sf-metadata-view-popover">

View File

@@ -9,6 +9,7 @@ import { Utils, isMobile } from '../../../utils/utils';
import { VIEW_TYPE_ICON } from '../../metadata-view/_basic'; import { VIEW_TYPE_ICON } from '../../metadata-view/_basic';
import './index.css'; import './index.css';
import { useMetadata } from '../../hooks';
const ViewItem = ({ const ViewItem = ({
canDelete, canDelete,
@@ -25,6 +26,9 @@ const ViewItem = ({
const [freeze, setFreeze] = useState(false); const [freeze, setFreeze] = useState(false);
const [isDropShow, setDropShow] = useState(false); const [isDropShow, setDropShow] = useState(false);
const [isShowRenamePopover, setRenamePopoverShow] = 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(() => { const canUpdate = useMemo(() => {
if (userPerm !== 'rw' && userPerm !== 'admin') return false; if (userPerm !== 'rw' && userPerm !== 'admin') return false;
@@ -89,8 +93,7 @@ const ViewItem = ({
} }
}, [onDelete, onCopy]); }, [onDelete, onCopy]);
const closeRenamePopover = useCallback((event) => { const closeRenamePopover = useCallback(() => {
event.stopPropagation();
setRenamePopoverShow(false); setRenamePopoverShow(false);
}, []); }, []);
@@ -177,7 +180,7 @@ const ViewItem = ({
</div> </div>
</div> </div>
{isShowRenamePopover && ( {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} />
)} )}
</> </>
); );

View 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;

View File

@@ -106,4 +106,5 @@ export {
getColumnOptionIdsByNames, getColumnOptionIdsByNames,
isLongTextValueExceedLimit, isLongTextValueExceedLimit,
getValidLongTextValue, getValidLongTextValue,
isValidViewName,
} from './utils'; } from './utils';

View File

@@ -96,6 +96,7 @@ export {
isValidEmail, isValidEmail,
ValidateFilter, ValidateFilter,
DATE_MODIFIERS_REQUIRE_TERM, DATE_MODIFIERS_REQUIRE_TERM,
isValidViewName,
} from './validate'; } from './validate';
export { export {
getViewById, getViewById,

View File

@@ -6,3 +6,4 @@ export {
export { export {
isValidPosition, isValidPosition,
} from './geolocation'; } from './geolocation';
export { isValidViewName } from './view';

View File

@@ -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 };
};

View File

@@ -1,76 +1,84 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useRef, useState } from 'react';
import { Form, Input, UncontrolledPopover } from 'reactstrap';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Alert, Input } from 'reactstrap';
import { CustomizePopover } from '@seafile/sf-metadata-ui-component';
import { gettext } from '../../../../utils'; import { gettext } from '../../../../utils';
import { isValidViewName } from '../../../../_basic';
import { isEnter } from '../../../../_basic/utils/hotkey';
import '../index.css'; import '../index.css';
const Rename = ({ value, target, toggle, onSubmit }) => { const Rename = ({ value, target, otherViewsName, toggle, onSubmit }) => {
const [inputValue, setInputValue] = useState(value || ''); const [inputValue, setInputValue] = useState(value || '');
const [errorMessage, setErrorMessage] = useState('');
const inputRef = useRef(null); const inputRef = useRef(null);
const onChange = useCallback((e) => { const onChange = useCallback((e) => {
setInputValue(e.target.value); setInputValue(e.target.value);
}, []); }, []);
const handleSubmit = useCallback((e) => { const onToggle = useCallback(() => {
e.preventDefault(); toggle();
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]); }, [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 ( return (
<UncontrolledPopover <CustomizePopover
className='sf-metadata-rename-view-popover' className='sf-metadata-rename-view-popover'
isOpen={true}
toggle={toggle}
target={target} target={target}
placement='right-start' placement='right-start'
hideArrow={true} hideArrow={true}
fade={false} fade={false}
boundariesElement={document.body} modifiers={{ preventOverflow: { boundariesElement: document.body } }}
canHide={!errorMessage}
hide={onToggle}
hideWithEsc={onToggle}
> >
<div className='sf-metadata-rename-view-popover-header'> <div className='sf-metadata-rename-view-popover-header'>
{gettext('Rename view')} {gettext('Rename view')}
</div> </div>
<div className="seafile-divider dropdown-divider"></div> <div className="seafile-divider dropdown-divider"></div>
<div className='sf-metadata-rename-view-popover-body'> <div className='sf-metadata-rename-view-popover-body'>
<Form onSubmit={handleSubmit}>
<Input <Input
innerRef={inputRef} innerRef={inputRef}
className='sf-metadata-view-input' className="sf-metadata-view-rename-input"
type='text'
id="rename-input"
name='rename'
value={inputValue} value={inputValue}
onChange={onChange} onChange={onChange}
autoFocus={true} autoFocus={true}
onBlur={handleSubmit}
onKeyDown={onKeyDown}
/> />
</Form> {errorMessage && (<Alert color="danger" className="mt-2 mb-0">{errorMessage}</Alert>)}
</div> </div>
</UncontrolledPopover> </CustomizePopover>
); );
}; };
Rename.propTypes = { Rename.propTypes = {
value: PropTypes.string, value: PropTypes.string,
target: PropTypes.string.isRequired, target: PropTypes.string.isRequired,
otherViewsName: PropTypes.array,
toggle: PropTypes.func.isRequired, toggle: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
}; };

View File

@@ -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) => { loadDirentList = (path) => {
let repoID = this.props.repoID; let repoID = this.props.repoID;
seafileAPI.listDir(repoID, path, { 'with_thumbnail': true }).then(res => { seafileAPI.listDir(repoID, path, { 'with_thumbnail': true }).then(res => {
@@ -1860,6 +1853,9 @@ class LibContentView extends React.Component {
} }
if (node.object.isDir()) { // isDir if (node.object.isDir()) { // isDir
if (this.state.path.includes(PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES)) {
this.isNeedUpdateHistoryState = true;
}
this.showDir(node.path); this.showDir(node.path);
} else { } else {
if (Utils.isMarkdownFile(node.path)) { if (Utils.isMarkdownFile(node.path)) {
@@ -2207,7 +2203,6 @@ class LibContentView extends React.Component {
<MetadataProvider <MetadataProvider
repoID={this.props.repoID} repoID={this.props.repoID}
selectMetadataView={this.onTreeNodeClick} selectMetadataView={this.onTreeNodeClick}
renameMetadataView={this.renameMetadataView}
hideMetadataView={this.hideFileMetadata} hideMetadataView={this.hideFileMetadata}
> >
<CollaboratorsProvider repoID={this.props.repoID}> <CollaboratorsProvider repoID={this.props.repoID}>