@@ -18,7 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import { getJSON } from '../helpers/request'; | |||
import { getJSON, post } from '../helpers/request'; | |||
export const getOrganizations = (organizations?: Array<string>) => { | |||
const data = {}; | |||
@@ -27,3 +27,28 @@ export const getOrganizations = (organizations?: Array<string>) => { | |||
} | |||
return getJSON('/api/organizations/search', data); | |||
}; | |||
type GetOrganizationType = null | { | |||
avatar: null | string, | |||
description: null | string, | |||
key: string, | |||
name: string, | |||
url: null | string | |||
}; | |||
type GetOrganizationNavigation = { | |||
canAdmin: boolean, | |||
isDefault: boolean | |||
}; | |||
export const getOrganization = (key: string): Promise<GetOrganizationType> => { | |||
return getOrganizations([key]).then(r => r.organizations.find(o => o.key === key)); | |||
}; | |||
export const getOrganizationNavigation = (key: string): Promise<GetOrganizationNavigation> => { | |||
return getJSON('/api/navigation/organization', { organization: key }).then(r => r.organization); | |||
}; | |||
export const updateOrganization = (key: string, changes: {}) => ( | |||
post('/api/organizations/update', { key, ...changes }) | |||
); |
@@ -50,6 +50,7 @@ import groupsRoutes from '../../apps/groups/routes'; | |||
import issuesRoutes from '../../apps/issues/routes'; | |||
import metricsRoutes from '../../apps/metrics/routes'; | |||
import overviewRoutes from '../../apps/overview/routes'; | |||
import organizationsRouters from '../../apps/organizations/routes'; | |||
import permissionTemplatesRoutes from '../../apps/permission-templates/routes'; | |||
import projectActivityRoutes from '../../apps/projectActivity/routes'; | |||
import projectAdminRoutes from '../../apps/project-admin/routes'; | |||
@@ -116,6 +117,7 @@ const startReactApp = () => { | |||
<Route path="component">{componentRoutes}</Route> | |||
<Route path="extension/:pluginKey/:extensionKey" component={GlobalPageExtension}/> | |||
<Route path="issues">{issuesRoutes}</Route> | |||
<Route path="organizations">{organizationsRouters}</Route> | |||
<Route path="projects">{projectsRoutes}</Route> | |||
<Route path="quality_gates">{qualityGatesRoutes}</Route> | |||
<Route path="profiles">{qualityProfilesRoutes}</Route> |
@@ -0,0 +1,48 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import * as api from '../../api/organizations'; | |||
import { onFail } from '../../store/rootActions'; | |||
import * as actions from '../../store/organizations/duck'; | |||
import { addGlobalSuccessMessage } from '../../store/globalMessages/duck'; | |||
import { translate } from '../../helpers/l10n'; | |||
export const fetchOrganization = (key: string): Function => (dispatch: Function): Promise<*> => { | |||
const onFulfilled = ([organization, navigation]) => { | |||
if (organization) { | |||
const organizationWithPermissions = { ...organization, ...navigation }; | |||
dispatch(actions.receiveOrganizations([organizationWithPermissions])); | |||
} | |||
}; | |||
return Promise.all([ | |||
api.getOrganization(key), | |||
api.getOrganizationNavigation(key) | |||
]).then(onFulfilled, onFail(dispatch)); | |||
}; | |||
export const updateOrganization = (key: string, changes: {}): Function => (dispatch: Function): Promise<*> => { | |||
const onFulfilled = () => { | |||
dispatch(actions.updateOrganization(key, changes)); | |||
dispatch(addGlobalSuccessMessage(translate('organization.updated'))); | |||
}; | |||
return api.updateOrganization(key, changes).then(onFulfilled, onFail(dispatch)); | |||
}; |
@@ -0,0 +1,65 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { getOrganizationByKey } from '../../../store/rootReducer'; | |||
import handleRequiredAuthorization from '../../../app/utils/handleRequiredAuthorization'; | |||
class OrganizationAdmin extends React.Component { | |||
props: { | |||
children: Object, | |||
organization: { canAdmin: boolean }, | |||
}; | |||
componentDidMount () { | |||
this.checkPermissions(); | |||
} | |||
componentDidUpdate () { | |||
this.checkPermissions(); | |||
} | |||
isOrganizationAdmin () { | |||
return this.props.organization.canAdmin; | |||
} | |||
checkPermissions () { | |||
if (!this.isOrganizationAdmin()) { | |||
handleRequiredAuthorization(); | |||
} | |||
} | |||
render () { | |||
if (!this.isOrganizationAdmin()) { | |||
return null; | |||
} | |||
return this.props.children; | |||
} | |||
} | |||
const mapStateToProps = (state, ownProps) => ({ | |||
organization: getOrganizationByKey(state, ownProps.params.organizationKey) | |||
}); | |||
export default connect(mapStateToProps)(OrganizationAdmin); | |||
export const UnconnectedOrganizationAdmin = OrganizationAdmin; |
@@ -0,0 +1,195 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import debounce from 'lodash/debounce'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { Organization } from '../../../store/organizations/duck'; | |||
import { getOrganizationByKey } from '../../../store/rootReducer'; | |||
import { updateOrganization } from '../actions'; | |||
type Props = { | |||
organization: Organization, | |||
updateOrganization: (string, Object) => Promise<*> | |||
}; | |||
class OrganizationEdit extends React.Component { | |||
mounted: boolean; | |||
props: Props; | |||
state: { | |||
loading: boolean, | |||
avatar: string, | |||
avatarImage: string, | |||
description: string, | |||
name: string, | |||
url: string | |||
}; | |||
constructor (props: Props) { | |||
super(props); | |||
this.state = { | |||
loading: false, | |||
avatar: props.organization.avatar || '', | |||
avatarImage: props.organization.avatar || '', | |||
description: props.organization.description || '', | |||
name: props.organization.name, | |||
url: props.organization.url || '' | |||
}; | |||
this.changeAvatarImage = debounce(this.changeAvatarImage, 500); | |||
} | |||
componentDidMount () { | |||
this.mounted = true; | |||
} | |||
componentWillUnmount () { | |||
this.mounted = false; | |||
} | |||
handleAvatarInputChange = (e: Object) => { | |||
const { value } = e.target; | |||
this.setState({ avatar: value }); | |||
this.changeAvatarImage(value); | |||
}; | |||
changeAvatarImage = (value: string) => { | |||
this.setState({ avatarImage: value }); | |||
}; | |||
handleSubmit = (e: Object) => { | |||
e.preventDefault(); | |||
const changes = { | |||
avatar: this.state.avatar, | |||
description: this.state.description, | |||
name: this.state.name, | |||
url: this.state.url | |||
}; | |||
this.setState({ loading: true }); | |||
this.props.updateOrganization(this.props.organization.key, changes).then(() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
}); | |||
}; | |||
render () { | |||
return ( | |||
<div className="page page-limited"> | |||
<header className="page-header"> | |||
<h1 className="page-title">{translate('organization.edit')}</h1> | |||
</header> | |||
<form onSubmit={this.handleSubmit}> | |||
<div className="modal-field"> | |||
<label htmlFor="organization-name"> | |||
{translate('organization.name')} | |||
<em className="mandatory">*</em> | |||
</label> | |||
<input id="organization-name" | |||
name="name" | |||
required={true} | |||
type="text" | |||
maxLength="64" | |||
value={this.state.name} | |||
disabled={this.state.loading} | |||
onChange={e => this.setState({ name: e.target.value })}/> | |||
<div className="modal-field-description"> | |||
{translate('organization.name.description')} | |||
</div> | |||
</div> | |||
<div className="modal-field"> | |||
<label htmlFor="organization-avatar"> | |||
{translate('organization.avatar')} | |||
</label> | |||
<input id="organization-avatar" | |||
name="avatar" | |||
type="text" | |||
maxLength="256" | |||
value={this.state.avatar} | |||
disabled={this.state.loading} | |||
onChange={this.handleAvatarInputChange}/> | |||
<div className="modal-field-description"> | |||
{translate('organization.avatar.description')} | |||
</div> | |||
{!!this.state.avatarImage && ( | |||
<div className="spacer-top spacer-bottom"> | |||
<div className="little-spacer-bottom"> | |||
{translate('organization.avatar.preview')} | |||
{':'} | |||
</div> | |||
<img src={this.state.avatarImage} alt="" height={30}/> | |||
</div> | |||
)} | |||
</div> | |||
<div className="modal-field"> | |||
<label htmlFor="organization-description"> | |||
{translate('description')} | |||
</label> | |||
<textarea id="organization-description" | |||
name="description" | |||
rows="3" | |||
maxLength="256" | |||
value={this.state.description} | |||
disabled={this.state.loading} | |||
onChange={e => this.setState({ description: e.target.value })}/> | |||
<div className="modal-field-description"> | |||
{translate('organization.description.description')} | |||
</div> | |||
</div> | |||
<div className="modal-field"> | |||
<label htmlFor="organization-url"> | |||
{translate('organization.url')} | |||
</label> | |||
<input id="organization-url" | |||
name="url" | |||
type="text" | |||
maxLength="256" | |||
value={this.state.url} | |||
disabled={this.state.loading} | |||
onChange={e => this.setState({ url: e.target.value })}/> | |||
<div className="modal-field-description"> | |||
{translate('organization.url.description')} | |||
</div> | |||
</div> | |||
<div className="modal-field"> | |||
<button type="submit" disabled={this.state.loading}>{translate('save')}</button> | |||
{this.state.loading && ( | |||
<i className="spinner spacer-left"/> | |||
)} | |||
</div> | |||
</form> | |||
</div> | |||
); | |||
} | |||
} | |||
const mapStateToProps = (state, ownProps) => ({ | |||
organization: getOrganizationByKey(state, ownProps.params.organizationKey) | |||
}); | |||
const mapDispatchToProps = { updateOrganization }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(OrganizationEdit); | |||
export const UnconnectedOrganizationEdit = OrganizationEdit; |
@@ -0,0 +1,89 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import OrganizationNavigation from '../navigation/OrganizationNavigation'; | |||
import { fetchOrganization } from '../actions'; | |||
import { getOrganizationByKey } from '../../../store/rootReducer'; | |||
import type { Organization } from '../../../store/organizations/duck'; | |||
import NotFound from '../../../app/components/NotFound'; | |||
type OwnProps = { | |||
params: { organizationKey: string } | |||
}; | |||
class OrganizationPage extends React.Component { | |||
mounted: boolean; | |||
props: { | |||
children: Object, | |||
location: Object, | |||
organization: null | Organization, | |||
params: { organizationKey: string }, | |||
fetchOrganization: (string) => Promise<*> | |||
}; | |||
state = { | |||
loading: true | |||
}; | |||
componentDidMount () { | |||
this.mounted = true; | |||
this.props.fetchOrganization(this.props.params.organizationKey).then(() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
}); | |||
} | |||
componentWillUnmount () { | |||
this.mounted = false; | |||
} | |||
render () { | |||
const { organization } = this.props; | |||
if (!organization || organization.isDefault == null) { | |||
if (this.state.loading) { | |||
return null; | |||
} else { | |||
return <NotFound/>; | |||
} | |||
} | |||
return ( | |||
<div> | |||
<OrganizationNavigation organization={organization} location={this.props.location}/> | |||
{this.props.children} | |||
</div> | |||
); | |||
} | |||
} | |||
const mapStateToProps = (state, ownProps: OwnProps) => ({ | |||
organization: getOrganizationByKey(state, ownProps.params.organizationKey) | |||
}); | |||
const mapDispatchToProps = { fetchOrganization }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(OrganizationPage); | |||
export const UnconnectedOrganizationPage = OrganizationPage; |
@@ -0,0 +1,40 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import { UnconnectedOrganizationAdmin } from '../OrganizationAdmin'; | |||
it('should render children', () => { | |||
const organization = { canAdmin: true }; | |||
expect(shallow( | |||
<UnconnectedOrganizationAdmin organization={organization}> | |||
<div>hello</div> | |||
</UnconnectedOrganizationAdmin> | |||
)).toMatchSnapshot(); | |||
}); | |||
it('should not render anything', () => { | |||
const organization = { canAdmin: false }; | |||
expect(shallow( | |||
<UnconnectedOrganizationAdmin organization={organization}> | |||
<div>hello</div> | |||
</UnconnectedOrganizationAdmin> | |||
)).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,40 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import { UnconnectedOrganizationEdit } from '../OrganizationEdit'; | |||
it('smoke test', () => { | |||
const organization = { key: 'foo', name: 'Foo' }; | |||
const wrapper = shallow(<UnconnectedOrganizationEdit organization={organization}/>); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.setState({ | |||
avatar: 'foo-avatar', | |||
avatarImage: 'foo-avatar-image', | |||
description: 'foo-description', | |||
name: 'New Foo', | |||
url: 'foo-url' | |||
}); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.setState({ loading: true }); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,45 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import { UnconnectedOrganizationPage } from '../OrganizationPage'; | |||
it('smoke test', () => { | |||
const wrapper = shallow( | |||
<UnconnectedOrganizationPage> | |||
<div>hello</div> | |||
</UnconnectedOrganizationPage> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
const organization = { key: 'foo', name: 'Foo', isDefault: false, canAdmin: false }; | |||
wrapper.setProps({ organization }); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('not found', () => { | |||
const wrapper = shallow( | |||
<UnconnectedOrganizationPage> | |||
<div>hello</div> | |||
</UnconnectedOrganizationPage> | |||
); | |||
wrapper.setState({ loading: false }); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,7 @@ | |||
exports[`test should not render anything 1`] = `null`; | |||
exports[`test should render children 1`] = ` | |||
<div> | |||
hello | |||
</div> | |||
`; |
@@ -0,0 +1,340 @@ | |||
exports[`test smoke test 1`] = ` | |||
<div | |||
className="page page-limited"> | |||
<header | |||
className="page-header"> | |||
<h1 | |||
className="page-title"> | |||
organization.edit | |||
</h1> | |||
</header> | |||
<form | |||
onSubmit={[Function]}> | |||
<div | |||
className="modal-field"> | |||
<label | |||
htmlFor="organization-name"> | |||
organization.name | |||
<em | |||
className="mandatory"> | |||
* | |||
</em> | |||
</label> | |||
<input | |||
disabled={false} | |||
id="organization-name" | |||
maxLength="64" | |||
name="name" | |||
onChange={[Function]} | |||
required={true} | |||
type="text" | |||
value="Foo" /> | |||
<div | |||
className="modal-field-description"> | |||
organization.name.description | |||
</div> | |||
</div> | |||
<div | |||
className="modal-field"> | |||
<label | |||
htmlFor="organization-avatar"> | |||
organization.avatar | |||
</label> | |||
<input | |||
disabled={false} | |||
id="organization-avatar" | |||
maxLength="256" | |||
name="avatar" | |||
onChange={[Function]} | |||
type="text" | |||
value="" /> | |||
<div | |||
className="modal-field-description"> | |||
organization.avatar.description | |||
</div> | |||
</div> | |||
<div | |||
className="modal-field"> | |||
<label | |||
htmlFor="organization-description"> | |||
description | |||
</label> | |||
<textarea | |||
disabled={false} | |||
id="organization-description" | |||
maxLength="256" | |||
name="description" | |||
onChange={[Function]} | |||
rows="3" | |||
value="" /> | |||
<div | |||
className="modal-field-description"> | |||
organization.description.description | |||
</div> | |||
</div> | |||
<div | |||
className="modal-field"> | |||
<label | |||
htmlFor="organization-url"> | |||
organization.url | |||
</label> | |||
<input | |||
disabled={false} | |||
id="organization-url" | |||
maxLength="256" | |||
name="url" | |||
onChange={[Function]} | |||
type="text" | |||
value="" /> | |||
<div | |||
className="modal-field-description"> | |||
organization.url.description | |||
</div> | |||
</div> | |||
<div | |||
className="modal-field"> | |||
<button | |||
disabled={false} | |||
type="submit"> | |||
save | |||
</button> | |||
</div> | |||
</form> | |||
</div> | |||
`; | |||
exports[`test smoke test 2`] = ` | |||
<div | |||
className="page page-limited"> | |||
<header | |||
className="page-header"> | |||
<h1 | |||
className="page-title"> | |||
organization.edit | |||
</h1> | |||
</header> | |||
<form | |||
onSubmit={[Function]}> | |||
<div | |||
className="modal-field"> | |||
<label | |||
htmlFor="organization-name"> | |||
organization.name | |||
<em | |||
className="mandatory"> | |||
* | |||
</em> | |||
</label> | |||
<input | |||
disabled={false} | |||
id="organization-name" | |||
maxLength="64" | |||
name="name" | |||
onChange={[Function]} | |||
required={true} | |||
type="text" | |||
value="New Foo" /> | |||
<div | |||
className="modal-field-description"> | |||
organization.name.description | |||
</div> | |||
</div> | |||
<div | |||
className="modal-field"> | |||
<label | |||
htmlFor="organization-avatar"> | |||
organization.avatar | |||
</label> | |||
<input | |||
disabled={false} | |||
id="organization-avatar" | |||
maxLength="256" | |||
name="avatar" | |||
onChange={[Function]} | |||
type="text" | |||
value="foo-avatar" /> | |||
<div | |||
className="modal-field-description"> | |||
organization.avatar.description | |||
</div> | |||
<div | |||
className="spacer-top spacer-bottom"> | |||
<div | |||
className="little-spacer-bottom"> | |||
organization.avatar.preview | |||
: | |||
</div> | |||
<img | |||
alt="" | |||
height={30} | |||
src="foo-avatar-image" /> | |||
</div> | |||
</div> | |||
<div | |||
className="modal-field"> | |||
<label | |||
htmlFor="organization-description"> | |||
description | |||
</label> | |||
<textarea | |||
disabled={false} | |||
id="organization-description" | |||
maxLength="256" | |||
name="description" | |||
onChange={[Function]} | |||
rows="3" | |||
value="foo-description" /> | |||
<div | |||
className="modal-field-description"> | |||
organization.description.description | |||
</div> | |||
</div> | |||
<div | |||
className="modal-field"> | |||
<label | |||
htmlFor="organization-url"> | |||
organization.url | |||
</label> | |||
<input | |||
disabled={false} | |||
id="organization-url" | |||
maxLength="256" | |||
name="url" | |||
onChange={[Function]} | |||
type="text" | |||
value="foo-url" /> | |||
<div | |||
className="modal-field-description"> | |||
organization.url.description | |||
</div> | |||
</div> | |||
<div | |||
className="modal-field"> | |||
<button | |||
disabled={false} | |||
type="submit"> | |||
save | |||
</button> | |||
</div> | |||
</form> | |||
</div> | |||
`; | |||
exports[`test smoke test 3`] = ` | |||
<div | |||
className="page page-limited"> | |||
<header | |||
className="page-header"> | |||
<h1 | |||
className="page-title"> | |||
organization.edit | |||
</h1> | |||
</header> | |||
<form | |||
onSubmit={[Function]}> | |||
<div | |||
className="modal-field"> | |||
<label | |||
htmlFor="organization-name"> | |||
organization.name | |||
<em | |||
className="mandatory"> | |||
* | |||
</em> | |||
</label> | |||
<input | |||
disabled={true} | |||
id="organization-name" | |||
maxLength="64" | |||
name="name" | |||
onChange={[Function]} | |||
required={true} | |||
type="text" | |||
value="New Foo" /> | |||
<div | |||
className="modal-field-description"> | |||
organization.name.description | |||
</div> | |||
</div> | |||
<div | |||
className="modal-field"> | |||
<label | |||
htmlFor="organization-avatar"> | |||
organization.avatar | |||
</label> | |||
<input | |||
disabled={true} | |||
id="organization-avatar" | |||
maxLength="256" | |||
name="avatar" | |||
onChange={[Function]} | |||
type="text" | |||
value="foo-avatar" /> | |||
<div | |||
className="modal-field-description"> | |||
organization.avatar.description | |||
</div> | |||
<div | |||
className="spacer-top spacer-bottom"> | |||
<div | |||
className="little-spacer-bottom"> | |||
organization.avatar.preview | |||
: | |||
</div> | |||
<img | |||
alt="" | |||
height={30} | |||
src="foo-avatar-image" /> | |||
</div> | |||
</div> | |||
<div | |||
className="modal-field"> | |||
<label | |||
htmlFor="organization-description"> | |||
description | |||
</label> | |||
<textarea | |||
disabled={true} | |||
id="organization-description" | |||
maxLength="256" | |||
name="description" | |||
onChange={[Function]} | |||
rows="3" | |||
value="foo-description" /> | |||
<div | |||
className="modal-field-description"> | |||
organization.description.description | |||
</div> | |||
</div> | |||
<div | |||
className="modal-field"> | |||
<label | |||
htmlFor="organization-url"> | |||
organization.url | |||
</label> | |||
<input | |||
disabled={true} | |||
id="organization-url" | |||
maxLength="256" | |||
name="url" | |||
onChange={[Function]} | |||
type="text" | |||
value="foo-url" /> | |||
<div | |||
className="modal-field-description"> | |||
organization.url.description | |||
</div> | |||
</div> | |||
<div | |||
className="modal-field"> | |||
<button | |||
disabled={true} | |||
type="submit"> | |||
save | |||
</button> | |||
<i | |||
className="spinner spacer-left" /> | |||
</div> | |||
</form> | |||
</div> | |||
`; |
@@ -0,0 +1,20 @@ | |||
exports[`test not found 1`] = `<NotFound />`; | |||
exports[`test smoke test 1`] = `null`; | |||
exports[`test smoke test 2`] = ` | |||
<div> | |||
<OrganizationNavigation | |||
organization={ | |||
Object { | |||
"canAdmin": false, | |||
"isDefault": false, | |||
"key": "foo", | |||
"name": "Foo", | |||
} | |||
} /> | |||
<div> | |||
hello | |||
</div> | |||
</div> | |||
`; |
@@ -0,0 +1,90 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { Link, IndexLink } from 'react-router'; | |||
import { translate } from '../../../helpers/l10n'; | |||
const ADMIN_PATHS = [ | |||
'edit' | |||
]; | |||
export default class OrganizationNavigation extends React.Component { | |||
props: { | |||
location: { pathname: string }, | |||
organization: { | |||
key: string, | |||
name: string, | |||
canAdmin?: boolean | |||
} | |||
}; | |||
renderAdministration () { | |||
const { organization, location } = this.props; | |||
const adminActive = ADMIN_PATHS.some(path => | |||
location.pathname.endsWith(`organizations/${organization.key}/${path}`) | |||
); | |||
return ( | |||
<li className={adminActive ? 'active': ''}> | |||
<a className="dropdown-toggle navbar-admin-link" data-toggle="dropdown" href="#"> | |||
{translate('layout.settings')} <i className="icon-dropdown"/> | |||
</a> | |||
<ul className="dropdown-menu"> | |||
<li> | |||
<Link to={`/organizations/${organization.key}/edit`} activeClassName="active"> | |||
{translate('edit')} | |||
</Link> | |||
</li> | |||
</ul> | |||
</li> | |||
); | |||
} | |||
render () { | |||
const { organization } = this.props; | |||
return ( | |||
<nav className="navbar navbar-context page-container" id="context-navigation"> | |||
<div className="navbar-context-inner"> | |||
<div className="container"> | |||
<ul className="nav navbar-nav nav-crumbs"> | |||
<li> | |||
<Link to={`/organizations/${organization.key}`}> | |||
{organization.name} | |||
</Link> | |||
</li> | |||
</ul> | |||
<ul className="nav navbar-nav nav-tabs"> | |||
<li> | |||
<IndexLink to={`/organizations/${organization.key}`} activeClassName="active"> | |||
<i className="icon-home"/> | |||
</IndexLink> | |||
</li> | |||
{organization.canAdmin && this.renderAdministration()} | |||
</ul> | |||
</div> | |||
</div> | |||
</nav> | |||
); | |||
} | |||
} |
@@ -0,0 +1,36 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import OrganizationNavigation from '../OrganizationNavigation'; | |||
it('regular user', () => { | |||
const organization = { key: 'foo', name: 'Foo', canAdmin: false }; | |||
expect(shallow( | |||
<OrganizationNavigation location={{ pathname: '/organizations/foo' }} organization={organization}/> | |||
)).toMatchSnapshot(); | |||
}); | |||
it('admin', () => { | |||
const organization = { key: 'foo', name: 'Foo', canAdmin: true }; | |||
expect(shallow( | |||
<OrganizationNavigation location={{ pathname: '/organizations/foo' }} organization={organization}/> | |||
)).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,93 @@ | |||
exports[`test admin 1`] = ` | |||
<nav | |||
className="navbar navbar-context page-container" | |||
id="context-navigation"> | |||
<div | |||
className="navbar-context-inner"> | |||
<div | |||
className="container"> | |||
<ul | |||
className="nav navbar-nav nav-crumbs"> | |||
<li> | |||
<Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/organizations/foo"> | |||
Foo | |||
</Link> | |||
</li> | |||
</ul> | |||
<ul | |||
className="nav navbar-nav nav-tabs"> | |||
<li> | |||
<IndexLink | |||
activeClassName="active" | |||
to="/organizations/foo"> | |||
<i | |||
className="icon-home" /> | |||
</IndexLink> | |||
</li> | |||
<li | |||
className=""> | |||
<a | |||
className="dropdown-toggle navbar-admin-link" | |||
data-toggle="dropdown" | |||
href="#"> | |||
layout.settings | |||
<i | |||
className="icon-dropdown" /> | |||
</a> | |||
<ul | |||
className="dropdown-menu"> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/organizations/foo/edit"> | |||
edit | |||
</Link> | |||
</li> | |||
</ul> | |||
</li> | |||
</ul> | |||
</div> | |||
</div> | |||
</nav> | |||
`; | |||
exports[`test regular user 1`] = ` | |||
<nav | |||
className="navbar navbar-context page-container" | |||
id="context-navigation"> | |||
<div | |||
className="navbar-context-inner"> | |||
<div | |||
className="container"> | |||
<ul | |||
className="nav navbar-nav nav-crumbs"> | |||
<li> | |||
<Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/organizations/foo"> | |||
Foo | |||
</Link> | |||
</li> | |||
</ul> | |||
<ul | |||
className="nav navbar-nav nav-tabs"> | |||
<li> | |||
<IndexLink | |||
activeClassName="active" | |||
to="/organizations/foo"> | |||
<i | |||
className="icon-home" /> | |||
</IndexLink> | |||
</li> | |||
</ul> | |||
</div> | |||
</div> | |||
</nav> | |||
`; |
@@ -0,0 +1,32 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import { Route } from 'react-router'; | |||
import OrganizationPage from './components/OrganizationPage'; | |||
import OrganizationAdmin from './components/OrganizationAdmin'; | |||
import OrganizationEdit from './components/OrganizationEdit'; | |||
export default ( | |||
<Route path=":organizationKey" component={OrganizationPage}> | |||
<Route component={OrganizationAdmin}> | |||
<Route path="edit" component={OrganizationEdit}/> | |||
</Route> | |||
</Route> | |||
); |
@@ -22,8 +22,13 @@ import { combineReducers } from 'redux'; | |||
import keyBy from 'lodash/keyBy'; | |||
export type Organization = { | |||
avatar: null | string, | |||
canAdmin?: boolean, | |||
description: null | string, | |||
isDefault?: boolean, | |||
key: string, | |||
name: string | |||
name: string, | |||
url: null | string | |||
}; | |||
type ReceiveOrganizationsAction = { | |||
@@ -31,7 +36,13 @@ type ReceiveOrganizationsAction = { | |||
organizations: Array<Organization> | |||
}; | |||
type Action = ReceiveOrganizationsAction; | |||
type UpdateOrganizationAction = { | |||
type: 'UPDATE_ORGANIZATION', | |||
key: string, | |||
changes: {} | |||
}; | |||
type Action = ReceiveOrganizationsAction | UpdateOrganizationAction; | |||
type ByKeyState = { | |||
[key: string]: Organization | |||
@@ -46,10 +57,25 @@ export const receiveOrganizations = (organizations: Array<Organization>): Receiv | |||
organizations | |||
}); | |||
export const updateOrganization = (key: string, changes: {}): UpdateOrganizationAction => ({ | |||
type: 'UPDATE_ORGANIZATION', | |||
key, | |||
changes | |||
}); | |||
const byKey = (state: ByKeyState = {}, action: Action) => { | |||
switch (action.type) { | |||
case 'RECEIVE_ORGANIZATIONS': | |||
return { ...state, ...keyBy(action.organizations, 'key') }; | |||
case 'UPDATE_ORGANIZATION': | |||
return { | |||
...state, | |||
[action.key]: { | |||
...state[action.key], | |||
key: action.key, | |||
...action.changes | |||
} | |||
}; | |||
default: | |||
return state; | |||
} |
@@ -2743,3 +2743,22 @@ about_page.scanners.jenkins=SonarQube Scanner for Jenkins | |||
about_page.scanners.jenkins.text=The SonarQube Scanner for Jenkins lets you integrate analysis seamlessly into a job or a pipeline. | |||
about_page.scanners.ant=SonarQube Scanner for Ant" | |||
about_page.scanners.ant.text=The SonarQube Scanner for Ant lets you start an analysis directly from an Apache Ant script. | |||
#------------------------------------------------------------------------------ | |||
# | |||
# ORGANIZATIONS | |||
# | |||
#------------------------------------------------------------------------------ | |||
organization.avatar=Avatar | |||
organization.avatar.description=Url of a small image that represents the organization (preferably 30px height). | |||
organization.avatar.preview=Preview | |||
organization.description=Description | |||
organization.description.description=Description of the organization (256 characters max). | |||
organization.edit=Edit Organization | |||
organization.key=Key | |||
organization.name=Name | |||
organization.name.description=Name of the organization (2 to 64 characters). | |||
organization.updated=Organization details have been updated. | |||
organization.url=Url | |||
organization.url.description=Url of the homepage of the organization (256 characters max). |