1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-16 15:19:06 +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 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>
);
}

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.
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 => {

View File

@@ -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">

View File

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

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,
isLongTextValueExceedLimit,
getValidLongTextValue,
isValidViewName,
} from './utils';

View File

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

View File

@@ -6,3 +6,4 @@ export {
export {
isValidPosition,
} 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 { 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,
};

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) => {
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}>