diff --git a/app/components/ChangelogModal.js b/app/components/ChangelogModal.js index 524990a84e..fb83d24405 100644 --- a/app/components/ChangelogModal.js +++ b/app/components/ChangelogModal.js @@ -45,7 +45,7 @@ class ChangelogModal extends ModalComponent { if (!changelog) { html = [ -
+
]; @@ -55,19 +55,19 @@ class ChangelogModal extends ModalComponent { changelog.map(change => { html = [ ...html, -

v{change.version} Changes

+

v{change.version} Changes

]; if (change.summary) { if (!Array.isArray(change.summary)) { html = [ ...html, -

{change.summary}

+

{change.summary}

] } else { html = [ ...html, -

{change.summary[0]}

, - ...change.summary.slice(1).map(text =>

{text}

) +

{change.summary[0]}

, + ...change.summary.slice(1).map((text, i) =>

{text}

) ] } } @@ -75,30 +75,36 @@ class ChangelogModal extends ModalComponent { if (change.major && change.major.length) { html = [ ...html, -

Noteworthy

, - +

Noteworthy

, + ]; } if (change.fixes && change.fixes.length) { html = [ ...html, -

Fixes

, - +

Fixes

, + ]; } if (change.minor && change.minor.length) { html = [ ...html, -

Minor Changes

, - +

Minor Changes

, + ]; } html = [ ...html, -
+
] }); } diff --git a/app/components/RequestSwitcherModal.js b/app/components/RequestSwitcherModal.js index 766aa036a7..cc1a47b35c 100644 --- a/app/components/RequestSwitcherModal.js +++ b/app/components/RequestSwitcherModal.js @@ -1,26 +1,214 @@ -import React from 'react'; +import React, {PropTypes} from 'react'; +import ReactDOM from 'react-dom'; +import classnames from 'classnames'; import Modal from './base/Modal'; -import ModalBody from './base/ModalBody'; import ModalHeader from './base/ModalHeader'; -import ModalFooter from './base/ModalFooter'; +import ModalBody from './base/ModalBody'; +import MethodTag from './MethodTag'; import ModalComponent from './lib/ModalComponent'; +import * as db from '../database'; + class RequestSwitcherModal extends ModalComponent { - render() { + constructor (props) { + super(props); + this.state = { + searchString: '', + matchedRequests: [], + requestGroups: [], + activeIndex: -1 + } + } + + _setActiveIndex (activeIndex) { + if (activeIndex < 0) { + activeIndex = this.state.matchedRequests.length - 1; + } else if (activeIndex >= this.state.matchedRequests.length) { + activeIndex = 0; + } + + this.setState({activeIndex}); + } + + _activateCurrentIndex () { + if (this.state.matchedRequests.length) { + // Activate the request if there is one + this._activateRequest(this.state.matchedRequests[this.state.activeIndex]); + } else { + // Create the request if nothing matched + const name = this.state.searchString; + const parentId = this.props.activeRequest.parentId; + + db.requestCreate({name, parentId}).then(request => { + this._activateRequest(request); + }); + } + } + + _activateRequest (request) { + if (!request) { + return; + } + + this.props.activateRequest(request); + this.hide(); + } + + _handleChange (searchString) { + Promise.all([ + db.requestAll(), + db.requestGroupAll() + ]).then(([ + requests, + requestGroups + ]) => { + const {activeRequest} = this.props; + const activeRequestGroup = requestGroups.find(rg => rg._id === activeRequest.parentId); + + const matchedRequests = requests.sort( + (a, b) => { + const rgA = requestGroups.find(rg => rg._id === a.parentId); + const rgB = requestGroups.find(rg => rg._id === b.parentId); + + if (rgA === activeRequestGroup && rgB !== activeRequestGroup) { + return -1; + } else if (rgB === activeRequestGroup && rgA !== activeRequestGroup) { + return 1; + } else { + // TODO: Sort by quality of match (eg. start of string vs mid string, etc) + return (rgA || {}).name > (rgB || {}).name ? 1 : -1; + } + } + ).filter(r => { + if (!searchString) { + return true; + } else { + return r.name.toLowerCase().indexOf(searchString.toLowerCase()) !== -1; + } + }); + + const activeIndex = searchString ? 0 : -1; + + this.setState({ + activeIndex, + matchedRequests, + requestGroups, + searchString + }); + }); + } + + _focusInput () { + // Need to focus after the Modal has shown, or else it won't exist yet + setTimeout(() => { + this.refs.input.focus(); + }); + } + + show () { + throw new Error('show() not supposed to be called on RequestSwitcherModal'); + } + + toggle () { + super.toggle(); + this._focusInput(); + this._handleChange(''); + } + + componentDidMount () { + super.componentDidMount(); + + ReactDOM.findDOMNode(this.refs.modal).addEventListener('keydown', e => { + const keyCode = e.keyCode; + + if (keyCode === 38 || (keyCode === 9 && e.shiftKey)) { + // Up or Shift+Tab + this._setActiveIndex(this.state.activeIndex - 1); + } else if (keyCode === 40 || keyCode === 9) { + // Down or Tab + this._setActiveIndex(this.state.activeIndex + 1); + } else if (keyCode === 13) { + // Enter + this._activateCurrentIndex(); + } else { + return; + } + + e.preventDefault(); + }) + } + + render () { + const {matchedRequests, requestGroups, searchString, activeIndex} = this.state; + return ( - - Switch Request - -

DO IT

+ + +

+ tab or +   + ↑ ↓  to navigate +     + ↵  to select +     + esc to dismiss +

+

Jump To Request

+
+ +
+ this._handleChange(e.target.value)} + /> +
+
    + {matchedRequests.map((r, i) => { + const requestGroup = requestGroups.find(rg => rg._id === r.parentId); + + return ( +
  • + +
  • + ) + })} +
+ + {matchedRequests.length === 0 ? ( +
+

+ No matches found for {searchString} +

+ +
+ ) : null}
- -
); } } -RequestSwitcherModal.propTypes = {}; +RequestSwitcherModal.propTypes = { + activateRequest: PropTypes.func.isRequired, + activeRequest: PropTypes.object.isRequired, + workspaceId: PropTypes.string.isRequired +}; export default RequestSwitcherModal; diff --git a/app/components/base/Input.js b/app/components/base/Input.js index a08c3c8eaf..1b599d145e 100644 --- a/app/components/base/Input.js +++ b/app/components/base/Input.js @@ -27,6 +27,10 @@ class Input extends Component { } else { this.refs.input.value = this.props.value || ''; } + + if (this.props.autofocus) { + this.focus(); + } } componentDidMount () { @@ -34,7 +38,7 @@ class Input extends Component { } componentDidUpdate () { - this._updateValueFromProps() + this._updateValueFromProps(); } focus () { @@ -46,7 +50,8 @@ class Input extends Component { } render () { - const {value, ...other} = this.props; + // NOTE: We're taking props base doesn't need + const {value, autofocus, ...other} = this.props; return ( ( -
- - {children} -
-); +const ModalHeader = ({hideCloseButton, className, children}) => { + let closeButton = null; + + if (!hideCloseButton) { + closeButton = ( + + ) + } + + return ( +
+ {closeButton} + {children} +
+ ); +}; + +ModalHeader.propTypes = { + hideCloseButton: PropTypes.bool +}; export default ModalHeader; diff --git a/app/containers/App.js b/app/containers/App.js index 514a22dac0..1fe899dfdf 100644 --- a/app/containers/App.js +++ b/app/containers/App.js @@ -150,11 +150,17 @@ class App extends Component { console.warn('-- Recreating Sort Keys --'); requestGroups.map((r, i) => { - db.requestGroupUpdate(r, {metaSortKey: i * 100, parentId: requestGroupToTarget.parentId}); + db.requestGroupUpdate(r, { + metaSortKey: i * 100, + parentId: requestGroupToTarget.parentId + }); }); } else { const metaSortKey = afterKey - (afterKey - beforeKey) / 2; - db.requestGroupUpdate(requestGroupToMove, {metaSortKey, parentId: requestGroupToTarget.parentId}); + db.requestGroupUpdate(requestGroupToMove, { + metaSortKey, + parentId: requestGroupToTarget.parentId + }); } break; @@ -226,7 +232,7 @@ class App extends Component { if (a.metaSortKey === b.metaSortKey) { return a._id > b._id ? -1 : 1; } else { - return a.metaSortKey < b.metaSortKey ? -1 : 1; + return a.metaSortKey < b.metaSortKey ? -1 : 1; } }); @@ -485,8 +491,12 @@ class App extends Component { - + db.workspaceUpdate(workspace, {metaActiveRequestId: r._id})} + /> db.requestGroupUpdate(rg)}/>
) diff --git a/app/css/components/dropdown.scss b/app/css/components/dropdown.scss index feac887497..2b2fc54d30 100644 --- a/app/css/components/dropdown.scss +++ b/app/css/components/dropdown.scss @@ -72,14 +72,12 @@ .dropdown__divider__label { position: relative; - top: -0.85rem; + top: -0.80rem; // HACK: Fine adjustment - color: $hl-xxl; + color: $hl; padding-right: 1em; background: #fff; font-size: $font-size-xs; - margin-top: 0.5em; - margin-bottom: 0.5em; text-transform: uppercase; } } diff --git a/app/css/components/forms.scss b/app/css/components/forms.scss index 8a80f20a68..ad1f346a95 100644 --- a/app/css/components/forms.scss +++ b/app/css/components/forms.scss @@ -75,6 +75,8 @@ label > .form-control, } } +.btn:focus:not(:disabled), +.btn.focus:not(:disabled), .btn:hover:not(:disabled) { background: $hl-sm; } diff --git a/app/css/components/modal.scss b/app/css/components/modal.scss index 7a7b13147c..1694c6d668 100644 --- a/app/css/components/modal.scss +++ b/app/css/components/modal.scss @@ -45,6 +45,10 @@ max-height: 100%; } + &.modal--fixed-top .modal__content { + margin: 0 auto auto auto; + } + .modal__header { border-bottom: 1px solid $hl-md; height: $line-height-md; diff --git a/app/css/components/requestswitcher.scss b/app/css/components/requestswitcher.scss new file mode 100644 index 0000000000..22d95e3170 --- /dev/null +++ b/app/css/components/requestswitcher.scss @@ -0,0 +1,22 @@ +@import '../constants/dimensions'; +@import '../constants/colors'; + +.request-switcher { + ul { + + } + + .btn:hover { + background: $hl-xs; + } + + .btn.focus { + background: $hl-sm; + } + + .tag { + width: 3em; + text-align: left; + padding-left: 0 !important; + } +} diff --git a/app/css/constants/colors.scss b/app/css/constants/colors.scss index b712220ac5..43d39ae9cb 100644 --- a/app/css/constants/colors.scss +++ b/app/css/constants/colors.scss @@ -26,7 +26,7 @@ $hl-xl: rgba(140, 140, 140, 0.5); $hl-xxl: rgba(140, 140, 140, 0.7); $hl: rgba(140, 140, 140, 1); -$success: #8fc62b; +$success: #88bd29; $notice: #e5df54; $warning: #ffb932; $danger: #ff6d59; diff --git a/app/css/index.scss b/app/css/index.scss index 45b24b02e4..855087a992 100644 --- a/app/css/index.scss +++ b/app/css/index.scss @@ -14,6 +14,7 @@ /* Components */ @import 'components/wrapper'; +@import 'components/requestswitcher'; @import 'components/changelog'; @import 'components/sidebar'; @import 'components/pane';