mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-03 07:55:36 +00:00
repo-metadata-views (#6344)
* repo-metadata-views * Update metadata_manage.py * feat: add view * feat: refresh view tool bar * update --------- Co-authored-by: 杨国璇 <ygx@192.168.124.14>
This commit is contained in:
@@ -62,16 +62,25 @@ class DirPath extends React.Component {
|
||||
if (item === '') {
|
||||
return null;
|
||||
}
|
||||
if (index === (pathList.length - 1)) {
|
||||
if (item === PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES) {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<span className="path-split">/</span>
|
||||
<span className="path-item">{gettext('File extended properties')}</span>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
if (index === pathList.length - 2 && item === PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES) {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<span className="path-split">/</span>
|
||||
<span className="path-item">{gettext('File extended properties')}</span>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (index === pathList.length - 1 && pathList[pathList.length - 2] === PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES) {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<span className="path-split">/</span>
|
||||
<span className="path-item">{item}</span>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (index === (pathList.length - 1)) {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<span className="path-split">/</span>
|
||||
@@ -129,7 +138,7 @@ class DirPath extends React.Component {
|
||||
const { currentPath } = this.props;
|
||||
const path = currentPath[currentPath.length - 1] === '/' ? currentPath.slice(0, currentPath.length - 1) : currentPath;
|
||||
const pathList = path.split('/');
|
||||
return pathList[pathList.length - 1] === PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES;
|
||||
return pathList[pathList.length - 2] === PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES;
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@@ -23,6 +23,7 @@ const propTypes = {
|
||||
sortBy: PropTypes.string,
|
||||
sortOrder: PropTypes.string,
|
||||
sortItems: PropTypes.func,
|
||||
metadataViewId: PropTypes.string,
|
||||
};
|
||||
|
||||
class DirTool extends React.Component {
|
||||
@@ -97,9 +98,9 @@ class DirTool extends React.Component {
|
||||
render() {
|
||||
const menuItems = this.getMenu();
|
||||
const { isDropdownMenuOpen } = this.state;
|
||||
const { repoID, currentMode, currentPath, sortBy, sortOrder } = this.props;
|
||||
const { repoID, currentMode, currentPath, sortBy, sortOrder, metadataViewId } = this.props;
|
||||
const propertiesText = TextTranslation.PROPERTIES.value;
|
||||
const isFileExtended = currentPath === '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES;
|
||||
const isFileExtended = currentPath.startsWith('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/');
|
||||
|
||||
const sortOptions = this.sortOptions.map(item => {
|
||||
return {
|
||||
@@ -111,7 +112,7 @@ class DirTool extends React.Component {
|
||||
if (isFileExtended) {
|
||||
return (
|
||||
<div className="d-flex">
|
||||
<MetadataViewToolBar />
|
||||
<MetadataViewToolBar metadataViewId={metadataViewId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -37,6 +37,7 @@ const propTypes = {
|
||||
filePermission: PropTypes.string,
|
||||
repoTags: PropTypes.array.isRequired,
|
||||
onFileTagChanged: PropTypes.func.isRequired,
|
||||
metadataViewId: PropTypes.string,
|
||||
};
|
||||
|
||||
class CurDirPath extends React.Component {
|
||||
@@ -96,6 +97,7 @@ class CurDirPath extends React.Component {
|
||||
sortBy={this.props.sortBy}
|
||||
sortOrder={this.props.sortOrder}
|
||||
sortItems={this.props.sortItems}
|
||||
metadataViewId={this.props.metadataViewId}
|
||||
/>}
|
||||
{!isDesktop && this.props.direntList.length > 0 &&
|
||||
<span className="sf3-font sf3-font-sort action-icon" onClick={this.toggleSortOptionsDialog}></span>}
|
||||
|
@@ -13,6 +13,7 @@ const propTypes = {
|
||||
isFileLoadedErr: PropTypes.bool.isRequired,
|
||||
filePermission: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
metadataViewId: PropTypes.string,
|
||||
lastModified: PropTypes.string,
|
||||
latestContributor: PropTypes.string,
|
||||
onLinkClick: PropTypes.func.isRequired,
|
||||
@@ -52,13 +53,14 @@ class DirColumnFile extends React.Component {
|
||||
}
|
||||
|
||||
if (this.props.content === '__sf-metadata') {
|
||||
const { repoID, currentRepoInfo, metadataViewId } = this.props;
|
||||
window.sfMetadata = {
|
||||
siteRoot,
|
||||
lang,
|
||||
mediaUrl,
|
||||
};
|
||||
|
||||
return (<SeafileMetadata repoID={this.props.repoID} currentRepoInfo={this.props.currentRepoInfo} />);
|
||||
return (<SeafileMetadata repoID={repoID} currentRepoInfo={currentRepoInfo} viewID={metadataViewId} />);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@@ -37,6 +37,7 @@ const propTypes = {
|
||||
hash: PropTypes.string,
|
||||
filePermission: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
metadataViewId: PropTypes.string,
|
||||
lastModified: PropTypes.string,
|
||||
latestContributor: PropTypes.string,
|
||||
onLinkClick: PropTypes.func.isRequired,
|
||||
@@ -192,6 +193,7 @@ class DirColumnView extends React.Component {
|
||||
isFileLoadedErr={this.props.isFileLoadedErr}
|
||||
filePermission={this.props.filePermission}
|
||||
content={this.props.content}
|
||||
metadataViewId={this.props.metadataViewId}
|
||||
currentRepoInfo={this.props.currentRepoInfo}
|
||||
lastModified={this.props.lastModified}
|
||||
latestContributor={this.props.latestContributor}
|
||||
|
@@ -67,7 +67,7 @@ const DirViews = ({ userPerm, repoID, currentPath, onNodeClick }) => {
|
||||
moreOperations={moreOperations}
|
||||
moreOperationClick={moreOperationClick}
|
||||
>
|
||||
{!loading && metadataStatus && (<MetadataTreeView repoID={repoID} currentPath={currentPath} onNodeClick={onNodeClick} />)}
|
||||
{!loading && metadataStatus && (<MetadataTreeView userPerm={userPerm} repoID={repoID} currentPath={currentPath} onNodeClick={onNodeClick} />)}
|
||||
</TreeSection>
|
||||
{showMetadataStatusManagementDialog && (
|
||||
<MetadataStatusManagementDialog
|
||||
|
@@ -107,6 +107,7 @@ class TreeView extends React.Component {
|
||||
return;
|
||||
}
|
||||
let dragStartNodeData = e.dataTransfer.getData('applicaiton/drag-item-info');
|
||||
if (!dragStartNodeData) return;
|
||||
dragStartNodeData = JSON.parse(dragStartNodeData);
|
||||
|
||||
let { nodeDirent, nodeParentPath, nodeRootPath } = dragStartNodeData;
|
||||
|
@@ -117,6 +117,43 @@ class MetadataManagerAPI {
|
||||
return this._sendPostRequest(url, params, { headers: { 'Content-type': 'application/json' } });
|
||||
};
|
||||
|
||||
// view
|
||||
listViews = (repoID) => {
|
||||
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/views/';
|
||||
return this.req.get(url);
|
||||
};
|
||||
|
||||
addView = (repoID, name) => {
|
||||
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/views/';
|
||||
const params = { name };
|
||||
return this._sendPostRequest(url, params, { headers: { 'Content-type': 'application/json' } });
|
||||
};
|
||||
|
||||
modifyView = (repoID, viewId, viewData) => {
|
||||
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/views/';
|
||||
const params = {
|
||||
view_id: viewId,
|
||||
view_data: viewData,
|
||||
};
|
||||
return this.req.put(url, params);
|
||||
};
|
||||
|
||||
deleteView = (repoID, viewId) => {
|
||||
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/views/';
|
||||
const params = {
|
||||
view_id: viewId,
|
||||
};
|
||||
return this.req.delete(url, params);
|
||||
};
|
||||
|
||||
moveView = (repoID, viewId, targetViewId) => {
|
||||
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/move-views/';
|
||||
const params = {
|
||||
view_id: viewId,
|
||||
target_view_id: targetViewId,
|
||||
};
|
||||
return this._sendPostRequest(url, params, { headers: { 'Content-type': 'application/json' } });
|
||||
};
|
||||
}
|
||||
|
||||
const metadataAPI = new MetadataManagerAPI();
|
||||
|
@@ -26,3 +26,33 @@
|
||||
width: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.metadata-tree-view .sf-metadata-add-view {
|
||||
border-top: none;
|
||||
height: 28px;
|
||||
padding: 2px 0 2px 28.8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.metadata-tree-view .sf-metadata-add-view:hover {
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.metadata-tree-view .sf-metadata-add-view .sf-metadata-add-view-icon {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 10px;
|
||||
font-weight: 400;
|
||||
fill: #666;
|
||||
}
|
||||
|
||||
.metadata-tree-view .sf-metadata-add-view .text-truncate {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
@@ -1,17 +1,43 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import Icon from '../../components/icon';
|
||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||
import metadataAPI from '../api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import toaster from '../../components/toast';
|
||||
import ViewItem from './view-item';
|
||||
import NameDialog from './name-dialog';
|
||||
|
||||
import './index.css';
|
||||
import { CustomizeAddTool } from '@seafile/sf-metadata-ui-component';
|
||||
|
||||
const MetadataTreeView = ({ repoID, currentPath, onNodeClick }) => {
|
||||
const node = useMemo(() => {
|
||||
return {
|
||||
const MetadataTreeView = ({ userPerm, repoID, currentPath, onNodeClick }) => {
|
||||
const canAdd = useMemo(() => {
|
||||
if (userPerm !== 'rw' && userPerm !== 'admin') return false;
|
||||
return true;
|
||||
}, [userPerm]);
|
||||
const [views, setViews] = useState([]);
|
||||
const [showAddViewDialog, setSowAddViewDialog] = useState(false);
|
||||
const [, setState] = useState(0);
|
||||
const viewsMap = useRef({});
|
||||
|
||||
useEffect(() => {
|
||||
metadataAPI.listViews(repoID).then(res => {
|
||||
const { navigation, views } = res.data;
|
||||
Array.isArray(views) && views.forEach(view => {
|
||||
viewsMap.current[view._id] = view;
|
||||
});
|
||||
setViews(navigation);
|
||||
}).catch(error => {
|
||||
const errorMsg = Utils.getErrorMsg(error);
|
||||
toaster.danger(errorMsg);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onClick = useCallback((view) => {
|
||||
const node = {
|
||||
children: [],
|
||||
path: '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES,
|
||||
path: '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view.name,
|
||||
isExpanded: false,
|
||||
isLoaded: true,
|
||||
isPreload: true,
|
||||
@@ -24,48 +50,105 @@ const MetadataTreeView = ({ repoID, currentPath, onNodeClick }) => {
|
||||
},
|
||||
parentNode: {},
|
||||
key: repoID,
|
||||
view_id: view._id,
|
||||
};
|
||||
onNodeClick(node);
|
||||
}, [onNodeClick]);
|
||||
|
||||
const openAddView = useCallback(() => {
|
||||
setSowAddViewDialog(true);
|
||||
}, []);
|
||||
|
||||
const closeAddView = useCallback(() => {
|
||||
setSowAddViewDialog(false);
|
||||
}, []);
|
||||
|
||||
const addView = useCallback((name, failCallback) => {
|
||||
metadataAPI.addView(repoID, name).then(res => {
|
||||
const view = res.data.view;
|
||||
let newViews = views.slice(0);
|
||||
newViews.push({ _id: view._id, type: 'view' });
|
||||
viewsMap.current[view._id] = view;
|
||||
setSowAddViewDialog(false);
|
||||
setViews(newViews);
|
||||
onClick(view);
|
||||
}).catch(error => {
|
||||
failCallback && failCallback(error);
|
||||
});
|
||||
}, [views, repoID, viewsMap, onClick]);
|
||||
|
||||
const onDeleteView = useCallback((viewId, isSelected) => {
|
||||
metadataAPI.deleteView(repoID, viewId).then(res => {
|
||||
const currentViewIndex = views.findIndex(item => item.id === viewId);
|
||||
const newViews = views.filter(item => item.id === viewId);
|
||||
delete viewsMap.current[viewId];
|
||||
setViews(newViews);
|
||||
if (isSelected) {
|
||||
const lastViewId = views[currentViewIndex - 1].id;
|
||||
const lastView = viewsMap.current[lastViewId];
|
||||
onNodeClick(lastView);
|
||||
}
|
||||
}).catch((error => {
|
||||
const errorMsg = Utils.getErrorMsg(error);
|
||||
toaster.danger(errorMsg);
|
||||
}));
|
||||
}, [views, onClick, viewsMap]);
|
||||
|
||||
const onUpdateView = useCallback((viewId, update, successCallback, failCallback) => {
|
||||
metadataAPI.modifyView(repoID, viewId, update).then(res => {
|
||||
successCallback && successCallback();
|
||||
const currentView = viewsMap.current[viewId];
|
||||
viewsMap.current[viewId] = { ...currentView, ...update };
|
||||
setState(n => n + 1);
|
||||
}).catch(error => {
|
||||
failCallback && failCallback(error);
|
||||
});
|
||||
}, [repoID, viewsMap]);
|
||||
|
||||
const onMoveView = useCallback((sourceViewId, targetViewId) => {
|
||||
metadataAPI.moveView(repoID, sourceViewId, targetViewId).then(res => {
|
||||
const { navigation } = res.data;
|
||||
setViews(navigation);
|
||||
}).catch(error => {
|
||||
const errorMsg = Utils.getErrorMsg(error);
|
||||
toaster.danger(errorMsg);
|
||||
});
|
||||
}, [repoID]);
|
||||
const [highlight, setHighlight] = useState(false);
|
||||
|
||||
const onMouseEnter = useCallback(() => {
|
||||
setHighlight(true);
|
||||
}, []);
|
||||
|
||||
const onMouseOver = useCallback(() => {
|
||||
setHighlight(true);
|
||||
}, []);
|
||||
|
||||
const onMouseLeave = useCallback(() => {
|
||||
setHighlight(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="tree-view tree metadata-tree-view">
|
||||
<div className="tree-node">
|
||||
<div className="children">
|
||||
<div
|
||||
className={classnames('tree-node-inner text-nowrap', { 'tree-node-inner-hover': highlight, 'tree-node-hight-light': currentPath === node.path })}
|
||||
title={gettext('File extended properties')}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onClick={() => onNodeClick(node)}
|
||||
>
|
||||
<div className="tree-node-text">{gettext('File extended properties')}</div>
|
||||
<div className="left-icon">
|
||||
<div className="tree-node-icon">
|
||||
<Icon symbol="table" className="metadata-views-icon" />
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<div className="tree-view tree metadata-tree-view">
|
||||
<div className="tree-node">
|
||||
<div className="children">
|
||||
{views.map((item, index) => {
|
||||
if (item.type !== 'view') return null;
|
||||
const view = viewsMap.current[item._id];
|
||||
const viewPath = '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view.name;
|
||||
const isSelected = currentPath === viewPath;
|
||||
return (
|
||||
<ViewItem
|
||||
key={view._id}
|
||||
canDelete={index !== 0}
|
||||
isSelected={isSelected}
|
||||
userPerm={userPerm}
|
||||
view={view}
|
||||
onClick={onClick}
|
||||
onDelete={() => onDeleteView(view._id, isSelected)}
|
||||
onUpdate={(update, successCallback, failCallback) => onUpdateView(view._id, update, successCallback, failCallback)}
|
||||
onMove={onMoveView}
|
||||
/>);
|
||||
})}
|
||||
{canAdd && (<CustomizeAddTool className="sf-metadata-add-view" callBack={openAddView} footerName={gettext('Add view')} addIconClassName="sf-metadata-add-view-icon" />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showAddViewDialog && (<NameDialog title={gettext('Add view')} onSubmit={addView} onToggle={closeAddView} />)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MetadataTreeView.propTypes = {
|
||||
userPerm: PropTypes.string,
|
||||
repoID: PropTypes.string.isRequired,
|
||||
currentPath: PropTypes.string,
|
||||
onNodeClick: PropTypes.func,
|
||||
|
@@ -0,0 +1,96 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Label, Input, Button, Alert } from 'reactstrap';
|
||||
import { KeyCodes } from '../../../constants';
|
||||
import { gettext } from '../../metadata-view/utils';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
|
||||
const NameDialog = ({ value: oldName, title, onSubmit, onToggle }) => {
|
||||
const [name, setName] = useState(oldName);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [isSubmitting, setSubmitting] = useState(false);
|
||||
|
||||
const onChange = useCallback((event) => {
|
||||
const value = event.target.value;
|
||||
if (value === name) return;
|
||||
setName(value);
|
||||
}, [name]);
|
||||
|
||||
const validate = useCallback((name) => {
|
||||
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') };
|
||||
}
|
||||
return { isValid: true, message: name };
|
||||
}, []);
|
||||
|
||||
const submit = useCallback(() => {
|
||||
setSubmitting(true);
|
||||
const { isValid, message } = validate(name);
|
||||
if (!isValid) {
|
||||
setErrorMessage(message);
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
if (message === oldName) {
|
||||
onToggle();
|
||||
return;
|
||||
}
|
||||
onSubmit(message, (error) => {
|
||||
const errorMsg = Utils.getErrorMsg(error);
|
||||
setErrorMessage(errorMsg);
|
||||
setSubmitting(false);
|
||||
});
|
||||
}, [validate, name, onSubmit]);
|
||||
|
||||
const onHotKey = useCallback((event) => {
|
||||
if (event.keyCode === KeyCodes.Enter) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}, [submit]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', onHotKey);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onHotKey);
|
||||
};
|
||||
}, [onHotKey]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} toggle={onToggle} autoFocus={false} className="sf-metadata-view-name-dialog">
|
||||
<ModalHeader toggle={onToggle}>{title}</ModalHeader>
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<Label>{gettext('Name')}</Label>
|
||||
<Input autoFocus={true} value={name} onChange={onChange}/>
|
||||
<Input style={{ display: 'none' }} />
|
||||
</FormGroup>
|
||||
</Form>
|
||||
{errorMessage && <Alert color="danger" className="mt-2">{errorMessage}</Alert>}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" disabled={isSubmitting} onClick={onToggle}>{gettext('Cancel')}</Button>
|
||||
<Button color="primary" disabled={isSubmitting} onClick={submit}>{gettext('Submit')}</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
NameDialog.propTypes = {
|
||||
value: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default NameDialog;
|
@@ -0,0 +1,4 @@
|
||||
.metadata-tree-view .sf-dropdown-toggle {
|
||||
display: inline-block;
|
||||
transform: rotate(90deg);
|
||||
}
|
179
frontend/src/metadata/metadata-tree-view/view-item/index.js
Normal file
179
frontend/src/metadata/metadata-tree-view/view-item/index.js
Normal file
@@ -0,0 +1,179 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
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 './index.css';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
|
||||
const ViewItem = ({
|
||||
canDelete,
|
||||
userPerm,
|
||||
isSelected,
|
||||
view,
|
||||
onClick,
|
||||
onDelete,
|
||||
onUpdate,
|
||||
onMove,
|
||||
}) => {
|
||||
const [highlight, setHighlight] = useState(false);
|
||||
const [freeze, setFreeze] = useState(false);
|
||||
const [isShowRenameDialog, setRenameDialogShow] = useState(false);
|
||||
const [isDropShow, setDropShow] = useState(false);
|
||||
const canUpdate = useMemo(() => {
|
||||
if (userPerm !== 'rw' && userPerm !== 'admin') return false;
|
||||
return true;
|
||||
}, [userPerm]);
|
||||
const canDrop = useMemo(() => {
|
||||
if (Utils.isIEBrower() || !canUpdate) return false;
|
||||
return true;
|
||||
}, [canUpdate]);
|
||||
const operations = useMemo(() => {
|
||||
if (!canUpdate) return [];
|
||||
let value = [
|
||||
{ key: 'rename', value: gettext('Rename') },
|
||||
];
|
||||
if (canDelete) {
|
||||
value.push({ key: 'delete', value: gettext('Delete') });
|
||||
}
|
||||
return value;
|
||||
}, [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);
|
||||
}, []);
|
||||
|
||||
const unfreezeItem = useCallback(() => {
|
||||
setFreeze(false);
|
||||
setHighlight(false);
|
||||
}, []);
|
||||
|
||||
const operationClick = useCallback((operationKey) => {
|
||||
if (operationKey === 'rename') {
|
||||
setRenameDialogShow(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (operationKey === 'delete') {
|
||||
onDelete();
|
||||
return;
|
||||
}
|
||||
}, [onDelete, view]);
|
||||
|
||||
const closeRenameDialog = useCallback(() => {
|
||||
setRenameDialogShow(false);
|
||||
}, []);
|
||||
|
||||
const renameView = useCallback((name, failCallback) => {
|
||||
onUpdate({ name }, () => {
|
||||
setRenameDialogShow(false);
|
||||
}, failCallback);
|
||||
}, [onUpdate]);
|
||||
|
||||
const onDragStart = useCallback((event) => {
|
||||
if (!canDrop) return false;
|
||||
const dragData = JSON.stringify({ type: 'sf-metadata-view', view_id: view._id });
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('applicaiton/drag-sf-metadata-view-info', dragData);
|
||||
}, [canDrop, view]);
|
||||
|
||||
const onDragEnter = useCallback((event) => {
|
||||
if (!canDrop) return false;
|
||||
setDropShow(true);
|
||||
}, [canDrop, view]);
|
||||
|
||||
const onDragLeave = useCallback(() => {
|
||||
if (!canDrop) return false;
|
||||
setDropShow(false);
|
||||
}, [canDrop, view]);
|
||||
|
||||
const onDragMove = useCallback(() => {
|
||||
if (!canDrop) return false;
|
||||
}, [canDrop]);
|
||||
|
||||
const onDrop = useCallback((event) => {
|
||||
if (!canDrop) return false;
|
||||
event.stopPropagation();
|
||||
setDropShow(false);
|
||||
|
||||
let dragData = event.dataTransfer.getData('applicaiton/drag-sf-metadata-view-info');
|
||||
if (!dragData) return;
|
||||
dragData = JSON.parse(dragData);
|
||||
if (dragData.type !== 'sf-metadata-view') return false;
|
||||
if (!dragData.view_id) return;
|
||||
onMove && onMove(dragData.view_id, view._id);
|
||||
}, [canDrop, view, onMove]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classnames('tree-node-inner text-nowrap', { 'tree-node-inner-hover': highlight, 'tree-node-hight-light': isSelected, 'tree-node-drop': isDropShow })}
|
||||
title={gettext('File extended properties')}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onClick={() => onClick(view)}
|
||||
>
|
||||
<div
|
||||
className="tree-node-text"
|
||||
draggable={canUpdate}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnter={onDragEnter}
|
||||
onDragLeave={onDragLeave}
|
||||
onDragOver={onDragMove}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
{view.name}
|
||||
</div>
|
||||
<div className="left-icon">
|
||||
<div className="tree-node-icon">
|
||||
<Icon symbol="table" className="metadata-views-icon" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="right-icon">
|
||||
{highlight && (
|
||||
<ItemDropdownMenu
|
||||
item={{ name: 'metadata-view' }}
|
||||
toggleClass="sf3-font sf3-font-more"
|
||||
freezeItem={freezeItem}
|
||||
unfreezeItem={unfreezeItem}
|
||||
getMenuList={() => operations}
|
||||
onMenuItemClick={operationClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isShowRenameDialog && (
|
||||
<NameDialog title={gettext('Rename view')} value={view.name} onSubmit={renameView} onToggle={closeRenameDialog} />
|
||||
)}
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
ViewItem.propTypes = {
|
||||
canDelete: PropTypes.bool,
|
||||
isSelected: PropTypes.bool,
|
||||
view: PropTypes.object,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ViewItem;
|
@@ -6,8 +6,7 @@ import { EVENT_BUS_TYPE } from '../../constants';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const ViewToolBar = () => {
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
const ViewToolBar = ({ metadataViewId }) => {
|
||||
const [view, setView] = useState(null);
|
||||
const [collaborators, setCollaborators] = useState([]);
|
||||
|
||||
@@ -41,28 +40,21 @@ const ViewToolBar = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let unsubscribeViewChange;
|
||||
let timer = setInterval(() => {
|
||||
if (window.sfMetadataContext && window.sfMetadataStore.data) {
|
||||
timer && clearInterval(timer);
|
||||
timer = null;
|
||||
setLoading(false);
|
||||
setView(window.sfMetadataStore.data.view);
|
||||
setCollaborators(window.sfMetadataStore?.collaborators || []);
|
||||
unsubscribeViewChange = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.VIEW_CHANGED, viewChange);
|
||||
}
|
||||
}, 300);
|
||||
return () => {
|
||||
timer && clearInterval(timer);
|
||||
unsubscribeViewChange && unsubscribeViewChange();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
const unsubscribeViewChange = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.VIEW_CHANGED, viewChange);
|
||||
return () => {
|
||||
unsubscribeViewChange();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoading]);
|
||||
}, [metadataViewId]);
|
||||
|
||||
if (!view) return null;
|
||||
|
||||
@@ -110,11 +102,7 @@ const ViewToolBar = () => {
|
||||
};
|
||||
|
||||
ViewToolBar.propTypes = {
|
||||
view: PropTypes.object,
|
||||
modifyFilters: PropTypes.func,
|
||||
modifySorts: PropTypes.func,
|
||||
modifyGroupbys: PropTypes.func,
|
||||
modifyHiddenColumns: PropTypes.func,
|
||||
metadataViewId: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ViewToolBar;
|
||||
|
@@ -25,8 +25,8 @@ class Context {
|
||||
this.metadataAPI = metadataAPI;
|
||||
|
||||
// init localStorage
|
||||
const { repoID } = this.settings;
|
||||
this.localStorage = new LocalStorage(`sf-metadata-${repoID}`);
|
||||
const { repoID, viewID } = this.settings;
|
||||
this.localStorage = new LocalStorage(`sf-metadata-${repoID}-${viewID}`);
|
||||
|
||||
// init userService
|
||||
this.userService = new UserService({ mediaUrl, api: this.metadataAPI.listUserInfo });
|
||||
@@ -71,6 +71,11 @@ class Context {
|
||||
return this.metadataAPI.getMetadata(repoID, params);
|
||||
};
|
||||
|
||||
getViews = () => {
|
||||
const repoID = this.settings['repoID'];
|
||||
return this.metadataAPI.listViews(repoID);
|
||||
};
|
||||
|
||||
canModifyCell = (column) => {
|
||||
const { editable } = column;
|
||||
if (!editable) return false;
|
||||
@@ -117,6 +122,10 @@ class Context {
|
||||
return this.metadataAPI.modifyRecords(repoId, recordsData, isCopyPaste);
|
||||
};
|
||||
|
||||
modifyView = (repoId, viewId, viewData) => {
|
||||
return this.metadataAPI.modifyView(repoId, viewId, viewData);
|
||||
};
|
||||
|
||||
getRowsByIds = () => {
|
||||
// todo
|
||||
};
|
||||
|
@@ -10,6 +10,9 @@ const MetadataContext = React.createContext(null);
|
||||
|
||||
export const MetadataProvider = ({
|
||||
children,
|
||||
repoID,
|
||||
viewID,
|
||||
currentRepoInfo,
|
||||
...params
|
||||
}) => {
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
@@ -31,12 +34,15 @@ export const MetadataProvider = ({
|
||||
|
||||
// init
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
// init context
|
||||
const context = new Context();
|
||||
window.sfMetadataContext = context;
|
||||
window.sfMetadataContext.init({ otherSettings: params });
|
||||
const repoId = window.sfMetadataContext.getSetting('repoID');
|
||||
storeRef.current = new Store({ context: window.sfMetadataContext, repoId });
|
||||
window.sfMetadataContext.setSetting('viewID', viewID);
|
||||
window.sfMetadataContext.setSetting('repoID', repoID);
|
||||
window.sfMetadataContext.setSetting('currentRepoInfo', currentRepoInfo);
|
||||
storeRef.current = new Store({ context: window.sfMetadataContext, repoId: repoID, viewId: viewID });
|
||||
window.sfMetadataStore = storeRef.current;
|
||||
storeRef.current.initStartIndex();
|
||||
storeRef.current.loadData(PER_LOAD_NUMBER).then(() => {
|
||||
@@ -54,13 +60,14 @@ export const MetadataProvider = ({
|
||||
|
||||
return () => {
|
||||
window.sfMetadataContext.destroy();
|
||||
window.sfMetadataStore.destroy();
|
||||
unsubscribeServerTableChanged();
|
||||
unsubscribeTableChanged();
|
||||
unsubscribeHandleTableError();
|
||||
unsubscribeUpdateRows();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [repoID, viewID, currentRepoInfo]);
|
||||
|
||||
return (
|
||||
<MetadataContext.Provider value={{ isLoading, metadata, store: storeRef.current }}>
|
||||
|
@@ -3,7 +3,9 @@ import {
|
||||
getRowById,
|
||||
getRowsByIds,
|
||||
} from '../_basic';
|
||||
import { Operation, LOCAL_APPLY_OPERATION_TYPE, NEED_APPLY_AFTER_SERVER_OPERATION, OPERATION_TYPE, UNDO_OPERATION_TYPE } from './operations';
|
||||
import { Operation, LOCAL_APPLY_OPERATION_TYPE, NEED_APPLY_AFTER_SERVER_OPERATION, OPERATION_TYPE, UNDO_OPERATION_TYPE,
|
||||
VIEW_OPERATION
|
||||
} from './operations';
|
||||
import { EVENT_BUS_TYPE, PER_LOAD_NUMBER } from '../constants';
|
||||
import DataProcessor from './data-processor';
|
||||
import ServerOperator from './server-operator';
|
||||
@@ -14,6 +16,7 @@ class Store {
|
||||
|
||||
constructor(props) {
|
||||
this.repoId = props.repoId;
|
||||
this.viewId = props.viewId;
|
||||
this.data = null;
|
||||
this.context = props.context;
|
||||
this.startIndex = 0;
|
||||
@@ -21,26 +24,30 @@ class Store {
|
||||
this.undos = [];
|
||||
this.pendingOperations = [];
|
||||
this.isSendingOperation = false;
|
||||
this.isTableReadonly = false;
|
||||
this.isReadonly = false;
|
||||
this.serverOperator = new ServerOperator();
|
||||
this.collaborators = [];
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
this.viewId = '';
|
||||
this.data = null;
|
||||
this.startIndex = 0;
|
||||
this.redos = [];
|
||||
this.undos = [];
|
||||
this.pendingOperations = [];
|
||||
this.isSendingOperation = false;
|
||||
};
|
||||
|
||||
initStartIndex = () => {
|
||||
this.startIndex = 0;
|
||||
};
|
||||
|
||||
saveView = () => {
|
||||
const { filters, sorts, gropbys, filter_conjunction } = this.data.view;
|
||||
const view = { filters, sorts, gropbys, filter_conjunction };
|
||||
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.VIEW_CHANGED, this.data.view);
|
||||
this.context.localStorage.setItem('view', view);
|
||||
};
|
||||
|
||||
async loadData(limit = PER_LOAD_NUMBER) {
|
||||
const res = await this.context.getMetadata({ start: this.startIndex, limit });
|
||||
const view = this.context.localStorage.getItem('view');
|
||||
const rows = res?.data?.results || [];
|
||||
const viewRes = await this.context.getViews();
|
||||
const view = viewRes?.data?.views.find(v => v._id === this.viewId) || {};
|
||||
const columns = normalizeColumns(res?.data?.metadata);
|
||||
let data = new Metadata({ rows, columns, view });
|
||||
data.view.rows = data.row_ids;
|
||||
@@ -144,6 +151,10 @@ class Store {
|
||||
this.syncOperationOnData(operation);
|
||||
}
|
||||
|
||||
if (VIEW_OPERATION.includes(operation.op_type)) {
|
||||
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.VIEW_CHANGED, this.data.view);
|
||||
}
|
||||
|
||||
operation.success_callback && operation.success_callback();
|
||||
this.context.eventBus.dispatch(EVENT_BUS_TYPE.SERVER_TABLE_CHANGED);
|
||||
|
||||
@@ -178,7 +189,7 @@ class Store {
|
||||
}
|
||||
|
||||
undoOperation() {
|
||||
if (this.isTableReadonly || this.undos.length === 0) return;
|
||||
if (this.isReadonly || this.undos.length === 0) return;
|
||||
const lastOperation = this.undos.pop();
|
||||
const lastInvertOperation = lastOperation.invert();
|
||||
if (NEED_APPLY_AFTER_SERVER_OPERATION.includes(lastInvertOperation.op_type)) {
|
||||
@@ -195,7 +206,7 @@ class Store {
|
||||
}
|
||||
|
||||
redoOperation() {
|
||||
if (this.isTableReadonly || this.redos.length === 0) return;
|
||||
if (this.isReadonly || this.redos.length === 0) return;
|
||||
let lastOperation = this.redos.pop();
|
||||
if (NEED_APPLY_AFTER_SERVER_OPERATION.includes(lastOperation.op_type)) {
|
||||
this.applyOperation(lastOperation, { handleUndo: false, asyncUndoRedo: (operation) => {
|
||||
@@ -325,37 +336,33 @@ class Store {
|
||||
modifyFilters(filterConjunction, filters) {
|
||||
const type = OPERATION_TYPE.MODIFY_FILTERS;
|
||||
const operation = this.createOperation({
|
||||
type, filter_conjunction: filterConjunction, filters,
|
||||
type, filter_conjunction: filterConjunction, filters, repo_id: this.repoId, view_id: this.viewId
|
||||
});
|
||||
this.applyOperation(operation);
|
||||
this.saveView();
|
||||
}
|
||||
|
||||
modifySorts(sorts) {
|
||||
const type = OPERATION_TYPE.MODIFY_SORTS;
|
||||
const operation = this.createOperation({
|
||||
type, sorts,
|
||||
type, sorts, repo_id: this.repoId, view_id: this.viewId
|
||||
});
|
||||
this.applyOperation(operation);
|
||||
this.saveView();
|
||||
}
|
||||
|
||||
modifyGroupbys(groupbys) {
|
||||
const type = OPERATION_TYPE.MODIFY_GROUPBYS;
|
||||
const operation = this.createOperation({
|
||||
type, groupbys,
|
||||
type, groupbys, repo_id: this.repoId, view_id: this.viewId
|
||||
});
|
||||
this.applyOperation(operation);
|
||||
this.saveView();
|
||||
}
|
||||
|
||||
modifyHiddenColumns(shown_column_keys) {
|
||||
const type = OPERATION_TYPE.MODIFY_HIDDEN_COLUMNS;
|
||||
const operation = this.createOperation({
|
||||
type, shown_column_keys
|
||||
type, shown_column_keys, repo_id: this.repoId, view_id: this.viewId
|
||||
});
|
||||
this.applyOperation(operation);
|
||||
this.saveView();
|
||||
}
|
||||
|
||||
insertColumn = (name, type, { key, data }) => {
|
||||
|
@@ -20,10 +20,10 @@ export const OPERATION_ATTRIBUTES = {
|
||||
[OPERATION_TYPE.MODIFY_RECORDS]: ['repo_id', 'row_ids', 'id_row_updates', 'id_original_row_updates', 'id_old_row_data', 'id_original_old_row_data', 'is_copy_paste'],
|
||||
[OPERATION_TYPE.RESTORE_RECORDS]: ['repo_id', 'rows_data', 'original_rows', 'link_infos', 'upper_row_ids'],
|
||||
[OPERATION_TYPE.RELOAD_RECORDS]: ['repo_id', 'row_ids'],
|
||||
[OPERATION_TYPE.MODIFY_FILTERS]: ['filter_conjunction', 'filters'],
|
||||
[OPERATION_TYPE.MODIFY_SORTS]: ['sorts'],
|
||||
[OPERATION_TYPE.MODIFY_GROUPBYS]: ['groupbys'],
|
||||
[OPERATION_TYPE.MODIFY_HIDDEN_COLUMNS]: ['shown_column_keys'],
|
||||
[OPERATION_TYPE.MODIFY_FILTERS]: ['repo_id', 'view_id', 'filter_conjunction', 'filters'],
|
||||
[OPERATION_TYPE.MODIFY_SORTS]: ['repo_id', 'view_id', 'sorts'],
|
||||
[OPERATION_TYPE.MODIFY_GROUPBYS]: ['repo_id', 'view_id', 'groupbys'],
|
||||
[OPERATION_TYPE.MODIFY_HIDDEN_COLUMNS]: ['repo_id', 'view_id', 'shown_column_keys'],
|
||||
[OPERATION_TYPE.LOCK_RECORD_VIA_BUTTON]: ['repo_id', 'row_id', 'button_column_key'],
|
||||
[OPERATION_TYPE.MODIFY_RECORD_VIA_BUTTON]: ['repo_id', 'row_id', 'updates', 'old_row_data', 'original_updates', 'original_old_row_data', 'button_column_key'],
|
||||
[OPERATION_TYPE.INSERT_COLUMN]: ['repo_id', 'name', 'column_type', 'key', 'data'],
|
||||
@@ -38,10 +38,7 @@ export const UNDO_OPERATION_TYPE = [
|
||||
|
||||
// only apply operation on the local
|
||||
export const LOCAL_APPLY_OPERATION_TYPE = [
|
||||
OPERATION_TYPE.MODIFY_FILTERS,
|
||||
OPERATION_TYPE.MODIFY_SORTS,
|
||||
OPERATION_TYPE.MODIFY_GROUPBYS,
|
||||
OPERATION_TYPE.MODIFY_HIDDEN_COLUMNS,
|
||||
|
||||
];
|
||||
|
||||
// apply operation after exec operation on the server
|
||||
@@ -49,4 +46,15 @@ export const NEED_APPLY_AFTER_SERVER_OPERATION = [
|
||||
OPERATION_TYPE.INSERT_COLUMN,
|
||||
OPERATION_TYPE.MODIFY_RECORD,
|
||||
OPERATION_TYPE.MODIFY_RECORDS,
|
||||
OPERATION_TYPE.MODIFY_FILTERS,
|
||||
OPERATION_TYPE.MODIFY_SORTS,
|
||||
OPERATION_TYPE.MODIFY_GROUPBYS,
|
||||
OPERATION_TYPE.MODIFY_HIDDEN_COLUMNS,
|
||||
];
|
||||
|
||||
export const VIEW_OPERATION = [
|
||||
OPERATION_TYPE.MODIFY_FILTERS,
|
||||
OPERATION_TYPE.MODIFY_SORTS,
|
||||
OPERATION_TYPE.MODIFY_GROUPBYS,
|
||||
OPERATION_TYPE.MODIFY_HIDDEN_COLUMNS,
|
||||
];
|
||||
|
@@ -8,6 +8,7 @@ export {
|
||||
UNDO_OPERATION_TYPE,
|
||||
LOCAL_APPLY_OPERATION_TYPE,
|
||||
NEED_APPLY_AFTER_SERVER_OPERATION,
|
||||
VIEW_OPERATION,
|
||||
} from './constants';
|
||||
|
||||
export {
|
||||
|
@@ -78,7 +78,42 @@ class ServerOperator {
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case OPERATION_TYPE.MODIFY_FILTERS: {
|
||||
const { repo_id, view_id, filter_conjunction, filters } = operation;
|
||||
window.sfMetadataContext.modifyView(repo_id, view_id, { filters, filter_conjunction }).then(res => {
|
||||
callback({ operation });
|
||||
}).catch(error => {
|
||||
callback({ error: 'Failed_to_modify_filter' });
|
||||
});
|
||||
break;
|
||||
}
|
||||
case OPERATION_TYPE.MODIFY_SORTS: {
|
||||
const { repo_id, view_id, sorts } = operation;
|
||||
window.sfMetadataContext.modifyView(repo_id, view_id, { sorts }).then(res => {
|
||||
callback({ operation });
|
||||
}).catch(error => {
|
||||
callback({ error: 'Failed_to_modify_sort' });
|
||||
});
|
||||
break;
|
||||
}
|
||||
case OPERATION_TYPE.MODIFY_GROUPBYS: {
|
||||
const { repo_id, view_id, groupbys } = operation;
|
||||
window.sfMetadataContext.modifyView(repo_id, view_id, { groupbys }).then(res => {
|
||||
callback({ operation });
|
||||
}).catch(error => {
|
||||
callback({ error: 'Failed_to_modify_group' });
|
||||
});
|
||||
break;
|
||||
}
|
||||
case OPERATION_TYPE.MODIFY_HIDDEN_COLUMNS: {
|
||||
const { repo_id, view_id, shown_column_keys } = operation;
|
||||
window.sfMetadataContext.modifyView(repo_id, view_id, { shown_column_keys }).then(res => {
|
||||
callback({ operation });
|
||||
}).catch(error => {
|
||||
callback({ error: 'Failed_to_modify_hidden_columns' });
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
|
@@ -39,6 +39,7 @@ const propTypes = {
|
||||
isFileLoading: PropTypes.bool.isRequired,
|
||||
filePermission: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
metadataViewId: PropTypes.string,
|
||||
lastModified: PropTypes.string,
|
||||
latestContributor: PropTypes.string,
|
||||
onLinkClick: PropTypes.func.isRequired,
|
||||
@@ -220,6 +221,7 @@ class LibContentContainer extends React.Component {
|
||||
filePermission={this.props.filePermission}
|
||||
onFileTagChanged={this.props.onToolbarFileTagChanged}
|
||||
repoTags={this.props.repoTags}
|
||||
metadataViewId={this.props.metadataViewId}
|
||||
/>
|
||||
<ToolbarForSelectedDirents
|
||||
repoID={this.props.repoID}
|
||||
@@ -275,6 +277,7 @@ class LibContentContainer extends React.Component {
|
||||
hash={this.props.hash}
|
||||
filePermission={this.props.filePermission}
|
||||
content={this.props.content}
|
||||
metadataViewId={this.props.metadataViewId}
|
||||
lastModified={this.props.lastModified}
|
||||
latestContributor={this.props.latestContributor}
|
||||
onLinkClick={this.props.onLinkClick}
|
||||
|
@@ -85,6 +85,7 @@ class LibContentView extends React.Component {
|
||||
asyncOperationType: 'move',
|
||||
asyncOperationProgress: 0,
|
||||
asyncOperatedFilesLength: 0,
|
||||
metadataViewId: '0000',
|
||||
};
|
||||
|
||||
this.oldonpopstate = window.onpopstate;
|
||||
@@ -490,9 +491,10 @@ class LibContentView extends React.Component {
|
||||
window.history.pushState({ url: url, path: filePath }, filePath, url);
|
||||
};
|
||||
|
||||
showFileMetadata = (filePath) => {
|
||||
showFileMetadata = (filePath, viewId) => {
|
||||
if (this.state.metadataViewId === viewId) return;
|
||||
const repoID = this.props.repoID;
|
||||
this.setState({ path: filePath, isViewFile: true, isFileLoading: false, isFileLoadedErr: false, content: '__sf-metadata' });
|
||||
this.setState({ path: filePath, isViewFile: true, isFileLoading: false, isFileLoadedErr: false, content: '__sf-metadata', metadataViewId: viewId });
|
||||
const repoInfo = this.state.currentRepoInfo;
|
||||
const url = siteRoot + 'library/' + repoID + '/' + encodeURIComponent(repoInfo.repo_name);
|
||||
window.history.pushState({ url: url, path: '' }, '', url);
|
||||
@@ -1718,7 +1720,7 @@ class LibContentView extends React.Component {
|
||||
}
|
||||
} else if (Utils.isFileMetadata(node?.object?.type)) {
|
||||
if (node.path !== this.state.path) {
|
||||
this.showFileMetadata(node.path);
|
||||
this.showFileMetadata(node.path, node.view_id || '0000');
|
||||
}
|
||||
} else {
|
||||
let url = siteRoot + 'lib/' + repoID + '/file' + Utils.encodePath(node.path);
|
||||
@@ -2084,6 +2086,7 @@ class LibContentView extends React.Component {
|
||||
isFileLoadedErr={this.state.isFileLoadedErr}
|
||||
filePermission={this.state.filePermission}
|
||||
content={this.state.content}
|
||||
metadataViewId={this.state.metadataViewId}
|
||||
lastModified={this.state.lastModified}
|
||||
latestContributor={this.state.latestContributor}
|
||||
onLinkClick={this.onLinkClick}
|
||||
|
Reference in New Issue
Block a user