1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-18 16:36:15 +00:00

Merge branch '8.0' into master

This commit is contained in:
lian
2021-08-06 13:07:36 +08:00
27 changed files with 876 additions and 63 deletions

View File

@@ -3762,11 +3762,11 @@
}
},
"@seafile/seafile-editor": {
"version": "0.3.63",
"resolved": "https://registry.npmjs.org/@seafile/seafile-editor/-/seafile-editor-0.3.63.tgz",
"integrity": "sha512-mzqdQlyfs6MpN4n263GtKI6nlYwbVAVO+fj+Q28CiZH8uMwMsa6e9KcQn0yyJWAvxEy6i/L1kWpgztcZFV42Ow==",
"version": "0.3.72",
"resolved": "https://registry.npmjs.org/@seafile/seafile-editor/-/seafile-editor-0.3.72.tgz",
"integrity": "sha512-fV7Arc6aIAGKCxbQ+QNhLiieafJefQsCnaXrGlyjWzLF46T8ivRMPZxfik6YZqQLt165JtJ1SET6dEgK7QBlYA==",
"requires": {
"@seafile/slate-react": "^0.54.12",
"@seafile/slate-react": "^0.54.13",
"codemirror": "^5.37.0",
"crypto-js": "^3.1.9-1",
"deep-equal": "^1.0.1",
@@ -3856,9 +3856,9 @@
}
},
"@seafile/slate-react": {
"version": "0.54.12",
"resolved": "https://registry.npmjs.org/@seafile/slate-react/-/slate-react-0.54.12.tgz",
"integrity": "sha512-AYV7u8zq/ztnJuMY5Vic9uaPeAJUVGXt9zkTU05Jh/j9eEr4rzuGUbJIoRVHbTFckAOn+spvUNUpCYME5OF+gg==",
"version": "0.54.13",
"resolved": "https://registry.npmjs.org/@seafile/slate-react/-/slate-react-0.54.13.tgz",
"integrity": "sha512-NjaY2EXwgMMJFJp2523w2+hK2S+dAWbHCMXadhuuM0S7ZzrerO86XA2N4ZBsaTwQqzJiEJBjg6+I/szD0HaNEA==",
"requires": {
"@types/debounce": "^1.2.0",
"@types/debug": "^4.1.5",
@@ -4270,9 +4270,9 @@
"integrity": "sha512-bWG5wapaWgbss9E238T0R6bfo5Fh3OkeoSt245CM7JJwVwpw6MEBCbIxLq5z8KzsE3uJhzcIuQkyiZmzV3M/Dw=="
},
"@types/debug": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
"integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ=="
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.6.tgz",
"integrity": "sha512-7fDOJFA/x8B+sO1901BmHlf5dE1cxBU8mRXj8QOEDnn16hhGJv/IHxJtZhvsabZsIMn0eLIyeOKAeqSNJJYTpA=="
},
"@types/eslint": {
"version": "7.2.8",
@@ -4321,9 +4321,9 @@
"dev": true
},
"@types/is-hotkey": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.2.tgz",
"integrity": "sha512-SUw9LpI3AIwbRNXS7FYy9AlXrTPIdBZGI7y4XxfIEYqgSW1UfFCUM9cMwHE/yCfTl0qeI0UQ/q8TdIxsIFgKPg=="
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.3.tgz",
"integrity": "sha512-Hz+eHHpMWLBX1CpDXSuQre9nYXN2e2VGVHvkkldxDzo9eFtRpHm5iOlJlZvnNGvele5584cUSkRnFRQb+Wcu0w=="
},
"@types/istanbul-lib-coverage": {
"version": "2.0.3",
@@ -4372,9 +4372,9 @@
"dev": true
},
"@types/lodash": {
"version": "4.14.168",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz",
"integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q=="
"version": "4.14.171",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.171.tgz",
"integrity": "sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg=="
},
"@types/minimatch": {
"version": "3.0.4",
@@ -6956,9 +6956,9 @@
"integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw=="
},
"commander": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.0.0.tgz",
"integrity": "sha512-Xvf85aAtu6v22+E5hfVoLHqyul/jyxh91zvqk/ioJTQuJR7Z78n7H558vMPKanPSRgIEeZemT92I2g9Y8LPbSQ=="
},
"common-tags": {
"version": "1.8.0",
@@ -7433,7 +7433,7 @@
},
"css-in-js-utils": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz",
"resolved": "http://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz",
"integrity": "sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA==",
"requires": {
"hyphenate-style-name": "^1.0.2",
@@ -13957,13 +13957,14 @@
}
},
"mathjax-full": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.1.2.tgz",
"integrity": "sha512-jFCwRFdFwIOa8J7r6VZT0AIv9ZwbLQ9aPc9YZp695NTvv7XKU2NunJodA+zDWzElIFJ7mTsImyfe5R3QyRNZjw==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.0.tgz",
"integrity": "sha512-D2EBNvUG+mJyhn+M1C858k0f2Fc4KxXvbEX2WCMXroV10212JwfYqaBJ336ECBSz5X9L5LRoamxb7AJtg3KaJA==",
"requires": {
"esm": "^3.2.25",
"mhchemparser": "^4.1.0",
"mj-context-menu": "^0.6.1",
"speech-rule-engine": "^3.1.1"
"speech-rule-engine": "^3.3.3"
}
},
"md5.js": {
@@ -14077,6 +14078,11 @@
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
"dev": true
},
"mhchemparser": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.1.1.tgz",
"integrity": "sha512-R75CUN6O6e1t8bgailrF1qPq+HhVeFTM3XQ0uzI+mXTybmphy3b6h4NbLOYhemViQ3lUs+6CKRkC3Ws1TlYREA=="
},
"microevent.ts": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz",
@@ -19187,11 +19193,11 @@
}
},
"speech-rule-engine": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-3.1.1.tgz",
"integrity": "sha512-FGX8B44yI3yGhmcw8nZ/by2ffUlZG6m5b/O3RULXsSiwhL/evL+jwQ6BXQxV3gGtOYptOFalTVCAFknAJgBKAg==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-3.3.3.tgz",
"integrity": "sha512-0exWw+0XauLjat+f/aFeo5T8SiDsO1JtwpY3qgJE4cWt+yL/Stl0WP4VNDWdh7lzGkubUD9lWP4J1ASnORXfyQ==",
"requires": {
"commander": "^6.0.0",
"commander": ">=7.0.0",
"wicked-good-xpath": "^1.3.0",
"xmldom-sre": "^0.1.31"
}

View File

@@ -7,7 +7,7 @@
"@seafile/react-image-lightbox": "0.0.1",
"@seafile/resumablejs": "1.1.16",
"@seafile/seafile-calendar": "0.0.12",
"@seafile/seafile-editor": "^0.3.63",
"@seafile/seafile-editor": "^0.3.72",
"MD5": "^1.3.0",
"chart.js": "2.9.4",
"classnames": "^2.2.6",
@@ -35,7 +35,7 @@
"react-responsive": "^6.1.2",
"react-select": "^2.4.1",
"reactstrap": "^6.4.0",
"seafile-js": "0.2.173",
"seafile-js": "0.2.175",
"socket.io-client": "^2.2.0",
"unified": "^7.0.0",
"url-parse": "^1.4.3",

View File

@@ -61,13 +61,21 @@ class ZipDownloadDialog extends React.Component {
const zipToken = this.state.zipToken;
seafileAPI.queryZipProgress(zipToken).then((res) => {
const data = res.data;
this.setState({
zipProgress: data.total == 0 ? '100%' : (data.zipped/data.total*100).toFixed(2) + '%'
});
if (data['total'] == data['zipped']) {
if (data.failed == 1) {
clearInterval(interval);
this.props.toggleDialog();
location.href = `${fileServerRoot}zip/${zipToken}`;
this.setState({
isLoading: false,
errorMsg: data.failed_reason
});
} else {
this.setState({
zipProgress: data.total == 0 ? '100%' : (data.zipped/data.total*100).toFixed(2) + '%'
});
if (data['total'] == data['zipped']) {
clearInterval(interval);
this.props.toggleDialog();
location.href = `${fileServerRoot}zip/${zipToken}`;
}
}
}).catch((error) => {
clearInterval(interval);

View File

@@ -107,7 +107,7 @@ class FileToolbar extends React.Component {
{(canEditFile && !err) &&
( this.props.isSaving ?
<button type={'button'} className={'btn btn-icon btn-secondary btn-active'}>
<button type={'button'} aria-label={gettext('Saving...')} className={'btn btn-icon btn-secondary btn-active'}>
<i className={'fa fa-spin fa-spinner'}/></button> :
(
this.props.needSave ?
@@ -149,7 +149,7 @@ class FileToolbar extends React.Component {
/>
)}
<ButtonDropdown isOpen={moreDropdownOpen} toggle={this.toggleMoreOpMenu}>
<DropdownToggle>
<DropdownToggle aria-label={gettext('More Operations')}>
<span className="fas fa-ellipsis-v"></span>
</DropdownToggle>
<DropdownMenu right={true}>
@@ -172,8 +172,8 @@ class FileToolbar extends React.Component {
<Dropdown isOpen={this.state.dropdownOpen} toggle={this.toggle} className="d-block d-md-none">
<ButtonGroup >
{(canEditFile && !err) &&
( this.props.isSaving ?
<button type={'button'} className={'btn btn-icon btn-secondary btn-active'}>
(this.props.isSaving ?
<button type={'button'} aria-label={gettext('Saving...')} className={'btn btn-icon btn-secondary btn-active'}>
<i className={'fa fa-spin fa-spinner'}/></button> :
(
this.props.needSave ?
@@ -192,7 +192,7 @@ class FileToolbar extends React.Component {
)}
</ButtonGroup>
<DropdownToggle className="sf2-icon-more mx-1">
<DropdownToggle className="sf2-icon-more mx-1" aria-label={gettext('More Operations')}>
</DropdownToggle>
<DropdownMenu right={true}>
<DropdownItem>

View File

@@ -47,6 +47,7 @@ class IconButton extends React.Component {
className={className}
tag="a"
href={this.props.href}
aria-label={this.props.text}
>
{btnContent}
</Button>
@@ -57,6 +58,7 @@ class IconButton extends React.Component {
id={this.props.id}
className={className}
onClick={this.props.onClick}
aria-label={this.props.text}
>
{btnContent}
</Button>

View File

@@ -95,6 +95,7 @@ class SelectEditor extends React.Component {
className="permission-editor-select"
classNamePrefix="permission-editor"
placeholder={this.props.translateOption(currentOption)}
value={currentOption}
onChange={this.onOptionChanged}
captureMenuScroll={false}
/>

View File

@@ -0,0 +1,41 @@
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,
statusOptions: PropTypes.array.isRequired,
currentStatus: PropTypes.string.isRequired,
onStatusChanged: PropTypes.func.isRequired
};
class SysAdminUserMembershipEditor extends React.Component {
translateStatus = (status) => {
switch (status) {
case 'is_org_staff':
return gettext('Admin');
case 'not_is_org_staff':
return gettext('Member');
}
}
render() {
return (
<SelectEditor
isTextMode={this.props.isTextMode}
isEditIconShow={this.props.isEditIconShow}
options={this.props.statusOptions}
currentOption={this.props.currentStatus}
onOptionChanged={this.props.onStatusChanged}
translateOption={this.translateStatus}
/>
);
}
}
SysAdminUserMembershipEditor.propTypes = propTypes;
export default SysAdminUserMembershipEditor;

View File

@@ -72,7 +72,7 @@ class MoreMenu extends React.PureComponent {
const isSmall = this.props.isSmallScreen;
return (
<Dropdown isOpen={this.state.dropdownOpen} toggle={this.dropdownToggle} direction="down" className="mx-1">
<DropdownToggle id="moreButton">
<DropdownToggle id="moreButton" aria-label={gettext('More Operations')}>
<i className="fa fa-ellipsis-v"/>
<Tooltip toggle={this.tooltipToggle} delay={{show: 0, hide: 0}} target="moreButton" placement='bottom' isOpen={this.state.tooltipOpen}>{gettext('More')}
</Tooltip>
@@ -179,11 +179,11 @@ class MarkdownViewerToolbar extends React.Component {
<IconButton id={'shareBtn'} text={gettext('Share')} icon={'fa fa-share-alt'}
onMouseDown={this.props.toggleShareLinkDialog}/>
}
{ saving ?
<button type={'button'} className={'btn btn-icon btn-secondary btn-active'}>
{saving ?
<button type={'button'} aria-label={gettext('Saving...')} className={'btn btn-icon btn-secondary btn-active'}>
<i className={'fa fa-spin fa-spinner'}/></button>
:
<IconButton text={gettext('Save')} id={'saveButton'} icon={'fa fa-save'} disabled={!contentChanged}
<IconButton text={gettext('Save')} id={'saveButton'} icon={'fa fa-save'} disabled={!contentChanged}
onMouseDown={window.seafileEditor && window.seafileEditor.onRichEditorSave} isActive={contentChanged}/>
}
{canDownloadFile && (
@@ -224,7 +224,7 @@ class MarkdownViewerToolbar extends React.Component {
<div className="topbar-btn-container">
<ButtonGroup>
{saving ?
<button type={'button'} className={'btn btn-icon btn-secondary btn-active'}>
<button type={'button'} aria-label={gettext('Saving...')} className={'btn btn-icon btn-secondary btn-active'}>
<i className={'fa fa-spin fa-spinner'}/></button>
:
<IconButton text={gettext('Save')} id={'saveButton'} icon={'fa fa-save'} disabled={!contentChanged}

View File

@@ -56,7 +56,12 @@ img[src=""] {
display:flex;
flex-direction:column;
overflow:hidden;
z-index: 1051; /* for mobile */
}
@media (max-width: 767px) {
.wiki-side-panel {
z-index: 1051;
}
}
.wiki-main-panel {

View File

@@ -4,11 +4,13 @@ import { Router } from '@reach/router';
import { siteRoot } from '../../utils/constants';
import SidePanel from './side-panel';
import OrgUsers from './org-users-users';
import OrgUsersSearchUsers from './org-users-search-users';
import OrgAdmins from './org-users-admins';
import OrgUserProfile from './org-user-profile';
import OrgUserRepos from './org-user-repos';
import OrgUserSharedRepos from './org-user-shared-repos';
import OrgGroups from './org-groups';
import OrgGroupsSearchGroups from './org-groups-search-groups';
import OrgGroupInfo from './org-group-info';
import OrgGroupRepos from './org-group-repos';
import OrgGroupMembers from './org-group-members';
@@ -68,11 +70,13 @@ class Org extends React.Component {
<Router className="reach-router">
<OrgInfo path={siteRoot + 'org/orgmanage'}/>
<OrgUsers path={siteRoot + 'org/useradmin'} />
<OrgUsersSearchUsers path={siteRoot + 'org/useradmin/search-users'} />
<OrgAdmins path={siteRoot + 'org/useradmin/admins/'} />
<OrgUserProfile path={siteRoot + 'org/useradmin/info/:email/'} />
<OrgUserRepos path={siteRoot + 'org/useradmin/info/:email/repos/'} />
<OrgUserSharedRepos path={siteRoot + 'org/useradmin/info/:email/shared-repos/'} />
<OrgGroups path={siteRoot + 'org/groupadmin'} />
<OrgGroupsSearchGroups path={siteRoot + 'org/groupadmin/search-groups'} />
<OrgGroupInfo path={siteRoot + 'org/groupadmin/:groupID/'} />
<OrgGroupRepos path={siteRoot + 'org/groupadmin/:groupID/repos/'} />
<OrgGroupMembers path={siteRoot + 'org/groupadmin/:groupID/members/'} />

View File

@@ -18,6 +18,7 @@ class MainPanelTopbar extends Component {
</div>
</div>
<div className="common-toolbar">
{this.props.search && this.props.search}
<Account isAdminPanel={true}/>
</div>
</div>

View File

@@ -0,0 +1,279 @@
import React, { Component, Fragment } from 'react';
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
import { Button, Form, FormGroup, Input, Col } from 'reactstrap';
import { Utils } from '../../utils/utils';
import { seafileAPI } from '../../utils/seafile-api';
import { gettext, orgID, siteRoot } from '../../utils/constants';
import toaster from '../../components/toast';
import OrgGroupInfo from '../../models/org-group';
class GroupItem extends React.Component {
constructor(props) {
super(props);
this.state = {
highlight: false,
showMenu: false,
isItemMenuShow: false
};
}
onMouseEnter = () => {
if (!this.props.isItemFreezed) {
this.setState({
showMenu: true,
highlight: true,
});
}
}
onMouseLeave = () => {
if (!this.props.isItemFreezed) {
this.setState({
showMenu: false,
highlight: false
});
}
}
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();
}
}
);
}
toggleDelete = () => {
this.props.deleteGroupItem(this.props.group);
}
renderGroupHref = (group) => {
let groupInfoHref;
if (group.creatorName == 'system admin') {
groupInfoHref = siteRoot + 'org/departmentadmin/groups/' + group.id + '/';
} else {
groupInfoHref = siteRoot + 'org/groupadmin/' + group.id + '/';
}
return groupInfoHref;
}
renderGroupCreator = (group) => {
let userInfoHref = siteRoot + 'org/useradmin/info/' + group.creatorEmail + '/';
if (group.creatorName == 'system admin') {
return (
<td> -- </td>
);
} else {
return(
<td>
<a href={userInfoHref} className="font-weight-normal">{group.creatorName}</a>
</td>
);
}
}
render() {
let { group } = this.props;
let isOperationMenuShow = (group.creatorName != 'system admin') && this.state.showMenu;
return (
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<td>
<a href={this.renderGroupHref(group)} className="font-weight-normal">{group.groupName}</a>
</td>
{this.renderGroupCreator(group)}
<td>{group.ctime}</td>
<td className="text-center cursor-pointer">
{isOperationMenuShow &&
<Dropdown isOpen={this.state.isItemMenuShow} toggle={this.toggleOperationMenu}>
<DropdownToggle
tag="a"
className="attr-action-icon 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>
</DropdownMenu>
</Dropdown>
}
</td>
</tr>
);
}
}
class OrgGroupsSearchGroupsResult extends React.Component {
constructor(props) {
super(props);
this.state = {
isItemFreezed: false
};
}
onFreezedItem = () => {
this.setState({isItemFreezed: true});
}
onUnfreezedItem = () => {
this.setState({isItemFreezed: false});
}
render() {
let { orgGroups } = this.props;
return (
<div className="cur-view-content">
<table>
<thead>
<tr>
<th width="30%">{gettext('Name')}</th>
<th width="35%">{gettext('Creator')}</th>
<th width="23%">{gettext('Created At')}</th>
<th width="12%" className="text-center">{gettext('Operations')}</th>
</tr>
</thead>
<tbody>
{orgGroups.map(item => {
return (
<GroupItem
key={item.id}
group={item}
isItemFreezed={this.state.isItemFreezed}
onFreezedItem={this.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
deleteGroupItem={this.props.toggleDelete}
/>
);
})}
</tbody>
</table>
</div>
);
}
}
class OrgGroupsSearchGroups extends Component {
constructor(props) {
super(props);
this.state = {
query: '',
orgGroups: [],
isSubmitBtnActive: false,
loading: true,
errorMsg: '',
};
}
componentDidMount () {
let params = (new URL(document.location)).searchParams;
this.setState({
query: params.get('query') || '',
}, () => {this.getItems();});
}
getItems = () => {
seafileAPI.orgAdminSearchGroup(orgID, this.state.query.trim()).then(res => {
let groupList = res.data.group_list.map(item => {
return new OrgGroupInfo(item);
});
this.setState({
orgGroups: groupList,
loading: false,
});
}).catch((error) => {
this.setState({
loading: false,
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
});
});
}
deleteGroupItem = (group) => {
seafileAPI.orgAdminDeleteOrgGroup(orgID, group.id).then(res => {
this.setState({
orgGroups: this.state.orgGroups.filter(item => item.id != group.id)
});
let msg = gettext('Successfully deleted {name}');
msg = msg.replace('{name}', group.groupName);
toaster.success(msg);
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
handleInputChange = (e) => {
this.setState({
query: e.target.value
}, this.checkSubmitBtnActive);
}
checkSubmitBtnActive = () => {
const { query } = this.state;
this.setState({
isSubmitBtnActive: query.trim()
});
}
render() {
const { query, isSubmitBtnActive } = this.state;
return (
<Fragment>
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<div className="cur-view-path">
<h3 className="sf-heading">{gettext('Groups')}</h3>
</div>
<div className="cur-view-content">
<div className="mt-4 mb-6">
<h4 className="border-bottom font-weight-normal mb-2 pb-1">{gettext('Search Groups')}</h4>
<Form>
<FormGroup row>
<Col sm={5}>
<Input type="text" name="query" value={query} placeholder={gettext('Search groups')} onChange={this.handleInputChange} />
</Col>
</FormGroup>
<FormGroup row>
<Col sm={{size: 5}}>
<button className="btn btn-outline-primary" disabled={!isSubmitBtnActive} onClick={this.getItems}>{gettext('Submit')}</button>
</Col>
</FormGroup>
</Form>
</div>
<div className="mt-4 mb-6">
<h4 className="border-bottom font-weight-normal mb-2 pb-1">{gettext('Result')}</h4>
<OrgGroupsSearchGroupsResult
toggleDelete={this.deleteGroupItem}
orgGroups={this.state.orgGroups}
/>
</div>
</div>
</div>
</div>
</Fragment>
);
}
}
export default OrgGroupsSearchGroups;

View File

@@ -1,4 +1,5 @@
import React, { Component, Fragment } from 'react';
import { navigate } from '@reach/router';
import PropTypes from 'prop-types';
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
import { siteRoot, gettext, orgID } from '../../utils/constants';
@@ -8,6 +9,55 @@ import toaster from '../../components/toast';
import OrgGroupInfo from '../../models/org-group';
import MainPanelTopbar from './main-panel-topbar';
class Search extends React.Component {
constructor(props) {
super(props);
this.state = {
value: ''
};
}
handleInputChange = (e) => {
this.setState({
value: e.target.value
});
}
handleKeyPress = (e) => {
if (e.key == 'Enter') {
e.preventDefault();
this.handleSubmit();
}
}
handleSubmit = () => {
const value = this.state.value.trim();
if (!value) {
return false;
}
this.props.submit(value);
}
render() {
return (
<div className="input-icon">
<i className="d-flex input-icon-addon fas fa-search"></i>
<input
type="text"
className="form-control search-input h-6 mr-1"
style={{width: '15rem'}}
placeholder={this.props.placeholder}
value={this.state.value}
onChange={this.handleInputChange}
onKeyPress={this.handleKeyPress}
autoComplete="off"
/>
</div>
);
}
}
class OrgGroups extends Component {
constructor(props) {
@@ -77,11 +127,22 @@ class OrgGroups extends Component {
});
}
searchItems = (keyword) => {
navigate(`${siteRoot}org/groupadmin/search-groups/?query=${encodeURIComponent(keyword)}`);
}
getSearch = () => {
return <Search
placeholder={gettext('Search groups by name')}
submit={this.searchItems}
/>;
}
render() {
let groups = this.state.orgGroups;
return (
<Fragment>
<MainPanelTopbar/>
<MainPanelTopbar search={this.getSearch()}/>
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<div className="cur-view-path">

View File

@@ -0,0 +1,186 @@
import React, { Component, Fragment } from 'react';
import { Button, Form, FormGroup, Input, Col } from 'reactstrap';
import { Utils } from '../../utils/utils';
import { seafileAPI } from '../../utils/seafile-api';
import { gettext, orgID } from '../../utils/constants';
import toaster from '../../components/toast';
import UserItem from './org-user-item';
import OrgUserInfo from '../../models/org-user';
class OrgUsersSearchUsersResult extends React.Component {
constructor(props) {
super(props);
this.state = {
isItemFreezed: false
};
}
onFreezedItem = () => {
this.setState({isItemFreezed: true});
}
onUnfreezedItem = () => {
this.setState({isItemFreezed: false});
}
render() {
let { orgUsers } = this.props;
return (
<div className="cur-view-content">
<table>
<thead>
<tr>
<th width="30%">{gettext('Name')}</th>
<th width="15%">{gettext('Status')}</th>
<th width="20%">
<a className="d-inline-block table-sort-op" href="#" >{gettext('Space Used')}</a> / {gettext('Quota')}
</th>
<th width="25%">{gettext('Created At')} / {gettext('Last Login')}</th>
<th width="10%">{/*Operations*/}</th>
</tr>
</thead>
<tbody>
{orgUsers.map((item, index) => {
return (
<UserItem
key={index}
user={item}
currentTab="users"
isItemFreezed={this.state.isItemFreezed}
toggleDelete={this.props.toggleDelete}
onFreezedItem={this.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
/>
);})}
</tbody>
</table>
</div>
);
}
}
class OrgUsersSearchUsers extends Component {
constructor(props) {
super(props);
this.state = {
query: '',
orgUsers: [],
org_id: '',
isSubmitBtnActive: false,
loading: true,
errorMsg: '',
};
}
componentDidMount () {
let params = (new URL(document.location)).searchParams;
this.setState({
query: params.get('query') || '',
}, () => {this.getItems();});
}
getItems = () => {
seafileAPI.orgAdminSearchUser(orgID, this.state.query.trim()).then(res => {
let userList = res.data.user_list.map(item => {
return new OrgUserInfo(item);
});
this.setState({
orgUsers: userList,
loading: false,
});
}).catch((error) => {
this.setState({
loading: false,
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
});
});
}
deleteUser = (email) => {
seafileAPI.orgAdminDeleteOrgUser(orgID, email).then(res => {
let newUserList = this.state.orgUsers.filter(item => {
return item.email != email;
});
this.setState({orgUsers: newUserList});
toaster.success(gettext('Successfully deleted 1 item.'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
updateUser = (email, key, value) => {
seafileAPI.sysAdminUpdateUser(email, key, value).then(res => {
let newUserList = this.state.orgUsers.map(item => {
if (item.email == email) {
item[key]= res.data[key];
}
return item;
});
this.setState({orgUsers: newUserList});
const msg = (key == 'is_active' && value) ?
res.data.update_status_tip : gettext('Edit succeeded');
toaster.success(msg);
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
handleInputChange = (e) => {
this.setState({
query: e.target.value
}, this.checkSubmitBtnActive);
}
checkSubmitBtnActive = () => {
const { query } = this.state;
this.setState({
isSubmitBtnActive: query.trim()
});
}
render() {
const { query, isSubmitBtnActive } = this.state;
return (
<Fragment>
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<div className="cur-view-path">
<h3 className="sf-heading">{gettext('Users')}</h3>
</div>
<div className="cur-view-content">
<div className="mt-4 mb-6">
<h4 className="border-bottom font-weight-normal mb-2 pb-1">{gettext('Search Users')}</h4>
<Form>
<FormGroup row>
<Col sm={5}>
<Input type="text" name="query" value={query} placeholder={gettext('Search users')} onChange={this.handleInputChange} />
</Col>
</FormGroup>
<FormGroup row>
<Col sm={{size: 5}}>
<button className="btn btn-outline-primary" disabled={!isSubmitBtnActive} onClick={this.getItems}>{gettext('Submit')}</button>
</Col>
</FormGroup>
</Form>
</div>
<div className="mt-4 mb-6">
<h4 className="border-bottom font-weight-normal mb-2 pb-1">{gettext('Result')}</h4>
<OrgUsersSearchUsersResult
toggleDelete={this.deleteUser}
orgUsers={this.state.orgUsers}
/>
</div>
</div>
</div>
</div>
</Fragment>
);
}
}
export default OrgUsersSearchUsers;

View File

@@ -9,9 +9,58 @@ import InviteUserDialog from '../../components/dialog/org-admin-invite-user-dial
import toaster from '../../components/toast';
import { seafileAPI } from '../../utils/seafile-api';
import OrgUserInfo from '../../models/org-user';
import { gettext, invitationLink, orgID } from '../../utils/constants';
import { gettext, invitationLink, orgID, siteRoot } from '../../utils/constants';
import { Utils } from '../../utils/utils';
class Search extends React.Component {
constructor(props) {
super(props);
this.state = {
value: ''
};
}
handleInputChange = (e) => {
this.setState({
value: e.target.value
});
}
handleKeyPress = (e) => {
if (e.key == 'Enter') {
e.preventDefault();
this.handleSubmit();
}
}
handleSubmit = () => {
const value = this.state.value.trim();
if (!value) {
return false;
}
this.props.submit(value);
}
render() {
return (
<div className="input-icon">
<i className="d-flex input-icon-addon fas fa-search"></i>
<input
type="text"
className="form-control search-input h-6 mr-1"
style={{width: '15rem'}}
placeholder={this.props.placeholder}
value={this.state.value}
onChange={this.handleInputChange}
onKeyPress={this.handleKeyPress}
autoComplete="off"
/>
</div>
);
}
}
class OrgUsers extends Component {
constructor(props) {
@@ -117,6 +166,17 @@ class OrgUsers extends Component {
});
}
searchItems = (keyword) => {
navigate(`${siteRoot}org/useradmin/search-users/?query=${encodeURIComponent(keyword)}`);
}
getSearch = () => {
return <Search
placeholder={gettext('Search users')}
submit={this.searchItems}
/>;
}
render() {
const topBtn = 'btn btn-secondary operation-item';
let topbarChildren;
@@ -143,7 +203,7 @@ class OrgUsers extends Component {
return (
<Fragment>
<MainPanelTopbar children={topbarChildren}/>
<MainPanelTopbar children={topbarChildren} search={this.getSearch()}/>
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<Nav currentItem="all" />

View File

@@ -8,6 +8,7 @@ import toaster from '../../../components/toast';
import EmptyTip from '../../../components/empty-tip';
import Loading from '../../../components/loading';
import SysAdminUserStatusEditor from '../../../components/select-editor/sysadmin-user-status-editor';
import SysAdminUserMembershipEditor from '../../../components/select-editor/sysadmin-user-membership-editor';
import SysAdminAddUserDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-add-user-dialog';
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
import OpMenu from '../../../components/dialog/op-menu';
@@ -50,9 +51,10 @@ class Content extends Component {
<thead>
<tr>
<th width="25%">{gettext('Name')}</th>
<th width="20%">{gettext('Status')}</th>
<th width="20%">{gettext('Space Used')}</th>
<th width="30%">{gettext('Created At')}{' / '}{gettext('Last Login')}</th>
<th width="15%">{gettext('Status')}</th>
<th width="15%">{gettext('Membership')}</th>
<th width="15%">{gettext('Space Used')}</th>
<th width="25%">{gettext('Created At')}{' / '}{gettext('Last Login')}</th>
<th width="5%">{/* Operations */}</th>
</tr>
</thead>
@@ -65,6 +67,7 @@ class Content extends Component {
onFreezedItem={this.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
updateStatus={this.props.updateStatus}
updateMembership={this.props.updateMembership}
deleteUser={this.props.deleteUser}
/>);
})}
@@ -146,6 +149,10 @@ class Item extends Component {
this.props.updateStatus(this.props.item.email, statusValue);
}
updateMembership= (membershipValue) => {
this.props.updateMembership(this.props.item.email, membershipValue);
}
deleteUser = () => {
const { item } = this.props;
this.props.deleteUser(item.org_id, item.email);
@@ -195,6 +202,15 @@ class Item extends Component {
onStatusChanged={this.updateStatus}
/>
</td>
<td>
<SysAdminUserMembershipEditor
isTextMode={true}
isEditIconShow={isOpIconShown}
currentStatus={item.is_org_staff ? 'is_org_staff' : 'not_is_org_staff'}
statusOptions={['is_org_staff', 'not_is_org_staff']}
onStatusChanged={this.updateMembership}
/>
</td>
<td>{`${Utils.bytesToSize(item.quota_usage)} / ${item.quota_total > 0 ? Utils.bytesToSize(item.quota_total) : '--'}`}</td>
<td>
{moment(item.create_time).format('YYYY-MM-DD HH:mm:ss')}{' / '}{item.last_login ? moment(item.last_login).fromNow() : '--'}
@@ -311,6 +327,22 @@ class OrgUsers extends Component {
});
}
updateMembership = (email, membershipValue) => {
const isOrgStaff = membershipValue == 'is_org_staff';
seafileAPI.sysAdminUpdateOrgUser(this.props.orgID, email, 'is_org_staff', isOrgStaff).then(res => {
let newUserList = this.state.userList.map(item => {
if (item.email == email) {
item.is_org_staff = res.data.is_org_staff;
}
return item;
});
this.setState({userList: newUserList});
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
render() {
const { isAddUserDialogOpen, orgName } = this.state;
return (
@@ -331,6 +363,7 @@ class OrgUsers extends Component {
errorMsg={this.state.errorMsg}
items={this.state.userList}
updateStatus={this.updateStatus}
updateMembership={this.updateMembership}
deleteUser={this.deleteUser}
/>
</div>