mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-02 07:27:04 +00:00
repo share admin (#5596)
* repo share admin * check share_to param * [repo 'share admin' dialog] removed 'internal links'; fixup & improvements --------- Co-authored-by: llj <lingjun.li1@gmail.com>
This commit is contained in:
130
frontend/src/components/dialog/repo-share-admin-dialog.js
Normal file
130
frontend/src/components/dialog/repo-share-admin-dialog.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, TabContent, TabPane, Nav, NavItem, NavLink } from 'reactstrap';
|
||||
import { gettext, canGenerateShareLink, canGenerateUploadLink } from '../../utils/constants';
|
||||
import RepoShareAdminShareLinks from './repo-share-admin/share-links';
|
||||
import RepoShareAdminUploadLinks from './repo-share-admin/upload-links';
|
||||
import RepoShareAdminUserShares from './repo-share-admin/user-shares';
|
||||
import RepoShareAdminGroupShares from './repo-share-admin/group-shares';
|
||||
|
||||
const propTypes = {
|
||||
repo: PropTypes.object.isRequired,
|
||||
toggleDialog: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class RepoShareAdminDialog extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.enableShareLink = !this.props.repo.encrypted && canGenerateShareLink;
|
||||
this.enableUploadLink = !this.props.repo.encrypted && canGenerateUploadLink;
|
||||
this.state = {
|
||||
activeTab: this.getInitialActiveTab()
|
||||
};
|
||||
}
|
||||
|
||||
getInitialActiveTab = () => {
|
||||
if (this.enableShareLink) {
|
||||
return 'shareLink';
|
||||
} else if (this.enableUploadLink) {
|
||||
return 'uploadLink';
|
||||
} else {
|
||||
return 'shareToUser';
|
||||
}
|
||||
}
|
||||
|
||||
toggle = (tab) => {
|
||||
if (this.state.activeTab !== tab) {
|
||||
this.setState({ activeTab: tab });
|
||||
}
|
||||
}
|
||||
|
||||
onTabKeyDown = (e) => {
|
||||
if (e.key == 'Enter' || e.key == 'Space') {
|
||||
e.target.click();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { activeTab } = this.state;
|
||||
const { repoName } = this.props.repo;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal isOpen={true} style={{maxWidth: '760px'}} className="share-dialog" toggle={this.props.toggleDialog}>
|
||||
<ModalHeader toggle={this.props.toggleDialog}>
|
||||
<span className="op-target" title={repoName}>{repoName}</span> {gettext('Share Admin')}
|
||||
</ModalHeader>
|
||||
<ModalBody className="dialog-list-container share-dialog-content" role="tablist">
|
||||
<Fragment>
|
||||
<div className="share-dialog-side">
|
||||
<Nav pills>
|
||||
{this.enableShareLink &&
|
||||
<NavItem role="tab" aria-selected={activeTab === 'shareLink'} aria-controls="share-link-panel">
|
||||
<NavLink className={activeTab === 'shareLink' ? 'active' : ''} onClick={(this.toggle.bind(this, 'shareLink'))} tabIndex="0" onKeyDown={this.onTabKeyDown}>
|
||||
{gettext('Share Links')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
}
|
||||
{this.enableUploadLink &&
|
||||
<NavItem role="tab" aria-selected={activeTab === 'uploadLink'} aria-controls="upload-link-panel">
|
||||
<NavLink className={activeTab === 'uploadLink' ? 'active' : ''} onClick={this.toggle.bind(this, 'uploadLink')} tabIndex="0" onKeyDown={this.onTabKeyDown}>
|
||||
{gettext('Upload Links')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
}
|
||||
<NavItem role="tab" aria-selected={activeTab === 'shareToUser'} aria-controls="share-to-user-panel">
|
||||
<NavLink className={activeTab === 'shareToUser' ? 'active' : ''} onClick={this.toggle.bind(this, 'shareToUser')} tabIndex="0" onKeyDown={this.onTabKeyDown}>
|
||||
{gettext('User Shares')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
<NavItem role="tab" aria-selected={activeTab === 'shareToGroup'} aria-controls="share-to-group-panel">
|
||||
<NavLink className={activeTab === 'shareToGroup' ? 'active' : ''} onClick={this.toggle.bind(this, 'shareToGroup')} tabIndex="0" onKeyDown={this.onTabKeyDown}>
|
||||
{gettext('Group Shares')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
</Nav>
|
||||
</div>
|
||||
<div className="share-dialog-main">
|
||||
<TabContent activeTab={this.state.activeTab}>
|
||||
{(this.enableShareLink && activeTab === 'shareLink') &&
|
||||
<TabPane tabId="shareLink" role="tabpanel" id="share-link-panel">
|
||||
<RepoShareAdminShareLinks
|
||||
repo={this.props.repo}
|
||||
/>
|
||||
</TabPane>
|
||||
}
|
||||
{(this.enableUploadLink && activeTab === 'uploadLink') &&
|
||||
<TabPane tabId="uploadLink" role="tabpanel" id="upload-link-panel">
|
||||
<RepoShareAdminUploadLinks
|
||||
repo={this.props.repo}
|
||||
/>
|
||||
</TabPane>
|
||||
}
|
||||
{activeTab === 'shareToUser' &&
|
||||
<TabPane tabId="shareToUser" role="tabpanel" id="share-to-user-panel">
|
||||
<RepoShareAdminUserShares
|
||||
repo={this.props.repo}
|
||||
/>
|
||||
</TabPane>
|
||||
}
|
||||
{activeTab === 'shareToGroup' &&
|
||||
<TabPane tabId="shareToGroup" role="tabpanel" id="share-to-group-panel">
|
||||
<RepoShareAdminGroupShares
|
||||
repo={this.props.repo}
|
||||
/>
|
||||
</TabPane>
|
||||
}
|
||||
</TabContent>
|
||||
</div>
|
||||
</Fragment>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RepoShareAdminDialog.propTypes = propTypes;
|
||||
|
||||
export default RepoShareAdminDialog;
|
213
frontend/src/components/dialog/repo-share-admin/group-shares.js
Normal file
213
frontend/src/components/dialog/repo-share-admin/group-shares.js
Normal file
@@ -0,0 +1,213 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from '@gatsbyjs/reach-router';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import { gettext, siteRoot, isPro, username } from '../../../utils/constants';
|
||||
import Loading from '../../../components/loading';
|
||||
import toaster from '../../../components/toast';
|
||||
import EmptyTip from '../../../components/empty-tip';
|
||||
import SharePermissionEditor from '../../../components/select-editor/share-permission-editor';
|
||||
|
||||
const itemPropTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
deleteItem: PropTypes.func.isRequired,
|
||||
isRepoOwner: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
class Item extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
permission: this.props.item.permission,
|
||||
isOperationShow: false,
|
||||
isShowPermEditor: false,
|
||||
};
|
||||
this.permissions = ['rw', 'r'];
|
||||
if (isPro) {
|
||||
if (this.props.item.path === '/' && this.props.isRepoOwner) {
|
||||
this.permissions.push('admin');
|
||||
}
|
||||
this.permissions.push('cloud-edit', 'preview');
|
||||
}
|
||||
}
|
||||
|
||||
onMouseEnter = () => {
|
||||
this.setState({isOperationShow: true});
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
this.setState({isOperationShow: false});
|
||||
};
|
||||
|
||||
onDeleteLink = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.deleteItem(this.props.item);
|
||||
};
|
||||
|
||||
changePerm = (permission) => {
|
||||
const item = this.props.item;
|
||||
seafileAPI.updateShareToGroupItemPermission(item.repo_id, item.path, 'group', item.share_to, permission).then(() => {
|
||||
this.setState({
|
||||
permission: permission,
|
||||
});
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
|
||||
onEditPermission = (event) => {
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
this.setState({isShowPermEditor: true});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let objUrl;
|
||||
let item = this.props.item;
|
||||
let path = item.path === '/' ? '/' : item.path.slice(0, item.path.length - 1);
|
||||
|
||||
objUrl = `${siteRoot}library/${item.repo_id}/${encodeURIComponent(item.repo_name)}${Utils.encodePath(path)}`;
|
||||
|
||||
return (
|
||||
<tr onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} onFocus={this.onMouseEnter}>
|
||||
<td>
|
||||
<Link to={objUrl}>{Utils.getFolderName(item.path)}</Link>
|
||||
</td>
|
||||
<td className="name">{item.share_to_name}</td>
|
||||
<td>
|
||||
{!this.state.isShowPermEditor && (
|
||||
<div>
|
||||
<span>{item.permission_name || Utils.sharePerms(this.state.permission)}</span>
|
||||
{this.state.isOperationShow && (
|
||||
<a href="#"
|
||||
role="button"
|
||||
aria-label={gettext('Edit')}
|
||||
title={gettext('Edit')}
|
||||
className="fa fa-pencil-alt attr-action-icon"
|
||||
onClick={this.onEditPermission}>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{this.state.isShowPermEditor && (
|
||||
<SharePermissionEditor
|
||||
repoID={item.repo_id}
|
||||
isTextMode={true}
|
||||
isEditIconShow={this.state.isOperationShow}
|
||||
isEditing={true}
|
||||
currentPermission={this.state.permission}
|
||||
permissions={this.permissions}
|
||||
onPermissionChanged={this.changePerm}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
className={`sf2-icon-x3 action-icon ${this.state.isOperationShow ? '' : 'invisible'}`}
|
||||
onClick={this.onDeleteLink}
|
||||
onKeyDown={Utils.onKeyDown}
|
||||
title={gettext('Delete')}
|
||||
aria-label={gettext('Delete')}
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Item.propTypes = itemPropTypes;
|
||||
|
||||
const propTypes = {
|
||||
repo: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
class RepoShareAdminGroupShares extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
errorMsg: '',
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
seafileAPI.getAllRepoFolderShareInfo(this.props.repo.repo_id, 'group').then((res) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
items: res.data.share_info_list,
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
deleteItem = (item) => {
|
||||
seafileAPI.deleteShareToGroupItem(item.repo_id, item.path, 'group', item.share_to).then(res => {
|
||||
let items = this.state.items.filter(shareItem => {
|
||||
return shareItem.path + shareItem.share_to !== item.path + item.share_to;
|
||||
});
|
||||
this.setState({items: items});
|
||||
let message = gettext('Successfully deleted 1 item');
|
||||
toaster.success(message);
|
||||
}).catch((error) => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, errorMsg, items } = this.state;
|
||||
const { repo } = this.props;
|
||||
const isRepoOwner = repo.owner_email === username;
|
||||
return (
|
||||
<Fragment>
|
||||
{loading && <Loading />}
|
||||
{!loading && errorMsg && <p className="error text-center mt-8">{errorMsg}</p>}
|
||||
{!loading && !errorMsg && !items.length &&
|
||||
<EmptyTip forDialog={true}>
|
||||
<p className="text-secondary">{gettext('No group shares')}</p>
|
||||
</EmptyTip>
|
||||
}
|
||||
{!loading && !errorMsg && items.length > 0 &&
|
||||
<table className="table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="30%">{gettext('Name')}</th>
|
||||
<th width="30%">{gettext('Group')}</th>
|
||||
<th width="30%">{gettext('Permission')}</th>
|
||||
<th width="10%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, index) => {
|
||||
return (
|
||||
<Item
|
||||
key={index}
|
||||
item={item}
|
||||
deleteItem={this.deleteItem}
|
||||
isRepoOwner={isRepoOwner}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RepoShareAdminGroupShares.propTypes = propTypes;
|
||||
|
||||
export default RepoShareAdminGroupShares;
|
162
frontend/src/components/dialog/repo-share-admin/share-links.js
Normal file
162
frontend/src/components/dialog/repo-share-admin/share-links.js
Normal file
@@ -0,0 +1,162 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from '@gatsbyjs/reach-router';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import { gettext, siteRoot } from '../../../utils/constants';
|
||||
import Loading from '../../../components/loading';
|
||||
import toaster from '../../../components/toast';
|
||||
import EmptyTip from '../../../components/empty-tip';
|
||||
|
||||
const itemPropTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
deleteItem: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class Item extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isOperationShow: false
|
||||
};
|
||||
}
|
||||
|
||||
onMouseEnter = () => {
|
||||
this.setState({isOperationShow: true});
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
this.setState({isOperationShow: false});
|
||||
};
|
||||
|
||||
onDeleteLink = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.deleteItem(this.props.item);
|
||||
};
|
||||
|
||||
render() {
|
||||
let objUrl;
|
||||
let item = this.props.item;
|
||||
let path = item.path === '/' ? '/' : item.path.slice(0, item.path.length - 1);
|
||||
|
||||
if (item.is_dir) {
|
||||
objUrl = `${siteRoot}library/${item.repo_id}/${encodeURIComponent(item.repo_name)}${Utils.encodePath(path)}`;
|
||||
} else {
|
||||
objUrl = `${siteRoot}lib/${item.repo_id}/file${Utils.encodePath(item.path)}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} onFocus={this.onMouseEnter}>
|
||||
<td className="name">{item.creator_name}</td>
|
||||
<td>
|
||||
{item.is_dir ? <Link to={objUrl}>{item.obj_name}</Link> :
|
||||
<a href={objUrl} target="_blank" rel="noreferrer">{item.obj_name}</a>}
|
||||
</td>
|
||||
<td>
|
||||
<a href={item.link} target="_blank" rel="noreferrer">{item.link}</a>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
className={`sf2-icon-x3 action-icon ${this.state.isOperationShow ? '' : 'invisible'}`}
|
||||
onClick={this.onDeleteLink}
|
||||
onKeyDown={Utils.onKeyDown}
|
||||
title={gettext('Delete')}
|
||||
aria-label={gettext('Delete')}
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Item.propTypes = itemPropTypes;
|
||||
|
||||
const propTypes = {
|
||||
repo: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
class RepoShareAdminShareLinks extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
errorMsg: '',
|
||||
items: []
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
seafileAPI.listRepoShareLinks(this.props.repo.repo_id).then((res) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
items: res.data,
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
deleteItem = (item) => {
|
||||
seafileAPI.deleteRepoShareLink(this.props.repo.repo_id, item.token).then(() => {
|
||||
let items = this.state.items.filter(linkItem => {
|
||||
return linkItem.token !== item.token;
|
||||
});
|
||||
this.setState({items: items});
|
||||
let message = gettext('Successfully deleted 1 item');
|
||||
toaster.success(message);
|
||||
}).catch((error) => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, errorMsg, items } = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
{loading && <Loading />}
|
||||
{!loading && errorMsg && <p className="error text-center mt-8">{errorMsg}</p>}
|
||||
{!loading && !errorMsg && !items.length &&
|
||||
<EmptyTip forDialog={true}>
|
||||
<p className="text-secondary">{gettext('No share links')}</p>
|
||||
</EmptyTip>
|
||||
}
|
||||
{!loading && !errorMsg && items.length > 0 &&
|
||||
<table className="table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="22%">{gettext('Creator')}</th>
|
||||
<th width="20%">{gettext('Name')}</th>
|
||||
<th width="50%">{gettext('Link')}</th>
|
||||
<th width="8%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, index) => {
|
||||
return (
|
||||
<Item
|
||||
key={index}
|
||||
item={item}
|
||||
deleteItem={this.deleteItem}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RepoShareAdminShareLinks.propTypes = propTypes;
|
||||
|
||||
export default RepoShareAdminShareLinks;
|
155
frontend/src/components/dialog/repo-share-admin/upload-links.js
Normal file
155
frontend/src/components/dialog/repo-share-admin/upload-links.js
Normal file
@@ -0,0 +1,155 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from '@gatsbyjs/reach-router';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import { gettext, siteRoot } from '../../../utils/constants';
|
||||
import Loading from '../../../components/loading';
|
||||
import toaster from '../../../components/toast';
|
||||
import EmptyTip from '../../../components/empty-tip';
|
||||
|
||||
const itemPropTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
deleteItem: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class Item extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isOperationShow: false
|
||||
};
|
||||
}
|
||||
|
||||
onMouseEnter = () => {
|
||||
this.setState({isOperationShow: true});
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
this.setState({isOperationShow: false});
|
||||
};
|
||||
|
||||
onDeleteLink = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.deleteItem(this.props.item);
|
||||
};
|
||||
|
||||
render() {
|
||||
let item = this.props.item;
|
||||
let path = item.path === '/' ? '/' : item.path.slice(0, item.path.length - 1);
|
||||
let objUrl = `${siteRoot}library/${item.repo_id}/${encodeURIComponent(item.repo_name)}${Utils.encodePath(path)}`;
|
||||
|
||||
return (
|
||||
<tr onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} onFocus={this.onMouseEnter}>
|
||||
<td className="name">{item.creator_name}</td>
|
||||
<td>
|
||||
<Link to={objUrl}>{item.obj_name}</Link>
|
||||
</td>
|
||||
<td>
|
||||
<a href={item.link} target="_blank" rel="noreferrer">{item.link}</a>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
className={`sf2-icon-x3 action-icon ${this.state.isOperationShow ? '' : 'invisible'}`}
|
||||
onClick={this.onDeleteLink}
|
||||
onKeyDown={Utils.onKeyDown}
|
||||
title={gettext('Delete')}
|
||||
aria-label={gettext('Delete')}
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Item.propTypes = itemPropTypes;
|
||||
|
||||
const propTypes = {
|
||||
repo: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
class RepoShareAdminUploadLinks extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
errorMsg: '',
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
seafileAPI.listRepoUploadLinks(this.props.repo.repo_id).then((res) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
items: res.data,
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
deleteItem = (item) => {
|
||||
seafileAPI.deleteRepoUploadLink(this.props.repo.repo_id, item.token).then(() => {
|
||||
let items = this.state.items.filter(linkItem => {
|
||||
return linkItem.token !== item.token;
|
||||
});
|
||||
this.setState({items: items});
|
||||
let message = gettext('Successfully deleted 1 item');
|
||||
toaster.success(message);
|
||||
}).catch((error) => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, errorMsg, items } = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
{loading && <Loading />}
|
||||
{!loading && errorMsg && <p className="error text-center mt-8">{errorMsg}</p>}
|
||||
{!loading && !errorMsg && !items.length &&
|
||||
<EmptyTip forDialog={true}>
|
||||
<p className="text-secondary">{gettext('No upload links')}</p>
|
||||
</EmptyTip>
|
||||
}
|
||||
{!loading && !errorMsg && items.length > 0 &&
|
||||
<table className="table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="22%">{gettext('Creator')}</th>
|
||||
<th width="20%">{gettext('Name')}</th>
|
||||
<th width="50%">{gettext('Link')}</th>
|
||||
<th width="8%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, index) => {
|
||||
return (
|
||||
<Item
|
||||
key={index}
|
||||
item={item}
|
||||
deleteItem={this.deleteItem}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RepoShareAdminUploadLinks.propTypes = propTypes;
|
||||
|
||||
export default RepoShareAdminUploadLinks;
|
213
frontend/src/components/dialog/repo-share-admin/user-shares.js
Normal file
213
frontend/src/components/dialog/repo-share-admin/user-shares.js
Normal file
@@ -0,0 +1,213 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from '@gatsbyjs/reach-router';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import { gettext, siteRoot, isPro, username } from '../../../utils/constants';
|
||||
import Loading from '../../../components/loading';
|
||||
import toaster from '../../../components/toast';
|
||||
import EmptyTip from '../../../components/empty-tip';
|
||||
import SharePermissionEditor from '../../../components/select-editor/share-permission-editor';
|
||||
|
||||
const itemPropTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
deleteItem: PropTypes.func.isRequired,
|
||||
isRepoOwner: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
class Item extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
permission: this.props.item.permission,
|
||||
isOperationShow: false,
|
||||
isShowPermEditor: false,
|
||||
};
|
||||
this.permissions = ['rw', 'r'];
|
||||
if (isPro) {
|
||||
if (this.props.item.path === '/' && this.props.isRepoOwner) {
|
||||
this.permissions.push('admin');
|
||||
}
|
||||
this.permissions.push('cloud-edit', 'preview');
|
||||
}
|
||||
}
|
||||
|
||||
onMouseEnter = () => {
|
||||
this.setState({isOperationShow: true});
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
this.setState({isOperationShow: false});
|
||||
};
|
||||
|
||||
onDeleteLink = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.deleteItem(this.props.item);
|
||||
};
|
||||
|
||||
changePerm = (permission) => {
|
||||
const item = this.props.item;
|
||||
seafileAPI.updateShareToUserItemPermission(item.repo_id, item.path, 'user', item.share_to, permission).then(() => {
|
||||
this.setState({
|
||||
permission: permission,
|
||||
});
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
|
||||
onEditPermission = (event) => {
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
this.setState({isShowPermEditor: true});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let objUrl;
|
||||
let item = this.props.item;
|
||||
let path = item.path === '/' ? '/' : item.path.slice(0, item.path.length - 1);
|
||||
|
||||
objUrl = `${siteRoot}library/${item.repo_id}/${encodeURIComponent(item.repo_name)}${Utils.encodePath(path)}`;
|
||||
|
||||
return (
|
||||
<tr onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} onFocus={this.onMouseEnter}>
|
||||
<td>
|
||||
<Link to={objUrl}>{Utils.getFolderName(item.path)}</Link>
|
||||
</td>
|
||||
<td className="name">{item.share_to_name}</td>
|
||||
<td>
|
||||
{!this.state.isShowPermEditor && (
|
||||
<div>
|
||||
<span>{item.permission_name || Utils.sharePerms(this.state.permission)}</span>
|
||||
{this.state.isOperationShow && (
|
||||
<a href="#"
|
||||
role="button"
|
||||
aria-label={gettext('Edit')}
|
||||
title={gettext('Edit')}
|
||||
className="fa fa-pencil-alt attr-action-icon"
|
||||
onClick={this.onEditPermission}>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{this.state.isShowPermEditor && (
|
||||
<SharePermissionEditor
|
||||
repoID={item.repo_id}
|
||||
isTextMode={true}
|
||||
isEditIconShow={this.state.isOperationShow}
|
||||
isEditing={true}
|
||||
currentPermission={this.state.permission}
|
||||
permissions={this.permissions}
|
||||
onPermissionChanged={this.changePerm}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
className={`sf2-icon-x3 action-icon ${this.state.isOperationShow ? '' : 'invisible'}`}
|
||||
onClick={this.onDeleteLink}
|
||||
onKeyDown={Utils.onKeyDown}
|
||||
title={gettext('Delete')}
|
||||
aria-label={gettext('Delete')}
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Item.propTypes = itemPropTypes;
|
||||
|
||||
const propTypes = {
|
||||
repo: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
class RepoShareAdminUserShares extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
errorMsg: '',
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
seafileAPI.getAllRepoFolderShareInfo(this.props.repo.repo_id, 'user').then((res) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
items: res.data.share_info_list,
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
deleteItem = (item) => {
|
||||
seafileAPI.deleteShareToUserItem(item.repo_id, item.path, 'user', item.share_to).then(res => {
|
||||
let items = this.state.items.filter(shareItem => {
|
||||
return shareItem.path + shareItem.share_to !== item.path + item.share_to;
|
||||
});
|
||||
this.setState({items: items});
|
||||
let message = gettext('Successfully deleted 1 item');
|
||||
toaster.success(message);
|
||||
}).catch((error) => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, errorMsg, items } = this.state;
|
||||
const { repo } = this.props;
|
||||
const isRepoOwner = repo.owner_email === username;
|
||||
return (
|
||||
<Fragment>
|
||||
{loading && <Loading />}
|
||||
{!loading && errorMsg && <p className="error text-center mt-8">{errorMsg}</p>}
|
||||
{!loading && !errorMsg && !items.length &&
|
||||
<EmptyTip forDialog={true}>
|
||||
<p className="text-secondary">{gettext('No user shares')}</p>
|
||||
</EmptyTip>
|
||||
}
|
||||
{!loading && !errorMsg && items.length > 0 &&
|
||||
<table className="table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="30%">{gettext('Name')}</th>
|
||||
<th width="30%">{gettext('User')}</th>
|
||||
<th width="30%">{gettext('Permission')}</th>
|
||||
<th width="10%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, index) => {
|
||||
return (
|
||||
<Item
|
||||
key={index}
|
||||
item={item}
|
||||
deleteItem={this.deleteItem}
|
||||
isRepoOwner={isRepoOwner}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RepoShareAdminUserShares.propTypes = propTypes;
|
||||
|
||||
export default RepoShareAdminUserShares;
|
@@ -1,220 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, NavLink } from 'reactstrap';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { siteRoot, gettext } from '../../utils/constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import toaster from '../toast';
|
||||
import Loading from '../loading';
|
||||
import EmptyTip from '../empty-tip';
|
||||
|
||||
const repoShareUploadLinkItemPropTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
deleteItem: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class RepoShareUploadLinkItem extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isOperationShow: false
|
||||
};
|
||||
}
|
||||
|
||||
onMouseEnter = () => {
|
||||
this.setState({isOperationShow: true});
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
this.setState({isOperationShow: false});
|
||||
};
|
||||
|
||||
onDeleteLink = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.deleteItem(this.props.item.token);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
let objUrl;
|
||||
let item = this.props.item;
|
||||
let path = item.path === '/' ? '/' : item.path.slice(0, item.path.length - 1);
|
||||
|
||||
if (this.props.activeTab === 'shareLinks') {
|
||||
if (item.is_dir) {
|
||||
objUrl = `${siteRoot}library/${item.repo_id}/${encodeURIComponent(item.repo_name)}${Utils.encodePath(path)}`;
|
||||
} else {
|
||||
objUrl = `${siteRoot}lib/${item.repo_id}/file${Utils.encodePath(item.path)}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.activeTab === 'uploadLinks') {
|
||||
objUrl = `${siteRoot}library/${item.repo_id}/${encodeURIComponent(item.repo_name)}${Utils.encodePath(path)}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} onFocus={this.onMouseEnter}>
|
||||
<td className="name">{item.creator_name}</td>
|
||||
<td>
|
||||
<a href={objUrl} target="_blank" className="text-inherit" rel="noreferrer">{item.obj_name}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href={item.link} target="_blank" className="text-inherit" rel="noreferrer">{item.link}</a>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
className={`sf2-icon-x3 action-icon ${this.state.isOperationShow ? '' : 'invisible'}`}
|
||||
onClick={this.onDeleteLink}
|
||||
onKeyDown={Utils.onKeyDown}
|
||||
title={gettext('Delete')}
|
||||
aria-label={gettext('Delete')}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RepoShareUploadLinkItem.propTypes = repoShareUploadLinkItemPropTypes;
|
||||
|
||||
const RepoShareUploadLinksDialogPropTypes = {
|
||||
repo: PropTypes.object.isRequired,
|
||||
toggleDialog: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class RepoShareUploadLinksDialog extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
activeTab: 'shareLinks',
|
||||
repoShareUploadLinkList: [],
|
||||
errorMsg: ''
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getItems('share-link');
|
||||
}
|
||||
|
||||
getItems = (itemType) => {
|
||||
const repoID = this.props.repo.repo_id;
|
||||
const request = itemType == 'share-link' ?
|
||||
seafileAPI.listRepoShareLinks(repoID) :
|
||||
seafileAPI.listRepoUploadLinks(repoID);
|
||||
request.then((res) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
repoShareUploadLinkList: res.data,
|
||||
});
|
||||
}).catch(error => {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
errorMsg: Utils.getErrorMsg(error, true)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
deleteItem = (token) => {
|
||||
const repoID = this.props.repo.repo_id;
|
||||
const request = this.state.activeTab == 'shareLinks' ?
|
||||
seafileAPI.deleteRepoShareLink(repoID, token) :
|
||||
seafileAPI.deleteRepoUploadLink(repoID, token);
|
||||
request.then((res) => {
|
||||
const repoShareUploadLinkList = this.state.repoShareUploadLinkList.filter(item => {
|
||||
return item.token !== token;
|
||||
});
|
||||
this.setState({
|
||||
repoShareUploadLinkList: repoShareUploadLinkList
|
||||
});
|
||||
}).catch(error => {
|
||||
toaster.danger(Utils.getErrorMsg(error));
|
||||
});
|
||||
};
|
||||
|
||||
toggle = (tab) => {
|
||||
if (this.state.activeTab !== tab) {
|
||||
this.setState({activeTab: tab});
|
||||
}
|
||||
if (tab == 'shareLinks') {
|
||||
this.getItems('share-link');
|
||||
}
|
||||
if (tab == 'uploadLinks') {
|
||||
this.getItems('upload-link');
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
const { loading, errorMsg, activeTab, repoShareUploadLinkList } = this.state;
|
||||
|
||||
const itemName = '<span class="op-target">' + Utils.HTMLescape(this.props.repo.repo_name) + '</span>';
|
||||
const title = gettext('{placeholder} Share/Upload Links').replace('{placeholder}', itemName);
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} style={{maxWidth: '800px'}}>
|
||||
<ModalHeader toggle={this.props.toggleDialog}>
|
||||
<span dangerouslySetInnerHTML={{__html: title}}></span>
|
||||
</ModalHeader>
|
||||
<ModalBody className="p-0 pb-4">
|
||||
<div className="main-panel-center">
|
||||
<div className="cur-view-container" role="tablist">
|
||||
<div className="cur-view-path share-upload-nav">
|
||||
<ul className="nav">
|
||||
<li className="nav-item" role="tab" aria-selected={activeTab === 'shareLinks'} aria-controls="share-links-panel">
|
||||
<NavLink className={activeTab === 'shareLinks' ? 'active' : ''} onClick={this.toggle.bind(this, 'shareLinks')} tabIndex="0" onKeyDown={Utils.onKeyDown}>{gettext('Share Links')}</NavLink>
|
||||
</li>
|
||||
<li className="nav-item" role="tab" aria-selected={activeTab === 'uploadLinks'} aria-controls="upload-links-panel">
|
||||
<NavLink className={activeTab === 'uploadLinks' ? 'active' : ''} onClick={this.toggle.bind(this, 'uploadLinks')} tabIndex="0" onKeyDown={Utils.onKeyDown}>{gettext('Upload Links')}</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="cur-view-content" style={{minHeight: '20rem', maxHeight: '20rem'}} role="tabpanel" id={activeTab === 'shareLinks' ? 'share-links-panel' : 'upload-links-panel'}>
|
||||
{loading && <Loading />}
|
||||
{!loading && errorMsg && <p className="error text-center mt-8">{errorMsg}</p>}
|
||||
{!loading && !errorMsg && !repoShareUploadLinkList.length &&
|
||||
<EmptyTip forDialog={true}>
|
||||
<p className="text-secondary">{activeTab == 'shareLinks' ? gettext('No share links') : gettext('No upload links')}</p>
|
||||
</EmptyTip>
|
||||
}
|
||||
{!loading && !errorMsg && repoShareUploadLinkList.length > 0 &&
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="22%">{gettext('Creator')}</th>
|
||||
<th width="20%">{gettext('Name')}</th>
|
||||
<th width="48%">{gettext('Link')}</th>
|
||||
<th width="10%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.state.repoShareUploadLinkList.map((item, index) => {
|
||||
return (
|
||||
<RepoShareUploadLinkItem
|
||||
key={index}
|
||||
item={item}
|
||||
activeTab={this.state.activeTab}
|
||||
deleteItem={this.deleteItem}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RepoShareUploadLinksDialog.propTypes = RepoShareUploadLinksDialogPropTypes;
|
||||
|
||||
export default RepoShareUploadLinksDialog;
|
@@ -16,7 +16,7 @@ import { seafileAPI } from '../../utils/seafile-api';
|
||||
import LibHistorySettingDialog from '../dialog/lib-history-setting-dialog';
|
||||
import toaster from '../toast';
|
||||
import RepoAPITokenDialog from '../dialog/repo-api-token-dialog';
|
||||
import RepoShareUploadLinksDialog from '../dialog/repo-share-upload-links-dialog';
|
||||
import RepoShareAdminDialog from '../dialog/repo-share-admin-dialog';
|
||||
import RepoMonitoredIcon from '../../components/repo-monitored-icon';
|
||||
|
||||
const propTypes = {
|
||||
@@ -48,7 +48,7 @@ class SharedRepoListItem extends React.Component {
|
||||
isHistorySettingDialogShow: false,
|
||||
isDeleteDialogShow: false,
|
||||
isAPITokenDialogShow: false,
|
||||
isRepoShareUploadLinksDialogOpen: false,
|
||||
isRepoShareAdminDialogOpen: false,
|
||||
isRepoDeleted: false,
|
||||
isChangePasswordDialogShow: false,
|
||||
isResetPasswordDialogShow: false
|
||||
@@ -156,8 +156,8 @@ class SharedRepoListItem extends React.Component {
|
||||
case 'API Token':
|
||||
this.onAPITokenToggle();
|
||||
break;
|
||||
case 'Share Links Admin':
|
||||
this.toggleRepoShareUploadLinksDialog();
|
||||
case 'Share Admin':
|
||||
this.toggleRepoShareAdminDialog();
|
||||
break;
|
||||
case 'Change Password':
|
||||
this.onChangePasswordToggle();
|
||||
@@ -275,8 +275,8 @@ class SharedRepoListItem extends React.Component {
|
||||
this.setState({isShowSharedDialog: false});
|
||||
}
|
||||
|
||||
toggleRepoShareUploadLinksDialog = () => {
|
||||
this.setState({isRepoShareUploadLinksDialogOpen: !this.state.isRepoShareUploadLinksDialogOpen});
|
||||
toggleRepoShareAdminDialog = () => {
|
||||
this.setState({isRepoShareAdminDialogOpen: !this.state.isRepoShareAdminDialogOpen});
|
||||
}
|
||||
|
||||
onAPITokenToggle = () => {
|
||||
@@ -312,8 +312,8 @@ class SharedRepoListItem extends React.Component {
|
||||
case 'History Setting':
|
||||
translateResult = gettext('History Setting');
|
||||
break;
|
||||
case 'Share Links Admin':
|
||||
translateResult = gettext('Share Links Admin');
|
||||
case 'Share Admin':
|
||||
translateResult = gettext('Share Admin');
|
||||
break;
|
||||
case 'Change Password':
|
||||
translateResult = gettext('Change Password');
|
||||
@@ -352,7 +352,7 @@ class SharedRepoListItem extends React.Component {
|
||||
if (folderPermEnabled) {
|
||||
operations.push('Folder Permission');
|
||||
}
|
||||
operations.push('Share Links Admin');
|
||||
operations.push('Share Admin');
|
||||
if (repo.encrypted) {
|
||||
operations.push('Change Password');
|
||||
}
|
||||
@@ -653,11 +653,11 @@ class SharedRepoListItem extends React.Component {
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
{this.state.isRepoShareUploadLinksDialogOpen && (
|
||||
{this.state.isRepoShareAdminDialogOpen && (
|
||||
<ModalPortal>
|
||||
<RepoShareUploadLinksDialog
|
||||
<RepoShareAdminDialog
|
||||
repo={repo}
|
||||
toggleDialog={this.toggleRepoShareUploadLinksDialog}
|
||||
toggleDialog={this.toggleRepoShareAdminDialog}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
|
@@ -11,7 +11,7 @@
|
||||
}
|
||||
|
||||
.share-dialog-content .share-dialog-side {
|
||||
display: flex;
|
||||
/*display: flex;*/
|
||||
flex-basis: 22%;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
@@ -19,7 +19,7 @@ import LibSubFolderPermissionDialog from '../../components/dialog/lib-sub-folder
|
||||
import Rename from '../../components/rename';
|
||||
import MylibRepoMenu from './mylib-repo-menu';
|
||||
import RepoAPITokenDialog from '../../components/dialog/repo-api-token-dialog';
|
||||
import RepoShareUploadLinksDialog from '../../components/dialog/repo-share-upload-links-dialog';
|
||||
import RepoShareAdminDialog from '../../components/dialog/repo-share-admin-dialog';
|
||||
import LibOldFilesAutoDelDialog from '../../components/dialog/lib-old-files-auto-del-dialog';
|
||||
import RepoMonitoredIcon from '../../components/repo-monitored-icon';
|
||||
|
||||
@@ -51,7 +51,7 @@ class MylibRepoListItem extends React.Component {
|
||||
isLabelRepoStateDialogOpen: false,
|
||||
isFolderPermissionDialogShow: false,
|
||||
isAPITokenDialogShow: false,
|
||||
isRepoShareUploadLinksDialogOpen: false,
|
||||
isRepoShareAdminDialogOpen: false,
|
||||
isRepoDeleted: false,
|
||||
isOldFilesAutoDelDialogOpen: false,
|
||||
};
|
||||
@@ -125,8 +125,8 @@ class MylibRepoListItem extends React.Component {
|
||||
case 'API Token':
|
||||
this.onAPITokenToggle();
|
||||
break;
|
||||
case 'Share Links Admin':
|
||||
this.toggleRepoShareUploadLinksDialog();
|
||||
case 'Share Admin':
|
||||
this.toggleRepoShareAdminDialog();
|
||||
break;
|
||||
case 'Old Files Auto Delete':
|
||||
this.toggleOldFilesAutoDelDialog();
|
||||
@@ -239,8 +239,8 @@ class MylibRepoListItem extends React.Component {
|
||||
this.setState({isAPITokenDialogShow: !this.state.isAPITokenDialogShow});
|
||||
}
|
||||
|
||||
toggleRepoShareUploadLinksDialog = () => {
|
||||
this.setState({isRepoShareUploadLinksDialogOpen: !this.state.isRepoShareUploadLinksDialogOpen});
|
||||
toggleRepoShareAdminDialog = () => {
|
||||
this.setState({isRepoShareAdminDialogOpen: !this.state.isRepoShareAdminDialogOpen});
|
||||
}
|
||||
|
||||
toggleOldFilesAutoDelDialog = () => {
|
||||
@@ -508,11 +508,11 @@ class MylibRepoListItem extends React.Component {
|
||||
</ModalPortal>
|
||||
)}
|
||||
|
||||
{this.state.isRepoShareUploadLinksDialogOpen && (
|
||||
{this.state.isRepoShareAdminDialogOpen && (
|
||||
<ModalPortal>
|
||||
<RepoShareUploadLinksDialog
|
||||
<RepoShareAdminDialog
|
||||
repo={repo}
|
||||
toggleDialog={this.toggleRepoShareUploadLinksDialog}
|
||||
toggleDialog={this.toggleRepoShareAdminDialog}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
|
@@ -69,7 +69,7 @@ class MylibRepoMenu extends React.Component {
|
||||
if (folderPermEnabled) {
|
||||
operations.push('Folder Permission');
|
||||
}
|
||||
operations.push('Share Links Admin', 'Divider');
|
||||
operations.push('Share Admin', 'Divider');
|
||||
|
||||
if (repo.encrypted) {
|
||||
operations.push('Change Password');
|
||||
@@ -139,8 +139,8 @@ class MylibRepoMenu extends React.Component {
|
||||
case 'API Token':
|
||||
translateResult = 'API Token'; // translation is not needed here
|
||||
break;
|
||||
case 'Share Links Admin':
|
||||
translateResult = gettext('Share Links Admin');
|
||||
case 'Share Admin':
|
||||
translateResult = gettext('Share Admin');
|
||||
break;
|
||||
case 'Old Files Auto Delete':
|
||||
translateResult = gettext('Auto Deletion Setting');
|
||||
|
@@ -277,7 +277,7 @@ export const Utils = {
|
||||
if (path === '/') {
|
||||
return path;
|
||||
}
|
||||
path = path[path.length - 1] !== '/' ? path : path.slice(0, path.length -2);
|
||||
path = path[path.length - 1] !== '/' ? path : path.slice(0, path.length - 1);
|
||||
return path.slice(path.lastIndexOf('/') + 1);
|
||||
},
|
||||
|
||||
|
135
seahub/api2/endpoints/repo_folder_share_info.py
Normal file
135
seahub/api2/endpoints/repo_folder_share_info.py
Normal file
@@ -0,0 +1,135 @@
|
||||
# Copyright (c) 2012-2016 Seafile Ltd.
|
||||
import logging
|
||||
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import status
|
||||
|
||||
from seaserv import seafile_api
|
||||
|
||||
from seahub.api2.utils import api_error
|
||||
from seahub.api2.authentication import TokenAuthentication
|
||||
from seahub.api2.throttling import UserRateThrottle
|
||||
|
||||
from seahub.constants import PERMISSION_ADMIN
|
||||
from seahub.utils import normalize_dir_path, is_org_context
|
||||
from seahub.utils.db_api import SeafileDB
|
||||
from seahub.group.utils import group_id_to_name
|
||||
from seahub.share.utils import is_repo_admin
|
||||
from seahub.share.models import CustomSharePermissions, \
|
||||
ExtraSharePermission, ExtraGroupsSharePermission
|
||||
from seahub.base.templatetags.seahub_tags import email2nickname
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RepoFolderShareInfo(APIView):
|
||||
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
throttle_classes = (UserRateThrottle,)
|
||||
|
||||
def get(self, request, format=None):
|
||||
""" List all shared out repos and folders.
|
||||
|
||||
Permission checking:
|
||||
1. repo admin
|
||||
"""
|
||||
|
||||
# argument check
|
||||
repo_id = request.GET.get('repo_id')
|
||||
if not repo_id:
|
||||
error_msg = 'repo_id invalid.'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
share_to = request.GET.get('share_to')
|
||||
if share_to and share_to not in ('user', 'group'):
|
||||
error_msg = 'share_to invalid.'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
# resource check
|
||||
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
|
||||
username = request.user.username
|
||||
if not is_repo_admin(username, repo_id):
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
# get share inifo
|
||||
share_info_list = []
|
||||
try:
|
||||
seafile_db = SeafileDB()
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
org_id = ''
|
||||
if is_org_context(request):
|
||||
org_id = request.user.org.org_id
|
||||
|
||||
if share_to == 'user':
|
||||
share_info_list += seafile_db.get_repo_user_share_list(repo_id,
|
||||
org_id)
|
||||
share_info_list += seafile_db.get_folder_user_share_list(repo_id,
|
||||
org_id)
|
||||
elif share_to == 'group':
|
||||
share_info_list += seafile_db.get_repo_group_share_list(repo_id,
|
||||
org_id)
|
||||
share_info_list += seafile_db.get_folder_group_share_list(repo_id,
|
||||
org_id)
|
||||
else:
|
||||
share_info_list += seafile_db.get_repo_user_share_list(repo_id,
|
||||
org_id)
|
||||
share_info_list += seafile_db.get_folder_user_share_list(repo_id,
|
||||
org_id)
|
||||
share_info_list += seafile_db.get_repo_group_share_list(repo_id,
|
||||
org_id)
|
||||
share_info_list += seafile_db.get_folder_group_share_list(repo_id,
|
||||
org_id)
|
||||
|
||||
custom_perm_dict = {}
|
||||
custom_perms = CustomSharePermissions.objects.filter(repo_id=repo_id)
|
||||
for custom_perm in custom_perms:
|
||||
custom_id = f'custom-{custom_perm.id}'
|
||||
custom_perm_dict[custom_id] = custom_perm.name
|
||||
|
||||
for share_info in share_info_list:
|
||||
|
||||
share_info['path'] = normalize_dir_path(share_info['path'])
|
||||
share_info['repo_name'] = repo.repo_name
|
||||
|
||||
share_type = share_info['share_type']
|
||||
share_to = share_info['share_to']
|
||||
|
||||
extra_perm = ''
|
||||
if share_type == 'user':
|
||||
|
||||
share_info['share_to_name'] = email2nickname(share_to)
|
||||
if share_info['path'] == '/':
|
||||
extra_perm = ExtraSharePermission.objects.get_user_permission(repo_id,
|
||||
share_to)
|
||||
if share_type == 'group':
|
||||
|
||||
share_info['share_to_name'] = group_id_to_name(share_to)
|
||||
if share_info['path'] == '/':
|
||||
extra_perm = ExtraGroupsSharePermission.objects.get_group_permission(repo_id,
|
||||
share_to)
|
||||
|
||||
if extra_perm == PERMISSION_ADMIN:
|
||||
share_info['permission'] = 'admin'
|
||||
|
||||
share_info['permission_name'] = ''
|
||||
if share_info['permission'].startswith('custom-'):
|
||||
share_info['permission_name'] = custom_perm_dict.get(share_info['permission'])
|
||||
|
||||
share_info = {}
|
||||
share_info['share_info_list'] = share_info_list
|
||||
|
||||
return Response(share_info)
|
@@ -22,7 +22,7 @@ from seahub.dingtalk.views import dingtalk_login, dingtalk_callback, \
|
||||
|
||||
from seahub.api2.endpoints.search_file import SearchFile
|
||||
|
||||
from seahub.api2.endpoints.smart_link import SmartLink, SmartLinkToken
|
||||
from seahub.api2.endpoints.smart_link import SmartLink, SmartLinkToken
|
||||
from seahub.api2.endpoints.groups import Groups, Group
|
||||
from seahub.api2.endpoints.all_groups import AllGroups
|
||||
from seahub.api2.endpoints.departments import Departments
|
||||
@@ -46,6 +46,7 @@ from seahub.api2.endpoints.share_links import ShareLinks, ShareLink, \
|
||||
ShareLinksCleanInvalid
|
||||
from seahub.api2.endpoints.multi_share_links import MultiShareLinks, \
|
||||
MultiShareLinksBatch
|
||||
from seahub.api2.endpoints.repo_folder_share_info import RepoFolderShareInfo
|
||||
from seahub.api2.endpoints.shared_folders import SharedFolders
|
||||
from seahub.api2.endpoints.shared_repos import SharedRepos, SharedRepo
|
||||
from seahub.api2.endpoints.upload_links import UploadLinks, UploadLink, \
|
||||
@@ -346,6 +347,9 @@ urlpatterns = [
|
||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/custom-share-permissions/$', CustomSharePermissionsView.as_view(), name='api-v2.1-custom-share-permissions'),
|
||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/custom-share-permissions/(?P<permission_id>\d+)/$', CustomSharePermissionView.as_view(), name='api-v2.1-custom-share-permission'),
|
||||
|
||||
## user::shared-repos-folders
|
||||
re_path(r'^api/v2.1/repo-folder-share-info/$', RepoFolderShareInfo.as_view(), name='api-v2.1-repo-folder-share-info'),
|
||||
|
||||
## user::shared-folders
|
||||
re_path(r'^api/v2.1/shared-folders/$', SharedFolders.as_view(), name='api-v2.1-shared-folders'),
|
||||
|
||||
|
205
seahub/utils/db_api.py
Normal file
205
seahub/utils/db_api.py
Normal file
@@ -0,0 +1,205 @@
|
||||
import os
|
||||
import configparser
|
||||
from django.db import connection
|
||||
|
||||
|
||||
class SeafileDB:
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.db_name = self._get_seafile_db_name()
|
||||
|
||||
def _get_seafile_db_name(self):
|
||||
|
||||
conf_dir = os.environ.get('SEAFILE_CENTRAL_CONF_DIR') or \
|
||||
os.environ.get('SEAFILE_CONF_DIR')
|
||||
|
||||
if not conf_dir:
|
||||
return ""
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
seafile_conf_path = os.path.join(conf_dir, 'seafile.conf')
|
||||
config.read(seafile_conf_path)
|
||||
|
||||
if not config.has_section('database'):
|
||||
return ''
|
||||
|
||||
if 'sqlite' in config.get('database', 'type'):
|
||||
return ''
|
||||
|
||||
db_name = config.get('database', 'db_name')
|
||||
if not db_name:
|
||||
raise Exception("Database name not configured.")
|
||||
|
||||
return db_name
|
||||
|
||||
def get_repo_user_share_list(self, repo_id, org_id=''):
|
||||
|
||||
# get repos shared to user
|
||||
|
||||
if not org_id:
|
||||
sql = f"""
|
||||
SELECT
|
||||
s.repo_id, s.from_email, s.to_email, s.permission
|
||||
FROM
|
||||
{self.db_name}.SharedRepo s
|
||||
WHERE
|
||||
repo_id = '{repo_id}';
|
||||
"""
|
||||
else:
|
||||
sql = f"""
|
||||
SELECT
|
||||
s.repo_id, s.from_email, s.to_email, s.permission
|
||||
FROM
|
||||
{self.db_name}.OrgSharedRepo s
|
||||
WHERE
|
||||
repo_id = '{repo_id}';
|
||||
"""
|
||||
|
||||
share_info_list = []
|
||||
with connection.cursor() as cursor:
|
||||
|
||||
cursor.execute(sql)
|
||||
for item in cursor.fetchall():
|
||||
|
||||
info = {}
|
||||
info['share_type'] = 'user'
|
||||
info['repo_id'] = item[0]
|
||||
info['path'] = '/'
|
||||
info['share_from'] = item[1]
|
||||
info['share_to'] = item[2]
|
||||
info['permission'] = item[3]
|
||||
|
||||
share_info_list.append(info)
|
||||
|
||||
return share_info_list
|
||||
|
||||
def get_repo_group_share_list(self, repo_id, org_id=''):
|
||||
|
||||
# get repos shared to group
|
||||
|
||||
if not org_id:
|
||||
sql = f"""
|
||||
SELECT
|
||||
s.repo_id, s.user_name, s.group_id, s.permission
|
||||
FROM
|
||||
{self.db_name}.RepoGroup s
|
||||
WHERE
|
||||
repo_id = '{repo_id}';
|
||||
"""
|
||||
else:
|
||||
sql = f"""
|
||||
SELECT
|
||||
s.repo_id, s.owner, s.group_id, s.permission
|
||||
FROM
|
||||
{self.db_name}.OrgGroupRepo s
|
||||
WHERE
|
||||
repo_id = '{repo_id}';
|
||||
"""
|
||||
|
||||
share_info_list = []
|
||||
with connection.cursor() as cursor:
|
||||
|
||||
cursor.execute(sql)
|
||||
for item in cursor.fetchall():
|
||||
|
||||
info = {}
|
||||
info['share_type'] = 'group'
|
||||
info['repo_id'] = item[0]
|
||||
info['path'] = '/'
|
||||
info['share_from'] = item[1]
|
||||
info['share_to'] = item[2]
|
||||
info['permission'] = item[3]
|
||||
|
||||
share_info_list.append(info)
|
||||
|
||||
return share_info_list
|
||||
|
||||
def get_folder_user_share_list(self, repo_id, org_id=''):
|
||||
|
||||
# get folders shared to user
|
||||
if not org_id:
|
||||
sql = f"""
|
||||
SELECT
|
||||
v.origin_repo, v.path, s.from_email, s.to_email, s.permission
|
||||
FROM
|
||||
{self.db_name}.SharedRepo s join {self.db_name}.VirtualRepo v
|
||||
ON
|
||||
s.repo_id=v.repo_id
|
||||
WHERE
|
||||
v.origin_repo = '{repo_id}';
|
||||
"""
|
||||
else:
|
||||
sql = f"""
|
||||
SELECT
|
||||
v.origin_repo, v.path, s.from_email, s.to_email, s.permission
|
||||
FROM
|
||||
{self.db_name}.OrgSharedRepo s join {self.db_name}.VirtualRepo v
|
||||
ON
|
||||
s.repo_id=v.repo_id
|
||||
WHERE
|
||||
v.origin_repo = '{repo_id}';
|
||||
"""
|
||||
|
||||
share_info_list = []
|
||||
with connection.cursor() as cursor:
|
||||
|
||||
cursor.execute(sql)
|
||||
for item in cursor.fetchall():
|
||||
|
||||
info = {}
|
||||
info['share_type'] = 'user'
|
||||
info['repo_id'] = item[0]
|
||||
info['path'] = item[1]
|
||||
info['share_from'] = item[2]
|
||||
info['share_to'] = item[3]
|
||||
info['permission'] = item[4]
|
||||
|
||||
share_info_list.append(info)
|
||||
|
||||
return share_info_list
|
||||
|
||||
def get_folder_group_share_list(self, repo_id, org_id=''):
|
||||
|
||||
# get folders shared to group
|
||||
|
||||
if not org_id:
|
||||
sql = f"""
|
||||
SELECT
|
||||
v.origin_repo, v.path, r.user_name, r.group_id, r.permission
|
||||
FROM
|
||||
{self.db_name}.RepoGroup r join {self.db_name}.VirtualRepo v
|
||||
ON
|
||||
r.repo_id=v.repo_id
|
||||
WHERE
|
||||
v.origin_repo = '{repo_id}';
|
||||
"""
|
||||
else:
|
||||
sql = f"""
|
||||
SELECT
|
||||
v.origin_repo, v.path, r.owner, r.group_id, r.permission
|
||||
FROM
|
||||
{self.db_name}.OrgGroupRepo r join {self.db_name}.VirtualRepo v
|
||||
ON
|
||||
r.repo_id=v.repo_id
|
||||
WHERE
|
||||
v.origin_repo = '{repo_id}';
|
||||
"""
|
||||
|
||||
share_info_list = []
|
||||
with connection.cursor() as cursor:
|
||||
|
||||
cursor.execute(sql)
|
||||
for item in cursor.fetchall():
|
||||
|
||||
info = {}
|
||||
info['share_type'] = 'group'
|
||||
info['repo_id'] = item[0]
|
||||
info['path'] = item[1]
|
||||
info['share_from'] = item[2]
|
||||
info['share_to'] = item[3]
|
||||
info['permission'] = item[4]
|
||||
|
||||
share_info_list.append(info)
|
||||
|
||||
return share_info_list
|
Reference in New Issue
Block a user