mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-02 07:27:04 +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 === '') {
|
if (item === '') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (index === (pathList.length - 1)) {
|
if (index === pathList.length - 2 && item === PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES) {
|
||||||
if (item === PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES) {
|
return (
|
||||||
return (
|
<Fragment key={index}>
|
||||||
<Fragment key={index}>
|
<span className="path-split">/</span>
|
||||||
<span className="path-split">/</span>
|
<span className="path-item">{gettext('File extended properties')}</span>
|
||||||
<span className="path-item">{gettext('File extended properties')}</span>
|
</Fragment>
|
||||||
</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 (
|
return (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
<span className="path-split">/</span>
|
<span className="path-split">/</span>
|
||||||
@@ -129,7 +138,7 @@ class DirPath extends React.Component {
|
|||||||
const { currentPath } = this.props;
|
const { currentPath } = this.props;
|
||||||
const path = currentPath[currentPath.length - 1] === '/' ? currentPath.slice(0, currentPath.length - 1) : currentPath;
|
const path = currentPath[currentPath.length - 1] === '/' ? currentPath.slice(0, currentPath.length - 1) : currentPath;
|
||||||
const pathList = path.split('/');
|
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() {
|
render() {
|
||||||
|
@@ -23,6 +23,7 @@ const propTypes = {
|
|||||||
sortBy: PropTypes.string,
|
sortBy: PropTypes.string,
|
||||||
sortOrder: PropTypes.string,
|
sortOrder: PropTypes.string,
|
||||||
sortItems: PropTypes.func,
|
sortItems: PropTypes.func,
|
||||||
|
metadataViewId: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
class DirTool extends React.Component {
|
class DirTool extends React.Component {
|
||||||
@@ -97,9 +98,9 @@ class DirTool extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const menuItems = this.getMenu();
|
const menuItems = this.getMenu();
|
||||||
const { isDropdownMenuOpen } = this.state;
|
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 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 => {
|
const sortOptions = this.sortOptions.map(item => {
|
||||||
return {
|
return {
|
||||||
@@ -111,7 +112,7 @@ class DirTool extends React.Component {
|
|||||||
if (isFileExtended) {
|
if (isFileExtended) {
|
||||||
return (
|
return (
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
<MetadataViewToolBar />
|
<MetadataViewToolBar metadataViewId={metadataViewId} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -37,6 +37,7 @@ const propTypes = {
|
|||||||
filePermission: PropTypes.string,
|
filePermission: PropTypes.string,
|
||||||
repoTags: PropTypes.array.isRequired,
|
repoTags: PropTypes.array.isRequired,
|
||||||
onFileTagChanged: PropTypes.func.isRequired,
|
onFileTagChanged: PropTypes.func.isRequired,
|
||||||
|
metadataViewId: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
class CurDirPath extends React.Component {
|
class CurDirPath extends React.Component {
|
||||||
@@ -96,6 +97,7 @@ class CurDirPath extends React.Component {
|
|||||||
sortBy={this.props.sortBy}
|
sortBy={this.props.sortBy}
|
||||||
sortOrder={this.props.sortOrder}
|
sortOrder={this.props.sortOrder}
|
||||||
sortItems={this.props.sortItems}
|
sortItems={this.props.sortItems}
|
||||||
|
metadataViewId={this.props.metadataViewId}
|
||||||
/>}
|
/>}
|
||||||
{!isDesktop && this.props.direntList.length > 0 &&
|
{!isDesktop && this.props.direntList.length > 0 &&
|
||||||
<span className="sf3-font sf3-font-sort action-icon" onClick={this.toggleSortOptionsDialog}></span>}
|
<span className="sf3-font sf3-font-sort action-icon" onClick={this.toggleSortOptionsDialog}></span>}
|
||||||
|
@@ -13,6 +13,7 @@ const propTypes = {
|
|||||||
isFileLoadedErr: PropTypes.bool.isRequired,
|
isFileLoadedErr: PropTypes.bool.isRequired,
|
||||||
filePermission: PropTypes.string,
|
filePermission: PropTypes.string,
|
||||||
content: PropTypes.string,
|
content: PropTypes.string,
|
||||||
|
metadataViewId: PropTypes.string,
|
||||||
lastModified: PropTypes.string,
|
lastModified: PropTypes.string,
|
||||||
latestContributor: PropTypes.string,
|
latestContributor: PropTypes.string,
|
||||||
onLinkClick: PropTypes.func.isRequired,
|
onLinkClick: PropTypes.func.isRequired,
|
||||||
@@ -52,13 +53,14 @@ class DirColumnFile extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.content === '__sf-metadata') {
|
if (this.props.content === '__sf-metadata') {
|
||||||
|
const { repoID, currentRepoInfo, metadataViewId } = this.props;
|
||||||
window.sfMetadata = {
|
window.sfMetadata = {
|
||||||
siteRoot,
|
siteRoot,
|
||||||
lang,
|
lang,
|
||||||
mediaUrl,
|
mediaUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (<SeafileMetadata repoID={this.props.repoID} currentRepoInfo={this.props.currentRepoInfo} />);
|
return (<SeafileMetadata repoID={repoID} currentRepoInfo={currentRepoInfo} viewID={metadataViewId} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -37,6 +37,7 @@ const propTypes = {
|
|||||||
hash: PropTypes.string,
|
hash: PropTypes.string,
|
||||||
filePermission: PropTypes.string,
|
filePermission: PropTypes.string,
|
||||||
content: PropTypes.string,
|
content: PropTypes.string,
|
||||||
|
metadataViewId: PropTypes.string,
|
||||||
lastModified: PropTypes.string,
|
lastModified: PropTypes.string,
|
||||||
latestContributor: PropTypes.string,
|
latestContributor: PropTypes.string,
|
||||||
onLinkClick: PropTypes.func.isRequired,
|
onLinkClick: PropTypes.func.isRequired,
|
||||||
@@ -192,6 +193,7 @@ class DirColumnView extends React.Component {
|
|||||||
isFileLoadedErr={this.props.isFileLoadedErr}
|
isFileLoadedErr={this.props.isFileLoadedErr}
|
||||||
filePermission={this.props.filePermission}
|
filePermission={this.props.filePermission}
|
||||||
content={this.props.content}
|
content={this.props.content}
|
||||||
|
metadataViewId={this.props.metadataViewId}
|
||||||
currentRepoInfo={this.props.currentRepoInfo}
|
currentRepoInfo={this.props.currentRepoInfo}
|
||||||
lastModified={this.props.lastModified}
|
lastModified={this.props.lastModified}
|
||||||
latestContributor={this.props.latestContributor}
|
latestContributor={this.props.latestContributor}
|
||||||
|
@@ -67,7 +67,7 @@ const DirViews = ({ userPerm, repoID, currentPath, onNodeClick }) => {
|
|||||||
moreOperations={moreOperations}
|
moreOperations={moreOperations}
|
||||||
moreOperationClick={moreOperationClick}
|
moreOperationClick={moreOperationClick}
|
||||||
>
|
>
|
||||||
{!loading && metadataStatus && (<MetadataTreeView repoID={repoID} currentPath={currentPath} onNodeClick={onNodeClick} />)}
|
{!loading && metadataStatus && (<MetadataTreeView userPerm={userPerm} repoID={repoID} currentPath={currentPath} onNodeClick={onNodeClick} />)}
|
||||||
</TreeSection>
|
</TreeSection>
|
||||||
{showMetadataStatusManagementDialog && (
|
{showMetadataStatusManagementDialog && (
|
||||||
<MetadataStatusManagementDialog
|
<MetadataStatusManagementDialog
|
||||||
|
@@ -107,6 +107,7 @@ class TreeView extends React.Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let dragStartNodeData = e.dataTransfer.getData('applicaiton/drag-item-info');
|
let dragStartNodeData = e.dataTransfer.getData('applicaiton/drag-item-info');
|
||||||
|
if (!dragStartNodeData) return;
|
||||||
dragStartNodeData = JSON.parse(dragStartNodeData);
|
dragStartNodeData = JSON.parse(dragStartNodeData);
|
||||||
|
|
||||||
let { nodeDirent, nodeParentPath, nodeRootPath } = dragStartNodeData;
|
let { nodeDirent, nodeParentPath, nodeRootPath } = dragStartNodeData;
|
||||||
|
@@ -117,6 +117,43 @@ class MetadataManagerAPI {
|
|||||||
return this._sendPostRequest(url, params, { headers: { 'Content-type': 'application/json' } });
|
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();
|
const metadataAPI = new MetadataManagerAPI();
|
||||||
|
@@ -26,3 +26,33 @@
|
|||||||
width: 14px;
|
width: 14px;
|
||||||
line-height: 1.5;
|
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 PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
|
||||||
import { gettext } from '../../utils/constants';
|
import { gettext } from '../../utils/constants';
|
||||||
import Icon from '../../components/icon';
|
|
||||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
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 './index.css';
|
||||||
|
import { CustomizeAddTool } from '@seafile/sf-metadata-ui-component';
|
||||||
|
|
||||||
const MetadataTreeView = ({ repoID, currentPath, onNodeClick }) => {
|
const MetadataTreeView = ({ userPerm, repoID, currentPath, onNodeClick }) => {
|
||||||
const node = useMemo(() => {
|
const canAdd = useMemo(() => {
|
||||||
return {
|
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: [],
|
children: [],
|
||||||
path: '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES,
|
path: '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view.name,
|
||||||
isExpanded: false,
|
isExpanded: false,
|
||||||
isLoaded: true,
|
isLoaded: true,
|
||||||
isPreload: true,
|
isPreload: true,
|
||||||
@@ -24,48 +50,105 @@ const MetadataTreeView = ({ repoID, currentPath, onNodeClick }) => {
|
|||||||
},
|
},
|
||||||
parentNode: {},
|
parentNode: {},
|
||||||
key: repoID,
|
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]);
|
}, [repoID]);
|
||||||
const [highlight, setHighlight] = useState(false);
|
|
||||||
|
|
||||||
const onMouseEnter = useCallback(() => {
|
|
||||||
setHighlight(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onMouseOver = useCallback(() => {
|
|
||||||
setHighlight(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onMouseLeave = useCallback(() => {
|
|
||||||
setHighlight(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tree-view tree metadata-tree-view">
|
<>
|
||||||
<div className="tree-node">
|
<div className="tree-view tree metadata-tree-view">
|
||||||
<div className="children">
|
<div className="tree-node">
|
||||||
<div
|
<div className="children">
|
||||||
className={classnames('tree-node-inner text-nowrap', { 'tree-node-inner-hover': highlight, 'tree-node-hight-light': currentPath === node.path })}
|
{views.map((item, index) => {
|
||||||
title={gettext('File extended properties')}
|
if (item.type !== 'view') return null;
|
||||||
onMouseEnter={onMouseEnter}
|
const view = viewsMap.current[item._id];
|
||||||
onMouseOver={onMouseOver}
|
const viewPath = '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view.name;
|
||||||
onMouseLeave={onMouseLeave}
|
const isSelected = currentPath === viewPath;
|
||||||
onClick={() => onNodeClick(node)}
|
return (
|
||||||
>
|
<ViewItem
|
||||||
<div className="tree-node-text">{gettext('File extended properties')}</div>
|
key={view._id}
|
||||||
<div className="left-icon">
|
canDelete={index !== 0}
|
||||||
<div className="tree-node-icon">
|
isSelected={isSelected}
|
||||||
<Icon symbol="table" className="metadata-views-icon" />
|
userPerm={userPerm}
|
||||||
</div>
|
view={view}
|
||||||
</div>
|
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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{showAddViewDialog && (<NameDialog title={gettext('Add view')} onSubmit={addView} onToggle={closeAddView} />)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
MetadataTreeView.propTypes = {
|
MetadataTreeView.propTypes = {
|
||||||
|
userPerm: PropTypes.string,
|
||||||
repoID: PropTypes.string.isRequired,
|
repoID: PropTypes.string.isRequired,
|
||||||
currentPath: PropTypes.string,
|
currentPath: PropTypes.string,
|
||||||
onNodeClick: PropTypes.func,
|
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';
|
import './index.css';
|
||||||
|
|
||||||
const ViewToolBar = () => {
|
const ViewToolBar = ({ metadataViewId }) => {
|
||||||
const [isLoading, setLoading] = useState(true);
|
|
||||||
const [view, setView] = useState(null);
|
const [view, setView] = useState(null);
|
||||||
const [collaborators, setCollaborators] = useState([]);
|
const [collaborators, setCollaborators] = useState([]);
|
||||||
|
|
||||||
@@ -41,28 +40,21 @@ const ViewToolBar = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let unsubscribeViewChange;
|
||||||
let timer = setInterval(() => {
|
let timer = setInterval(() => {
|
||||||
if (window.sfMetadataContext && window.sfMetadataStore.data) {
|
if (window.sfMetadataContext && window.sfMetadataStore.data) {
|
||||||
timer && clearInterval(timer);
|
timer && clearInterval(timer);
|
||||||
timer = null;
|
timer = null;
|
||||||
setLoading(false);
|
|
||||||
setView(window.sfMetadataStore.data.view);
|
setView(window.sfMetadataStore.data.view);
|
||||||
setCollaborators(window.sfMetadataStore?.collaborators || []);
|
setCollaborators(window.sfMetadataStore?.collaborators || []);
|
||||||
|
unsubscribeViewChange = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.VIEW_CHANGED, viewChange);
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
return () => {
|
return () => {
|
||||||
timer && clearInterval(timer);
|
timer && clearInterval(timer);
|
||||||
|
unsubscribeViewChange && unsubscribeViewChange();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [metadataViewId]);
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
if (!view) return null;
|
if (!view) return null;
|
||||||
|
|
||||||
@@ -110,11 +102,7 @@ const ViewToolBar = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ViewToolBar.propTypes = {
|
ViewToolBar.propTypes = {
|
||||||
view: PropTypes.object,
|
metadataViewId: PropTypes.string,
|
||||||
modifyFilters: PropTypes.func,
|
|
||||||
modifySorts: PropTypes.func,
|
|
||||||
modifyGroupbys: PropTypes.func,
|
|
||||||
modifyHiddenColumns: PropTypes.func,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ViewToolBar;
|
export default ViewToolBar;
|
||||||
|
@@ -25,8 +25,8 @@ class Context {
|
|||||||
this.metadataAPI = metadataAPI;
|
this.metadataAPI = metadataAPI;
|
||||||
|
|
||||||
// init localStorage
|
// init localStorage
|
||||||
const { repoID } = this.settings;
|
const { repoID, viewID } = this.settings;
|
||||||
this.localStorage = new LocalStorage(`sf-metadata-${repoID}`);
|
this.localStorage = new LocalStorage(`sf-metadata-${repoID}-${viewID}`);
|
||||||
|
|
||||||
// init userService
|
// init userService
|
||||||
this.userService = new UserService({ mediaUrl, api: this.metadataAPI.listUserInfo });
|
this.userService = new UserService({ mediaUrl, api: this.metadataAPI.listUserInfo });
|
||||||
@@ -71,6 +71,11 @@ class Context {
|
|||||||
return this.metadataAPI.getMetadata(repoID, params);
|
return this.metadataAPI.getMetadata(repoID, params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getViews = () => {
|
||||||
|
const repoID = this.settings['repoID'];
|
||||||
|
return this.metadataAPI.listViews(repoID);
|
||||||
|
};
|
||||||
|
|
||||||
canModifyCell = (column) => {
|
canModifyCell = (column) => {
|
||||||
const { editable } = column;
|
const { editable } = column;
|
||||||
if (!editable) return false;
|
if (!editable) return false;
|
||||||
@@ -117,6 +122,10 @@ class Context {
|
|||||||
return this.metadataAPI.modifyRecords(repoId, recordsData, isCopyPaste);
|
return this.metadataAPI.modifyRecords(repoId, recordsData, isCopyPaste);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
modifyView = (repoId, viewId, viewData) => {
|
||||||
|
return this.metadataAPI.modifyView(repoId, viewId, viewData);
|
||||||
|
};
|
||||||
|
|
||||||
getRowsByIds = () => {
|
getRowsByIds = () => {
|
||||||
// todo
|
// todo
|
||||||
};
|
};
|
||||||
|
@@ -10,6 +10,9 @@ const MetadataContext = React.createContext(null);
|
|||||||
|
|
||||||
export const MetadataProvider = ({
|
export const MetadataProvider = ({
|
||||||
children,
|
children,
|
||||||
|
repoID,
|
||||||
|
viewID,
|
||||||
|
currentRepoInfo,
|
||||||
...params
|
...params
|
||||||
}) => {
|
}) => {
|
||||||
const [isLoading, setLoading] = useState(true);
|
const [isLoading, setLoading] = useState(true);
|
||||||
@@ -31,12 +34,15 @@ export const MetadataProvider = ({
|
|||||||
|
|
||||||
// init
|
// init
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
// init context
|
// init context
|
||||||
const context = new Context();
|
const context = new Context();
|
||||||
window.sfMetadataContext = context;
|
window.sfMetadataContext = context;
|
||||||
window.sfMetadataContext.init({ otherSettings: params });
|
window.sfMetadataContext.init({ otherSettings: params });
|
||||||
const repoId = window.sfMetadataContext.getSetting('repoID');
|
window.sfMetadataContext.setSetting('viewID', viewID);
|
||||||
storeRef.current = new Store({ context: window.sfMetadataContext, repoId });
|
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;
|
window.sfMetadataStore = storeRef.current;
|
||||||
storeRef.current.initStartIndex();
|
storeRef.current.initStartIndex();
|
||||||
storeRef.current.loadData(PER_LOAD_NUMBER).then(() => {
|
storeRef.current.loadData(PER_LOAD_NUMBER).then(() => {
|
||||||
@@ -54,13 +60,14 @@ export const MetadataProvider = ({
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.sfMetadataContext.destroy();
|
window.sfMetadataContext.destroy();
|
||||||
|
window.sfMetadataStore.destroy();
|
||||||
unsubscribeServerTableChanged();
|
unsubscribeServerTableChanged();
|
||||||
unsubscribeTableChanged();
|
unsubscribeTableChanged();
|
||||||
unsubscribeHandleTableError();
|
unsubscribeHandleTableError();
|
||||||
unsubscribeUpdateRows();
|
unsubscribeUpdateRows();
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, [repoID, viewID, currentRepoInfo]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MetadataContext.Provider value={{ isLoading, metadata, store: storeRef.current }}>
|
<MetadataContext.Provider value={{ isLoading, metadata, store: storeRef.current }}>
|
||||||
|
@@ -3,7 +3,9 @@ import {
|
|||||||
getRowById,
|
getRowById,
|
||||||
getRowsByIds,
|
getRowsByIds,
|
||||||
} from '../_basic';
|
} 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 { EVENT_BUS_TYPE, PER_LOAD_NUMBER } from '../constants';
|
||||||
import DataProcessor from './data-processor';
|
import DataProcessor from './data-processor';
|
||||||
import ServerOperator from './server-operator';
|
import ServerOperator from './server-operator';
|
||||||
@@ -14,6 +16,7 @@ class Store {
|
|||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
this.repoId = props.repoId;
|
this.repoId = props.repoId;
|
||||||
|
this.viewId = props.viewId;
|
||||||
this.data = null;
|
this.data = null;
|
||||||
this.context = props.context;
|
this.context = props.context;
|
||||||
this.startIndex = 0;
|
this.startIndex = 0;
|
||||||
@@ -21,26 +24,30 @@ class Store {
|
|||||||
this.undos = [];
|
this.undos = [];
|
||||||
this.pendingOperations = [];
|
this.pendingOperations = [];
|
||||||
this.isSendingOperation = false;
|
this.isSendingOperation = false;
|
||||||
this.isTableReadonly = false;
|
this.isReadonly = false;
|
||||||
this.serverOperator = new ServerOperator();
|
this.serverOperator = new ServerOperator();
|
||||||
this.collaborators = [];
|
this.collaborators = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
destroy = () => {
|
||||||
|
this.viewId = '';
|
||||||
|
this.data = null;
|
||||||
|
this.startIndex = 0;
|
||||||
|
this.redos = [];
|
||||||
|
this.undos = [];
|
||||||
|
this.pendingOperations = [];
|
||||||
|
this.isSendingOperation = false;
|
||||||
|
};
|
||||||
|
|
||||||
initStartIndex = () => {
|
initStartIndex = () => {
|
||||||
this.startIndex = 0;
|
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) {
|
async loadData(limit = PER_LOAD_NUMBER) {
|
||||||
const res = await this.context.getMetadata({ start: this.startIndex, limit });
|
const res = await this.context.getMetadata({ start: this.startIndex, limit });
|
||||||
const view = this.context.localStorage.getItem('view');
|
|
||||||
const rows = res?.data?.results || [];
|
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);
|
const columns = normalizeColumns(res?.data?.metadata);
|
||||||
let data = new Metadata({ rows, columns, view });
|
let data = new Metadata({ rows, columns, view });
|
||||||
data.view.rows = data.row_ids;
|
data.view.rows = data.row_ids;
|
||||||
@@ -144,6 +151,10 @@ class Store {
|
|||||||
this.syncOperationOnData(operation);
|
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();
|
operation.success_callback && operation.success_callback();
|
||||||
this.context.eventBus.dispatch(EVENT_BUS_TYPE.SERVER_TABLE_CHANGED);
|
this.context.eventBus.dispatch(EVENT_BUS_TYPE.SERVER_TABLE_CHANGED);
|
||||||
|
|
||||||
@@ -178,7 +189,7 @@ class Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
undoOperation() {
|
undoOperation() {
|
||||||
if (this.isTableReadonly || this.undos.length === 0) return;
|
if (this.isReadonly || this.undos.length === 0) return;
|
||||||
const lastOperation = this.undos.pop();
|
const lastOperation = this.undos.pop();
|
||||||
const lastInvertOperation = lastOperation.invert();
|
const lastInvertOperation = lastOperation.invert();
|
||||||
if (NEED_APPLY_AFTER_SERVER_OPERATION.includes(lastInvertOperation.op_type)) {
|
if (NEED_APPLY_AFTER_SERVER_OPERATION.includes(lastInvertOperation.op_type)) {
|
||||||
@@ -195,7 +206,7 @@ class Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
redoOperation() {
|
redoOperation() {
|
||||||
if (this.isTableReadonly || this.redos.length === 0) return;
|
if (this.isReadonly || this.redos.length === 0) return;
|
||||||
let lastOperation = this.redos.pop();
|
let lastOperation = this.redos.pop();
|
||||||
if (NEED_APPLY_AFTER_SERVER_OPERATION.includes(lastOperation.op_type)) {
|
if (NEED_APPLY_AFTER_SERVER_OPERATION.includes(lastOperation.op_type)) {
|
||||||
this.applyOperation(lastOperation, { handleUndo: false, asyncUndoRedo: (operation) => {
|
this.applyOperation(lastOperation, { handleUndo: false, asyncUndoRedo: (operation) => {
|
||||||
@@ -325,37 +336,33 @@ class Store {
|
|||||||
modifyFilters(filterConjunction, filters) {
|
modifyFilters(filterConjunction, filters) {
|
||||||
const type = OPERATION_TYPE.MODIFY_FILTERS;
|
const type = OPERATION_TYPE.MODIFY_FILTERS;
|
||||||
const operation = this.createOperation({
|
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.applyOperation(operation);
|
||||||
this.saveView();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
modifySorts(sorts) {
|
modifySorts(sorts) {
|
||||||
const type = OPERATION_TYPE.MODIFY_SORTS;
|
const type = OPERATION_TYPE.MODIFY_SORTS;
|
||||||
const operation = this.createOperation({
|
const operation = this.createOperation({
|
||||||
type, sorts,
|
type, sorts, repo_id: this.repoId, view_id: this.viewId
|
||||||
});
|
});
|
||||||
this.applyOperation(operation);
|
this.applyOperation(operation);
|
||||||
this.saveView();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
modifyGroupbys(groupbys) {
|
modifyGroupbys(groupbys) {
|
||||||
const type = OPERATION_TYPE.MODIFY_GROUPBYS;
|
const type = OPERATION_TYPE.MODIFY_GROUPBYS;
|
||||||
const operation = this.createOperation({
|
const operation = this.createOperation({
|
||||||
type, groupbys,
|
type, groupbys, repo_id: this.repoId, view_id: this.viewId
|
||||||
});
|
});
|
||||||
this.applyOperation(operation);
|
this.applyOperation(operation);
|
||||||
this.saveView();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
modifyHiddenColumns(shown_column_keys) {
|
modifyHiddenColumns(shown_column_keys) {
|
||||||
const type = OPERATION_TYPE.MODIFY_HIDDEN_COLUMNS;
|
const type = OPERATION_TYPE.MODIFY_HIDDEN_COLUMNS;
|
||||||
const operation = this.createOperation({
|
const operation = this.createOperation({
|
||||||
type, shown_column_keys
|
type, shown_column_keys, repo_id: this.repoId, view_id: this.viewId
|
||||||
});
|
});
|
||||||
this.applyOperation(operation);
|
this.applyOperation(operation);
|
||||||
this.saveView();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
insertColumn = (name, type, { key, data }) => {
|
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.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.RESTORE_RECORDS]: ['repo_id', 'rows_data', 'original_rows', 'link_infos', 'upper_row_ids'],
|
||||||
[OPERATION_TYPE.RELOAD_RECORDS]: ['repo_id', 'row_ids'],
|
[OPERATION_TYPE.RELOAD_RECORDS]: ['repo_id', 'row_ids'],
|
||||||
[OPERATION_TYPE.MODIFY_FILTERS]: ['filter_conjunction', 'filters'],
|
[OPERATION_TYPE.MODIFY_FILTERS]: ['repo_id', 'view_id', 'filter_conjunction', 'filters'],
|
||||||
[OPERATION_TYPE.MODIFY_SORTS]: ['sorts'],
|
[OPERATION_TYPE.MODIFY_SORTS]: ['repo_id', 'view_id', 'sorts'],
|
||||||
[OPERATION_TYPE.MODIFY_GROUPBYS]: ['groupbys'],
|
[OPERATION_TYPE.MODIFY_GROUPBYS]: ['repo_id', 'view_id', 'groupbys'],
|
||||||
[OPERATION_TYPE.MODIFY_HIDDEN_COLUMNS]: ['shown_column_keys'],
|
[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.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.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'],
|
[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
|
// only apply operation on the local
|
||||||
export const LOCAL_APPLY_OPERATION_TYPE = [
|
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
|
// 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.INSERT_COLUMN,
|
||||||
OPERATION_TYPE.MODIFY_RECORD,
|
OPERATION_TYPE.MODIFY_RECORD,
|
||||||
OPERATION_TYPE.MODIFY_RECORDS,
|
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,
|
UNDO_OPERATION_TYPE,
|
||||||
LOCAL_APPLY_OPERATION_TYPE,
|
LOCAL_APPLY_OPERATION_TYPE,
|
||||||
NEED_APPLY_AFTER_SERVER_OPERATION,
|
NEED_APPLY_AFTER_SERVER_OPERATION,
|
||||||
|
VIEW_OPERATION,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@@ -78,7 +78,42 @@ class ServerOperator {
|
|||||||
});
|
});
|
||||||
break;
|
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: {
|
default: {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@@ -39,6 +39,7 @@ const propTypes = {
|
|||||||
isFileLoading: PropTypes.bool.isRequired,
|
isFileLoading: PropTypes.bool.isRequired,
|
||||||
filePermission: PropTypes.string,
|
filePermission: PropTypes.string,
|
||||||
content: PropTypes.string,
|
content: PropTypes.string,
|
||||||
|
metadataViewId: PropTypes.string,
|
||||||
lastModified: PropTypes.string,
|
lastModified: PropTypes.string,
|
||||||
latestContributor: PropTypes.string,
|
latestContributor: PropTypes.string,
|
||||||
onLinkClick: PropTypes.func.isRequired,
|
onLinkClick: PropTypes.func.isRequired,
|
||||||
@@ -220,6 +221,7 @@ class LibContentContainer extends React.Component {
|
|||||||
filePermission={this.props.filePermission}
|
filePermission={this.props.filePermission}
|
||||||
onFileTagChanged={this.props.onToolbarFileTagChanged}
|
onFileTagChanged={this.props.onToolbarFileTagChanged}
|
||||||
repoTags={this.props.repoTags}
|
repoTags={this.props.repoTags}
|
||||||
|
metadataViewId={this.props.metadataViewId}
|
||||||
/>
|
/>
|
||||||
<ToolbarForSelectedDirents
|
<ToolbarForSelectedDirents
|
||||||
repoID={this.props.repoID}
|
repoID={this.props.repoID}
|
||||||
@@ -275,6 +277,7 @@ class LibContentContainer extends React.Component {
|
|||||||
hash={this.props.hash}
|
hash={this.props.hash}
|
||||||
filePermission={this.props.filePermission}
|
filePermission={this.props.filePermission}
|
||||||
content={this.props.content}
|
content={this.props.content}
|
||||||
|
metadataViewId={this.props.metadataViewId}
|
||||||
lastModified={this.props.lastModified}
|
lastModified={this.props.lastModified}
|
||||||
latestContributor={this.props.latestContributor}
|
latestContributor={this.props.latestContributor}
|
||||||
onLinkClick={this.props.onLinkClick}
|
onLinkClick={this.props.onLinkClick}
|
||||||
|
@@ -85,6 +85,7 @@ class LibContentView extends React.Component {
|
|||||||
asyncOperationType: 'move',
|
asyncOperationType: 'move',
|
||||||
asyncOperationProgress: 0,
|
asyncOperationProgress: 0,
|
||||||
asyncOperatedFilesLength: 0,
|
asyncOperatedFilesLength: 0,
|
||||||
|
metadataViewId: '0000',
|
||||||
};
|
};
|
||||||
|
|
||||||
this.oldonpopstate = window.onpopstate;
|
this.oldonpopstate = window.onpopstate;
|
||||||
@@ -490,9 +491,10 @@ class LibContentView extends React.Component {
|
|||||||
window.history.pushState({ url: url, path: filePath }, filePath, url);
|
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;
|
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 repoInfo = this.state.currentRepoInfo;
|
||||||
const url = siteRoot + 'library/' + repoID + '/' + encodeURIComponent(repoInfo.repo_name);
|
const url = siteRoot + 'library/' + repoID + '/' + encodeURIComponent(repoInfo.repo_name);
|
||||||
window.history.pushState({ url: url, path: '' }, '', url);
|
window.history.pushState({ url: url, path: '' }, '', url);
|
||||||
@@ -1718,7 +1720,7 @@ class LibContentView extends React.Component {
|
|||||||
}
|
}
|
||||||
} else if (Utils.isFileMetadata(node?.object?.type)) {
|
} else if (Utils.isFileMetadata(node?.object?.type)) {
|
||||||
if (node.path !== this.state.path) {
|
if (node.path !== this.state.path) {
|
||||||
this.showFileMetadata(node.path);
|
this.showFileMetadata(node.path, node.view_id || '0000');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let url = siteRoot + 'lib/' + repoID + '/file' + Utils.encodePath(node.path);
|
let url = siteRoot + 'lib/' + repoID + '/file' + Utils.encodePath(node.path);
|
||||||
@@ -2084,6 +2086,7 @@ class LibContentView extends React.Component {
|
|||||||
isFileLoadedErr={this.state.isFileLoadedErr}
|
isFileLoadedErr={this.state.isFileLoadedErr}
|
||||||
filePermission={this.state.filePermission}
|
filePermission={this.state.filePermission}
|
||||||
content={this.state.content}
|
content={this.state.content}
|
||||||
|
metadataViewId={this.state.metadataViewId}
|
||||||
lastModified={this.state.lastModified}
|
lastModified={this.state.lastModified}
|
||||||
latestContributor={this.state.latestContributor}
|
latestContributor={this.state.latestContributor}
|
||||||
onLinkClick={this.onLinkClick}
|
onLinkClick={this.onLinkClick}
|
||||||
|
@@ -7,7 +7,7 @@ from rest_framework.views import APIView
|
|||||||
from seahub.api2.utils import api_error, to_python_boolean
|
from seahub.api2.utils import api_error, to_python_boolean
|
||||||
from seahub.api2.throttling import UserRateThrottle
|
from seahub.api2.throttling import UserRateThrottle
|
||||||
from seahub.api2.authentication import TokenAuthentication
|
from seahub.api2.authentication import TokenAuthentication
|
||||||
from seahub.repo_metadata.models import RepoMetadata
|
from seahub.repo_metadata.models import RepoMetadata, RepoMetadataViews
|
||||||
from seahub.views import check_folder_permission
|
from seahub.views import check_folder_permission
|
||||||
from seahub.repo_metadata.utils import add_init_metadata_task, gen_unique_id
|
from seahub.repo_metadata.utils import add_init_metadata_task, gen_unique_id
|
||||||
from seahub.repo_metadata.metadata_server_api import MetadataServerAPI, list_metadata_records
|
from seahub.repo_metadata.metadata_server_api import MetadataServerAPI, list_metadata_records
|
||||||
@@ -68,7 +68,7 @@ class MetadataManage(APIView):
|
|||||||
error_msg = f'The metadata module is enabled for repo {repo_id}.'
|
error_msg = f'The metadata module is enabled for repo {repo_id}.'
|
||||||
return api_error(status.HTTP_409_CONFLICT, error_msg)
|
return api_error(status.HTTP_409_CONFLICT, error_msg)
|
||||||
|
|
||||||
# recource check
|
# resource check
|
||||||
repo = seafile_api.get_repo(repo_id)
|
repo = seafile_api.get_repo(repo_id)
|
||||||
if not repo:
|
if not repo:
|
||||||
error_msg = 'Library %s not found.' % repo_id
|
error_msg = 'Library %s not found.' % repo_id
|
||||||
@@ -87,6 +87,9 @@ class MetadataManage(APIView):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
task_id = add_init_metadata_task(params=params)
|
task_id = add_init_metadata_task(params=params)
|
||||||
|
metadata_view = RepoMetadataViews.objects.filter(repo_id=repo_id).first()
|
||||||
|
if not metadata_view:
|
||||||
|
RepoMetadataViews.objects.add_view(repo_id, 'All files')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
|
||||||
@@ -131,6 +134,7 @@ class MetadataManage(APIView):
|
|||||||
try:
|
try:
|
||||||
record.enabled = False
|
record.enabled = False
|
||||||
record.save()
|
record.save()
|
||||||
|
RepoMetadataViews.objects.filter(repo_id=repo_id).delete()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
error_msg = 'Internal Server Error'
|
error_msg = 'Internal Server Error'
|
||||||
@@ -443,3 +447,243 @@ class MetadataColumns(APIView):
|
|||||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
return Response({'column': column})
|
return Response({'column': column})
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataViews(APIView):
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def get(self, request, repo_id):
|
||||||
|
record = RepoMetadata.objects.filter(repo_id=repo_id).first()
|
||||||
|
if not record or not record.enabled:
|
||||||
|
error_msg = f'The metadata module is disabled for repo {repo_id}.'
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
repo = seafile_api.get_repo(repo_id)
|
||||||
|
if not repo:
|
||||||
|
error_msg = 'Library %s not found.' % repo_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
permission = check_folder_permission(request, repo_id, '/')
|
||||||
|
if permission != 'rw':
|
||||||
|
error_msg = 'Permission denied.'
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
metadata_views = RepoMetadataViews.objects.list_views(repo_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
return Response(metadata_views)
|
||||||
|
|
||||||
|
def post(self, request, repo_id):
|
||||||
|
# Add a metadata view
|
||||||
|
view_name = request.data.get('name')
|
||||||
|
if not view_name:
|
||||||
|
error_msg = 'view name is invalid.'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
record = RepoMetadata.objects.filter(repo_id=repo_id).first()
|
||||||
|
if not record or not record.enabled:
|
||||||
|
error_msg = f'The metadata module is disabled for repo {repo_id}.'
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
repo = seafile_api.get_repo(repo_id)
|
||||||
|
if not repo:
|
||||||
|
error_msg = 'Library %s not found.' % repo_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
permission = check_folder_permission(request, repo_id, '/')
|
||||||
|
if permission != 'rw':
|
||||||
|
error_msg = 'Permission denied.'
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
new_view = RepoMetadataViews.objects.add_view(repo_id, view_name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
return Response({'view': new_view})
|
||||||
|
|
||||||
|
|
||||||
|
def put(self, request, repo_id):
|
||||||
|
# Update a metadata view, including rename, change filters and so on
|
||||||
|
# by a json data
|
||||||
|
view_id = request.data.get('view_id', None)
|
||||||
|
view_data = request.data.get('view_data', None)
|
||||||
|
if not view_id:
|
||||||
|
error_msg = 'view_id is invalid.'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
if not view_data:
|
||||||
|
error_msg = 'view_data is invalid.'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
record = RepoMetadata.objects.filter(repo_id=repo_id).first()
|
||||||
|
if not record or not record.enabled:
|
||||||
|
error_msg = f'The metadata module is disabled for repo {repo_id}.'
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
views = RepoMetadataViews.objects.filter(
|
||||||
|
repo_id = repo_id,
|
||||||
|
).first()
|
||||||
|
if not views:
|
||||||
|
error_msg = 'The metadata views does not exists.'
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
if view_id not in views.view_ids:
|
||||||
|
error_msg = 'view_id %s does not exists.' % view_id
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
repo = seafile_api.get_repo(repo_id)
|
||||||
|
if not repo:
|
||||||
|
error_msg = 'Library %s not found.' % repo_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
permission = check_folder_permission(request, repo_id, '/')
|
||||||
|
if permission != 'rw':
|
||||||
|
error_msg = 'Permission denied.'
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = RepoMetadataViews.objects.update_view(repo_id, view_id, view_data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
return Response({'success': True})
|
||||||
|
|
||||||
|
def delete(self, request, repo_id):
|
||||||
|
# Update a metadata view, including rename, change filters and so on
|
||||||
|
# by a json data
|
||||||
|
view_id = request.data.get('view_id', None)
|
||||||
|
if not view_id:
|
||||||
|
error_msg = 'view_id is invalid.'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
record = RepoMetadata.objects.filter(repo_id=repo_id).first()
|
||||||
|
if not record or not record.enabled:
|
||||||
|
error_msg = f'The metadata module is disabled for repo {repo_id}.'
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
views = RepoMetadataViews.objects.filter(
|
||||||
|
repo_id=repo_id
|
||||||
|
).first()
|
||||||
|
if not views:
|
||||||
|
error_msg = 'The metadata views does not exists.'
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
if view_id not in views.view_ids:
|
||||||
|
error_msg = 'view_id %s does not exists.' % view_id
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
repo = seafile_api.get_repo(repo_id)
|
||||||
|
if not repo:
|
||||||
|
error_msg = 'Library %s not found.' % repo_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
permission = check_folder_permission(request, repo_id, '/')
|
||||||
|
if permission != 'rw':
|
||||||
|
error_msg = 'Permission denied.'
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = RepoMetadataViews.objects.delete_view(repo_id, view_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
return Response({'success': True})
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataViewsDetailView(APIView):
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def get(self, request, repo_id, view_id):
|
||||||
|
record = RepoMetadata.objects.filter(repo_id=repo_id).first()
|
||||||
|
if not record or not record.enabled:
|
||||||
|
error_msg = f'The metadata module is disabled for repo {repo_id}.'
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
repo = seafile_api.get_repo(repo_id)
|
||||||
|
if not repo:
|
||||||
|
error_msg = 'Library %s not found.' % repo_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
permission = check_folder_permission(request, repo_id, '/')
|
||||||
|
if permission != 'rw':
|
||||||
|
error_msg = 'Permission denied.'
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
view = RepoMetadataViews.objects.get_view(repo_id, view_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
return Response({'view': view})
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataViewsMoveView(APIView):
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def post(self, request, repo_id):
|
||||||
|
# put view_id in front of target_view_id
|
||||||
|
view_id = request.data.get('view_id')
|
||||||
|
if not view_id:
|
||||||
|
error_msg = 'view_id is invalid.'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
target_view_id = request.data.get('target_view_id')
|
||||||
|
if not target_view_id:
|
||||||
|
error_msg = 'target_view_id is invalid.'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
record = RepoMetadata.objects.filter(repo_id=repo_id).first()
|
||||||
|
if not record or not record.enabled:
|
||||||
|
error_msg = f'The metadata module is disabled for repo {repo_id}.'
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
views = RepoMetadataViews.objects.filter(
|
||||||
|
repo_id=repo_id,
|
||||||
|
).first()
|
||||||
|
if not views:
|
||||||
|
error_msg = 'The metadata views does not exists.'
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
if view_id not in views.view_ids:
|
||||||
|
error_msg = 'view_id %s does not exists.' % view_id
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
if target_view_id not in views.view_ids:
|
||||||
|
error_msg = 'target_view_id %s does not exists.' % target_view_id
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
repo = seafile_api.get_repo(repo_id)
|
||||||
|
if not repo:
|
||||||
|
error_msg = 'Library %s not found.' % repo_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
permission = check_folder_permission(request, repo_id, '/')
|
||||||
|
if permission != 'rw':
|
||||||
|
error_msg = 'Permission denied.'
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
results = RepoMetadataViews.objects.move_view(repo_id, view_id, target_view_id)
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
return Response({'navigation': results['navigation']})
|
||||||
|
@@ -1,8 +1,32 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import string
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
from seahub.utils import get_no_duplicate_obj_name
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_random_string_lower_digits(length):
|
||||||
|
letters_and_digits = string.ascii_lowercase + string.digits
|
||||||
|
random_string = ''.join(random.choice(letters_and_digits) for i in range(length))
|
||||||
|
return random_string
|
||||||
|
|
||||||
|
|
||||||
|
def generate_view_id(length, view_ids=None):
|
||||||
|
if not view_ids:
|
||||||
|
return generate_random_string_lower_digits(length)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
new_id = generate_random_string_lower_digits(length)
|
||||||
|
if new_id not in view_ids:
|
||||||
|
break
|
||||||
|
|
||||||
|
return new_id
|
||||||
|
|
||||||
class RepoMetadata(models.Model):
|
class RepoMetadata(models.Model):
|
||||||
|
|
||||||
repo_id = models.CharField(max_length=36, unique=True)
|
repo_id = models.CharField(max_length=36, unique=True)
|
||||||
@@ -12,3 +36,143 @@ class RepoMetadata(models.Model):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'repo_metadata'
|
db_table = 'repo_metadata'
|
||||||
|
|
||||||
|
|
||||||
|
class RepoView(object):
|
||||||
|
|
||||||
|
def __init__(self, name, view_ids=None):
|
||||||
|
self.name = name
|
||||||
|
self.view_json = {}
|
||||||
|
|
||||||
|
self.init_view(view_ids)
|
||||||
|
|
||||||
|
def init_view(self, view_ids=None):
|
||||||
|
self.view_json = {
|
||||||
|
"_id": generate_view_id(4, view_ids),
|
||||||
|
"table_id": '0001', # by default
|
||||||
|
"name": self.name,
|
||||||
|
"filters": [],
|
||||||
|
"sorts": [],
|
||||||
|
"groupbys": [],
|
||||||
|
"filter_conjunction": "And",
|
||||||
|
"hidden_columns": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RepoMetadataViewsManager(models.Manager):
|
||||||
|
|
||||||
|
def add_view(self, repo_id, view_name):
|
||||||
|
metadata_views = self.filter(repo_id=repo_id).first()
|
||||||
|
if not metadata_views:
|
||||||
|
new_view = RepoView(view_name)
|
||||||
|
view_json = new_view.view_json
|
||||||
|
view_id = view_json.get('_id')
|
||||||
|
view_details = {
|
||||||
|
'views': [view_json],
|
||||||
|
'navigation': [{'_id': view_id, 'type': 'view'}, ]
|
||||||
|
}
|
||||||
|
self.create(
|
||||||
|
repo_id=repo_id,
|
||||||
|
details=json.dumps(view_details)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
view_details = json.loads(metadata_views.details)
|
||||||
|
view_name = get_no_duplicate_obj_name(view_name, metadata_views.view_names)
|
||||||
|
exist_view_ids = metadata_views.view_ids
|
||||||
|
new_view = RepoView(view_name, exist_view_ids)
|
||||||
|
view_json = new_view.view_json
|
||||||
|
view_id = view_json.get('_id')
|
||||||
|
view_details['views'].append(view_json)
|
||||||
|
view_details['navigation'].append({'_id': view_id, 'type': 'view'})
|
||||||
|
metadata_views.details = json.dumps(view_details)
|
||||||
|
metadata_views.save()
|
||||||
|
return new_view.view_json
|
||||||
|
|
||||||
|
def list_views(self, repo_id):
|
||||||
|
metadata_views = self.filter(repo_id=repo_id).first()
|
||||||
|
if not metadata_views:
|
||||||
|
return {'views': [], 'navigation': []}
|
||||||
|
return json.loads(metadata_views.details)
|
||||||
|
|
||||||
|
def get_view(self, repo_id, view_id):
|
||||||
|
metadata_views = self.filter(repo_id=repo_id).first()
|
||||||
|
if not metadata_views:
|
||||||
|
return None
|
||||||
|
view_details = json.loads(metadata_views.details)
|
||||||
|
for v in view_details['views']:
|
||||||
|
if v.get('_id') == view_id:
|
||||||
|
return v
|
||||||
|
|
||||||
|
def update_view(self, repo_id, view_id, view_dict):
|
||||||
|
metadata_views = self.filter(repo_id=repo_id).first()
|
||||||
|
view_dict.pop('_id', '')
|
||||||
|
if 'name' in view_dict:
|
||||||
|
exist_obj_names = metadata_views.view_names
|
||||||
|
view_dict['name'] = get_no_duplicate_obj_name(view_dict['name'], exist_obj_names)
|
||||||
|
view_details = json.loads(metadata_views.details)
|
||||||
|
for v in view_details['views']:
|
||||||
|
if v.get('_id') == view_id:
|
||||||
|
v.update(view_dict)
|
||||||
|
break
|
||||||
|
metadata_views.details = json.dumps(view_details)
|
||||||
|
metadata_views.save()
|
||||||
|
return json.loads(metadata_views.details)
|
||||||
|
|
||||||
|
def delete_view(self, repo_id, view_id):
|
||||||
|
metadata_views = self.filter(repo_id=repo_id).first()
|
||||||
|
view_details = json.loads(metadata_views.details)
|
||||||
|
for v in view_details['views']:
|
||||||
|
if v.get('_id') == view_id:
|
||||||
|
view_details['views'].remove(v)
|
||||||
|
break
|
||||||
|
for v in view_details['navigation']:
|
||||||
|
if v.get('_id') == view_id:
|
||||||
|
view_details['navigation'].remove(v)
|
||||||
|
break
|
||||||
|
metadata_views.details = json.dumps(view_details)
|
||||||
|
metadata_views.save()
|
||||||
|
return json.loads(metadata_views.details)
|
||||||
|
|
||||||
|
def move_view(self, repo_id, view_id, target_view_id):
|
||||||
|
metadata_views = self.filter(repo_id=repo_id).first()
|
||||||
|
view_details = json.loads(metadata_views.details)
|
||||||
|
view_index = None
|
||||||
|
target_index = None
|
||||||
|
for i, view in enumerate(view_details['navigation']):
|
||||||
|
if view['_id'] == view_id:
|
||||||
|
view_index = i
|
||||||
|
if view['_id'] == target_view_id:
|
||||||
|
target_index = i
|
||||||
|
|
||||||
|
if view_index is not None and target_index is not None:
|
||||||
|
if view_index < target_index:
|
||||||
|
view_to_move = view_details['navigation'][view_index]
|
||||||
|
view_details['navigation'].insert(target_index, view_to_move)
|
||||||
|
view_details['navigation'].pop(view_index)
|
||||||
|
else:
|
||||||
|
view_to_move = view_details['navigation'].pop(view_index)
|
||||||
|
view_details['navigation'].insert(target_index, view_to_move)
|
||||||
|
|
||||||
|
metadata_views.details = json.dumps(view_details)
|
||||||
|
metadata_views.save()
|
||||||
|
return json.loads(metadata_views.details)
|
||||||
|
|
||||||
|
|
||||||
|
class RepoMetadataViews(models.Model):
|
||||||
|
repo_id = models.CharField(max_length=36, db_index=True)
|
||||||
|
details = models.TextField()
|
||||||
|
|
||||||
|
objects = RepoMetadataViewsManager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'repo_metadata_view'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def view_ids(self):
|
||||||
|
views = json.loads(self.details)['views']
|
||||||
|
return [v.get('_id') for v in views]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def view_names(self):
|
||||||
|
views = json.loads(self.details)['views']
|
||||||
|
return [v.get('name') for v in views]
|
||||||
|
@@ -207,7 +207,8 @@ from seahub.ai.apis import LibrarySdocIndexes, Search, LibrarySdocIndex, TaskSta
|
|||||||
from seahub.wiki2.views import wiki_view
|
from seahub.wiki2.views import wiki_view
|
||||||
from seahub.api2.endpoints.wiki2 import Wikis2View, Wiki2View, Wiki2ConfigView, Wiki2PagesView, Wiki2PageView, Wiki2DuplicatePageView
|
from seahub.api2.endpoints.wiki2 import Wikis2View, Wiki2View, Wiki2ConfigView, Wiki2PagesView, Wiki2PageView, Wiki2DuplicatePageView
|
||||||
from seahub.api2.endpoints.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView
|
from seahub.api2.endpoints.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView
|
||||||
from seahub.api2.endpoints.metadata_manage import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecordInfo
|
from seahub.api2.endpoints.metadata_manage import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecordInfo, \
|
||||||
|
MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView
|
||||||
from seahub.api2.endpoints.user_list import UserListView
|
from seahub.api2.endpoints.user_list import UserListView
|
||||||
|
|
||||||
|
|
||||||
@@ -1037,5 +1038,8 @@ if settings.ENABLE_METADATA_MANAGEMENT:
|
|||||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/records/$', MetadataRecords.as_view(), name='api-v2.1-metadata-records'),
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/records/$', MetadataRecords.as_view(), name='api-v2.1-metadata-records'),
|
||||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/record/$', MetadataRecordInfo.as_view(), name='api-v2.1-metadata-record-info'),
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/record/$', MetadataRecordInfo.as_view(), name='api-v2.1-metadata-record-info'),
|
||||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/columns/$', MetadataColumns.as_view(), name='api-v2.1-metadata-columns'),
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/columns/$', MetadataColumns.as_view(), name='api-v2.1-metadata-columns'),
|
||||||
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/views/$', MetadataViews.as_view(), name='api-v2.1-metadata-views'),
|
||||||
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/views/(?P<view_id>[-0-9a-zA-Z]{4})/$', MetadataViewsDetailView.as_view(), name='api-v2.1-metadata-views-detail'),
|
||||||
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/move-views/$', MetadataViewsMoveView.as_view(), name='api-v2.1-metadata-views-move'),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
Reference in New Issue
Block a user