aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/account/notifications/Projects.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src/main/js/apps/account/notifications/Projects.tsx')
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/Projects.tsx205
1 files changed, 114 insertions, 91 deletions
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/Projects.tsx b/server/sonar-web/src/main/js/apps/account/notifications/Projects.tsx
index 663ad924912..57eebfa2a89 100644
--- a/server/sonar-web/src/main/js/apps/account/notifications/Projects.tsx
+++ b/server/sonar-web/src/main/js/apps/account/notifications/Projects.tsx
@@ -17,130 +17,153 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { differenceWith } from 'lodash';
+import { groupBy, sortBy, uniqBy } from 'lodash';
import * as React from 'react';
-import { AsyncSelect } from 'sonar-ui-common/components/controls/Select';
+import { Button } from 'sonar-ui-common/components/controls/buttons';
+import SearchBox from 'sonar-ui-common/components/controls/SearchBox';
import { translate } from 'sonar-ui-common/helpers/l10n';
-import { getSuggestions } from '../../../api/components';
-import Organization from '../../../components/shared/Organization';
+import ProjectModal from './ProjectModal';
import ProjectNotifications from './ProjectNotifications';
-import { NotificationProject } from './types';
export interface Props {
addNotification: (n: T.Notification) => void;
channels: string[];
- notificationsByProject: T.Dict<T.Notification[]>;
- projects: NotificationProject[];
+ initialProjectNotificationsCount: number;
+ notifications: T.Notification[];
removeNotification: (n: T.Notification) => void;
types: string[];
}
+const THRESHOLD_COLLAPSED = 3;
+
interface State {
- addedProjects: NotificationProject[];
+ addedProjects: T.NotificationProject[];
+ search: string;
+ showModal: boolean;
+}
+
+function isNotificationProject(project: {
+ project?: string;
+ projectName?: string;
+}): project is T.NotificationProject {
+ return project.project !== undefined && project.projectName !== undefined;
}
export default class Projects extends React.PureComponent<Props, State> {
- state: State = { addedProjects: [] };
-
- componentWillReceiveProps(nextProps: Props) {
- // remove all projects from `this.state.addedProjects`
- // that already exist in `nextProps.projects`
- this.setState(state => ({
- addedProjects: differenceWith(
- state.addedProjects,
- Object.keys(nextProps.projects),
- (stateProject, propsProjectKey) => stateProject.key !== propsProjectKey
- )
- }));
- }
+ state: State = {
+ addedProjects: [],
+ search: '',
+ showModal: false
+ };
+
+ filterSearch = (project: T.NotificationProject, search: string) => {
+ return project.projectName && project.projectName.toLowerCase().includes(search);
+ };
- loadOptions = (query: string) => {
- if (query.length < 2) {
- return Promise.resolve({ options: [] });
+ handleAddProject = (project: T.NotificationProject) => {
+ this.setState(state => {
+ return {
+ addedProjects: [...state.addedProjects, project]
+ };
+ });
+ };
+
+ handleSearch = (search = '') => {
+ this.setState({ search: search.toLowerCase() });
+ };
+
+ handleSubmit = (selectedProject: T.NotificationProject) => {
+ if (selectedProject) {
+ this.handleAddProject(selectedProject);
}
- return getSuggestions(query)
- .then(r => {
- const projects = r.results.find(domain => domain.q === 'TRK');
- return projects ? projects.items : [];
- })
- .then(projects => {
- return projects
- .filter(
- project =>
- !this.props.projects.find(p => p.key === project.key) &&
- !this.state.addedProjects.find(p => p.key === project.key)
- )
- .map(project => ({
- value: project.key,
- label: project.name,
- organization: project.organization
- }));
- })
- .then(options => {
- return { options };
- });
+ this.closeModal();
};
- handleAddProject = (selected: { label: string; organization: string; value: string }) => {
- const project = {
- key: selected.value,
- name: selected.label,
- organization: selected.organization
- };
- this.setState(state => ({
- addedProjects: [...state.addedProjects, project]
- }));
+ closeModal = () => {
+ this.setState({ showModal: false });
};
- renderOption = (option: { label: string; organization: string; value: string }) => {
- return (
- <span>
- <Organization link={false} organizationKey={option.organization} />
- <strong>{option.label}</strong>
- </span>
- );
+ openModal = () => {
+ this.setState({ showModal: true });
+ };
+
+ removeNotification = (removed: T.Notification, allProjects: T.NotificationProject[]) => {
+ const projectToRemove = allProjects.find(p => p.project === removed.project);
+ if (projectToRemove) {
+ this.handleAddProject(projectToRemove);
+ }
+
+ this.props.removeNotification(removed);
};
render() {
- const allProjects = [...this.props.projects, ...this.state.addedProjects];
+ const { initialProjectNotificationsCount, notifications } = this.props;
+ const { addedProjects, search } = this.state;
+
+ const projects = uniqBy(notifications, project => project.project).filter(
+ isNotificationProject
+ ) as T.NotificationProject[];
+ const notificationsByProject = groupBy(notifications, n => n.project);
+ const allProjects = uniqBy([...addedProjects, ...projects], project => project.project);
+ const filteredProjects = sortBy(allProjects, 'projectName').filter(p =>
+ this.filterSearch(p, search)
+ );
+ const shouldBeCollapsed = initialProjectNotificationsCount > THRESHOLD_COLLAPSED;
return (
- <section className="boxed-group">
- <h2>{translate('my_profile.per_project_notifications.title')}</h2>
+ <section className="boxed-group" data-test="account__project-notifications">
+ <div className="boxed-group-inner">
+ <div className="page-actions">
+ <Button onClick={this.openModal}>
+ <span data-test="account__add-project-notification">
+ {translate('my_profile.per_project_notifications.add')}
+ </span>
+ </Button>
+ </div>
+
+ <h2>{translate('my_profile.per_project_notifications.title')}</h2>
+ </div>
+
+ {this.state.showModal && (
+ <ProjectModal
+ addedProjects={allProjects}
+ closeModal={this.closeModal}
+ onSubmit={this.handleSubmit}
+ />
+ )}
<div className="boxed-group-inner">
{allProjects.length === 0 && (
<div className="note">{translate('my_account.no_project_notifications')}</div>
)}
- {allProjects.map(project => (
- <ProjectNotifications
- addNotification={this.props.addNotification}
- channels={this.props.channels}
- key={project.key}
- notifications={this.props.notificationsByProject[project.key] || []}
- project={project}
- removeNotification={this.props.removeNotification}
- types={this.props.types}
- />
- ))}
-
- <div className="spacer-top panel bg-muted">
- <span className="text-middle spacer-right">
- {translate('my_account.set_notifications_for')}:
- </span>
- <AsyncSelect
- autoload={false}
- cache={false}
- className="input-super-large"
- loadOptions={this.loadOptions}
- name="new_project"
- onChange={this.handleAddProject}
- optionRenderer={this.renderOption}
- placeholder={translate('my_account.search_project')}
- />
- </div>
+ {allProjects.length > 0 && (
+ <div className="big-spacer-bottom">
+ <SearchBox
+ onChange={this.handleSearch}
+ placeholder={translate('search.search_for_projects')}
+ />
+ </div>
+ )}
+
+ {filteredProjects.map(project => {
+ const collapsed = addedProjects.find(p => p.project === project.project)
+ ? false
+ : shouldBeCollapsed;
+ return (
+ <ProjectNotifications
+ addNotification={this.props.addNotification}
+ channels={this.props.channels}
+ collapsed={collapsed}
+ key={project.project}
+ notifications={notificationsByProject[project.project] || []}
+ project={project}
+ removeNotification={n => this.removeNotification(n, allProjects)}
+ types={this.props.types}
+ />
+ );
+ })}
</div>
</section>
);