Have org selector properly show when no default org (#5955)

* Update logic when looking for org to check length of orgs excluding default

- Also hide the ‘Me | Org’ selector altogether since it makes to sense
to be there, you can’t choose ‘Me’ because it doesn’t exist.

* Remove radios for 'me' and 'an org' entirely

- have default org show up as ‘Your personal organization’

* Select default org by default

* Preselect 'personal org' or first org in list by default

* clean up test selectors + fix failing runs list spec

* Add react-select for orgs select / add back showing avatar for 'personal org'

* Have loader properly display in setup project model when orgs are still loading
This commit is contained in:
Jennifer Shehane
2019-12-20 00:20:38 +06:30
committed by GitHub
parent d83788dd43
commit efe83ca84c
6 changed files with 227 additions and 245 deletions

View File

@@ -1,14 +1,14 @@
[
{
"id": "777",
"name": "Acme Developers",
"default": false
},
{
"id": "000",
"name": "Jane Lane",
"default": true
},
{
"id": "777",
"name": "Acme Developers",
"default": false
},
{
"id": "999",
"name": "Osato Devs",

View File

@@ -715,8 +715,10 @@ describe('Runs List', function () {
it('clears message after setting up to record', function () {
cy.contains('.btn', 'Set up project').click()
cy.get('.modal-body')
.contains('.btn', 'Me').click()
cy.get('.organizations-select__dropdown-indicator').click()
cy.get('.organizations-select__menu').should('be.visible')
cy.get('.organizations-select__option')
.contains('Your personal organization').click()
cy.get('.privacy-radio').find('input').last().check()
cy.get('.modal-body')
@@ -784,8 +786,10 @@ describe('Runs List', function () {
it('clears message after setting up CI', function () {
cy.contains('.btn', 'Set up a new project').click()
cy.get('.modal-body')
.contains('.btn', 'Me').click()
cy.get('.organizations-select__dropdown-indicator').click()
cy.get('.organizations-select__menu').should('be.visible')
cy.get('.organizations-select__option')
.contains('Your personal organization').click()
cy.get('.privacy-radio').find('input').last().check()
cy.get('.modal-body')

View File

@@ -80,27 +80,43 @@ describe('Set Up Project', function () {
.should('have.value', 'New Project Here')
})
describe('default owner', function () {
it('has no owner selected by default', function () {
cy.get('#me').should('not.be.selected')
cy.get('#org').should('not.be.selected')
})
it('org docs are linked', () => {
cy.contains('label', 'Who should own this')
.find('a').click().then(function () {
expect(this.ipc.externalOpen).to.be.calledWith('https://on.cypress.io/what-are-organizations')
})
it('org docs are linked', () => {
cy.contains('label', 'Who should own this')
.find('a').click().then(function () {
expect(this.ipc.externalOpen).to.be.calledWith('https://on.cypress.io/what-are-organizations')
})
})
})
describe('selecting me as owner', function () {
describe('loading behavior', function () {
beforeEach(function () {
cy.get('.btn').contains('Set up project').click()
})
it('calls getOrgs', function () {
expect(this.ipc.getOrgs).to.be.calledOnce
})
it('displays loading view before orgs load', function () {
cy.get('.loader').then(function () {
this.getOrgs.resolve(this.orgs)
})
cy.get('.loader').should('not.exist')
})
})
describe('selecting an org', function () {
describe('selecting Personal org', function () {
beforeEach(function () {
cy.get('.privacy-radio').should('not.be.visible')
this.getOrgs.resolve(this.orgs)
cy.get('.btn').contains('Set up project').click()
cy.get('.modal-content')
.contains('.btn', 'Me').click()
cy.get('.organizations-select__dropdown-indicator').click()
cy.get('.organizations-select__menu').should('be.visible')
cy.get('.organizations-select__option')
.contains('Your personal organization').click()
})
it('access docs are linked', () => {
@@ -115,25 +131,29 @@ describe('Set Up Project', function () {
.find('input').should('not.be.checked')
})
})
})
describe('selecting an org', function () {
context('with orgs', function () {
beforeEach(function () {
this.getOrgs.resolve(this.orgs)
cy.get('.btn').contains('Set up project').click()
cy.get('.modal-content')
.contains('.btn', 'An Organization').click()
})
it('lists organizations to assign to project', function () {
cy.get('#organizations-select').find('option')
cy.get('.empty-select-orgs').should('not.be.visible')
cy.get('.organizations-select__dropdown-indicator').click()
cy.get('.organizations-select__menu').should('be.visible')
cy.get('.organizations-select__option')
.should('have.length', this.orgs.length)
})
it('selects none by default', () => {
cy.get('#organizations-select').should('have.value', '')
it('selects personal org by default', function () {
cy.get('.organizations-select').contains(
'Your personal organization'
)
cy.get('.privacy-radio').should('be.visible')
})
it('opens external link on click of manage', () => {
@@ -143,23 +163,49 @@ describe('Set Up Project', function () {
})
it('displays public & private radios on select', function () {
cy.get('.privacy-radio').should('not.be.visible')
cy.get('select').select('Acme Developers')
cy.get('.organizations-select__dropdown-indicator').click()
cy.get('.organizations-select__menu').should('be.visible')
cy.get('.organizations-select__option')
.contains('Acme Developers').click()
cy.get('.privacy-radio').should('be.visible')
.find('input').should('not.be.checked')
})
})
it('clears selections when switching back to Me', function () {
cy.get('select').select('Acme Developers')
cy.get('.privacy-radio')
.find('input').first().check()
context('orgs with no default org', function () {
beforeEach(function () {
this.getOrgs.resolve(Cypress._.filter(this.orgs, { 'default': false }))
cy.get('.btn').contains('Set up project').click()
})
cy.get('.btn').contains('Me').click()
cy.get('.privacy-radio').find('input').should('not.be.checked')
cy.get('.btn').contains('An Organization').click()
it('lists organizations to assign to project', function () {
cy.get('.empty-select-orgs').should('not.be.visible')
cy.get('.organizations-select__dropdown-indicator').click()
cy.get('.organizations-select__menu').should('be.visible')
cy.get('.organizations-select__option')
// do not count the default org we removed
.should('have.length', this.orgs.length - 1)
})
cy.get('#organizations-select').should('have.value', '')
it('selects first org by default', function () {
cy.get('.organizations-select').contains(this.orgs[1].name)
})
it('opens external link on click of manage', () => {
cy.get('.manage-orgs-btn').click().then(function () {
expect(this.ipc.externalOpen).to.be.calledWith('https://on.cypress.io/dashboard/organizations')
})
})
it('displays public & private radios on select', function () {
cy.get('.organizations-select__dropdown-indicator').click()
cy.get('.organizations-select__menu').should('be.visible')
cy.get('.organizations-select__option')
.contains('Acme Developers').click()
cy.get('.privacy-radio').should('be.visible')
.find('input').should('not.be.checked')
})
})
@@ -167,13 +213,12 @@ describe('Set Up Project', function () {
beforeEach(function () {
this.getOrgs.resolve([])
cy.get('.btn').contains('Set up project').click()
cy.get('.modal-content')
.contains('.btn', 'An Organization').click()
})
it('displays empty message', () => {
cy.get('.empty-select-orgs').should('be.visible')
cy.get('.organizations-select').should('not.be.visible')
cy.get('.privacy-radio').should('not.be.visible')
})
it('opens dashboard organizations when \'create org\' is clicked', () => {
@@ -183,7 +228,7 @@ describe('Set Up Project', function () {
})
})
context('without only default org', function () {
context('with only default org', function () {
beforeEach(function () {
this.getOrgs.resolve([{
'id': '000',
@@ -192,19 +237,13 @@ describe('Set Up Project', function () {
}])
cy.get('.btn').contains('Set up project').click()
cy.get('.modal-content')
.contains('.btn', 'An Organization').click()
})
it('displays empty message', () => {
cy.get('.empty-select-orgs').should('be.visible')
})
it('opens dashboard organizations when \'create org\' is clicked', () => {
cy.contains('Create organization').click().then(function () {
expect(this.ipc.externalOpen).to.be.calledWith('https://on.cypress.io/dashboard/organizations')
})
it('displays in dropdown', () => {
cy.get('.organizations-select__dropdown-indicator').click()
cy.get('.organizations-select__menu').should('be.visible')
cy.get('.organizations-select__option').should('have.length', 1)
})
})
@@ -213,9 +252,6 @@ describe('Set Up Project', function () {
cy.clock()
this.getOrgs.resolve(this.orgs)
cy.get('.btn').contains('Set up project').click()
cy.get('.modal-content')
.contains('.btn', 'An Organization').click()
})
it('polls for orgs twice in 10+sec on click of org', function () {
@@ -226,12 +262,14 @@ describe('Set Up Project', function () {
it('updates org name on list on successful poll', function () {
this.name = 'Foo Bar Devs'
this.orgs[0].name = this.name
this.orgs[1].name = this.name
this.getOrgsAgain = this.ipc.getOrgs.onCall(2).resolves(this.orgs)
cy.tick(11000)
cy.get('#organizations-select').find('option')
cy.get('.organizations-select__dropdown-indicator').click()
cy.get('.organizations-select__menu').should('be.visible')
cy.get('.organizations-select__option')
.contains(this.name)
})
@@ -246,7 +284,9 @@ describe('Set Up Project', function () {
cy.tick(11000)
cy.get('#organizations-select').find('option')
cy.get('.organizations-select__dropdown-indicator').click()
cy.get('.organizations-select__menu').should('be.visible')
cy.get('.organizations-select__option')
.should('have.length', this.orgs.length)
})
})
@@ -256,8 +296,10 @@ describe('Set Up Project', function () {
beforeEach(function () {
this.getOrgs.resolve(this.orgs)
cy.contains('.btn', 'Set up project').click()
cy.get('.modal-body')
.contains('.btn', 'Me').click()
cy.get('.organizations-select__dropdown-indicator').click()
cy.get('.organizations-select__menu').should('be.visible')
cy.get('.organizations-select__option')
.contains('Your personal organization').click()
cy.get('.privacy-radio').find('input').last().check()
@@ -292,11 +334,12 @@ describe('Set Up Project', function () {
})
it('sends project name, org id, and public flag to ipc event', function () {
cy.get('.modal-body')
.contains('.btn', 'An Organization').click()
cy.get('#projectName').clear().type('New Project')
cy.get('select').select('Acme Developers')
cy.get('.organizations-select__dropdown-indicator').click()
cy.get('.organizations-select__menu').should('be.visible')
cy.get('.organizations-select__option')
.contains('Acme Developers').click()
cy.get('.privacy-radio').find('input').first().check()
cy.get('.modal-body')
@@ -312,10 +355,11 @@ describe('Set Up Project', function () {
context('org/public', function () {
beforeEach(function () {
cy.get('.modal-body')
.contains('.btn', 'An Organization').click()
cy.get('.organizations-select__dropdown-indicator').click()
cy.get('.organizations-select__menu').should('be.visible')
cy.get('.organizations-select__option')
.contains('Acme Developers').click()
cy.get('select').select('Acme Developers')
cy.get('.privacy-radio').find('input').first().check()
cy.get('.modal-body')
@@ -333,11 +377,12 @@ describe('Set Up Project', function () {
context('me/private', function () {
beforeEach(function () {
cy.get('.modal-body')
.contains('.btn', 'Me').click()
cy.get('.organizations-select__dropdown-indicator').click()
cy.get('.organizations-select__menu').should('be.visible')
cy.get('.organizations-select__option')
.contains('Your personal organization').click()
cy.get('.privacy-radio').find('input').last().check()
cy.get('.modal-body')
.contains('.btn', 'Set up project').click()
})
@@ -353,11 +398,12 @@ describe('Set Up Project', function () {
context('me/public', function () {
beforeEach(function () {
cy.get('.modal-body')
.contains('.btn', 'Me').click()
cy.get('.organizations-select__dropdown-indicator').click()
cy.get('.organizations-select__menu').should('be.visible')
cy.get('.organizations-select__option')
.contains('Your personal organization').click()
cy.get('.privacy-radio').find('input').first().check()
cy.get('.modal-body')
.contains('.btn', 'Set up project').click()
})
@@ -392,8 +438,10 @@ describe('Set Up Project', function () {
beforeEach(function () {
this.getOrgs.resolve(this.orgs)
cy.contains('.btn', 'Set up project').click()
cy.get('.modal-body')
.contains('.btn', 'Me').click()
cy.get('.organizations-select__dropdown-indicator').click()
cy.get('.organizations-select__menu').should('be.visible')
cy.get('.organizations-select__option')
.contains('Your personal organization').click()
cy.get('.privacy-radio').find('input').last().check()

View File

@@ -17,7 +17,7 @@
"watch": "npm run build -- --watch --progress"
},
"devDependencies": {
"@babel/polyfill": "^7.7.0",
"@babel/polyfill": "7.7.0",
"@cypress/icons": "0.7.0",
"@cypress/json-schemas": "5.33.0",
"@cypress/react-tooltip": "0.5.3",
@@ -41,8 +41,9 @@
"react": "16.8.6",
"react-bootstrap-modal": "4.2.0",
"react-dom": "16.8.6",
"react-inspector": "^4.0.0",
"react-inspector": "4.0.0",
"react-loader": "2.4.5",
"react-select": "3.0.8",
"webpack": "4.35.3",
"webpack-cli": "3.3.2"
},

View File

@@ -322,6 +322,15 @@
}
.setup-project-modal {
form {
overflow: visible;
}
.loader {
position: relative;
min-height: 181px;
}
a>i {
color: #999;
cursor: pointer;
@@ -345,34 +354,14 @@
}
.owner-parts {
overflow: auto;
clear: both;
}
.owner-part-one {
float: left;
width: 42%;
margin-right: 1%;
}
.owner-part-two {
float: left;
width: 57%;
}
.radio + .radio {
margin-bottom: 0;
}
.form-horizontal {
background: none;
margin: 0;
padding: 0;
border: 0;
.form-group {
overflow: auto;
}
}
.help-block {
@@ -390,22 +379,10 @@
border-color: #dadada !important;
}
.btn-group .btn {
padding: 4px 12px;
i {
font-size: 12px;
position: relative;
top: -1px;
margin-right: 2px;
}
}
.manage-orgs-btn {
padding: 0;
font-size: 12px;
margin-top: 20px;
line-height: 30px;
line-height: 20px;
margin-left: 10px;
}
@@ -415,7 +392,7 @@
}
.well.empty-select-orgs {
margin-top: 20px;
margin-top: 10px;
a.btn.btn-link {
padding: 0;
@@ -429,6 +406,7 @@
.user-avatar {
position: relative;
top: -2px;
margin-right: 5px;
}
.control-label {
@@ -443,7 +421,7 @@
input[type='text'].form-control {
padding: 3px 6px;
height: 28px;
height: 38px;
}
.privacy-radio {
@@ -469,6 +447,8 @@
.actions.form-group {
margin-bottom: 0;
display: flex;
flex-direction: row-reverse;
button {
position: relative;
@@ -478,12 +458,13 @@
.text-danger {
margin-bottom: 0;
}
.alert-danger {
margin: 0 0 1em;
pre.alert-danger {
margin: 1em 0;
padding: 5px;
max-height: 200px;
overflow: auto;
text-align: left;
border-radius: 5px;
}
}
@@ -531,11 +512,9 @@
}
.select-orgs {
overflow: auto;
select {
float: left;
width: 205px;
margin-top: 20px;
margin-top: 10px;
}
}

View File

@@ -5,10 +5,11 @@ import PropTypes from 'prop-types'
import { observer } from 'mobx-react'
import BootstrapModal from 'react-bootstrap-modal'
import Loader from 'react-loader'
import Select from 'react-select'
import { gravatarUrl } from '../lib/utils'
import authStore from '../auth/auth-store'
import ipc from '../lib/ipc'
import { gravatarUrl } from '../lib/utils'
import orgsStore from '../organizations/organizations-store'
import orgsApi from '../organizations/organizations-api'
@@ -26,8 +27,7 @@ class SetupProject extends Component {
error: null,
projectName: this.props.project.displayName,
public: null,
owner: null,
orgId: null,
selectedOrg: {},
showNameMissingError: false,
isSubmitting: false,
}
@@ -75,10 +75,6 @@ class SetupProject extends Component {
return null
}
if (!orgsStore.isLoaded) {
this._loading()
}
return (
<div className='setup-project-modal modal-body os-dialog'>
<BootstrapModal.Dismiss className='btn btn-link close'>x</BootstrapModal.Dismiss>
@@ -91,7 +87,7 @@ class SetupProject extends Component {
{this._accessSelector()}
{this._error()}
<div className='actions form-group'>
<div className='pull-right'>
<div>
<button
disabled={this.state.isSubmitting || this._formNotFilled()}
className='btn btn-primary btn-block'
@@ -110,15 +106,6 @@ class SetupProject extends Component {
)
}
_loading () {
return (
<div className='setup-project-modal modal-body os-dialog'>
<BootstrapModal.Dismiss className='btn btn-link close'>x</BootstrapModal.Dismiss>
<Loader color='#888' scale={0.5} />
</div>
)
}
_nameField () {
return (
<div className='form-group'>
@@ -154,110 +141,91 @@ class SetupProject extends Component {
<a onClick={this._openOrgDocs}>
<i className='fas fa-question-circle'></i>
</a>
</label>
<a
href='#'
className='btn btn-link manage-orgs-btn pull-right'
onClick={this._manageOrgs}>
Manage organizations
</a>
</div>
<div className='owner-parts'>
<div>
<div className='btn-group' data-toggle='buttons'>
<label className={cs('btn btn-default', {
'active': this.state.owner === 'me',
})}>
<input
type='radio'
name='owner-toggle'
id='me'
autoComplete='off'
value='me'
checked={this.state.owner === 'me'}
onChange={this._updateOwner}
/>
<img
className='user-avatar'
height='13'
width='13'
src={`${gravatarUrl(authStore.user && authStore.user.email)}`}
/>
{' '}Me
</label>
<label className={`btn btn-default ${this.state.owner === 'org' ? 'active' : ''}`}>
<input
type='radio'
name='owner-toggle'
id='org'
autoComplete='off'
value='org'
checked={this.state.owner === 'org'}
onChange={this._updateOwner}
/>
<i className='far fa-building'></i>
{' '}An Organization
</label>
</div>
</div>
<div className='select-orgs'>
<div className={cs({ 'hidden': this.state.owner !== 'org' || this._hasOrgsOtherThanDefault() })}>
<div className='empty-select-orgs well'>
<p>You don't have any organizations yet.</p>
<p>Organizations can help you manage projects, including billing.</p>
<p>
<a
href='#'
className={cs('btn btn-link', { 'hidden': this.state.owner !== 'org' })}
onClick={this._manageOrgs}>
<i className='fas fa-plus'></i>{' '}
Create organization
</a>
</p>
</div>
</div>
{this._orgSelector()}
{
orgsStore.isLoaded ?
this._hasOrgs() ?
this._orgSelector() :
<div className='empty-select-orgs well'>
<p>You don't have any organizations yet.</p>
<p>Organizations can help you manage projects, including billing.</p>
<p>
<a
href='#'
className='btn btn-link'
onClick={this._manageOrgs}>
<i className='fas fa-plus'></i>{' '}
Create organization
</a>
</p>
</div>
: <Loader color='#888' scale={0.5} />
}
</div>
</div>
</div>
)
}
_hasOrgsOtherThanDefault () {
return orgsStore.orgs.length > 1
_hasOrgs () {
return orgsStore.orgs.length
}
_orgSelectValue (options) {
if (!_.isEmpty(this.state.selectedOrg)) {
return this.state.selectedOrg
}
return this._hasDefaultOrg() ?
_.find(options, { default: true }) :
options[0]
}
_orgSelector () {
return (
<div className={cs({ 'hidden': this.state.owner !== 'org' || !(this._hasOrgsOtherThanDefault()) })}>
<select
ref='orgId'
id='organizations-select'
className='form-control float-left'
value={this.state.orgId || ''}
onChange={this._updateOrgId}
>
<option value=''>-- Select organization --</option>
{_.map(orgsStore.orgs, (org) => {
if (org.default) return null
const options = _.map(orgsStore.orgs, (org) => {
return {
value: org.id,
default: org.default,
label: org.default ?
<div>
<img
className='user-avatar'
height='13'
width='13'
src={`${gravatarUrl(authStore.user && authStore.user.email)}`}
/>
Your personal organization
</div> : org.name,
}
})
return (
<option
key={org.id}
value={org.id}
>
{org.name}
</option>
)
})}
</select>
<a
href='#'
className='btn btn-link manage-orgs-btn float-left'
onClick={this._manageOrgs}>
(manage organizations)
</a>
return (
<div className={!this._hasOrgs() ? 'hidden' : ''}>
<Select
className='organizations-select'
classNamePrefix='organizations-select'
value={this._orgSelectValue(options)}
onChange={this._updateSelectedOrg}
isLoading={!orgsStore.isLoaded}
options={options}
/>
</div>
)
}
_accessSelector () {
return (
<div className={cs({ 'hidden': !this.state.orgId })}>
<div className={cs({ 'hidden': !this._hasOrgs() })}>
<hr />
<label htmlFor='projectName' className='control-label'>
Who should see the runs and recordings?
@@ -334,17 +302,15 @@ class SetupProject extends Component {
)
}
_updateOrgId = () => {
const orgIsNotSelected = this.refs.orgId.value === '-- Select Organization --'
const orgId = orgIsNotSelected ? null : this.refs.orgId.value
_updateSelectedOrg = (selectedOrg, action) => {
const orgIsNotSelected = _.isEmpty(selectedOrg)
this.setState({
orgId,
selectedOrg,
})
// deselect their choice for access
// if they didn'tselect anything
// if they didn't select anything
if (orgIsNotSelected) {
this.setState({
public: null,
@@ -362,24 +328,8 @@ class SetupProject extends Component {
return _.trim(this.state.projectName)
}
_updateOwner = (e) => {
let owner = e.target.value
// if they clicked the same radio button that's
// already selected, then ignore it
if (this.state.owner === owner) return
const defaultOrg = _.find(orgsStore.orgs, { default: true })
let chosenOrgId = owner === 'me' ? defaultOrg.id : null
// we want to clear all selects below the radio buttons
// otherwise it looks jarring to already have selects
this.setState({
owner,
orgId: chosenOrgId,
public: null,
})
_hasDefaultOrg () {
return _.find(orgsStore.orgs, { default: true })
}
_updateAccess = (e) => {
@@ -409,7 +359,7 @@ class SetupProject extends Component {
_setupProject () {
ipc.setupDashboardProject({
projectName: this.state.projectName,
orgId: this.state.orgId,
orgId: this.state.selectedOrg.value,
public: this.state.public,
})
.then((projectDetails) => {