diff options
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.tsx | 205 |
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> ); |