]> source.dussan.org Git - sonarqube.git/commitdiff
rewrite parts of permission templates app in react (#3070)
authorStas Vilchik <stas.vilchik@sonarsource.com>
Fri, 16 Feb 2018 08:12:23 +0000 (09:12 +0100)
committerGitHub <noreply@github.com>
Fri, 16 Feb 2018 08:12:23 +0000 (09:12 +0100)
27 files changed:
server/sonar-web/src/main/js/api/permissions.ts
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js [deleted file]
server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permission-templates/components/Form.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permission-templates/components/Header.js [deleted file]
server/sonar-web/src/main/js/apps/permission-templates/components/Header.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-delete.hbs [deleted file]
server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-form.hbs [deleted file]
server/sonar-web/src/main/js/apps/permission-templates/views/CreateView.js [deleted file]
server/sonar-web/src/main/js/apps/permission-templates/views/DeleteView.js [deleted file]
server/sonar-web/src/main/js/apps/permission-templates/views/FormView.js [deleted file]
server/sonar-web/src/main/js/apps/permission-templates/views/UpdateView.js [deleted file]
server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js [deleted file]
server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/project/templates/ApplyTemplateTemplate.hbs [deleted file]
server/sonar-web/src/main/js/apps/permissions/project/views/ApplyTemplateView.js [deleted file]
server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap

index b87dbc9956da89d45565d8fcdca58067fbf80dcd..465d4fedea3b66ea8d00c0e6c63b209a4c86a7f7 100644 (file)
@@ -18,6 +18,8 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { BaseSearchProjectsParameters } from './components';
+import { PermissionTemplate } from '../app/types';
+import throwGlobalError from '../app/utils/throwGlobalError';
 import { getJSON, post, postJSON, RequestData } from '../helpers/request';
 
 const PAGE_SIZE = 100;
@@ -86,21 +88,6 @@ export function revokePermissionFromGroup(
   return post('/api/permissions/remove_group', data);
 }
 
-export interface PermissionTemplate {
-  id: string;
-  name: string;
-  description?: string;
-  projectKeyPattern?: string;
-  createdAt: string;
-  updatedAt?: string;
-  permissions: Array<{
-    key: string;
-    usersCount: number;
-    groupsCount: number;
-    withProjectCreator?: boolean;
-  }>;
-}
-
 interface GetPermissionTemplatesResponse {
   permissionTemplates: PermissionTemplate[];
   defaultTemplates: Array<{ templateId: string; qualifier: string }>;
@@ -122,8 +109,8 @@ export function updatePermissionTemplate(data: RequestData): Promise<void> {
   return post('/api/permissions/update_template', data);
 }
 
-export function deletePermissionTemplate(data: RequestData): Promise<void> {
-  return post('/api/permissions/delete_template', data);
+export function deletePermissionTemplate(data: RequestData) {
+  return post('/api/permissions/delete_template', data).catch(throwGlobalError);
 }
 
 /**
@@ -133,8 +120,8 @@ export function setDefaultPermissionTemplate(templateId: string, qualifier: stri
   return post('/api/permissions/set_default_template', { templateId, qualifier });
 }
 
-export function applyTemplateToProject(data: RequestData): Promise<void> {
-  return post('/api/permissions/apply_template', data);
+export function applyTemplateToProject(data: RequestData) {
+  return post('/api/permissions/apply_template', data).catch(throwGlobalError);
 }
 
 export function bulkApplyTemplate(data: BaseSearchProjectsParameters): Promise<void> {
index 504e86547be5689ac9bae628a2a72ec3263bdfcc..4ec38c6b96193e16686b7fb0b823e0fa6c7169f8 100644 (file)
@@ -99,6 +99,7 @@ export interface Component extends LightComponent {
 }
 
 interface ComponentConfiguration {
+  canApplyPermissionTemplate?: boolean;
   extensions?: Extension[];
   showBackgroundTasks?: boolean;
   showLinks?: boolean;
@@ -314,3 +315,19 @@ export interface CustomMeasure {
   value: string;
   updatedAt?: string;
 }
+
+export interface PermissionTemplate {
+  defaultFor: string[];
+  id: string;
+  name: string;
+  description?: string;
+  projectKeyPattern?: string;
+  createdAt: string;
+  updatedAt?: string;
+  permissions: Array<{
+    key: string;
+    usersCount: number;
+    groupsCount: number;
+    withProjectCreator?: boolean;
+  }>;
+}
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js
deleted file mode 100644 (file)
index e01fe8c..0000000
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * 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 React from 'react';
-import PropTypes from 'prop-types';
-import { Link } from 'react-router';
-import { difference } from 'lodash';
-import Backbone from 'backbone';
-import { PermissionTemplateType, CallbackType } from '../propTypes';
-import ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown';
-import QualifierIcon from '../../../components/shared/QualifierIcon';
-import UpdateView from '../views/UpdateView';
-import DeleteView from '../views/DeleteView';
-import { translate } from '../../../helpers/l10n';
-import { setDefaultPermissionTemplate } from '../../../api/permissions';
-
-export default class ActionsCell extends React.PureComponent {
-  static propTypes = {
-    organization: PropTypes.object,
-    permissionTemplate: PermissionTemplateType.isRequired,
-    topQualifiers: PropTypes.array.isRequired,
-    refresh: CallbackType,
-    fromDetails: PropTypes.bool
-  };
-
-  static defaultProps = {
-    fromDetails: false
-  };
-
-  static contextTypes = {
-    router: PropTypes.object
-  };
-
-  handleUpdateClick = () => {
-    new UpdateView({
-      model: new Backbone.Model(this.props.permissionTemplate),
-      refresh: this.props.refresh
-    }).render();
-  };
-
-  handleDeleteClick = () => {
-    new DeleteView({
-      model: new Backbone.Model(this.props.permissionTemplate)
-    })
-      .on('done', () => {
-        const pathname = this.props.organization
-          ? `/organizations/${this.props.organization.key}/permission_templates`
-          : '/permission_templates';
-        this.context.router.replace(pathname);
-        this.props.refresh();
-      })
-      .render();
-  };
-
-  setDefault = qualifier => () => {
-    setDefaultPermissionTemplate(this.props.permissionTemplate.id, qualifier).then(
-      this.props.refresh,
-      () => {}
-    );
-  };
-
-  getAvailableQualifiers() {
-    const topQualifiers =
-      this.props.organization && !this.props.organization.isDefault
-        ? ['TRK']
-        : this.props.topQualifiers;
-    return difference(topQualifiers, this.props.permissionTemplate.defaultFor);
-  }
-
-  renderSetDefaultsControl() {
-    const availableQualifiers = this.getAvailableQualifiers();
-
-    if (availableQualifiers.length === 0) {
-      return null;
-    }
-
-    return this.props.topQualifiers.length === 1
-      ? this.renderIfSingleTopQualifier(availableQualifiers)
-      : this.renderIfMultipleTopQualifiers(availableQualifiers);
-  }
-
-  renderSetDefaultLink(qualifier, child) {
-    return (
-      <ActionsDropdownItem
-        key={qualifier}
-        className="js-set-default"
-        data-qualifier={qualifier}
-        onClick={this.setDefault(qualifier)}>
-        {child}
-      </ActionsDropdownItem>
-    );
-  }
-
-  renderIfSingleTopQualifier(availableQualifiers) {
-    return availableQualifiers.map(qualifier =>
-      this.renderSetDefaultLink(
-        qualifier,
-        <span>{translate('permission_templates.set_default')}</span>
-      )
-    );
-  }
-
-  renderIfMultipleTopQualifiers(availableQualifiers) {
-    return availableQualifiers.map(qualifier =>
-      this.renderSetDefaultLink(
-        qualifier,
-        <span>
-          {translate('permission_templates.set_default_for')}{' '}
-          <QualifierIcon qualifier={qualifier} /> {translate('qualifiers', qualifier)}
-        </span>
-      )
-    );
-  }
-
-  render() {
-    const { permissionTemplate: t, organization } = this.props;
-
-    const pathname = organization
-      ? `/organizations/${organization.key}/permission_templates`
-      : '/permission_templates';
-
-    return (
-      <ActionsDropdown>
-        {this.renderSetDefaultsControl()}
-
-        {!this.props.fromDetails && (
-          <ActionsDropdownItem to={{ pathname, query: { id: t.id } }}>
-            {translate('edit_permissions')}
-          </ActionsDropdownItem>
-        )}
-
-        <ActionsDropdownItem className="js-update" onClick={this.handleUpdateClick}>
-          {translate('update_details')}
-        </ActionsDropdownItem>
-
-        {t.defaultFor.length === 0 && (
-          <ActionsDropdownItem className="js-delete" onClick={this.handleDeleteClick}>
-            {translate('delete')}
-          </ActionsDropdownItem>
-        )}
-      </ActionsDropdown>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx
new file mode 100644 (file)
index 0000000..121c008
--- /dev/null
@@ -0,0 +1,205 @@
+/*
+ * 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 PropTypes from 'prop-types';
+import { difference } from 'lodash';
+import Form from './Form';
+import {
+  setDefaultPermissionTemplate,
+  deletePermissionTemplate,
+  updatePermissionTemplate
+} from '../../../api/permissions';
+import { PermissionTemplate } from '../../../app/types';
+import ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown';
+import ConfirmButton from '../../../components/controls/ConfirmButton';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+interface Props {
+  fromDetails?: boolean;
+  organization?: { isDefault?: boolean; key: string };
+  permissionTemplate: PermissionTemplate;
+  refresh: () => void;
+  topQualifiers: string[];
+}
+
+interface State {
+  updateModal: boolean;
+}
+
+export default class ActionsCell extends React.PureComponent<Props, State> {
+  mounted = false;
+
+  static contextTypes = {
+    router: PropTypes.object
+  };
+
+  state: State = { updateModal: false };
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  handleUpdateClick = () => {
+    this.setState({ updateModal: true });
+  };
+
+  handleCloseUpdateModal = () => {
+    if (this.mounted) {
+      this.setState({ updateModal: false });
+    }
+  };
+
+  handleSubmitUpdateModal = (data: {
+    description: string;
+    name: string;
+    projectKeyPattern: string;
+  }) => {
+    return updatePermissionTemplate({ id: this.props.permissionTemplate.id, ...data }).then(
+      this.props.refresh
+    );
+  };
+
+  handleDelete = (templateId: string) => {
+    return deletePermissionTemplate({ templateId }).then(() => {
+      const pathname = this.props.organization
+        ? `/organizations/${this.props.organization.key}/permission_templates`
+        : '/permission_templates';
+      this.context.router.replace(pathname);
+      this.props.refresh();
+    });
+  };
+
+  setDefault = (qualifier: string) => () => {
+    setDefaultPermissionTemplate(this.props.permissionTemplate.id, qualifier).then(
+      this.props.refresh,
+      () => {}
+    );
+  };
+
+  getAvailableQualifiers() {
+    const topQualifiers =
+      this.props.organization && !this.props.organization.isDefault
+        ? ['TRK']
+        : this.props.topQualifiers;
+    return difference(topQualifiers, this.props.permissionTemplate.defaultFor);
+  }
+
+  renderSetDefaultsControl() {
+    const availableQualifiers = this.getAvailableQualifiers();
+
+    if (availableQualifiers.length === 0) {
+      return null;
+    }
+
+    return this.props.topQualifiers.length === 1
+      ? this.renderIfSingleTopQualifier(availableQualifiers)
+      : this.renderIfMultipleTopQualifiers(availableQualifiers);
+  }
+
+  renderSetDefaultLink(qualifier: string, child: React.ReactNode) {
+    return (
+      <ActionsDropdownItem
+        className="js-set-default"
+        data-qualifier={qualifier}
+        key={qualifier}
+        onClick={this.setDefault(qualifier)}>
+        {child}
+      </ActionsDropdownItem>
+    );
+  }
+
+  renderIfSingleTopQualifier(availableQualifiers: string[]) {
+    return availableQualifiers.map(qualifier =>
+      this.renderSetDefaultLink(
+        qualifier,
+        <span>{translate('permission_templates.set_default')}</span>
+      )
+    );
+  }
+
+  renderIfMultipleTopQualifiers(availableQualifiers: string[]) {
+    return availableQualifiers.map(qualifier =>
+      this.renderSetDefaultLink(
+        qualifier,
+        <span>
+          {translate('permission_templates.set_default_for')}{' '}
+          <QualifierIcon qualifier={qualifier} /> {translate('qualifiers', qualifier)}
+        </span>
+      )
+    );
+  }
+
+  render() {
+    const { permissionTemplate: t, organization } = this.props;
+
+    const pathname = organization
+      ? `/organizations/${organization.key}/permission_templates`
+      : '/permission_templates';
+
+    return (
+      <ActionsDropdown>
+        {this.renderSetDefaultsControl()}
+
+        {!this.props.fromDetails && (
+          <ActionsDropdownItem to={{ pathname, query: { id: t.id } }}>
+            {translate('edit_permissions')}
+          </ActionsDropdownItem>
+        )}
+
+        <ActionsDropdownItem className="js-update" onClick={this.handleUpdateClick}>
+          {translate('update_details')}
+        </ActionsDropdownItem>
+        {this.state.updateModal && (
+          <Form
+            confirmButtonText={translate('update_verb')}
+            header={translate('permission_template.edit_template')}
+            onClose={this.handleCloseUpdateModal}
+            onSubmit={this.handleSubmitUpdateModal}
+            permissionTemplate={t}
+          />
+        )}
+
+        {t.defaultFor.length === 0 && (
+          <ConfirmButton
+            confirmButtonText={translate('delete')}
+            confirmData={t.id}
+            isDestructive={true}
+            modalBody={translateWithParameters(
+              'permission_template.do_you_want_to_delete_template_xxx',
+              t.name
+            )}
+            modalHeader={translate('permission_template.delete_confirm_title')}
+            onConfirm={this.handleDelete}>
+            {({ onClick }) => (
+              <ActionsDropdownItem className="js-delete" destructive={true} onClick={onClick}>
+                {translate('delete')}
+              </ActionsDropdownItem>
+            )}
+          </ConfirmButton>
+        )}
+      </ActionsDropdown>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/Form.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/Form.tsx
new file mode 100644 (file)
index 0000000..3bf36d1
--- /dev/null
@@ -0,0 +1,154 @@
+/*
+ * 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 DeferredSpinner from '../../../components/common/DeferredSpinner';
+import SimpleModal from '../../../components/controls/SimpleModal';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  confirmButtonText: string;
+  header: string;
+  permissionTemplate?: { description?: string; name: string; projectKeyPattern?: string };
+  onClose: () => void;
+  onSubmit: (
+    data: { description: string; name: string; projectKeyPattern: string }
+  ) => Promise<void>;
+}
+
+interface State {
+  description: string;
+  name: string;
+  projectKeyPattern: string;
+}
+
+export default class Form extends React.PureComponent<Props, State> {
+  mounted = false;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      description: (props.permissionTemplate && props.permissionTemplate.description) || '',
+      name: (props.permissionTemplate && props.permissionTemplate.name) || '',
+      projectKeyPattern:
+        (props.permissionTemplate && props.permissionTemplate.projectKeyPattern) || ''
+    };
+  }
+
+  handleSubmit = () => {
+    return this.props
+      .onSubmit({
+        description: this.state.description,
+        name: this.state.name,
+        projectKeyPattern: this.state.projectKeyPattern
+      })
+      .then(this.props.onClose);
+  };
+
+  handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    this.setState({ name: event.currentTarget.value });
+  };
+
+  handleDescriptionChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
+    this.setState({ description: event.currentTarget.value });
+  };
+
+  handleProjectKeyPatternChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    this.setState({ projectKeyPattern: event.currentTarget.value });
+  };
+
+  render() {
+    return (
+      <SimpleModal
+        header={this.props.header}
+        onClose={this.props.onClose}
+        onSubmit={this.handleSubmit}>
+        {({ onCloseClick, onFormSubmit, submitting }) => (
+          <form id="permission-template-form" onSubmit={onFormSubmit}>
+            <header className="modal-head">
+              <h2>{this.props.header}</h2>
+            </header>
+
+            <div className="modal-body">
+              <div className="modal-field">
+                <label htmlFor="permission-template-name">
+                  {translate('name')}
+                  <em className="mandatory">*</em>
+                </label>
+                <input
+                  autoFocus={true}
+                  id="permission-template-name"
+                  maxLength={256}
+                  name="name"
+                  onChange={this.handleNameChange}
+                  required={true}
+                  type="text"
+                  value={this.state.name}
+                />
+                <div className="modal-field-description">{translate('should_be_unique')}</div>
+              </div>
+
+              <div className="modal-field">
+                <label htmlFor="permission-template-description">{translate('description')}</label>
+                <textarea
+                  id="permission-template-description"
+                  name="description"
+                  onChange={this.handleDescriptionChange}
+                  value={this.state.description}
+                />
+              </div>
+
+              <div className="modal-field">
+                <label htmlFor="permission-template-project-key-pattern">
+                  {translate('permission_template.key_pattern')}
+                </label>
+                <input
+                  id="permission-template-project-key-pattern"
+                  maxLength={500}
+                  name="projectKeyPattern"
+                  onChange={this.handleProjectKeyPatternChange}
+                  type="text"
+                  value={this.state.projectKeyPattern}
+                />
+                <div className="modal-field-description">
+                  {translate('permission_template.key_pattern.description')}
+                </div>
+              </div>
+            </div>
+
+            <footer className="modal-foot">
+              <DeferredSpinner className="spacer-right" loading={submitting} />
+              <button disabled={submitting} id="permission-template-submit" type="submit">
+                {this.props.confirmButtonText}
+              </button>
+              <button
+                className="button-link"
+                disabled={submitting}
+                id="permission-template-cancel"
+                onClick={onCloseClick}
+                type="reset">
+                {translate('cancel')}
+              </button>
+            </footer>
+          </form>
+        )}
+      </SimpleModal>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/Header.js b/server/sonar-web/src/main/js/apps/permission-templates/components/Header.js
deleted file mode 100644 (file)
index 2635bbb..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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 React from 'react';
-import PropTypes from 'prop-types';
-import CreateView from '../views/CreateView';
-import { translate } from '../../../helpers/l10n';
-import { CallbackType } from '../propTypes';
-
-export default class Header extends React.PureComponent {
-  static propTypes = {
-    organization: PropTypes.object,
-    ready: PropTypes.bool.isRequired,
-    refresh: CallbackType
-  };
-
-  static contextTypes = {
-    router: PropTypes.object
-  };
-
-  componentWillMount() {
-    this.handleCreateClick = this.handleCreateClick.bind(this);
-  }
-
-  handleCreateClick(e) {
-    e.preventDefault();
-    const { organization } = this.props;
-
-    new CreateView({ organization })
-      .on('done', r => {
-        this.props.refresh().then(() => {
-          const pathname = organization
-            ? `/organizations/${organization.key}/permission_templates`
-            : '/permission_templates';
-          this.context.router.push({
-            pathname,
-            query: { id: r.permissionTemplate.id }
-          });
-        });
-      })
-      .render();
-  }
-
-  render() {
-    return (
-      <header id="project-permissions-header" className="page-header">
-        <h1 className="page-title">{translate('permission_templates.page')}</h1>
-
-        {!this.props.ready && <i className="spinner" />}
-
-        <div className="page-actions">
-          <button onClick={this.handleCreateClick}>{translate('create')}</button>
-        </div>
-
-        <p className="page-description">{translate('permission_templates.page.description')}</p>
-      </header>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/Header.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/Header.tsx
new file mode 100644 (file)
index 0000000..1db8017
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * 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 PropTypes from 'prop-types';
+import Form from './Form';
+import { createPermissionTemplate } from '../../../api/permissions';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  organization?: { key: string };
+  ready?: boolean;
+  refresh: () => Promise<void>;
+}
+
+interface State {
+  createModal: boolean;
+}
+
+export default class Header extends React.PureComponent<Props, State> {
+  mounted = false;
+
+  static contextTypes = {
+    router: PropTypes.object
+  };
+
+  state: State = { createModal: false };
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  handleCreateClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    this.setState({ createModal: true });
+  };
+
+  handleCreateModalClose = () => {
+    if (this.mounted) {
+      this.setState({ createModal: false });
+    }
+  };
+
+  handleCreateModalSubmit = (data: {
+    description: string;
+    name: string;
+    projectKeyPattern: string;
+  }) => {
+    const organization = this.props.organization && this.props.organization.key;
+    return createPermissionTemplate({ ...data, organization }).then(response => {
+      this.props.refresh().then(() => {
+        const pathname = organization
+          ? `/organizations/${organization}/permission_templates`
+          : '/permission_templates';
+        this.context.router.push({ pathname, query: { id: response.permissionTemplate.id } });
+      });
+    });
+  };
+
+  render() {
+    return (
+      <header className="page-header" id="project-permissions-header">
+        <h1 className="page-title">{translate('permission_templates.page')}</h1>
+
+        {!this.props.ready && <i className="spinner" />}
+
+        <div className="page-actions">
+          <button onClick={this.handleCreateClick} type="button">
+            {translate('create')}
+          </button>
+
+          {this.state.createModal && (
+            <Form
+              confirmButtonText={translate('create')}
+              header={translate('permission_template.new_template')}
+              onClose={this.handleCreateModalClose}
+              onSubmit={this.handleCreateModalSubmit}
+            />
+          )}
+        </div>
+
+        <p className="page-description">{translate('permission_templates.page.description')}</p>
+      </header>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-delete.hbs b/server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-delete.hbs
deleted file mode 100644 (file)
index 6cdf78a..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-<form id="delete-permission-template-form">
-  <div class="modal-head">
-    <h2>{{t 'permission_template.delete_confirm_title'}}</h2>
-  </div>
-  <div class="modal-body">
-    <div class="js-modal-messages"></div>
-    {{tp 'permission_template.do_you_want_to_delete_template_xxx' name}}
-  </div>
-  <div class="modal-foot">
-    <button id="delete-permission-template-submit" class="button-red">{{t 'delete'}}</button>
-    <a href="#" class="js-modal-close" id="delete-permission-template-cancel">{{t 'cancel'}}</a>
-  </div>
-</form>
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-form.hbs b/server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-form.hbs
deleted file mode 100644 (file)
index b88dece..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-<form id="permission-template-form" autocomplete="off">
-  <div class="modal-head">
-    <h2>{{#if id}}{{t 'permission_template.edit_template'}}{{else}}{{t 'permission_template.new_template'}}{{/if}}</h2>
-  </div>
-  <div class="modal-body">
-    <div class="js-modal-messages"></div>
-
-    <div class="modal-field">
-      <label for="permission-template-name">{{t 'name'}}<em class="mandatory">*</em></label>
-      <input id="permission-template-name" name="name" type="text" maxlength="256" required value="{{name}}">
-      <div class="modal-field-description">
-        {{t 'should_be_unique'}}
-      </div>
-    </div>
-
-    <div class="modal-field">
-      <label for="permission-template-description">{{t 'description'}}</label>
-      <textarea id="permission-template-description" name="description" maxlength="4000" rows="5">{{description}}</textarea>
-    </div>
-
-    <div class="modal-field">
-      <label for="permission-template-project-key-pattern">{{t 'permission_template.key_pattern'}}</label>
-      <input id="permission-template-project-key-pattern" name="keyPattern" type="text" maxlength="500"
-             value="{{projectKeyPattern}}">
-      <div class="modal-field-description">
-        {{t 'permission_template.key_pattern.description'}}
-      </div>
-    </div>
-  </div>
-  <div class="modal-foot">
-    <button id="permission-template-submit">{{#if id}}{{t 'update_verb'}}{{else}}{{t 'create'}}{{/if}}</button>
-    <a href="#" class="js-modal-close" id="permission-template-cancel">{{t 'cancel'}}</a>
-  </div>
-</form>
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/views/CreateView.js b/server/sonar-web/src/main/js/apps/permission-templates/views/CreateView.js
deleted file mode 100644 (file)
index 0e78892..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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 FormView from './FormView';
-import { createPermissionTemplate } from '../../../api/permissions';
-import { parseError } from '../../../helpers/request';
-
-export default FormView.extend({
-  sendRequest() {
-    this.disableForm();
-    const data = {
-      name: this.$('#permission-template-name').val(),
-      description: this.$('#permission-template-description').val(),
-      projectKeyPattern: this.$('#permission-template-project-key-pattern').val()
-    };
-    if (this.options.organization) {
-      Object.assign(data, { organization: this.options.organization.key });
-    }
-    createPermissionTemplate(data).then(
-      r => {
-        this.trigger('done', r);
-        this.destroy();
-      },
-      e => {
-        this.enableForm();
-        parseError(e).then(message => this.showSingleError(message));
-      }
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/views/DeleteView.js b/server/sonar-web/src/main/js/apps/permission-templates/views/DeleteView.js
deleted file mode 100644 (file)
index ea780dd..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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 ModalForm from '../../../components/common/modal-form';
-import { deletePermissionTemplate } from '../../../api/permissions';
-import Template from '../templates/permission-templates-delete.hbs';
-import { parseError } from '../../../helpers/request';
-
-export default ModalForm.extend({
-  template: Template,
-
-  onFormSubmit() {
-    ModalForm.prototype.onFormSubmit.apply(this, arguments);
-    this.sendRequest();
-  },
-
-  sendRequest() {
-    deletePermissionTemplate({ templateId: this.model.id }).then(
-      () => {
-        this.trigger('done');
-        this.destroy();
-      },
-      e => {
-        this.enableForm();
-        parseError(e).then(message => this.showSingleError(message));
-      }
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/views/FormView.js b/server/sonar-web/src/main/js/apps/permission-templates/views/FormView.js
deleted file mode 100644 (file)
index 0c9e5d4..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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 ModalForm from '../../../components/common/modal-form';
-import Template from '../templates/permission-templates-form.hbs';
-
-export default ModalForm.extend({
-  template: Template,
-
-  onRender() {
-    ModalForm.prototype.onRender.apply(this, arguments);
-    this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' });
-    this.$('#create-custom-measure-metric').select2({
-      width: '250px',
-      minimumResultsForSearch: 20
-    });
-  },
-
-  onDestroy() {
-    ModalForm.prototype.onDestroy.apply(this, arguments);
-    this.$('[data-toggle="tooltip"]').tooltip('destroy');
-  },
-
-  onFormSubmit() {
-    ModalForm.prototype.onFormSubmit.apply(this, arguments);
-    this.sendRequest();
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/views/UpdateView.js b/server/sonar-web/src/main/js/apps/permission-templates/views/UpdateView.js
deleted file mode 100644 (file)
index 5284685..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * 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 FormView from './FormView';
-import { updatePermissionTemplate } from '../../../api/permissions';
-import { parseError } from '../../../helpers/request';
-
-export default FormView.extend({
-  sendRequest() {
-    this.disableForm();
-    updatePermissionTemplate({
-      id: this.model.id,
-      name: this.$('#permission-template-name').val(),
-      description: this.$('#permission-template-description').val(),
-      projectKeyPattern: this.$('#permission-template-project-key-pattern').val()
-    }).then(
-      () => {
-        this.options.refresh();
-        this.destroy();
-      },
-      e => {
-        this.enableForm();
-        parseError(e).then(message => this.showSingleError(message));
-      }
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx
new file mode 100644 (file)
index 0000000..e8550ce
--- /dev/null
@@ -0,0 +1,155 @@
+/*
+ * 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 { getPermissionTemplates, applyTemplateToProject } from '../../../../api/permissions';
+import { PermissionTemplate } from '../../../../app/types';
+import DeferredSpinner from '../../../../components/common/DeferredSpinner';
+import SimpleModal from '../../../../components/controls/SimpleModal';
+import Select from '../../../../components/controls/Select';
+import { translateWithParameters, translate } from '../../../../helpers/l10n';
+
+interface Props {
+  onApply?: () => void;
+  onClose: () => void;
+  organization: string | undefined;
+  project: { key: string; name: string };
+}
+
+interface State {
+  done: boolean;
+  loading: boolean;
+  permissionTemplate?: string;
+  permissionTemplates?: PermissionTemplate[];
+}
+
+export default class ApplyTemplate extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = { done: false, loading: true };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchPermissionTemplates();
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchPermissionTemplates = () => {
+    getPermissionTemplates(this.props.organization).then(
+      ({ permissionTemplates }) => {
+        if (this.mounted) {
+          this.setState({ loading: false, permissionTemplates });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  handleSubmit = () => {
+    if (this.state.permissionTemplate) {
+      return applyTemplateToProject({
+        organization: this.props.organization,
+        projectKey: this.props.project.key,
+        templateId: this.state.permissionTemplate
+      }).then(() => {
+        if (this.mounted) {
+          if (this.props.onApply) {
+            this.props.onApply();
+          }
+          this.setState({ done: true });
+        }
+      });
+    } else {
+      return Promise.reject(undefined);
+    }
+  };
+
+  handlePermissionTemplateChange = ({ value }: { value: string }) => {
+    this.setState({ permissionTemplate: value });
+  };
+
+  render() {
+    const header = translateWithParameters(
+      'projects_role.apply_template_to_xxx',
+      this.props.project.name
+    );
+
+    return (
+      <SimpleModal header={header} onClose={this.props.onClose} onSubmit={this.handleSubmit}>
+        {({ onCloseClick, onFormSubmit, submitting }) => (
+          <form id="project-permissions-apply-template-form" onSubmit={onFormSubmit}>
+            <header className="modal-head">
+              <h2>{header}</h2>
+            </header>
+
+            <div className="modal-body">
+              {this.state.done ? (
+                <div className="alert alert-success">
+                  {translate('projects_role.apply_template.success')}
+                </div>
+              ) : (
+                <>
+                  {this.state.loading ? (
+                    <i className="spinner" />
+                  ) : (
+                    <div className="modal-field">
+                      <label htmlFor="project-permissions-template">
+                        {translate('template')}
+                        <em className="mandatory">*</em>
+                      </label>
+                      {this.state.permissionTemplates && (
+                        <Select
+                          clearable={false}
+                          onChange={this.handlePermissionTemplateChange}
+                          options={this.state.permissionTemplates.map(permissionTemplate => ({
+                            label: permissionTemplate.name,
+                            value: permissionTemplate.id
+                          }))}
+                          value={this.state.permissionTemplate}
+                        />
+                      )}
+                    </div>
+                  )}
+                </>
+              )}
+            </div>
+
+            <footer className="modal-foot">
+              <DeferredSpinner className="spacer-right" loading={submitting} />
+              {!this.state.done && (
+                <button disabled={submitting || !this.state.permissionTemplate} type="submit">
+                  {translate('apply')}
+                </button>
+              )}
+              <button className="button-link" onClick={onCloseClick} type="reset">
+                {translate(this.state.done ? 'close' : 'cancel')}
+              </button>
+            </footer>
+          </form>
+        )}
+      </SimpleModal>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js
deleted file mode 100644 (file)
index 02eed6c..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * 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.
- */
-// @flow
-import React from 'react';
-import { translate } from '../../../../helpers/l10n';
-import ApplyTemplateView from '../views/ApplyTemplateView';
-
-/*::
-type Props = {|
-  component: {
-    configuration?: {
-      canApplyPermissionTemplate: boolean,
-      canUpdateProjectVisibilityToPrivate: boolean
-    },
-    key: string,
-    qualifier: string,
-    visibility: string
-  },
-  loadHolders: () => void,
-  loading: boolean
-|};
-*/
-
-export default class PageHeader extends React.PureComponent {
-  /*:: props: Props; */
-
-  handleApplyTemplate = (e /*: Event & { target: HTMLButtonElement } */) => {
-    e.preventDefault();
-    e.target.blur();
-    const { component, loadHolders } = this.props;
-    const organization = component.organization ? { key: component.organization } : null;
-    new ApplyTemplateView({ project: component, organization })
-      .on('done', () => loadHolders())
-      .render();
-  };
-
-  render() {
-    const { component } = this.props;
-    const configuration = component.configuration;
-    const canApplyPermissionTemplate =
-      configuration != null && configuration.canApplyPermissionTemplate;
-
-    const description = ['VW', 'SVW', 'APP'].includes(component.qualifier)
-      ? translate('roles.page.description_portfolio')
-      : translate('roles.page.description2');
-
-    const visibilityDescription =
-      component.qualifier === 'TRK'
-        ? translate('visibility', component.visibility, 'description')
-        : null;
-
-    return (
-      <header className="page-header">
-        <h1 className="page-title">{translate('permissions.page')}</h1>
-
-        {this.props.loading && <i className="spinner" />}
-
-        {canApplyPermissionTemplate && (
-          <div className="page-actions">
-            <button className="js-apply-template" onClick={this.handleApplyTemplate}>
-              {translate('projects_role.apply_template')}
-            </button>
-          </div>
-        )}
-
-        <div className="page-description">
-          <p>{description}</p>
-          {visibilityDescription != null && <p>{visibilityDescription}</p>}
-        </div>
-      </header>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx
new file mode 100644 (file)
index 0000000..bab1ba6
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * 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 ApplyTemplate from './ApplyTemplate';
+import { Component } from '../../../../app/types';
+import { translate } from '../../../../helpers/l10n';
+
+interface Props {
+  component: Component;
+  loadHolders: () => void;
+  loading: boolean;
+}
+
+interface State {
+  applyTemplateModal: boolean;
+}
+
+export default class PageHeader extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = { applyTemplateModal: false };
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  handleApplyTemplate = (event: React.SyntheticEvent<HTMLButtonElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    this.setState({ applyTemplateModal: true });
+  };
+
+  handleApplyTemplateClose = () => {
+    if (this.mounted) {
+      this.setState({ applyTemplateModal: false });
+    }
+  };
+
+  render() {
+    const { component } = this.props;
+    const { configuration } = component;
+    const canApplyPermissionTemplate =
+      configuration != null && configuration.canApplyPermissionTemplate;
+
+    const description = ['VW', 'SVW', 'APP'].includes(component.qualifier)
+      ? translate('roles.page.description_portfolio')
+      : translate('roles.page.description2');
+
+    const visibilityDescription =
+      component.qualifier === 'TRK' && component.visibility
+        ? translate('visibility', component.visibility, 'description')
+        : null;
+
+    return (
+      <header className="page-header">
+        <h1 className="page-title">{translate('permissions.page')}</h1>
+
+        {this.props.loading && <i className="spinner" />}
+
+        {canApplyPermissionTemplate && (
+          <div className="page-actions">
+            <button className="js-apply-template" onClick={this.handleApplyTemplate} type="button">
+              {translate('projects_role.apply_template')}
+            </button>
+
+            {this.state.applyTemplateModal && (
+              <ApplyTemplate
+                onApply={this.props.loadHolders}
+                onClose={this.handleApplyTemplateClose}
+                organization={component.organization}
+                project={component}
+              />
+            )}
+          </div>
+        )}
+
+        <div className="page-description">
+          <p>{description}</p>
+          {visibilityDescription != null && <p>{visibilityDescription}</p>}
+        </div>
+      </header>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/templates/ApplyTemplateTemplate.hbs b/server/sonar-web/src/main/js/apps/permissions/project/templates/ApplyTemplateTemplate.hbs
deleted file mode 100644 (file)
index 37e8ff3..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-<form id="project-permissions-apply-template-form" autocomplete="off">
-  <div class="modal-head">
-    <h2>{{tp 'projects_role.apply_template_to_xxx' project.name}}</h2>
-  </div>
-
-  <div class="modal-body">
-    <div class="js-modal-messages"></div>
-
-    {{#if done}}
-      <div class="alert alert-success">
-        {{t 'projects_role.apply_template.success'}}
-      </div>
-    {{/if}}
-
-    {{#unless done}}
-      {{#notNull permissionTemplates}}
-        <div class="modal-field">
-          <label for="project-permissions-template">
-            {{t 'template'}}<em class="mandatory">*</em>
-          </label>
-          <select id="project-permissions-template">
-            {{#each permissionTemplates}}
-              <option value="{{id}}">{{name}}</option>
-            {{/each}}
-          </select>
-        </div>
-      {{else}}
-        <i class="spinner"></i>
-      {{/notNull}}
-    {{/unless}}
-  </div>
-
-  <div class="modal-foot">
-    {{#unless done}}
-      {{#notNull permissionTemplates}}
-        <button id="project-permissions-apply-template">{{t 'apply'}}</button>
-      {{/notNull}}
-    {{/unless}}
-    <a href="#" class="js-modal-close">{{t 'close'}}</a>
-  </div>
-</form>
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/views/ApplyTemplateView.js b/server/sonar-web/src/main/js/apps/permissions/project/views/ApplyTemplateView.js
deleted file mode 100644 (file)
index 6bba0c0..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * 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 ModalForm from '../../../../components/common/modal-form';
-import { applyTemplateToProject, getPermissionTemplates } from '../../../../api/permissions';
-import Template from '../templates/ApplyTemplateTemplate.hbs';
-
-export default ModalForm.extend({
-  template: Template,
-
-  initialize() {
-    this.loadPermissionTemplates();
-    this.done = false;
-  },
-
-  loadPermissionTemplates() {
-    return getPermissionTemplates(this.options.organization.key).then(r => {
-      this.permissionTemplates = r.permissionTemplates;
-      this.render();
-    });
-  },
-
-  onRender() {
-    ModalForm.prototype.onRender.apply(this, arguments);
-    this.$('#project-permissions-template').select2({
-      width: '250px',
-      minimumResultsForSearch: 20
-    });
-  },
-
-  onFormSubmit() {
-    ModalForm.prototype.onFormSubmit.apply(this, arguments);
-    const permissionTemplate = this.$('#project-permissions-template').val();
-    this.disableForm();
-
-    const data = {
-      organization: this.options.organization.key,
-      projectKey: this.options.project.key,
-      templateId: permissionTemplate
-    };
-    applyTemplateToProject(data)
-      .then(() => {
-        this.trigger('done');
-        this.done = true;
-        this.render();
-      })
-      .catch(function(e) {
-        e.response.json().then(r => {
-          this.showErrors(r.errors, r.warnings);
-          this.enableForm();
-        });
-      });
-  },
-
-  serializeData() {
-    return {
-      permissionTemplates: this.permissionTemplates,
-      project: this.options.project,
-      done: this.done
-    };
-  }
-});
index 726b4f697092fe0c768c349401f7cc213fe7126c..69bb55161c70d398a9f3005c8134c8478b8466b9 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import {
-  getPermissionTemplates,
-  bulkApplyTemplate,
-  PermissionTemplate
-} from '../../api/permissions';
+import { getPermissionTemplates, bulkApplyTemplate } from '../../api/permissions';
+import { PermissionTemplate } from '../../app/types';
 import { translate, translateWithParameters } from '../../helpers/l10n';
 import AlertWarnIcon from '../../components/icons-components/AlertWarnIcon';
 import Modal from '../../components/controls/Modal';
index cea505f986d0d1af83a9249afa2f61515bbea05f..72e658c8727ee090448c202b14c056c04dd7f928 100644 (file)
@@ -29,8 +29,8 @@ import DateTooltipFormatter from '../../components/intl/DateTooltipFormatter';
 
 interface Props {
   currentUser: { login: string };
-  onApplyTemplate: (project: Project) => void;
   onProjectCheck: (project: Project, checked: boolean) => void;
+  organization: string | undefined;
   project: Project;
   selected: boolean;
 }
@@ -51,8 +51,8 @@ export default class ProjectRow extends React.PureComponent<Props> {
 
         <td className="nowrap">
           <Link
-            to={{ pathname: '/dashboard', query: { id: project.key } }}
-            className="link-with-icon">
+            className="link-with-icon"
+            to={{ pathname: '/dashboard', query: { id: project.key } }}>
             <QualifierIcon qualifier={project.qualifier} /> <span>{project.name}</span>
           </Link>
         </td>
@@ -78,7 +78,7 @@ export default class ProjectRow extends React.PureComponent<Props> {
         <td className="thin nowrap">
           <ProjectRowActions
             currentUser={this.props.currentUser}
-            onApplyTemplate={this.props.onApplyTemplate}
+            organization={this.props.organization}
             project={project}
           />
         </td>
index 7f188592892c907700f5a699535e6e7b10e749ba..b43fd2d96fa6b89f33b22932063dfa4fb533cb23 100644 (file)
@@ -20,6 +20,7 @@
 import * as React from 'react';
 import RestoreAccessModal from './RestoreAccessModal';
 import { Project } from './utils';
+import ApplyTemplate from '../permissions/project/components/ApplyTemplate';
 import { getComponentShow } from '../../api/components';
 import { getComponentNavigation } from '../../api/nav';
 import ActionsDropdown, { ActionsDropdownItem } from '../../components/controls/ActionsDropdown';
@@ -28,11 +29,12 @@ import { getComponentPermissionsUrl } from '../../helpers/urls';
 
 export interface Props {
   currentUser: { login: string };
-  onApplyTemplate: (project: Project) => void;
+  organization: string | undefined;
   project: Project;
 }
 
 interface State {
+  applyTemplateModal: boolean;
   hasAccess?: boolean;
   loading: boolean;
   restoreAccessModal: boolean;
@@ -40,7 +42,7 @@ interface State {
 
 export default class ProjectRowActions extends React.PureComponent<Props, State> {
   mounted = false;
-  state: State = { loading: false, restoreAccessModal: false };
+  state: State = { applyTemplateModal: false, loading: false, restoreAccessModal: false };
 
   componentDidMount() {
     this.mounted = true;
@@ -81,7 +83,13 @@ export default class ProjectRowActions extends React.PureComponent<Props, State>
   };
 
   handleApplyTemplateClick = () => {
-    this.props.onApplyTemplate(this.props.project);
+    this.setState({ applyTemplateModal: true });
+  };
+
+  handleApplyTemplateClose = () => {
+    if (this.mounted) {
+      this.setState({ applyTemplateModal: false });
+    }
   };
 
   handleRestoreAccessClick = () => {
@@ -125,6 +133,14 @@ export default class ProjectRowActions extends React.PureComponent<Props, State>
             project={this.props.project}
           />
         )}
+
+        {this.state.applyTemplateModal && (
+          <ApplyTemplate
+            onClose={this.handleApplyTemplateClose}
+            organization={this.props.organization}
+            project={this.props.project}
+          />
+        )}
       </ActionsDropdown>
     );
   }
index b0872d7532bde4c6b4c4506b423613ece03a37d8..7e72fb9b057d236f2f96ec742995dc7bb474d8a3 100644 (file)
@@ -21,7 +21,6 @@ import * as React from 'react';
 import * as classNames from 'classnames';
 import ProjectRow from './ProjectRow';
 import { Project } from './utils';
-import ApplyTemplateView from '../permissions/project/views/ApplyTemplateView';
 import { Organization } from '../../app/types';
 import { translate } from '../../helpers/l10n';
 
@@ -44,10 +43,6 @@ export default class Projects extends React.PureComponent<Props> {
     }
   };
 
-  handleApplyTemplate = (project: Project) => {
-    new ApplyTemplateView({ project, organization: this.props.organization }).render();
-  };
-
   render() {
     return (
       <div className="boxed-group boxed-group-inner">
@@ -69,8 +64,8 @@ export default class Projects extends React.PureComponent<Props> {
               <ProjectRow
                 currentUser={this.props.currentUser}
                 key={project.key}
-                onApplyTemplate={this.handleApplyTemplate}
                 onProjectCheck={this.onProjectCheck}
+                organization={this.props.organization && this.props.organization.key}
                 project={project}
                 selected={this.props.selection.includes(project.key)}
               />
index f9a5eee840673039da2fc2ae6d049917ca0066bc..4c9e74dc1b12da3612671197e87950ddff7272dd 100644 (file)
@@ -54,17 +54,16 @@ it('restores access', async () => {
 });
 
 it('applies permission template', () => {
-  const onApplyTemplate = jest.fn();
-  const wrapper = shallowRender({ onApplyTemplate });
+  const wrapper = shallowRender();
   click(wrapper.find('.js-apply-template'));
-  expect(onApplyTemplate).toBeCalledWith(project);
+  expect(wrapper.find('ApplyTemplate')).toMatchSnapshot();
 });
 
 function shallowRender(props: Partial<Props> = {}) {
   const wrapper = shallow(
     <ProjectRowActions
       currentUser={{ login: 'admin' }}
-      onApplyTemplate={jest.fn()}
+      organization="org"
       project={project}
       {...props}
     />
index 76c6be4271303c930c23f62134b4033693e51315..25b2a6ac78dce78a6291dbdd3e64c26eff6b3fe8 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-/* eslint-disable import/first */
-jest.mock('../../permissions/project/views/ApplyTemplateView');
-
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import Projects from '../Projects';
-import ApplyTemplateView from '../../permissions/project/views/ApplyTemplateView';
 import { Visibility } from '../../../app/types';
 
 const organization = { key: 'org', name: 'org', projectVisibility: 'public' };
@@ -55,15 +51,6 @@ it('selects and deselects project', () => {
   expect(onProjectDeselected).toBeCalledWith('a');
 });
 
-it('opens modal to apply permission template', () => {
-  const wrapper = shallowRender({ projects });
-  wrapper
-    .find('ProjectRow')
-    .first()
-    .prop<Function>('onApplyTemplate')(projects[0]);
-  expect(ApplyTemplateView).toBeCalledWith({ organization, project: projects[0] });
-});
-
 function shallowRender(props?: any) {
   return shallow(
     <Projects
index 7c62905a04cf8a1cb0b0370d209fbb972ec40a15..6d05b0406f7dc788720fa74966e1d02141aa20f3 100644 (file)
@@ -70,7 +70,6 @@ exports[`renders 1`] = `
           "login": "foo",
         }
       }
-      onApplyTemplate={[MockFunction]}
       project={
         Object {
           "key": "project",
@@ -152,7 +151,6 @@ exports[`renders 2`] = `
           "login": "foo",
         }
       }
-      onApplyTemplate={[MockFunction]}
       project={
         Object {
           "key": "project",
index d2737d581940765210cfd29abbdf5f65182ea609..b824855ce963672407ecea81cb9a23b18ef414a5 100644 (file)
@@ -1,5 +1,22 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`applies permission template 1`] = `
+<ApplyTemplate
+  onClose={[Function]}
+  organization="org"
+  project={
+    Object {
+      "id": "",
+      "key": "project",
+      "name": "Project",
+      "organization": "org",
+      "qualifier": "TRK",
+      "visibility": "private",
+    }
+  }
+/>
+`;
+
 exports[`restores access 1`] = `
 <ActionsDropdown
   onToggleClick={[Function]}
index 1503fed2c4e9a04c90ccf2d2931cfa4db29fbc64..678979cf56afb8571d4bc25e1fa315aa9a5b1ebd 100644 (file)
@@ -34,8 +34,8 @@ exports[`renders list of projects 1`] = `
           }
         }
         key="a"
-        onApplyTemplate={[Function]}
         onProjectCheck={[Function]}
+        organization="org"
         project={
           Object {
             "key": "a",
@@ -53,8 +53,8 @@ exports[`renders list of projects 1`] = `
           }
         }
         key="b"
-        onApplyTemplate={[Function]}
         onProjectCheck={[Function]}
+        organization="org"
         project={
           Object {
             "key": "b",