);
}
+export function searchGates(data: {
+ gateId: number;
+ organization?: string;
+ page: number;
+ pageSize: number;
+ selected: string;
+}): Promise<void | Response> {
+ return getJSON('/api/qualitygates/search', data).catch(throwGlobalError);
+}
+
export function associateGateWithProject(data: {
gateId: number;
organization?: string;
- projectKey: string;
+ projectKey?: string;
+ projectId?: string;
}): Promise<void | Response> {
return post('/api/qualitygates/select', data).catch(throwGlobalError);
}
export function dissociateGateWithProject(data: {
gateId: number;
organization?: string;
- projectKey: string;
+ projectKey?: string;
+ projectId?: string;
}): Promise<void | Response> {
return post('/api/qualitygates/deselect', data).catch(throwGlobalError);
}
.then(parseJSON);
}
-export function getProfileProjects(data: RequestData): Promise<any> {
+export interface ProfileProject {
+ id: number;
+ key: string;
+ name: string;
+ selected: boolean;
+}
+
+export function getProfileProjects(
+ data: RequestData
+): Promise<{ more: boolean; results: ProfileProject[] }> {
return getJSON('/api/qualityprofiles/projects', data).catch(throwGlobalError);
}
return getJSON('/api/qualityprofiles/compare', { leftKey, rightKey });
}
-export function associateProject(profileKey: string, projectKey: string): Promise<void> {
- return post('/api/qualityprofiles/add_project', { profileKey, projectKey });
+export function associateProject(profileKey: string, projectKey: string) {
+ return post('/api/qualityprofiles/add_project', { profileKey, projectKey }).catch(
+ throwGlobalError
+ );
}
-export function dissociateProject(profileKey: string, projectKey: string): Promise<void> {
- return post('/api/qualityprofiles/remove_project', { profileKey, projectKey });
+export function dissociateProject(profileKey: string, projectKey: string) {
+ return post('/api/qualityprofiles/remove_project', { profileKey, projectKey }).catch(
+ throwGlobalError
+ );
}
export interface SearchUsersGroupsParameters {
ps?: number;
q?: string;
}): Promise<{ groups: Group[]; paging: Paging }> {
- return getJSON('/api/user_groups/search', data);
+ return getJSON('/api/user_groups/search', data).catch(throwGlobalError);
+}
+
+export interface GroupUser {
+ login: string;
+ name: string;
+ selected: boolean;
+}
+
+export function getUsersInGroup(data: {
+ id?: number;
+ name?: string;
+ organization?: string;
+ p?: number;
+ ps?: number;
+ q?: string;
+ selected?: string;
+}): Promise<{ paging: Paging; users: GroupUser[] }> {
+ return getJSON('/api/user_groups/users', data).catch(throwGlobalError);
}
export function addUserToGroup(data: {
login?: string;
organization?: string;
}) {
- return post('/api/user_groups/add_user', data);
+ return post('/api/user_groups/add_user', data).catch(throwGlobalError);
}
export function removeUserFromGroup(data: {
login?: string;
organization?: string;
}) {
- return post('/api/user_groups/remove_user', data);
+ return post('/api/user_groups/remove_user', data).catch(throwGlobalError);
}
export function createGroup(data: {
return post('/api/users/change_password', data);
}
-export function getUserGroups(login: string, organization?: string): Promise<any> {
+export interface UserGroup {
+ default: boolean;
+ description: string;
+ id: number;
+ name: string;
+ selected: boolean;
+}
+
+export function getUserGroups(
+ login: string,
+ organization?: string,
+ query?: string,
+ selected?: string
+): Promise<{ paging: Paging; groups: UserGroup[] }> {
const data: RequestData = { login };
if (organization) {
data.organization = organization;
}
+ if (query) {
+ data.q = query;
+ }
+ if (selected) {
+ data.selected = selected;
+ }
return getJSON('/api/users/groups', data);
}
background-image: url('data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2014%2014%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20stroke-linejoin%3D%22round%22%20stroke-miterlimit%3D%221.414%22%3E%3Cpath%20d%3D%22M10%204.698C10%204.312%209.688%204%209.302%204H4.698C4.312%204%204%204.312%204%204.698v4.604c0%20.386.312.698.698.698h4.604c.386%200%20.698-.312.698-.698V4.698z%22%20fill%3D%22%23fff%22%2F%3E%3C%2Fsvg%3E');
}
+.icon-checkbox-disabled:before {
+ border: 1px solid #bbb;
+ cursor: not-allowed;
+}
+
+.icon-checkbox-disabled.icon-checkbox-checked:before {
+ background-color: #bbb;
+}
+
.icon-checkbox-invisible {
visibility: hidden;
}
}
.text-muted {
- color: var(--secondFontColor);
+ color: var(--secondFontColor) !important;
}
.text-muted-2 {
import SearchBox from '../../components/controls/SearchBox';
import Select from '../../components/controls/Select';
import Tooltip from '../../components/controls/Tooltip';
-import SelectList from '../../components/SelectList';
+import SelectList from '../../components/SelectList/SelectList';
import CoverageRating from '../../components/ui/CoverageRating';
import DuplicationsRating from '../../components/ui/DuplicationsRating';
import Level from '../../components/ui/Level';
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import * as escapeHtml from 'escape-html';
+import { find, without } from 'lodash';
import { Group } from '../../../app/types';
import Modal from '../../../components/controls/Modal';
import BulletListIcon from '../../../components/icons-components/BulletListIcon';
-import SelectList from '../../../components/SelectList';
+import SelectList, { Filter } from '../../../components/SelectList/SelectList';
import { ButtonIcon, ResetButtonLink } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n';
-import { getBaseUrl } from '../../../helpers/urls';
+import {
+ getUsersInGroup,
+ addUserToGroup,
+ removeUserFromGroup,
+ GroupUser
+} from '../../../api/user_groups';
interface Props {
group: Group;
interface State {
modal: boolean;
+ users: GroupUser[];
+ selectedUsers: string[];
}
export default class EditMembers extends React.PureComponent<Props, State> {
container?: HTMLElement | null;
mounted = false;
- state: State = { modal: false };
+ state: State = { modal: false, users: [], selectedUsers: [] };
componentDidMount() {
+ this.handleSearch('', Filter.Selected);
this.mounted = true;
}
this.mounted = false;
}
- handleMembersClick = () => {
- this.setState({ modal: true }, () => {
- // defer rendering of the SelectList to make sure we have `ref` assigned
- setTimeout(this.renderSelectList, 0);
+ handleSearch = (query: string, selected: Filter) => {
+ return getUsersInGroup({
+ id: this.props.group.id,
+ organization: this.props.organization,
+ ps: 100,
+ q: query !== '' ? query : undefined,
+ selected
+ }).then(data => {
+ this.setState({
+ users: data.users,
+ selectedUsers: data.users.filter(user => user.selected).map(user => user.login)
+ });
+ });
+ };
+
+ handleSelect = (login: string) => {
+ return addUserToGroup({
+ name: this.props.group.name,
+ login,
+ organization: this.props.organization
+ }).then(() => {
+ this.setState((state: State) => ({
+ selectedUsers: [...state.selectedUsers, login]
+ }));
+ });
+ };
+
+ handleUnselect = (login: string) => {
+ return removeUserFromGroup({
+ name: this.props.group.name,
+ login,
+ organization: this.props.organization
+ }).then(() => {
+ this.setState((state: State) => ({
+ selectedUsers: without(state.selectedUsers, login)
+ }));
});
};
+ handleMembersClick = () => {
+ this.setState({ modal: true });
+ };
+
handleModalClose = () => {
if (this.mounted) {
this.setState({ modal: false });
}
};
- renderSelectList = () => {
- if (this.container) {
- const extra = { name: this.props.group.name, organization: this.props.organization };
-
- /* eslint-disable no-new */
- new SelectList({
- el: this.container,
- width: '100%',
- readOnly: false,
- focusSearch: false,
- dangerouslyUnescapedHtmlFormat: (item: { login: string; name: string }) =>
- `${escapeHtml(item.name)}<br><span class="note">${escapeHtml(item.login)}</span>`,
- queryParam: 'q',
- searchUrl: getBaseUrl() + '/api/user_groups/users?ps=100&id=' + this.props.group.id,
- selectUrl: getBaseUrl() + '/api/user_groups/add_user',
- deselectUrl: getBaseUrl() + '/api/user_groups/remove_user',
- extra,
- selectParameter: 'login',
- selectParameterValue: 'login',
- parse: (r: any) => r.users
- });
- /* eslint-enable no-new */
- }
+ renderElement = (login: string): React.ReactNode => {
+ const user = find(this.state.users, { login });
+ return user === undefined ? login : user.login;
};
render() {
</header>
<div className="modal-body">
- <div id="groups-users" ref={node => (this.container = node)} />
+ <SelectList
+ elements={this.state.users.map(user => user.login)}
+ onSearch={this.handleSearch}
+ onSelect={this.handleSelect}
+ onUnselect={this.handleUnselect}
+ renderElement={this.renderElement}
+ selectedElements={this.state.selectedUsers}
+ />
</div>
<footer className="modal-foot">
<div
className="modal-body"
>
- <div
- id="groups-users"
+ <SelectList
+ elements={Array []}
+ onSearch={[Function]}
+ onSelect={[Function]}
+ onUnselect={[Function]}
+ renderElement={[Function]}
+ selectedElements={Array []}
/>
</div>
<footer
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
-import escapeHtml from 'escape-html';
-import SelectList from '../../../components/SelectList';
+import { find, without } from 'lodash';
+import SelectList, { Filter } from '../../../components/SelectList/SelectList';
import { translate } from '../../../helpers/l10n';
-import { getBaseUrl } from '../../../helpers/urls';
+import {
+ searchGates,
+ associateGateWithProject,
+ dissociateGateWithProject
+} from '../../../api/quality-gates';
+/*:: import { Project } from '../../projects/types'; */
+
+/*::
+type State = {
+ projects: Projects[],
+ selectedProjects: string[]
+};
+*/
export default class Projects extends React.PureComponent {
+ state /*: State */ = { projects: [], selectedProjects: [] };
+
componentDidMount() {
- this.renderSelectList();
+ this.handleSearch('', Filter.Selected);
}
- renderSelectList = () => {
- if (!this.container) return;
-
- const { qualityGate, edit, organization } = this.props;
+ handleSearch = (query /*: string*/, selected /*: string */) => {
+ return searchGates({
+ gateId: this.props.qualityGate.id,
+ organization: this.props.organization,
+ pageSize: 100,
+ query: query !== '' ? query : undefined,
+ selected
+ }).then(data => {
+ this.setState({
+ projects: data.results,
+ selectedProjects: data.results
+ .filter(project => project.selected)
+ .map(project => project.id)
+ });
+ });
+ };
- const extra = { gateId: qualityGate.id };
- let orgQuery = '';
- if (organization) {
- extra.organization = organization;
- orgQuery = '&organization=' + organization;
- }
+ handleSelect = (id /*: string*/) => {
+ return associateGateWithProject({
+ gateId: this.props.qualityGate.id,
+ organization: this.props.organization,
+ projectId: id
+ }).then(() => {
+ this.setState((state /*: State*/) => ({
+ selectedProjects: [...state.selectedProjects, id]
+ }));
+ });
+ };
- // eslint-disable-next-line no-new
- new SelectList({
- el: this.container,
- width: '100%',
- readOnly: !edit,
- focusSearch: false,
- dangerouslyUnescapedHtmlFormat: item => escapeHtml(item.name),
- searchUrl: getBaseUrl() + `/api/qualitygates/search?gateId=${qualityGate.id}${orgQuery}`,
- selectUrl: getBaseUrl() + '/api/qualitygates/select',
- deselectUrl: getBaseUrl() + '/api/qualitygates/deselect',
- extra,
- selectParameter: 'projectId',
- selectParameterValue: 'id',
- labels: {
- selected: translate('quality_gates.projects.with'),
- deselected: translate('quality_gates.projects.without'),
- all: translate('quality_gates.projects.all'),
- noResults: translate('quality_gates.projects.noResults')
+ handleUnselect = (id /*: string*/) => {
+ return dissociateGateWithProject({
+ gateId: this.props.qualityGate.id,
+ organization: this.props.organization,
+ projectId: id
+ }).then(
+ () => {
+ this.setState((state /*: State*/) => ({
+ selectedProjects: without(state.selectedProjects, id)
+ }));
},
- tooltips: {
- select: translate('quality_gates.projects.select_hint'),
- deselect: translate('quality_gates.projects.deselect_hint')
- }
- });
+ () => {}
+ );
+ };
+
+ renderElement = (id /*: string*/) /*: React.ReactNode*/ => {
+ const project = find(this.state.projects, { id });
+ return project === undefined ? id : project.name;
};
render() {
- return <div ref={node => (this.container = node)} />;
+ return (
+ <SelectList
+ elements={this.state.projects.map(project => project.id)}
+ labelAll={translate('quality_gates.projects.all')}
+ labelSelected={translate('quality_gates.projects.with')}
+ labelUnselected={translate('quality_gates.projects.without')}
+ onSearch={this.handleSearch}
+ onSelect={this.handleSelect}
+ onUnselect={this.handleUnselect}
+ renderElement={this.renderElement}
+ selectedElements={this.state.selectedProjects}
+ />
+ );
}
}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import * as escapeHtml from 'escape-html';
+import { find, without } from 'lodash';
import Modal from '../../../components/controls/Modal';
-import SelectList from '../../../components/SelectList';
+import SelectList, { Filter } from '../../../components/SelectList/SelectList';
import { translate } from '../../../helpers/l10n';
import { Profile } from '../types';
+import {
+ getProfileProjects,
+ associateProject,
+ dissociateProject,
+ ProfileProject
+} from '../../../api/quality-profiles';
interface Props {
onClose: () => void;
profile: Profile;
}
+interface State {
+ projects: ProfileProject[];
+ selectedProjects: string[];
+}
+
export default class ChangeProjectsForm extends React.PureComponent<Props> {
container?: HTMLElement | null;
+ state: State = { projects: [], selectedProjects: [] };
+
+ componentDidMount() {
+ this.handleSearch('', Filter.Selected);
+ }
+
+ handleSearch = (query: string, selected: Filter) => {
+ return getProfileProjects({
+ key: this.props.profile.key,
+ organization: this.props.organization,
+ pageSize: 100,
+ query: query !== '' ? query : undefined,
+ selected
+ }).then(
+ data => {
+ this.setState({
+ projects: data.results,
+ selectedProjects: data.results
+ .filter(project => project.selected)
+ .map(project => project.key)
+ });
+ },
+ () => {}
+ );
+ };
+
+ handleSelect = (key: string) => {
+ return associateProject(this.props.profile.key, key).then(() => {
+ this.setState((state: State) => ({
+ selectedProjects: [...state.selectedProjects, key]
+ }));
+ });
+ };
+
+ handleUnselect = (key: string) => {
+ return dissociateProject(this.props.profile.key, key).then(() => {
+ this.setState((state: State) => ({ selectedProjects: without(state.selectedProjects, key) }));
+ });
+ };
handleCloseClick = (event: React.SyntheticEvent<HTMLElement>) => {
event.preventDefault();
this.props.onClose();
};
- renderSelectList = () => {
- if (this.container) {
- const { key } = this.props.profile;
-
- const searchUrl =
- (window as any).baseUrl + '/api/qualityprofiles/projects?key=' + encodeURIComponent(key);
-
- new SelectList({
- searchUrl,
- el: this.container,
- width: '100%',
- readOnly: false,
- focusSearch: false,
- dangerouslyUnescapedHtmlFormat: (item: { name: string }) => escapeHtml(item.name),
- selectUrl: (window as any).baseUrl + '/api/qualityprofiles/add_project',
- deselectUrl: (window as any).baseUrl + '/api/qualityprofiles/remove_project',
- extra: { profileKey: key },
- selectParameter: 'projectUuid',
- selectParameterValue: 'uuid',
- labels: {
- selected: translate('quality_gates.projects.with'),
- deselected: translate('quality_gates.projects.without'),
- all: translate('quality_gates.projects.all'),
- noResults: translate('quality_gates.projects.noResults')
- },
- tooltips: {
- select: translate('quality_profiles.projects.select_hint'),
- deselect: translate('quality_profiles.projects.deselect_hint')
- }
- });
- }
+ renderElement = (key: string): React.ReactNode => {
+ const project = find(this.state.projects, { key });
+ return project === undefined ? key : project.name;
};
render() {
const header = translate('projects');
return (
- <Modal
- contentLabel={header}
- onAfterOpen={this.renderSelectList}
- onRequestClose={this.props.onClose}>
+ <Modal contentLabel={header} onRequestClose={this.props.onClose}>
<div className="modal-head">
<h2>{header}</h2>
</div>
- <div className="modal-body">
- <div id="profile-projects" ref={node => (this.container = node)} />
+ <div className="modal-body" id="profile-projects">
+ <SelectList
+ elements={this.state.projects.map(project => project.key)}
+ labelAll={translate('quality_gates.projects.all')}
+ labelSelected={translate('quality_gates.projects.with')}
+ labelUnselected={translate('quality_gates.projects.without')}
+ onSearch={this.handleSearch}
+ onSelect={this.handleSelect}
+ onUnselect={this.handleUnselect}
+ renderElement={this.renderElement}
+ selectedElements={this.state.selectedProjects}
+ />
</div>
<div className="modal-foot">
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import * as escapeHtml from 'escape-html';
+import { find, without } from 'lodash';
import { User } from '../../../app/types';
import Modal from '../../../components/controls/Modal';
-import SelectList from '../../../components/SelectList';
+import SelectList, { Filter } from '../../../components/SelectList/SelectList';
import { translate } from '../../../helpers/l10n';
-import { getBaseUrl } from '../../../helpers/urls';
+import { getUserGroups, UserGroup } from '../../../api/users';
+import { addUserToGroup, removeUserFromGroup } from '../../../api/user_groups';
interface Props {
onClose: () => void;
user: User;
}
+interface State {
+ error: string;
+ groups: UserGroup[];
+ selectedGroups: string[];
+}
+
export default class GroupsForm extends React.PureComponent<Props> {
container?: HTMLDivElement | null;
+ state: State = { error: '', groups: [], selectedGroups: [] };
+
+ componentDidMount() {
+ this.handleSearch('', Filter.Selected);
+ }
+
+ handleSearch = (query: string, selected: Filter) => {
+ return getUserGroups(this.props.user.login, undefined, query, selected).then(data => {
+ this.setState({
+ groups: data.groups,
+ selectedGroups: data.groups.filter(group => group.selected).map(group => group.name)
+ });
+ });
+ };
+
+ handleSelect = (name: string) => {
+ return addUserToGroup({
+ name,
+ login: this.props.user.login
+ }).then(() => {
+ this.setState((state: State) => ({ selectedGroups: [...state.selectedGroups, name] }));
+ });
+ };
+
+ handleUnselect = (name: string) => {
+ return removeUserFromGroup({
+ name,
+ login: this.props.user.login
+ }).then(() => {
+ this.setState((state: State) => ({
+ selectedGroups: without(state.selectedGroups, name)
+ }));
+ });
+ };
handleCloseClick = (event: React.SyntheticEvent<HTMLElement>) => {
event.preventDefault();
this.props.onClose();
};
- renderSelectList = () => {
- const searchUrl = `${getBaseUrl()}/api/users/groups?ps=100&login=${encodeURIComponent(
- this.props.user.login
- )}`;
-
- new (SelectList as any)({
- el: this.container,
- width: '100%',
- readOnly: false,
- focusSearch: false,
- dangerouslyUnescapedHtmlFormat: (item: { name: string; description: string }) =>
- `${escapeHtml(item.name)}<br><span class="note">${escapeHtml(item.description)}</span>`,
- queryParam: 'q',
- searchUrl,
- selectUrl: getBaseUrl() + '/api/user_groups/add_user',
- deselectUrl: getBaseUrl() + '/api/user_groups/remove_user',
- extra: { login: this.props.user.login },
- selectParameter: 'id',
- selectParameterValue: 'id',
- parse(r: any) {
- this.more = false;
- return r.groups;
- }
- });
+ renderElement = (name: string): React.ReactNode => {
+ const group = find(this.state.groups, { name });
+ return group === undefined ? name : group.name;
};
render() {
const header = translate('users.update_groups');
return (
- <Modal
- contentLabel={header}
- onAfterOpen={this.renderSelectList}
- onRequestClose={this.handleClose}>
+ <Modal contentLabel={header} onRequestClose={this.handleClose}>
<div className="modal-head">
<h2>{header}</h2>
</div>
<div className="modal-body">
- <div id="user-groups" ref={node => (this.container = node)} />
+ {this.state.error !== '' && (
+ <div className="alert alert-danger">
+ <p>{this.state.error}</p>
+ </div>
+ )}
+ <SelectList
+ elements={this.state.groups.map(group => group.name)}
+ onSearch={this.handleSearch}
+ onSelect={this.handleSelect}
+ onUnselect={this.handleUnselect}
+ renderElement={this.renderElement}
+ selectedElements={this.state.selectedGroups}
+ />
</div>
<footer className="modal-foot">
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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 * as React from 'react';
+import SelectListListContainer from './SelectListListContainer';
+import { translate } from '../../helpers/l10n';
+import SearchBox from '../controls/SearchBox';
+import RadioToggle from '../controls/RadioToggle';
+import './styles.css';
+
+export enum Filter {
+ All = 'all',
+ Selected = 'selected',
+ Unselected = 'deselected'
+}
+
+interface Props {
+ elements: string[];
+ disabledElements?: string[];
+ labelSelected?: string;
+ labelUnselected?: string;
+ labelAll?: string;
+ onSearch: (query: string, tab: Filter) => Promise<void>;
+ onSelect: (element: string) => Promise<void>;
+ onUnselect: (element: string) => Promise<void>;
+ renderElement: (element: string) => React.ReactNode;
+ selectedElements: string[];
+}
+
+interface State {
+ filter: Filter;
+ loading: boolean;
+ query: string;
+}
+
+export default class SelectList extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { filter: Filter.Selected, loading: false, query: '' };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ stopLoading = () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ };
+
+ changeFilter = (filter: Filter) => {
+ this.setState({ filter, loading: true });
+ this.props.onSearch(this.state.query, filter).then(this.stopLoading, this.stopLoading);
+ };
+
+ handleQueryChange = (query: string) => {
+ this.setState({ loading: true, query });
+ this.props.onSearch(query, this.getFilter()).then(this.stopLoading, this.stopLoading);
+ };
+
+ getFilter = () => {
+ return this.state.query === '' ? this.state.filter : Filter.All;
+ };
+
+ render() {
+ const {
+ labelSelected = translate('selected'),
+ labelUnselected = translate('unselected'),
+ labelAll = translate('all')
+ } = this.props;
+ const { filter } = this.state;
+
+ const disabled = this.state.query !== '';
+
+ return (
+ <div className="select-list">
+ <div className="display-flex-center">
+ <RadioToggle
+ className="spacer-right"
+ name="filter"
+ onCheck={this.changeFilter}
+ options={[
+ { disabled, label: labelSelected, value: Filter.Selected },
+ { disabled, label: labelUnselected, value: Filter.Unselected },
+ { disabled, label: labelAll, value: Filter.All }
+ ]}
+ value={filter}
+ />
+ <SearchBox
+ autoFocus={true}
+ loading={this.state.loading}
+ onChange={this.handleQueryChange}
+ placeholder={translate('search_verb')}
+ value={this.state.query}
+ />
+ </div>
+ <SelectListListContainer
+ disabledElements={this.props.disabledElements || []}
+ elements={this.props.elements}
+ filter={this.getFilter()}
+ onSelect={this.props.onSelect}
+ onUnselect={this.props.onUnselect}
+ renderElement={this.props.renderElement}
+ selectedElements={this.props.selectedElements}
+ />
+ </div>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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 * as React from 'react';
+import { Filter } from './SelectList';
+import SelectListListElement from './SelectListListElement';
+
+interface Props {
+ elements: string[];
+ disabledElements: string[];
+ filter: Filter;
+ onSelect: (element: string) => Promise<void>;
+ onUnselect: (element: string) => Promise<void>;
+ renderElement: (element: string) => React.ReactNode;
+ selectedElements: string[];
+}
+
+export default class SelectListListContainer extends React.PureComponent<Props> {
+ isDisabled = (element: string): boolean => {
+ return this.props.disabledElements.includes(element);
+ };
+
+ isSelected = (element: string): boolean => {
+ return this.props.selectedElements.includes(element);
+ };
+
+ render() {
+ const { elements, filter } = this.props;
+ const filteredElements = elements.filter(element => {
+ if (filter === Filter.All) {
+ return true;
+ }
+ const isSelected = this.isSelected(element);
+ return filter === Filter.Selected ? isSelected : !isSelected;
+ });
+
+ return (
+ <div className="select-list-list-container spacer-top">
+ <ul className="menu">
+ {filteredElements.map(element => (
+ <SelectListListElement
+ disabled={this.isDisabled(element)}
+ element={element}
+ key={element}
+ onSelect={this.props.onSelect}
+ onUnselect={this.props.onUnselect}
+ renderElement={this.props.renderElement}
+ selected={this.isSelected(element)}
+ />
+ ))}
+ </ul>
+ </div>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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 * as React from 'react';
+import * as classNames from 'classnames';
+import Checkbox from '../controls/Checkbox';
+
+interface Props {
+ active?: boolean;
+ disabled?: boolean;
+ element: string;
+ onSelect: (element: string) => Promise<void>;
+ onUnselect: (element: string) => Promise<void>;
+ renderElement: (element: string) => React.ReactNode;
+ selected: boolean;
+}
+
+interface State {
+ loading: boolean;
+}
+
+export default class SelectListListElement extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { loading: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ stopLoading = () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ };
+
+ handleCheck = (checked: boolean) => {
+ this.setState({ loading: true });
+ const request = checked ? this.props.onSelect : this.props.onUnselect;
+ request(this.props.element).then(this.stopLoading, this.stopLoading);
+ };
+
+ render() {
+ return (
+ <li>
+ <Checkbox
+ checked={this.props.selected}
+ className={classNames({ active: this.props.active })}
+ disabled={this.props.disabled}
+ onCheck={this.handleCheck}>
+ <span className="little-spacer-left">{this.props.renderElement(this.props.element)}</span>
+ </Checkbox>
+ </li>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import SelectList, { Filter } from '../SelectList';
+import { waitAndUpdate } from '../../../helpers/testUtils';
+
+const selectList = (
+ <SelectList
+ elements={['foo', 'bar', 'baz']}
+ onSearch={jest.fn(() => Promise.resolve())}
+ onSelect={jest.fn(() => Promise.resolve())}
+ onUnselect={jest.fn(() => Promise.resolve())}
+ renderElement={(foo: string) => foo}
+ selectedElements={['foo']}
+ />
+);
+
+it('should display selected elements only by default', () => {
+ const wrapper = shallow(selectList);
+ expect(wrapper.state().filter).toBe(Filter.Selected);
+});
+
+it('should display a loader when searching', async () => {
+ const wrapper = shallow(selectList);
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.state().loading).toBe(false);
+
+ (wrapper.instance() as SelectList).handleQueryChange('');
+ expect(wrapper.state().loading).toBe(true);
+ expect(wrapper).toMatchSnapshot();
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state().loading).toBe(false);
+});
+
+it('should display a loader when updating filter', async () => {
+ const wrapper = shallow(selectList);
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.state().loading).toBe(false);
+
+ (wrapper.instance() as SelectList).changeFilter(Filter.Unselected);
+ expect(wrapper.state().loading).toBe(true);
+ expect(wrapper).toMatchSnapshot();
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state().filter).toBe(Filter.Unselected);
+ expect(wrapper.state().loading).toBe(false);
+});
+
+it('should cancel filter selection when search is active', async () => {
+ const wrapper = shallow(selectList);
+
+ wrapper.setState({ filter: Filter.Selected });
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.setState({ query: 'foobar' });
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import SelectListListContainer from '../SelectListListContainer';
+import { Filter } from '../SelectList';
+
+const elementsContainer = (
+ <SelectListListContainer
+ disabledElements={[]}
+ elements={['foo', 'bar', 'baz']}
+ filter={Filter.All}
+ onSelect={jest.fn(() => Promise.resolve())}
+ onUnselect={jest.fn(() => Promise.resolve())}
+ renderElement={(foo: string) => foo}
+ selectedElements={['foo']}
+ />
+);
+
+it('should display elements based on filters', () => {
+ const wrapper = shallow(elementsContainer);
+ expect(wrapper.find('SelectListListElement')).toHaveLength(3);
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.setProps({ filter: Filter.Unselected });
+ expect(wrapper.find('SelectListListElement')).toHaveLength(2);
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.setProps({ filter: Filter.Selected });
+ expect(wrapper.find('SelectListListElement')).toHaveLength(1);
+ expect(wrapper).toMatchSnapshot();
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import SelectListListElement from '../SelectListListElement';
+import { waitAndUpdate } from '../../../helpers/testUtils';
+
+const listElement = (
+ <SelectListListElement
+ element={'foo'}
+ key={'foo'}
+ onSelect={jest.fn(() => Promise.resolve())}
+ onUnselect={jest.fn(() => Promise.resolve())}
+ renderElement={(foo: string) => foo}
+ selected={false}
+ />
+);
+
+it('should display a loader when checking', async () => {
+ const wrapper = shallow(listElement);
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.state().loading).toBe(false);
+
+ (wrapper.instance() as SelectListListElement).handleCheck(true);
+ expect(wrapper.state().loading).toBe(true);
+ expect(wrapper).toMatchSnapshot();
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state().loading).toBe(false);
+});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should cancel filter selection when search is active 1`] = `
+<div
+ className="select-list"
+>
+ <div
+ className="display-flex-center"
+ >
+ <RadioToggle
+ className="spacer-right"
+ disabled={false}
+ name="filter"
+ onCheck={[Function]}
+ options={
+ Array [
+ Object {
+ "disabled": false,
+ "label": "selected",
+ "value": "selected",
+ },
+ Object {
+ "disabled": false,
+ "label": "unselected",
+ "value": "deselected",
+ },
+ Object {
+ "disabled": false,
+ "label": "all",
+ "value": "all",
+ },
+ ]
+ }
+ value="selected"
+ />
+ <SearchBox
+ autoFocus={true}
+ loading={false}
+ onChange={[Function]}
+ placeholder="search_verb"
+ value=""
+ />
+ </div>
+ <SelectListListContainer
+ disabledElements={Array []}
+ elements={
+ Array [
+ "foo",
+ "bar",
+ "baz",
+ ]
+ }
+ filter="selected"
+ onSelect={[MockFunction]}
+ onUnselect={[MockFunction]}
+ renderElement={[Function]}
+ selectedElements={
+ Array [
+ "foo",
+ ]
+ }
+ />
+</div>
+`;
+
+exports[`should cancel filter selection when search is active 2`] = `
+<div
+ className="select-list"
+>
+ <div
+ className="display-flex-center"
+ >
+ <RadioToggle
+ className="spacer-right"
+ disabled={false}
+ name="filter"
+ onCheck={[Function]}
+ options={
+ Array [
+ Object {
+ "disabled": true,
+ "label": "selected",
+ "value": "selected",
+ },
+ Object {
+ "disabled": true,
+ "label": "unselected",
+ "value": "deselected",
+ },
+ Object {
+ "disabled": true,
+ "label": "all",
+ "value": "all",
+ },
+ ]
+ }
+ value="selected"
+ />
+ <SearchBox
+ autoFocus={true}
+ loading={false}
+ onChange={[Function]}
+ placeholder="search_verb"
+ value="foobar"
+ />
+ </div>
+ <SelectListListContainer
+ disabledElements={Array []}
+ elements={
+ Array [
+ "foo",
+ "bar",
+ "baz",
+ ]
+ }
+ filter="all"
+ onSelect={[MockFunction]}
+ onUnselect={[MockFunction]}
+ renderElement={[Function]}
+ selectedElements={
+ Array [
+ "foo",
+ ]
+ }
+ />
+</div>
+`;
+
+exports[`should display a loader when searching 1`] = `
+<div
+ className="select-list"
+>
+ <div
+ className="display-flex-center"
+ >
+ <RadioToggle
+ className="spacer-right"
+ disabled={false}
+ name="filter"
+ onCheck={[Function]}
+ options={
+ Array [
+ Object {
+ "disabled": false,
+ "label": "selected",
+ "value": "selected",
+ },
+ Object {
+ "disabled": false,
+ "label": "unselected",
+ "value": "deselected",
+ },
+ Object {
+ "disabled": false,
+ "label": "all",
+ "value": "all",
+ },
+ ]
+ }
+ value="selected"
+ />
+ <SearchBox
+ autoFocus={true}
+ loading={false}
+ onChange={[Function]}
+ placeholder="search_verb"
+ value=""
+ />
+ </div>
+ <SelectListListContainer
+ disabledElements={Array []}
+ elements={
+ Array [
+ "foo",
+ "bar",
+ "baz",
+ ]
+ }
+ filter="selected"
+ onSelect={[MockFunction]}
+ onUnselect={[MockFunction]}
+ renderElement={[Function]}
+ selectedElements={
+ Array [
+ "foo",
+ ]
+ }
+ />
+</div>
+`;
+
+exports[`should display a loader when searching 2`] = `
+<div
+ className="select-list"
+>
+ <div
+ className="display-flex-center"
+ >
+ <RadioToggle
+ className="spacer-right"
+ disabled={false}
+ name="filter"
+ onCheck={[Function]}
+ options={
+ Array [
+ Object {
+ "disabled": false,
+ "label": "selected",
+ "value": "selected",
+ },
+ Object {
+ "disabled": false,
+ "label": "unselected",
+ "value": "deselected",
+ },
+ Object {
+ "disabled": false,
+ "label": "all",
+ "value": "all",
+ },
+ ]
+ }
+ value="selected"
+ />
+ <SearchBox
+ autoFocus={true}
+ loading={false}
+ onChange={[Function]}
+ placeholder="search_verb"
+ value=""
+ />
+ </div>
+ <SelectListListContainer
+ disabledElements={Array []}
+ elements={
+ Array [
+ "foo",
+ "bar",
+ "baz",
+ ]
+ }
+ filter="selected"
+ onSelect={[MockFunction]}
+ onUnselect={[MockFunction]}
+ renderElement={[Function]}
+ selectedElements={
+ Array [
+ "foo",
+ ]
+ }
+ />
+</div>
+`;
+
+exports[`should display a loader when updating filter 1`] = `
+<div
+ className="select-list"
+>
+ <div
+ className="display-flex-center"
+ >
+ <RadioToggle
+ className="spacer-right"
+ disabled={false}
+ name="filter"
+ onCheck={[Function]}
+ options={
+ Array [
+ Object {
+ "disabled": false,
+ "label": "selected",
+ "value": "selected",
+ },
+ Object {
+ "disabled": false,
+ "label": "unselected",
+ "value": "deselected",
+ },
+ Object {
+ "disabled": false,
+ "label": "all",
+ "value": "all",
+ },
+ ]
+ }
+ value="selected"
+ />
+ <SearchBox
+ autoFocus={true}
+ loading={false}
+ onChange={[Function]}
+ placeholder="search_verb"
+ value=""
+ />
+ </div>
+ <SelectListListContainer
+ disabledElements={Array []}
+ elements={
+ Array [
+ "foo",
+ "bar",
+ "baz",
+ ]
+ }
+ filter="selected"
+ onSelect={[MockFunction]}
+ onUnselect={[MockFunction]}
+ renderElement={[Function]}
+ selectedElements={
+ Array [
+ "foo",
+ ]
+ }
+ />
+</div>
+`;
+
+exports[`should display a loader when updating filter 2`] = `
+<div
+ className="select-list"
+>
+ <div
+ className="display-flex-center"
+ >
+ <RadioToggle
+ className="spacer-right"
+ disabled={false}
+ name="filter"
+ onCheck={[Function]}
+ options={
+ Array [
+ Object {
+ "disabled": false,
+ "label": "selected",
+ "value": "selected",
+ },
+ Object {
+ "disabled": false,
+ "label": "unselected",
+ "value": "deselected",
+ },
+ Object {
+ "disabled": false,
+ "label": "all",
+ "value": "all",
+ },
+ ]
+ }
+ value="selected"
+ />
+ <SearchBox
+ autoFocus={true}
+ loading={false}
+ onChange={[Function]}
+ placeholder="search_verb"
+ value=""
+ />
+ </div>
+ <SelectListListContainer
+ disabledElements={Array []}
+ elements={
+ Array [
+ "foo",
+ "bar",
+ "baz",
+ ]
+ }
+ filter="selected"
+ onSelect={[MockFunction]}
+ onUnselect={[MockFunction]}
+ renderElement={[Function]}
+ selectedElements={
+ Array [
+ "foo",
+ ]
+ }
+ />
+</div>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display elements based on filters 1`] = `
+<div
+ className="select-list-list-container spacer-top"
+>
+ <ul
+ className="menu"
+ >
+ <SelectListListElement
+ disabled={false}
+ element="foo"
+ key="foo"
+ onSelect={[MockFunction]}
+ onUnselect={[MockFunction]}
+ renderElement={[Function]}
+ selected={true}
+ />
+ <SelectListListElement
+ disabled={false}
+ element="bar"
+ key="bar"
+ onSelect={[MockFunction]}
+ onUnselect={[MockFunction]}
+ renderElement={[Function]}
+ selected={false}
+ />
+ <SelectListListElement
+ disabled={false}
+ element="baz"
+ key="baz"
+ onSelect={[MockFunction]}
+ onUnselect={[MockFunction]}
+ renderElement={[Function]}
+ selected={false}
+ />
+ </ul>
+</div>
+`;
+
+exports[`should display elements based on filters 2`] = `
+<div
+ className="select-list-list-container spacer-top"
+>
+ <ul
+ className="menu"
+ >
+ <SelectListListElement
+ disabled={false}
+ element="bar"
+ key="bar"
+ onSelect={[MockFunction]}
+ onUnselect={[MockFunction]}
+ renderElement={[Function]}
+ selected={false}
+ />
+ <SelectListListElement
+ disabled={false}
+ element="baz"
+ key="baz"
+ onSelect={[MockFunction]}
+ onUnselect={[MockFunction]}
+ renderElement={[Function]}
+ selected={false}
+ />
+ </ul>
+</div>
+`;
+
+exports[`should display elements based on filters 3`] = `
+<div
+ className="select-list-list-container spacer-top"
+>
+ <ul
+ className="menu"
+ >
+ <SelectListListElement
+ disabled={false}
+ element="foo"
+ key="foo"
+ onSelect={[MockFunction]}
+ onUnselect={[MockFunction]}
+ renderElement={[Function]}
+ selected={true}
+ />
+ </ul>
+</div>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display a loader when checking 1`] = `
+<li>
+ <Checkbox
+ checked={false}
+ className=""
+ onCheck={[Function]}
+ thirdState={false}
+ >
+ <span
+ className="little-spacer-left"
+ >
+ foo
+ </span>
+ </Checkbox>
+</li>
+`;
+
+exports[`should display a loader when checking 2`] = `
+<li>
+ <Checkbox
+ checked={false}
+ className=""
+ onCheck={[Function]}
+ thirdState={false}
+ >
+ <span
+ className="little-spacer-left"
+ >
+ foo
+ </span>
+ </Checkbox>
+</li>
+`;
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info 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 $ from 'jquery';
-import Backbone from 'backbone';
-import { debounce, throttle } from 'lodash';
-import escapeHtml from 'escape-html';
-import ItemTemplate from './templates/item.hbs';
-import ListTemplate from './templates/list.hbs';
-import { translate } from '../../helpers/l10n';
-import './styles.css';
-import '../controls/SearchBox.css';
-
-let showError = null;
-
-/*
- * SelectList Collection
- */
-
-const SelectListCollection = Backbone.Collection.extend({
- initialize(options) {
- this.options = options;
- },
-
- parse(r) {
- return this.options.parse.call(this, r);
- },
-
- fetch(options) {
- const data = $.extend(
- {
- page: 1,
- pageSize: 100
- },
- options.data || {}
- );
- const settings = $.extend({}, options, { data });
-
- this.settings = {
- url: settings.url,
- data
- };
-
- Backbone.Collection.prototype.fetch.call(this, settings);
- },
-
- fetchNextPage(options) {
- if (this.more) {
- const nextPage = this.settings.data.page + 1;
- const settings = $.extend(this.settings, options);
-
- settings.data.page = nextPage;
- settings.remove = false;
- this.fetch(settings);
- } else {
- options.error();
- }
- }
-});
-
-/*
- * SelectList Item View
- */
-
-const SelectListItemView = Backbone.View.extend({
- tagName: 'li',
- template: ItemTemplate,
-
- events: {
- 'change .select-list-list-checkbox': 'toggle'
- },
-
- initialize(options) {
- this.listenTo(this.model, 'change', this.render);
- this.settings = options.settings;
- },
-
- render() {
- this.$el.html(this.template(this.settings.dangerouslyUnescapedHtmlFormat(this.model.toJSON())));
- this.$('input').prop('name', this.model.get('name'));
- this.$el.toggleClass('selected', this.model.get('selected'));
- this.$('.select-list-list-checkbox')
- .prop(
- 'title',
- this.model.get('selected') ? this.settings.tooltips.deselect : this.settings.tooltips.select
- )
- .prop('checked', this.model.get('selected'));
-
- if (this.settings.readOnly) {
- this.$('.select-list-list-checkbox').prop('disabled', true);
- }
- },
-
- remove(postpone) {
- if (postpone) {
- this.$el.addClass(this.model.get('selected') ? 'added' : 'removed');
- setTimeout(() => {
- Backbone.View.prototype.remove.call(this, arguments);
- }, 500);
- } else {
- Backbone.View.prototype.remove.call(this, arguments);
- }
- },
-
- toggle() {
- const selected = this.model.get('selected');
- const that = this;
- const url = selected ? this.settings.deselectUrl : this.settings.selectUrl;
- const data = $.extend({}, this.settings.extra || {});
-
- data[this.settings.selectParameter] = this.model.get(this.settings.selectParameterValue);
-
- that.$el.addClass('progress');
- $.ajax({
- url,
- data,
- type: 'POST',
- statusCode: {
- // do not show global error
- 400: null,
- 403: null,
- 500: null
- }
- })
- .done(() => {
- that.model.set('selected', !selected);
- })
- .fail(jqXHR => {
- that.render();
- showError(jqXHR);
- })
- .always(() => {
- that.$el.removeClass('progress');
- });
- }
-});
-
-/*
- * SelectList View
- */
-
-const SelectListView = Backbone.View.extend({
- template: ListTemplate,
-
- events: {
- 'click .select-list-control-button[name=selected]': 'showSelected',
- 'click .select-list-control-button[name=deselected]': 'showDeselected',
- 'click .select-list-control-button[name=all]': 'showAll',
- 'click .js-reset': 'onResetClick'
- },
-
- initialize(options) {
- this.listenTo(this.collection, 'add', this.renderListItem);
- this.listenTo(this.collection, 'reset', this.renderList);
- this.listenTo(this.collection, 'remove', this.removeModel);
- this.listenTo(this.collection, 'change:selected', this.confirmFilter);
- this.settings = options.settings;
-
- const that = this;
- this.showFetchSpinner = function() {
- that.$listContainer.addClass('loading');
- };
- this.hideFetchSpinner = function() {
- that.$listContainer.removeClass('loading');
- };
-
- const onScroll = function() {
- that.showFetchSpinner();
-
- that.collection.fetchNextPage({
- success() {
- that.hideFetchSpinner();
- },
- error() {
- that.hideFetchSpinner();
- }
- });
- };
- this.onScroll = throttle(onScroll, 1000);
- },
-
- render() {
- const that = this;
- const keyup = function() {
- that.search();
- };
-
- this.$el.html(this.template(this.settings.labels)).width(this.settings.width);
-
- this.$listContainer = this.$('.select-list-list-container');
- if (!this.settings.readOnly) {
- this.$listContainer
- .height(this.settings.height)
- .css('overflow', 'auto')
- .on('scroll', () => {
- that.scroll();
- });
- } else {
- this.$listContainer.addClass('select-list-list-container-readonly');
- }
-
- this.$list = this.$('.select-list-list');
-
- const searchInput = this.$('.select-list-search-control input')
- .on('keyup', debounce(keyup, 250))
- .on('search', debounce(keyup, 250));
-
- if (this.settings.focusSearch) {
- setTimeout(() => {
- searchInput.focus();
- }, 250);
- }
-
- this.listItemViews = [];
-
- showError = function(jqXHR) {
- let message = translate('default_error_message');
- if (jqXHR != null && jqXHR.responseJSON != null && jqXHR.responseJSON.errors != null) {
- message = jqXHR.responseJSON.errors.map(e => e.msg).join('. ');
- }
-
- that.$el.prevAll('.alert').remove();
- $('<div>')
- .addClass('alert alert-danger')
- .text(message)
- .insertBefore(that.$el);
- };
-
- if (this.settings.readOnly) {
- this.$('.select-list-control').remove();
- }
- },
-
- renderList() {
- this.listItemViews.forEach(view => {
- view.remove();
- });
- this.listItemViews = [];
- if (this.collection.length > 0) {
- this.collection.each(this.renderListItem, this);
- } else if (this.settings.readOnly) {
- this.renderEmpty();
- }
- this.$listContainer.scrollTop(0);
- },
-
- renderListItem(item) {
- const itemView = new SelectListItemView({
- model: item,
- settings: this.settings
- });
- this.listItemViews.push(itemView);
- this.$list.append(itemView.el);
- itemView.render();
- },
-
- renderEmpty() {
- this.$list.append(`<li class="empty-message">${this.settings.labels.noResults}</li>`);
- },
-
- confirmFilter(model) {
- if (this.currentFilter !== 'all') {
- this.collection.remove(model);
- }
- },
-
- removeModel(model, collection, options) {
- this.listItemViews[options.index].remove(true);
- this.listItemViews.splice(options.index, 1);
- },
-
- filterBySelection(filter) {
- const that = this;
- filter = this.currentFilter = filter || this.currentFilter;
-
- if (filter != null) {
- this.$('.select-list-check-control').toggleClass('disabled', false);
- this.$('.select-list-search-control').toggleClass('disabled', true);
- this.$('.select-list-search-control input').val('');
-
- this.$('.select-list-control-button')
- .removeClass('active')
- .filter(`[name=${filter}]`)
- .addClass('active');
-
- this.showFetchSpinner();
-
- this.collection.fetch({
- url: this.settings.searchUrl,
- reset: true,
- data: { selected: filter },
- success() {
- that.hideFetchSpinner();
- },
- error: showError
- });
- }
- },
-
- showSelected() {
- this.filterBySelection('selected');
- },
-
- showDeselected() {
- this.filterBySelection('deselected');
- },
-
- showAll() {
- this.filterBySelection('all');
- },
-
- search() {
- const query = this.$('.select-list-search-control input').val();
- const hasQuery = query.length > 0;
- const that = this;
- const data = {};
-
- this.$('.select-list-check-control').toggleClass('disabled', hasQuery);
- this.$('.select-list-search-control').toggleClass('disabled', !hasQuery);
- this.$('.js-reset').toggleClass('hidden', !hasQuery);
-
- if (hasQuery) {
- this.showFetchSpinner();
- this.currentFilter = 'all';
-
- data[this.settings.queryParam] = query;
- data.selected = 'all';
- this.collection.fetch({
- data,
- url: this.settings.searchUrl,
- reset: true,
- success() {
- that.hideFetchSpinner();
- },
- error: showError
- });
- } else {
- this.filterBySelection();
- }
- },
-
- onResetClick(e) {
- e.preventDefault();
- e.currentTarget.blur();
- this.$('.select-list-search-control input')
- .val('')
- .focus()
- .trigger('search');
- },
-
- searchByQuery(query) {
- this.$('.select-list-search-control input').val(query);
- this.search();
- },
-
- clearSearch() {
- this.filterBySelection();
- },
-
- scroll() {
- const scrollBottom =
- this.$listContainer.scrollTop() >=
- this.$list[0].scrollHeight - this.$listContainer.outerHeight();
-
- if (scrollBottom && this.collection.more) {
- this.onScroll();
- }
- }
-});
-
-/*
- * SelectList Entry Point
- */
-
-const SelectList = function(options) {
- this.settings = $.extend(this.defaults, options);
-
- this.collection = new SelectListCollection({
- parse: this.settings.parse
- });
-
- this.view = new SelectListView({
- el: this.settings.el,
- collection: this.collection,
- settings: this.settings
- });
-
- this.view.render();
- this.filter('selected');
- return this;
-};
-
-/*
- * SelectList API Methods
- */
-
-SelectList.prototype.filter = function(filter) {
- this.view.filterBySelection(filter);
- return this;
-};
-
-SelectList.prototype.search = function(query) {
- this.view.searchByQuery(query);
- return this;
-};
-
-/*
- * SelectList Defaults
- */
-
-SelectList.prototype.defaults = {
- width: '50%',
- height: 400,
-
- readOnly: false,
- focusSearch: true,
-
- dangerouslyUnescapedHtmlFormat(item) {
- return escapeHtml(item.value);
- },
-
- parse(r) {
- this.more = r.more;
- return r.results;
- },
-
- queryParam: 'query',
-
- labels: {
- selected: 'Selected',
- deselected: 'Deselected',
- all: 'All',
- noResults: ''
- },
-
- tooltips: {
- select: 'Click this to select item',
- deselect: 'Click this to deselect item'
- },
-
- errorMessage: 'Something gone wrong, try to reload the page and try again.'
-};
-
-export default SelectList;
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+.select-list {
+}
+
.select-list-container {
min-width: 500px;
box-sizing: border-box;
.select-list-list-container {
border: 1px solid #bfbfbf;
box-sizing: border-box;
+ height: 400px;
+ overflow: auto;
}
.select-list-list-container.loading .select-list-list {
+++ /dev/null
-<input class="select-list-list-checkbox" type="checkbox">
-<div class="select-list-list-item">{{{this}}}</div>
+++ /dev/null
-<div class="select-list-container">
- <div class="select-list-control">
- <div class="select-list-check-control">
- <a class="select-list-control-button" name="selected">{{this.selected}}</a><a class="select-list-control-button" name="deselected">{{this.deselected}}</a><a class="select-list-control-button" name="all">{{this.all}}</a>
- </div>
- <div class="select-list-search-control">
- <div class="search-box">
- <input class="search-box-input" type="text" name="q" placeholder="{{t 'search_verb'}}" maxlength="100" autocomplete="off">
- <svg class="search-box-magnifier" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
- <g transform="matrix(0.0288462,0,0,0.0288462,2,1.07692)">
- <path d="M288,208C288,177.167 277.042,150.792 255.125,128.875C233.208,106.958 206.833,96 176,96C145.167,96 118.792,106.958 96.875,128.875C74.958,150.792 64,177.167 64,208C64,238.833 74.958,265.208 96.875,287.125C118.792,309.042 145.167,320 176,320C206.833,320 233.208,309.042 255.125,287.125C277.042,265.208 288,238.833 288,208ZM416,416C416,424.667 412.833,432.167 406.5,438.5C400.167,444.833 392.667,448 384,448C375,448 367.5,444.833 361.5,438.5L275.75,353C245.917,373.667 212.667,384 176,384C152.167,384 129.375,379.375 107.625,370.125C85.875,360.875 67.125,348.375 51.375,332.625C35.625,316.875 23.125,298.125 13.875,276.375C4.625,254.625 0,231.833 0,208C0,184.167 4.625,161.375 13.875,139.625C23.125,117.875 35.625,99.125 51.375,83.375C67.125,67.625 85.875,55.125 107.625,45.875C129.375,36.625 152.167,32 176,32C199.833,32 222.625,36.625 244.375,45.875C266.125,55.125 284.875,67.625 300.625,83.375C316.375,99.125 328.875,117.875 338.125,139.625C347.375,161.375 352,184.167 352,208C352,244.667 341.667,277.917 321,307.75L406.75,393.5C412.917,399.667 416,407.167 416,416Z" style="fill:currentColor;fill-rule:nonzero;"/>
- </g>
- </svg>
- <button class="js-reset hidden button-tiny search-box-clear button-icon" style="color: rgb(153, 153, 153);" type="reset">
- <svg width="12" height="12" viewBox="0 0 16 16" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">
- <path d="M14 4.242L11.758 2l-3.76 3.76L4.242 2 2 4.242l3.756 3.756L2 11.758 4.242 14l3.756-3.76 3.76 3.76L14 11.758l-3.76-3.76L14 4.242z" style="fill: currentcolor;"/>
- </svg>
- </button>
- </div>
- </div>
- </div>
- <div class="select-list-list-container">
- <ul class="select-list-list"></ul>
- </div>
-</div>
interface Props {
checked: boolean;
+ disabled?: boolean;
children?: React.ReactNode;
className?: string;
id?: string;
thirdState: false
};
- handleClick = (e: React.SyntheticEvent<HTMLElement>) => {
- e.preventDefault();
- e.currentTarget.blur();
- this.props.onCheck(!this.props.checked, this.props.id);
+ handleClick = (event: React.SyntheticEvent<HTMLElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ if (!this.props.disabled) {
+ this.props.onCheck(!this.props.checked, this.props.id);
+ }
};
render() {
const className = classNames('icon-checkbox', {
'icon-checkbox-checked': this.props.checked,
- 'icon-checkbox-single': this.props.thirdState
+ 'icon-checkbox-single': this.props.thirdState,
+ 'icon-checkbox-disabled': this.props.disabled
});
if (this.props.children) {
return (
<a
- id={this.props.id}
- className={classNames('link-checkbox', this.props.className)}
+ className={classNames('link-checkbox', this.props.className, {
+ 'text-muted': this.props.disabled
+ })}
href="#"
+ id={this.props.id}
onClick={this.handleClick}>
<i className={className} />
{this.props.children}
return (
<a
- id={this.props.id}
className={classNames(className, this.props.className)}
href="#"
+ id={this.props.id}
onClick={this.handleClick}
/>
);
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import * as classNames from 'classnames';
import Tooltip from './Tooltip';
interface Option {
}
interface Props {
+ className?: string;
name: string;
onCheck: (value: string) => void;
options: Option[];
};
render() {
- return <ul className="radio-toggle">{this.props.options.map(this.renderOption)}</ul>;
+ return (
+ <ul className={classNames('radio-toggle', this.props.className)}>
+ {this.props.options.map(this.renderOption)}
+ </ul>
+ );
}
}
expect(checkbox.is('.icon-checkbox-checked')).toBe(true);
});
+it('should render disabled', () => {
+ const checkbox = shallow(<Checkbox checked={true} disabled={true} onCheck={() => true} />);
+ expect(checkbox.is('.icon-checkbox-disabled')).toBe(true);
+});
+
it('should render unchecked third state', () => {
- const checkbox = shallow(<Checkbox checked={false} thirdState={true} onCheck={() => true} />);
+ const checkbox = shallow(<Checkbox checked={false} onCheck={() => true} thirdState={true} />);
expect(checkbox.is('.icon-checkbox-single')).toBe(true);
expect(checkbox.is('.icon-checkbox-checked')).toBe(false);
});
-it('should render checked third state', () => {
- const checkbox = shallow(<Checkbox checked={true} thirdState={true} onCheck={() => true} />);
+it('should render checked third state', () => {
+ const checkbox = shallow(<Checkbox checked={true} onCheck={() => true} thirdState={true} />);
expect(checkbox.is('.icon-checkbox-single')).toBe(true);
expect(checkbox.is('.icon-checkbox-checked')).toBe(true);
});
expect(onCheck).toBeCalledWith(true, undefined);
});
+it('should not call onCheck when disabled', () => {
+ const onCheck = jest.fn();
+ const checkbox = shallow(<Checkbox checked={false} disabled={true} onCheck={onCheck} />);
+ click(checkbox);
+ expect(onCheck).toHaveBeenCalledTimes(0);
+});
+
it('should call onCheck with id as second parameter', () => {
const onCheck = jest.fn();
- const checkbox = shallow(<Checkbox id="foo" checked={false} onCheck={onCheck} />);
+ const checkbox = shallow(<Checkbox checked={false} id="foo" onCheck={onCheck} />);
click(checkbox);
expect(onCheck).toBeCalledWith(true, 'foo');
});
it('should apply custom class', () => {
const checkbox = shallow(
- <Checkbox className="customclass" checked={true} onCheck={() => true} />
+ <Checkbox checked={true} className="customclass" onCheck={() => true} />
);
expect(checkbox.is('.customclass')).toBe(true);
});
search_verb=Search
see_all=See All
select_verb=Select
+selected=Selected
set=Set
severity=Severity
shared=Shared
updated_on=Updated on
update_verb=Update
updating=Updating
+unselected=Unselected
user=User
value=Value
variation=Variation
webhooks.url.bad_format=Bad format of URL.
webhooks.url.bad_protocol=URL must start with "http://" or "https://".
webhooks.url.description=Server endpoint that will receive the webhook payload, for example: "http://my_server/foo". If HTTP Basic authentication is used, HTTPS is recommended to avoid man in the middle attacks. Example: "https://myLogin:myPassword@my_server/foo"
-webhooks.url.required=URL is required.
\ No newline at end of file
+webhooks.url.required=URL is required.