1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-04 16:31:13 +00:00

Change select group UI (#7201)

* 01 share repo to group UI

* 02 ADD_SHARED_REPO_INTO_GROUP

* 03 share folder to groups

* 04 system admin share repo to group

* 05 remove old NoGroupMessage

* 06 change API

* change API

* change select icons indents
This commit is contained in:
Michael An
2024-12-19 09:53:32 +08:00
committed by GitHub
parent 68f791bb32
commit fc139a83fd
16 changed files with 774 additions and 90 deletions

View File

@@ -5,7 +5,7 @@ export const EVENT_BUS_TYPE = {
// group
ADD_NEW_GROUP: 'add_new_group',
ADD_SHARED_REPO_INO_GROUP: 'add_shared_repo_into_group',
ADD_SHARED_REPO_INTO_GROUP: 'add_shared_repo_into_group',
UNSHARE_REPO_TO_GROUP: 'unshare_repo_to_group',
RESTORE_IMAGE: 'restore_image',

View File

@@ -0,0 +1,43 @@
import React from 'react';
import PropTypes from 'prop-types';
class ClickOutside extends React.Component {
isClickedInside = false;
componentDidMount() {
document.addEventListener('mousedown', this.handleDocumentClick);
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.handleDocumentClick);
}
handleDocumentClick = (e) => {
if (this.isClickedInside) {
this.isClickedInside = false;
return;
}
this.props.onClickOutside(e);
};
handleMouseDown = () => {
this.isClickedInside = true;
};
render() {
return React.cloneElement(
React.Children.only(this.props.children), {
onMouseDownCapture: this.handleMouseDown
}
);
}
}
ClickOutside.propTypes = {
children: PropTypes.element.isRequired,
onClickOutside: PropTypes.func.isRequired,
};
export default ClickOutside;

View File

@@ -0,0 +1,103 @@
.group-select {
position: relative;
}
.group-select.custom-select {
display: flex;
padding: 5px 10px;
border-radius: 3px;
align-items: center;
justify-content: space-between;
max-width: 900px;
user-select: none;
text-align: left;
border-color: hsl(0, 0%, 80%);
height: auto;
min-height: 38px;
}
.group-select.custom-select:hover {
border-color: hsl(0, 0%, 70%);
}
.group-select.custom-select:focus,
.group-select.custom-select.focus {
border-color: #1991eb !important;
box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);
}
.group-select.custom-select.disabled:focus,
.group-select.custom-select.focus.disabled,
.group-select.custom-select.disabled:hover {
border-color: rgba(0, 40, 100, 0.12) !important;
box-shadow: unset;
cursor: default;
}
.group-select.custom-select:hover {
cursor: pointer;
border-color: rgb(179, 179, 179);
}
.group-select .sf3-font-down {
display: inline-block;
color: #999;
transform: translateY(2px);
transition: all 0.1s;
font-size: 14px !important;
}
.group-select .sf3-font-down:hover {
color: #666;
}
.group-select .selected-option {
display: flex;
flex: 1;
overflow: hidden;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
background: #fff;
}
.group-select.selector-collaborator .option-group .option-group-content {
padding: 10px;
}
.group-select.custom-select.selector-collaborator .option-group .option-group-content {
padding: 10px 0;
}
.group-select.custom-select.selector-collaborator .option {
padding: 5px 0 5px 10px !important;
line-height: 20px;
}
.group-select .select-placeholder {
line-height: 1;
font-size: 14px;
white-space: nowrap;
}
.group-select .selected-option-show {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.group-select .selected-option-show .selected-option-item {
background-color: rgb(240, 240, 240);
border-radius: 16px;
display: flex;
align-items: center;
}
.group-select .selected-option-show .selected-option-item .selected-option-item-name {
font-size: 13px;
}
.group-select .selected-option-show .selected-option-item .sf2-icon-close {
cursor: pointer;
color: rgb(103, 103, 103);
}

View File

@@ -0,0 +1,138 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import ModalPortal from '../../modal-portal';
import SelectOptionGroup from './select-option-group.js';
import './index.css';
class GroupSelect extends Component {
constructor(props) {
super(props);
this.state = {
isShowSelectOptions: false
};
}
onSelectToggle = (event) => {
event.preventDefault();
if (this.state.isShowSelectOptions) event.stopPropagation();
let eventClassName = event.target.className;
if (eventClassName.indexOf('sf2-icon-close') > -1 || eventClassName === 'option-group-search') return;
if (event.target.value === '') return;
this.setState({
isShowSelectOptions: !this.state.isShowSelectOptions
});
};
onClickOutside = (event) => {
if (this.props.isShowSelected && event.target.className.includes('icon-fork-number')) {
return;
}
if (!this.selector.contains(event.target)) {
this.closeSelect();
}
};
closeSelect = () => {
this.setState({ isShowSelectOptions: false });
};
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.selectedOptions.length !== this.props.selectedOptions.length) {
// when selectedOptions change and dom rendered, calculate top
setTimeout(() => {
this.forceUpdate();
}, 1);
}
}
getSelectedOptionTop = () => {
if (!this.selector) return 38;
const { height } = this.selector.getBoundingClientRect();
return height;
};
getFilterOptions = (searchValue) => {
const { options } = this.props;
const validSearchVal = searchValue.trim().toLowerCase();
if (!validSearchVal) return options || [];
return options.filter(option => option.name.toLowerCase().includes(validSearchVal));
};
render() {
let { className, selectedOptions, options, placeholder, searchPlaceholder, noOptionsPlaceholder, isInModal } = this.props;
return (
<div
ref={(node) => this.selector = node}
className={classnames('group-select custom-select',
{ 'focus': this.state.isShowSelectOptions },
className
)}
onClick={this.onSelectToggle}>
<div className="selected-option">
{selectedOptions.length > 0 ?
<span className="selected-option-show">
{selectedOptions.map(item =>
<span key={item.id} className="selected-option-item mr-1 px-1">
<span className='selected-option-item-name'>{item.name}</span>
<i className="sf2-icon-close ml-1" onClick={() => {this.props.onDeleteOption(item);}}></i>
</span>
)}
</span>
:
<span className="select-placeholder">{placeholder}</span>
}
<i className="sf3-font-down sf3-font"></i>
</div>
{this.state.isShowSelectOptions && !isInModal && (
<SelectOptionGroup
selectedOptions={selectedOptions}
top={this.getSelectedOptionTop()}
options={options}
onSelectOption={this.props.onSelectOption}
searchPlaceholder={searchPlaceholder}
noOptionsPlaceholder={noOptionsPlaceholder}
onClickOutside={this.onClickOutside}
closeSelect={this.closeSelect}
getFilterOptions={this.getFilterOptions}
/>
)}
{this.state.isShowSelectOptions && isInModal && (
<ModalPortal>
<SelectOptionGroup
className={className}
selectedOptions={selectedOptions}
position={this.selector.getBoundingClientRect()}
isInModal={isInModal}
top={this.getSelectedOptionTop()}
options={options}
onSelectOption={this.props.onSelectOption}
searchPlaceholder={searchPlaceholder}
noOptionsPlaceholder={noOptionsPlaceholder}
onClickOutside={this.onClickOutside}
closeSelect={this.closeSelect}
getFilterOptions={this.getFilterOptions}
/>
</ModalPortal>
)}
</div>
);
}
}
GroupSelect.propTypes = {
className: PropTypes.string,
selectedOptions: PropTypes.array,
options: PropTypes.array,
placeholder: PropTypes.string,
onSelectOption: PropTypes.func,
onDeleteOption: PropTypes.func,
searchable: PropTypes.bool,
searchPlaceholder: PropTypes.string,
noOptionsPlaceholder: PropTypes.string,
isInModal: PropTypes.bool, // if select component in a modal (option group need ModalPortal to show)
};
export default GroupSelect;

View File

@@ -0,0 +1,46 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Option extends Component {
onSelectOption = (e) => {
e.stopPropagation();
this.props.onSelectOption(this.props.option);
};
onMouseEnter = () => {
if (!this.props.disableHover) {
this.props.changeIndex(this.props.index);
}
};
onMouseLeave = () => {
if (!this.props.disableHover) {
this.props.changeIndex(-1);
}
};
render() {
return (
<div
className={this.props.isActive ? 'option option-active' : 'option'}
onClick={this.onSelectOption}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>{this.props.children}
</div>
);
}
}
Option.propTypes = {
index: PropTypes.number,
isActive: PropTypes.bool,
changeIndex: PropTypes.func,
option: PropTypes.object,
children: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
onSelectOption: PropTypes.func,
disableHover: PropTypes.bool,
};
export default Option;

View File

@@ -0,0 +1,86 @@
.option-group {
position: absolute;
left: 0;
min-height: 60px;
max-height: 300px;
min-width: 100%;
max-width: 15rem;
padding: 0.5rem 0;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
background: #fff;
border: 1px solid rgba(0, 40, 100, 0.12);
border-radius: 3px;
z-index: 10001;
}
.option-group .option-group-search {
width: 100%;
padding: 6px 10px;
min-width: 170px;
}
.option-group-search .form-control {
height: 31px;
}
.option-group .none-search-result {
height: 100px;
width: 100%;
padding: 10px;
color: #666666;
}
.option-group .option-group-content {
max-height: 252px;
overflow-y: auto;
}
.option {
display: block;
width: 100%;
line-height: 24px;
padding: 6px 10px;
clear: both;
font-weight: 400;
text-align: inherit;
background-color: transparent;
border: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: flex;
align-items: center;
justify-content: space-between;
}
.option.option-active {
background-color: #20a0ff;
color: #fff;
cursor: pointer;
}
.option.option-active .sf2-icon-tick,
.option.option-active .select-option-name {
color: #fff !important;
}
.option .select-option-name .single-select-option {
margin: 0 0 0 12px;
}
.option .select-option-name .multiple-select-option {
margin: 0;
}
.option-group-selector-single-select .select-option-name {
display: flex;
align-items: center;
justify-content: space-between;
}
.option-group-selector-single-select .option:hover,
.option-group-selector-single-select .option.option-active,
.option-group-selector-multiple-select .option:hover,
.option-group-selector-multiple-select .option.option-active {
background-color: #f5f5f5;
}

View File

@@ -0,0 +1,215 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { DTableSearchInput } from 'dtable-ui-component';
import Option from './option';
import KeyCodes from '../../../constants/keyCodes';
import ClickOutside from './click-outside';
import './select-option-group.css';
const OPTION_HEIGHT = 32;
class SelectOptionGroup extends Component {
constructor(props) {
super(props);
this.state = {
searchVal: '',
activeIndex: -1,
disableHover: false,
};
this.filterOptions = null;
this.timer = null;
this.searchInputRef = React.createRef();
}
componentDidMount() {
window.addEventListener('keydown', this.onHotKey);
setTimeout(() => {
this.resetMenuStyle();
}, 1);
}
componentWillUnmount() {
this.filterOptions = null;
this.timer && clearTimeout(this.timer);
window.removeEventListener('keydown', this.onHotKey);
}
resetMenuStyle = () => {
const { isInModal, position } = this.props;
const { top, height } = this.optionGroupRef.getBoundingClientRect();
if (isInModal) {
if (position.y + position.height + height > window.innerHeight) {
this.optionGroupRef.style.top = (position.y - height) + 'px';
}
this.optionGroupRef.style.opacity = 1;
this.searchInputRef.current && this.searchInputRef.current.inputRef.focus();
}
else {
if (height + top > window.innerHeight) {
const borderWidth = 2;
this.optionGroupRef.style.top = -1 * (height + borderWidth) + 'px';
}
}
};
onHotKey = (event) => {
const keyCode = event.keyCode;
if (keyCode === KeyCodes.UpArrow) {
this.onPressUp();
} else if (keyCode === KeyCodes.DownArrow) {
this.onPressDown();
} else if (keyCode === KeyCodes.Enter) {
let option = this.filterOptions && this.filterOptions[this.state.activeIndex];
if (option) {
this.props.onSelectOption(option);
}
} else if (keyCode === KeyCodes.Tab || keyCode === KeyCodes.Escape) {
this.props.closeSelect();
}
};
onPressUp = () => {
if (this.state.activeIndex > 0) {
this.setState({ activeIndex: this.state.activeIndex - 1 }, () => {
this.scrollContent();
});
}
};
onPressDown = () => {
if (this.filterOptions && this.state.activeIndex < this.filterOptions.length - 1) {
this.setState({ activeIndex: this.state.activeIndex + 1 }, () => {
this.scrollContent();
});
}
};
onMouseDown = (e) => {
const { isInModal } = this.props;
// prevent event propagation when click option or search input
if (isInModal) {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
}
};
scrollContent = () => {
const { offsetHeight, scrollTop } = this.optionGroupContentRef;
this.setState({ disableHover: true });
this.timer = setTimeout(() => {
this.setState({ disableHover: false });
}, 500);
if (this.state.activeIndex * OPTION_HEIGHT === 0) {
this.optionGroupContentRef.scrollTop = 0;
return;
}
if (this.state.activeIndex * OPTION_HEIGHT < scrollTop) {
this.optionGroupContentRef.scrollTop = scrollTop - OPTION_HEIGHT;
}
else if (this.state.activeIndex * OPTION_HEIGHT > offsetHeight + scrollTop) {
this.optionGroupContentRef.scrollTop = scrollTop + OPTION_HEIGHT;
}
};
changeIndex = (index) => {
this.setState({ activeIndex: index });
};
onChangeSearch = (searchVal) => {
this.setState({ searchVal: searchVal || '', activeIndex: -1, });
};
clearValue = () => {
this.setState({ searchVal: '', activeIndex: -1, });
};
renderOptGroup = (searchVal) => {
let { noOptionsPlaceholder, onSelectOption, selectedOptions } = this.props;
this.filterOptions = this.props.getFilterOptions(searchVal);
if (this.filterOptions.length === 0) {
return (
<div className="none-search-result">{noOptionsPlaceholder}</div>
);
}
return this.filterOptions.map((option, index) => {
const isSelected = selectedOptions.some(item => item.id === option.id);
return (
<Option
key={`${option.id}-${index}`}
index={index}
isActive={this.state.activeIndex === index}
option={option}
onSelectOption={onSelectOption}
changeIndex={this.changeIndex}
disableHover={this.state.disableHover}
>
<div className='option-label'>{option.label}</div>
{isSelected && <i className="sf2-icon-tick text-gray font-weight-bold"></i>}
</Option>
);
});
};
render() {
const { searchPlaceholder, top, left, minWidth, isInModal, position, className } = this.props;
let { searchVal } = this.state;
let style = { top: top || 0, left: left || 0 };
if (minWidth) {
style = { top: top || 0, left: left || 0, minWidth };
}
if (isInModal) {
style = {
position: 'fixed',
left: position.x,
top: position.y + position.height,
minWidth: position.width,
opacity: 0,
};
}
return (
<ClickOutside onClickOutside={this.props.onClickOutside}>
<div
className={classnames('pt-0 option-group', className ? 'option-group-' + className : '')}
ref={(ref) => this.optionGroupRef = ref}
style={style}
onMouseDown={this.onMouseDown}
>
<div className="option-group-search position-relative">
<DTableSearchInput
className="option-search-control"
placeholder={searchPlaceholder}
onChange={this.onChangeSearch}
ref={this.searchInputRef}
/>
</div>
<div className="option-group-content" ref={(ref) => this.optionGroupContentRef = ref}>
{this.renderOptGroup(searchVal)}
</div>
</div>
</ClickOutside>
);
}
}
SelectOptionGroup.propTypes = {
top: PropTypes.number,
left: PropTypes.number,
minWidth: PropTypes.number,
options: PropTypes.array,
onSelectOption: PropTypes.func,
searchPlaceholder: PropTypes.string,
noOptionsPlaceholder: PropTypes.string,
onClickOutside: PropTypes.func.isRequired,
closeSelect: PropTypes.func.isRequired,
getFilterOptions: PropTypes.func.isRequired,
selectedOptions: PropTypes.array,
isInModal: PropTypes.bool,
position: PropTypes.object,
className: PropTypes.string,
};
export default SelectOptionGroup;

View File

@@ -1,5 +1,4 @@
import SeahubSelect from './seahub-select';
import { NoGroupMessage } from './no-group-message';
import { MenuSelectStyle, UserSelectStyle, NoOptionsStyle } from './seahub-select-style';
export { SeahubSelect, NoGroupMessage, MenuSelectStyle, UserSelectStyle, NoOptionsStyle };
export { SeahubSelect, MenuSelectStyle, UserSelectStyle, NoOptionsStyle };

View File

@@ -1,16 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { gettext } from '../../../utils/constants';
import { NoOptionsStyle } from './seahub-select-style';
const NoGroupMessage = (props) => {
return (
<div {...props.innerProps} style={NoOptionsStyle}>{gettext('Group not found')}</div>
);
};
NoGroupMessage.propTypes = {
innerProps: PropTypes.any.isRequired,
};
export { NoGroupMessage };

View File

@@ -3,13 +3,27 @@ import PropTypes from 'prop-types';
import Select, { components } from 'react-select';
import { MenuSelectStyle } from './seahub-select-style';
const DropdownIndicator = props => {
return (
components.DropdownIndicator && (
<components.DropdownIndicator {...props}>
<span className="sf3-font sf3-font-down" style={{ fontSize: '12px', marginLeft: '-2px' }} aria-hidden="true"></span>
</components.DropdownIndicator>
)
);
};
const ClearIndicator = ({ innerProps, ...props }) => {
const onMouseDown = e => {
e.nativeEvent.stopImmediatePropagation();
innerProps.onMouseDown(e);
};
props.innerProps = { ...innerProps, onMouseDown };
return <components.ClearIndicator {...props} />;
return (
<components.ClearIndicator {...props} >
<span className="sf3-font sf3-font-x-01" style={{ fontSize: '12px', marginLeft: '-2px' }} aria-hidden="true"></span>
</components.ClearIndicator>
);
};
ClearIndicator.propTypes = {
@@ -93,7 +107,7 @@ export default class SeahubSelect extends React.Component {
className={className}
classNamePrefix={classNamePrefix}
styles={MenuSelectStyle}
components={{ Option, MenuList, ClearIndicator }}
components={{ Option, DropdownIndicator, MenuList, ClearIndicator }}
placeholder={placeholder}
isSearchable={isSearchable}
isClearable={isClearable}

View File

@@ -6,7 +6,7 @@ import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import SharePermissionEditor from '../select-editor/share-permission-editor';
import FileChooser from '../file-chooser';
import { SeahubSelect, NoGroupMessage } from '../common/select';
import GroupSelect from '../common/group-select';
import toaster from '../../components/toast';
class GroupItem extends React.Component {
@@ -96,7 +96,7 @@ class LibSubFolderSetGroupPermissionDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedOption: null,
selectedOptions: [],
errorMsg: [],
permission: 'rw',
groupPermissionItems: [],
@@ -111,10 +111,6 @@ class LibSubFolderSetGroupPermissionDialog extends React.Component {
}
}
handleSelectChange = (option) => {
this.setState({ selectedOption: option });
};
componentDidMount() {
this.loadOptions();
this.listGroupPermissionItems();
@@ -126,6 +122,7 @@ class LibSubFolderSetGroupPermissionDialog extends React.Component {
return {
id: item.id,
label: item.name,
name: item.name,
value: item.name
};
});
@@ -152,20 +149,41 @@ class LibSubFolderSetGroupPermissionDialog extends React.Component {
});
};
onSelectOption = (option) => {
const selectedOptions = this.state.selectedOptions.slice(0);
const index = selectedOptions.findIndex(item => item.id === option.id);
if (index > -1) {
selectedOptions.splice(index, 1);
} else {
selectedOptions.push(option);
}
this.setState({ selectedOptions: selectedOptions });
};
onDeleteOption = (option) => {
const selectedOptions = this.state.selectedOptions.slice(0);
const index = selectedOptions.findIndex(item => item.id === option.id);
if (index > -1) {
selectedOptions.splice(index, 1);
}
this.setState({ selectedOptions: selectedOptions });
};
setPermission = (permission) => {
this.setState({ permission: permission });
};
addGroupFolderPerm = () => {
const { selectedOption } = this.state;
const { selectedOptions } = this.state;
const folderPath = this.props.folderPath || this.state.folderPath;
if (!selectedOption || !folderPath) {
if (selectedOptions.length === 0 || !folderPath) {
return false;
}
const targetGroupIds = selectedOptions.map(op => op.id);
const request = this.props.isDepartmentRepo ?
seafileAPI.addDepartmentRepoGroupFolderPerm(this.props.repoID, this.state.permission, folderPath, selectedOption.id) :
seafileAPI.addGroupFolderPerm(this.props.repoID, this.state.permission, folderPath, selectedOption.id);
seafileAPI.addDepartmentRepoGroupFolderPerm(this.props.repoID, this.state.permission, folderPath, targetGroupIds) :
seafileAPI.addGroupFolderPerm(this.props.repoID, this.state.permission, folderPath, targetGroupIds);
request.then(res => {
let errorMsg = [];
if (res.data.failed.length > 0) {
@@ -176,7 +194,7 @@ class LibSubFolderSetGroupPermissionDialog extends React.Component {
this.setState({
errorMsg: errorMsg,
groupPermissionItems: this.state.groupPermissionItems.concat(res.data.success),
selectedOption: null,
selectedOptions: [],
permission: 'rw',
folderPath: ''
});
@@ -296,13 +314,14 @@ class LibSubFolderSetGroupPermissionDialog extends React.Component {
<tbody>
<tr>
<td>
<SeahubSelect
onChange={this.handleSelectChange}
<GroupSelect
selectedOptions={this.state.selectedOptions}
options={this.options}
placeholder={gettext('Select a group')}
maxMenuHeight={200}
value={this.state.selectedOption}
noOptionsMessage={NoGroupMessage}
onSelectOption={this.onSelectOption}
onDeleteOption={this.onDeleteOption}
searchPlaceholder={gettext('Search groups')}
noOptionsPlaceholder={gettext('No results')}
isInModal={true}
/>
</td>
{showPath &&

View File

@@ -6,9 +6,9 @@ import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import toaster from '../toast';
import SharePermissionEditor from '../select-editor/share-permission-editor';
import { SeahubSelect, NoGroupMessage } from '../common/select';
import EventBus from '../common/event-bus';
import { EVENT_BUS_TYPE } from '../common/event-bus-type';
import GroupSelect from '../common/group-select';
class GroupItem extends React.Component {
@@ -127,7 +127,7 @@ class ShareToGroup extends React.Component {
super(props);
this.state = {
options: [],
selectedOption: null,
selectedOptions: [],
errorMsg: [],
permission: 'rw',
sharedItems: [],
@@ -151,8 +151,24 @@ class ShareToGroup extends React.Component {
}
}
handleSelectChange = (option) => {
this.setState({ selectedOption: option });
onSelectOption = (option) => {
const selectedOptions = this.state.selectedOptions.slice(0);
const index = selectedOptions.findIndex(item => item.id === option.id);
if (index > -1) {
selectedOptions.splice(index, 1);
} else {
selectedOptions.push(option);
}
this.setState({ selectedOptions: selectedOptions });
};
onDeleteOption = (option) => {
const selectedOptions = this.state.selectedOptions.slice(0);
const index = selectedOptions.findIndex(item => item.id === option.id);
if (index > -1) {
selectedOptions.splice(index, 1);
}
this.setState({ selectedOptions: selectedOptions });
};
componentDidMount() {
@@ -174,6 +190,7 @@ class ShareToGroup extends React.Component {
obj.value = res.data[i].name;
obj.id = res.data[i].id;
obj.label = res.data[i].name;
obj.name = res.data[i].name;
options.push(obj);
}
this.setState({ options: options });
@@ -205,10 +222,10 @@ class ShareToGroup extends React.Component {
shareToGroup = () => {
const eventBus = EventBus.getInstance();
const { isGroupOwnedRepo, itemPath: path, repoID } = this.props;
const { permission, selectedOption } = this.state;
const targetGroupId = selectedOption.id;
const { permission, selectedOptions } = this.state;
const targetGroupIds = selectedOptions.map(item => item.id);
if (isGroupOwnedRepo) {
seafileAPI.shareGroupOwnedRepoToGroup(repoID, permission, targetGroupId, path).then(res => {
seafileAPI.shareGroupOwnedRepoToGroup(repoID, permission, targetGroupIds, path).then(res => {
let errorMsg = [];
if (res.data.failed.length > 0) {
for (let i = 0 ; i < res.data.failed.length ; i++) {
@@ -228,13 +245,15 @@ class ShareToGroup extends React.Component {
if (this.props.repo && res.data.success.length > 0) {
const sharedRepo = { ...this.props.repo, permission: res.data.success[0].permission };
eventBus.dispatch(EVENT_BUS_TYPE.ADD_SHARED_REPO_INO_GROUP, { repo: sharedRepo, group_id: targetGroupId });
targetGroupIds.forEach(targetGroupId => {
eventBus.dispatch(EVENT_BUS_TYPE.ADD_SHARED_REPO_INTO_GROUP, { repo: sharedRepo, group_id: targetGroupId });
});
}
this.setState({
errorMsg: errorMsg,
sharedItems: this.state.sharedItems.concat(items),
selectedOption: null,
selectedOptions: [],
permission: 'rw',
});
}).catch(error => {
@@ -242,7 +261,7 @@ class ShareToGroup extends React.Component {
toaster.danger(errMessage);
});
} else {
seafileAPI.shareFolder(repoID, path, 'group', permission, [targetGroupId]).then(res => {
seafileAPI.shareFolder(repoID, path, 'group', permission, targetGroupIds).then(res => {
let errorMsg = [];
if (res.data.failed.length > 0) {
for (let i = 0 ; i < res.data.failed.length ; i++) {
@@ -252,13 +271,15 @@ class ShareToGroup extends React.Component {
if (this.props.repo && res.data.success.length > 0) {
const sharedRepo = { ...this.props.repo, permission: res.data.success[0].permission };
eventBus.dispatch(EVENT_BUS_TYPE.ADD_SHARED_REPO_INO_GROUP, { repo: sharedRepo, group_id: targetGroupId });
targetGroupIds.forEach(targetGroupId => {
eventBus.dispatch(EVENT_BUS_TYPE.ADD_SHARED_REPO_INTO_GROUP, { repo: sharedRepo, group_id: targetGroupId });
});
}
this.setState({
errorMsg: errorMsg,
sharedItems: this.state.sharedItems.concat(res.data.success),
selectedOption: null,
selectedOptions: [],
permission: 'rw'
});
}).catch(error => {
@@ -347,15 +368,14 @@ class ShareToGroup extends React.Component {
<tbody>
<tr>
<td>
<SeahubSelect
onChange={this.handleSelectChange}
<GroupSelect
selectedOptions={this.state.selectedOptions}
options={this.state.options}
placeholder={gettext('Select groups')}
maxMenuHeight={200}
value={this.state.selectedOption}
noOptionsMessage={NoGroupMessage}
isSearchable={true}
isClearable={true}
onSelectOption={this.onSelectOption}
onDeleteOption={this.onDeleteOption}
searchPlaceholder={gettext('Search groups')}
noOptionsPlaceholder={gettext('No results')}
isInModal={true}
/>
</td>
<td>

View File

@@ -80,7 +80,7 @@ class SysAdminShareDialog extends React.Component {
render() {
return (
<div>
<Modal isOpen={true} style={{ maxWidth: '720px' }} className="share-dialog" toggle={this.props.toggleDialog}>
<Modal isOpen={true} style={{ maxWidth: '800px' }} className="share-dialog" toggle={this.props.toggleDialog}>
<ModalHeader toggle={this.props.toggleDialog}>{gettext('Share')} <span className="op-target" title={this.props.itemName}>{this.props.itemName}</span></ModalHeader>
<ModalBody className="share-dialog-content">
{this.renderDirContent()}

View File

@@ -7,7 +7,7 @@ import { systemAdminAPI } from '../../../utils/system-admin-api';
import { Utils } from '../../../utils/utils';
import toaster from '../../toast';
import SharePermissionEditor from '../../select-editor/share-permission-editor';
import { SeahubSelect, NoGroupMessage } from '../../common/select';
import GroupSelect from '../../common/group-select';
class GroupItem extends React.Component {
@@ -116,7 +116,7 @@ class SysAdminShareToGroup extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedOption: null,
selectedOptions: [],
errorMsg: [],
permission: 'rw',
sharedItems: []
@@ -128,10 +128,6 @@ class SysAdminShareToGroup extends React.Component {
}
}
handleSelectChange = (option) => {
this.setState({ selectedOption: option });
};
componentDidMount() {
this.loadOptions();
this.listSharedGroups();
@@ -142,6 +138,7 @@ class SysAdminShareToGroup extends React.Component {
this.options = [];
for (let i = 0 ; i < res.data.length; i++) {
let obj = {};
obj.name = res.data[i].name;
obj.value = res.data[i].name;
obj.id = res.data[i].id;
obj.label = res.data[i].name;
@@ -153,6 +150,26 @@ class SysAdminShareToGroup extends React.Component {
});
};
onSelectOption = (option) => {
const selectedOptions = this.state.selectedOptions.slice(0);
const index = selectedOptions.findIndex(item => item.id === option.id);
if (index > -1) {
selectedOptions.splice(index, 1);
} else {
selectedOptions.push(option);
}
this.setState({ selectedOptions: selectedOptions });
};
onDeleteOption = (option) => {
const selectedOptions = this.state.selectedOptions.slice(0);
const index = selectedOptions.findIndex(item => item.id === option.id);
if (index > -1) {
selectedOptions.splice(index, 1);
}
this.setState({ selectedOptions: selectedOptions });
};
listSharedGroups = () => {
let repoID = this.props.repoID;
systemAdminAPI.sysAdminListRepoSharedItems(repoID, 'group').then((res) => {
@@ -172,12 +189,11 @@ class SysAdminShareToGroup extends React.Component {
};
shareToGroup = () => {
let groups = [];
let repoID = this.props.repoID;
if (this.state.selectedOption) {
groups[0] = this.state.selectedOption.id;
}
systemAdminAPI.sysAdminAddRepoSharedItem(repoID, 'group', groups, this.state.permission).then(res => {
let { selectedOptions } = this.state;
if (selectedOptions.length === 0) return;
let targetGroupIDs = selectedOptions.map(item => { return item.id; });
systemAdminAPI.sysAdminAddRepoSharedItem(repoID, 'group', targetGroupIDs, this.state.permission).then(res => {
let errorMsg = [];
if (res.data.failed.length > 0) {
for (let i = 0 ; i < res.data.failed.length ; i++) {
@@ -188,7 +204,7 @@ class SysAdminShareToGroup extends React.Component {
this.setState({
errorMsg: errorMsg,
sharedItems: this.state.sharedItems.concat(items),
selectedOption: null,
selectedOptions: [],
permission: 'rw',
});
}).catch(error => {
@@ -247,13 +263,14 @@ class SysAdminShareToGroup extends React.Component {
<tbody>
<tr>
<td>
<SeahubSelect
onChange={this.handleSelectChange}
<GroupSelect
selectedOptions={this.state.selectedOptions}
options={this.options}
placeholder={gettext('Select groups')}
maxMenuHeight={200}
value={this.state.selectedOption}
noOptionsMessage={NoGroupMessage}
onSelectOption={this.onSelectOption}
onDeleteOption={this.onDeleteOption}
searchPlaceholder={gettext('Search groups')}
noOptionsPlaceholder={gettext('No results')}
isInModal={true}
/>
</td>
<td>

View File

@@ -52,7 +52,7 @@ class Libraries extends Component {
const eventBus = EventBus.getInstance();
this.unsubscribeAddNewGroup = eventBus.subscribe(EVENT_BUS_TYPE.ADD_NEW_GROUP, this.addNewGroup);
this.unsubscribeAddSharedRepoIntoGroup = eventBus.subscribe(EVENT_BUS_TYPE.ADD_SHARED_REPO_INO_GROUP, this.insertRepoIntoGroup);
this.unsubscribeAddSharedRepoIntoGroup = eventBus.subscribe(EVENT_BUS_TYPE.ADD_SHARED_REPO_INTO_GROUP, this.insertRepoIntoGroup);
this.unsubscribeUnsharedRepoToGroup = eventBus.subscribe(EVENT_BUS_TYPE.UN_SHARE_REPO_TO_GROUP, this.unshareRepoToGroup);
}

View File

@@ -267,18 +267,14 @@ class SeafileAPI {
return this.req.delete(url, { data: params });
}
shareGroupOwnedRepoToGroup(repoID, permission, groupID, path) {
shareGroupOwnedRepoToGroup(repoID, permission, groupIDs, path) {
const url = this.server + '/api/v2.1/group-owned-libraries/' + repoID + '/group-share/';
let form = new FormData();
form.append('permission', permission);
form.append('path', path);
if (Array.isArray(groupID)) {
groupID.forEach(item => {
form.append('group_id', item);
});
} else {
form.append('group_id', groupID);
}
groupIDs.forEach(item => {
form.append('group_id', item);
});
return this._sendPostRequest(url, form);
}
@@ -1825,12 +1821,14 @@ class SeafileAPI {
return this.req.get(url);
}
addGroupFolderPerm(repoID, permission, folderPath, groupID) {
addGroupFolderPerm(repoID, permission, folderPath, groupIDs) {
const url = this.server + '/api2/repos/' + repoID + '/group-folder-perm/';
let form = new FormData();
form.append('permission', permission);
form.append('folder_path', folderPath);
form.append('group_id', groupID);
groupIDs.forEach(item => {
form.append('group_id', item);
});
return this._sendPostRequest(url, form);
}
@@ -1903,12 +1901,14 @@ class SeafileAPI {
return this.req.get(url);
}
addDepartmentRepoGroupFolderPerm(repoID, permission, folderPath, groupID) {
addDepartmentRepoGroupFolderPerm(repoID, permission, folderPath, groupIDs) {
const url = this.server + '/api/v2.1/group-owned-libraries/' + repoID + '/group-folder-permission/';
let form = new FormData();
form.append('permission', permission);
form.append('folder_path', folderPath);
form.append('group_id', groupID);
groupIDs.forEach(item => {
form.append('group_id', item);
});
return this._sendPostRequest(url, form);
}