]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20925 Migrating project's links page to new UI
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>
Wed, 1 Nov 2023 07:16:34 +0000 (08:16 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 2 Nov 2023 20:02:41 +0000 (20:02 +0000)
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/apps/projectLinks/CreationModal.tsx
server/sonar-web/src/main/js/apps/projectLinks/Header.tsx
server/sonar-web/src/main/js/apps/projectLinks/LinkRow.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectLinks/ProjectLinkRow.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectLinks/ProjectLinkTable.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectLinks/ProjectLinksApp.tsx
server/sonar-web/src/main/js/apps/projectLinks/Table.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectLinks/__tests__/ProjectLinksApp-it.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index dccd65c13045c88b8fec02c22f48bb669b99f3e2..f8c8b6c0ace6a6a5f836768ad3956298bdcdbb6a 100644 (file)
@@ -61,6 +61,7 @@ const TEMP_PAGELIST_WITH_NEW_BACKGROUND_WHITE = [
   '/project/branches',
   '/project/key',
   '/project/deletion',
+  '/project/links',
 ];
 
 export default function GlobalContainer() {
index 30823e150ca74f16fe3b614490b0f30044e42725..830dfb8ed81b26e1a70343fb993a1a8f1aeeae8f 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.
  */
+import { ButtonPrimary, FormField, InputField, Modal } from 'design-system';
 import * as React from 'react';
-import SimpleModal from '../../components/controls/SimpleModal';
-import { ResetButtonLink, SubmitButton } from '../../components/controls/buttons';
-import MandatoryFieldMarker from '../../components/ui/MandatoryFieldMarker';
 import MandatoryFieldsExplanation from '../../components/ui/MandatoryFieldsExplanation';
-import Spinner from '../../components/ui/Spinner';
 import { translate } from '../../helpers/l10n';
 
 interface Props {
@@ -35,10 +32,13 @@ interface State {
   url: string;
 }
 
+const FORM_ID = 'create-link-form';
+
 export default class CreationModal extends React.PureComponent<Props, State> {
   state: State = { name: '', url: '' };
 
-  handleSubmit = () => {
+  handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+    event.preventDefault();
     return this.props.onSubmit(this.state.name, this.state.url).then(this.props.onClose);
   };
 
@@ -53,68 +53,55 @@ export default class CreationModal extends React.PureComponent<Props, State> {
   render() {
     const header = translate('project_links.create_new_project_link');
 
-    return (
-      <SimpleModal
-        header={header}
-        onClose={this.props.onClose}
-        onSubmit={this.handleSubmit}
-        size="small"
-      >
-        {({ onCloseClick, onFormSubmit, submitting }) => (
-          <form onSubmit={onFormSubmit}>
-            <header className="modal-head">
-              <h2>{header}</h2>
-            </header>
-
-            <div className="modal-body">
-              <MandatoryFieldsExplanation className="modal-field" />
+    const formBody = (
+      <form id={FORM_ID} onSubmit={this.handleSubmit} className="sw-mb-2">
+        <MandatoryFieldsExplanation />
 
-              <div className="modal-field">
-                <label htmlFor="create-link-name">
-                  {translate('project_links.name')}
-                  <MandatoryFieldMarker />
-                </label>
-                <input
-                  autoFocus
-                  id="create-link-name"
-                  maxLength={128}
-                  name="name"
-                  onChange={this.handleNameChange}
-                  required
-                  type="text"
-                  value={this.state.name}
-                />
-              </div>
+        <FormField
+          label={translate('project_links.name')}
+          htmlFor="create-link-name"
+          className="sw-mt-4"
+          required
+        >
+          <InputField
+            autoFocus
+            required
+            size="auto"
+            id="create-link-name"
+            maxLength={128}
+            name="name"
+            onChange={this.handleNameChange}
+            type="text"
+            value={this.state.name}
+          />
+        </FormField>
 
-              <div className="modal-field">
-                <label htmlFor="create-link-url">
-                  {translate('project_links.url')}
-                  <MandatoryFieldMarker />
-                </label>
-                <input
-                  id="create-link-url"
-                  maxLength={128}
-                  name="url"
-                  onChange={this.handleUrlChange}
-                  required
-                  type="text"
-                  value={this.state.url}
-                />
-              </div>
-            </div>
+        <FormField label={translate('project_links.url')} htmlFor="create-link-url" required>
+          <InputField
+            size="auto"
+            required
+            id="create-link-url"
+            maxLength={128}
+            name="url"
+            onChange={this.handleUrlChange}
+            type="text"
+            value={this.state.url}
+          />
+        </FormField>
+      </form>
+    );
 
-            <footer className="modal-foot">
-              <Spinner className="spacer-right" loading={submitting} />
-              <SubmitButton disabled={submitting} id="create-link-confirm">
-                {translate('create')}
-              </SubmitButton>
-              <ResetButtonLink disabled={submitting} onClick={onCloseClick}>
-                {translate('cancel')}
-              </ResetButtonLink>
-            </footer>
-          </form>
-        )}
-      </SimpleModal>
+    return (
+      <Modal
+        headerTitle={header}
+        onClose={this.props.onClose}
+        body={formBody}
+        primaryButton={
+          <ButtonPrimary form={FORM_ID} type="submit">
+            {translate('create')}
+          </ButtonPrimary>
+        }
+      />
     );
   }
 }
index 141ed03726d8fac7ea267c6e3f9005b202d17379..a8729cf784fcf1804c1867d51c1e8eec7310021f 100644 (file)
@@ -17,8 +17,9 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+
+import { ButtonPrimary, Title } from 'design-system';
 import * as React from 'react';
-import { Button } from '../../components/controls/buttons';
 import { translate } from '../../helpers/l10n';
 import CreationModal from './CreationModal';
 
@@ -55,14 +56,14 @@ export default class Header extends React.PureComponent<Props, State> {
   render() {
     return (
       <>
-        <header className="page-header">
-          <h1 className="page-title">{translate('project_links.page')}</h1>
-          <div className="page-actions">
-            <Button id="create-project-link" onClick={this.handleCreateClick}>
+        <header className="sw-mt-8 sw-mb-4">
+          <div className="sw-flex sw-justify-between">
+            <Title className="sw-mb-4">{translate('project_links.page')}</Title>
+            <ButtonPrimary id="create-project-link" onClick={this.handleCreateClick}>
               {translate('create')}
-            </Button>
+            </ButtonPrimary>
           </div>
-          <div className="page-description">{translate('project_links.page.description')}</div>
+          <p>{translate('project_links.page.description')}</p>
         </header>
         {this.state.creationModal && (
           <CreationModal onClose={this.handleCreationModalClose} onSubmit={this.props.onCreate} />
diff --git a/server/sonar-web/src/main/js/apps/projectLinks/LinkRow.tsx b/server/sonar-web/src/main/js/apps/projectLinks/LinkRow.tsx
deleted file mode 100644 (file)
index 056d03b..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 isValidUri from '../../app/utils/isValidUri';
-import Link from '../../components/common/Link';
-import { Button } from '../../components/controls/buttons';
-import ConfirmButton from '../../components/controls/ConfirmButton';
-import ProjectLinkIcon from '../../components/icons/ProjectLinkIcon';
-import { translate, translateWithParameters } from '../../helpers/l10n';
-import { getLinkName, isProvided } from '../../helpers/projectLinks';
-import { ProjectLink } from '../../types/types';
-
-interface Props {
-  link: ProjectLink;
-  onDelete: (linkId: string) => Promise<void>;
-}
-
-export default class LinkRow extends React.PureComponent<Props> {
-  renderNameForProvided = (link: ProjectLink) => {
-    return (
-      <div className="display-inline-block text-top">
-        <div>
-          <span className="js-name">{getLinkName(link)}</span>
-        </div>
-        <div className="note little-spacer-top">
-          <span className="js-type">{`sonar.links.${link.type}`}</span>
-        </div>
-      </div>
-    );
-  };
-
-  renderName = (link: ProjectLink) => {
-    return (
-      <div>
-        <ProjectLinkIcon className="little-spacer-right" type={link.type} />
-        {isProvided(link) ? (
-          this.renderNameForProvided(link)
-        ) : (
-          <div className="display-inline-block text-top">
-            <span className="js-name">{link.name}</span>
-          </div>
-        )}
-      </div>
-    );
-  };
-
-  renderDeleteButton = (link: ProjectLink) => {
-    if (isProvided(link)) {
-      return null;
-    }
-
-    return (
-      <ConfirmButton
-        confirmButtonText={translate('delete')}
-        confirmData={link.id}
-        isDestructive
-        modalBody={translateWithParameters(
-          'project_links.are_you_sure_to_delete_x_link',
-          link.name!,
-        )}
-        modalHeader={translate('project_links.delete_project_link')}
-        onConfirm={this.props.onDelete}
-      >
-        {({ onClick }) => (
-          <Button
-            className="button-red js-delete-button"
-            aria-label={translateWithParameters('project_links.delete_x_link', link.name ?? '')}
-            onClick={onClick}
-          >
-            {translate('delete')}
-          </Button>
-        )}
-      </ConfirmButton>
-    );
-  };
-
-  render() {
-    const { link } = this.props;
-
-    return (
-      <tr data-name={link.name}>
-        <td className="nowrap">{this.renderName(link)}</td>
-        <td className="nowrap js-url">
-          {isValidUri(link.url) ? (
-            <Link to={link.url} target="_blank">
-              {link.url}
-            </Link>
-          ) : (
-            link.url
-          )}
-        </td>
-        <td className="thin nowrap">{this.renderDeleteButton(link)}</td>
-      </tr>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projectLinks/ProjectLinkRow.tsx b/server/sonar-web/src/main/js/apps/projectLinks/ProjectLinkRow.tsx
new file mode 100644 (file)
index 0000000..f676e1f
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 {
+  ActionCell,
+  ContentCell,
+  DestructiveIcon,
+  Link,
+  Note,
+  TableRow,
+  TrashIcon,
+} from 'design-system';
+import * as React from 'react';
+import isValidUri from '../../app/utils/isValidUri';
+import ConfirmButton from '../../components/controls/ConfirmButton';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import { getLinkName, isProvided } from '../../helpers/projectLinks';
+import { ProjectLink } from '../../types/types';
+
+interface Props {
+  link: ProjectLink;
+  onDelete: (linkId: string) => Promise<void>;
+}
+
+export default class LinkRow extends React.PureComponent<Props> {
+  renderNameForProvided = (link: ProjectLink) => {
+    return (
+      <div>
+        <div>
+          <span>{getLinkName(link)}</span>
+        </div>
+        <Note className="sw-mt-1">
+          <span>{`sonar.links.${link.type}`}</span>
+        </Note>
+      </div>
+    );
+  };
+
+  renderDeleteButton = (link: ProjectLink) => {
+    if (isProvided(link)) {
+      return null;
+    }
+
+    return (
+      <ConfirmButton
+        confirmButtonText={translate('delete')}
+        confirmData={link.id}
+        isDestructive
+        modalBody={translateWithParameters(
+          'project_links.are_you_sure_to_delete_x_link',
+          link.name!,
+        )}
+        modalHeader={translate('project_links.delete_project_link')}
+        onConfirm={this.props.onDelete}
+      >
+        {({ onClick }) => (
+          <DestructiveIcon
+            Icon={TrashIcon}
+            aria-label={translateWithParameters('project_links.delete_x_link', link.name ?? '')}
+            onClick={onClick}
+            size="small"
+          />
+        )}
+      </ConfirmButton>
+    );
+  };
+
+  render() {
+    const { link } = this.props;
+
+    return (
+      <TableRow data-name={link.name}>
+        <ContentCell>
+          {isProvided(link) ? (
+            this.renderNameForProvided(link)
+          ) : (
+            <div>
+              <span>{link.name}</span>
+            </div>
+          )}
+        </ContentCell>
+        <ContentCell>
+          {isValidUri(link.url) ? (
+            <Link to={link.url} target="_blank">
+              {link.url}
+            </Link>
+          ) : (
+            link.url
+          )}
+        </ContentCell>
+        <ActionCell>{this.renderDeleteButton(link)}</ActionCell>
+      </TableRow>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectLinks/ProjectLinkTable.tsx b/server/sonar-web/src/main/js/apps/projectLinks/ProjectLinkTable.tsx
new file mode 100644 (file)
index 0000000..9974c6c
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { ActionCell, ContentCell, Note, Table, TableRow } from 'design-system';
+import * as React from 'react';
+import { translate } from '../../helpers/l10n';
+import { orderLinks } from '../../helpers/projectLinks';
+import { ProjectLink } from '../../types/types';
+import LinkRow from './ProjectLinkRow';
+
+interface Props {
+  links: ProjectLink[];
+  onDelete: (linkId: string) => Promise<void>;
+}
+
+export default function ProjectLinkTable({ links, onDelete }: Readonly<Props>) {
+  if (!links.length) {
+    return <Note>{translate('project_links.no_results')}</Note>;
+  }
+
+  const orderedLinks = orderLinks(links);
+
+  const linkRows = orderedLinks.map((link) => (
+    <LinkRow key={link.id} link={link} onDelete={onDelete} />
+  ));
+
+  const header = (
+    <TableRow>
+      <ContentCell>{translate('project_links.name')}</ContentCell>
+      <ContentCell>{translate('project_links.url')}</ContentCell>
+      <ActionCell>&nbsp;</ActionCell>
+    </TableRow>
+  );
+
+  return (
+    <Table columnCount={3} header={header}>
+      {linkRows}
+    </Table>
+  );
+}
index 9ab0eddd0ede34abec2556713bfe7c115a4db2e6..5165a7b37bb0c1a6acb87a46ac60cbb0ed526e95 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.
  */
+
+import { LargeCenteredLayout, PageContentFontWrapper, Spinner } from 'design-system';
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
 import { createLink, deleteLink, getProjectLinks } from '../../api/projectLinks';
 import withComponentContext from '../../app/components/componentContext/withComponentContext';
-import Spinner from '../../components/ui/Spinner';
 import { translate } from '../../helpers/l10n';
 import { Component, ProjectLink } from '../../types/types';
 import Header from './Header';
-import Table from './Table';
+import ProjectLinkTable from './ProjectLinkTable';
 
 interface Props {
   component: Component;
@@ -102,13 +103,15 @@ export class ProjectLinksApp extends React.PureComponent<Props, State> {
   render() {
     const { loading, links } = this.state;
     return (
-      <div className="page page-limited">
-        <Helmet defer={false} title={translate('project_links.page')} />
-        <Header onCreate={this.handleCreateLink} />
-        <Spinner loading={loading}>
-          {links && <Table links={links} onDelete={this.handleDeleteLink} />}
-        </Spinner>
-      </div>
+      <LargeCenteredLayout>
+        <PageContentFontWrapper className="sw-my-8 sw-body-sm">
+          <Helmet defer={false} title={translate('project_links.page')} />
+          <Header onCreate={this.handleCreateLink} />
+          <Spinner loading={loading}>
+            <ProjectLinkTable links={links ?? []} onDelete={this.handleDeleteLink} />
+          </Spinner>
+        </PageContentFontWrapper>
+      </LargeCenteredLayout>
     );
   }
 }
diff --git a/server/sonar-web/src/main/js/apps/projectLinks/Table.tsx b/server/sonar-web/src/main/js/apps/projectLinks/Table.tsx
deleted file mode 100644 (file)
index 32807d0..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { translate } from '../../helpers/l10n';
-import { orderLinks } from '../../helpers/projectLinks';
-import { ProjectLink } from '../../types/types';
-import LinkRow from './LinkRow';
-
-interface Props {
-  links: ProjectLink[];
-  onDelete: (linkId: string) => Promise<void>;
-}
-
-export default class Table extends React.PureComponent<Props> {
-  renderHeader() {
-    // keep empty cell for actions
-    return (
-      <thead>
-        <tr>
-          <th className="nowrap">{translate('project_links.name')}</th>
-          <th className="nowrap width-100">{translate('project_links.url')}</th>
-          <th className="thin">&nbsp;</th>
-        </tr>
-      </thead>
-    );
-  }
-
-  render() {
-    if (!this.props.links.length) {
-      return <div className="note">{translate('no_results')}</div>;
-    }
-
-    const orderedLinks = orderLinks(this.props.links);
-
-    const linkRows = orderedLinks.map((link) => (
-      <LinkRow key={link.id} link={link} onDelete={this.props.onDelete} />
-    ));
-
-    return (
-      <div className="boxed-group boxed-group-inner">
-        <table className="data zebra" id="project-links">
-          {this.renderHeader()}
-          <tbody>{linkRows}</tbody>
-        </table>
-      </div>
-    );
-  }
-}
index 932f16b3b7b1032c281a4192e66126a73c5a3f0c..77051abf2f0b740baaa2ba4bd08d16e79bfe3160 100644 (file)
@@ -72,7 +72,7 @@ function getPageObjects() {
 
   const ui = {
     pageTitle: byRole('heading', { name: 'project_links.page' }),
-    noResultsTable: byText('no_results'),
+    noResultsTable: byText('project_links.no_results'),
     createLinkButton: byRole('button', { name: 'create' }),
     nameInput: byRole('textbox', { name: /project_links.name/ }),
     urlInput: byRole('textbox', { name: /project_links.url/ }),
index fe1d55674955cfc11715dfd69133907b9d597ffd..1568ee6a866e7b9add2612c2db066201a305b5ba 100644 (file)
@@ -510,6 +510,7 @@ project_links.are_you_sure_to_delete_x_link=Are you sure you want to delete the
 project_links.delete_x_link=Delete "{0}" link
 project_links.name=Name
 project_links.url=URL
+project_links.no_results=No links yet. Click "Create" to add one.
 
 
 #------------------------------------------------------------------------------