1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-12 13:24:52 +00:00

Org user page (#2941)

* init org user

* optimized code style

* freezed item

* update select-ediotr style

* optimized code style

* add state
This commit is contained in:
陈钦亮
2019-02-27 19:44:22 +08:00
committed by Daniel Pan
parent e28f6361d0
commit e2ad2fd023
21 changed files with 1033 additions and 16 deletions

View File

@@ -113,6 +113,11 @@ module.exports = {
require.resolve('./polyfills'), require.resolve('./polyfills'),
require.resolve('react-dev-utils/webpackHotDevClient'), require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + "/view-file-xmind.js", paths.appSrc + "/view-file-xmind.js",
],
orgAdmin: [
require.resolve('./polyfills'),
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + "/pages/org-admin",
] ]
}, },

View File

@@ -71,6 +71,7 @@ module.exports = {
viewFileText: [require.resolve('./polyfills'), paths.appSrc + "/view-file-text.js"], viewFileText: [require.resolve('./polyfills'), paths.appSrc + "/view-file-text.js"],
viewFileImage: [require.resolve('./polyfills'), paths.appSrc + "/view-file-image.js"], viewFileImage: [require.resolve('./polyfills'), paths.appSrc + "/view-file-image.js"],
viewFileXmind: [require.resolve('./polyfills'), paths.appSrc + "/view-file-xmind.js"], viewFileXmind: [require.resolve('./polyfills'), paths.appSrc + "/view-file-xmind.js"],
orgAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/org-admin"],
}, },
output: { output: {

View File

@@ -640,7 +640,7 @@
}, },
"axios": { "axios": {
"version": "0.18.0", "version": "0.18.0",
"resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz",
"integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=",
"requires": { "requires": {
"follow-redirects": "^1.3.0", "follow-redirects": "^1.3.0",
@@ -5318,7 +5318,7 @@
}, },
"git-up": { "git-up": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "http://registry.npmjs.org/git-up/-/git-up-1.2.1.tgz", "resolved": "https://registry.npmjs.org/git-up/-/git-up-1.2.1.tgz",
"integrity": "sha1-JkSAoAax2EJhrB/gmjpRacV+oZ0=", "integrity": "sha1-JkSAoAax2EJhrB/gmjpRacV+oZ0=",
"requires": { "requires": {
"is-ssh": "^1.0.0", "is-ssh": "^1.0.0",
@@ -5327,7 +5327,7 @@
}, },
"git-url-parse": { "git-url-parse": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "http://registry.npmjs.org/git-url-parse/-/git-url-parse-5.0.1.tgz", "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-5.0.1.tgz",
"integrity": "sha1-/j15xnRq4FBIz6UIyB553du6OEM=", "integrity": "sha1-/j15xnRq4FBIz6UIyB553du6OEM=",
"requires": { "requires": {
"git-up": "^1.0.0" "git-up": "^1.0.0"
@@ -7927,7 +7927,7 @@
}, },
"node-status-codes": { "node-status-codes": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "http://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz", "resolved": "https://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz",
"integrity": "sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8=" "integrity": "sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8="
}, },
"noop6": { "noop6": {
@@ -8234,7 +8234,7 @@
}, },
"package.json": { "package.json": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "http://registry.npmjs.org/package.json/-/package.json-2.0.1.tgz", "resolved": "https://registry.npmjs.org/package.json/-/package.json-2.0.1.tgz",
"integrity": "sha1-+IYFnSpJ7QduZIg2ldc7K0bSHW0=", "integrity": "sha1-+IYFnSpJ7QduZIg2ldc7K0bSHW0=",
"requires": { "requires": {
"git-package-json": "^1.4.0", "git-package-json": "^1.4.0",
@@ -8244,7 +8244,7 @@
"dependencies": { "dependencies": {
"got": { "got": {
"version": "5.7.1", "version": "5.7.1",
"resolved": "http://registry.npmjs.org/got/-/got-5.7.1.tgz", "resolved": "https://registry.npmjs.org/got/-/got-5.7.1.tgz",
"integrity": "sha1-X4FjWmHkplifGAVp6k44FoClHzU=", "integrity": "sha1-X4FjWmHkplifGAVp6k44FoClHzU=",
"requires": { "requires": {
"create-error-class": "^3.0.1", "create-error-class": "^3.0.1",
@@ -8266,7 +8266,7 @@
}, },
"package-json": { "package-json": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "http://registry.npmjs.org/package-json/-/package-json-2.4.0.tgz", "resolved": "https://registry.npmjs.org/package-json/-/package-json-2.4.0.tgz",
"integrity": "sha1-DRW9Z9HLvduyyiIv8u24a8sxqLs=", "integrity": "sha1-DRW9Z9HLvduyyiIv8u24a8sxqLs=",
"requires": { "requires": {
"got": "^5.0.0", "got": "^5.0.0",
@@ -10932,9 +10932,9 @@
} }
}, },
"seafile-js": { "seafile-js": {
"version": "0.2.64", "version": "0.2.66",
"resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.64.tgz", "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.66.tgz",
"integrity": "sha512-gaurvv8Gwq1IjXkHh1BufbeQxkmBRzxpNt/TqzOFWwBG2xsUW878T7HxiO+uw6amsc+K7PjYk2BVVmo29MgHqg==", "integrity": "sha512-a51numCHkkMzNSp/7HpC0o/WYRF2m3+1g4yRPqASEnVXRSiZHiHY1fSR0W5eLmDqAmMoYiWdk99Y+kdjfhxb4A==",
"requires": { "requires": {
"axios": "^0.18.0", "axios": "^0.18.0",
"form-data": "^2.3.2", "form-data": "^2.3.2",

View File

@@ -34,7 +34,7 @@
"react-responsive": "^6.1.1", "react-responsive": "^6.1.1",
"react-select": "^2.4.1", "react-select": "^2.4.1",
"reactstrap": "^6.4.0", "reactstrap": "^6.4.0",
"seafile-js": "^0.2.64", "seafile-js": "^0.2.66",
"socket.io-client": "^2.2.0", "socket.io-client": "^2.2.0",
"sw-precache-webpack-plugin": "0.11.4", "sw-precache-webpack-plugin": "0.11.4",
"unified": "^7.0.0", "unified": "^7.0.0",

View File

@@ -0,0 +1,91 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import AsyncSelect from 'react-select/lib/Async';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
import { gettext } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
const propTypes = {
toggle: PropTypes.func.isRequired,
addOrgAdmin: PropTypes.func.isRequired,
};
class AddOrgAdminDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedOption: null
};
this.options = [];
}
loadOptions = (value, callback) => {
if (value.trim().length > 0) {
seafileAPI.searchUsers(value.trim()).then((res) => {
this.options = [];
for (let i = 0 ; i < res.data.users.length; i++) {
let obj = {};
obj.value = res.data.users[i].name;
obj.email = res.data.users[i].email;
obj.label =
<Fragment>
<img src={res.data.users[i].avatar_url} className="select-module select-module-icon avatar" alt="Avatar"/>
<span className='select-module select-module-name'>{res.data.users[i].name}</span>
</Fragment>;
this.options.push(obj);
}
callback(this.options);
});
}
}
handleSelectChange = (option) => {
this.setState({selectedOption: option});
this.options = [];
}
addOrgAdmin = () => {
let users = [];
if (this.state.selectedOption && this.state.selectedOption.length > 0 ) {
for (let i = 0; i < this.state.selectedOption.length; i ++) {
users[i] = this.state.selectedOption[i].email;
}
}
this.props.addOrgAdmin(users)
}
toggle = () => {
this.props.toggle();
}
render() {
return (
<Modal isOpen={true}>
<ModalHeader>{gettext('Add Admins')}</ModalHeader>
<ModalBody>
<AsyncSelect
inputId={'react-select-1-input'}
className='reviewer-select'
placeholder={gettext('Select a user as admin...')}
loadOptions={this.loadOptions}
onChange={this.handleSelectChange}
value={this.state.selectedOption}
maxMenuHeight={200}
isMulti
isFocused
isClearable
classNamePrefix
/>
</ModalBody>
<ModalFooter>
<Button color="primary" onClick={this.addOrgAdmin}>{gettext('Submit')}</Button>
<Button color="secondary" onClick={this.toggle}>{gettext('Close')}</Button>
</ModalFooter>
</Modal>
);
}
}
AddOrgAdminDialog.propTypes = propTypes;
export default AddOrgAdminDialog;

View File

@@ -0,0 +1,146 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Modal, Input, ModalHeader, ModalBody, Label, Form, InputGroup, InputGroupAddon, FormGroup } from 'reactstrap';
import { gettext } from '../../utils/constants';
const propTypes = {
toggle: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired,
};
class AddOrgUserDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
isPasswordVisible: true,
email: '',
name: '',
password: '',
passwdnew: '',
errMessage: ''
};
}
handleSubmit = () => {
let isValid = this.validateInputParams();
if (isValid) {
let { email, name, password } = this.state;
this.props.handleSubmit(email, name, password);
}
}
handleKeyPress = (e) => {
e.preventDefault();
if (e.key == 'Enter') {
this.handleSubmit(e);
}
};
togglePasswordVisible = () => {
this.setState({
isPasswordVisible: !this.state.isPasswordVisible
});
}
generatePassword = () => {
let val = Math.random().toString(36).substr(5);
this.setState({
password: val,
passwdnew: val
});
}
inputEmail = (e) => {
let email = e.target.value.trim();
this.setState({email: email});
}
inputName = (e) => {
let name = e.target.value.trim();
this.setState({name: name});
}
inputPassword = (e) => {
let passwd = e.target.value.trim();
this.setState({password: passwd});
}
inputPasswordNew = (e) => {
let passwd = e.target.value.trim();
this.setState({passwdnew: passwd});
}
toggle = () => {
this.props.toggle();
};
validateInputParams() {
let errMessage = '';
let email = this.state.email;
if (!email.length) {
errMessage = 'email is required';
this.setState({errMessage: errMessage});
return false;
}
let password1 = this.state.password;
let password2 = this.state.passwdnew;
if (!password1.length) {
errMessage = 'Please enter password';
this.setState({errMessage: errMessage});
return false;
}
if (!password2.length) {
errMessage = 'Please enter the password again';
this.setState({errMessage: errMessage});
return false;
}
if (password1 !== password2) {
errMessage = 'Passwords don\'t match';
this.setState({errMessage: errMessage});
return false;
}
return true;
}
render() {
return (
<Modal isOpen={true}>
<ModalHeader toggle={this.toggle}>{gettext('Add User')}</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<Label>{gettext('Email')}</Label>
<Input value={this.state.email || ''} onChange={this.inputEmail} />
</FormGroup>
<FormGroup>
<Label>{gettext('Name(optional)')}</Label>
<Input value={this.state.name || ''} onChange={this.inputName} />
</FormGroup>
<FormGroup>
<Label>{gettext('Password')}</Label>
<InputGroup className="passwd">
<Input type={this.state.isPasswordVisible ? 'text' : 'password'} value={this.state.password || ''} onChange={this.inputPassword}/>
<InputGroupAddon addonType="append">
<Button onClick={this.togglePasswordVisible}><i className={`link-operation-icon fas ${this.state.isPasswordVisible ? 'fa-eye': 'fa-eye-slash'}`}></i></Button>
<Button onClick={this.generatePassword}><i className="link-operation-icon fas fa-magic"></i></Button>
</InputGroupAddon>
</InputGroup>
</FormGroup>
<FormGroup>
<Label>{gettext('Confirm Password')}</Label>
<Input className="passwd" type={this.state.isPasswordVisible ? 'text' : 'password'} value={this.state.passwdnew || ''} onChange={this.inputPasswordNew} />
</FormGroup>
<Button onClick={this.handleSubmit}>{gettext('Submit')}</Button>
</Form>
<Label className="err-message">{gettext(this.state.errMessage)}</Label>
</ModalBody>
</Modal>
);
}
}
AddOrgUserDialog.propTypes = propTypes;
export default AddOrgUserDialog;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import PropTypes from 'prop-types';
import { gettext } from '../../utils/constants';
import SelectEditor from './select-editor';
const propTypes = {
isTextMode: PropTypes.bool.isRequired,
isEditIconShow: PropTypes.bool.isRequired,
statusArray: PropTypes.array.isRequired,
currentStatus: PropTypes.string.isRequired,
onStatusChanged: PropTypes.func.isRequired
};
class UserStatusEditor extends React.Component {
translateStatus = (userStatus) => {
if (userStatus === 'active') {
return gettext('Active');
}
if (userStatus === 'inactive') {
return gettext('Inactive');
}
}
render() {
return (
<SelectEditor
isTextMode={this.props.isTextMode}
isEditIconShow={this.props.isEditIconShow}
options={this.props.statusArray}
currentOption={this.props.currentStatus}
onOptionChanged={this.props.onStatusChanged}
translateOption={this.translateStatus}
/>
);
}
}
UserStatusEditor.propTypes = propTypes;
export default UserStatusEditor;

View File

@@ -129,10 +129,6 @@
border-radius: 2px; border-radius: 2px;
} }
.cur-view-content .permission-editor-select .permission-editor__control {
height: 24px;
min-height: 24px;
}
.cur-view-detail { .cur-view-detail {
flex: 0 0 20rem; flex: 0 0 20rem;
display: flex; display: flex;

View File

@@ -0,0 +1,9 @@
.paginator {
text-align: center;
margin: 10px 0;
font-size: 14px;
}
.cur-view-path.org-user-nav {
padding: 0 1rem;
}

View File

@@ -13,4 +13,21 @@
} }
.permission-editor .permission-editor__control .permission-editor-explanation { .permission-editor .permission-editor__control .permission-editor-explanation {
display: none; display: none;
} }
.cur-view-content .permission-editor-select .permission-editor__control,
.cur-view-content .permission-editor-select .permission-editor__control div,
.cur-view-content .permission-editor-select .permission-editor__control .permission-editor__input,
.cur-view-content .permission-editor-select .permission-editor__indicators {
height: 1.5rem;
min-height: 1.5rem;
}
.cur-view-content .permission-editor-select .permission-editor__value-container div:nth-child(2) {
margin: 0;
padding: 0;
}
.cur-view-content .permission-editor-select .permission-editor__indicators .permission-editor__indicator {
padding: 0 0.5rem;
}

View File

@@ -0,0 +1,21 @@
import { Utils } from '../utils/utils';
import { lang } from '../utils/constants';
import moment from 'moment';
moment.locale(lang);
class OrgUserInfo {
constructor(object) {
this.id = object.id;
this.name = object.name;
this.email = object.email;
this.contact_email = object.owner_contact_email;
this.is_active = object.is_active;
this.quota = object.quota > 0 ? Utils.bytesToSize(object.quota) : '';
this.self_usage = Utils.bytesToSize(object.self_usage);
this.last_login = object.last_login ? moment(object.last_login).fromNow() : '--';
this.ctime = moment(object.ctime).format('YYYY-MM-DD HH:mm:ss');
}
}
export default OrgUserInfo;

View File

@@ -0,0 +1,82 @@
// Import React!
import React from 'react';
import ReactDOM from 'react-dom';
import { Router } from '@reach/router';
import { siteRoot } from '../../utils/constants';
import SidePanel from './side-panel';
import MainPanel from './main-panel';
import OrgUsers from './org-users';
import OrgUsersList from './org-users-list';
import OrgAdminList from './org-admin-list';
import '../../assets/css/fa-solid.css';
import '../../assets/css/fa-regular.css';
import '../../assets/css/fontawesome.css';
import '../../css/layout.css';
import '../../css/toolbar.css';
class Org extends React.Component {
constructor(props) {
super(props);
this.state = {
isSidePanelClosed: false,
isShowAddOrgUserDialog: false,
isShowAddOrgAdminDialog: false,
currentTab: 'users',
};
}
componentDidMount() {
let href = window.location.href.split('/');
let currentTab = href[href.length - 2];
if (currentTab == 'useradmin') {
currentTab = 'users';
}
this.setState({currentTab: currentTab});
}
onCloseSidePanel = () => {
this.setState({isSidePanelClosed: !this.state.isSidePanelClosed});
}
tabItemClick = (param) => {
this.setState({currentTab: param});
}
toggleAddOrgUser = () => {
this.setState({isShowAddOrgUserDialog: !this.state.isShowAddOrgUserDialog});
}
toggleAddOrgAdmin = () => {
this.setState({isShowAddOrgAdminDialog: !this.state.isShowAddOrgAdminDialog});
}
render() {
let { isSidePanelClosed, currentTab, isShowAddOrgUserDialog, isShowAddOrgAdminDialog } = this.state;
return (
<div id="main">
<SidePanel isSidePanelClosed={isSidePanelClosed} onCloseSidePanel={this.onCloseSidePanel} />
<MainPanel>
<Router>
<OrgUsers
path={siteRoot + "org/useradmin"}
currentTab={currentTab}
tabItemClick={this.tabItemClick}
toggleAddOrgAdmin={this.toggleAddOrgAdmin}
toggleAddOrgUser={this.toggleAddOrgUser}
>
<OrgUsersList path="/" currentTab={currentTab} isShowAddOrgUserDialog={isShowAddOrgUserDialog} toggleAddOrgUser={this.toggleAddOrgUser} />
<OrgAdminList path="admins" currentTab={currentTab} isShowAddOrgAdminDialog={isShowAddOrgAdminDialog} toggleAddOrgAdmin={this.toggleAddOrgAdmin} />
</OrgUsers>
</Router>
</MainPanel>
</div>
);
}
}
ReactDOM.render(
<Org />,
document.getElementById('wrapper')
);

View File

@@ -0,0 +1,30 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Account from '../../components/common/account';
const propTypes = {
children: PropTypes.object.isRequired,
};
class MainPanel extends Component {
render() {
return (
<div className="main-panel o-hidden">
<div className="main-panel-north border-left-show">
<div className="cur-view-toolbar">
<span className="sf2-icon-menu side-nav-toggle hidden-md-up d-md-none" title="Side Nav Menu"></span>
</div>
<div className="common-toolbar">
<Account />
</div>
</div>
{this.props.children}
</div>
);
}
}
MainPanel.propTypes = propTypes;
export default MainPanel;

View File

@@ -0,0 +1,125 @@
import React from 'react';
import PropTypes from 'prop-types';
import { gettext, orgID } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import Toast from '../../components/toast';
import OrgUserInfo from '../../models/org-user';
import ModalPortal from '../../components/modal-portal';
import AddOrgAdminDialog from '../../components/dialog/org-add-admin-dialog';
import UserItem from './org-user-item';
import '../../css/org-admin-paginator.css';
const propTypes = {
toggleAddOrgAdmin: PropTypes.func.isRequired,
currentTab: PropTypes.string.isRequired,
isShowAddOrgAdminDialog: PropTypes.bool.isRequired,
};
class OrgAdminList extends React.Component {
constructor(props) {
super(props);
this.state = {
orgAdminUsers: [],
isItemFreezed: false,
};
}
componentDidMount() {
seafileAPI.listOrgUsers(orgID, true).then(res => {
let userList = res.data.user_list.map(item => {
return new OrgUserInfo(item);
});
this.setState({orgAdminUsers: userList});
});
}
onFreezedItem = () => {
this.setState({isItemFreezed: true});
}
onUnfreezedItem = () => {
this.setState({isItemFreezed: false});
}
toggleDelete = (email) => {
seafileAPI.deleteOrgUser(orgID, email).then(res => {
this.setState({
orgAdminUsers: this.state.orgAdminUsers.filter(item => item.email != email)
});
let msg = gettext('Successfully deleted %s');
msg = msg.replace('%s', email);
Toast.success(msg);
});
}
toggleRevokeAdmin = (email) => {
seafileAPI.setOrgAdmin(orgID, email, false).then(res => {
this.setState({
orgAdminUsers: this.state.orgAdminUsers.filter(item => item.email != email)
});
let msg = gettext('Successfully revoke the admin permission of %s');
msg = msg.replace('%s', email);
Toast.success(msg);
});
}
addOrgAdmin = (users) => {
seafileAPI.setOrgAdmin(orgID, users, true).then(res => {
let userInfo = new OrgUserInfo(res.data);
this.state.orgAdminUsers.unshift(userInfo);
this.setState({
orgAdminUsers: this.state.orgAdminUsers
});
this.props.toggleAddOrgAdmin();
let msg = gettext('Successfully set %s as admin.');
msg = msg.replace('%s', userInfo.email);
Toast.success(msg);
});
}
render() {
let orgAdminUsers = this.state.orgAdminUsers;
return (
<div className="cur-view-content">
<table>
<thead>
<tr>
<th width="30%">{gettext('Name')}</th>
<th width="15%">{gettext('Status')}</th>
<th width="15%">{gettext('Space Used')}</th>
<th width="20%">{gettext('Create At / Last Login')}</th>
<th width="20%" className="text-center">{gettext('Operations')}</th>
</tr>
</thead>
<tbody>
{orgAdminUsers.map(item => {
return (
<UserItem
key={item.id}
user={item}
currentTab={this.props.currentTab}
isItemFreezed={this.state.isItemFreezed}
toggleDelete={this.toggleDelete}
toggleRevokeAdmin={this.toggleRevokeAdmin}
onFreezedItem={this.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
/>
)})}
</tbody>
</table>
{this.props.isShowAddOrgAdminDialog && (
<ModalPortal>
<AddOrgAdminDialog toggle={this.props.toggleAddOrgAdmin} addOrgAdmin={this.addOrgAdmin} />
</ModalPortal>
)}
</div>
);
}
}
OrgAdminList.propTypes = propTypes;
export default OrgAdminList;

View File

@@ -0,0 +1,160 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
import { gettext, siteRoot, orgID, username } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import Toast from '../../components/toast';
import UserStatusEditor from '../../components/select-editor/user-status-editor';
const propTypes = {
currentTab: PropTypes.string,
toggleRevokeAdmin: PropTypes.func,
isItemFreezed: PropTypes.bool.isRequired,
toggleDelete: PropTypes.func.isRequired,
onFreezedItem: PropTypes.func.isRequired,
onUnfreezedItem: PropTypes.func.isRequired,
};
class UserItem extends React.Component {
constructor(props) {
super(props);
this.state = {
highlight: false,
showMenu: false,
currentStatus: this.props.user.is_active ? 'active' : 'inactive',
isItemMenuShow: false
};
this.statusArray = ['active', 'inactive'];
}
onMouseEnter = () => {
if (!this.props.isItemFreezed) {
this.setState({
showMenu: true,
highlight: true,
});
}
}
onMouseLeave = () => {
if (!this.props.isItemFreezed) {
this.setState({
showMenu: false,
highlight: false
});
}
}
toggleDelete = () => {
const email = this.props.user.email;
this.props.toggleDelete(email);
}
toggleResetPW = () => {
const email = this.props.user.email;
seafileAPI.resetOrgUserPassword(orgID, email).then(res => {
let msg;
msg = gettext('Successfully reset password to %(passwd)s for user %(user)s.');
msg = msg.replace('%(passwd)s', res.data.new_password);
msg = msg.replace('%(user)s', email);
Toast.success(msg);
});
}
toggleRevokeAdmin = () => {
const email = this.props.user.email;
this.props.toggleRevokeAdmin(email);
}
changeStatus = (st) => {
let statusCode;
if (st == 'active') {
statusCode = 1;
} else {
statusCode = 0;
}
seafileAPI.changeOrgUserStatus(this.props.user.id, statusCode).then(res => {
this.setState({
currentStatus: statusCode == 1 ? 'active' : 'inactive',
highlight: false,
showMenu: false,
});
Toast.success(gettext('Edit succeeded.'));
}).catch(err => {
Toast.danger(gettext('Edit falied.'));
});
}
onDropdownToggleClick = (e) => {
e.preventDefault();
this.toggleOperationMenu(e);
}
toggleOperationMenu = (e) => {
e.stopPropagation();
this.setState(
{isItemMenuShow: !this.state.isItemMenuShow }, () => {
if (this.state.isItemMenuShow) {
this.props.onFreezedItem();
} else {
this.setState({
highlight: false,
showMenu: false,
});
this.props.onUnfreezedItem();
}
}
);
}
render() {
let { user, currentTab } = this.props;
let href = siteRoot + 'org/useradmin/info/' + encodeURIComponent(user.email) + '/';
let isOperationMenuShow = (user.email !== username) && this.state.showMenu;
let isEditIconShow = isOperationMenuShow;
return (
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<td>
<a href={href} className="font-weight-normal">{user.name}</a>
</td>
<td>
<UserStatusEditor
isTextMode={true}
isEditIconShow={isEditIconShow}
currentStatus={this.state.currentStatus}
statusArray={this.statusArray}
onStatusChanged={this.changeStatus}
/>
</td>
<td>{user.quota ? user.self_usage + ' / ' + user.quota : user.self_usage}</td>
<td style={{'fontSize': '11px'}}>{user.ctime} / {user.last_login ? user.last_login : '--'}</td>
<td className="text-center cursor-pointer">
{isOperationMenuShow && (
<Dropdown isOpen={this.state.isItemMenuShow} toggle={this.toggleOperationMenu}>
<DropdownToggle
tag="a"
className="fas fa-ellipsis-v"
title={gettext('More Operations')}
data-toggle="dropdown"
aria-expanded={this.state.isItemMenuShow}
onClick={this.onDropdownToggleClick}
/>
<DropdownMenu>
<DropdownItem onClick={this.toggleDelete}>{gettext('Delete')}</DropdownItem>
<DropdownItem onClick={this.toggleResetPW}>{gettext('ResetPwd')}</DropdownItem>
{currentTab == 'admins' && <DropdownItem onClick={this.toggleRevokeAdmin}>{gettext('Revoke Admin')}</DropdownItem>}
</DropdownMenu>
</Dropdown>
)}
</td>
</tr>
);
}
}
UserItem.propTypes = propTypes;
export default UserItem;

View File

@@ -0,0 +1,142 @@
import React from 'react';
import PropTypes from 'prop-types';
import { gettext, orgID } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import OrgUserInfo from '../../models/org-user';
import Toast from '../../components/toast';
import ModalPortal from '../../components/modal-portal';
import AddOrgUserDialog from '../../components/dialog/org-add-user-dialog';
import UserItem from './org-user-item';
const propTypes = {
toggleAddOrgUser: PropTypes.func.isRequired,
currentTab: PropTypes.string.isRequired,
isShowAddOrgUserDialog: PropTypes.bool.isRequired,
};
class OrgUsersList extends React.Component {
constructor(props) {
super(props);
this.state = {
orgUsers: [],
isItemFreezed: false,
page: 1,
pageNext: 2,
};
}
componentDidMount() {
let page = this.state.page;
this.initData(page);
}
initData = (page) => {
seafileAPI.listOrgUsers(orgID, '', page).then(res => {
let userList = res.data.user_list.map(item => {
return new OrgUserInfo(item);
});
this.setState({
orgUsers: userList,
pageNext: res.data.page_next,
page: res.data.page,
});
});
}
onFreezedItem = () => {
this.setState({isItemFreezed: true});
}
onUnfreezedItem = () => {
this.setState({isItemFreezed: false});
}
toggleDelete = (email) => {
seafileAPI.deleteOrgUser(orgID, email).then(res => {
let users = this.state.orgUsers.filter(item => item.email != email);
this.setState({orgUsers: users});
let msg = gettext('Successfully deleted %s');
msg = msg.replace('%s', email);
Toast.success(msg);
})
}
handleSubmit = (email, name, password) => {
seafileAPI.addOrgUser(orgID, email, name, password).then(res => {
let userInfo = new OrgUserInfo(res.data);
this.state.orgUsers.unshift(userInfo);
this.setState({
orgUsers: this.state.orgUsers
});
this.props.toggleAddOrgUser();
let msg;
msg = gettext('successfully added user %s.');
msg = msg.replace('%s', email);
Toast.success(msg);
}).catch(err => {
Toast.danger(err.response.data.error_msg);
this.props.toggleAddOrgUser();
});
}
onChangePageNum = (e, num) => {
e.preventDefault();
let page = this.state.page;
if (num == 1) {
page = page + 1;
} else {
page = page - 1;
}
this.initData(page);
}
render() {
let orgUsers = this.state.orgUsers;
return (
<div className="cur-view-content">
<table>
<thead>
<tr>
<th width="30%">{gettext('Name')}</th>
<th width="15%">{gettext('Status')}</th>
<th width="15%">{gettext('Space Used')}</th>
<th width="20%">{gettext('Create At / Last Login')}</th>
<th width="20%" className="text-center">{gettext('Operations')}</th>
</tr>
</thead>
<tbody>
{orgUsers.map(item => {
return (
<UserItem
key={item.id}
user={item}
currentTab={this.props.currentTab}
isItemFreezed={this.state.isItemFreezed}
toggleDelete={this.toggleDelete}
onFreezedItem={this.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
/>
)})}
</tbody>
</table>
<div className="paginator">
{this.state.page !=1 && <a href="#" onClick={(e) => this.onChangePageNum(e, -1)}>{gettext("Previous")}{' | '}</a>}
{this.state.pageNext && <a href="#" onClick={(e) => this.onChangePageNum(e, 1)}>{gettext("Next")}</a>}
</div>
{this.props.isShowAddOrgUserDialog && (
<ModalPortal>
<AddOrgUserDialog toggle={this.props.toggleAddOrgUser} handleSubmit={this.handleSubmit} />
</ModalPortal>
)}
</div>
);
}
}
OrgUsersList.propTypes = propTypes;
export default OrgUsersList;

View File

@@ -0,0 +1,58 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Link } from '@reach/router';
import { siteRoot, gettext } from '../../utils/constants';
class OrgUsers extends Component {
constructor(props) {
super(props);
}
tabItemClick = (param) => {
this.props.tabItemClick(param);
}
toggleAddOrgUser = () => {
this.props.toggleAddOrgUser();
}
toggleAddOrgAdmin = () => {
this.props.toggleAddOrgAdmin();
}
render() {
return (
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<div className="cur-view-path org-user-nav">
<ul className="nav">
<li className="nav-item" onClick={() => this.tabItemClick('users')}>
<Link className={`nav-link ${this.props.currentTab === 'users' ? 'active': ''}`} to={siteRoot + "org/useradmin/"} title={gettext('All')}>{gettext('All')}</Link>
</li>
<li className="nav-item" onClick={() => this.tabItemClick('admins')}>
<Link className={`nav-link ${this.props.currentTab === 'admins' ? 'active': ''}`} to={siteRoot + "org/useradmin/admins/"} title={gettext('Admin')}>{gettext('Admin')}</Link>
</li>
</ul>
<div className="operation mt-1">
{this.props.currentTab === 'users' &&
<button className="btn btn-secondary operation-item" title={gettext('Add user')} onClick={this.toggleAddOrgUser}>
<i className="fas fa-plus-square text-secondary mr-1"></i>{gettext('Add user')}
</button>
}
{this.props.currentTab === 'admins' &&
<button className="btn btn-secondary operation-item" title={gettext('Add admin')} onClick={this.toggleAddOrgAdmin}>
<i className="fas fa-plus-square text-secondary mr-1"></i>{gettext('Add admin')}
</button>
}
</div>
</div>
{this.props.children}
</div>
</div>
);
}
}
export default OrgUsers;

View File

@@ -0,0 +1,78 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from '@reach/router';
import Logo from '../../components/logo';
import { gettext, siteRoot } from '../../utils/constants';
const propTypes = {
isSidePanelClosed: PropTypes.bool.isRequired,
onCloseSidePanel: PropTypes.func.isRequired,
};
class SidePanel extends React.Component {
render() {
return (
<div className={`side-panel ${this.props.isSidePanelClosed ? '' : 'left-zero'}`}>
<div className="side-panel-north">
<Logo onCloseSidePanel={this.props.onCloseSidePanel}/>
</div>
<div className="side-panel-center">
<div className="side-nav">
<div className="side-nav-con">
<h3 className="sf-heading" style={{ 'color': '#f7941d' }}>{gettext('Admin')}</h3>
<ul className="nav nav-pills flex-column nav-container">
<li className="nav-item">
<a className='nav-link ellipsis' href={siteRoot + "org/orgmanage/"}>
<span className="sf2-icon-monitor" aria-hidden="true"></span>
<span className="nav-text">{gettext('Info')}</span>
</a>
</li>
<li className="nav-item">
<a className='nav-link ellipsis' href={siteRoot + "org/repoadmin/"}>
<span className="sf2-icon-library"></span>
<span className="nav-text">{gettext('Libraries')}</span>
</a>
</li>
<li className="nav-item">
<Link className='nav-link ellipsis active' to={siteRoot + "org/useradmin/"}>
<span className="sf2-icon-user"></span>
<span className="nav-text">{gettext('Users')}</span>
</Link>
</li>
<li className="nav-item">
<a href="/org/admin/#address-book/" className="nav-link ellipsis">
<span className="sf2-icon-organization"></span>
<span className="nav-text">{gettext('Departments')}</span>
</a>
</li>
<li className="nav-item">
<a href="/org/groupadmin/" className="nav-link ellipsis">
<span className="sf2-icon-group"></span>
<span className="nav-text">{gettext('Groups')}</span>
</a>
</li>
<li className="nav-item">
<a href="/org/publinkadmin/" className="nav-link ellipsis">
<span className="sf2-icon-link"></span>
<span className="nav-text">{gettext('Links')}</span>
</a>
</li>
<li className="nav-item">
<a href="/org/file-audit-admin/" className="nav-link ellipsis">
<span className="sf2-icon-clock"></span>
<span className="nav-text">{gettext('Logs')}</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
);
}
}
SidePanel.propTypes = propTypes;
export default SidePanel;

View File

@@ -75,3 +75,6 @@ export const author = window.draftReview ? window.draftReview.config.author : ''
export const authorAvatar = window.draftReview ? window.draftReview.config.authorAvatar : ''; export const authorAvatar = window.draftReview ? window.draftReview.config.authorAvatar : '';
export const originFileExists = window.draftReview ? window.draftReview.config.originFileExists : ''; export const originFileExists = window.draftReview ? window.draftReview.config.originFileExists : '';
export const draftFileExists = window.draftReview ? window.draftReview.config.draftFileExists : ''; export const draftFileExists = window.draftReview ? window.draftReview.config.draftFileExists : '';
// org admin
export const orgID = window.org ? window.org.pageOptions.orgID : '';

View File

@@ -93,6 +93,7 @@
.sf2-icon-readme:before {content:"\e039"} .sf2-icon-readme:before {content:"\e039"}
.sf2-icon-drafts:before {content:"\e03a"} .sf2-icon-drafts:before {content:"\e03a"}
.sf2-icon-recycle:before {content:"\e03b"} .sf2-icon-recycle:before {content:"\e03b"}
.sf2-icon-library:before { content:"\e00d"; }
/* common class and element style*/ /* common class and element style*/
a { color:#eb8205; } a { color:#eb8205; }

View File

@@ -94,3 +94,12 @@ class IsProVersion(BasePermission):
def has_permission(self, request, *args, **kwargs): def has_permission(self, request, *args, **kwargs):
return is_pro_version() return is_pro_version()
class IsOrgAdminUser(BasePermission):
"""
Check whether user is org admin
"""
def has_permission(self, request, view, obj=None):
org_id = int(view.kwargs.get('org_id', ''))
return True if request.user.org.is_staff and \
request.user.org.org_id == org_id else False