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 ( +