diff --git a/frontend/src/components/common/event-bus-type.js b/frontend/src/components/common/event-bus-type.js index 8cf87b544b..06d6551a0c 100644 --- a/frontend/src/components/common/event-bus-type.js +++ b/frontend/src/components/common/event-bus-type.js @@ -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', diff --git a/frontend/src/components/common/group-select/click-outside.js b/frontend/src/components/common/group-select/click-outside.js new file mode 100644 index 0000000000..8a740d1178 --- /dev/null +++ b/frontend/src/components/common/group-select/click-outside.js @@ -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; diff --git a/frontend/src/components/common/group-select/index.css b/frontend/src/components/common/group-select/index.css new file mode 100644 index 0000000000..48e0e8cf81 --- /dev/null +++ b/frontend/src/components/common/group-select/index.css @@ -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); +} diff --git a/frontend/src/components/common/group-select/index.js b/frontend/src/components/common/group-select/index.js new file mode 100644 index 0000000000..4577ffe5e3 --- /dev/null +++ b/frontend/src/components/common/group-select/index.js @@ -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 ( +
this.selector = node} + className={classnames('group-select custom-select', + { 'focus': this.state.isShowSelectOptions }, + className + )} + onClick={this.onSelectToggle}> +
+ {selectedOptions.length > 0 ? + + {selectedOptions.map(item => + + {item.name} + {this.props.onDeleteOption(item);}}> + + )} + + : + {placeholder} + } + +
+ {this.state.isShowSelectOptions && !isInModal && ( + + )} + {this.state.isShowSelectOptions && isInModal && ( + + + + )} +
+ ); + } +} + +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; diff --git a/frontend/src/components/common/group-select/option.js b/frontend/src/components/common/group-select/option.js new file mode 100644 index 0000000000..1b24768614 --- /dev/null +++ b/frontend/src/components/common/group-select/option.js @@ -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 ( +
{this.props.children} +
+ ); + } +} + +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; diff --git a/frontend/src/components/common/group-select/select-option-group.css b/frontend/src/components/common/group-select/select-option-group.css new file mode 100644 index 0000000000..39f8bbf414 --- /dev/null +++ b/frontend/src/components/common/group-select/select-option-group.css @@ -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; +} diff --git a/frontend/src/components/common/group-select/select-option-group.js b/frontend/src/components/common/group-select/select-option-group.js new file mode 100644 index 0000000000..558a2dc7a1 --- /dev/null +++ b/frontend/src/components/common/group-select/select-option-group.js @@ -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 ( +
{noOptionsPlaceholder}
+ ); + } + return this.filterOptions.map((option, index) => { + const isSelected = selectedOptions.some(item => item.id === option.id); + return ( + + ); + }); + }; + + 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 ( + +
this.optionGroupRef = ref} + style={style} + onMouseDown={this.onMouseDown} + > +
+ +
+
this.optionGroupContentRef = ref}> + {this.renderOptGroup(searchVal)} +
+
+
+ ); + } +} + +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; diff --git a/frontend/src/components/common/select/index.js b/frontend/src/components/common/select/index.js index 6764f95dcd..c6bc4f0894 100644 --- a/frontend/src/components/common/select/index.js +++ b/frontend/src/components/common/select/index.js @@ -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 }; diff --git a/frontend/src/components/common/select/no-group-message.js b/frontend/src/components/common/select/no-group-message.js deleted file mode 100644 index 7f97e853fd..0000000000 --- a/frontend/src/components/common/select/no-group-message.js +++ /dev/null @@ -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 ( -
{gettext('Group not found')}
- ); -}; - -NoGroupMessage.propTypes = { - innerProps: PropTypes.any.isRequired, -}; - -export { NoGroupMessage }; diff --git a/frontend/src/components/common/select/seahub-select.js b/frontend/src/components/common/select/seahub-select.js index e377f4ae3b..be6e2c2a52 100644 --- a/frontend/src/components/common/select/seahub-select.js +++ b/frontend/src/components/common/select/seahub-select.js @@ -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 && ( + + + + ) + ); +}; + const ClearIndicator = ({ innerProps, ...props }) => { const onMouseDown = e => { e.nativeEvent.stopImmediatePropagation(); innerProps.onMouseDown(e); }; props.innerProps = { ...innerProps, onMouseDown }; - return ; + return ( + + + + ); }; 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} diff --git a/frontend/src/components/dialog/lib-sub-folder-set-group-permission-dialog.js b/frontend/src/components/dialog/lib-sub-folder-set-group-permission-dialog.js index 5b4dbbe0cc..b7a0ab3dca 100644 --- a/frontend/src/components/dialog/lib-sub-folder-set-group-permission-dialog.js +++ b/frontend/src/components/dialog/lib-sub-folder-set-group-permission-dialog.js @@ -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 { - {showPath && diff --git a/frontend/src/components/dialog/share-to-group.js b/frontend/src/components/dialog/share-to-group.js index e84ecd65d5..ce16e4e912 100644 --- a/frontend/src/components/dialog/share-to-group.js +++ b/frontend/src/components/dialog/share-to-group.js @@ -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 { - diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-dialog.js index 8936d10c69..2d41e07ab9 100644 --- a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-dialog.js +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-dialog.js @@ -80,7 +80,7 @@ class SysAdminShareDialog extends React.Component { render() { return (
- + {gettext('Share')} {this.props.itemName} {this.renderDirContent()} diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-to-group.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-to-group.js index 9711b3f63d..2d1e38e5de 100644 --- a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-to-group.js +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-to-group.js @@ -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 { - diff --git a/frontend/src/pages/libraries/index.js b/frontend/src/pages/libraries/index.js index d00e94b7cf..cade45ffe8 100644 --- a/frontend/src/pages/libraries/index.js +++ b/frontend/src/pages/libraries/index.js @@ -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); } diff --git a/frontend/src/utils/seafile-api.js b/frontend/src/utils/seafile-api.js index ab80c4a1a8..82b655239e 100644 --- a/frontend/src/utils/seafile-api.js +++ b/frontend/src/utils/seafile-api.js @@ -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); }