]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20522 Not able to delete GitHub projects from Projects Management page
authorguillaume-peoch-sonarsource <guillaume.peoch@sonarsource.com>
Wed, 4 Oct 2023 14:00:47 +0000 (16:00 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 5 Oct 2023 20:02:47 +0000 (20:02 +0000)
server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx
server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx
server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ProjectManagementApp.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index bde6229adbe2e9e8989e0e44b64d33a35e370966..7f1f9a41cab856a769d0d7a9ec85b73444326cc6 100644 (file)
@@ -19,6 +19,7 @@
  */
 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';
@@ -36,7 +37,7 @@ export interface Props {
   provisioned: boolean;
   qualifier: string;
   query: string;
-  selection: string[];
+  selection: Project[];
   total: number;
 }
 
@@ -87,9 +88,10 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
     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,
           }
@@ -120,21 +122,53 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
     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 }))
@@ -148,7 +182,7 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
         <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)}
@@ -161,6 +195,8 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
     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">
@@ -178,7 +214,7 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
             <>
               <MandatoryFieldsExplanation className="spacer-bottom" />
               {this.renderWarning()}
-              {this.renderSelect()}
+              {this.renderSelect(isSelectionOnlyManaged)}
             </>
           )}
         </div>
@@ -186,7 +222,10 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
         <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>
           )}
index 26fe9322eb7675fbfc1a37c9c02e81d9bf9baf75..d592ee5fb4f4b3704d564d8b0bd39d12f43fbbc3 100644 (file)
@@ -18,7 +18,7 @@
  * 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';
@@ -32,7 +32,7 @@ export interface Props {
   provisioned: boolean;
   qualifier: string;
   query: string;
-  selection: string[];
+  selection: Project[];
   total: number;
 }
 
@@ -57,7 +57,7 @@ export default class DeleteModal extends React.PureComponent<Props, State> {
     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),
index 1c3dd413f3c922f7e6b02f8cb2f95ec0e6abcac6..07d061f64c7fe9db40a2ad93537a5ef339e4b3a6 100644 (file)
@@ -18,6 +18,7 @@
  * 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';
@@ -30,61 +31,46 @@ export interface Props {
   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>
+  );
 }
index 3ec6239e1b448a40cd2fe9ef493cf9f360180d63..a4060df3bb2cc12bf93a353b1f29da29ead0fb21 100644 (file)
@@ -17,7 +17,7 @@
  * 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 {
@@ -56,7 +56,7 @@ interface State {
   qualifiers: string;
   query: string;
   ready: boolean;
-  selection: string[];
+  selection: Project[];
   total: number;
   visibility?: Visibility;
 }
@@ -179,17 +179,21 @@ class ProjectManagementApp extends React.PureComponent<Props, State> {
   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,
     }));
   };
 
index 76fda3be0512fb4a5e1d55f098beaf707f04eb55..9ced6b325454a02ffe0a5b18beeefbd36c06fbf1 100644 (file)
@@ -54,7 +54,6 @@ export default function ProjectRow(props: Props) {
         <Checkbox
           label={translateWithParameters('projects_management.select_project', project.name)}
           checked={selected}
-          disabled={project.managed}
           onCheck={handleProjectCheck}
         />
       </td>
index 447e1a64d3d77a1799c48dc22e3bfa44848be4d5..6afda55017b867ff112b48de94c20d1fb0ad7595 100644 (file)
@@ -26,52 +26,52 @@ import ProjectRow from './ProjectRow';
 
 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>
+  );
 }
index 4449078d6cc4310f82880e1a23264ab9c039da5a..d83d6fb46b529b7cb4e64e6ab40818fdcac5392a 100644 (file)
@@ -236,57 +236,119 @@ it('should delete projects, but not Portfolios or Applications', async () => {
   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 () => {
@@ -461,15 +523,15 @@ it('should show github warning on changing default visibility to admin', 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();
index e9a392af9b676dde47ffcbabf5a5ece73eb2422e..b47918383fd8c7e020b8e4730470effcfa80c978 100644 (file)
@@ -3229,6 +3229,8 @@ permission_templates.project_creators.explanation=When a new project, portfolio
 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}