Browse Source

SONAR-8654 Create page to edit organization

tags/6.3-RC1
Stas Vilchik 7 years ago
parent
commit
48352acd15
18 changed files with 1215 additions and 3 deletions
  1. 26
    1
      server/sonar-web/src/main/js/api/organizations.js
  2. 2
    0
      server/sonar-web/src/main/js/app/utils/startReactApp.js
  3. 48
    0
      server/sonar-web/src/main/js/apps/organizations/actions.js
  4. 65
    0
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationAdmin.js
  5. 195
    0
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationEdit.js
  6. 89
    0
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.js
  7. 40
    0
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationAdmin-test.js
  8. 40
    0
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationEdit-test.js
  9. 45
    0
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.js
  10. 7
    0
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationAdmin-test.js.snap
  11. 340
    0
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEdit-test.js.snap
  12. 20
    0
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationPage-test.js.snap
  13. 90
    0
      server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js
  14. 36
    0
      server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.js
  15. 93
    0
      server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap
  16. 32
    0
      server/sonar-web/src/main/js/apps/organizations/routes.js
  17. 28
    2
      server/sonar-web/src/main/js/store/organizations/duck.js
  18. 19
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 26
- 1
server/sonar-web/src/main/js/api/organizations.js View File

@@ -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 })
);

+ 2
- 0
server/sonar-web/src/main/js/app/utils/startReactApp.js View File

@@ -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>

+ 48
- 0
server/sonar-web/src/main/js/apps/organizations/actions.js View File

@@ -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));
};

+ 65
- 0
server/sonar-web/src/main/js/apps/organizations/components/OrganizationAdmin.js View File

@@ -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;

+ 195
- 0
server/sonar-web/src/main/js/apps/organizations/components/OrganizationEdit.js View File

@@ -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;

+ 89
- 0
server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.js View File

@@ -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;

+ 40
- 0
server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationAdmin-test.js View File

@@ -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();
});

+ 40
- 0
server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationEdit-test.js View File

@@ -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();
});

+ 45
- 0
server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.js View File

@@ -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();
});

+ 7
- 0
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationAdmin-test.js.snap View File

@@ -0,0 +1,7 @@
exports[`test should not render anything 1`] = `null`;

exports[`test should render children 1`] = `
<div>
hello
</div>
`;

+ 340
- 0
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEdit-test.js.snap View File

@@ -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>
`;

+ 20
- 0
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationPage-test.js.snap View File

@@ -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>
`;

+ 90
- 0
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js View File

@@ -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')}&nbsp;<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>
);
}
}

+ 36
- 0
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.js View File

@@ -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();
});

+ 93
- 0
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap View File

@@ -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>
`;

+ 32
- 0
server/sonar-web/src/main/js/apps/organizations/routes.js View File

@@ -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>
);

+ 28
- 2
server/sonar-web/src/main/js/store/organizations/duck.js View File

@@ -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;
}

+ 19
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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).

Loading…
Cancel
Save