*/
import * as React from 'react';
import { bulkApplyTemplate, getPermissionTemplates } from '../../api/permissions';
+import { Project } from '../../api/project-management';
import Modal from '../../components/controls/Modal';
import Select from '../../components/controls/Select';
import { ResetButtonLink, SubmitButton } from '../../components/controls/buttons';
provisioned: boolean;
qualifier: string;
query: string;
- selection: string[];
+ selection: Project[];
total: number;
}
const { permissionTemplate } = this.state;
if (permissionTemplate) {
this.setState({ submitting: true });
- const parameters = this.props.selection.length
+ const selection = this.props.selection.filter((s) => !s.managed);
+ const parameters = selection.length
? {
- projects: this.props.selection.join(),
+ projects: selection.map((s) => s.key).join(),
qualifiers: this.props.qualifier,
templateId: permissionTemplate,
}
this.setState({ permissionTemplate: value });
};
- renderWarning = () => (
- <Alert variant="warning">
- {this.props.selection.length
- ? translateWithParameters(
- 'permission_templates.bulk_apply_permission_template.apply_to_selected',
- this.props.selection.length,
- )
- : translateWithParameters(
- 'permission_templates.bulk_apply_permission_template.apply_to_all',
- this.props.total,
+ renderWarning = () => {
+ const { selection } = this.props;
+
+ const managedProjects = selection.filter((s) => s.managed);
+ const localProjects = selection.filter((s) => !s.managed);
+ const isSelectionOnlyManaged = !!managedProjects.length && !localProjects.length;
+ const isSelectionOnlyLocal = !managedProjects.length && !!localProjects.length;
+
+ if (isSelectionOnlyManaged) {
+ return (
+ <Alert variant="error">
+ {translate(
+ 'permission_templates.bulk_apply_permission_template.apply_to_only_github_projects',
)}
- </Alert>
- );
+ </Alert>
+ );
+ } else if (isSelectionOnlyLocal) {
+ return (
+ <Alert variant="warning">
+ {this.props.selection.length
+ ? translateWithParameters(
+ 'permission_templates.bulk_apply_permission_template.apply_to_selected',
+ this.props.selection.length,
+ )
+ : translateWithParameters(
+ 'permission_templates.bulk_apply_permission_template.apply_to_all',
+ this.props.total,
+ )}
+ </Alert>
+ );
+ }
+ return (
+ <Alert variant="warning">
+ {translateWithParameters(
+ 'permission_templates.bulk_apply_permission_template.apply_to_selected',
+ localProjects.length,
+ )}
+ <br />
+ {translateWithParameters(
+ 'permission_templates.bulk_apply_permission_template.apply_to_github_projects',
+ managedProjects.length,
+ )}
+ </Alert>
+ );
+ };
- renderSelect = () => {
+ renderSelect = (isSelectionOnlyManaged: boolean) => {
const options =
this.state.permissionTemplates !== undefined
? this.state.permissionTemplates.map((t) => ({ label: t.name, value: t.id }))
<Select
id="bulk-apply-template"
inputId="bulk-apply-template-input"
- isDisabled={this.state.submitting}
+ isDisabled={this.state.submitting || isSelectionOnlyManaged}
onChange={this.handlePermissionTemplateChange}
options={options}
value={options.find((option) => option.value === this.state.permissionTemplate)}
const { done, loading, permissionTemplates, submitting } = this.state;
const header = translate('permission_templates.bulk_apply_permission_template');
+ const isSelectionOnlyManaged = this.props.selection.every((s) => s.managed === true);
+
return (
<Modal contentLabel={header} onRequestClose={this.props.onClose} size="small">
<header className="modal-head">
<>
<MandatoryFieldsExplanation className="spacer-bottom" />
{this.renderWarning()}
- {this.renderSelect()}
+ {this.renderSelect(isSelectionOnlyManaged)}
</>
)}
</div>
<footer className="modal-foot">
{submitting && <i className="spinner spacer-right" />}
{!loading && !done && permissionTemplates && (
- <SubmitButton disabled={submitting} onClick={this.handleConfirmClick}>
+ <SubmitButton
+ disabled={submitting || isSelectionOnlyManaged}
+ onClick={this.handleConfirmClick}
+ >
{translate('apply')}
</SubmitButton>
)}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { bulkDeleteProjects } from '../../api/project-management';
+import { Project, bulkDeleteProjects } from '../../api/project-management';
import Modal from '../../components/controls/Modal';
import { ResetButtonLink, SubmitButton } from '../../components/controls/buttons';
import { Alert } from '../../components/ui/Alert';
provisioned: boolean;
qualifier: string;
query: string;
- selection: string[];
+ selection: Project[];
total: number;
}
const { analyzedBefore } = this.props;
const parameters = this.props.selection.length
? {
- projects: this.props.selection.join(),
+ projects: this.props.selection.map((s) => s.key).join(),
}
: {
analyzedBefore: analyzedBefore && toISO8601WithOffsetString(analyzedBefore),
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { useState } from 'react';
import { Button, EditButton } from '../../components/controls/buttons';
import { translate } from '../../helpers/l10n';
import { Visibility } from '../../types/component';
onChangeDefaultProjectVisibility: (visibility: Visibility) => void;
}
-interface State {
- visibilityForm: boolean;
-}
-
-export default class Header extends React.PureComponent<Props, State> {
- state: State = { visibilityForm: false };
-
- handleChangeVisibilityClick = () => {
- this.setState({ visibilityForm: true });
- };
-
- closeVisiblityForm = () => {
- this.setState({ visibilityForm: false });
- };
+export default function Header(props: Readonly<Props>) {
+ const [visibilityForm, setVisibilityForm] = useState(false);
- render() {
- const { defaultProjectVisibility, hasProvisionPermission } = this.props;
- const { visibilityForm } = this.state;
+ const { defaultProjectVisibility, hasProvisionPermission } = props;
- return (
- <header className="page-header">
- <h1 className="page-title">{translate('projects_management')}</h1>
+ return (
+ <header className="page-header">
+ <h1 className="page-title">{translate('projects_management')}</h1>
- <div className="page-actions">
- <span className="big-spacer-right">
- <span className="text-middle">
- {translate('settings.projects.default_visibility_of_new_projects')}{' '}
- <strong>
- {defaultProjectVisibility ? translate('visibility', defaultProjectVisibility) : '—'}
- </strong>
- </span>
- <EditButton
- className="js-change-visibility spacer-left button-small"
- onClick={this.handleChangeVisibilityClick}
- aria-label={translate('settings.projects.change_visibility_form.label')}
- />
+ <div className="page-actions">
+ <span className="big-spacer-right">
+ <span className="text-middle">
+ {translate('settings.projects.default_visibility_of_new_projects')}{' '}
+ <strong>
+ {defaultProjectVisibility ? translate('visibility', defaultProjectVisibility) : '—'}
+ </strong>
</span>
+ <EditButton
+ className="js-change-visibility spacer-left button-small"
+ onClick={() => setVisibilityForm(true)}
+ aria-label={translate('settings.projects.change_visibility_form.label')}
+ />
+ </span>
- {hasProvisionPermission && (
- <Button id="create-project" onClick={this.props.onProjectCreate}>
- {translate('qualifiers.create.TRK')}
- </Button>
- )}
- </div>
+ {hasProvisionPermission && (
+ <Button id="create-project" onClick={props.onProjectCreate}>
+ {translate('qualifiers.create.TRK')}
+ </Button>
+ )}
+ </div>
- <p className="page-description">{translate('projects_management.page.description')}</p>
+ <p className="page-description">{translate('projects_management.page.description')}</p>
- {visibilityForm && (
- <ChangeDefaultVisibilityForm
- defaultVisibility={defaultProjectVisibility ?? Visibility.Public}
- onClose={this.closeVisiblityForm}
- onConfirm={this.props.onChangeDefaultProjectVisibility}
- />
- )}
- </header>
- );
- }
+ {visibilityForm && (
+ <ChangeDefaultVisibilityForm
+ defaultVisibility={defaultProjectVisibility ?? Visibility.Public}
+ onClose={() => setVisibilityForm(false)}
+ onConfirm={props.onChangeDefaultProjectVisibility}
+ />
+ )}
+ </header>
+ );
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { debounce, uniq, without } from 'lodash';
+import { debounce, uniq } from 'lodash';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import {
qualifiers: string;
query: string;
ready: boolean;
- selection: string[];
+ selection: Project[];
total: number;
visibility?: Visibility;
}
handleDateChanged = (analyzedBefore: Date | undefined) =>
this.setState({ ready: false, page: 1, analyzedBefore }, this.requestProjects);
- onProjectSelected = (project: string) => {
- this.setState(({ selection }) => ({ selection: uniq([...selection, project]) }));
+ onProjectSelected = (project: Project) => {
+ this.setState(({ selection }) => ({
+ selection: uniq([...selection, project]),
+ }));
};
- onProjectDeselected = (project: string) => {
- this.setState(({ selection }) => ({ selection: without(selection, project) }));
+ onProjectDeselected = (project: Project) => {
+ this.setState(({ selection }) => ({
+ selection: selection.filter(({ key }) => key !== project.key),
+ }));
};
onAllSelected = () => {
this.setState(({ projects }) => ({
- selection: projects.filter((p) => !p.managed).map((project) => project.key),
+ selection: projects,
}));
};
<Checkbox
label={translateWithParameters('projects_management.select_project', project.name)}
checked={selected}
- disabled={project.managed}
onCheck={handleProjectCheck}
/>
</td>
interface Props {
currentUser: Pick<LoggedInUser, 'login'>;
- onProjectDeselected: (project: string) => void;
- onProjectSelected: (project: string) => void;
+ onProjectDeselected: (project: Project) => void;
+ onProjectSelected: (project: Project) => void;
projects: Project[];
ready?: boolean;
- selection: string[];
+ selection: Project[];
}
-export default class Projects extends React.PureComponent<Props> {
- onProjectCheck = (project: Project, checked: boolean) => {
+export default function Projects(props: Readonly<Props>) {
+ const { ready, projects, currentUser, selection } = props;
+
+ const onProjectCheck = (project: Project, checked: boolean) => {
if (checked) {
- this.props.onProjectSelected(project.key);
+ props.onProjectSelected(project);
} else {
- this.props.onProjectDeselected(project.key);
+ props.onProjectDeselected(project);
}
};
- render() {
- return (
- <div className="boxed-group boxed-group-inner">
- <table
- className={classNames('data', 'zebra', { 'new-loading': !this.props.ready })}
- id="projects-management-page-projects"
- >
- <thead>
- <tr>
- <th />
- <th>{translate('name')}</th>
- <th />
- <th>{translate('key')}</th>
- <th className="thin nowrap text-right">{translate('last_analysis')}</th>
- <th />
- </tr>
- </thead>
- <tbody>
- {this.props.projects.map((project) => (
- <ProjectRow
- currentUser={this.props.currentUser}
- key={project.key}
- onProjectCheck={this.onProjectCheck}
- project={project}
- selected={this.props.selection.includes(project.key)}
- />
- ))}
- </tbody>
- </table>
- </div>
- );
- }
+ return (
+ <div className="boxed-group boxed-group-inner">
+ <table
+ className={classNames('data', 'zebra', { 'new-loading': !ready })}
+ id="projects-management-page-projects"
+ >
+ <thead>
+ <tr>
+ <th />
+ <th>{translate('name')}</th>
+ <th />
+ <th>{translate('key')}</th>
+ <th className="thin nowrap text-right">{translate('last_analysis')}</th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {projects.map((project) => (
+ <ProjectRow
+ currentUser={currentUser}
+ key={project.key}
+ onProjectCheck={onProjectCheck}
+ project={project}
+ selected={selection.some((s) => s.key === project.key)}
+ />
+ ))}
+ </tbody>
+ </table>
+ </div>
+ );
}
expect(ui.row.getAll()).toHaveLength(3);
});
-it('should bulk apply permission templates to projects', async () => {
- const user = userEvent.setup();
- handler.setProjects(
- Array.from({ length: 11 }, (_, i) => mockProject({ key: i.toString(), name: `Test ${i}` })),
- );
- renderProjectManagementApp();
-
- expect(await ui.bulkApplyButton.find()).toBeDisabled();
- const projects = ui.row.getAll().slice(1);
- expect(projects).toHaveLength(11);
- await user.click(ui.checkAll.get());
- expect(ui.bulkApplyButton.get()).toBeEnabled();
-
- await user.click(ui.bulkApplyButton.get());
- expect(await ui.bulkApplyDialog.find()).toBeInTheDocument();
- expect(
- within(ui.bulkApplyDialog.get()).getByText(
- 'permission_templates.bulk_apply_permission_template.apply_to_selected.11',
- ),
- ).toBeInTheDocument();
-
- await user.click(ui.apply.get(ui.bulkApplyDialog.get()));
- expect(
- await screen.findByText('bulk apply permission template error message'),
- ).toBeInTheDocument();
- expect(ui.bulkApplyDialog.get()).toBeInTheDocument();
-
- await user.click(ui.cancel.get(ui.bulkApplyDialog.get()));
+describe('Bulk permission templates', () => {
+ it('should be applied to local projects', async () => {
+ const user = userEvent.setup();
+ handler.setProjects(
+ Array.from({ length: 11 }, (_, i) => mockProject({ key: i.toString(), name: `Test ${i}` })),
+ );
+ renderProjectManagementApp();
+
+ expect(await ui.bulkApplyButton.find()).toBeDisabled();
+ const projects = ui.row.getAll().slice(1);
+ expect(projects).toHaveLength(11);
+ await user.click(ui.checkAll.get());
+ expect(ui.bulkApplyButton.get()).toBeEnabled();
+
+ await user.click(ui.bulkApplyButton.get());
+ expect(await ui.bulkApplyDialog.find()).toBeInTheDocument();
+ expect(
+ within(ui.bulkApplyDialog.get()).getByText(
+ 'permission_templates.bulk_apply_permission_template.apply_to_selected.11',
+ ),
+ ).toBeInTheDocument();
+
+ await user.click(ui.apply.get(ui.bulkApplyDialog.get()));
+ expect(
+ await screen.findByText('bulk apply permission template error message'),
+ ).toBeInTheDocument();
+ expect(ui.bulkApplyDialog.get()).toBeInTheDocument();
+
+ await user.click(ui.cancel.get(ui.bulkApplyDialog.get()));
+
+ await user.click(ui.uncheckAll.get());
+ await user.click(ui.checkbox.get(projects[8]));
+ await user.click(ui.checkbox.get(projects[9]));
+ await user.click(ui.checkbox.get(projects[10]));
+ await user.click(ui.checkbox.get(projects[9])); // uncheck one
+ await user.click(ui.bulkApplyButton.get());
+
+ expect(await ui.bulkApplyDialog.find()).toBeInTheDocument();
+ expect(
+ within(ui.bulkApplyDialog.get()).getByText(
+ 'permission_templates.bulk_apply_permission_template.apply_to_selected.2',
+ ),
+ ).toBeInTheDocument();
+ await selectEvent.select(
+ ui.selectTemplate.get(ui.bulkApplyDialog.get()),
+ 'Permission Template 2',
+ );
+ await user.click(ui.apply.get(ui.bulkApplyDialog.get()));
- await user.click(ui.uncheckAll.get());
- await user.click(ui.checkbox.get(projects[8]));
- await user.click(ui.checkbox.get(projects[9]));
- await user.click(ui.checkbox.get(projects[10]));
- await user.click(ui.checkbox.get(projects[9])); // uncheck one
- await user.click(ui.bulkApplyButton.get());
+ expect(
+ await within(ui.bulkApplyDialog.get()).findByText('projects_role.apply_template.success'),
+ ).toBeInTheDocument();
+ });
- expect(await ui.bulkApplyDialog.find()).toBeInTheDocument();
- expect(
- within(ui.bulkApplyDialog.get()).getByText(
- 'permission_templates.bulk_apply_permission_template.apply_to_selected.2',
- ),
- ).toBeInTheDocument();
- await selectEvent.select(
- ui.selectTemplate.get(ui.bulkApplyDialog.get()),
- 'Permission Template 2',
- );
- await user.click(ui.apply.get(ui.bulkApplyDialog.get()));
+ it('should not be applied to managed projects', async () => {
+ const user = userEvent.setup();
+ handler.setProjects(
+ Array.from({ length: 11 }, (_, i) =>
+ mockProject({ key: i.toString(), name: `Test ${i}`, managed: true }),
+ ),
+ );
+ renderProjectManagementApp();
+
+ expect(await ui.bulkApplyButton.find()).toBeDisabled();
+ const projects = ui.row.getAll().slice(1);
+ expect(projects).toHaveLength(11);
+ await user.click(ui.checkAll.get());
+ expect(ui.bulkApplyButton.get()).toBeEnabled();
+
+ await user.click(ui.bulkApplyButton.get());
+ expect(await ui.bulkApplyDialog.find()).toBeInTheDocument();
+ expect(
+ within(ui.bulkApplyDialog.get()).getByText(
+ 'permission_templates.bulk_apply_permission_template.apply_to_only_github_projects',
+ ),
+ ).toBeInTheDocument();
+ expect(ui.apply.get(ui.bulkApplyDialog.get())).toBeDisabled();
+ });
- expect(
- await within(ui.bulkApplyDialog.get()).findByText('projects_role.apply_template.success'),
- ).toBeInTheDocument();
+ it('should not be applied to managed projects but to local project', async () => {
+ const user = userEvent.setup();
+ const allProjects = [
+ ...Array.from({ length: 6 }, (_, i) =>
+ mockProject({ key: `${i.toString()} managed`, name: `Test managed ${i}`, managed: true }),
+ ),
+ ...Array.from({ length: 5 }, (_, i) =>
+ mockProject({ key: `${i.toString()} local`, name: `Test local ${i}`, managed: false }),
+ ),
+ ];
+
+ handler.setProjects(allProjects);
+ renderProjectManagementApp();
+
+ expect(await ui.bulkApplyButton.find()).toBeDisabled();
+ const projects = ui.row.getAll().slice(1);
+ expect(projects).toHaveLength(11);
+ await user.click(ui.checkAll.get());
+ expect(ui.bulkApplyButton.get()).toBeEnabled();
+
+ await user.click(ui.bulkApplyButton.get());
+ expect(await ui.bulkApplyDialog.find()).toBeInTheDocument();
+ expect(
+ within(ui.bulkApplyDialog.get()).getByText(
+ /permission_templates.bulk_apply_permission_template.apply_to_selected.5/,
+ ),
+ ).toBeInTheDocument();
+ expect(
+ within(ui.bulkApplyDialog.get()).getByText(
+ /permission_templates.bulk_apply_permission_template.apply_to_github_projects.6/,
+ ),
+ ).toBeInTheDocument();
+ expect(ui.apply.get(ui.bulkApplyDialog.get())).toBeEnabled();
+ });
});
it('should load more and change the filter without caching old pages', async () => {
expect(ui.defaultVisibilityWarning.get()).toHaveTextContent('.github');
});
-it('should not allow apply permissions for managed projects', async () => {
+it('should not apply permissions for github projects', async () => {
const user = userEvent.setup();
renderProjectManagementApp();
await waitFor(() => expect(ui.row.getAll()).toHaveLength(5));
const rows = ui.row.getAll();
- expect(ui.checkbox.get(rows[4])).toHaveAttribute('aria-disabled', 'true');
+ expect(ui.checkbox.get(rows[4])).not.toHaveAttribute('aria-disabled');
expect(ui.checkbox.get(rows[1])).not.toHaveAttribute('aria-disabled');
await user.click(ui.checkAll.get());
- expect(ui.checkbox.get(rows[4])).not.toBeChecked();
+ expect(ui.checkbox.get(rows[4])).toBeChecked();
expect(ui.checkbox.get(rows[1])).toBeChecked();
await act(() => user.click(ui.projectActions.get(rows[4])));
expect(ui.applyPermissionTemplate.query()).not.toBeInTheDocument();
permission_templates.bulk_apply_permission_template=Bulk Apply Permission Template
permission_templates.bulk_apply_permission_template.apply_to_selected=You're about to apply the selected permission template to {0} selected item(s).
permission_templates.bulk_apply_permission_template.apply_to_all=You're about to apply the selected permission template to {0} item(s).
+permission_templates.bulk_apply_permission_template.apply_to_github_projects=You can't apply the selected permission template to the {0} GitHub project(s).
+permission_templates.bulk_apply_permission_template.apply_to_only_github_projects=You can't apply permission templates to GitHub projects.
permission_templates.select_to_delete=You must select at least one item
permission_templates.delete_selected=Delete all selected items
permission_templates.show_actions_for_x=Show actions for template {0}